diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..ccff05290a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Eclipse project +**/.classpath +**/.project + +# IntelliJ project +**/.idea +**/*.iml +**/*.ipr diff --git a/OWNERS b/OWNERS index 48e54da82c..22b5561933 100644 --- a/OWNERS +++ b/OWNERS @@ -1,3 +1,7 @@ -set noparent - -include platform/frameworks/base:/services/core/java/com/android/server/net/OWNERS +codewiz@google.com +jchalard@google.com +junyulai@google.com +lorenzo@google.com +maze@google.com +reminv@google.com +satk@google.com diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg new file mode 100644 index 0000000000..ebc1264c7a --- /dev/null +++ b/PREUPLOAD.cfg @@ -0,0 +1,4 @@ +[Hook Scripts] +checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT} + +ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES} diff --git a/TEST_MAPPING b/TEST_MAPPING index 94f9232bc4..d7d4bcb98c 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -1,4 +1,37 @@ { + "presubmit": [ + // Run in addition to mainline-presubmit as mainline-presubmit is not + // supported in every branch. + // CtsNetTestCasesLatestSdk uses stable API shims, so does not exercise + // some latest APIs. Run CtsNetTestCases to get coverage of newer APIs. + { + "name": "CtsNetTestCases", + "options": [ + { + "exclude-annotation": "com.android.testutils.SkipPresubmit" + } + ] + } + ], + "mainline-presubmit": [ + { + // TODO: add back the tethering modules when updatable in this branch + "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex]", + "options": [ + { + "exclude-annotation": "com.android.testutils.SkipPresubmit" + } + ] + } + ], + // Tests on physical devices with SIM cards: postsubmit only for capacity constraints + "mainline-postsubmit": [ + { + // TODO: add back the tethering module when updatable in this branch + "name": "CtsNetTestCasesLatestSdk[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex]", + "keywords": ["sim"] + } + ], "imports": [ { "path": "frameworks/base/core/java/android/net" @@ -16,4 +49,4 @@ "path": "packages/modules/Connectivity/Tethering" } ] -} \ No newline at end of file +} diff --git a/Tethering/Android.bp b/Tethering/Android.bp new file mode 100644 index 0000000000..f469d89866 --- /dev/null +++ b/Tethering/Android.bp @@ -0,0 +1,164 @@ +// +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_defaults { + name: "TetheringAndroidLibraryDefaults", + sdk_version: "module_current", + min_sdk_version: "30", + srcs: [ + "apishim/**/*.java", + "src/**/*.java", + ":framework-connectivity-shared-srcs", + ":tethering-module-utils-srcs", + ":services-tethering-shared-srcs", + ], + static_libs: [ + "NetworkStackApiStableShims", + "androidx.annotation_annotation", + "modules-utils-build", + "netlink-client", + "networkstack-client", + "android.hardware.tetheroffload.config-V1.0-java", + "android.hardware.tetheroffload.control-V1.0-java", + "android.hardware.tetheroffload.control-V1.1-java", + "net-utils-framework-common", + "net-utils-device-common", + "netd-client", + "NetworkStackApiCurrentShims", + ], + libs: [ + "framework-connectivity", + "framework-statsd.stubs.module_lib", + "framework-tethering.impl", + "framework-wifi", + "unsupportedappusage", + ], + plugins: ["java_api_finder"], + manifest: "AndroidManifestBase.xml", +} + +// Build tethering static library, used to compile both variants of the tethering. +android_library { + name: "TetheringApiCurrentLib", + defaults: ["TetheringAndroidLibraryDefaults"], +} + +// Due to b/143733063, APK can't access a jni lib that is in APEX (but not in the APK). +cc_library { + name: "libtetherutilsjni", + sdk_version: "current", + apex_available: [ + "//apex_available:platform", // Used by InProcessTethering + "com.android.tethering", + ], + min_sdk_version: "30", + header_libs: [ + "bpf_syscall_wrappers", + "bpf_tethering_headers", + ], + srcs: [ + "jni/*.cpp", + ], + shared_libs: [ + "liblog", + "libnativehelper_compat_libc++", + ], + static_libs: [ + "libnetjniutils", + ], + + // We cannot use plain "libc++" here to link libc++ dynamically because it results in: + // java.lang.UnsatisfiedLinkError: dlopen failed: library "libc++_shared.so" not found + // even if "libc++" is added into jni_libs below. Adding "libc++_shared" into jni_libs doesn't + // build because soong complains of: + // module Tethering missing dependencies: libc++_shared + // + // So, link libc++ statically. This means that we also need to ensure that all the C++ libraries + // we depend on do not dynamically link libc++. This is currently the case, because liblog is + // C-only and libnativehelper_compat_libc also uses stl: "c++_static". + stl: "c++_static", + + cflags: [ + "-Wall", + "-Werror", + "-Wno-unused-parameter", + "-Wthread-safety", + ], + + ldflags: ["-Wl,--exclude-libs=ALL,-error-limit=0"], +} + +// Common defaults for compiling the actual APK. +java_defaults { + name: "TetheringAppDefaults", + sdk_version: "module_current", + privileged: true, + jni_libs: [ + "libtetherutilsjni", + ], + resource_dirs: [ + "res", + ], + libs: [ + "framework-tethering", + "framework-wifi", + ], + jarjar_rules: "jarjar-rules.txt", + optimize: { + proguard_flags_files: ["proguard.flags"], + }, +} + +// Non-updatable tethering running in the system server process for devices not using the module +android_app { + name: "InProcessTethering", + defaults: ["TetheringAppDefaults"], + static_libs: ["TetheringApiCurrentLib"], + certificate: "platform", + manifest: "AndroidManifest_InProcess.xml", + // InProcessTethering is a replacement for Tethering + overrides: ["Tethering"], + apex_available: ["com.android.tethering"], + min_sdk_version: "30", +} + +// Updatable tethering packaged as an application +android_app { + name: "Tethering", + defaults: ["TetheringAppDefaults"], + static_libs: ["TetheringApiCurrentLib"], + certificate: "networkstack", + manifest: "AndroidManifest.xml", + use_embedded_native_libs: true, + // The permission configuration *must* be included to ensure security of the device + required: [ + "NetworkPermissionConfig", + "privapp_whitelist_com.android.networkstack.tethering", + ], + apex_available: ["com.android.tethering"], + min_sdk_version: "30", +} + +sdk { + name: "tethering-module-sdk", + java_sdk_libs: [ + "framework-tethering", + ], +} diff --git a/Tethering/AndroidManifest.xml b/Tethering/AndroidManifest.xml new file mode 100644 index 0000000000..e6444f3ead --- /dev/null +++ b/Tethering/AndroidManifest.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tethering/AndroidManifestBase.xml b/Tethering/AndroidManifestBase.xml new file mode 100644 index 0000000000..97c3988829 --- /dev/null +++ b/Tethering/AndroidManifestBase.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/Tethering/AndroidManifest_InProcess.xml b/Tethering/AndroidManifest_InProcess.xml new file mode 100644 index 0000000000..b1f124097c --- /dev/null +++ b/Tethering/AndroidManifest_InProcess.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/Tethering/OWNERS b/Tethering/OWNERS new file mode 100644 index 0000000000..5b42d49041 --- /dev/null +++ b/Tethering/OWNERS @@ -0,0 +1,2 @@ +include platform/packages/modules/NetworkStack/:/OWNERS +markchien@google.com diff --git a/Tethering/TEST_MAPPING b/Tethering/TEST_MAPPING new file mode 100644 index 0000000000..5617b0c13c --- /dev/null +++ b/Tethering/TEST_MAPPING @@ -0,0 +1,12 @@ +{ + "presubmit": [ + { + "name": "TetheringTests" + } + ], + "postsubmit": [ + { + "name": "TetheringIntegrationTests" + } + ] +} diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp new file mode 100644 index 0000000000..31577ee55d --- /dev/null +++ b/Tethering/apex/Android.bp @@ -0,0 +1,84 @@ +// +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +apex { + name: "com.android.tethering", + compile_multilib: "both", + updatable: true, + min_sdk_version: "30", + bootclasspath_fragments: [ + "com.android.tethering-bootclasspath-fragment", + ], + java_libs: [ + "service-connectivity", + ], + multilib: { + first: { + jni_libs: ["libservice-connectivity"] + }, + both: { + jni_libs: ["libframework-connectivity-jni"], + } + }, + bpfs: [ + "offload.o", + "test.o", + ], + apps: [ + "ServiceConnectivityResources", + "Tethering", + ], + prebuilts: ["current_sdkinfo"], + manifest: "manifest.json", + key: "com.android.tethering.key", + + androidManifest: "AndroidManifest.xml", +} + +apex_key { + name: "com.android.tethering.key", + public_key: "com.android.tethering.avbpubkey", + private_key: "com.android.tethering.pem", +} + +android_app_certificate { + name: "com.android.tethering.certificate", + certificate: "com.android.tethering", +} + +// Encapsulate the contributions made by the com.android.tethering to the bootclasspath. +bootclasspath_fragment { + name: "com.android.tethering-bootclasspath-fragment", + contents: [ + "framework-connectivity", + "framework-tethering", + ], + apex_available: ["com.android.tethering"], +} + +override_apex { + name: "com.android.tethering.inprocess", + base: "com.android.tethering", + package_name: "com.android.tethering.inprocess", + apps: [ + "ServiceConnectivityResources", + "InProcessTethering", + ], +} diff --git a/Tethering/apex/AndroidManifest.xml b/Tethering/apex/AndroidManifest.xml new file mode 100644 index 0000000000..4aae3cc300 --- /dev/null +++ b/Tethering/apex/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/Tethering/apex/com.android.tethering.avbpubkey b/Tethering/apex/com.android.tethering.avbpubkey new file mode 100644 index 0000000000..9a2c0174e4 Binary files /dev/null and b/Tethering/apex/com.android.tethering.avbpubkey differ diff --git a/Tethering/apex/com.android.tethering.pem b/Tethering/apex/com.android.tethering.pem new file mode 100644 index 0000000000..d4f39abd3b --- /dev/null +++ b/Tethering/apex/com.android.tethering.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKgIBAAKCAgEA+AWTp03PBRMGt4mVNLt5PDoFFSfmFOVTM7jt5AJXnQMIDsAM +1cyWGWRridGIpoHAaCALVgW5aRySgi8yV5xP4w0YHcKbfh9M6I9oz4RUo4GQBZfX ++lFIGaLjb6I3tEJxPuxps4sW26Io63ihwTnKeGyADHdHGWDUs9WU0Ml+QTvKrdjy +qC03M0dehYXILGiA9m+UXwKoKxhWgfDUhWLhDBUtLJLPL4WeqKc9sG9h+zzVqE+8 +LzJsfrodKhTTrLpWOXi6YLRTk8dzsuPz/Nu98sJd1w3fHd20DrmkqsxVhgN1h+nk +zcPpxyGYIP6qYVZCmIXCwZZNtPeb7y/tOs967VHoZ4Qj7p2tE0CAWFMZFGjA/pcZ +7fi6CsIuMOYBbj4+wRlJwpG1g5zSJBCjzhv7dZp8S5oXmLShNYOMYEdsPfaZbm08 +3pVY+k8DVf7idcANXNw1lM+sPbE2hp5VuEuVpK+ca5x8hIMpTqJ84wDAjnC1kCwm +X2xfNvYPKNF58SvqlNCPN8X7hQjoeaEb7w24vCdZMRqeGBmu1GNQvCyzbBO0huQm +f5CQPrZjPcnoImlP879VPxY4YB6tAjsA/ZLiub9VdT108lCjb5r8criMzpMAA/AQ +NqQLWFI3M43xPemGBTiIguTYgpRgGcdRZf7XuTgTY5qzQZZuZMVuwaqSD2cCAwEA +AQKCAgEA0jMvw3BPTrakT7Lb8JgelKt7mUV6WyVMUZ6eh0pw5JIoJxAfEKfWYmjY +NzKNRMjcv6LA2MP7MplTld/YI6ZHkl+Lm9VOISL39HVuV8mIThbFb+gT1INEvu1t +IjRyT2SsQ67rmo377mLNmVtgg7mt3kfecjI44MpPGqad/CF4zmKVUKd4aI4BpYUM +F8+dKf3bpoBEWA2RZwy2bGQmSXHW132vDoLR8y2knL04rCqJ+PrC/WWuULXEe9bS +VtLV3yMBZq3qD4Fk/+7fILLPGvNFVdPi4htQiChYrM4rP9HzfaO63VieYMF0hR70 +pqoOznXj9Q4QVC9FZmUgFCQjQ1+KhqJw3OldIo0SnvpsLdTO/inKkhQWKC5HlPyh +/rqvro2j3pTHWPAziuBr+oQPcdVCOlCBZ+B99L1tO7aGktVPEIVQG7G7jlFMBiJ1 +j/kRGk2RTX8RaPQJTnwUqp8mWUV2fwxHiXNadjejA5ZU3eQT2eAOhXl1w6Lv2jEl +0wMOwPMJGcF77CcqnnWHON8fkxCbAfyy5Uo6Pm9g/Zzecn+ji2sabG7Ge5t0gzdL +LKRcGoyakN2CrbQ8pxlCTgE4HX5oPY+VuqOf8L3AIWIJBsyLbXHVkL1mqQ/Ed2uz +zaaSFYUZw81+m/5bl8JLPaIFNPyikZrXTD0YRer3V06XiyP/kYECggEBAP033xeF +OhgRwkRTjd68hwRJpyHsZDWxHiUqQf6l6yFv5mEE355G2IGI7cZmR2+tUDjQdxLv +tAZIszTK4PFCdVTeWfGVFbVF84eNWLB124pHDMM79GN/AMcuHnQPR756a8IO1hIy +4KxIUE1a1PKN5b9IgE5Lu4TZM96HDpFcUAmCT5urdYDmg3++IWT9PYQlGS7Hhiar +r+Hh646waM8Qx619CwXBqy+Y37+WHVbYqJClr6AcpVMrGA+6cgpskFpZAPLsoy7G +RSsVfyV8pH2JKm/hzk7XCwIpczxeWQSfpJWZ+oOPFHu+zM60Cdj2UrQyKrNHwew8 ++WYe9eCA+MiNBcECggEBAPq/F1vdqROiLv9uzhKb8ybgdL7CmREELiqwK+MvNE9t +W7lQz7lcWzav+b2n0M+VJBxUWB3XClgoIvA/AllgTgsYXfKAxNakhKLSBoMmvKCW +HtWcGr/D3RcmacK+DTMWlVS/LuueAFLuH6UmBIUFKc+qA5x7oQecAFALBFupE3G4 +LtAspLBI6P8gRtRav5p2whs9H8qjYcyf2f6liWpkmFITcXvPvAxFHicR6ZJdwZ/S +PiX2LJQnOpT7L3+2PWnYwzFStb4MkMGlFKcscU9CvS53JcP/J4Asjk0I4zDB2gri +xzFHPlVzCr2IVVGptKCQ3sdYiMIzQKzEXQHCU8h37ycCggEBAJu8aC48Fz3Edlm1 +ldS+2L9vWSaJEBzhoSu0cMBgZVu8SdGzwKDE69XHVI4oS5lI28UFmaaA3JTc07MN +cAmSGT2oP2NQkPhbXGsrKLfm1K6YAiZ1Ulp7OwxFth8lYreo7Wt92nV46yuqkhDx +Y3UGhp39xkPhWiRbvgYHxJLsVqFyjumsK2mq3IeNdVZ6VgJXGsTlnAFeqJ7hZxHs +N5natSRjeosA0PtGJ57agZLvT8Ue0gREef3LzFGoFwmIOcQHZ4kAt2BGOzZDU17H +6Rb4bKxBEbT1l2St/5zKXi90zDHicOvG7Q8qiyY6HrBc1wLSs+ZtpLxZx/3h3tFE +IT6fVUECggEBAMSAQm8Ey76OJ+SXUjk1K50442SnHcs/Cmr7urkEQitImUwl71Pk +87pst/uP6szypOTqmE9yOTIS6iZ6Sn3+QcriIqWrkhZfwW3Tx7S6A7KZUrq15iSH ++thsiw9JXxC9TvOmC8AsBzb2U6hZncsc28JZCxFztSNAduJDb/vhCVLiMxWDFuDr +kmR1R+yc3XDQRpeQFDz6QudYEj9EPOc6xD/16sZLaqP2+oVFvVSt0tJLsdaQECle +gMNGAdhE2eX8MCOUHMc+E6cdlozYAEhMFfO2/cqWR79jq3TlVR3dnOFRDScqHMhc +KnuTvsELjHkUbvGsCSiff7yk+fop7vy4OJsCggEAPemJdItO2rhib8EofrZdY72I +oifX1jhPZ1BWD2GKgcx+eVyJGbONBbJVexvvskTfZBvCcAegmgp+sngP6MO6yZkr +cHMfAJeApYZnshsgXksHGMDtSB50/w1JLrc/nqpxdpy/aTazt0Eu1pLWpze1HFZ/ +Xyu4PcmrU+4P1vN7c396slHMktEvly6QqOn4nfBbGDJ17Ow6X1XFvGjAxQPIDTB+ +6loV14AHymwmqwMrGn84O72rzqyw+41GxW5+oXhOZ4MeXF3u89TBLWvXDpPy/YQU +EiKpodN0YeEn6Ghzplan8rUha+7TP7AYnS5pCszsCHKd03Py0lMLkF+uAfVsDA== +-----END RSA PRIVATE KEY----- diff --git a/Tethering/apex/com.android.tethering.pk8 b/Tethering/apex/com.android.tethering.pk8 new file mode 100644 index 0000000000..3b94405945 Binary files /dev/null and b/Tethering/apex/com.android.tethering.pk8 differ diff --git a/Tethering/apex/com.android.tethering.x509.pem b/Tethering/apex/com.android.tethering.x509.pem new file mode 100644 index 0000000000..a1786e35e8 --- /dev/null +++ b/Tethering/apex/com.android.tethering.x509.pem @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIGKTCCBBGgAwIBAgIUNiSs5EMqxCZ31gWWCcRJVp9HffAwDQYJKoZIhvcNAQEL +BQAwgaIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH +DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy +b2lkMR4wHAYDVQQDDBVjb20uYW5kcm9pZC50ZXRoZXJpbmcxIjAgBgkqhkiG9w0B +CQEWE2FuZHJvaWRAYW5kcm9pZC5jb20wIBcNMTkxMjE4MDcwMDQ4WhgPNDc1NzEx +MTMwNzAwNDhaMIGiMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEW +MBQGA1UEBwwNTW91bnRhaW4gVmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4GA1UE +CwwHQW5kcm9pZDEeMBwGA1UEAwwVY29tLmFuZHJvaWQudGV0aGVyaW5nMSIwIAYJ +KoZIhvcNAQkBFhNhbmRyb2lkQGFuZHJvaWQuY29tMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAxvTUA4seblYjZLfTVNwZuJH914QVNFTj+vD94pWmt5Aq +sH1DVTpBvpXXegc/P5HI2XF/71poSBib1WaQSuXG0fU5K75T18bOGL0qF+fhMtBO +wUyvulcjO0h4XE/xf0txY54exUjAA4JS9ERGJOgb4GOwSbPyzekfmzIyCZ2Yawwu ++oGwD2ZNzZRaPOoWxjwohBWQ6mySuvF9RRRb300qmxxUGFM9Ki3aqrWlYlHEOwOC +M+gIXxYFO7S+yUzf6/gMZLOz2YqfcTOup4hAxtExR7niutxJSsRLPBL237exAJoz +OupoXjtWAlPK4ZwZ/Nl1jdTWauJ+Kv3WqzhHGEb2gn3ZpeO3IdOjJhDgFJ6m1OT/ +kjRbW1LCuKGrKaoqsEDT2X3a7Izfripn65hSNTfR5gNLtgELaI3/vXi8Fmzw1AfH ++qi6ulElZvSwx0qm+S0QiPyGFlxrsdnHoGJl1tzjJW8KdNZRvzRLUQtbphPp+VkL +5i0bNKum+AwbfdUkLkNLfw9XdbujgBkZTZDQbZGsNjgrvyXcPO2KiJee0hVCZRs0 +rhDi5Pfm7BnN/I2vaTRz/W4mdct9H2RWMuqlSH90JvmKtWcND8ahmOJ3sggrvzfO +QNs3k4JTRecamMzqIkylhlnEC4FjWc6Bx4wsEpwBMZOkF/tGGMZYf2C09a8tpP0C +AwEAAaNTMFEwHQYDVR0OBBYEFNP5gIpNWmq0xa411M1GaRPbEijvMB8GA1UdIwQY +MBaAFNP5gIpNWmq0xa411M1GaRPbEijvMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI +hvcNAQELBQADggIBADJGmU3QP4EGbt6eBhVPeo/efsqrHsuB2fvFzvIobJbfkSob +cmvjbzIikOlPAgFWj8lT5SDcIWRorFf1u2JylClJ0nSDcqJMHVKmT7wseV/KtX// +1yUyJFRQVzmjC89dp8OIc00GmItivKLer3NbJdkR3rTUjg7+bNUO27Qp3AFREmiJ +P+M7ouvcQRvByUWbp/LOrJpMdJLysRBO562RwrtwTjltdvufyYswbBZOKEiUh1Jc +Ged+3+SJdhwq3Wy+R3Uj7YE7mUMu1QNbANIMrwF8W93EA53eoL2+cKmuaVU6ZURL +xgSJaY6TrunnSI9XTROLtjsFlJorYWy2tvG7Q5Hw3OkO2Xdz/mm85VTkiusg9DMB +WWTv607YtsIO0FhKmcV4bp3q/EkRj3t/zLvL9uFJrWDGkuShZq6fQvqbCvaokOPY ++M0ZRIwgwa9UpEE0BMklVWqR6BGyap614gOgcOjYM70WRNl59Qne+g128ZN7g9nz +61F70i7kUngV0ZUz1/Fu/NCG+6wGF85ZbFmQl60YHPDw1FtjVUuKyBblaDzdJunx +yQr2t9RUokzFBFK0lGW3+yf0WDQ5fqTMs5h8bz1FCq8/HzWmpdOfqePLe4zsld3b +1nFuSohaIfbn/HDdTNtTBGQPgz8ZswQ6ejJJqTLz9D/odbqn9LeIhDZXcQTf +-----END CERTIFICATE----- diff --git a/Tethering/apex/manifest.json b/Tethering/apex/manifest.json new file mode 100644 index 0000000000..11e205d1b7 --- /dev/null +++ b/Tethering/apex/manifest.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.tethering", + "version": 309999900 +} diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java new file mode 100644 index 0000000000..33f1c29ea8 --- /dev/null +++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java @@ -0,0 +1,197 @@ +/* + * 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.networkstack.tethering.apishim.api30; + +import android.net.INetd; +import android.net.MacAddress; +import android.net.TetherStatsParcel; +import android.net.util.SharedLog; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.util.SparseArray; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.networkstack.tethering.BpfCoordinator.Dependencies; +import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule; +import com.android.networkstack.tethering.Tether4Key; +import com.android.networkstack.tethering.Tether4Value; +import com.android.networkstack.tethering.TetherStatsValue; + +/** + * Bpf coordinator class for API shims. + */ +public class BpfCoordinatorShimImpl + extends com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim { + private static final String TAG = "api30.BpfCoordinatorShimImpl"; + + @NonNull + private final SharedLog mLog; + @NonNull + private final INetd mNetd; + + public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) { + mLog = deps.getSharedLog().forSubComponent(TAG); + mNetd = deps.getNetd(); + } + + @Override + public boolean isInitialized() { + return true; + }; + + @Override + public boolean tetherOffloadRuleAdd(@NonNull final Ipv6ForwardingRule rule) { + try { + mNetd.tetherOffloadRuleAdd(rule.toTetherOffloadRuleParcel()); + } catch (RemoteException | ServiceSpecificException e) { + mLog.e("Could not add IPv6 forwarding rule: ", e); + return false; + } + + return true; + }; + + @Override + public boolean tetherOffloadRuleRemove(@NonNull final Ipv6ForwardingRule rule) { + try { + mNetd.tetherOffloadRuleRemove(rule.toTetherOffloadRuleParcel()); + } catch (RemoteException | ServiceSpecificException e) { + mLog.e("Could not remove IPv6 forwarding rule: ", e); + return false; + } + return true; + } + + @Override + public boolean startUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex, + @NonNull MacAddress inDstMac, @NonNull MacAddress outSrcMac, + @NonNull MacAddress outDstMac, int mtu) { + return true; + } + + @Override + public boolean stopUpstreamIpv6Forwarding(int downstreamIfindex, + int upstreamIfindex, @NonNull MacAddress inDstMac) { + return true; + } + + @Override + @Nullable + public SparseArray tetherOffloadGetStats() { + final TetherStatsParcel[] tetherStatsList; + try { + // The reported tether stats are total data usage for all currently-active upstream + // interfaces since tethering start. There will only ever be one entry for a given + // interface index. + tetherStatsList = mNetd.tetherOffloadGetStats(); + } catch (RemoteException | ServiceSpecificException e) { + mLog.e("Fail to fetch tethering stats from netd: " + e); + return null; + } + + return toTetherStatsValueSparseArray(tetherStatsList); + } + + @Override + public boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes) { + try { + mNetd.tetherOffloadSetInterfaceQuota(ifIndex, quotaBytes); + } catch (RemoteException | ServiceSpecificException e) { + mLog.e("Exception when updating quota " + quotaBytes + ": ", e); + return false; + } + return true; + } + + @NonNull + private SparseArray toTetherStatsValueSparseArray( + @NonNull final TetherStatsParcel[] parcels) { + final SparseArray tetherStatsList = new SparseArray(); + + for (TetherStatsParcel p : parcels) { + tetherStatsList.put(p.ifIndex, new TetherStatsValue(p.rxPackets, p.rxBytes, + 0 /* rxErrors */, p.txPackets, p.txBytes, 0 /* txErrors */)); + } + + return tetherStatsList; + } + + @Override + @Nullable + public TetherStatsValue tetherOffloadGetAndClearStats(int ifIndex) { + try { + final TetherStatsParcel stats = + mNetd.tetherOffloadGetAndClearStats(ifIndex); + return new TetherStatsValue(stats.rxPackets, stats.rxBytes, 0 /* rxErrors */, + stats.txPackets, stats.txBytes, 0 /* txErrors */); + } catch (RemoteException | ServiceSpecificException e) { + mLog.e("Exception when cleanup tether stats for upstream index " + + ifIndex + ": ", e); + return null; + } + } + + @Override + public boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key, + @NonNull Tether4Value value) { + /* no op */ + return true; + } + + @Override + public boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key) { + /* no op */ + return true; + } + + @Override + public boolean attachProgram(String iface, boolean downstream) { + /* no op */ + return true; + } + + @Override + public boolean detachProgram(String iface) { + /* no op */ + return true; + } + + @Override + public boolean isAnyIpv4RuleOnUpstream(int ifIndex) { + /* no op */ + return false; + } + + @Override + public boolean addDevMap(int ifIndex) { + /* no op */ + return false; + } + + @Override + public boolean removeDevMap(int ifIndex) { + /* no op */ + return false; + } + + @Override + public String toString() { + return "Netd used"; + } +} diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java new file mode 100644 index 0000000000..74ddcbce18 --- /dev/null +++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java @@ -0,0 +1,522 @@ +/* + * 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.networkstack.tethering.apishim.api31; + +import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED; + +import android.net.MacAddress; +import android.net.util.SharedLog; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; +import android.util.Log; +import android.util.SparseArray; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.networkstack.tethering.BpfCoordinator.Dependencies; +import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule; +import com.android.networkstack.tethering.BpfMap; +import com.android.networkstack.tethering.BpfUtils; +import com.android.networkstack.tethering.Tether4Key; +import com.android.networkstack.tethering.Tether4Value; +import com.android.networkstack.tethering.Tether6Value; +import com.android.networkstack.tethering.TetherDevKey; +import com.android.networkstack.tethering.TetherDevValue; +import com.android.networkstack.tethering.TetherDownstream6Key; +import com.android.networkstack.tethering.TetherLimitKey; +import com.android.networkstack.tethering.TetherLimitValue; +import com.android.networkstack.tethering.TetherStatsKey; +import com.android.networkstack.tethering.TetherStatsValue; +import com.android.networkstack.tethering.TetherUpstream6Key; + +import java.io.FileDescriptor; +import java.io.IOException; + +/** + * Bpf coordinator class for API shims. + */ +public class BpfCoordinatorShimImpl + extends com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim { + private static final String TAG = "api31.BpfCoordinatorShimImpl"; + + // AF_KEY socket type. See include/linux/socket.h. + private static final int AF_KEY = 15; + // PFKEYv2 constants. See include/uapi/linux/pfkeyv2.h. + private static final int PF_KEY_V2 = 2; + + @NonNull + private final SharedLog mLog; + + // BPF map for downstream IPv4 forwarding. + @Nullable + private final BpfMap mBpfDownstream4Map; + + // BPF map for upstream IPv4 forwarding. + @Nullable + private final BpfMap mBpfUpstream4Map; + + // BPF map for downstream IPv6 forwarding. + @Nullable + private final BpfMap mBpfDownstream6Map; + + // BPF map for upstream IPv6 forwarding. + @Nullable + private final BpfMap mBpfUpstream6Map; + + // BPF map of tethering statistics of the upstream interface since tethering startup. + @Nullable + private final BpfMap mBpfStatsMap; + + // BPF map of per-interface quota for tethering offload. + @Nullable + private final BpfMap mBpfLimitMap; + + // BPF map of interface index mapping for XDP. + @Nullable + private final BpfMap mBpfDevMap; + + // Tracking IPv4 rule count while any rule is using the given upstream interfaces. Used for + // reducing the BPF map iteration query. The count is increased or decreased when the rule is + // added or removed successfully on mBpfDownstream4Map. Counting the rules on downstream4 map + // is because tetherOffloadRuleRemove can't get upstream interface index from upstream key, + // unless pass upstream value which is not required for deleting map entry. The upstream + // interface index is the same in Upstream4Value.oif and Downstream4Key.iif. For now, it is + // okay to count on Downstream4Key. See BpfConntrackEventConsumer#accept. + // Note that except the constructor, any calls to mBpfDownstream4Map.clear() need to clear + // this counter as well. + // TODO: Count the rule on upstream if multi-upstream is supported and the + // packet needs to be sent and responded on different upstream interfaces. + // TODO: Add IPv6 rule count. + private final SparseArray mRule4CountOnUpstream = new SparseArray<>(); + + public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) { + mLog = deps.getSharedLog().forSubComponent(TAG); + + mBpfDownstream4Map = deps.getBpfDownstream4Map(); + mBpfUpstream4Map = deps.getBpfUpstream4Map(); + mBpfDownstream6Map = deps.getBpfDownstream6Map(); + mBpfUpstream6Map = deps.getBpfUpstream6Map(); + mBpfStatsMap = deps.getBpfStatsMap(); + mBpfLimitMap = deps.getBpfLimitMap(); + mBpfDevMap = deps.getBpfDevMap(); + + // Clear the stubs of the maps for handling the system service crash if any. + // Doesn't throw the exception and clear the stubs as many as possible. + try { + if (mBpfDownstream4Map != null) mBpfDownstream4Map.clear(); + } catch (ErrnoException e) { + mLog.e("Could not clear mBpfDownstream4Map: " + e); + } + try { + if (mBpfUpstream4Map != null) mBpfUpstream4Map.clear(); + } catch (ErrnoException e) { + mLog.e("Could not clear mBpfUpstream4Map: " + e); + } + try { + if (mBpfDownstream6Map != null) mBpfDownstream6Map.clear(); + } catch (ErrnoException e) { + mLog.e("Could not clear mBpfDownstream6Map: " + e); + } + try { + if (mBpfUpstream6Map != null) mBpfUpstream6Map.clear(); + } catch (ErrnoException e) { + mLog.e("Could not clear mBpfUpstream6Map: " + e); + } + try { + if (mBpfStatsMap != null) mBpfStatsMap.clear(); + } catch (ErrnoException e) { + mLog.e("Could not clear mBpfStatsMap: " + e); + } + try { + if (mBpfLimitMap != null) mBpfLimitMap.clear(); + } catch (ErrnoException e) { + mLog.e("Could not clear mBpfLimitMap: " + e); + } + try { + if (mBpfDevMap != null) mBpfDevMap.clear(); + } catch (ErrnoException e) { + mLog.e("Could not clear mBpfDevMap: " + e); + } + } + + @Override + public boolean isInitialized() { + return mBpfDownstream4Map != null && mBpfUpstream4Map != null && mBpfDownstream6Map != null + && mBpfUpstream6Map != null && mBpfStatsMap != null && mBpfLimitMap != null + && mBpfDevMap != null; + } + + @Override + public boolean tetherOffloadRuleAdd(@NonNull final Ipv6ForwardingRule rule) { + if (!isInitialized()) return false; + + final TetherDownstream6Key key = rule.makeTetherDownstream6Key(); + final Tether6Value value = rule.makeTether6Value(); + + try { + mBpfDownstream6Map.updateEntry(key, value); + } catch (ErrnoException e) { + mLog.e("Could not update entry: ", e); + return false; + } + + return true; + } + + @Override + public boolean tetherOffloadRuleRemove(@NonNull final Ipv6ForwardingRule rule) { + if (!isInitialized()) return false; + + try { + mBpfDownstream6Map.deleteEntry(rule.makeTetherDownstream6Key()); + } catch (ErrnoException e) { + // Silent if the rule did not exist. + if (e.errno != OsConstants.ENOENT) { + mLog.e("Could not update entry: ", e); + return false; + } + } + return true; + } + + @Override + public boolean startUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex, + @NonNull MacAddress inDstMac, @NonNull MacAddress outSrcMac, + @NonNull MacAddress outDstMac, int mtu) { + if (!isInitialized()) return false; + + final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfindex, inDstMac); + final Tether6Value value = new Tether6Value(upstreamIfindex, outSrcMac, + outDstMac, OsConstants.ETH_P_IPV6, mtu); + try { + mBpfUpstream6Map.insertEntry(key, value); + } catch (ErrnoException | IllegalStateException e) { + mLog.e("Could not insert upstream6 entry: " + e); + return false; + } + return true; + } + + @Override + public boolean stopUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex, + @NonNull MacAddress inDstMac) { + if (!isInitialized()) return false; + + final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfindex, inDstMac); + try { + mBpfUpstream6Map.deleteEntry(key); + } catch (ErrnoException e) { + mLog.e("Could not delete upstream IPv6 entry: " + e); + return false; + } + return true; + } + + @Override + @Nullable + public SparseArray tetherOffloadGetStats() { + if (!isInitialized()) return null; + + final SparseArray tetherStatsList = new SparseArray(); + try { + // The reported tether stats are total data usage for all currently-active upstream + // interfaces since tethering start. + mBpfStatsMap.forEach((key, value) -> tetherStatsList.put((int) key.ifindex, value)); + } catch (ErrnoException e) { + mLog.e("Fail to fetch tethering stats from BPF map: ", e); + return null; + } + return tetherStatsList; + } + + @Override + public boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes) { + if (!isInitialized()) return false; + + // The common case is an update, where the stats already exist, + // hence we read first, even though writing with BPF_NOEXIST + // first would make the code simpler. + long rxBytes, txBytes; + TetherStatsValue statsValue = null; + + try { + statsValue = mBpfStatsMap.getValue(new TetherStatsKey(ifIndex)); + } catch (ErrnoException e) { + // The BpfMap#getValue doesn't throw an errno ENOENT exception. Catch other error + // while trying to get stats entry. + mLog.e("Could not get stats entry of interface index " + ifIndex + ": ", e); + return false; + } + + if (statsValue != null) { + // Ok, there was a stats entry. + rxBytes = statsValue.rxBytes; + txBytes = statsValue.txBytes; + } else { + // No stats entry - create one with zeroes. + try { + // This function is the *only* thing that can create entries. + // BpfMap#insertEntry use BPF_NOEXIST to create the entry. The entry is created + // if and only if it doesn't exist. + mBpfStatsMap.insertEntry(new TetherStatsKey(ifIndex), new TetherStatsValue( + 0 /* rxPackets */, 0 /* rxBytes */, 0 /* rxErrors */, 0 /* txPackets */, + 0 /* txBytes */, 0 /* txErrors */)); + } catch (ErrnoException | IllegalArgumentException e) { + mLog.e("Could not create stats entry: ", e); + return false; + } + rxBytes = 0; + txBytes = 0; + } + + // rxBytes + txBytes won't overflow even at 5gbps for ~936 years. + long newLimit = rxBytes + txBytes + quotaBytes; + + // if adding limit (e.g., if limit is QUOTA_UNLIMITED) caused overflow: clamp to 'infinity' + if (newLimit < rxBytes + txBytes) newLimit = QUOTA_UNLIMITED; + + try { + mBpfLimitMap.updateEntry(new TetherLimitKey(ifIndex), new TetherLimitValue(newLimit)); + } catch (ErrnoException e) { + mLog.e("Fail to set quota " + quotaBytes + " for interface index " + ifIndex + ": ", e); + return false; + } + + return true; + } + + @Override + @Nullable + public TetherStatsValue tetherOffloadGetAndClearStats(int ifIndex) { + if (!isInitialized()) return null; + + // getAndClearTetherOffloadStats is called after all offload rules have already been + // deleted for the given upstream interface. Before starting to do cleanup stuff in this + // function, use synchronizeKernelRCU to make sure that all the current running eBPF + // programs are finished on all CPUs, especially the unfinished packet processing. After + // synchronizeKernelRCU returned, we can safely read or delete on the stats map or the + // limit map. + final int res = synchronizeKernelRCU(); + if (res != 0) { + // Error log but don't return. Do as much cleanup as possible. + mLog.e("synchronize_rcu() failed: " + res); + } + + TetherStatsValue statsValue = null; + try { + statsValue = mBpfStatsMap.getValue(new TetherStatsKey(ifIndex)); + } catch (ErrnoException e) { + mLog.e("Could not get stats entry for interface index " + ifIndex + ": ", e); + return null; + } + + if (statsValue == null) { + mLog.e("Could not get stats entry for interface index " + ifIndex); + return null; + } + + try { + mBpfStatsMap.deleteEntry(new TetherStatsKey(ifIndex)); + } catch (ErrnoException e) { + mLog.e("Could not delete stats entry for interface index " + ifIndex + ": ", e); + return null; + } + + try { + mBpfLimitMap.deleteEntry(new TetherLimitKey(ifIndex)); + } catch (ErrnoException e) { + mLog.e("Could not delete limit for interface index " + ifIndex + ": ", e); + return null; + } + + return statsValue; + } + + @Override + public boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key, + @NonNull Tether4Value value) { + if (!isInitialized()) return false; + + try { + if (downstream) { + mBpfDownstream4Map.insertEntry(key, value); + + // Increase the rule count while a adding rule is using a given upstream interface. + final int upstreamIfindex = (int) key.iif; + int count = mRule4CountOnUpstream.get(upstreamIfindex, 0 /* default */); + mRule4CountOnUpstream.put(upstreamIfindex, ++count); + } else { + mBpfUpstream4Map.insertEntry(key, value); + } + } catch (ErrnoException e) { + mLog.e("Could not insert entry (" + key + ", " + value + "): " + e); + return false; + } catch (IllegalStateException e) { + // Silent if the rule already exists. Note that the errno EEXIST was rethrown as + // IllegalStateException. See BpfMap#insertEntry. + } + return true; + } + + @Override + public boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key) { + if (!isInitialized()) return false; + + try { + if (downstream) { + if (!mBpfDownstream4Map.deleteEntry(key)) { + mLog.e("Could not delete entry (key: " + key + ")"); + return false; + } + + // Decrease the rule count while a deleting rule is not using a given upstream + // interface anymore. + final int upstreamIfindex = (int) key.iif; + Integer count = mRule4CountOnUpstream.get(upstreamIfindex); + if (count == null) { + Log.wtf(TAG, "Could not delete count for interface " + upstreamIfindex); + return false; + } + + if (--count == 0) { + // Remove the entry if the count decreases to zero. + mRule4CountOnUpstream.remove(upstreamIfindex); + } else { + mRule4CountOnUpstream.put(upstreamIfindex, count); + } + } else { + mBpfUpstream4Map.deleteEntry(key); + } + } catch (ErrnoException e) { + // Silent if the rule did not exist. + if (e.errno != OsConstants.ENOENT) { + mLog.e("Could not delete entry: ", e); + return false; + } + } + return true; + } + + @Override + public boolean attachProgram(String iface, boolean downstream) { + if (!isInitialized()) return false; + + try { + BpfUtils.attachProgram(iface, downstream); + } catch (IOException e) { + mLog.e("Could not attach program: " + e); + return false; + } + return true; + } + + @Override + public boolean detachProgram(String iface) { + if (!isInitialized()) return false; + + try { + BpfUtils.detachProgram(iface); + } catch (IOException e) { + mLog.e("Could not detach program: " + e); + return false; + } + return true; + } + + @Override + public boolean isAnyIpv4RuleOnUpstream(int ifIndex) { + // No entry means no rule for the given interface because 0 has never been stored. + return mRule4CountOnUpstream.get(ifIndex) != null; + } + + @Override + public boolean addDevMap(int ifIndex) { + if (!isInitialized()) return false; + + try { + mBpfDevMap.updateEntry(new TetherDevKey(ifIndex), new TetherDevValue(ifIndex)); + } catch (ErrnoException e) { + mLog.e("Could not add interface " + ifIndex + ": " + e); + return false; + } + return true; + } + + @Override + public boolean removeDevMap(int ifIndex) { + if (!isInitialized()) return false; + + try { + mBpfDevMap.deleteEntry(new TetherDevKey(ifIndex)); + } catch (ErrnoException e) { + mLog.e("Could not delete interface " + ifIndex + ": " + e); + return false; + } + return true; + } + + private String mapStatus(BpfMap m, String name) { + return name + "{" + (m != null ? "OK" : "ERROR") + "}"; + } + + @Override + public String toString() { + return String.join(", ", new String[] { + mapStatus(mBpfDownstream6Map, "mBpfDownstream6Map"), + mapStatus(mBpfUpstream6Map, "mBpfUpstream6Map"), + mapStatus(mBpfDownstream4Map, "mBpfDownstream4Map"), + mapStatus(mBpfUpstream4Map, "mBpfUpstream4Map"), + mapStatus(mBpfStatsMap, "mBpfStatsMap"), + mapStatus(mBpfLimitMap, "mBpfLimitMap"), + mapStatus(mBpfDevMap, "mBpfDevMap") + }); + } + + /** + * Call synchronize_rcu() to block until all existing RCU read-side critical sections have + * been completed. + * Note that BpfCoordinatorTest have no permissions to create or close pf_key socket. It is + * okay for now because the caller #bpfGetAndClearStats doesn't care the result of this + * function. The tests don't be broken. + * TODO: Wrap this function into Dependencies for mocking in tests. + */ + private int synchronizeKernelRCU() { + // This is a temporary hack for network stats map swap on devices running + // 4.9 kernels. The kernel code of socket release on pf_key socket will + // explicitly call synchronize_rcu() which is exactly what we need. + FileDescriptor pfSocket; + try { + pfSocket = Os.socket(AF_KEY, OsConstants.SOCK_RAW | OsConstants.SOCK_CLOEXEC, + PF_KEY_V2); + } catch (ErrnoException e) { + mLog.e("create PF_KEY socket failed: ", e); + return e.errno; + } + + // When closing socket, synchronize_rcu() gets called in sock_release(). + try { + Os.close(pfSocket); + } catch (ErrnoException e) { + mLog.e("failed to close the PF_KEY socket: ", e); + return e.errno; + } + + return 0; + } +} diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java new file mode 100644 index 0000000000..8a7a49c961 --- /dev/null +++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java @@ -0,0 +1,180 @@ +/* + * 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.networkstack.tethering.apishim.common; + +import android.net.MacAddress; +import android.util.SparseArray; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.networkstack.tethering.BpfCoordinator.Dependencies; +import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule; +import com.android.networkstack.tethering.Tether4Key; +import com.android.networkstack.tethering.Tether4Value; +import com.android.networkstack.tethering.TetherStatsValue; + +/** + * Bpf coordinator class for API shims. + */ +public abstract class BpfCoordinatorShim { + /** + * Get BpfCoordinatorShim object by OS build version. + */ + @NonNull + public static BpfCoordinatorShim getBpfCoordinatorShim(@NonNull final Dependencies deps) { + if (deps.isAtLeastS()) { + return new com.android.networkstack.tethering.apishim.api31.BpfCoordinatorShimImpl( + deps); + } else { + return new com.android.networkstack.tethering.apishim.api30.BpfCoordinatorShimImpl( + deps); + } + } + + /** + * Return true if this class has been initialized, otherwise return false. + */ + public abstract boolean isInitialized(); + + /** + * Adds a tethering offload rule to BPF map, or updates it if it already exists. + * + * Currently, only downstream /128 IPv6 entries are supported. An existing rule will be updated + * if the input interface and destination prefix match. Otherwise, a new rule will be created. + * Note that this can be only called on handler thread. + * + * @param rule The rule to add or update. + */ + public abstract boolean tetherOffloadRuleAdd(@NonNull Ipv6ForwardingRule rule); + + /** + * Deletes a tethering offload rule from the BPF map. + * + * Currently, only downstream /128 IPv6 entries are supported. An existing rule will be deleted + * if the destination IP address and the source interface match. It is not an error if there is + * no matching rule to delete. + * + * @param rule The rule to delete. + */ + public abstract boolean tetherOffloadRuleRemove(@NonNull Ipv6ForwardingRule rule); + + /** + * Starts IPv6 forwarding between the specified interfaces. + + * @param downstreamIfindex the downstream interface index + * @param upstreamIfindex the upstream interface index + * @param inDstMac the destination MAC address to use for XDP + * @param outSrcMac the source MAC address to use for packets + * @param outDstMac the destination MAC address to use for packets + * @return true if operation succeeded or was a no-op, false otherwise + */ + public abstract boolean startUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex, + @NonNull MacAddress inDstMac, @NonNull MacAddress outSrcMac, + @NonNull MacAddress outDstMac, int mtu); + + /** + * Stops IPv6 forwarding between the specified interfaces. + + * @param downstreamIfindex the downstream interface index + * @param upstreamIfindex the upstream interface index + * @param inDstMac the destination MAC address to use for XDP + * @return true if operation succeeded or was a no-op, false otherwise + */ + public abstract boolean stopUpstreamIpv6Forwarding(int downstreamIfindex, + int upstreamIfindex, @NonNull MacAddress inDstMac); + + /** + * Return BPF tethering offload statistics. + * + * @return an array of TetherStatsValue's, where each entry contains the upstream interface + * index and its tethering statistics since tethering was first started. + * There will only ever be one entry for a given interface index. + */ + @Nullable + public abstract SparseArray tetherOffloadGetStats(); + + /** + * Set a per-interface quota for tethering offload. + * + * @param ifIndex Index of upstream interface + * @param quotaBytes The quota defined as the number of bytes, starting from zero and counting + * from *now*. A value of QUOTA_UNLIMITED (-1) indicates there is no limit. + */ + @Nullable + public abstract boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes); + + /** + * Return BPF tethering offload statistics and clear the stats for a given upstream. + * + * Must only be called once all offload rules have already been deleted for the given upstream + * interface. The existing stats will be fetched and returned. The stats and the limit for the + * given upstream interface will be deleted as well. + * + * The stats and limit for a given upstream interface must be initialized (using + * tetherOffloadSetInterfaceQuota) before any offload will occur on that interface. + * + * Note that this can be only called while the BPF maps were initialized. + * + * @param ifIndex Index of upstream interface. + * @return TetherStatsValue, which contains the given upstream interface's tethering statistics + * since tethering was first started on that upstream interface. + */ + @Nullable + public abstract TetherStatsValue tetherOffloadGetAndClearStats(int ifIndex); + + /** + * Adds a tethering IPv4 offload rule to appropriate BPF map. + */ + public abstract boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key, + @NonNull Tether4Value value); + + /** + * Deletes a tethering IPv4 offload rule from the appropriate BPF map. + */ + public abstract boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key); + + /** + * Whether there is currently any IPv4 rule on the specified upstream. + */ + public abstract boolean isAnyIpv4RuleOnUpstream(int ifIndex); + + /** + * Attach BPF program. + * + * TODO: consider using InterfaceParams to replace interface name. + */ + public abstract boolean attachProgram(@NonNull String iface, boolean downstream); + + /** + * Detach BPF program. + * + * TODO: consider using InterfaceParams to replace interface name. + */ + public abstract boolean detachProgram(@NonNull String iface); + + /** + * Add interface index mapping. + */ + public abstract boolean addDevMap(int ifIndex); + + /** + * Remove interface index mapping. + */ + public abstract boolean removeDevMap(int ifIndex); +} + diff --git a/Tethering/bpf_progs/Android.bp b/Tethering/bpf_progs/Android.bp new file mode 100644 index 0000000000..289d75d0b0 --- /dev/null +++ b/Tethering/bpf_progs/Android.bp @@ -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. +// + +// +// struct definitions shared with JNI +// +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +cc_library_headers { + name: "bpf_tethering_headers", + vendor_available: false, + host_supported: false, + export_include_dirs: ["."], + cflags: [ + "-Wall", + "-Werror", + ], + sdk_version: "30", + min_sdk_version: "30", + apex_available: ["com.android.tethering"], + visibility: [ + "//packages/modules/Connectivity/Tethering", + ], +} + +// +// bpf kernel programs +// +bpf { + name: "offload.o", + srcs: ["offload.c"], + cflags: [ + "-Wall", + "-Werror", + ], + include_dirs: [ + // TODO: get rid of system/netd. + "system/netd/bpf_progs", // for bpf_net_helpers.h + ], +} + +bpf { + name: "test.o", + srcs: ["test.c"], + cflags: [ + "-Wall", + "-Werror", + ], + include_dirs: [ + // TODO: get rid of system/netd. + "system/netd/bpf_progs", // for bpf_net_helpers.h + ], +} diff --git a/Tethering/bpf_progs/bpf_tethering.h b/Tethering/bpf_progs/bpf_tethering.h new file mode 100644 index 0000000000..5fdf8cd88e --- /dev/null +++ b/Tethering/bpf_progs/bpf_tethering.h @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +// Common definitions for BPF code in the tethering mainline module. +// These definitions are available to: +// - The BPF programs in Tethering/bpf_progs/ +// - JNI code that depends on the bpf_tethering_headers library. + +#define BPF_TETHER_ERRORS \ + ERR(INVALID_IP_VERSION) \ + ERR(LOW_TTL) \ + ERR(INVALID_TCP_HEADER) \ + ERR(TCP_CONTROL_PACKET) \ + ERR(NON_GLOBAL_SRC) \ + ERR(NON_GLOBAL_DST) \ + ERR(LOCAL_SRC_DST) \ + ERR(NO_STATS_ENTRY) \ + ERR(NO_LIMIT_ENTRY) \ + ERR(BELOW_IPV4_MTU) \ + ERR(BELOW_IPV6_MTU) \ + ERR(LIMIT_REACHED) \ + ERR(CHANGE_HEAD_FAILED) \ + ERR(TOO_SHORT) \ + ERR(HAS_IP_OPTIONS) \ + ERR(IS_IP_FRAG) \ + ERR(CHECKSUM) \ + ERR(NON_TCP_UDP) \ + ERR(NON_TCP) \ + ERR(SHORT_L4_HEADER) \ + ERR(SHORT_TCP_HEADER) \ + ERR(SHORT_UDP_HEADER) \ + ERR(UDP_CSUM_ZERO) \ + ERR(TRUNCATED_IPV4) \ + ERR(_MAX) + +#define ERR(x) BPF_TETHER_ERR_ ##x, +enum { + BPF_TETHER_ERRORS +}; +#undef ERR + +#define ERR(x) #x, +static const char *bpf_tether_errors[] = { + BPF_TETHER_ERRORS +}; +#undef ERR + +// This header file is shared by eBPF kernel programs (C) and netd (C++) and +// some of the maps are also accessed directly from Java mainline module code. +// +// Hence: explicitly pad all relevant structures and assert that their size +// is the sum of the sizes of their fields. +#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.") + + +#define BPF_PATH_TETHER BPF_PATH "tethering/" + +#define TETHER_STATS_MAP_PATH BPF_PATH_TETHER "map_offload_tether_stats_map" + +typedef uint32_t TetherStatsKey; // upstream ifindex + +typedef struct { + uint64_t rxPackets; + uint64_t rxBytes; + uint64_t rxErrors; + uint64_t txPackets; + uint64_t txBytes; + uint64_t txErrors; +} TetherStatsValue; +STRUCT_SIZE(TetherStatsValue, 6 * 8); // 48 + +#define TETHER_LIMIT_MAP_PATH BPF_PATH_TETHER "map_offload_tether_limit_map" + +typedef uint32_t TetherLimitKey; // upstream ifindex +typedef uint64_t TetherLimitValue; // in bytes + +#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream6_rawip" +#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream6_ether" + +#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME +#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME + +#define TETHER_DOWNSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream6_map" + +// For now tethering offload only needs to support downstreams that use 6-byte MAC addresses, +// because all downstream types that are currently supported (WiFi, USB, Bluetooth and +// Ethernet) have 6-byte MAC addresses. + +typedef struct { + uint32_t iif; // The input interface index + uint8_t dstMac[ETH_ALEN]; // destination ethernet mac address (zeroed iff rawip ingress) + uint8_t zero[2]; // zero pad for 8 byte alignment + struct in6_addr neigh6; // The destination IPv6 address +} TetherDownstream6Key; +STRUCT_SIZE(TetherDownstream6Key, 4 + 6 + 2 + 16); // 28 + +typedef struct { + uint32_t oif; // The output interface to redirect to + struct ethhdr macHeader; // includes dst/src mac and ethertype (zeroed iff rawip egress) + uint16_t pmtu; // The maximum L3 output path/route mtu +} Tether6Value; +STRUCT_SIZE(Tether6Value, 4 + 14 + 2); // 20 + +#define TETHER_DOWNSTREAM64_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream64_map" + +typedef struct { + uint32_t iif; // The input interface index + uint8_t dstMac[ETH_ALEN]; // destination ethernet mac address (zeroed iff rawip ingress) + uint16_t l4Proto; // IPPROTO_TCP/UDP/... + struct in6_addr src6; // source & + struct in6_addr dst6; // destination IPv6 addresses + __be16 srcPort; // source & + __be16 dstPort; // destination tcp/udp/... ports +} TetherDownstream64Key; +STRUCT_SIZE(TetherDownstream64Key, 4 + 6 + 2 + 16 + 16 + 2 + 2); // 48 + +typedef struct { + uint32_t oif; // The output interface to redirect to + struct ethhdr macHeader; // includes dst/src mac and ethertype (zeroed iff rawip egress) + uint16_t pmtu; // The maximum L3 output path/route mtu + struct in_addr src4; // source & + struct in_addr dst4; // destination IPv4 addresses + __be16 srcPort; // source & + __be16 outPort; // destination tcp/udp/... ports + uint64_t lastUsed; // Kernel updates on each use with bpf_ktime_get_boot_ns() +} TetherDownstream64Value; +STRUCT_SIZE(TetherDownstream64Value, 4 + 14 + 2 + 4 + 4 + 2 + 2 + 8); // 40 + +#define TETHER_UPSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_upstream6_rawip" +#define TETHER_UPSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_upstream6_ether" + +#define TETHER_UPSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM6_TC_PROG_RAWIP_NAME +#define TETHER_UPSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM6_TC_PROG_ETHER_NAME + +#define TETHER_UPSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_upstream6_map" + +typedef struct { + uint32_t iif; // The input interface index + uint8_t dstMac[ETH_ALEN]; // destination ethernet mac address (zeroed iff rawip ingress) + uint8_t zero[2]; // zero pad for 8 byte alignment + // TODO: extend this to include src ip /64 subnet +} TetherUpstream6Key; +STRUCT_SIZE(TetherUpstream6Key, 12); + +#define TETHER_DOWNSTREAM4_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream4_rawip" +#define TETHER_DOWNSTREAM4_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream4_ether" + +#define TETHER_DOWNSTREAM4_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM4_TC_PROG_RAWIP_NAME +#define TETHER_DOWNSTREAM4_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM4_TC_PROG_ETHER_NAME + +#define TETHER_DOWNSTREAM4_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream4_map" + + +#define TETHER_UPSTREAM4_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_upstream4_rawip" +#define TETHER_UPSTREAM4_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_upstream4_ether" + +#define TETHER_UPSTREAM4_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM4_TC_PROG_RAWIP_NAME +#define TETHER_UPSTREAM4_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM4_TC_PROG_ETHER_NAME + +#define TETHER_UPSTREAM4_MAP_PATH BPF_PATH_TETHER "map_offload_tether_upstream4_map" + +typedef struct { + uint32_t iif; // The input interface index + uint8_t dstMac[ETH_ALEN]; // destination ethernet mac address (zeroed iff rawip ingress) + uint16_t l4Proto; // IPPROTO_TCP/UDP/... + struct in_addr src4; // source & + struct in_addr dst4; // destination IPv4 addresses + __be16 srcPort; // source & + __be16 dstPort; // destination TCP/UDP/... ports +} Tether4Key; +STRUCT_SIZE(Tether4Key, 4 + 6 + 2 + 4 + 4 + 2 + 2); // 24 + +typedef struct { + uint32_t oif; // The output interface to redirect to + struct ethhdr macHeader; // includes dst/src mac and ethertype (zeroed iff rawip egress) + uint16_t pmtu; // Maximum L3 output path/route mtu + struct in6_addr src46; // source & (always IPv4 mapped for downstream) + struct in6_addr dst46; // destination IP addresses (may be IPv4 mapped or IPv6 for upstream) + __be16 srcPort; // source & + __be16 dstPort; // destination tcp/udp/... ports + uint64_t last_used; // Kernel updates on each use with bpf_ktime_get_boot_ns() +} Tether4Value; +STRUCT_SIZE(Tether4Value, 4 + 14 + 2 + 16 + 16 + 2 + 2 + 8); // 64 + +#define TETHER_DOWNSTREAM_XDP_PROG_RAWIP_NAME "prog_offload_xdp_tether_downstream_rawip" +#define TETHER_DOWNSTREAM_XDP_PROG_ETHER_NAME "prog_offload_xdp_tether_downstream_ether" + +#define TETHER_DOWNSTREAM_XDP_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM_XDP_PROG_RAWIP_NAME +#define TETHER_DOWNSTREAM_XDP_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM_XDP_PROG_ETHER_NAME + +#define TETHER_UPSTREAM_XDP_PROG_RAWIP_NAME "prog_offload_xdp_tether_upstream_rawip" +#define TETHER_UPSTREAM_XDP_PROG_ETHER_NAME "prog_offload_xdp_tether_upstream_ether" + +#define TETHER_UPSTREAM_XDP_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM_XDP_PROG_RAWIP_NAME +#define TETHER_UPSTREAM_XDP_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM_XDP_PROG_ETHER_NAME + +#undef STRUCT_SIZE diff --git a/Tethering/bpf_progs/offload.c b/Tethering/bpf_progs/offload.c new file mode 100644 index 0000000000..6ff370c23e --- /dev/null +++ b/Tethering/bpf_progs/offload.c @@ -0,0 +1,838 @@ +/* + * 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. + */ + +#include +#include +#include +#include +#include + +// bionic kernel uapi linux/udp.h header is munged... +#define __kernel_udphdr udphdr +#include + +#include "bpf_helpers.h" +#include "bpf_net_helpers.h" +#include "bpf_tethering.h" + +// From kernel:include/net/ip.h +#define IP_DF 0x4000 // Flag: "Don't Fragment" + +// ----- Helper functions for offsets to fields ----- + +// They all assume simple IP packets: +// - no VLAN ethernet tags +// - no IPv4 options (see IPV4_HLEN/TCP4_OFFSET/UDP4_OFFSET) +// - no IPv6 extension headers +// - no TCP options (see TCP_HLEN) + +//#define ETH_HLEN sizeof(struct ethhdr) +#define IP4_HLEN sizeof(struct iphdr) +#define IP6_HLEN sizeof(struct ipv6hdr) +#define TCP_HLEN sizeof(struct tcphdr) +#define UDP_HLEN sizeof(struct udphdr) + +// Offsets from beginning of L4 (TCP/UDP) header +#define TCP_OFFSET(field) offsetof(struct tcphdr, field) +#define UDP_OFFSET(field) offsetof(struct udphdr, field) + +// Offsets from beginning of L3 (IPv4) header +#define IP4_OFFSET(field) offsetof(struct iphdr, field) +#define IP4_TCP_OFFSET(field) (IP4_HLEN + TCP_OFFSET(field)) +#define IP4_UDP_OFFSET(field) (IP4_HLEN + UDP_OFFSET(field)) + +// Offsets from beginning of L3 (IPv6) header +#define IP6_OFFSET(field) offsetof(struct ipv6hdr, field) +#define IP6_TCP_OFFSET(field) (IP6_HLEN + TCP_OFFSET(field)) +#define IP6_UDP_OFFSET(field) (IP6_HLEN + UDP_OFFSET(field)) + +// Offsets from beginning of L2 (ie. Ethernet) header (which must be present) +#define ETH_IP4_OFFSET(field) (ETH_HLEN + IP4_OFFSET(field)) +#define ETH_IP4_TCP_OFFSET(field) (ETH_HLEN + IP4_TCP_OFFSET(field)) +#define ETH_IP4_UDP_OFFSET(field) (ETH_HLEN + IP4_UDP_OFFSET(field)) +#define ETH_IP6_OFFSET(field) (ETH_HLEN + IP6_OFFSET(field)) +#define ETH_IP6_TCP_OFFSET(field) (ETH_HLEN + IP6_TCP_OFFSET(field)) +#define ETH_IP6_UDP_OFFSET(field) (ETH_HLEN + IP6_UDP_OFFSET(field)) + +// ----- Tethering Error Counters ----- + +DEFINE_BPF_MAP_GRW(tether_error_map, ARRAY, uint32_t, uint32_t, BPF_TETHER_ERR__MAX, + AID_NETWORK_STACK) + +#define COUNT_AND_RETURN(counter, ret) do { \ + uint32_t code = BPF_TETHER_ERR_ ## counter; \ + uint32_t *count = bpf_tether_error_map_lookup_elem(&code); \ + if (count) __sync_fetch_and_add(count, 1); \ + return ret; \ +} while(0) + +#define TC_DROP(counter) COUNT_AND_RETURN(counter, TC_ACT_SHOT) +#define TC_PUNT(counter) COUNT_AND_RETURN(counter, TC_ACT_OK) + +#define XDP_DROP(counter) COUNT_AND_RETURN(counter, XDP_DROP) +#define XDP_PUNT(counter) COUNT_AND_RETURN(counter, XDP_PASS) + +// ----- Tethering Data Stats and Limits ----- + +// Tethering stats, indexed by upstream interface. +DEFINE_BPF_MAP_GRW(tether_stats_map, HASH, TetherStatsKey, TetherStatsValue, 16, AID_NETWORK_STACK) + +// Tethering data limit, indexed by upstream interface. +// (tethering allowed when stats[iif].rxBytes + stats[iif].txBytes < limit[iif]) +DEFINE_BPF_MAP_GRW(tether_limit_map, HASH, TetherLimitKey, TetherLimitValue, 16, AID_NETWORK_STACK) + +// ----- IPv6 Support ----- + +DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 64, + AID_NETWORK_STACK) + +DEFINE_BPF_MAP_GRW(tether_downstream64_map, HASH, TetherDownstream64Key, TetherDownstream64Value, + 1024, AID_NETWORK_STACK) + +DEFINE_BPF_MAP_GRW(tether_upstream6_map, HASH, TetherUpstream6Key, Tether6Value, 64, + AID_NETWORK_STACK) + +static inline __always_inline int do_forward6(struct __sk_buff* skb, const bool is_ethernet, + const bool downstream) { + // Must be meta-ethernet IPv6 frame + if (skb->protocol != htons(ETH_P_IPV6)) return TC_ACT_OK; + + // Require ethernet dst mac address to be our unicast address. + if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_OK; + + const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0; + + // Since the program never writes via DPA (direct packet access) auto-pull/unclone logic does + // not trigger and thus we need to manually make sure we can read packet headers via DPA. + // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter. + // It has to be done early cause it will invalidate any skb->data/data_end derived pointers. + try_make_readable(skb, l2_header_size + IP6_HLEN + TCP_HLEN); + + void* data = (void*)(long)skb->data; + const void* data_end = (void*)(long)skb->data_end; + struct ethhdr* eth = is_ethernet ? data : NULL; // used iff is_ethernet + struct ipv6hdr* ip6 = is_ethernet ? (void*)(eth + 1) : data; + + // Must have (ethernet and) ipv6 header + if (data + l2_header_size + sizeof(*ip6) > data_end) return TC_ACT_OK; + + // Ethertype - if present - must be IPv6 + if (is_ethernet && (eth->h_proto != htons(ETH_P_IPV6))) return TC_ACT_OK; + + // IP version must be 6 + if (ip6->version != 6) TC_PUNT(INVALID_IP_VERSION); + + // Cannot decrement during forward if already zero or would be zero, + // Let the kernel's stack handle these cases and generate appropriate ICMP errors. + if (ip6->hop_limit <= 1) TC_PUNT(LOW_TTL); + + // If hardware offload is running and programming flows based on conntrack entries, + // try not to interfere with it. + if (ip6->nexthdr == IPPROTO_TCP) { + struct tcphdr* tcph = (void*)(ip6 + 1); + + // Make sure we can get at the tcp header + if (data + l2_header_size + sizeof(*ip6) + sizeof(*tcph) > data_end) + TC_PUNT(INVALID_TCP_HEADER); + + // Do not offload TCP packets with any one of the SYN/FIN/RST flags + if (tcph->syn || tcph->fin || tcph->rst) TC_PUNT(TCP_CONTROL_PACKET); + } + + // Protect against forwarding packets sourced from ::1 or fe80::/64 or other weirdness. + __be32 src32 = ip6->saddr.s6_addr32[0]; + if (src32 != htonl(0x0064ff9b) && // 64:ff9b:/32 incl. XLAT464 WKP + (src32 & htonl(0xe0000000)) != htonl(0x20000000)) // 2000::/3 Global Unicast + TC_PUNT(NON_GLOBAL_SRC); + + // Protect against forwarding packets destined to ::1 or fe80::/64 or other weirdness. + __be32 dst32 = ip6->daddr.s6_addr32[0]; + if (dst32 != htonl(0x0064ff9b) && // 64:ff9b:/32 incl. XLAT464 WKP + (dst32 & htonl(0xe0000000)) != htonl(0x20000000)) // 2000::/3 Global Unicast + TC_PUNT(NON_GLOBAL_DST); + + // In the upstream direction do not forward traffic within the same /64 subnet. + if (!downstream && (src32 == dst32) && (ip6->saddr.s6_addr32[1] == ip6->daddr.s6_addr32[1])) + TC_PUNT(LOCAL_SRC_DST); + + TetherDownstream6Key kd = { + .iif = skb->ifindex, + .neigh6 = ip6->daddr, + }; + + TetherUpstream6Key ku = { + .iif = skb->ifindex, + }; + if (is_ethernet) __builtin_memcpy(downstream ? kd.dstMac : ku.dstMac, eth->h_dest, ETH_ALEN); + + Tether6Value* v = downstream ? bpf_tether_downstream6_map_lookup_elem(&kd) + : bpf_tether_upstream6_map_lookup_elem(&ku); + + // If we don't find any offload information then simply let the core stack handle it... + if (!v) return TC_ACT_OK; + + uint32_t stat_and_limit_k = downstream ? skb->ifindex : v->oif; + + TetherStatsValue* stat_v = bpf_tether_stats_map_lookup_elem(&stat_and_limit_k); + + // If we don't have anywhere to put stats, then abort... + if (!stat_v) TC_PUNT(NO_STATS_ENTRY); + + uint64_t* limit_v = bpf_tether_limit_map_lookup_elem(&stat_and_limit_k); + + // If we don't have a limit, then abort... + if (!limit_v) TC_PUNT(NO_LIMIT_ENTRY); + + // Required IPv6 minimum mtu is 1280, below that not clear what we should do, abort... + if (v->pmtu < IPV6_MIN_MTU) TC_PUNT(BELOW_IPV6_MTU); + + // Approximate handling of TCP/IPv6 overhead for incoming LRO/GRO packets: default + // outbound path mtu of 1500 is not necessarily correct, but worst case we simply + // undercount, which is still better then not accounting for this overhead at all. + // Note: this really shouldn't be device/path mtu at all, but rather should be + // derived from this particular connection's mss (ie. from gro segment size). + // This would require a much newer kernel with newer ebpf accessors. + // (This is also blindly assuming 12 bytes of tcp timestamp option in tcp header) + uint64_t packets = 1; + uint64_t bytes = skb->len; + if (bytes > v->pmtu) { + const int tcp_overhead = sizeof(struct ipv6hdr) + sizeof(struct tcphdr) + 12; + const int mss = v->pmtu - tcp_overhead; + const uint64_t payload = bytes - tcp_overhead; + packets = (payload + mss - 1) / mss; + bytes = tcp_overhead * packets + payload; + } + + // Are we past the limit? If so, then abort... + // Note: will not overflow since u64 is 936 years even at 5Gbps. + // Do not drop here. Offload is just that, whenever we fail to handle + // a packet we let the core stack deal with things. + // (The core stack needs to handle limits correctly anyway, + // since we don't offload all traffic in both directions) + if (stat_v->rxBytes + stat_v->txBytes + bytes > *limit_v) TC_PUNT(LIMIT_REACHED); + + if (!is_ethernet) { + // Try to inject an ethernet header, and simply return if we fail. + // We do this even if TX interface is RAWIP and thus does not need an ethernet header, + // because this is easier and the kernel will strip extraneous ethernet header. + if (bpf_skb_change_head(skb, sizeof(struct ethhdr), /*flags*/ 0)) { + __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1); + TC_PUNT(CHANGE_HEAD_FAILED); + } + + // bpf_skb_change_head() invalidates all pointers - reload them + data = (void*)(long)skb->data; + data_end = (void*)(long)skb->data_end; + eth = data; + ip6 = (void*)(eth + 1); + + // I do not believe this can ever happen, but keep the verifier happy... + if (data + sizeof(struct ethhdr) + sizeof(*ip6) > data_end) { + __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1); + TC_DROP(TOO_SHORT); + } + }; + + // At this point we always have an ethernet header - which will get stripped by the + // kernel during transmit through a rawip interface. ie. 'eth' pointer is valid. + // Additionally note that 'is_ethernet' and 'l2_header_size' are no longer correct. + + // CHECKSUM_COMPLETE is a 16-bit one's complement sum, + // thus corrections for it need to be done in 16-byte chunks at even offsets. + // IPv6 nexthdr is at offset 6, while hop limit is at offset 7 + uint8_t old_hl = ip6->hop_limit; + --ip6->hop_limit; + uint8_t new_hl = ip6->hop_limit; + + // bpf_csum_update() always succeeds if the skb is CHECKSUM_COMPLETE and returns an error + // (-ENOTSUPP) if it isn't. + bpf_csum_update(skb, 0xFFFF - ntohs(old_hl) + ntohs(new_hl)); + + __sync_fetch_and_add(downstream ? &stat_v->rxPackets : &stat_v->txPackets, packets); + __sync_fetch_and_add(downstream ? &stat_v->rxBytes : &stat_v->txBytes, bytes); + + // Overwrite any mac header with the new one + // For a rawip tx interface it will simply be a bunch of zeroes and later stripped. + *eth = v->macHeader; + + // Redirect to forwarded interface. + // + // Note that bpf_redirect() cannot fail unless you pass invalid flags. + // The redirect actually happens after the ebpf program has already terminated, + // and can fail for example for mtu reasons at that point in time, but there's nothing + // we can do about it here. + return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */); +} + +DEFINE_BPF_PROG("schedcls/tether_downstream6_ether", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream6_ether) +(struct __sk_buff* skb) { + return do_forward6(skb, /* is_ethernet */ true, /* downstream */ true); +} + +DEFINE_BPF_PROG("schedcls/tether_upstream6_ether", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream6_ether) +(struct __sk_buff* skb) { + return do_forward6(skb, /* is_ethernet */ true, /* downstream */ false); +} + +// Note: section names must be unique to prevent programs from appending to each other, +// so instead the bpf loader will strip everything past the final $ symbol when actually +// pinning the program into the filesystem. +// +// bpf_skb_change_head() is only present on 4.14+ and 2 trivial kernel patches are needed: +// ANDROID: net: bpf: Allow TC programs to call BPF_FUNC_skb_change_head +// ANDROID: net: bpf: permit redirect from ingress L3 to egress L2 devices at near max mtu +// (the first of those has already been upstreamed) +// +// 5.4 kernel support was only added to Android Common Kernel in R, +// and thus a 5.4 kernel always supports this. +// +// Hence, these mandatory (must load successfully) implementations for 5.4+ kernels: +DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$5_4", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream6_rawip_5_4, KVER(5, 4, 0)) +(struct __sk_buff* skb) { + return do_forward6(skb, /* is_ethernet */ false, /* downstream */ true); +} + +DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$5_4", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream6_rawip_5_4, KVER(5, 4, 0)) +(struct __sk_buff* skb) { + return do_forward6(skb, /* is_ethernet */ false, /* downstream */ false); +} + +// and these identical optional (may fail to load) implementations for [4.14..5.4) patched kernels: +DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$4_14", + AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream6_rawip_4_14, + KVER(4, 14, 0), KVER(5, 4, 0)) +(struct __sk_buff* skb) { + return do_forward6(skb, /* is_ethernet */ false, /* downstream */ true); +} + +DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$4_14", + AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream6_rawip_4_14, + KVER(4, 14, 0), KVER(5, 4, 0)) +(struct __sk_buff* skb) { + return do_forward6(skb, /* is_ethernet */ false, /* downstream */ false); +} + +// and define no-op stubs for [4.9,4.14) and unpatched [4.14,5.4) kernels. +// (if the above real 4.14+ program loaded successfully, then bpfloader will have already pinned +// it at the same location this one would be pinned at and will thus skip loading this stub) +DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER(5, 4, 0)) +(struct __sk_buff* skb) { + return TC_ACT_OK; +} + +DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER(5, 4, 0)) +(struct __sk_buff* skb) { + return TC_ACT_OK; +} + +// ----- IPv4 Support ----- + +DEFINE_BPF_MAP_GRW(tether_downstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK) + +DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK) + +static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet, + const bool downstream, const bool updatetime) { + // Require ethernet dst mac address to be our unicast address. + if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_OK; + + // Must be meta-ethernet IPv4 frame + if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_OK; + + const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0; + + // Since the program never writes via DPA (direct packet access) auto-pull/unclone logic does + // not trigger and thus we need to manually make sure we can read packet headers via DPA. + // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter. + // It has to be done early cause it will invalidate any skb->data/data_end derived pointers. + try_make_readable(skb, l2_header_size + IP4_HLEN + TCP_HLEN); + + void* data = (void*)(long)skb->data; + const void* data_end = (void*)(long)skb->data_end; + struct ethhdr* eth = is_ethernet ? data : NULL; // used iff is_ethernet + struct iphdr* ip = is_ethernet ? (void*)(eth + 1) : data; + + // Must have (ethernet and) ipv4 header + if (data + l2_header_size + sizeof(*ip) > data_end) return TC_ACT_OK; + + // Ethertype - if present - must be IPv4 + if (is_ethernet && (eth->h_proto != htons(ETH_P_IP))) return TC_ACT_OK; + + // IP version must be 4 + if (ip->version != 4) TC_PUNT(INVALID_IP_VERSION); + + // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header + if (ip->ihl != 5) TC_PUNT(HAS_IP_OPTIONS); + + // Calculate the IPv4 one's complement checksum of the IPv4 header. + __wsum sum4 = 0; + for (int i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) { + sum4 += ((__u16*)ip)[i]; + } + // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4 + sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse u32 into range 1 .. 0x1FFFE + sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse any potential carry into u16 + // for a correct checksum we should get *a* zero, but sum4 must be positive, ie 0xFFFF + if (sum4 != 0xFFFF) TC_PUNT(CHECKSUM); + + // Minimum IPv4 total length is the size of the header + if (ntohs(ip->tot_len) < sizeof(*ip)) TC_PUNT(TRUNCATED_IPV4); + + // We are incapable of dealing with IPv4 fragments + if (ip->frag_off & ~htons(IP_DF)) TC_PUNT(IS_IP_FRAG); + + // Cannot decrement during forward if already zero or would be zero, + // Let the kernel's stack handle these cases and generate appropriate ICMP errors. + if (ip->ttl <= 1) TC_PUNT(LOW_TTL); + + // If we cannot update the 'last_used' field due to lack of bpf_ktime_get_boot_ns() helper, + // then it is not safe to offload UDP due to the small conntrack timeouts, as such, + // in such a situation we can only support TCP. This also has the added nice benefit of + // using a separate error counter, and thus making it obvious which version of the program + // is loaded. + if (!updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP); + + // We do not support offloading anything besides IPv4 TCP and UDP, due to need for NAT, + // but no need to check this if !updatetime due to check immediately above. + if (updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP)) + TC_PUNT(NON_TCP_UDP); + + // We want to make sure that the compiler will, in the !updatetime case, entirely optimize + // out all the non-tcp logic. Also note that at this point is_udp === !is_tcp. + const bool is_tcp = !updatetime || (ip->protocol == IPPROTO_TCP); + + // This is a bit of a hack to make things easier on the bpf verifier. + // (In particular I believe the Linux 4.14 kernel's verifier can get confused later on about + // what offsets into the packet are valid and can spuriously reject the program, this is + // because it fails to realize that is_tcp && !is_tcp is impossible) + // + // For both TCP & UDP we'll need to read and modify the src/dst ports, which so happen to + // always be in the first 4 bytes of the L4 header. Additionally for UDP we'll need access + // to the checksum field which is in bytes 7 and 8. While for TCP we'll need to read the + // TCP flags (at offset 13) and access to the checksum field (2 bytes at offset 16). + // As such we *always* need access to at least 8 bytes. + if (data + l2_header_size + sizeof(*ip) + 8 > data_end) TC_PUNT(SHORT_L4_HEADER); + + struct tcphdr* tcph = is_tcp ? (void*)(ip + 1) : NULL; + struct udphdr* udph = is_tcp ? NULL : (void*)(ip + 1); + + if (is_tcp) { + // Make sure we can get at the tcp header + if (data + l2_header_size + sizeof(*ip) + sizeof(*tcph) > data_end) + TC_PUNT(SHORT_TCP_HEADER); + + // If hardware offload is running and programming flows based on conntrack entries, try not + // to interfere with it, so do not offload TCP packets with any one of the SYN/FIN/RST flags + if (tcph->syn || tcph->fin || tcph->rst) TC_PUNT(TCP_CONTROL_PACKET); + } else { // UDP + // Make sure we can get at the udp header + if (data + l2_header_size + sizeof(*ip) + sizeof(*udph) > data_end) + TC_PUNT(SHORT_UDP_HEADER); + + // Skip handling of CHECKSUM_COMPLETE packets with udp checksum zero due to need for + // additional updating of skb->csum (this could be fixed up manually with more effort). + // + // Note that the in-kernel implementation of 'int64_t bpf_csum_update(skb, u32 csum)' is: + // if (skb->ip_summed == CHECKSUM_COMPLETE) + // return (skb->csum = csum_add(skb->csum, csum)); + // else + // return -ENOTSUPP; + // + // So this will punt any CHECKSUM_COMPLETE packet with a zero UDP checksum, + // and leave all other packets unaffected (since it just at most adds zero to skb->csum). + // + // In practice this should almost never trigger because most nics do not generate + // CHECKSUM_COMPLETE packets on receive - especially so for nics/drivers on a phone. + // + // Additionally since we're forwarding, in most cases the value of the skb->csum field + // shouldn't matter (it's not used by physical nic egress). + // + // It only matters if we're ingressing through a CHECKSUM_COMPLETE capable nic + // and egressing through a virtual interface looping back to the kernel itself + // (ie. something like veth) where the CHECKSUM_COMPLETE/skb->csum can get reused + // on ingress. + // + // If we were in the kernel we'd simply probably call + // void skb_checksum_complete_unset(struct sk_buff *skb) { + // if (skb->ip_summed == CHECKSUM_COMPLETE) skb->ip_summed = CHECKSUM_NONE; + // } + // here instead. Perhaps there should be a bpf helper for that? + if (!udph->check && (bpf_csum_update(skb, 0) >= 0)) TC_PUNT(UDP_CSUM_ZERO); + } + + Tether4Key k = { + .iif = skb->ifindex, + .l4Proto = ip->protocol, + .src4.s_addr = ip->saddr, + .dst4.s_addr = ip->daddr, + .srcPort = is_tcp ? tcph->source : udph->source, + .dstPort = is_tcp ? tcph->dest : udph->dest, + }; + if (is_ethernet) __builtin_memcpy(k.dstMac, eth->h_dest, ETH_ALEN); + + Tether4Value* v = downstream ? bpf_tether_downstream4_map_lookup_elem(&k) + : bpf_tether_upstream4_map_lookup_elem(&k); + + // If we don't find any offload information then simply let the core stack handle it... + if (!v) return TC_ACT_OK; + + uint32_t stat_and_limit_k = downstream ? skb->ifindex : v->oif; + + TetherStatsValue* stat_v = bpf_tether_stats_map_lookup_elem(&stat_and_limit_k); + + // If we don't have anywhere to put stats, then abort... + if (!stat_v) TC_PUNT(NO_STATS_ENTRY); + + uint64_t* limit_v = bpf_tether_limit_map_lookup_elem(&stat_and_limit_k); + + // If we don't have a limit, then abort... + if (!limit_v) TC_PUNT(NO_LIMIT_ENTRY); + + // Required IPv4 minimum mtu is 68, below that not clear what we should do, abort... + if (v->pmtu < 68) TC_PUNT(BELOW_IPV4_MTU); + + // Approximate handling of TCP/IPv4 overhead for incoming LRO/GRO packets: default + // outbound path mtu of 1500 is not necessarily correct, but worst case we simply + // undercount, which is still better then not accounting for this overhead at all. + // Note: this really shouldn't be device/path mtu at all, but rather should be + // derived from this particular connection's mss (ie. from gro segment size). + // This would require a much newer kernel with newer ebpf accessors. + // (This is also blindly assuming 12 bytes of tcp timestamp option in tcp header) + uint64_t packets = 1; + uint64_t bytes = skb->len; + if (bytes > v->pmtu) { + const int tcp_overhead = sizeof(struct iphdr) + sizeof(struct tcphdr) + 12; + const int mss = v->pmtu - tcp_overhead; + const uint64_t payload = bytes - tcp_overhead; + packets = (payload + mss - 1) / mss; + bytes = tcp_overhead * packets + payload; + } + + // Are we past the limit? If so, then abort... + // Note: will not overflow since u64 is 936 years even at 5Gbps. + // Do not drop here. Offload is just that, whenever we fail to handle + // a packet we let the core stack deal with things. + // (The core stack needs to handle limits correctly anyway, + // since we don't offload all traffic in both directions) + if (stat_v->rxBytes + stat_v->txBytes + bytes > *limit_v) TC_PUNT(LIMIT_REACHED); + + if (!is_ethernet) { + // Try to inject an ethernet header, and simply return if we fail. + // We do this even if TX interface is RAWIP and thus does not need an ethernet header, + // because this is easier and the kernel will strip extraneous ethernet header. + if (bpf_skb_change_head(skb, sizeof(struct ethhdr), /*flags*/ 0)) { + __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1); + TC_PUNT(CHANGE_HEAD_FAILED); + } + + // bpf_skb_change_head() invalidates all pointers - reload them + data = (void*)(long)skb->data; + data_end = (void*)(long)skb->data_end; + eth = data; + ip = (void*)(eth + 1); + tcph = is_tcp ? (void*)(ip + 1) : NULL; + udph = is_tcp ? NULL : (void*)(ip + 1); + + // I do not believe this can ever happen, but keep the verifier happy... + if (data + sizeof(struct ethhdr) + sizeof(*ip) + (is_tcp ? sizeof(*tcph) : sizeof(*udph)) > data_end) { + __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1); + TC_DROP(TOO_SHORT); + } + }; + + // At this point we always have an ethernet header - which will get stripped by the + // kernel during transmit through a rawip interface. ie. 'eth' pointer is valid. + // Additionally note that 'is_ethernet' and 'l2_header_size' are no longer correct. + + // Overwrite any mac header with the new one + // For a rawip tx interface it will simply be a bunch of zeroes and later stripped. + *eth = v->macHeader; + + const int l4_offs_csum = is_tcp ? ETH_IP4_TCP_OFFSET(check) : ETH_IP4_UDP_OFFSET(check); + const int sz4 = sizeof(__be32); + // UDP 0 is special and stored as FFFF (this flag also causes a csum of 0 to be unmodified) + const int l4_flags = is_tcp ? 0 : BPF_F_MARK_MANGLED_0; + const __be32 old_daddr = k.dst4.s_addr; + const __be32 old_saddr = k.src4.s_addr; + const __be32 new_daddr = v->dst46.s6_addr32[3]; + const __be32 new_saddr = v->src46.s6_addr32[3]; + + bpf_l4_csum_replace(skb, l4_offs_csum, old_daddr, new_daddr, sz4 | BPF_F_PSEUDO_HDR | l4_flags); + bpf_l3_csum_replace(skb, ETH_IP4_OFFSET(check), old_daddr, new_daddr, sz4); + bpf_skb_store_bytes(skb, ETH_IP4_OFFSET(daddr), &new_daddr, sz4, 0); + + bpf_l4_csum_replace(skb, l4_offs_csum, old_saddr, new_saddr, sz4 | BPF_F_PSEUDO_HDR | l4_flags); + bpf_l3_csum_replace(skb, ETH_IP4_OFFSET(check), old_saddr, new_saddr, sz4); + bpf_skb_store_bytes(skb, ETH_IP4_OFFSET(saddr), &new_saddr, sz4, 0); + + const int sz2 = sizeof(__be16); + // The offsets for TCP and UDP ports: source (u16 @ L4 offset 0) & dest (u16 @ L4 offset 2) are + // actually the same, so the compiler should just optimize them both down to a constant. + bpf_l4_csum_replace(skb, l4_offs_csum, k.srcPort, v->srcPort, sz2 | l4_flags); + bpf_skb_store_bytes(skb, is_tcp ? ETH_IP4_TCP_OFFSET(source) : ETH_IP4_UDP_OFFSET(source), + &v->srcPort, sz2, 0); + + bpf_l4_csum_replace(skb, l4_offs_csum, k.dstPort, v->dstPort, sz2 | l4_flags); + bpf_skb_store_bytes(skb, is_tcp ? ETH_IP4_TCP_OFFSET(dest) : ETH_IP4_UDP_OFFSET(dest), + &v->dstPort, sz2, 0); + + // TEMP HACK: lack of TTL decrement + + // This requires the bpf_ktime_get_boot_ns() helper which was added in 5.8, + // and backported to all Android Common Kernel 4.14+ trees. + if (updatetime) v->last_used = bpf_ktime_get_boot_ns(); + + __sync_fetch_and_add(downstream ? &stat_v->rxPackets : &stat_v->txPackets, packets); + __sync_fetch_and_add(downstream ? &stat_v->rxBytes : &stat_v->txBytes, bytes); + + // Redirect to forwarded interface. + // + // Note that bpf_redirect() cannot fail unless you pass invalid flags. + // The redirect actually happens after the ebpf program has already terminated, + // and can fail for example for mtu reasons at that point in time, but there's nothing + // we can do about it here. + return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */); +} + +// Full featured (required) implementations for 5.8+ kernels (these are S+ by definition) + +DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream4_rawip_5_8, KVER(5, 8, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ true); +} + +DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream4_rawip_5_8, KVER(5, 8, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ true); +} + +DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream4_ether_5_8, KVER(5, 8, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ true); +} + +DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream4_ether_5_8, KVER(5, 8, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ true); +} + +// Full featured (optional) implementations for 4.14-S, 4.19-S & 5.4-S kernels +// (optional, because we need to be able to fallback for 4.14/4.19/5.4 pre-S kernels) + +DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt", + AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream4_rawip_opt, + KVER(4, 14, 0), KVER(5, 8, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ true); +} + +DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt", + AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream4_rawip_opt, + KVER(4, 14, 0), KVER(5, 8, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ true); +} + +DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt", + AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream4_ether_opt, + KVER(4, 14, 0), KVER(5, 8, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ true); +} + +DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt", + AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream4_ether_opt, + KVER(4, 14, 0), KVER(5, 8, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ true); +} + +// Partial (TCP-only: will not update 'last_used' field) implementations for 4.14+ kernels. +// These will be loaded only if the above optional ones failed (loading of *these* must succeed +// for 5.4+, since that is always an R patched kernel). +// +// [Note: as a result TCP connections will not have their conntrack timeout refreshed, however, +// since /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established defaults to 432000 (seconds), +// this in practice means they'll break only after 5 days. This seems an acceptable trade-off. +// +// Additionally kernel/tests change "net-test: add bpf_ktime_get_ns / bpf_ktime_get_boot_ns tests" +// which enforces and documents the required kernel cherrypicks will make it pretty unlikely that +// many devices upgrading to S will end up relying on these fallback programs. + +// RAWIP: Required for 5.4-R kernels -- which always support bpf_skb_change_head(). + +DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ false); +} + +DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ false); +} + +// RAWIP: Optional for 4.14/4.19 (R) kernels -- which support bpf_skb_change_head(). +// [Note: fallback for 4.14/4.19 (P/Q) kernels is below in stub section] + +DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$4_14", + AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream4_rawip_4_14, + KVER(4, 14, 0), KVER(5, 4, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ false); +} + +DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$4_14", + AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream4_rawip_4_14, + KVER(4, 14, 0), KVER(5, 4, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ false); +} + +// ETHER: Required for 4.14-Q/R, 4.19-Q/R & 5.4-R kernels. + +DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ false); +} + +DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ false); +} + +// Placeholder (no-op) implementations for older Q kernels + +// RAWIP: 4.9-P/Q, 4.14-P/Q & 4.19-Q kernels -- without bpf_skb_change_head() for tc programs + +DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0)) +(struct __sk_buff* skb) { + return TC_ACT_OK; +} + +DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0)) +(struct __sk_buff* skb) { + return TC_ACT_OK; +} + +// ETHER: 4.9-P/Q kernel + +DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER(4, 14, 0)) +(struct __sk_buff* skb) { + return TC_ACT_OK; +} + +DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER(4, 14, 0)) +(struct __sk_buff* skb) { + return TC_ACT_OK; +} + +// ----- XDP Support ----- + +DEFINE_BPF_MAP_GRW(tether_dev_map, DEVMAP_HASH, uint32_t, uint32_t, 64, AID_NETWORK_STACK) + +static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const bool is_ethernet, + const bool downstream) { + return XDP_PASS; +} + +static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const bool is_ethernet, + const bool downstream) { + return XDP_PASS; +} + +static inline __always_inline int do_xdp_forward_ether(struct xdp_md *ctx, const bool downstream) { + const void* data = (void*)(long)ctx->data; + const void* data_end = (void*)(long)ctx->data_end; + const struct ethhdr* eth = data; + + // Make sure we actually have an ethernet header + if ((void*)(eth + 1) > data_end) return XDP_PASS; + + if (eth->h_proto == htons(ETH_P_IPV6)) + return do_xdp_forward6(ctx, /* is_ethernet */ true, downstream); + if (eth->h_proto == htons(ETH_P_IP)) + return do_xdp_forward4(ctx, /* is_ethernet */ true, downstream); + + // Anything else we don't know how to handle... + return XDP_PASS; +} + +static inline __always_inline int do_xdp_forward_rawip(struct xdp_md *ctx, const bool downstream) { + const void* data = (void*)(long)ctx->data; + const void* data_end = (void*)(long)ctx->data_end; + + // The top nibble of both IPv4 and IPv6 headers is the IP version. + if (data_end - data < 1) return XDP_PASS; + const uint8_t v = (*(uint8_t*)data) >> 4; + + if (v == 6) return do_xdp_forward6(ctx, /* is_ethernet */ false, downstream); + if (v == 4) return do_xdp_forward4(ctx, /* is_ethernet */ false, downstream); + + // Anything else we don't know how to handle... + return XDP_PASS; +} + +#define DEFINE_XDP_PROG(str, func) \ + DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER(5, 9, 0))(struct xdp_md *ctx) + +DEFINE_XDP_PROG("xdp/tether_downstream_ether", + xdp_tether_downstream_ether) { + return do_xdp_forward_ether(ctx, /* downstream */ true); +} + +DEFINE_XDP_PROG("xdp/tether_downstream_rawip", + xdp_tether_downstream_rawip) { + return do_xdp_forward_rawip(ctx, /* downstream */ true); +} + +DEFINE_XDP_PROG("xdp/tether_upstream_ether", + xdp_tether_upstream_ether) { + return do_xdp_forward_ether(ctx, /* downstream */ false); +} + +DEFINE_XDP_PROG("xdp/tether_upstream_rawip", + xdp_tether_upstream_rawip) { + return do_xdp_forward_rawip(ctx, /* downstream */ false); +} + +LICENSE("Apache 2.0"); +CRITICAL("tethering"); diff --git a/Tethering/bpf_progs/test.c b/Tethering/bpf_progs/test.c new file mode 100644 index 0000000000..3f0df2eae7 --- /dev/null +++ b/Tethering/bpf_progs/test.c @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "bpf_helpers.h" +#include "bpf_net_helpers.h" +#include "bpf_tethering.h" + +// Used only by TetheringPrivilegedTests, not by production code. +DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16, + AID_NETWORK_STACK) + +DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", AID_ROOT, AID_NETWORK_STACK, + xdp_test, KVER(5, 9, 0)) +(struct xdp_md *ctx) { + void *data = (void *)(long)ctx->data; + void *data_end = (void *)(long)ctx->data_end; + + struct ethhdr *eth = data; + int hsize = sizeof(*eth); + + struct iphdr *ip = data + hsize; + hsize += sizeof(struct iphdr); + + if (data + hsize > data_end) return XDP_PASS; + if (eth->h_proto != htons(ETH_P_IP)) return XDP_PASS; + if (ip->protocol == IPPROTO_UDP) return XDP_DROP; + return XDP_PASS; +} + +LICENSE("Apache 2.0"); diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp new file mode 100644 index 0000000000..fce436079b --- /dev/null +++ b/Tethering/common/TetheringLib/Android.bp @@ -0,0 +1,63 @@ +// +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_sdk_library { + name: "framework-tethering", + defaults: ["framework-module-defaults"], + impl_library_visibility: [ + "//packages/modules/Connectivity/Tethering:__subpackages__", + ], + + srcs: [":framework-tethering-srcs"], + libs: ["framework-connectivity.stubs.module_lib"], + stub_only_libs: ["framework-connectivity.stubs.module_lib"], + aidl: { + include_dirs: [ + "packages/modules/Connectivity/framework/aidl-export", + ], + }, + + jarjar_rules: "jarjar-rules.txt", + installable: true, + + hostdex: true, // for hiddenapi check + apex_available: ["com.android.tethering"], + permitted_packages: ["android.net"], + min_sdk_version: "30", +} + +filegroup { + name: "framework-tethering-srcs", + srcs: [ + "src/android/net/TetheredClient.aidl", + "src/android/net/TetheredClient.java", + "src/android/net/TetheringManager.java", + "src/android/net/TetheringConstants.java", + "src/android/net/IIntResultListener.aidl", + "src/android/net/ITetheringEventCallback.aidl", + "src/android/net/ITetheringConnector.aidl", + "src/android/net/TetheringCallbackStartedParcel.aidl", + "src/android/net/TetheringConfigurationParcel.aidl", + "src/android/net/TetheringRequestParcel.aidl", + "src/android/net/TetherStatesParcel.aidl", + "src/android/net/TetheringInterface.aidl", + "src/android/net/TetheringInterface.java", + ], + path: "src" +} diff --git a/Tethering/common/TetheringLib/api/current.txt b/Tethering/common/TetheringLib/api/current.txt new file mode 100644 index 0000000000..d802177e24 --- /dev/null +++ b/Tethering/common/TetheringLib/api/current.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/Tethering/common/TetheringLib/api/module-lib-current.txt b/Tethering/common/TetheringLib/api/module-lib-current.txt new file mode 100644 index 0000000000..0566040dd7 --- /dev/null +++ b/Tethering/common/TetheringLib/api/module-lib-current.txt @@ -0,0 +1,41 @@ +// Signature format: 2.0 +package android.net { + + public final class TetheringConstants { + field public static final String EXTRA_ADD_TETHER_TYPE = "extraAddTetherType"; + field public static final String EXTRA_PROVISION_CALLBACK = "extraProvisionCallback"; + field public static final String EXTRA_REM_TETHER_TYPE = "extraRemTetherType"; + field public static final String EXTRA_RUN_PROVISION = "extraRunProvision"; + field public static final String EXTRA_SET_ALARM = "extraSetAlarm"; + } + + public class TetheringManager { + ctor public TetheringManager(@NonNull android.content.Context, @NonNull java.util.function.Supplier); + method public int getLastTetherError(@NonNull String); + method @NonNull public String[] getTetherableBluetoothRegexs(); + method @NonNull public String[] getTetherableIfaces(); + method @NonNull public String[] getTetherableUsbRegexs(); + method @NonNull public String[] getTetherableWifiRegexs(); + method @NonNull public String[] getTetheredIfaces(); + method @NonNull public String[] getTetheringErroredIfaces(); + method public boolean isTetheringSupported(); + method public boolean isTetheringSupported(@NonNull String); + method public void requestLatestTetheringEntitlementResult(int, @NonNull android.os.ResultReceiver, boolean); + method @Deprecated public int setUsbTethering(boolean); + method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void startTethering(int, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback); + method @Deprecated public int tether(@NonNull String); + method @Deprecated public int untether(@NonNull String); + } + + public static interface TetheringManager.TetheringEventCallback { + method @Deprecated public default void onTetherableInterfaceRegexpsChanged(@NonNull android.net.TetheringManager.TetheringInterfaceRegexps); + } + + @Deprecated public static class TetheringManager.TetheringInterfaceRegexps { + method @Deprecated @NonNull public java.util.List getTetherableBluetoothRegexs(); + method @Deprecated @NonNull public java.util.List getTetherableUsbRegexs(); + method @Deprecated @NonNull public java.util.List getTetherableWifiRegexs(); + } + +} + diff --git a/Tethering/common/TetheringLib/api/module-lib-removed.txt b/Tethering/common/TetheringLib/api/module-lib-removed.txt new file mode 100644 index 0000000000..d802177e24 --- /dev/null +++ b/Tethering/common/TetheringLib/api/module-lib-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/Tethering/common/TetheringLib/api/removed.txt b/Tethering/common/TetheringLib/api/removed.txt new file mode 100644 index 0000000000..d802177e24 --- /dev/null +++ b/Tethering/common/TetheringLib/api/removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/Tethering/common/TetheringLib/api/system-current.txt b/Tethering/common/TetheringLib/api/system-current.txt new file mode 100644 index 0000000000..844ff6471a --- /dev/null +++ b/Tethering/common/TetheringLib/api/system-current.txt @@ -0,0 +1,117 @@ +// Signature format: 2.0 +package android.net { + + public final class TetheredClient implements android.os.Parcelable { + ctor public TetheredClient(@NonNull android.net.MacAddress, @NonNull java.util.Collection, int); + method public int describeContents(); + method @NonNull public java.util.List getAddresses(); + method @NonNull public android.net.MacAddress getMacAddress(); + method public int getTetheringType(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class TetheredClient.AddressInfo implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public android.net.LinkAddress getAddress(); + method @Nullable public String getHostname(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public final class TetheringInterface implements android.os.Parcelable { + ctor public TetheringInterface(int, @NonNull String); + method public int describeContents(); + method @NonNull public String getInterface(); + method public int getType(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public class TetheringManager { + method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerTetheringEventCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.TetheringEventCallback); + method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void requestLatestTetheringEntitlementResult(int, boolean, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.OnTetheringEntitlementResultListener); + method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void startTethering(@NonNull android.net.TetheringManager.TetheringRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.TetheringManager.StartTetheringCallback); + method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopAllTethering(); + method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.WRITE_SETTINGS}) public void stopTethering(int); + method @RequiresPermission(anyOf={android.Manifest.permission.TETHER_PRIVILEGED, android.Manifest.permission.ACCESS_NETWORK_STATE}) public void unregisterTetheringEventCallback(@NonNull android.net.TetheringManager.TetheringEventCallback); + field @Deprecated public static final String ACTION_TETHER_STATE_CHANGED = "android.net.conn.TETHER_STATE_CHANGED"; + field public static final int CONNECTIVITY_SCOPE_GLOBAL = 1; // 0x1 + field public static final int CONNECTIVITY_SCOPE_LOCAL = 2; // 0x2 + field public static final String EXTRA_ACTIVE_LOCAL_ONLY = "android.net.extra.ACTIVE_LOCAL_ONLY"; + field public static final String EXTRA_ACTIVE_TETHER = "tetherArray"; + field public static final String EXTRA_AVAILABLE_TETHER = "availableArray"; + field public static final String EXTRA_ERRORED_TETHER = "erroredArray"; + field public static final int TETHERING_BLUETOOTH = 2; // 0x2 + field public static final int TETHERING_ETHERNET = 5; // 0x5 + field public static final int TETHERING_INVALID = -1; // 0xffffffff + field public static final int TETHERING_NCM = 4; // 0x4 + field public static final int TETHERING_USB = 1; // 0x1 + field public static final int TETHERING_WIFI = 0; // 0x0 + field public static final int TETHERING_WIFI_P2P = 3; // 0x3 + field public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12; // 0xc + field public static final int TETHER_ERROR_DISABLE_FORWARDING_ERROR = 9; // 0x9 + field public static final int TETHER_ERROR_ENABLE_FORWARDING_ERROR = 8; // 0x8 + field public static final int TETHER_ERROR_ENTITLEMENT_UNKNOWN = 13; // 0xd + field public static final int TETHER_ERROR_IFACE_CFG_ERROR = 10; // 0xa + field public static final int TETHER_ERROR_INTERNAL_ERROR = 5; // 0x5 + field public static final int TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION = 15; // 0xf + field public static final int TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14; // 0xe + field public static final int TETHER_ERROR_NO_ERROR = 0; // 0x0 + field public static final int TETHER_ERROR_PROVISIONING_FAILED = 11; // 0xb + field public static final int TETHER_ERROR_SERVICE_UNAVAIL = 2; // 0x2 + field public static final int TETHER_ERROR_TETHER_IFACE_ERROR = 6; // 0x6 + field public static final int TETHER_ERROR_UNAVAIL_IFACE = 4; // 0x4 + field public static final int TETHER_ERROR_UNKNOWN_IFACE = 1; // 0x1 + field public static final int TETHER_ERROR_UNKNOWN_TYPE = 16; // 0x10 + field public static final int TETHER_ERROR_UNSUPPORTED = 3; // 0x3 + field public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = 7; // 0x7 + field public static final int TETHER_HARDWARE_OFFLOAD_FAILED = 2; // 0x2 + field public static final int TETHER_HARDWARE_OFFLOAD_STARTED = 1; // 0x1 + field public static final int TETHER_HARDWARE_OFFLOAD_STOPPED = 0; // 0x0 + } + + public static interface TetheringManager.OnTetheringEntitlementResultListener { + method public void onTetheringEntitlementResult(int); + } + + public static interface TetheringManager.StartTetheringCallback { + method public default void onTetheringFailed(int); + method public default void onTetheringStarted(); + } + + public static interface TetheringManager.TetheringEventCallback { + method public default void onClientsChanged(@NonNull java.util.Collection); + method public default void onError(@NonNull String, int); + method public default void onError(@NonNull android.net.TetheringInterface, int); + method public default void onLocalOnlyInterfacesChanged(@NonNull java.util.List); + method public default void onLocalOnlyInterfacesChanged(@NonNull java.util.Set); + method public default void onOffloadStatusChanged(int); + method public default void onTetherableInterfacesChanged(@NonNull java.util.List); + method public default void onTetherableInterfacesChanged(@NonNull java.util.Set); + method public default void onTetheredInterfacesChanged(@NonNull java.util.List); + method public default void onTetheredInterfacesChanged(@NonNull java.util.Set); + method public default void onTetheringSupported(boolean); + method public default void onUpstreamChanged(@Nullable android.net.Network); + } + + public static class TetheringManager.TetheringRequest { + method @Nullable public android.net.LinkAddress getClientStaticIpv4Address(); + method public int getConnectivityScope(); + method @Nullable public android.net.LinkAddress getLocalIpv4Address(); + method public boolean getShouldShowEntitlementUi(); + method public int getTetheringType(); + method public boolean isExemptFromEntitlementCheck(); + } + + public static class TetheringManager.TetheringRequest.Builder { + ctor public TetheringManager.TetheringRequest.Builder(int); + method @NonNull public android.net.TetheringManager.TetheringRequest build(); + method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setConnectivityScope(int); + method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setExemptFromEntitlementCheck(boolean); + method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setShouldShowEntitlementUi(boolean); + method @NonNull @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) public android.net.TetheringManager.TetheringRequest.Builder setStaticIpv4Addresses(@NonNull android.net.LinkAddress, @NonNull android.net.LinkAddress); + } + +} + diff --git a/Tethering/common/TetheringLib/api/system-removed.txt b/Tethering/common/TetheringLib/api/system-removed.txt new file mode 100644 index 0000000000..d802177e24 --- /dev/null +++ b/Tethering/common/TetheringLib/api/system-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/Tethering/common/TetheringLib/jarjar-rules.txt b/Tethering/common/TetheringLib/jarjar-rules.txt new file mode 100644 index 0000000000..e459fad549 --- /dev/null +++ b/Tethering/common/TetheringLib/jarjar-rules.txt @@ -0,0 +1 @@ +# jarjar rules for the bootclasspath tethering framework library here \ No newline at end of file diff --git a/Tethering/common/TetheringLib/src/android/net/IIntResultListener.aidl b/Tethering/common/TetheringLib/src/android/net/IIntResultListener.aidl new file mode 100644 index 0000000000..c3d66ee145 --- /dev/null +++ b/Tethering/common/TetheringLib/src/android/net/IIntResultListener.aidl @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +/** + * Listener interface allowing objects to listen to various module event. + * {@hide} + */ +oneway interface IIntResultListener { + void onResult(int resultCode); +} diff --git a/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl b/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl new file mode 100644 index 0000000000..cf094aac2c --- /dev/null +++ b/Tethering/common/TetheringLib/src/android/net/ITetheringConnector.aidl @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net; + +import android.net.IIntResultListener; +import android.net.ITetheringEventCallback; +import android.net.TetheringRequestParcel; +import android.os.ResultReceiver; + +/** @hide */ +oneway interface ITetheringConnector { + void tether(String iface, String callerPkg, String callingAttributionTag, + IIntResultListener receiver); + + void untether(String iface, String callerPkg, String callingAttributionTag, + IIntResultListener receiver); + + void setUsbTethering(boolean enable, String callerPkg, + String callingAttributionTag, IIntResultListener receiver); + + void startTethering(in TetheringRequestParcel request, String callerPkg, + String callingAttributionTag, IIntResultListener receiver); + + void stopTethering(int type, String callerPkg, String callingAttributionTag, + IIntResultListener receiver); + + void requestLatestTetheringEntitlementResult(int type, in ResultReceiver receiver, + boolean showEntitlementUi, String callerPkg, String callingAttributionTag); + + void registerTetheringEventCallback(ITetheringEventCallback callback, String callerPkg); + + void unregisterTetheringEventCallback(ITetheringEventCallback callback, String callerPkg); + + void isTetheringSupported(String callerPkg, String callingAttributionTag, + IIntResultListener receiver); + + void stopAllTethering(String callerPkg, String callingAttributionTag, + IIntResultListener receiver); +} diff --git a/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl b/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl new file mode 100644 index 0000000000..b4e3ba4679 --- /dev/null +++ b/Tethering/common/TetheringLib/src/android/net/ITetheringEventCallback.aidl @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.net.Network; +import android.net.TetheredClient; +import android.net.TetheringConfigurationParcel; +import android.net.TetheringCallbackStartedParcel; +import android.net.TetherStatesParcel; + +/** + * Callback class for receiving tethering changed events. + * @hide + */ +oneway interface ITetheringEventCallback +{ + /** Called immediately after the callbacks are registered */ + void onCallbackStarted(in TetheringCallbackStartedParcel parcel); + void onCallbackStopped(int errorCode); + void onUpstreamChanged(in Network network); + void onConfigurationChanged(in TetheringConfigurationParcel config); + void onTetherStatesChanged(in TetherStatesParcel states); + void onTetherClientsChanged(in List clients); + void onOffloadStatusChanged(int status); +} diff --git a/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl new file mode 100644 index 0000000000..43262fb9d1 --- /dev/null +++ b/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.net.TetheringInterface; + +/** + * Status details for tethering downstream interfaces. + * {@hide} + */ +parcelable TetherStatesParcel { + TetheringInterface[] availableList; + TetheringInterface[] tetheredList; + TetheringInterface[] localOnlyList; + TetheringInterface[] erroredIfaceList; + // List of Last error code corresponding to each errored iface in erroredIfaceList. */ + // TODO: Improve this as b/143122247. + int[] lastErrorList; +} diff --git a/Tethering/common/TetheringLib/src/android/net/TetheredClient.aidl b/Tethering/common/TetheringLib/src/android/net/TetheredClient.aidl new file mode 100644 index 0000000000..0b279b8823 --- /dev/null +++ b/Tethering/common/TetheringLib/src/android/net/TetheredClient.aidl @@ -0,0 +1,18 @@ +/** + * 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; + +@JavaOnlyStableParcelable parcelable TetheredClient; \ No newline at end of file diff --git a/Tethering/common/TetheringLib/src/android/net/TetheredClient.java b/Tethering/common/TetheringLib/src/android/net/TetheredClient.java new file mode 100644 index 0000000000..0b223f42b9 --- /dev/null +++ b/Tethering/common/TetheringLib/src/android/net/TetheredClient.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; + +/** + * Information on a tethered downstream client. + * @hide + */ +@SystemApi +public final class TetheredClient implements Parcelable { + @NonNull + private final MacAddress mMacAddress; + @NonNull + private final List mAddresses; + // TODO: use an @IntDef here + private final int mTetheringType; + + public TetheredClient(@NonNull MacAddress macAddress, + @NonNull Collection addresses, int tetheringType) { + mMacAddress = macAddress; + mAddresses = new ArrayList<>(addresses); + mTetheringType = tetheringType; + } + + private TetheredClient(@NonNull Parcel in) { + this(in.readParcelable(null), in.createTypedArrayList(AddressInfo.CREATOR), in.readInt()); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(mMacAddress, flags); + dest.writeTypedList(mAddresses); + dest.writeInt(mTetheringType); + } + + /** + * Get the MAC address used to identify the client. + */ + @NonNull + public MacAddress getMacAddress() { + return mMacAddress; + } + + /** + * Get information on the list of addresses that are associated with the client. + */ + @NonNull + public List getAddresses() { + return new ArrayList<>(mAddresses); + } + + /** + * Get the type of tethering used by the client. + * @return one of the {@code TetheringManager#TETHERING_*} constants. + */ + public int getTetheringType() { + return mTetheringType; + } + + /** + * Return a new {@link TetheredClient} that has all the attributes of this instance, plus the + * {@link AddressInfo} of the provided {@link TetheredClient}. + * + *

Duplicate addresses are removed. + * @hide + */ + public TetheredClient addAddresses(@NonNull TetheredClient other) { + final LinkedHashSet newAddresses = new LinkedHashSet<>( + mAddresses.size() + other.mAddresses.size()); + newAddresses.addAll(mAddresses); + newAddresses.addAll(other.mAddresses); + return new TetheredClient(mMacAddress, newAddresses, mTetheringType); + } + + @Override + public int hashCode() { + return Objects.hash(mMacAddress, mAddresses, mTetheringType); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof TetheredClient)) return false; + final TetheredClient other = (TetheredClient) obj; + return mMacAddress.equals(other.mMacAddress) + && mAddresses.equals(other.mAddresses) + && mTetheringType == other.mTetheringType; + } + + /** + * Information on an lease assigned to a tethered client. + */ + public static final class AddressInfo implements Parcelable { + @NonNull + private final LinkAddress mAddress; + @Nullable + private final String mHostname; + + /** @hide */ + public AddressInfo(@NonNull LinkAddress address, @Nullable String hostname) { + this.mAddress = address; + this.mHostname = hostname; + } + + private AddressInfo(Parcel in) { + this(in.readParcelable(null), in.readString()); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(mAddress, flags); + dest.writeString(mHostname); + } + + /** + * Get the link address (including prefix length and lifetime) used by the client. + * + * This may be an IPv4 or IPv6 address. + */ + @NonNull + public LinkAddress getAddress() { + return mAddress; + } + + /** + * Get the hostname that was advertised by the client when obtaining its address, if any. + */ + @Nullable + public String getHostname() { + return mHostname; + } + + /** + * Get the expiration time of the address assigned to the client. + * @hide + */ + public long getExpirationTime() { + return mAddress.getExpirationTime(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public int hashCode() { + return Objects.hash(mAddress, mHostname); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof AddressInfo)) return false; + final AddressInfo other = (AddressInfo) obj; + // Use .equals() for addresses as all changes, including address expiry changes, + // should be included. + return other.mAddress.equals(mAddress) + && Objects.equals(mHostname, other.mHostname); + } + + @NonNull + public static final Creator CREATOR = new Creator() { + @NonNull + @Override + public AddressInfo createFromParcel(@NonNull Parcel in) { + return new AddressInfo(in); + } + + @NonNull + @Override + public AddressInfo[] newArray(int size) { + return new AddressInfo[size]; + } + }; + + @NonNull + @Override + public String toString() { + return "AddressInfo {" + + mAddress + + (mHostname != null ? ", hostname " + mHostname : "") + + "}"; + } + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Creator CREATOR = new Creator() { + @NonNull + @Override + public TetheredClient createFromParcel(@NonNull Parcel in) { + return new TetheredClient(in); + } + + @NonNull + @Override + public TetheredClient[] newArray(int size) { + return new TetheredClient[size]; + } + }; + + @NonNull + @Override + public String toString() { + return "TetheredClient {hwAddr " + mMacAddress + + ", addresses " + mAddresses + + ", tetheringType " + mTetheringType + + "}"; + } +} diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl new file mode 100644 index 0000000000..253eacbd23 --- /dev/null +++ b/Tethering/common/TetheringLib/src/android/net/TetheringCallbackStartedParcel.aidl @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.net.Network; +import android.net.TetheredClient; +import android.net.TetheringConfigurationParcel; +import android.net.TetherStatesParcel; + +/** + * Initial information reported by tethering upon callback registration. + * @hide + */ +parcelable TetheringCallbackStartedParcel { + boolean tetheringSupported; + Network upstreamNetwork; + TetheringConfigurationParcel config; + TetherStatesParcel states; + List tetheredClients; + int offloadStatus; +} diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringConfigurationParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringConfigurationParcel.aidl new file mode 100644 index 0000000000..89f38132ff --- /dev/null +++ b/Tethering/common/TetheringLib/src/android/net/TetheringConfigurationParcel.aidl @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * Configuration details for tethering. + * @hide + */ +parcelable TetheringConfigurationParcel { + int subId; + String[] tetherableUsbRegexs; + String[] tetherableWifiRegexs; + String[] tetherableBluetoothRegexs; + boolean isDunRequired; + boolean chooseUpstreamAutomatically; + int[] preferredUpstreamIfaceTypes; + String[] legacyDhcpRanges; + String[] defaultIPv4DNS; + boolean enableLegacyDhcpServer; + String[] provisioningApp; + String provisioningAppNoUi; + int provisioningCheckPeriod; +} diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringConstants.java b/Tethering/common/TetheringLib/src/android/net/TetheringConstants.java new file mode 100644 index 0000000000..f14def6a3a --- /dev/null +++ b/Tethering/common/TetheringLib/src/android/net/TetheringConstants.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; + +import android.annotation.SystemApi; +import android.os.ResultReceiver; + +/** + * Collections of constants for internal tethering usage. + * + *

These hidden constants are not in TetheringManager as they are not part of the API stubs + * generated for TetheringManager, which prevents the tethering module from linking them at + * build time. + * TODO: investigate changing the tethering build rules so that Tethering can reference hidden + * symbols from framework-tethering even when they are in a non-hidden class. + * @hide + */ +@SystemApi(client = MODULE_LIBRARIES) +public final class TetheringConstants { + /** An explicit private class to avoid exposing constructor.*/ + private TetheringConstants() { } + + /** + * Extra used for communicating with the TetherService and TetherProvisioningActivity. + * Includes the type of tethering to enable if any. + */ + public static final String EXTRA_ADD_TETHER_TYPE = "extraAddTetherType"; + /** + * Extra used for communicating with the TetherService. Includes the type of tethering for + * which to cancel provisioning. + */ + public static final String EXTRA_REM_TETHER_TYPE = "extraRemTetherType"; + /** + * Extra used for communicating with the TetherService. True to schedule a recheck of tether + * provisioning. + */ + public static final String EXTRA_SET_ALARM = "extraSetAlarm"; + /** + * Tells the TetherService to run a provision check now. + */ + public static final String EXTRA_RUN_PROVISION = "extraRunProvision"; + /** + * Extra used for communicating with the TetherService and TetherProvisioningActivity. + * Contains the {@link ResultReceiver} which will receive provisioning results. + * Can not be empty. + */ + public static final String EXTRA_PROVISION_CALLBACK = "extraProvisionCallback"; + + /** + * Extra used for communicating with the TetherService and TetherProvisioningActivity. + * Contains the subId of current active cellular upstream. + * @hide + */ + public static final String EXTRA_TETHER_SUBID = "android.net.extra.TETHER_SUBID"; + + /** + * Extra used for telling TetherProvisioningActivity the entitlement package name and class + * name to start UI entitlement check. + * @hide + */ + public static final String EXTRA_TETHER_UI_PROVISIONING_APP_NAME = + "android.net.extra.TETHER_UI_PROVISIONING_APP_NAME"; + + /** + * Extra used for telling TetherService the intent action to start silent entitlement check. + * @hide + */ + public static final String EXTRA_TETHER_SILENT_PROVISIONING_ACTION = + "android.net.extra.TETHER_SILENT_PROVISIONING_ACTION"; + + /** + * Extra used for TetherService to receive the response of provisioning check. + * @hide + */ + public static final String EXTRA_TETHER_PROVISIONING_RESPONSE = + "android.net.extra.TETHER_PROVISIONING_RESPONSE"; +} diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringInterface.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.aidl new file mode 100644 index 0000000000..715198447f --- /dev/null +++ b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.aidl @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net; + +@JavaOnlyStableParcelable parcelable TetheringInterface; diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java new file mode 100644 index 0000000000..84cdef1163 --- /dev/null +++ b/Tethering/common/TetheringLib/src/android/net/TetheringInterface.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.net.TetheringManager.TetheringType; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * The mapping of tethering interface and type. + * @hide + */ +@SystemApi +public final class TetheringInterface implements Parcelable { + private final int mType; + private final String mInterface; + + public TetheringInterface(@TetheringType int type, @NonNull String iface) { + Objects.requireNonNull(iface); + mType = type; + mInterface = iface; + } + + private TetheringInterface(@NonNull Parcel in) { + this(in.readInt(), in.readString()); + } + + /** Get tethering type. */ + public int getType() { + return mType; + } + + /** Get tethering interface. */ + @NonNull + public String getInterface() { + return mInterface; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mType); + dest.writeString(mInterface); + } + + @Override + public int hashCode() { + return Objects.hash(mType, mInterface); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof TetheringInterface)) return false; + final TetheringInterface other = (TetheringInterface) obj; + return mType == other.mType && mInterface.equals(other.mInterface); + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Creator CREATOR = new Creator() { + @NonNull + @Override + public TetheringInterface createFromParcel(@NonNull Parcel in) { + return new TetheringInterface(in); + } + + @NonNull + @Override + public TetheringInterface[] newArray(int size) { + return new TetheringInterface[size]; + } + }; + + @NonNull + @Override + public String toString() { + return "TetheringInterface {mType=" + mType + + ", mInterface=" + mInterface + "}"; + } +} diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java new file mode 100644 index 0000000000..edd141d383 --- /dev/null +++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java @@ -0,0 +1,1541 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; + +import android.Manifest; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.content.Context; +import android.os.Bundle; +import android.os.ConditionVariable; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +/** + * This class provides the APIs to control the tethering service. + *

The primary responsibilities of this class are to provide the APIs for applications to + * start tethering, stop tethering, query configuration and query status. + * + * @hide + */ +@SystemApi +public class TetheringManager { + private static final String TAG = TetheringManager.class.getSimpleName(); + private static final int DEFAULT_TIMEOUT_MS = 60_000; + private static final long CONNECTOR_POLL_INTERVAL_MILLIS = 200L; + + @GuardedBy("mConnectorWaitQueue") + @Nullable + private ITetheringConnector mConnector; + @GuardedBy("mConnectorWaitQueue") + @NonNull + private final List mConnectorWaitQueue = new ArrayList<>(); + private final Supplier mConnectorSupplier; + + private final TetheringCallbackInternal mCallback; + private final Context mContext; + private final ArrayMap + mTetheringEventCallbacks = new ArrayMap<>(); + + private volatile TetheringConfigurationParcel mTetheringConfiguration; + private volatile TetherStatesParcel mTetherStatesParcel; + + /** + * Broadcast Action: A tetherable connection has come or gone. + * Uses {@code TetheringManager.EXTRA_AVAILABLE_TETHER}, + * {@code TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY}, + * {@code TetheringManager.EXTRA_ACTIVE_TETHER}, and + * {@code TetheringManager.EXTRA_ERRORED_TETHER} to indicate + * the current state of tethering. Each include a list of + * interface names in that state (may be empty). + * + * @deprecated New client should use TetheringEventCallback instead. + */ + @Deprecated + public static final String ACTION_TETHER_STATE_CHANGED = + "android.net.conn.TETHER_STATE_CHANGED"; + + /** + * gives a String[] listing all the interfaces configured for + * tethering and currently available for tethering. + */ + public static final String EXTRA_AVAILABLE_TETHER = "availableArray"; + + /** + * gives a String[] listing all the interfaces currently in local-only + * mode (ie, has DHCPv4+IPv6-ULA support and no packet forwarding) + */ + public static final String EXTRA_ACTIVE_LOCAL_ONLY = "android.net.extra.ACTIVE_LOCAL_ONLY"; + + /** + * gives a String[] listing all the interfaces currently tethered + * (ie, has DHCPv4 support and packets potentially forwarded/NATed) + */ + public static final String EXTRA_ACTIVE_TETHER = "tetherArray"; + + /** + * gives a String[] listing all the interfaces we tried to tether and + * failed. Use {@link #getLastTetherError} to find the error code + * for any interfaces listed here. + */ + public static final String EXTRA_ERRORED_TETHER = "erroredArray"; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = false, value = { + TETHERING_WIFI, + TETHERING_USB, + TETHERING_BLUETOOTH, + TETHERING_WIFI_P2P, + TETHERING_NCM, + TETHERING_ETHERNET, + }) + public @interface TetheringType { + } + + /** + * Invalid tethering type. + * @see #startTethering. + */ + public static final int TETHERING_INVALID = -1; + + /** + * Wifi tethering type. + * @see #startTethering. + */ + public static final int TETHERING_WIFI = 0; + + /** + * USB tethering type. + * @see #startTethering. + */ + public static final int TETHERING_USB = 1; + + /** + * Bluetooth tethering type. + * @see #startTethering. + */ + public static final int TETHERING_BLUETOOTH = 2; + + /** + * Wifi P2p tethering type. + * Wifi P2p tethering is set through events automatically, and don't + * need to start from #startTethering. + */ + public static final int TETHERING_WIFI_P2P = 3; + + /** + * Ncm local tethering type. + * @see #startTethering(TetheringRequest, Executor, StartTetheringCallback) + */ + public static final int TETHERING_NCM = 4; + + /** + * Ethernet tethering type. + * @see #startTethering(TetheringRequest, Executor, StartTetheringCallback) + */ + public static final int TETHERING_ETHERNET = 5; + + /** + * WIGIG tethering type. Use a separate type to prevent + * conflicts with TETHERING_WIFI + * This type is only used internally by the tethering module + * @hide + */ + public static final int TETHERING_WIGIG = 6; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + TETHER_ERROR_NO_ERROR, + TETHER_ERROR_PROVISIONING_FAILED, + TETHER_ERROR_ENTITLEMENT_UNKNOWN, + }) + public @interface EntitlementResult { + } + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + TETHER_ERROR_NO_ERROR, + TETHER_ERROR_UNKNOWN_IFACE, + TETHER_ERROR_SERVICE_UNAVAIL, + TETHER_ERROR_INTERNAL_ERROR, + TETHER_ERROR_TETHER_IFACE_ERROR, + TETHER_ERROR_ENABLE_FORWARDING_ERROR, + TETHER_ERROR_DISABLE_FORWARDING_ERROR, + TETHER_ERROR_IFACE_CFG_ERROR, + TETHER_ERROR_DHCPSERVER_ERROR, + }) + public @interface TetheringIfaceError { + } + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + TETHER_ERROR_SERVICE_UNAVAIL, + TETHER_ERROR_INTERNAL_ERROR, + TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION, + TETHER_ERROR_UNKNOWN_TYPE, + }) + public @interface StartTetheringError { + } + + public static final int TETHER_ERROR_NO_ERROR = 0; + public static final int TETHER_ERROR_UNKNOWN_IFACE = 1; + public static final int TETHER_ERROR_SERVICE_UNAVAIL = 2; + public static final int TETHER_ERROR_UNSUPPORTED = 3; + public static final int TETHER_ERROR_UNAVAIL_IFACE = 4; + public static final int TETHER_ERROR_INTERNAL_ERROR = 5; + public static final int TETHER_ERROR_TETHER_IFACE_ERROR = 6; + public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = 7; + public static final int TETHER_ERROR_ENABLE_FORWARDING_ERROR = 8; + public static final int TETHER_ERROR_DISABLE_FORWARDING_ERROR = 9; + public static final int TETHER_ERROR_IFACE_CFG_ERROR = 10; + public static final int TETHER_ERROR_PROVISIONING_FAILED = 11; + public static final int TETHER_ERROR_DHCPSERVER_ERROR = 12; + public static final int TETHER_ERROR_ENTITLEMENT_UNKNOWN = 13; + public static final int TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION = 14; + public static final int TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION = 15; + public static final int TETHER_ERROR_UNKNOWN_TYPE = 16; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = false, value = { + TETHER_HARDWARE_OFFLOAD_STOPPED, + TETHER_HARDWARE_OFFLOAD_STARTED, + TETHER_HARDWARE_OFFLOAD_FAILED, + }) + public @interface TetherOffloadStatus { + } + + /** Tethering offload status is stopped. */ + public static final int TETHER_HARDWARE_OFFLOAD_STOPPED = 0; + /** Tethering offload status is started. */ + public static final int TETHER_HARDWARE_OFFLOAD_STARTED = 1; + /** Fail to start tethering offload. */ + public static final int TETHER_HARDWARE_OFFLOAD_FAILED = 2; + + /** + * Create a TetheringManager object for interacting with the tethering service. + * + * @param context Context for the manager. + * @param connectorSupplier Supplier for the manager connector; may return null while the + * service is not connected. + * {@hide} + */ + @SystemApi(client = MODULE_LIBRARIES) + public TetheringManager(@NonNull final Context context, + @NonNull Supplier connectorSupplier) { + mContext = context; + mCallback = new TetheringCallbackInternal(); + mConnectorSupplier = connectorSupplier; + + final String pkgName = mContext.getOpPackageName(); + + final IBinder connector = mConnectorSupplier.get(); + // If the connector is available on start, do not start a polling thread. This introduces + // differences in the thread that sends the oneway binder calls to the service between the + // first few seconds after boot and later, but it avoids always having differences between + // the first usage of TetheringManager from a process and subsequent usages (so the + // difference is only on boot). On boot binder calls may be queued until the service comes + // up and be sent from a worker thread; later, they are always sent from the caller thread. + // Considering that it's just oneway binder calls, and ordering is preserved, this seems + // better than inconsistent behavior persisting after boot. + if (connector != null) { + mConnector = ITetheringConnector.Stub.asInterface(connector); + } else { + startPollingForConnector(); + } + + Log.i(TAG, "registerTetheringEventCallback:" + pkgName); + getConnector(c -> c.registerTetheringEventCallback(mCallback, pkgName)); + } + + private void startPollingForConnector() { + new Thread(() -> { + while (true) { + try { + Thread.sleep(CONNECTOR_POLL_INTERVAL_MILLIS); + } catch (InterruptedException e) { + // Not much to do here, the system needs to wait for the connector + } + + final IBinder connector = mConnectorSupplier.get(); + if (connector != null) { + onTetheringConnected(ITetheringConnector.Stub.asInterface(connector)); + return; + } + } + }).start(); + } + + private interface ConnectorConsumer { + void onConnectorAvailable(ITetheringConnector connector) throws RemoteException; + } + + private void onTetheringConnected(ITetheringConnector connector) { + // Process the connector wait queue in order, including any items that are added + // while processing. + // + // 1. Copy the queue to a local variable under lock. + // 2. Drain the local queue with the lock released (otherwise, enqueuing future commands + // would block on the lock). + // 3. Acquire the lock again. If any new tasks were queued during step 2, goto 1. + // If not, set mConnector to non-null so future tasks are run immediately, not queued. + // + // For this to work, all calls to the tethering service must use getConnector(), which + // ensures that tasks are added to the queue with the lock held. + // + // Once mConnector is set to non-null, it will never be null again. If the network stack + // process crashes, no recovery is possible. + // TODO: evaluate whether it is possible to recover from network stack process crashes + // (though in most cases the system will have crashed when the network stack process + // crashes). + do { + final List localWaitQueue; + synchronized (mConnectorWaitQueue) { + localWaitQueue = new ArrayList<>(mConnectorWaitQueue); + mConnectorWaitQueue.clear(); + } + + // Allow more tasks to be added at the end without blocking while draining the queue. + for (ConnectorConsumer task : localWaitQueue) { + try { + task.onConnectorAvailable(connector); + } catch (RemoteException e) { + // Most likely the network stack process crashed, which is likely to crash the + // system. Keep processing other requests but report the error loudly. + Log.wtf(TAG, "Error processing request for the tethering connector", e); + } + } + + synchronized (mConnectorWaitQueue) { + if (mConnectorWaitQueue.size() == 0) { + mConnector = connector; + return; + } + } + } while (true); + } + + /** + * Asynchronously get the ITetheringConnector to execute some operation. + * + *

If the connector is already available, the operation will be executed on the caller's + * thread. Otherwise it will be queued and executed on a worker thread. The operation should be + * limited to performing oneway binder calls to minimize differences due to threading. + */ + private void getConnector(ConnectorConsumer consumer) { + final ITetheringConnector connector; + synchronized (mConnectorWaitQueue) { + connector = mConnector; + if (connector == null) { + mConnectorWaitQueue.add(consumer); + return; + } + } + + try { + consumer.onConnectorAvailable(connector); + } catch (RemoteException e) { + throw new IllegalStateException(e); + } + } + + private interface RequestHelper { + void runRequest(ITetheringConnector connector, IIntResultListener listener); + } + + // Used to dispatch legacy ConnectivityManager methods that expect tethering to be able to + // return results and perform operations synchronously. + // TODO: remove once there are no callers of these legacy methods. + private class RequestDispatcher { + private final ConditionVariable mWaiting; + public volatile int mRemoteResult; + + private final IIntResultListener mListener = new IIntResultListener.Stub() { + @Override + public void onResult(final int resultCode) { + mRemoteResult = resultCode; + mWaiting.open(); + } + }; + + RequestDispatcher() { + mWaiting = new ConditionVariable(); + } + + int waitForResult(final RequestHelper request) { + getConnector(c -> request.runRequest(c, mListener)); + if (!mWaiting.block(DEFAULT_TIMEOUT_MS)) { + throw new IllegalStateException("Callback timeout"); + } + + throwIfPermissionFailure(mRemoteResult); + + return mRemoteResult; + } + } + + private void throwIfPermissionFailure(final int errorCode) { + switch (errorCode) { + case TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION: + throw new SecurityException("No android.permission.TETHER_PRIVILEGED" + + " or android.permission.WRITE_SETTINGS permission"); + case TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION: + throw new SecurityException( + "No android.permission.ACCESS_NETWORK_STATE permission"); + } + } + + private class TetheringCallbackInternal extends ITetheringEventCallback.Stub { + private volatile int mError = TETHER_ERROR_NO_ERROR; + private final ConditionVariable mWaitForCallback = new ConditionVariable(); + + @Override + public void onCallbackStarted(TetheringCallbackStartedParcel parcel) { + mTetheringConfiguration = parcel.config; + mTetherStatesParcel = parcel.states; + mWaitForCallback.open(); + } + + @Override + public void onCallbackStopped(int errorCode) { + mError = errorCode; + mWaitForCallback.open(); + } + + @Override + public void onUpstreamChanged(Network network) { } + + @Override + public void onConfigurationChanged(TetheringConfigurationParcel config) { + mTetheringConfiguration = config; + } + + @Override + public void onTetherStatesChanged(TetherStatesParcel states) { + mTetherStatesParcel = states; + } + + @Override + public void onTetherClientsChanged(List clients) { } + + @Override + public void onOffloadStatusChanged(int status) { } + + public void waitForStarted() { + mWaitForCallback.block(DEFAULT_TIMEOUT_MS); + throwIfPermissionFailure(mError); + } + } + + /** + * Attempt to tether the named interface. This will setup a dhcp server + * on the interface, forward and NAT IP v4 packets and forward DNS requests + * to the best active upstream network interface. Note that if no upstream + * IP network interface is available, dhcp will still run and traffic will be + * allowed between the tethered devices and this device, though upstream net + * access will of course fail until an upstream network interface becomes + * active. + * + * @deprecated The only usages is PanService. It uses this for legacy reasons + * and will migrate away as soon as possible. + * + * @param iface the interface name to tether. + * @return error a {@code TETHER_ERROR} value indicating success or failure type + * + * {@hide} + */ + @Deprecated + @SystemApi(client = MODULE_LIBRARIES) + public int tether(@NonNull final String iface) { + final String callerPkg = mContext.getOpPackageName(); + Log.i(TAG, "tether caller:" + callerPkg); + final RequestDispatcher dispatcher = new RequestDispatcher(); + + return dispatcher.waitForResult((connector, listener) -> { + try { + connector.tether(iface, callerPkg, getAttributionTag(), listener); + } catch (RemoteException e) { + throw new IllegalStateException(e); + } + }); + } + + /** + * @return the context's attribution tag + */ + private @Nullable String getAttributionTag() { + return mContext.getAttributionTag(); + } + + /** + * Stop tethering the named interface. + * + * @deprecated The only usages is PanService. It uses this for legacy reasons + * and will migrate away as soon as possible. + * + * {@hide} + */ + @Deprecated + @SystemApi(client = MODULE_LIBRARIES) + public int untether(@NonNull final String iface) { + final String callerPkg = mContext.getOpPackageName(); + Log.i(TAG, "untether caller:" + callerPkg); + + final RequestDispatcher dispatcher = new RequestDispatcher(); + + return dispatcher.waitForResult((connector, listener) -> { + try { + connector.untether(iface, callerPkg, getAttributionTag(), listener); + } catch (RemoteException e) { + throw new IllegalStateException(e); + } + }); + } + + /** + * Attempt to both alter the mode of USB and Tethering of USB. + * + * @deprecated New clients should not use this API anymore. All clients should use + * #startTethering or #stopTethering which encapsulate proper entitlement logic. If the API is + * used and an entitlement check is needed, downstream USB tethering will be enabled but will + * not have any upstream. + * + * {@hide} + */ + @Deprecated + @SystemApi(client = MODULE_LIBRARIES) + public int setUsbTethering(final boolean enable) { + final String callerPkg = mContext.getOpPackageName(); + Log.i(TAG, "setUsbTethering caller:" + callerPkg); + + final RequestDispatcher dispatcher = new RequestDispatcher(); + + return dispatcher.waitForResult((connector, listener) -> { + try { + connector.setUsbTethering(enable, callerPkg, getAttributionTag(), + listener); + } catch (RemoteException e) { + throw new IllegalStateException(e); + } + }); + } + + /** + * Indicates that this tethering connection will provide connectivity beyond this device (e.g., + * global Internet access). + */ + public static final int CONNECTIVITY_SCOPE_GLOBAL = 1; + + /** + * Indicates that this tethering connection will only provide local connectivity. + */ + public static final int CONNECTIVITY_SCOPE_LOCAL = 2; + + /** + * Connectivity scopes for {@link TetheringRequest.Builder#setConnectivityScope}. + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = "CONNECTIVITY_SCOPE_", value = { + CONNECTIVITY_SCOPE_GLOBAL, + CONNECTIVITY_SCOPE_LOCAL, + }) + public @interface ConnectivityScope {} + + /** + * Use with {@link #startTethering} to specify additional parameters when starting tethering. + */ + public static class TetheringRequest { + /** A configuration set for TetheringRequest. */ + private final TetheringRequestParcel mRequestParcel; + + private TetheringRequest(final TetheringRequestParcel request) { + mRequestParcel = request; + } + + /** Builder used to create TetheringRequest. */ + public static class Builder { + private final TetheringRequestParcel mBuilderParcel; + + /** Default constructor of Builder. */ + public Builder(@TetheringType final int type) { + mBuilderParcel = new TetheringRequestParcel(); + mBuilderParcel.tetheringType = type; + mBuilderParcel.localIPv4Address = null; + mBuilderParcel.staticClientAddress = null; + mBuilderParcel.exemptFromEntitlementCheck = false; + mBuilderParcel.showProvisioningUi = true; + mBuilderParcel.connectivityScope = getDefaultConnectivityScope(type); + } + + /** + * Configure tethering with static IPv4 assignment. + * + * A DHCP server will be started, but will only be able to offer the client address. + * The two addresses must be in the same prefix. + * + * @param localIPv4Address The preferred local IPv4 link address to use. + * @param clientAddress The static client address. + */ + @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) + @NonNull + public Builder setStaticIpv4Addresses(@NonNull final LinkAddress localIPv4Address, + @NonNull final LinkAddress clientAddress) { + Objects.requireNonNull(localIPv4Address); + Objects.requireNonNull(clientAddress); + if (!checkStaticAddressConfiguration(localIPv4Address, clientAddress)) { + throw new IllegalArgumentException("Invalid server or client addresses"); + } + + mBuilderParcel.localIPv4Address = localIPv4Address; + mBuilderParcel.staticClientAddress = clientAddress; + return this; + } + + /** Start tethering without entitlement checks. */ + @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) + @NonNull + public Builder setExemptFromEntitlementCheck(boolean exempt) { + mBuilderParcel.exemptFromEntitlementCheck = exempt; + return this; + } + + /** + * If an entitlement check is needed, sets whether to show the entitlement UI or to + * perform a silent entitlement check. By default, the entitlement UI is shown. + */ + @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) + @NonNull + public Builder setShouldShowEntitlementUi(boolean showUi) { + mBuilderParcel.showProvisioningUi = showUi; + return this; + } + + /** + * Sets the connectivity scope to be provided by this tethering downstream. + */ + @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED) + @NonNull + public Builder setConnectivityScope(@ConnectivityScope int scope) { + if (!checkConnectivityScope(mBuilderParcel.tetheringType, scope)) { + throw new IllegalArgumentException("Invalid connectivity scope " + scope); + } + + mBuilderParcel.connectivityScope = scope; + return this; + } + + /** Build {@link TetheringRequest} with the currently set configuration. */ + @NonNull + public TetheringRequest build() { + return new TetheringRequest(mBuilderParcel); + } + } + + /** + * Get the local IPv4 address, if one was configured with + * {@link Builder#setStaticIpv4Addresses}. + */ + @Nullable + public LinkAddress getLocalIpv4Address() { + return mRequestParcel.localIPv4Address; + } + + /** + * Get the static IPv4 address of the client, if one was configured with + * {@link Builder#setStaticIpv4Addresses}. + */ + @Nullable + public LinkAddress getClientStaticIpv4Address() { + return mRequestParcel.staticClientAddress; + } + + /** Get tethering type. */ + @TetheringType + public int getTetheringType() { + return mRequestParcel.tetheringType; + } + + /** Get connectivity type */ + @ConnectivityScope + public int getConnectivityScope() { + return mRequestParcel.connectivityScope; + } + + /** Check if exempt from entitlement check. */ + public boolean isExemptFromEntitlementCheck() { + return mRequestParcel.exemptFromEntitlementCheck; + } + + /** Check if show entitlement ui. */ + public boolean getShouldShowEntitlementUi() { + return mRequestParcel.showProvisioningUi; + } + + /** + * Check whether the two addresses are ipv4 and in the same prefix. + * @hide + */ + public static boolean checkStaticAddressConfiguration( + @NonNull final LinkAddress localIPv4Address, + @NonNull final LinkAddress clientAddress) { + return localIPv4Address.getPrefixLength() == clientAddress.getPrefixLength() + && localIPv4Address.isIpv4() && clientAddress.isIpv4() + && new IpPrefix(localIPv4Address.toString()).equals( + new IpPrefix(clientAddress.toString())); + } + + /** + * Returns the default connectivity scope for the given tethering type. Usually this is + * CONNECTIVITY_SCOPE_GLOBAL, except for NCM which for historical reasons defaults to local. + * @hide + */ + public static @ConnectivityScope int getDefaultConnectivityScope(int tetheringType) { + return tetheringType != TETHERING_NCM + ? CONNECTIVITY_SCOPE_GLOBAL + : CONNECTIVITY_SCOPE_LOCAL; + } + + /** + * Checks whether the requested connectivity scope is allowed. + * @hide + */ + private static boolean checkConnectivityScope(int type, int scope) { + if (scope == CONNECTIVITY_SCOPE_GLOBAL) return true; + return type == TETHERING_USB || type == TETHERING_ETHERNET || type == TETHERING_NCM; + } + + /** + * Get a TetheringRequestParcel from the configuration + * @hide + */ + public TetheringRequestParcel getParcel() { + return mRequestParcel; + } + + /** String of TetheringRequest detail. */ + public String toString() { + return "TetheringRequest [ type= " + mRequestParcel.tetheringType + + ", localIPv4Address= " + mRequestParcel.localIPv4Address + + ", staticClientAddress= " + mRequestParcel.staticClientAddress + + ", exemptFromEntitlementCheck= " + + mRequestParcel.exemptFromEntitlementCheck + ", showProvisioningUi= " + + mRequestParcel.showProvisioningUi + " ]"; + } + } + + /** + * Callback for use with {@link #startTethering} to find out whether tethering succeeded. + */ + public interface StartTetheringCallback { + /** + * Called when tethering has been successfully started. + */ + default void onTetheringStarted() {} + + /** + * Called when starting tethering failed. + * + * @param error The error that caused the failure. + */ + default void onTetheringFailed(@StartTetheringError final int error) {} + } + + /** + * Starts tethering and runs tether provisioning for the given type if needed. If provisioning + * fails, stopTethering will be called automatically. + * + *

Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will + * fail if a tethering entitlement check is required. + * + * @param request a {@link TetheringRequest} which can specify the preferred configuration. + * @param executor {@link Executor} to specify the thread upon which the callback of + * TetheringRequest will be invoked. + * @param callback A callback that will be called to indicate the success status of the + * tethering start request. + */ + @RequiresPermission(anyOf = { + android.Manifest.permission.TETHER_PRIVILEGED, + android.Manifest.permission.WRITE_SETTINGS + }) + public void startTethering(@NonNull final TetheringRequest request, + @NonNull final Executor executor, @NonNull final StartTetheringCallback callback) { + final String callerPkg = mContext.getOpPackageName(); + Log.i(TAG, "startTethering caller:" + callerPkg); + + final IIntResultListener listener = new IIntResultListener.Stub() { + @Override + public void onResult(final int resultCode) { + executor.execute(() -> { + if (resultCode == TETHER_ERROR_NO_ERROR) { + callback.onTetheringStarted(); + } else { + callback.onTetheringFailed(resultCode); + } + }); + } + }; + getConnector(c -> c.startTethering(request.getParcel(), callerPkg, + getAttributionTag(), listener)); + } + + /** + * Starts tethering and runs tether provisioning for the given type if needed. If provisioning + * fails, stopTethering will be called automatically. + * + *

Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will + * fail if a tethering entitlement check is required. + * + * @param type The tethering type, on of the {@code TetheringManager#TETHERING_*} constants. + * @param executor {@link Executor} to specify the thread upon which the callback of + * TetheringRequest will be invoked. + * @hide + */ + @RequiresPermission(anyOf = { + android.Manifest.permission.TETHER_PRIVILEGED, + android.Manifest.permission.WRITE_SETTINGS + }) + @SystemApi(client = MODULE_LIBRARIES) + public void startTethering(int type, @NonNull final Executor executor, + @NonNull final StartTetheringCallback callback) { + startTethering(new TetheringRequest.Builder(type).build(), executor, callback); + } + + /** + * Stops tethering for the given type. Also cancels any provisioning rechecks for that type if + * applicable. + * + *

Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will + * fail if a tethering entitlement check is required. + */ + @RequiresPermission(anyOf = { + android.Manifest.permission.TETHER_PRIVILEGED, + android.Manifest.permission.WRITE_SETTINGS + }) + public void stopTethering(@TetheringType final int type) { + final String callerPkg = mContext.getOpPackageName(); + Log.i(TAG, "stopTethering caller:" + callerPkg); + + getConnector(c -> c.stopTethering(type, callerPkg, getAttributionTag(), + new IIntResultListener.Stub() { + @Override + public void onResult(int resultCode) { + // TODO: provide an API to obtain result + // This has never been possible as stopTethering has always been void and never + // taken a callback object. The only indication that callers have is if the call + // results in a TETHER_STATE_CHANGE broadcast. + } + })); + } + + /** + * Callback for use with {@link #getLatestTetheringEntitlementResult} to find out whether + * entitlement succeeded. + */ + public interface OnTetheringEntitlementResultListener { + /** + * Called to notify entitlement result. + * + * @param resultCode an int value of entitlement result. It may be one of + * {@link #TETHER_ERROR_NO_ERROR}, + * {@link #TETHER_ERROR_PROVISIONING_FAILED}, or + * {@link #TETHER_ERROR_ENTITLEMENT_UNKNOWN}. + */ + void onTetheringEntitlementResult(@EntitlementResult int result); + } + + /** + * Request the latest value of the tethering entitlement check. + * + *

This method will only return the latest entitlement result if it is available. If no + * cached entitlement result is available, and {@code showEntitlementUi} is false, + * {@link #TETHER_ERROR_ENTITLEMENT_UNKNOWN} will be returned. If {@code showEntitlementUi} is + * true, entitlement will be run. + * + *

Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will + * fail if a tethering entitlement check is required. + * + * @param type the downstream type of tethering. Must be one of {@code #TETHERING_*} constants. + * @param showEntitlementUi a boolean indicating whether to check result for the UI-based + * entitlement check or the silent entitlement check. + * @param executor the executor on which callback will be invoked. + * @param listener an {@link OnTetheringEntitlementResultListener} which will be called to + * notify the caller of the result of entitlement check. The listener may be called zero + * or one time. + */ + @RequiresPermission(anyOf = { + android.Manifest.permission.TETHER_PRIVILEGED, + android.Manifest.permission.WRITE_SETTINGS + }) + public void requestLatestTetheringEntitlementResult(@TetheringType int type, + boolean showEntitlementUi, + @NonNull Executor executor, + @NonNull final OnTetheringEntitlementResultListener listener) { + if (listener == null) { + throw new IllegalArgumentException( + "OnTetheringEntitlementResultListener cannot be null."); + } + + ResultReceiver wrappedListener = new ResultReceiver(null /* handler */) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + executor.execute(() -> { + listener.onTetheringEntitlementResult(resultCode); + }); + } + }; + + requestLatestTetheringEntitlementResult(type, wrappedListener, + showEntitlementUi); + } + + /** + * Helper function of #requestLatestTetheringEntitlementResult to remain backwards compatible + * with ConnectivityManager#getLatestTetheringEntitlementResult + * + * {@hide} + */ + // TODO: improve the usage of ResultReceiver, b/145096122 + @SystemApi(client = MODULE_LIBRARIES) + public void requestLatestTetheringEntitlementResult(@TetheringType final int type, + @NonNull final ResultReceiver receiver, final boolean showEntitlementUi) { + final String callerPkg = mContext.getOpPackageName(); + Log.i(TAG, "getLatestTetheringEntitlementResult caller:" + callerPkg); + + getConnector(c -> c.requestLatestTetheringEntitlementResult( + type, receiver, showEntitlementUi, callerPkg, getAttributionTag())); + } + + /** + * Callback for use with {@link registerTetheringEventCallback} to find out tethering + * upstream status. + */ + public interface TetheringEventCallback { + /** + * Called when tethering supported status changed. + * + *

This will be called immediately after the callback is registered, and may be called + * multiple times later upon changes. + * + *

Tethering may be disabled via system properties, device configuration, or device + * policy restrictions. + * + * @param supported The new supported status + */ + default void onTetheringSupported(boolean supported) {} + + /** + * Called when tethering upstream changed. + * + *

This will be called immediately after the callback is registered, and may be called + * multiple times later upon changes. + * + * @param network the {@link Network} of tethering upstream. Null means tethering doesn't + * have any upstream. + */ + default void onUpstreamChanged(@Nullable Network network) {} + + /** + * Called when there was a change in tethering interface regular expressions. + * + *

This will be called immediately after the callback is registered, and may be called + * multiple times later upon changes. + * @param reg The new regular expressions. + * + * @deprecated New clients should use the callbacks with {@link TetheringInterface} which + * has the mapping between tethering type and interface. InterfaceRegex is no longer needed + * to determine the mapping of tethering type and interface. + * + * @hide + */ + @Deprecated + @SystemApi(client = MODULE_LIBRARIES) + default void onTetherableInterfaceRegexpsChanged(@NonNull TetheringInterfaceRegexps reg) {} + + /** + * Called when there was a change in the list of tetherable interfaces. Tetherable + * interface means this interface is available and can be used for tethering. + * + *

This will be called immediately after the callback is registered, and may be called + * multiple times later upon changes. + * @param interfaces The list of tetherable interface names. + */ + default void onTetherableInterfacesChanged(@NonNull List interfaces) {} + + /** + * Called when there was a change in the list of tetherable interfaces. Tetherable + * interface means this interface is available and can be used for tethering. + * + *

This will be called immediately after the callback is registered, and may be called + * multiple times later upon changes. + * @param interfaces The set of TetheringInterface of currently tetherable interface. + */ + default void onTetherableInterfacesChanged(@NonNull Set interfaces) { + // By default, the new callback calls the old callback, so apps + // implementing the old callback just work. + onTetherableInterfacesChanged(toIfaces(interfaces)); + } + + /** + * Called when there was a change in the list of tethered interfaces. + * + *

This will be called immediately after the callback is registered, and may be called + * multiple times later upon changes. + * @param interfaces The lit of 0 or more String of currently tethered interface names. + */ + default void onTetheredInterfacesChanged(@NonNull List interfaces) {} + + /** + * Called when there was a change in the list of tethered interfaces. + * + *

This will be called immediately after the callback is registered, and may be called + * multiple times later upon changes. + * @param interfaces The set of 0 or more TetheringInterface of currently tethered + * interface. + */ + default void onTetheredInterfacesChanged(@NonNull Set interfaces) { + // By default, the new callback calls the old callback, so apps + // implementing the old callback just work. + onTetheredInterfacesChanged(toIfaces(interfaces)); + } + + /** + * Called when there was a change in the list of local-only interfaces. + * + *

This will be called immediately after the callback is registered, and may be called + * multiple times later upon changes. + * @param interfaces The list of 0 or more String of active local-only interface names. + */ + default void onLocalOnlyInterfacesChanged(@NonNull List interfaces) {} + + /** + * Called when there was a change in the list of local-only interfaces. + * + *

This will be called immediately after the callback is registered, and may be called + * multiple times later upon changes. + * @param interfaces The set of 0 or more TetheringInterface of active local-only + * interface. + */ + default void onLocalOnlyInterfacesChanged(@NonNull Set interfaces) { + // By default, the new callback calls the old callback, so apps + // implementing the old callback just work. + onLocalOnlyInterfacesChanged(toIfaces(interfaces)); + } + + /** + * Called when an error occurred configuring tethering. + * + *

This will be called immediately after the callback is registered if the latest status + * on the interface is an error, and may be called multiple times later upon changes. + * @param ifName Name of the interface. + * @param error One of {@code TetheringManager#TETHER_ERROR_*}. + */ + default void onError(@NonNull String ifName, @TetheringIfaceError int error) {} + + /** + * Called when an error occurred configuring tethering. + * + *

This will be called immediately after the callback is registered if the latest status + * on the interface is an error, and may be called multiple times later upon changes. + * @param iface The interface that experienced the error. + * @param error One of {@code TetheringManager#TETHER_ERROR_*}. + */ + default void onError(@NonNull TetheringInterface iface, @TetheringIfaceError int error) { + // By default, the new callback calls the old callback, so apps + // implementing the old callback just work. + onError(iface.getInterface(), error); + } + + /** + * Called when the list of tethered clients changes. + * + *

This callback provides best-effort information on connected clients based on state + * known to the system, however the list cannot be completely accurate (and should not be + * used for security purposes). For example, clients behind a bridge and using static IP + * assignments are not visible to the tethering device; or even when using DHCP, such + * clients may still be reported by this callback after disconnection as the system cannot + * determine if they are still connected. + * @param clients The new set of tethered clients; the collection is not ordered. + */ + default void onClientsChanged(@NonNull Collection clients) {} + + /** + * Called when tethering offload status changes. + * + *

This will be called immediately after the callback is registered. + * @param status The offload status. + */ + default void onOffloadStatusChanged(@TetherOffloadStatus int status) {} + } + + /** + * Covert DownStreamInterface collection to interface String array list. Internal use only. + * + * @hide + */ + public static ArrayList toIfaces(Collection tetherIfaces) { + final ArrayList ifaces = new ArrayList<>(); + for (TetheringInterface tether : tetherIfaces) { + ifaces.add(tether.getInterface()); + } + + return ifaces; + } + + private static String[] toIfaces(TetheringInterface[] tetherIfaces) { + final String[] ifaces = new String[tetherIfaces.length]; + for (int i = 0; i < tetherIfaces.length; i++) { + ifaces[i] = tetherIfaces[i].getInterface(); + } + + return ifaces; + } + + + /** + * Regular expressions used to identify tethering interfaces. + * + * @deprecated Instead of using regex to determine tethering type. New client could use the + * callbacks with {@link TetheringInterface} which has the mapping of type and interface. + * @hide + */ + @Deprecated + @SystemApi(client = MODULE_LIBRARIES) + public static class TetheringInterfaceRegexps { + private final String[] mTetherableBluetoothRegexs; + private final String[] mTetherableUsbRegexs; + private final String[] mTetherableWifiRegexs; + + /** @hide */ + public TetheringInterfaceRegexps(@NonNull String[] tetherableBluetoothRegexs, + @NonNull String[] tetherableUsbRegexs, @NonNull String[] tetherableWifiRegexs) { + mTetherableBluetoothRegexs = tetherableBluetoothRegexs.clone(); + mTetherableUsbRegexs = tetherableUsbRegexs.clone(); + mTetherableWifiRegexs = tetherableWifiRegexs.clone(); + } + + @NonNull + public List getTetherableBluetoothRegexs() { + return Collections.unmodifiableList(Arrays.asList(mTetherableBluetoothRegexs)); + } + + @NonNull + public List getTetherableUsbRegexs() { + return Collections.unmodifiableList(Arrays.asList(mTetherableUsbRegexs)); + } + + @NonNull + public List getTetherableWifiRegexs() { + return Collections.unmodifiableList(Arrays.asList(mTetherableWifiRegexs)); + } + + @Override + public int hashCode() { + return Objects.hash(mTetherableBluetoothRegexs, mTetherableUsbRegexs, + mTetherableWifiRegexs); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof TetheringInterfaceRegexps)) return false; + final TetheringInterfaceRegexps other = (TetheringInterfaceRegexps) obj; + return Arrays.equals(mTetherableBluetoothRegexs, other.mTetherableBluetoothRegexs) + && Arrays.equals(mTetherableUsbRegexs, other.mTetherableUsbRegexs) + && Arrays.equals(mTetherableWifiRegexs, other.mTetherableWifiRegexs); + } + } + + /** + * Start listening to tethering change events. Any new added callback will receive the last + * tethering status right away. If callback is registered, + * {@link TetheringEventCallback#onUpstreamChanged} will immediately be called. If tethering + * has no upstream or disabled, the argument of callback will be null. The same callback object + * cannot be registered twice. + * + * @param executor the executor on which callback will be invoked. + * @param callback the callback to be called when tethering has change events. + */ + @RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE) + public void registerTetheringEventCallback(@NonNull Executor executor, + @NonNull TetheringEventCallback callback) { + final String callerPkg = mContext.getOpPackageName(); + Log.i(TAG, "registerTetheringEventCallback caller:" + callerPkg); + + synchronized (mTetheringEventCallbacks) { + if (mTetheringEventCallbacks.containsKey(callback)) { + throw new IllegalArgumentException("callback was already registered."); + } + final ITetheringEventCallback remoteCallback = new ITetheringEventCallback.Stub() { + // Only accessed with a lock on this object + private final HashMap mErrorStates = new HashMap<>(); + private TetheringInterface[] mLastTetherableInterfaces = null; + private TetheringInterface[] mLastTetheredInterfaces = null; + private TetheringInterface[] mLastLocalOnlyInterfaces = null; + + @Override + public void onUpstreamChanged(Network network) throws RemoteException { + executor.execute(() -> { + callback.onUpstreamChanged(network); + }); + } + + private synchronized void sendErrorCallbacks(final TetherStatesParcel newStates) { + for (int i = 0; i < newStates.erroredIfaceList.length; i++) { + final TetheringInterface tetherIface = newStates.erroredIfaceList[i]; + final Integer lastError = mErrorStates.get(tetherIface); + final int newError = newStates.lastErrorList[i]; + if (newError != TETHER_ERROR_NO_ERROR + && !Objects.equals(lastError, newError)) { + callback.onError(tetherIface, newError); + } + mErrorStates.put(tetherIface, newError); + } + } + + private synchronized void maybeSendTetherableIfacesChangedCallback( + final TetherStatesParcel newStates) { + if (Arrays.equals(mLastTetherableInterfaces, newStates.availableList)) return; + mLastTetherableInterfaces = newStates.availableList.clone(); + callback.onTetherableInterfacesChanged( + Collections.unmodifiableSet((new ArraySet(mLastTetherableInterfaces)))); + } + + private synchronized void maybeSendTetheredIfacesChangedCallback( + final TetherStatesParcel newStates) { + if (Arrays.equals(mLastTetheredInterfaces, newStates.tetheredList)) return; + mLastTetheredInterfaces = newStates.tetheredList.clone(); + callback.onTetheredInterfacesChanged( + Collections.unmodifiableSet((new ArraySet(mLastTetheredInterfaces)))); + } + + private synchronized void maybeSendLocalOnlyIfacesChangedCallback( + final TetherStatesParcel newStates) { + if (Arrays.equals(mLastLocalOnlyInterfaces, newStates.localOnlyList)) return; + mLastLocalOnlyInterfaces = newStates.localOnlyList.clone(); + callback.onLocalOnlyInterfacesChanged( + Collections.unmodifiableSet((new ArraySet(mLastLocalOnlyInterfaces)))); + } + + // Called immediately after the callbacks are registered. + @Override + public void onCallbackStarted(TetheringCallbackStartedParcel parcel) { + executor.execute(() -> { + callback.onTetheringSupported(parcel.tetheringSupported); + callback.onUpstreamChanged(parcel.upstreamNetwork); + sendErrorCallbacks(parcel.states); + sendRegexpsChanged(parcel.config); + maybeSendTetherableIfacesChangedCallback(parcel.states); + maybeSendTetheredIfacesChangedCallback(parcel.states); + maybeSendLocalOnlyIfacesChangedCallback(parcel.states); + callback.onClientsChanged(parcel.tetheredClients); + callback.onOffloadStatusChanged(parcel.offloadStatus); + }); + } + + @Override + public void onCallbackStopped(int errorCode) { + executor.execute(() -> { + throwIfPermissionFailure(errorCode); + }); + } + + private void sendRegexpsChanged(TetheringConfigurationParcel parcel) { + callback.onTetherableInterfaceRegexpsChanged(new TetheringInterfaceRegexps( + parcel.tetherableBluetoothRegexs, + parcel.tetherableUsbRegexs, + parcel.tetherableWifiRegexs)); + } + + @Override + public void onConfigurationChanged(TetheringConfigurationParcel config) { + executor.execute(() -> sendRegexpsChanged(config)); + } + + @Override + public void onTetherStatesChanged(TetherStatesParcel states) { + executor.execute(() -> { + sendErrorCallbacks(states); + maybeSendTetherableIfacesChangedCallback(states); + maybeSendTetheredIfacesChangedCallback(states); + maybeSendLocalOnlyIfacesChangedCallback(states); + }); + } + + @Override + public void onTetherClientsChanged(final List clients) { + executor.execute(() -> callback.onClientsChanged(clients)); + } + + @Override + public void onOffloadStatusChanged(final int status) { + executor.execute(() -> callback.onOffloadStatusChanged(status)); + } + }; + getConnector(c -> c.registerTetheringEventCallback(remoteCallback, callerPkg)); + mTetheringEventCallbacks.put(callback, remoteCallback); + } + } + + /** + * Remove tethering event callback previously registered with + * {@link #registerTetheringEventCallback}. + * + * @param callback previously registered callback. + */ + @RequiresPermission(anyOf = { + Manifest.permission.TETHER_PRIVILEGED, + Manifest.permission.ACCESS_NETWORK_STATE + }) + public void unregisterTetheringEventCallback(@NonNull final TetheringEventCallback callback) { + final String callerPkg = mContext.getOpPackageName(); + Log.i(TAG, "unregisterTetheringEventCallback caller:" + callerPkg); + + synchronized (mTetheringEventCallbacks) { + ITetheringEventCallback remoteCallback = mTetheringEventCallbacks.remove(callback); + if (remoteCallback == null) { + throw new IllegalArgumentException("callback was not registered."); + } + + getConnector(c -> c.unregisterTetheringEventCallback(remoteCallback, callerPkg)); + } + } + + /** + * Get a more detailed error code after a Tethering or Untethering + * request asynchronously failed. + * + * @param iface The name of the interface of interest + * @return error The error code of the last error tethering or untethering the named + * interface + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public int getLastTetherError(@NonNull final String iface) { + mCallback.waitForStarted(); + if (mTetherStatesParcel == null) return TETHER_ERROR_NO_ERROR; + + int i = 0; + for (TetheringInterface errored : mTetherStatesParcel.erroredIfaceList) { + if (iface.equals(errored.getInterface())) return mTetherStatesParcel.lastErrorList[i]; + + i++; + } + return TETHER_ERROR_NO_ERROR; + } + + /** + * Get the list of regular expressions that define any tetherable + * USB network interfaces. If USB tethering is not supported by the + * device, this list should be empty. + * + * @return an array of 0 or more regular expression Strings defining + * what interfaces are considered tetherable usb interfaces. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public @NonNull String[] getTetherableUsbRegexs() { + mCallback.waitForStarted(); + return mTetheringConfiguration.tetherableUsbRegexs; + } + + /** + * Get the list of regular expressions that define any tetherable + * Wifi network interfaces. If Wifi tethering is not supported by the + * device, this list should be empty. + * + * @return an array of 0 or more regular expression Strings defining + * what interfaces are considered tetherable wifi interfaces. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public @NonNull String[] getTetherableWifiRegexs() { + mCallback.waitForStarted(); + return mTetheringConfiguration.tetherableWifiRegexs; + } + + /** + * Get the list of regular expressions that define any tetherable + * Bluetooth network interfaces. If Bluetooth tethering is not supported by the + * device, this list should be empty. + * + * @return an array of 0 or more regular expression Strings defining + * what interfaces are considered tetherable bluetooth interfaces. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public @NonNull String[] getTetherableBluetoothRegexs() { + mCallback.waitForStarted(); + return mTetheringConfiguration.tetherableBluetoothRegexs; + } + + /** + * Get the set of tetherable, available interfaces. This list is limited by + * device configuration and current interface existence. + * + * @return an array of 0 or more Strings of tetherable interface names. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public @NonNull String[] getTetherableIfaces() { + mCallback.waitForStarted(); + if (mTetherStatesParcel == null) return new String[0]; + + return toIfaces(mTetherStatesParcel.availableList); + } + + /** + * Get the set of tethered interfaces. + * + * @return an array of 0 or more String of currently tethered interface names. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public @NonNull String[] getTetheredIfaces() { + mCallback.waitForStarted(); + if (mTetherStatesParcel == null) return new String[0]; + + return toIfaces(mTetherStatesParcel.tetheredList); + } + + /** + * Get the set of interface names which attempted to tether but + * failed. Re-attempting to tether may cause them to reset to the Tethered + * state. Alternatively, causing the interface to be destroyed and recreated + * may cause them to reset to the available state. + * {@link TetheringManager#getLastTetherError} can be used to get more + * information on the cause of the errors. + * + * @return an array of 0 or more String indicating the interface names + * which failed to tether. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public @NonNull String[] getTetheringErroredIfaces() { + mCallback.waitForStarted(); + if (mTetherStatesParcel == null) return new String[0]; + + return toIfaces(mTetherStatesParcel.erroredIfaceList); + } + + /** + * Get the set of tethered dhcp ranges. + * + * @deprecated This API just return the default value which is not used in DhcpServer. + * @hide + */ + @Deprecated + public @NonNull String[] getTetheredDhcpRanges() { + mCallback.waitForStarted(); + return mTetheringConfiguration.legacyDhcpRanges; + } + + /** + * Check if the device allows for tethering. It may be disabled via + * {@code ro.tether.denied} system property, Settings.TETHER_SUPPORTED or + * due to device configuration. + * + * @return a boolean - {@code true} indicating Tethering is supported. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public boolean isTetheringSupported() { + final String callerPkg = mContext.getOpPackageName(); + + return isTetheringSupported(callerPkg); + } + + /** + * Check if the device allows for tethering. It may be disabled via {@code ro.tether.denied} + * system property, Settings.TETHER_SUPPORTED or due to device configuration. This is useful + * for system components that query this API on behalf of an app. In particular, Bluetooth + * has @UnsupportedAppUsage calls that will let apps turn on bluetooth tethering if they have + * the right permissions, but such an app needs to know whether it can (permissions as well + * as support from the device) turn on tethering in the first place to show the appropriate UI. + * + * @param callerPkg The caller package name, if it is not matching the calling uid, + * SecurityException would be thrown. + * @return a boolean - {@code true} indicating Tethering is supported. + * @hide + */ + @SystemApi(client = MODULE_LIBRARIES) + public boolean isTetheringSupported(@NonNull final String callerPkg) { + + final RequestDispatcher dispatcher = new RequestDispatcher(); + final int ret = dispatcher.waitForResult((connector, listener) -> { + try { + connector.isTetheringSupported(callerPkg, getAttributionTag(), listener); + } catch (RemoteException e) { + throw new IllegalStateException(e); + } + }); + + return ret == TETHER_ERROR_NO_ERROR; + } + + /** + * Stop all active tethering. + * + *

Without {@link android.Manifest.permission.TETHER_PRIVILEGED} permission, the call will + * fail if a tethering entitlement check is required. + */ + @RequiresPermission(anyOf = { + android.Manifest.permission.TETHER_PRIVILEGED, + android.Manifest.permission.WRITE_SETTINGS + }) + public void stopAllTethering() { + final String callerPkg = mContext.getOpPackageName(); + Log.i(TAG, "stopAllTethering caller:" + callerPkg); + + getConnector(c -> c.stopAllTethering(callerPkg, getAttributionTag(), + new IIntResultListener.Stub() { + @Override + public void onResult(int resultCode) { + // TODO: add an API parameter to send result to caller. + // This has never been possible as stopAllTethering has always been void + // and never taken a callback object. The only indication that callers have + // is if the call results in a TETHER_STATE_CHANGE broadcast. + } + })); + } +} diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl new file mode 100644 index 0000000000..f13c970d28 --- /dev/null +++ b/Tethering/common/TetheringLib/src/android/net/TetheringRequestParcel.aidl @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.net.LinkAddress; + +/** + * Configuration details for requesting tethering. + * @hide + */ +parcelable TetheringRequestParcel { + int tetheringType; + LinkAddress localIPv4Address; + LinkAddress staticClientAddress; + boolean exemptFromEntitlementCheck; + boolean showProvisioningUi; + int connectivityScope; +} diff --git a/Tethering/jarjar-rules.txt b/Tethering/jarjar-rules.txt new file mode 100644 index 0000000000..5de4b97b76 --- /dev/null +++ b/Tethering/jarjar-rules.txt @@ -0,0 +1,14 @@ +# These must be kept in sync with the framework-connectivity-shared-srcs filegroup. +# Classes from the framework-connectivity-shared-srcs filegroup. +# If there are files in that filegroup that are not covered below, the classes in the +# module will be overwritten by the ones in the framework. +rule com.android.internal.util.** com.android.networkstack.tethering.util.@1 +rule android.util.LocalLog* com.android.networkstack.tethering.util.LocalLog@1 + +rule android.net.shared.Inet4AddressUtils* com.android.networkstack.tethering.shared.Inet4AddressUtils@1 + +# Classes from net-utils-framework-common +rule com.android.net.module.util.** com.android.networkstack.tethering.util.@1 + +# Classes from net-utils-device-common +rule com.android.net.module.util.Struct* com.android.networkstack.tethering.util.Struct@1 diff --git a/Tethering/jni/android_net_util_TetheringUtils.cpp b/Tethering/jni/android_net_util_TetheringUtils.cpp new file mode 100644 index 0000000000..27c84cf280 --- /dev/null +++ b/Tethering/jni/android_net_util_TetheringUtils.cpp @@ -0,0 +1,184 @@ +/* + * 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 +#include +#include +#include +#include + +namespace android { + +static const uint32_t kIPv6NextHeaderOffset = offsetof(ip6_hdr, ip6_nxt); +static const uint32_t kIPv6PayloadStart = sizeof(ip6_hdr); +static const uint32_t kICMPv6TypeOffset = kIPv6PayloadStart + offsetof(icmp6_hdr, icmp6_type); + +static void android_net_util_setupIcmpFilter(JNIEnv *env, jobject javaFd, uint32_t type) { + sock_filter filter_code[] = { + // Check header is ICMPv6. + BPF_STMT(BPF_LD | BPF_B | BPF_ABS, kIPv6NextHeaderOffset), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_ICMPV6, 0, 3), + + // Check ICMPv6 type. + BPF_STMT(BPF_LD | BPF_B | BPF_ABS, kICMPv6TypeOffset), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, type, 0, 1), + + // Accept or reject. + BPF_STMT(BPF_RET | BPF_K, 0xffff), + BPF_STMT(BPF_RET | BPF_K, 0) + }; + + const sock_fprog filter = { + sizeof(filter_code) / sizeof(filter_code[0]), + filter_code, + }; + + int fd = netjniutils::GetNativeFileDescriptor(env, javaFd); + if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno)); + } +} + +static void android_net_util_setupNaSocket(JNIEnv *env, jobject clazz, jobject javaFd) +{ + android_net_util_setupIcmpFilter(env, javaFd, ND_NEIGHBOR_ADVERT); +} + +static void android_net_util_setupNsSocket(JNIEnv *env, jobject clazz, jobject javaFd) +{ + android_net_util_setupIcmpFilter(env, javaFd, ND_NEIGHBOR_SOLICIT); +} + +static void android_net_util_setupRaSocket(JNIEnv *env, jobject clazz, jobject javaFd, + jint ifIndex) +{ + static const int kLinkLocalHopLimit = 255; + + int fd = netjniutils::GetNativeFileDescriptor(env, javaFd); + + // Set an ICMPv6 filter that only passes Router Solicitations. + struct icmp6_filter rs_only; + ICMP6_FILTER_SETBLOCKALL(&rs_only); + ICMP6_FILTER_SETPASS(ND_ROUTER_SOLICIT, &rs_only); + socklen_t len = sizeof(rs_only); + if (setsockopt(fd, IPPROTO_ICMPV6, ICMP6_FILTER, &rs_only, len) != 0) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "setsockopt(ICMP6_FILTER): %s", strerror(errno)); + return; + } + + // Most/all of the rest of these options can be set via Java code, but + // because we're here on account of setting an icmp6_filter go ahead + // and do it all natively for now. + + // Set the multicast hoplimit to 255 (link-local only). + int hops = kLinkLocalHopLimit; + len = sizeof(hops); + if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &hops, len) != 0) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "setsockopt(IPV6_MULTICAST_HOPS): %s", strerror(errno)); + return; + } + + // Set the unicast hoplimit to 255 (link-local only). + hops = kLinkLocalHopLimit; + len = sizeof(hops); + if (setsockopt(fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &hops, len) != 0) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "setsockopt(IPV6_UNICAST_HOPS): %s", strerror(errno)); + return; + } + + // Explicitly disable multicast loopback. + int off = 0; + len = sizeof(off); + if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &off, len) != 0) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "setsockopt(IPV6_MULTICAST_LOOP): %s", strerror(errno)); + return; + } + + // Specify the IPv6 interface to use for outbound multicast. + len = sizeof(ifIndex); + if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &ifIndex, len) != 0) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "setsockopt(IPV6_MULTICAST_IF): %s", strerror(errno)); + return; + } + + // Additional options to be considered: + // - IPV6_TCLASS + // - IPV6_RECVPKTINFO + // - IPV6_RECVHOPLIMIT + + // Bind to [::]. + const struct sockaddr_in6 sin6 = { + .sin6_family = AF_INET6, + .sin6_port = 0, + .sin6_flowinfo = 0, + .sin6_addr = IN6ADDR_ANY_INIT, + .sin6_scope_id = 0, + }; + auto sa = reinterpret_cast(&sin6); + len = sizeof(sin6); + if (bind(fd, sa, len) != 0) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "bind(IN6ADDR_ANY): %s", strerror(errno)); + return; + } + + // Join the all-routers multicast group, ff02::2%index. + struct ipv6_mreq all_rtrs = { + .ipv6mr_multiaddr = {{{0xff,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2}}}, + .ipv6mr_interface = ifIndex, + }; + len = sizeof(all_rtrs); + if (setsockopt(fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &all_rtrs, len) != 0) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "setsockopt(IPV6_JOIN_GROUP): %s", strerror(errno)); + return; + } +} + +/* + * JNI registration. + */ +static const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + { "setupNaSocket", "(Ljava/io/FileDescriptor;)V", + (void*) android_net_util_setupNaSocket }, + { "setupNsSocket", "(Ljava/io/FileDescriptor;)V", + (void*) android_net_util_setupNsSocket }, + { "setupRaSocket", "(Ljava/io/FileDescriptor;I)V", + (void*) android_net_util_setupRaSocket }, +}; + +int register_android_net_util_TetheringUtils(JNIEnv* env) { + return jniRegisterNativeMethods(env, + "android/net/util/TetheringUtils", + gMethods, NELEM(gMethods)); +} + +}; // namespace android diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfCoordinator.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfCoordinator.cpp new file mode 100644 index 0000000000..27357f88d6 --- /dev/null +++ b/Tethering/jni/com_android_networkstack_tethering_BpfCoordinator.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "bpf_tethering.h" + +namespace android { + +static jobjectArray getBpfCounterNames(JNIEnv *env) { + size_t size = BPF_TETHER_ERR__MAX; + jobjectArray ret = env->NewObjectArray(size, env->FindClass("java/lang/String"), nullptr); + for (int i = 0; i < size; i++) { + env->SetObjectArrayElement(ret, i, env->NewStringUTF(bpf_tether_errors[i])); + } + return ret; +} + +/* + * JNI registration. + */ +static const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + { "getBpfCounterNames", "()[Ljava/lang/String;", (void*) getBpfCounterNames }, +}; + +int register_com_android_networkstack_tethering_BpfCoordinator(JNIEnv* env) { + return jniRegisterNativeMethods(env, + "com/android/networkstack/tethering/BpfCoordinator", + gMethods, NELEM(gMethods)); +} + +}; // namespace android diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfMap.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfMap.cpp new file mode 100644 index 0000000000..eadc210e31 --- /dev/null +++ b/Tethering/jni/com_android_networkstack_tethering_BpfMap.cpp @@ -0,0 +1,175 @@ +/* + * 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. + */ + +#include +#include +#include +#include + +#include "nativehelper/scoped_primitive_array.h" +#include "nativehelper/scoped_utf_chars.h" + +#define BPF_FD_JUST_USE_INT +#include "BpfSyscallWrappers.h" + +namespace android { + +static jclass sErrnoExceptionClass; +static jmethodID sErrnoExceptionCtor2; +static jmethodID sErrnoExceptionCtor3; + +static void throwErrnoException(JNIEnv* env, const char* functionName, int error) { + if (sErrnoExceptionClass == nullptr || sErrnoExceptionClass == nullptr) return; + + jthrowable cause = nullptr; + if (env->ExceptionCheck()) { + cause = env->ExceptionOccurred(); + env->ExceptionClear(); + } + + ScopedLocalRef msg(env, env->NewStringUTF(functionName)); + + // Not really much we can do here if msg is null, let's try to stumble on... + if (msg.get() == nullptr) env->ExceptionClear(); + + jobject errnoException; + if (cause != nullptr) { + errnoException = env->NewObject(sErrnoExceptionClass, sErrnoExceptionCtor3, msg.get(), + error, cause); + } else { + errnoException = env->NewObject(sErrnoExceptionClass, sErrnoExceptionCtor2, msg.get(), + error); + } + env->Throw(static_cast(errnoException)); +} + +static jint com_android_networkstack_tethering_BpfMap_closeMap(JNIEnv *env, jobject clazz, + jint fd) { + int ret = close(fd); + + if (ret) throwErrnoException(env, "closeMap", errno); + + return ret; +} + +static jint com_android_networkstack_tethering_BpfMap_bpfFdGet(JNIEnv *env, jobject clazz, + jstring path, jint mode) { + ScopedUtfChars pathname(env, path); + + jint fd = bpf::bpfFdGet(pathname.c_str(), static_cast(mode)); + + return fd; +} + +static void com_android_networkstack_tethering_BpfMap_writeToMapEntry(JNIEnv *env, jobject clazz, + jint fd, jbyteArray key, jbyteArray value, jint flags) { + ScopedByteArrayRO keyRO(env, key); + ScopedByteArrayRO valueRO(env, value); + + int ret = bpf::writeToMapEntry(static_cast(fd), keyRO.get(), valueRO.get(), + static_cast(flags)); + + if (ret) throwErrnoException(env, "writeToMapEntry", errno); +} + +static jboolean throwIfNotEnoent(JNIEnv *env, const char* functionName, int ret, int err) { + if (ret == 0) return true; + + if (err != ENOENT) throwErrnoException(env, functionName, err); + return false; +} + +static jboolean com_android_networkstack_tethering_BpfMap_deleteMapEntry(JNIEnv *env, jobject clazz, + jint fd, jbyteArray key) { + ScopedByteArrayRO keyRO(env, key); + + // On success, zero is returned. If the element is not found, -1 is returned and errno is set + // to ENOENT. + int ret = bpf::deleteMapEntry(static_cast(fd), keyRO.get()); + + return throwIfNotEnoent(env, "deleteMapEntry", ret, errno); +} + +static jboolean com_android_networkstack_tethering_BpfMap_getNextMapKey(JNIEnv *env, jobject clazz, + jint fd, jbyteArray key, jbyteArray nextKey) { + // If key is found, the operation returns zero and sets the next key pointer to the key of the + // next element. If key is not found, the operation returns zero and sets the next key pointer + // to the key of the first element. If key is the last element, -1 is returned and errno is + // set to ENOENT. Other possible errno values are ENOMEM, EFAULT, EPERM, and EINVAL. + ScopedByteArrayRW nextKeyRW(env, nextKey); + int ret; + if (key == nullptr) { + // Called by getFirstKey. Find the first key in the map. + ret = bpf::getNextMapKey(static_cast(fd), nullptr, nextKeyRW.get()); + } else { + ScopedByteArrayRO keyRO(env, key); + ret = bpf::getNextMapKey(static_cast(fd), keyRO.get(), nextKeyRW.get()); + } + + return throwIfNotEnoent(env, "getNextMapKey", ret, errno); +} + +static jboolean com_android_networkstack_tethering_BpfMap_findMapEntry(JNIEnv *env, jobject clazz, + jint fd, jbyteArray key, jbyteArray value) { + ScopedByteArrayRO keyRO(env, key); + ScopedByteArrayRW valueRW(env, value); + + // If an element is found, the operation returns zero and stores the element's value into + // "value". If no element is found, the operation returns -1 and sets errno to ENOENT. + int ret = bpf::findMapEntry(static_cast(fd), keyRO.get(), valueRW.get()); + + return throwIfNotEnoent(env, "findMapEntry", ret, errno); +} + +/* + * JNI registration. + */ +static const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + { "closeMap", "(I)I", + (void*) com_android_networkstack_tethering_BpfMap_closeMap }, + { "bpfFdGet", "(Ljava/lang/String;I)I", + (void*) com_android_networkstack_tethering_BpfMap_bpfFdGet }, + { "writeToMapEntry", "(I[B[BI)V", + (void*) com_android_networkstack_tethering_BpfMap_writeToMapEntry }, + { "deleteMapEntry", "(I[B)Z", + (void*) com_android_networkstack_tethering_BpfMap_deleteMapEntry }, + { "getNextMapKey", "(I[B[B)Z", + (void*) com_android_networkstack_tethering_BpfMap_getNextMapKey }, + { "findMapEntry", "(I[B[B)Z", + (void*) com_android_networkstack_tethering_BpfMap_findMapEntry }, + +}; + +int register_com_android_networkstack_tethering_BpfMap(JNIEnv* env) { + sErrnoExceptionClass = static_cast(env->NewGlobalRef( + env->FindClass("android/system/ErrnoException"))); + if (sErrnoExceptionClass == nullptr) return JNI_ERR; + + sErrnoExceptionCtor2 = env->GetMethodID(sErrnoExceptionClass, "", + "(Ljava/lang/String;I)V"); + if (sErrnoExceptionCtor2 == nullptr) return JNI_ERR; + + sErrnoExceptionCtor3 = env->GetMethodID(sErrnoExceptionClass, "", + "(Ljava/lang/String;ILjava/lang/Throwable;)V"); + if (sErrnoExceptionCtor3 == nullptr) return JNI_ERR; + + return jniRegisterNativeMethods(env, + "com/android/networkstack/tethering/BpfMap", + gMethods, NELEM(gMethods)); +} + +}; // namespace android diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp new file mode 100644 index 0000000000..1611f9d514 --- /dev/null +++ b/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// TODO: use unique_fd. +#define BPF_FD_JUST_USE_INT +#include "BpfSyscallWrappers.h" +#include "bpf_tethering.h" +#include "nativehelper/scoped_utf_chars.h" + +// The maximum length of TCA_BPF_NAME. Sync from net/sched/cls_bpf.c. +#define CLS_BPF_NAME_LEN 256 + +// Classifier name. See cls_bpf_ops in net/sched/cls_bpf.c. +#define CLS_BPF_KIND_NAME "bpf" + +namespace android { +// Sync from system/netd/server/NetlinkCommands.h +const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK; +const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0}; + +// TODO: move to frameworks/libs/net/common/native for sharing with +// system/netd/server/OffloadUtils.{c, h}. +static void sendAndProcessNetlinkResponse(JNIEnv* env, const void* req, int len) { + int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); // TODO: use unique_fd + if (fd == -1) { + jniThrowExceptionFmt(env, "java/io/IOException", + "socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE): %s", + strerror(errno)); + return; + } + + static constexpr int on = 1; + if (setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on))) { + jniThrowExceptionFmt(env, "java/io/IOException", + "setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, %d)", on); + close(fd); + return; + } + + // this is needed to get valid strace netlink parsing, it allocates the pid + if (bind(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR))) { + jniThrowExceptionFmt(env, "java/io/IOException", "bind(fd, {AF_NETLINK, 0, 0}): %s", + strerror(errno)); + close(fd); + return; + } + + // we do not want to receive messages from anyone besides the kernel + if (connect(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR))) { + jniThrowExceptionFmt(env, "java/io/IOException", "connect(fd, {AF_NETLINK, 0, 0}): %s", + strerror(errno)); + close(fd); + return; + } + + int rv = send(fd, req, len, 0); + + if (rv == -1) { + jniThrowExceptionFmt(env, "java/io/IOException", "send(fd, req, len, 0): %s", + strerror(errno)); + close(fd); + return; + } + + if (rv != len) { + jniThrowExceptionFmt(env, "java/io/IOException", "send(fd, req, len, 0): %s", + strerror(EMSGSIZE)); + close(fd); + return; + } + + struct { + nlmsghdr h; + nlmsgerr e; + char buf[256]; + } resp = {}; + + rv = recv(fd, &resp, sizeof(resp), MSG_TRUNC); + + if (rv == -1) { + jniThrowExceptionFmt(env, "java/io/IOException", "recv() failed: %s", strerror(errno)); + close(fd); + return; + } + + if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) { + jniThrowExceptionFmt(env, "java/io/IOException", "recv() returned short packet: %d", rv); + close(fd); + return; + } + + if (resp.h.nlmsg_len != (unsigned)rv) { + jniThrowExceptionFmt(env, "java/io/IOException", + "recv() returned invalid header length: %d != %d", resp.h.nlmsg_len, + rv); + close(fd); + return; + } + + if (resp.h.nlmsg_type != NLMSG_ERROR) { + jniThrowExceptionFmt(env, "java/io/IOException", + "recv() did not return NLMSG_ERROR message: %d", resp.h.nlmsg_type); + close(fd); + return; + } + + if (resp.e.error) { // returns 0 on success + jniThrowExceptionFmt(env, "java/io/IOException", "NLMSG_ERROR message return error: %s", + strerror(-resp.e.error)); + } + close(fd); + return; +} + +static int hardwareAddressType(const char* interface) { + int fd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (fd < 0) return -errno; + + struct ifreq ifr = {}; + // We use strncpy() instead of strlcpy() since kernel has to be able + // to handle non-zero terminated junk passed in by userspace anyway, + // and this way too long interface names (more than IFNAMSIZ-1 = 15 + // characters plus terminating NULL) will not get truncated to 15 + // characters and zero-terminated and thus potentially erroneously + // match a truncated interface if one were to exist. + strncpy(ifr.ifr_name, interface, sizeof(ifr.ifr_name)); + + int rv; + if (ioctl(fd, SIOCGIFHWADDR, &ifr, sizeof(ifr))) { + rv = -errno; + } else { + rv = ifr.ifr_hwaddr.sa_family; + } + + close(fd); + return rv; +} + +static jboolean com_android_networkstack_tethering_BpfUtils_isEthernet(JNIEnv* env, jobject clazz, + jstring iface) { + ScopedUtfChars interface(env, iface); + + int rv = hardwareAddressType(interface.c_str()); + if (rv < 0) { + jniThrowExceptionFmt(env, "java/io/IOException", + "Get hardware address type of interface %s failed: %s", + interface.c_str(), strerror(-rv)); + return false; + } + + switch (rv) { + case ARPHRD_ETHER: + return true; + case ARPHRD_NONE: + case ARPHRD_RAWIP: // in Linux 4.14+ rmnet support was upstreamed and this is 519 + case 530: // this is ARPHRD_RAWIP on some Android 4.9 kernels with rmnet + return false; + default: + jniThrowExceptionFmt(env, "java/io/IOException", + "Unknown hardware address type %s on interface %s", rv, + interface.c_str()); + return false; + } +} + +// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/... +// direct-action +static void com_android_networkstack_tethering_BpfUtils_tcFilterAddDevBpf( + JNIEnv* env, jobject clazz, jint ifIndex, jboolean ingress, jshort prio, jshort proto, + jstring bpfProgPath) { + ScopedUtfChars pathname(env, bpfProgPath); + + const int bpfFd = bpf::retrieveProgram(pathname.c_str()); + if (bpfFd == -1) { + jniThrowExceptionFmt(env, "java/io/IOException", "retrieveProgram failed %s", + strerror(errno)); + return; + } + + struct { + nlmsghdr n; + tcmsg t; + struct { + nlattr attr; + // The maximum classifier name length is defined in + // tcf_proto_ops in include/net/sch_generic.h. + char str[NLMSG_ALIGN(sizeof(CLS_BPF_KIND_NAME))]; + } kind; + struct { + nlattr attr; + struct { + nlattr attr; + __u32 u32; + } fd; + struct { + nlattr attr; + char str[NLMSG_ALIGN(CLS_BPF_NAME_LEN)]; + } name; + struct { + nlattr attr; + __u32 u32; + } flags; + } options; + } req = { + .n = + { + .nlmsg_len = sizeof(req), + .nlmsg_type = RTM_NEWTFILTER, + .nlmsg_flags = NETLINK_REQUEST_FLAGS | NLM_F_EXCL | NLM_F_CREATE, + }, + .t = + { + .tcm_family = AF_UNSPEC, + .tcm_ifindex = ifIndex, + .tcm_handle = TC_H_UNSPEC, + .tcm_parent = TC_H_MAKE(TC_H_CLSACT, + ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS), + .tcm_info = static_cast<__u32>((static_cast(prio) << 16) | + htons(static_cast(proto))), + }, + .kind = + { + .attr = + { + .nla_len = sizeof(req.kind), + .nla_type = TCA_KIND, + }, + .str = CLS_BPF_KIND_NAME, + }, + .options = + { + .attr = + { + .nla_len = sizeof(req.options), + .nla_type = NLA_F_NESTED | TCA_OPTIONS, + }, + .fd = + { + .attr = + { + .nla_len = sizeof(req.options.fd), + .nla_type = TCA_BPF_FD, + }, + .u32 = static_cast<__u32>(bpfFd), + }, + .name = + { + .attr = + { + .nla_len = sizeof(req.options.name), + .nla_type = TCA_BPF_NAME, + }, + // Visible via 'tc filter show', but + // is overwritten by strncpy below + .str = "placeholder", + }, + .flags = + { + .attr = + { + .nla_len = sizeof(req.options.flags), + .nla_type = TCA_BPF_FLAGS, + }, + .u32 = TCA_BPF_FLAG_ACT_DIRECT, + }, + }, + }; + + snprintf(req.options.name.str, sizeof(req.options.name.str), "%s:[*fsobj]", + basename(pathname.c_str())); + + // The exception may be thrown from sendAndProcessNetlinkResponse. Close the file descriptor of + // BPF program before returning the function in any case. + sendAndProcessNetlinkResponse(env, &req, sizeof(req)); + close(bpfFd); +} + +// tc filter del dev .. in/egress prio .. protocol .. +static void com_android_networkstack_tethering_BpfUtils_tcFilterDelDev(JNIEnv* env, jobject clazz, + jint ifIndex, + jboolean ingress, + jshort prio, jshort proto) { + const struct { + nlmsghdr n; + tcmsg t; + } req = { + .n = + { + .nlmsg_len = sizeof(req), + .nlmsg_type = RTM_DELTFILTER, + .nlmsg_flags = NETLINK_REQUEST_FLAGS, + }, + .t = + { + .tcm_family = AF_UNSPEC, + .tcm_ifindex = ifIndex, + .tcm_handle = TC_H_UNSPEC, + .tcm_parent = TC_H_MAKE(TC_H_CLSACT, + ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS), + .tcm_info = static_cast<__u32>((static_cast(prio) << 16) | + htons(static_cast(proto))), + }, + }; + + sendAndProcessNetlinkResponse(env, &req, sizeof(req)); +} + +/* + * JNI registration. + */ +static const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + {"isEthernet", "(Ljava/lang/String;)Z", + (void*)com_android_networkstack_tethering_BpfUtils_isEthernet}, + {"tcFilterAddDevBpf", "(IZSSLjava/lang/String;)V", + (void*)com_android_networkstack_tethering_BpfUtils_tcFilterAddDevBpf}, + {"tcFilterDelDev", "(IZSS)V", + (void*)com_android_networkstack_tethering_BpfUtils_tcFilterDelDev}, +}; + +int register_com_android_networkstack_tethering_BpfUtils(JNIEnv* env) { + return jniRegisterNativeMethods(env, "com/android/networkstack/tethering/BpfUtils", gMethods, + NELEM(gMethods)); +} + +}; // namespace android diff --git a/Tethering/jni/onload.cpp b/Tethering/jni/onload.cpp new file mode 100644 index 0000000000..02e602d99e --- /dev/null +++ b/Tethering/jni/onload.cpp @@ -0,0 +1,48 @@ +/* + * 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. + */ + +#include +#include "jni.h" + +#define LOG_TAG "TetheringJni" +#include + +namespace android { + +int register_android_net_util_TetheringUtils(JNIEnv* env); +int register_com_android_networkstack_tethering_BpfMap(JNIEnv* env); +int register_com_android_networkstack_tethering_BpfCoordinator(JNIEnv* env); +int register_com_android_networkstack_tethering_BpfUtils(JNIEnv* env); + +extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { + JNIEnv *env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, "ERROR: GetEnv failed"); + return JNI_ERR; + } + + if (register_android_net_util_TetheringUtils(env) < 0) return JNI_ERR; + + if (register_com_android_networkstack_tethering_BpfMap(env) < 0) return JNI_ERR; + + if (register_com_android_networkstack_tethering_BpfCoordinator(env) < 0) return JNI_ERR; + + if (register_com_android_networkstack_tethering_BpfUtils(env) < 0) return JNI_ERR; + + return JNI_VERSION_1_6; +} + +}; // namespace android diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags new file mode 100644 index 0000000000..75ecdce8d2 --- /dev/null +++ b/Tethering/proguard.flags @@ -0,0 +1,17 @@ +# Keep class's integer static field for MessageUtils to parsing their name. +-keep class com.android.networkstack.tethering.Tethering$TetherMainSM { + static final int CMD_*; + static final int EVENT_*; +} + +-keep class com.android.networkstack.tethering.BpfMap { + native ; +} + +-keepclassmembers public class * extends com.android.networkstack.tethering.util.Struct { + *; +} + +-keepclassmembers class android.net.ip.IpServer { + static final int CMD_*; +} diff --git a/Tethering/res/drawable-hdpi/stat_sys_tether_bluetooth.png b/Tethering/res/drawable-hdpi/stat_sys_tether_bluetooth.png new file mode 100644 index 0000000000..9451174d65 Binary files /dev/null and b/Tethering/res/drawable-hdpi/stat_sys_tether_bluetooth.png differ diff --git a/Tethering/res/drawable-hdpi/stat_sys_tether_general.png b/Tethering/res/drawable-hdpi/stat_sys_tether_general.png new file mode 100644 index 0000000000..79d5756ae3 Binary files /dev/null and b/Tethering/res/drawable-hdpi/stat_sys_tether_general.png differ diff --git a/Tethering/res/drawable-hdpi/stat_sys_tether_usb.png b/Tethering/res/drawable-hdpi/stat_sys_tether_usb.png new file mode 100644 index 0000000000..cae1bd1b25 Binary files /dev/null and b/Tethering/res/drawable-hdpi/stat_sys_tether_usb.png differ diff --git a/Tethering/res/drawable-ldpi/stat_sys_tether_bluetooth.png b/Tethering/res/drawable-ldpi/stat_sys_tether_bluetooth.png new file mode 100644 index 0000000000..ffe8e8c982 Binary files /dev/null and b/Tethering/res/drawable-ldpi/stat_sys_tether_bluetooth.png differ diff --git a/Tethering/res/drawable-ldpi/stat_sys_tether_general.png b/Tethering/res/drawable-ldpi/stat_sys_tether_general.png new file mode 100644 index 0000000000..ca20f73520 Binary files /dev/null and b/Tethering/res/drawable-ldpi/stat_sys_tether_general.png differ diff --git a/Tethering/res/drawable-ldpi/stat_sys_tether_usb.png b/Tethering/res/drawable-ldpi/stat_sys_tether_usb.png new file mode 100644 index 0000000000..65e907565e Binary files /dev/null and b/Tethering/res/drawable-ldpi/stat_sys_tether_usb.png differ diff --git a/Tethering/res/drawable-mdpi/stat_sys_tether_bluetooth.png b/Tethering/res/drawable-mdpi/stat_sys_tether_bluetooth.png new file mode 100644 index 0000000000..f42dae0fdc Binary files /dev/null and b/Tethering/res/drawable-mdpi/stat_sys_tether_bluetooth.png differ diff --git a/Tethering/res/drawable-mdpi/stat_sys_tether_general.png b/Tethering/res/drawable-mdpi/stat_sys_tether_general.png new file mode 100644 index 0000000000..065516185a Binary files /dev/null and b/Tethering/res/drawable-mdpi/stat_sys_tether_general.png differ diff --git a/Tethering/res/drawable-mdpi/stat_sys_tether_usb.png b/Tethering/res/drawable-mdpi/stat_sys_tether_usb.png new file mode 100644 index 0000000000..2e2b8ca2e9 Binary files /dev/null and b/Tethering/res/drawable-mdpi/stat_sys_tether_usb.png differ diff --git a/Tethering/res/drawable-xhdpi/stat_sys_tether_bluetooth.png b/Tethering/res/drawable-xhdpi/stat_sys_tether_bluetooth.png new file mode 100644 index 0000000000..3f57d1c76c Binary files /dev/null and b/Tethering/res/drawable-xhdpi/stat_sys_tether_bluetooth.png differ diff --git a/Tethering/res/drawable-xhdpi/stat_sys_tether_general.png b/Tethering/res/drawable-xhdpi/stat_sys_tether_general.png new file mode 100644 index 0000000000..34b0cb3673 Binary files /dev/null and b/Tethering/res/drawable-xhdpi/stat_sys_tether_general.png differ diff --git a/Tethering/res/drawable-xhdpi/stat_sys_tether_usb.png b/Tethering/res/drawable-xhdpi/stat_sys_tether_usb.png new file mode 100644 index 0000000000..36afe485b5 Binary files /dev/null and b/Tethering/res/drawable-xhdpi/stat_sys_tether_usb.png differ diff --git a/Tethering/res/drawable-xxhdpi/stat_sys_tether_bluetooth.png b/Tethering/res/drawable-xxhdpi/stat_sys_tether_bluetooth.png new file mode 100644 index 0000000000..25acfbb01b Binary files /dev/null and b/Tethering/res/drawable-xxhdpi/stat_sys_tether_bluetooth.png differ diff --git a/Tethering/res/drawable-xxhdpi/stat_sys_tether_general.png b/Tethering/res/drawable-xxhdpi/stat_sys_tether_general.png new file mode 100644 index 0000000000..5c656012e6 Binary files /dev/null and b/Tethering/res/drawable-xxhdpi/stat_sys_tether_general.png differ diff --git a/Tethering/res/drawable-xxhdpi/stat_sys_tether_usb.png b/Tethering/res/drawable-xxhdpi/stat_sys_tether_usb.png new file mode 100644 index 0000000000..28b4b5438e Binary files /dev/null and b/Tethering/res/drawable-xxhdpi/stat_sys_tether_usb.png differ diff --git a/Tethering/res/values-af/strings.xml b/Tethering/res/values-af/strings.xml new file mode 100644 index 0000000000..056168b12e --- /dev/null +++ b/Tethering/res/values-af/strings.xml @@ -0,0 +1,29 @@ + + + + + "Verbinding of warmkol is aktief" + "Tik om op te stel." + "Verbinding is gedeaktiveer" + "Kontak jou administrateur vir besonderhede" + "Warmkol- en verbindingstatus" + + + + + + diff --git a/Tethering/res/values-am/strings.xml b/Tethering/res/values-am/strings.xml new file mode 100644 index 0000000000..ac468dd144 --- /dev/null +++ b/Tethering/res/values-am/strings.xml @@ -0,0 +1,29 @@ + + + + + "እንደ ሞደም መሰካት ወይም መገናኛ ነጥብ ገባሪ" + "ለማዋቀር መታ ያድርጉ።" + "እንደ ሞደም መሰካት ተሰናክሏል" + "ለዝርዝሮች የእርስዎን አስተዳዳሪ ያነጋግሩ" + "መገናኛ ነጥብ እና እንደ ሞደም የመሰካት ሁኔታ" + + + + + + diff --git a/Tethering/res/values-ar/strings.xml b/Tethering/res/values-ar/strings.xml new file mode 100644 index 0000000000..7d5bad34da --- /dev/null +++ b/Tethering/res/values-ar/strings.xml @@ -0,0 +1,29 @@ + + + + + "النطاق نشط أو نقطة الاتصال نشطة" + "انقر للإعداد." + "التوصيل متوقف." + "تواصَل مع المشرف للحصول على التفاصيل." + "حالة نقطة الاتصال والتوصيل" + + + + + + diff --git a/Tethering/res/values-as/strings.xml b/Tethering/res/values-as/strings.xml new file mode 100644 index 0000000000..091350455b --- /dev/null +++ b/Tethering/res/values-as/strings.xml @@ -0,0 +1,29 @@ + + + + + "টে\'ডাৰিং অথবা হ\'টস্প\'ট সক্ৰিয় অৱস্থাত আছে" + "ছেট আপ কৰিবলৈ টিপক।" + "টে\'ডাৰিঙৰ সুবিধাটো অক্ষম কৰি থোৱা হৈছে" + "সবিশেষ জানিবলৈ আপোনাৰ প্ৰশাসকৰ সৈতে যোগাযোগ কৰক" + "হ’টস্প\'ট আৰু টে\'ডাৰিঙৰ স্থিতি" + + + + + + diff --git a/Tethering/res/values-az/strings.xml b/Tethering/res/values-az/strings.xml new file mode 100644 index 0000000000..dce70da178 --- /dev/null +++ b/Tethering/res/values-az/strings.xml @@ -0,0 +1,29 @@ + + + + + "Birləşmə və ya hotspot aktivdir" + "Ayarlamaq üçün toxunun." + "Birləşmə deaktivdir" + "Detallar üçün adminlə əlaqə saxlayın" + "Hotspot & birləşmə statusu" + + + + + + diff --git a/Tethering/res/values-b+sr+Latn/strings.xml b/Tethering/res/values-b+sr+Latn/strings.xml new file mode 100644 index 0000000000..b0774ec9a8 --- /dev/null +++ b/Tethering/res/values-b+sr+Latn/strings.xml @@ -0,0 +1,29 @@ + + + + + "Privezivanje ili hotspot je aktivan" + "Dodirnite da biste podesili." + "Privezivanje je onemogućeno" + "Potražite detalje od administratora" + "Status hotspota i privezivanja" + + + + + + diff --git a/Tethering/res/values-be/strings.xml b/Tethering/res/values-be/strings.xml new file mode 100644 index 0000000000..a8acebe2e9 --- /dev/null +++ b/Tethering/res/values-be/strings.xml @@ -0,0 +1,29 @@ + + + + + "Мадэм або хот-спот актыўныя" + "Дакраніцеся, каб наладзіць." + "Рэжым мадэма выключаны" + "Звярніцеся да адміністратара па падрабязную інфармацыю" + "Стан \"Хот-спот і мадэм\"" + + + + + + diff --git a/Tethering/res/values-bg/strings.xml b/Tethering/res/values-bg/strings.xml new file mode 100644 index 0000000000..94fb2d8f17 --- /dev/null +++ b/Tethering/res/values-bg/strings.xml @@ -0,0 +1,29 @@ + + + + + "Има активна споделена връзка или точка за достъп" + "Докоснете, за да настроите." + "Функцията за тетъринг е деактивирана" + "Свържете се с администратора си за подробности" + "Състояние на функцията за точка за достъп и тетъринг" + + + + + + diff --git a/Tethering/res/values-bn/strings.xml b/Tethering/res/values-bn/strings.xml new file mode 100644 index 0000000000..aea02b9ddf --- /dev/null +++ b/Tethering/res/values-bn/strings.xml @@ -0,0 +1,29 @@ + + + + + "টিথারিং বা হটস্পট চালু আছে" + "সেট-আপ করতে ট্যাপ করুন।" + "টিথারিং বন্ধ করা আছে" + "বিশদে জানতে অ্যাডমিনের সাথে যোগাযোগ করুন" + "হটস্পট ও টিথারিং স্ট্যাটাস" + + + + + + diff --git a/Tethering/res/values-bs/strings.xml b/Tethering/res/values-bs/strings.xml new file mode 100644 index 0000000000..de232724c5 --- /dev/null +++ b/Tethering/res/values-bs/strings.xml @@ -0,0 +1,29 @@ + + + + + "Aktivno je povezivanje putem mobitela ili pristupna tačka" + "Dodirnite da postavite." + "Povezivanje putem mobitela je onemogućeno" + "Kontaktirajte svog administratora za detalje" + "Status pristupne tačke i povezivanja putem mobitela" + + + + + + diff --git a/Tethering/res/values-ca/strings.xml b/Tethering/res/values-ca/strings.xml new file mode 100644 index 0000000000..88b795c1f8 --- /dev/null +++ b/Tethering/res/values-ca/strings.xml @@ -0,0 +1,29 @@ + + + + + "Compartició de xarxa o punt d\'accés Wi‑Fi actius" + "Toca per configurar." + "La compartició de xarxa està desactivada" + "Contacta amb el teu administrador per obtenir més informació" + "Estat del punt d\'accés Wi‑Fi i de la compartició de xarxa" + + + + + + diff --git a/Tethering/res/values-cs/strings.xml b/Tethering/res/values-cs/strings.xml new file mode 100644 index 0000000000..8c1b83bf3e --- /dev/null +++ b/Tethering/res/values-cs/strings.xml @@ -0,0 +1,29 @@ + + + + + "Tethering nebo hotspot je aktivní" + "Klepnutím zahájíte nastavení." + "Tethering je zakázán" + "O podrobnosti požádejte administrátora" + "Stav hotspotu a tetheringu" + + + + + + diff --git a/Tethering/res/values-da/strings.xml b/Tethering/res/values-da/strings.xml new file mode 100644 index 0000000000..f413e70548 --- /dev/null +++ b/Tethering/res/values-da/strings.xml @@ -0,0 +1,29 @@ + + + + + "Netdeling eller hotspot er aktivt" + "Tryk for at konfigurere." + "Netdeling er deaktiveret" + "Kontakt din administrator for at få oplysninger" + "Status for hotspot og netdeling" + + + + + + diff --git a/Tethering/res/values-de/strings.xml b/Tethering/res/values-de/strings.xml new file mode 100644 index 0000000000..f057d7824e --- /dev/null +++ b/Tethering/res/values-de/strings.xml @@ -0,0 +1,29 @@ + + + + + "Tethering oder Hotspot aktiv" + "Zum Einrichten tippen." + "Tethering ist deaktiviert" + "Bitte wende dich für weitere Informationen an den Administrator" + "Hotspot- und Tethering-Status" + + + + + + diff --git a/Tethering/res/values-el/strings.xml b/Tethering/res/values-el/strings.xml new file mode 100644 index 0000000000..b3c986bdaf --- /dev/null +++ b/Tethering/res/values-el/strings.xml @@ -0,0 +1,29 @@ + + + + + "Πρόσδεση ή σύνδεση σημείου πρόσβασης ενεργή" + "Πατήστε για ρύθμιση." + "Η σύνδεση είναι απενεργοποιημένη" + "Επικοινωνήστε με τον διαχειριστή σας για λεπτομέρειες" + "Κατάσταση σημείου πρόσβασης Wi-Fi και σύνδεσης" + + + + + + diff --git a/Tethering/res/values-en-rAU/strings.xml b/Tethering/res/values-en-rAU/strings.xml new file mode 100644 index 0000000000..769e01208a --- /dev/null +++ b/Tethering/res/values-en-rAU/strings.xml @@ -0,0 +1,29 @@ + + + + + "Tethering or hotspot active" + "Tap to set up." + "Tethering is disabled" + "Contact your admin for details" + "Hotspot and tethering status" + + + + + + diff --git a/Tethering/res/values-en-rCA/strings.xml b/Tethering/res/values-en-rCA/strings.xml new file mode 100644 index 0000000000..769e01208a --- /dev/null +++ b/Tethering/res/values-en-rCA/strings.xml @@ -0,0 +1,29 @@ + + + + + "Tethering or hotspot active" + "Tap to set up." + "Tethering is disabled" + "Contact your admin for details" + "Hotspot and tethering status" + + + + + + diff --git a/Tethering/res/values-en-rGB/strings.xml b/Tethering/res/values-en-rGB/strings.xml new file mode 100644 index 0000000000..769e01208a --- /dev/null +++ b/Tethering/res/values-en-rGB/strings.xml @@ -0,0 +1,29 @@ + + + + + "Tethering or hotspot active" + "Tap to set up." + "Tethering is disabled" + "Contact your admin for details" + "Hotspot and tethering status" + + + + + + diff --git a/Tethering/res/values-en-rIN/strings.xml b/Tethering/res/values-en-rIN/strings.xml new file mode 100644 index 0000000000..769e01208a --- /dev/null +++ b/Tethering/res/values-en-rIN/strings.xml @@ -0,0 +1,29 @@ + + + + + "Tethering or hotspot active" + "Tap to set up." + "Tethering is disabled" + "Contact your admin for details" + "Hotspot and tethering status" + + + + + + diff --git a/Tethering/res/values-en-rXC/strings.xml b/Tethering/res/values-en-rXC/strings.xml new file mode 100644 index 0000000000..f1674bed4e --- /dev/null +++ b/Tethering/res/values-en-rXC/strings.xml @@ -0,0 +1,29 @@ + + + + + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‏‎‎‏‎‏‏‏‏‏‎‏‏‏‏‎‏‏‎‎‎‏‎‎‎‎‎‏‏‎‏‏‏‏‎‎‎‎‏‏‏‎‎‏‎‎‏‎‏‏‎‏‏‎‎‎‎‎Tethering or hotspot active‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‎‎‏‏‏‎‎‏‏‎‎‎‏‏‏‎‎‎‎‎‎‎‏‏‏‎‎‏‏‏‏‎‏‏‏‏‏‎‏‏‏‏‎‎‏‎‏‏‎‏‎‎‎‏‏‎‎‎‎Tap to set up.‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‎‏‏‎‏‏‎‎‏‎‎‎‏‎‏‎‎‏‎‎‏‎‎‎‏‎‎‎‎‏‏‏‎‏‎‎‎‏‏‏‎‎‎‏‎‏‎‎‎‏‏‎‎‏‏‏‏‏‎Tethering is disabled‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‎‏‎‎‏‏‏‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‏‎‏‏‏‏‏‎‎‏‏‎‎‏‎‎‏‎‏‎‎‏‏‏‎‏‎‏‎‏‎‎‏‎‎‎Contact your admin for details‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‎‎‏‏‏‏‎‏‏‎‏‎‎‎‎‏‏‎‎‏‎‎‏‏‎‎‎‎‏‏‎‏‏‎‏‏‎‎‎‏‏‏‏‏‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎Hotspot & tethering status‎‏‎‎‏‎" + + + + + + diff --git a/Tethering/res/values-es-rUS/strings.xml b/Tethering/res/values-es-rUS/strings.xml new file mode 100644 index 0000000000..63689f4399 --- /dev/null +++ b/Tethering/res/values-es-rUS/strings.xml @@ -0,0 +1,29 @@ + + + + + "Conexión a red o hotspot conectados" + "Presiona para configurar esta opción." + "Se inhabilitó la conexión mediante dispositivo portátil" + "Para obtener más información, comunícate con el administrador" + "Estado del hotspot y la conexión mediante dispositivo portátil" + + + + + + diff --git a/Tethering/res/values-es/strings.xml b/Tethering/res/values-es/strings.xml new file mode 100644 index 0000000000..9a34ed5e38 --- /dev/null +++ b/Tethering/res/values-es/strings.xml @@ -0,0 +1,29 @@ + + + + + "Conexión compartida o punto de acceso activos" + "Toca para configurar." + "La conexión compartida está inhabilitada" + "Solicita más información a tu administrador" + "Estado del punto de acceso y de la conexión compartida" + + + + + + diff --git a/Tethering/res/values-et/strings.xml b/Tethering/res/values-et/strings.xml new file mode 100644 index 0000000000..0970341ab0 --- /dev/null +++ b/Tethering/res/values-et/strings.xml @@ -0,0 +1,29 @@ + + + + + "Jagamine või kuumkoht on aktiivne" + "Puudutage seadistamiseks." + "Jagamine on keelatud" + "Lisateabe saamiseks võtke ühendust oma administraatoriga" + "Kuumkoha ja jagamise olek" + + + + + + diff --git a/Tethering/res/values-eu/strings.xml b/Tethering/res/values-eu/strings.xml new file mode 100644 index 0000000000..632019e2ef --- /dev/null +++ b/Tethering/res/values-eu/strings.xml @@ -0,0 +1,29 @@ + + + + + "Konexioa partekatzea edo wifi-gunea aktibo dago" + "Sakatu konfiguratzeko." + "Desgaituta dago konexioa partekatzeko aukera" + "Xehetasunak lortzeko, jarri administratzailearekin harremanetan" + "Wifi-gunearen eta konexioa partekatzeko eginbidearen egoera" + + + + + + diff --git a/Tethering/res/values-fa/strings.xml b/Tethering/res/values-fa/strings.xml new file mode 100644 index 0000000000..2e21c85fa1 --- /dev/null +++ b/Tethering/res/values-fa/strings.xml @@ -0,0 +1,29 @@ + + + + + "اشتراک‌گذاری اینترنت یا نقطه اتصال فعال" + "برای راه‌اندازی ضربه بزنید." + "اشتراک‌گذاری اینترنت غیرفعال است" + "برای جزئیات، با سرپرستتان تماس بگیرید" + "وضعیت نقطه اتصال و اشتراک‌گذاری اینترنت" + + + + + + diff --git a/Tethering/res/values-fi/strings.xml b/Tethering/res/values-fi/strings.xml new file mode 100644 index 0000000000..413db3f0f8 --- /dev/null +++ b/Tethering/res/values-fi/strings.xml @@ -0,0 +1,29 @@ + + + + + "Yhteyden jakaminen tai hotspot käytössä" + "Ota käyttöön napauttamalla." + "Yhteyden jakaminen on poistettu käytöstä" + "Pyydä lisätietoja järjestelmänvalvojalta" + "Hotspotin ja yhteyden jakamisen tila" + + + + + + diff --git a/Tethering/res/values-fr-rCA/strings.xml b/Tethering/res/values-fr-rCA/strings.xml new file mode 100644 index 0000000000..eb2e4ba540 --- /dev/null +++ b/Tethering/res/values-fr-rCA/strings.xml @@ -0,0 +1,29 @@ + + + + + "Partage de connexion ou point d\'accès sans fil activé" + "Touchez pour configurer." + "Le partage de connexion est désactivé" + "Communiquez avec votre administrateur pour obtenir plus de détails" + "Point d\'accès et partage de connexion" + + + + + + diff --git a/Tethering/res/values-fr/strings.xml b/Tethering/res/values-fr/strings.xml new file mode 100644 index 0000000000..22259c52ab --- /dev/null +++ b/Tethering/res/values-fr/strings.xml @@ -0,0 +1,29 @@ + + + + + "Partage de connexion ou point d\'accès activé" + "Appuyez pour effectuer la configuration." + "Le partage de connexion est désactivé" + "Pour en savoir plus, contactez votre administrateur" + "État du point d\'accès et du partage de connexion" + + + + + + diff --git a/Tethering/res/values-gl/strings.xml b/Tethering/res/values-gl/strings.xml new file mode 100644 index 0000000000..ded82fcd54 --- /dev/null +++ b/Tethering/res/values-gl/strings.xml @@ -0,0 +1,29 @@ + + + + + "Conexión compartida ou zona wifi activada" + "Toca para configurar." + "A conexión compartida está desactivada" + "Contacta co administrador para obter información" + "Estado da zona wifi e da conexión compartida" + + + + + + diff --git a/Tethering/res/values-gu/strings.xml b/Tethering/res/values-gu/strings.xml new file mode 100644 index 0000000000..7cbbc2de3d --- /dev/null +++ b/Tethering/res/values-gu/strings.xml @@ -0,0 +1,29 @@ + + + + + "ઇન્ટરનેટ શેર કરવાની સુવિધા અથવા હૉટસ્પૉટ સક્રિય છે" + "સેટઅપ કરવા માટે ટૅપ કરો." + "ઇન્ટરનેટ શેર કરવાની સુવિધા બંધ કરી છે" + "વિગતો માટે તમારા વ્યવસ્થાપકનો સંપર્ક કરો" + "હૉટસ્પૉટ અને ઇન્ટરનેટ શેર કરવાની સુવિધાનું સ્ટેટસ" + + + + + + diff --git a/Tethering/res/values-hi/strings.xml b/Tethering/res/values-hi/strings.xml new file mode 100644 index 0000000000..08af81b826 --- /dev/null +++ b/Tethering/res/values-hi/strings.xml @@ -0,0 +1,29 @@ + + + + + "टेदरिंग या हॉटस्पॉट चालू है" + "सेट अप करने के लिए टैप करें." + "टेदरिंग बंद है" + "जानकारी के लिए अपने एडमिन से संपर्क करें" + "हॉटस्पॉट और टेदरिंग की स्थिति" + + + + + + diff --git a/Tethering/res/values-hr/strings.xml b/Tethering/res/values-hr/strings.xml new file mode 100644 index 0000000000..827c135f20 --- /dev/null +++ b/Tethering/res/values-hr/strings.xml @@ -0,0 +1,29 @@ + + + + + "Modemsko povezivanje ili žarišna točka aktivni" + "Dodirnite da biste postavili." + "Modemsko je povezivanje onemogućeno" + "Obratite se administratoru da biste saznali pojedinosti" + "Status žarišne točke i modemskog povezivanja" + + + + + + diff --git a/Tethering/res/values-hu/strings.xml b/Tethering/res/values-hu/strings.xml new file mode 100644 index 0000000000..eb68d6babf --- /dev/null +++ b/Tethering/res/values-hu/strings.xml @@ -0,0 +1,29 @@ + + + + + "Megosztás vagy aktív hotspot" + "Koppintson a beállításhoz." + "Az internetmegosztás le van tiltva" + "A részletekért forduljon rendszergazdájához" + "Hotspot és internetmegosztás állapota" + + + + + + diff --git a/Tethering/res/values-hy/strings.xml b/Tethering/res/values-hy/strings.xml new file mode 100644 index 0000000000..912941e538 --- /dev/null +++ b/Tethering/res/values-hy/strings.xml @@ -0,0 +1,29 @@ + + + + + "Մոդեմի ռեժիմը միացված է" + "Հպեք՝ կարգավորելու համար։" + "Մոդեմի ռեժիմն անջատված է" + "Մանրամասների համար դիմեք ձեր ադմինիստրատորին" + "Թեժ կետի և մոդեմի ռեժիմի կարգավիճակը" + + + + + + diff --git a/Tethering/res/values-in/strings.xml b/Tethering/res/values-in/strings.xml new file mode 100644 index 0000000000..a4e175a439 --- /dev/null +++ b/Tethering/res/values-in/strings.xml @@ -0,0 +1,29 @@ + + + + + "Tethering atau hotspot aktif" + "Ketuk untuk menyiapkan." + "Tethering dinonaktifkan" + "Hubungi admin untuk mengetahui detailnya" + "Status hotspot & tethering" + + + + + + diff --git a/Tethering/res/values-is/strings.xml b/Tethering/res/values-is/strings.xml new file mode 100644 index 0000000000..e9f6670bcd --- /dev/null +++ b/Tethering/res/values-is/strings.xml @@ -0,0 +1,29 @@ + + + + + "Kveikt á tjóðrun eða aðgangsstað" + "Ýttu til að setja upp." + "Slökkt er á tjóðrun" + "Hafðu samband við kerfisstjórann til að fá upplýsingar" + "Staða heits reits og tjóðrunar" + + + + + + diff --git a/Tethering/res/values-it/strings.xml b/Tethering/res/values-it/strings.xml new file mode 100644 index 0000000000..ffb9196f5e --- /dev/null +++ b/Tethering/res/values-it/strings.xml @@ -0,0 +1,29 @@ + + + + + "Hotspot o tethering attivo" + "Tocca per impostare." + "Tethering disattivato" + "Contatta il tuo amministratore per avere informazioni dettagliate" + "Stato hotspot e tethering" + + + + + + diff --git a/Tethering/res/values-iw/strings.xml b/Tethering/res/values-iw/strings.xml new file mode 100644 index 0000000000..7adcb47350 --- /dev/null +++ b/Tethering/res/values-iw/strings.xml @@ -0,0 +1,29 @@ + + + + + "נקודה לשיתוף אינטרנט או שיתוף אינטרנט בין מכשירים: בסטטוס פעיל" + "יש להקיש כדי להגדיר." + "שיתוף האינטרנט בין מכשירים מושבת" + "לפרטים, יש לפנות למנהל המערכת" + "סטטוס של נקודה לשיתוף אינטרנט ושיתוף אינטרנט בין מכשירים" + + + + + + diff --git a/Tethering/res/values-ja/strings.xml b/Tethering/res/values-ja/strings.xml new file mode 100644 index 0000000000..f68a73010b --- /dev/null +++ b/Tethering/res/values-ja/strings.xml @@ -0,0 +1,29 @@ + + + + + "テザリングまたはアクセス ポイントが有効です" + "タップしてセットアップします。" + "テザリングは無効に設定されています" + "詳しくは、管理者にお問い合わせください" + "アクセス ポイントとテザリングのステータス" + + + + + + diff --git a/Tethering/res/values-ka/strings.xml b/Tethering/res/values-ka/strings.xml new file mode 100644 index 0000000000..7c22e82bd3 --- /dev/null +++ b/Tethering/res/values-ka/strings.xml @@ -0,0 +1,29 @@ + + + + + "ტეტერინგი ან უსადენო ქსელი აქტიურია" + "შეეხეთ დასაყენებლად." + "ტეტერინგი გათიშულია" + "დამატებითი ინფორმაციისთვის დაუკავშირდით თქვენს ადმინისტრატორს" + "უსადენო ქსელის და ტეტერინგის სტატუსი" + + + + + + diff --git a/Tethering/res/values-kk/strings.xml b/Tethering/res/values-kk/strings.xml new file mode 100644 index 0000000000..0857d06de2 --- /dev/null +++ b/Tethering/res/values-kk/strings.xml @@ -0,0 +1,29 @@ + + + + + "Тетеринг немесе хотспот қосулы" + "Реттеу үшін түртіңіз." + "Тетеринг өшірілді." + "Мәліметтерді әкімшіден алыңыз." + "Хотспот және тетеринг күйі" + + + + + + diff --git a/Tethering/res/values-km/strings.xml b/Tethering/res/values-km/strings.xml new file mode 100644 index 0000000000..536e3d1703 --- /dev/null +++ b/Tethering/res/values-km/strings.xml @@ -0,0 +1,29 @@ + + + + + "ការភ្ជាប់ ឬហតស្ប៉ត​កំពុងដំណើរការ" + "ចុច​ដើម្បី​រៀបចំ។" + "ការភ្ជាប់​ត្រូវបានបិទ" + "ទាក់ទងអ្នកគ្រប់គ្រង​របស់អ្នក ដើម្បីទទួលបានព័ត៌មានលម្អិត" + "ស្ថានភាពនៃការភ្ជាប់ និងហតស្ប៉ត" + + + + + + diff --git a/Tethering/res/values-kn/strings.xml b/Tethering/res/values-kn/strings.xml new file mode 100644 index 0000000000..32f54926f4 --- /dev/null +++ b/Tethering/res/values-kn/strings.xml @@ -0,0 +1,29 @@ + + + + + "ಟೆಥರಿಂಗ್ ಅಥವಾ ಹಾಟ್‌ಸ್ಪಾಟ್ ಸಕ್ರಿಯವಾಗಿದೆ" + "ಸೆಟಪ್ ಮಾಡಲು ಟ್ಯಾಪ್ ಮಾಡಿ." + "ಟೆಥರಿಂಗ್ ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ" + "ವಿವರಗಳಿಗಾಗಿ ನಿಮ್ಮ ನಿರ್ವಾಹಕರನ್ನು ಸಂಪರ್ಕಿಸಿ" + "ಹಾಟ್‌ಸ್ಪಾಟ್ ಮತ್ತು ಟೆಥರಿಂಗ್‌ ಸ್ಥಿತಿ" + + + + + + diff --git a/Tethering/res/values-ko/strings.xml b/Tethering/res/values-ko/strings.xml new file mode 100644 index 0000000000..156b24786d --- /dev/null +++ b/Tethering/res/values-ko/strings.xml @@ -0,0 +1,29 @@ + + + + + "테더링 또는 핫스팟 사용" + "설정하려면 탭하세요." + "테더링이 사용 중지됨" + "자세한 정보는 관리자에게 문의하세요." + "핫스팟 및 테더링 상태" + + + + + + diff --git a/Tethering/res/values-ky/strings.xml b/Tethering/res/values-ky/strings.xml new file mode 100644 index 0000000000..18ee5fd357 --- /dev/null +++ b/Tethering/res/values-ky/strings.xml @@ -0,0 +1,29 @@ + + + + + "Модем режими күйүп турат" + "Жөндөө үчүн таптап коюңуз." + "Телефонду модем катары колдонууга болбойт" + "Кеңири маалымат үчүн администраторуңузга кайрылыңыз" + "Байланыш түйүнүнүн жана модем режиминин статусу" + + + + + + diff --git a/Tethering/res/values-lo/strings.xml b/Tethering/res/values-lo/strings.xml new file mode 100644 index 0000000000..b12767018c --- /dev/null +++ b/Tethering/res/values-lo/strings.xml @@ -0,0 +1,29 @@ + + + + + "ເປີດການປ່ອຍສັນຍານ ຫຼື ຮັອດສະປອດແລ້ວ" + "ແຕະເພື່ອຕັ້ງຄ່າ." + "ການປ່ອຍສັນຍານຖືກປິດໄວ້" + "ຕິດຕໍ່ຜູ້ເບິ່ງແຍງລະບົບສຳລັບລາຍລະອຽດ" + "ສະຖານະຮັອດສະປອດ ແລະ ການປ່ອຍສັນຍານ" + + + + + + diff --git a/Tethering/res/values-lt/strings.xml b/Tethering/res/values-lt/strings.xml new file mode 100644 index 0000000000..8427baf39f --- /dev/null +++ b/Tethering/res/values-lt/strings.xml @@ -0,0 +1,29 @@ + + + + + "Įrenginys naudojamas kaip modemas arba įjungtas viešosios interneto prieigos taškas" + "Palieskite, kad nustatytumėte." + "Įrenginio kaip modemo naudojimas išjungtas" + "Jei reikia išsamios informacijos, susisiekite su administratoriumi" + "Viešosios interneto prieigos taško ir įrenginio kaip modemo naudojimo būsena" + + + + + + diff --git a/Tethering/res/values-lv/strings.xml b/Tethering/res/values-lv/strings.xml new file mode 100644 index 0000000000..aa2d6990e0 --- /dev/null +++ b/Tethering/res/values-lv/strings.xml @@ -0,0 +1,29 @@ + + + + + "Piesaiste vai tīklājs ir aktīvs." + "Pieskarieties, lai to iestatītu." + "Piesaiste ir atspējota" + "Lai iegūtu detalizētu informāciju, sazinieties ar savu administratoru." + "Tīklāja un piesaistes statuss" + + + + + + diff --git a/Tethering/res/values-mcc204-mnc04-af/strings.xml b/Tethering/res/values-mcc204-mnc04-af/strings.xml new file mode 100644 index 0000000000..052ca091ac --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-af/strings.xml @@ -0,0 +1,25 @@ + + + + + "Warmkol het nie internet nie" + "Toestelle kan nie aan internet koppel nie" + "Skakel warmkol af" + "Warmkol is aan" + "Bykomende heffings kan geld terwyl jy swerf" + "Gaan voort" + diff --git a/Tethering/res/values-mcc204-mnc04-am/strings.xml b/Tethering/res/values-mcc204-mnc04-am/strings.xml new file mode 100644 index 0000000000..0518c5a14f --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-am/strings.xml @@ -0,0 +1,25 @@ + + + + + "መገናኛ ነጥቡ በይነመረብ የለውም" + "መሣሪያዎች ከበይነመረብ ጋር መገናኘት አይችሉም" + "መገናኛ ነጥብ ያጥፉ" + "የመገናኛ ነጥብ በርቷል" + "በሚያንዣብብበት ጊዜ ተጨማሪ ክፍያዎች ተፈጻሚ ሊሆኑ ይችላሉ" + "ቀጥል" + diff --git a/Tethering/res/values-mcc204-mnc04-ar/strings.xml b/Tethering/res/values-mcc204-mnc04-ar/strings.xml new file mode 100644 index 0000000000..e6d8423f46 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-ar/strings.xml @@ -0,0 +1,25 @@ + + + + + "نقطة الاتصال غير متصلة بالإنترنت." + "لا يمكن للأجهزة الاتصال بالإنترنت." + "إيقاف نقطة الاتصال" + "نقطة الاتصال مفعّلة" + "قد يتم تطبيق رسوم إضافية أثناء التجوال." + "متابعة" + diff --git a/Tethering/res/values-mcc204-mnc04-as/strings.xml b/Tethering/res/values-mcc204-mnc04-as/strings.xml new file mode 100644 index 0000000000..4c57f21eae --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-as/strings.xml @@ -0,0 +1,25 @@ + + + + + "হটস্পটৰ কোনো ইণ্টাৰনেট নাই" + "ডিভাইচসমূহ ইণ্টাৰনেটৰ সৈতে সংযোগ কৰিব নোৱাৰি" + "হটস্পট অফ কৰক" + "হটস্পট অন হৈ আছে" + "ৰ\'মিঙত থাকিলে অতিৰিক্ত মাচুল প্ৰযোজ্য হ’ব পাৰে" + "অব্যাহত ৰাখক" + diff --git a/Tethering/res/values-mcc204-mnc04-az/strings.xml b/Tethering/res/values-mcc204-mnc04-az/strings.xml new file mode 100644 index 0000000000..2610ab1bec --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-az/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspotun internetə girişi yoxdur" + "Cihazlar internetə qoşula bilmir" + "Hotspot\'u deaktiv edin" + "Hotspot aktivdir" + "Rouminq zamanı əlavə ödənişlər tətbiq edilə bilər" + "Davam edin" + diff --git a/Tethering/res/values-mcc204-mnc04-b+sr+Latn/strings.xml b/Tethering/res/values-mcc204-mnc04-b+sr+Latn/strings.xml new file mode 100644 index 0000000000..7b032badf0 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-b+sr+Latn/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspot nema pristup internetu" + "Uređaji ne mogu da se povežu na internet" + "Isključi hotspot" + "Hotspot je uključen" + "Možda važe dodatni troškovi u romingu" + "Nastavi" + diff --git a/Tethering/res/values-mcc204-mnc04-be/strings.xml b/Tethering/res/values-mcc204-mnc04-be/strings.xml new file mode 100644 index 0000000000..2362a1e6a5 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-be/strings.xml @@ -0,0 +1,25 @@ + + + + + "Хот-спот не падключаны да інтэрнэту" + "Прылады не могуць падключацца да інтэрнэту" + "Выключыць хот-спот" + "Хот-спот уключаны" + "Пры выкарыстанні роўмінгу можа спаганяцца дадатковая плата" + "Працягнуць" + diff --git a/Tethering/res/values-mcc204-mnc04-bg/strings.xml b/Tethering/res/values-mcc204-mnc04-bg/strings.xml new file mode 100644 index 0000000000..6ef1b0bbaf --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-bg/strings.xml @@ -0,0 +1,25 @@ + + + + + "Точката за достъп няма връзка с интернет" + "Устройствата не могат да се свържат с интернет" + "Изключване на точката за достъп" + "Точката за достъп е включена" + "Възможно е да ви бъдат начислени допълнителни такси при роуминг" + "Напред" + diff --git a/Tethering/res/values-mcc204-mnc04-bn/strings.xml b/Tethering/res/values-mcc204-mnc04-bn/strings.xml new file mode 100644 index 0000000000..9a3033c94d --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-bn/strings.xml @@ -0,0 +1,25 @@ + + + + + "হটস্পটের সাথে ইন্টারনেট কানেক্ট করা নেই" + "ডিভাইস ইন্টারনেটের সাথে কানেক্ট করতে পারছে না" + "হটস্পট বন্ধ করুন" + "হটস্পট চালু আছে" + "রোমিংয়ের সময় অতিরিক্ত চার্জ করা হতে পারে" + "চালিয়ে যান" + diff --git a/Tethering/res/values-mcc204-mnc04-bs/strings.xml b/Tethering/res/values-mcc204-mnc04-bs/strings.xml new file mode 100644 index 0000000000..57f6d88a4e --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-bs/strings.xml @@ -0,0 +1,25 @@ + + + + + "Pristupna tačka nema internet" + "Uređaji se ne mogu povezati na internet" + "Isključi pristupnu tačku" + "Pristupna tačka je uključena" + "Mogu nastati dodatni troškovi u romingu" + "Nastavi" + diff --git a/Tethering/res/values-mcc204-mnc04-ca/strings.xml b/Tethering/res/values-mcc204-mnc04-ca/strings.xml new file mode 100644 index 0000000000..e3ad666c0b --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-ca/strings.xml @@ -0,0 +1,25 @@ + + + + + "El punt d\'accés Wi‑Fi no té accés a Internet" + "Els dispositius no es poden connectar a Internet" + "Desactiva el punt d\'accés Wi‑Fi" + "El punt d\'accés Wi‑Fi està activat" + "És possible que s\'apliquin costos addicionals en itinerància" + "Continua" + diff --git a/Tethering/res/values-mcc204-mnc04-cs/strings.xml b/Tethering/res/values-mcc204-mnc04-cs/strings.xml new file mode 100644 index 0000000000..f0992814c1 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-cs/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspot nemá připojení k internetu" + "Zařízení se nemohou připojit k internetu" + "Vypnout hotspot" + "Hotspot je aktivní" + "Při roamingu mohou být účtovány dodatečné poplatky" + "Pokračovat" + diff --git a/Tethering/res/values-mcc204-mnc04-da/strings.xml b/Tethering/res/values-mcc204-mnc04-da/strings.xml new file mode 100644 index 0000000000..1fb2374487 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-da/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspottet har intet internet" + "Enheder kan ikke oprette forbindelse til internettet" + "Deaktiver hotspot" + "Hotspottet er aktiveret" + "Der opkræves muligvis yderligere gebyrer ved roaming" + "Fortsæt" + diff --git a/Tethering/res/values-mcc204-mnc04-de/strings.xml b/Tethering/res/values-mcc204-mnc04-de/strings.xml new file mode 100644 index 0000000000..56d1d1df58 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-de/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspot ist nicht mit dem Internet verbunden" + "Geräte können nicht mit dem Internet verbunden werden" + "Hotspot deaktivieren" + "Hotspot aktiviert" + "Für das Roaming können zusätzliche Gebühren anfallen" + "Weiter" + diff --git a/Tethering/res/values-mcc204-mnc04-el/strings.xml b/Tethering/res/values-mcc204-mnc04-el/strings.xml new file mode 100644 index 0000000000..674f1f6798 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-el/strings.xml @@ -0,0 +1,25 @@ + + + + + "Το σημείο πρόσβασης Wi-Fi δεν έχει πρόσβαση στο διαδίκτυο." + "Δεν είναι η δυνατή η σύνδεση των συσκευών στο διαδίκτυο." + "Απενεργοποίηση σημείου πρόσβασης Wi-Fi" + "Σημείο πρόσβασης Wi-Fi ενεργό" + "Ενδέχεται να ισχύουν επιπλέον χρεώσεις κατά την περιαγωγή." + "Συνέχεια" + diff --git a/Tethering/res/values-mcc204-mnc04-en-rAU/strings.xml b/Tethering/res/values-mcc204-mnc04-en-rAU/strings.xml new file mode 100644 index 0000000000..3046a3725d --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-en-rAU/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspot has no Internet" + "Devices can’t connect to Internet" + "Turn off hotspot" + "Hotspot is on" + "Additional charges may apply while roaming" + "Continue" + diff --git a/Tethering/res/values-mcc204-mnc04-en-rCA/strings.xml b/Tethering/res/values-mcc204-mnc04-en-rCA/strings.xml new file mode 100644 index 0000000000..3046a3725d --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-en-rCA/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspot has no Internet" + "Devices can’t connect to Internet" + "Turn off hotspot" + "Hotspot is on" + "Additional charges may apply while roaming" + "Continue" + diff --git a/Tethering/res/values-mcc204-mnc04-en-rGB/strings.xml b/Tethering/res/values-mcc204-mnc04-en-rGB/strings.xml new file mode 100644 index 0000000000..3046a3725d --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-en-rGB/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspot has no Internet" + "Devices can’t connect to Internet" + "Turn off hotspot" + "Hotspot is on" + "Additional charges may apply while roaming" + "Continue" + diff --git a/Tethering/res/values-mcc204-mnc04-en-rIN/strings.xml b/Tethering/res/values-mcc204-mnc04-en-rIN/strings.xml new file mode 100644 index 0000000000..3046a3725d --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-en-rIN/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspot has no Internet" + "Devices can’t connect to Internet" + "Turn off hotspot" + "Hotspot is on" + "Additional charges may apply while roaming" + "Continue" + diff --git a/Tethering/res/values-mcc204-mnc04-en-rXC/strings.xml b/Tethering/res/values-mcc204-mnc04-en-rXC/strings.xml new file mode 100644 index 0000000000..20c9b94cd5 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-en-rXC/strings.xml @@ -0,0 +1,25 @@ + + + + + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‎‏‎‏‏‏‎‏‏‎‏‏‎‎‎‎‏‎‏‏‏‏‏‏‎‎‏‎‎‎‏‎‎‏‎‏‎‏‎‎‎‏‏‎‏‎‏‏‏‏‏‎‏‎‎‎Hotspot has no internet‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‎‏‏‎‎‎‎‏‏‏‏‎‏‏‏‎‎‏‏‏‏‎‏‏‎‏‏‏‎‏‎‏‎‎‏‏‏‎‏‎‎‏‏‎‎‏‎‎‏‎‎‏‎‏‏‎‏‏‎Devices can’t connect to internet‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‎‎‎‏‎‎‎‏‏‎‎‏‎‎‏‏‎‏‎‎‏‎‏‎‎‏‏‎‎‎‎‏‎‎‎‎‎‏‏‏‎‎‏‏‏‏‏‎‎‎‎‏‎‏‎‎‏‏‎Turn off hotspot‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‎‎‏‏‎‏‏‏‎‎‏‎‎‏‏‎‎‎‏‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‏‎‏‎‏‎‏‏‏‏‏‎‏‎‏‏‏‎‎‎‎‎Hotspot is on‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‏‎‏‏‎‎‎‏‎‏‎‎‏‎‎‎‎‎‏‏‏‏‏‎‏‏‎‏‏‏‏‏‎‏‏‎‎‎‏‏‏‏‏‏‎‎‏‎‏‎‏‏‏‎‎‏‎‎Additional charges may apply while roaming‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‏‏‏‏‎‎‎‏‏‎‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‎‏‎‏‏‎‏‎‎‏‏‏‏‏‎‎‎‎‎‏‏‏‎‎‏‎‎‏‏‎‎Continue‎‏‎‎‏‎" + diff --git a/Tethering/res/values-mcc204-mnc04-es-rUS/strings.xml b/Tethering/res/values-mcc204-mnc04-es-rUS/strings.xml new file mode 100644 index 0000000000..956547cc6d --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-es-rUS/strings.xml @@ -0,0 +1,25 @@ + + + + + "El hotspot no tiene conexión a Internet" + "Los dispositivos no pueden conectarse a Internet" + "Desactiva el hotspot" + "El hotspot está activado" + "Es posible que se apliquen cargos adicionales por roaming" + "Continuar" + diff --git a/Tethering/res/values-mcc204-mnc04-es/strings.xml b/Tethering/res/values-mcc204-mnc04-es/strings.xml new file mode 100644 index 0000000000..831ec1fb1e --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-es/strings.xml @@ -0,0 +1,25 @@ + + + + + "El punto de acceso no tiene conexión a Internet" + "Los dispositivos no se pueden conectar a Internet" + "Desactivar punto de acceso" + "Punto de acceso activado" + "Puede que se apliquen cargos adicionales en itinerancia" + "Continuar" + diff --git a/Tethering/res/values-mcc204-mnc04-et/strings.xml b/Tethering/res/values-mcc204-mnc04-et/strings.xml new file mode 100644 index 0000000000..ff8dde5422 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-et/strings.xml @@ -0,0 +1,25 @@ + + + + + "Kuumkohal puudub Interneti-ühendus" + "Seadmed ei saa Internetiga ühendust luua" + "Lülita kuumkoht välja" + "Kuumkoht on sees" + "Rändluse kasutamisega võivad kaasneda lisatasud" + "Jätka" + diff --git a/Tethering/res/values-mcc204-mnc04-eu/strings.xml b/Tethering/res/values-mcc204-mnc04-eu/strings.xml new file mode 100644 index 0000000000..c4f70a3eb4 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-eu/strings.xml @@ -0,0 +1,25 @@ + + + + + "Sare publikoak ez du Interneteko konexiorik" + "Gailuak ezin dira konektatu Internetera" + "Desaktibatu sare publikoa" + "Sare publikoa aktibatuta dago" + "Baliteke kostu gehigarriak ordaindu behar izatea ibiltaritzan" + "Egin aurrera" + diff --git a/Tethering/res/values-mcc204-mnc04-fa/strings.xml b/Tethering/res/values-mcc204-mnc04-fa/strings.xml new file mode 100644 index 0000000000..79e3ef11d6 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-fa/strings.xml @@ -0,0 +1,25 @@ + + + + + "نقطه اتصال به اینترنت دسترسی ندارد" + "دستگاه‌ها به اینترنت متصل نشدند" + "نقطه اتصال را خاموش کنید" + "نقطه اتصال روشن است" + "ممکن است درحین فراگردی تغییرات دیگر اعمال شود" + "ادامه" + diff --git a/Tethering/res/values-mcc204-mnc04-fi/strings.xml b/Tethering/res/values-mcc204-mnc04-fi/strings.xml new file mode 100644 index 0000000000..64921bca9f --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-fi/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspotilla ei ole internetyhteyttä" + "Laitteet eivät voi yhdistää internetiin" + "Laita hotspot pois päältä" + "Hotspot on päällä" + "Roaming voi aiheuttaa lisämaksuja" + "Jatka" + diff --git a/Tethering/res/values-mcc204-mnc04-fr-rCA/strings.xml b/Tethering/res/values-mcc204-mnc04-fr-rCA/strings.xml new file mode 100644 index 0000000000..eda7b59761 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-fr-rCA/strings.xml @@ -0,0 +1,25 @@ + + + + + "Le point d\'accès n\'est pas connecté à Internet" + "Appareils non connectés à Internet" + "Désactiver le point d\'accès" + "Le point d\'accès est activé" + "En itinérance, des frais supplémentaires peuvent s\'appliquer" + "Continuer" + diff --git a/Tethering/res/values-mcc204-mnc04-fr/strings.xml b/Tethering/res/values-mcc204-mnc04-fr/strings.xml new file mode 100644 index 0000000000..eda7b59761 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-fr/strings.xml @@ -0,0 +1,25 @@ + + + + + "Le point d\'accès n\'est pas connecté à Internet" + "Appareils non connectés à Internet" + "Désactiver le point d\'accès" + "Le point d\'accès est activé" + "En itinérance, des frais supplémentaires peuvent s\'appliquer" + "Continuer" + diff --git a/Tethering/res/values-mcc204-mnc04-gl/strings.xml b/Tethering/res/values-mcc204-mnc04-gl/strings.xml new file mode 100644 index 0000000000..c163c61fbd --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-gl/strings.xml @@ -0,0 +1,25 @@ + + + + + "A zona wifi non ten acceso a Internet" + "Os dispositivos non se poden conectar a Internet" + "Desactivar zona wifi" + "A zona wifi está activada" + "Pódense aplicar cargos adicionais en itinerancia" + "Continuar" + diff --git a/Tethering/res/values-mcc204-mnc04-gu/strings.xml b/Tethering/res/values-mcc204-mnc04-gu/strings.xml new file mode 100644 index 0000000000..796d42ec52 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-gu/strings.xml @@ -0,0 +1,25 @@ + + + + + "હૉટસ્પૉટથી ઇન્ટરનેટ ચાલી રહ્યું નથી" + "ડિવાઇસ, ઇન્ટરનેટ સાથે કનેક્ટ થઈ શકતા નથી" + "હૉટસ્પૉટ બંધ કરો" + "હૉટસ્પૉટ ચાલુ છે" + "રોમિંગમાં વધારાના શુલ્ક લાગી શકે છે" + "આગળ વધો" + diff --git a/Tethering/res/values-mcc204-mnc04-hi/strings.xml b/Tethering/res/values-mcc204-mnc04-hi/strings.xml new file mode 100644 index 0000000000..a2442009b5 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-hi/strings.xml @@ -0,0 +1,25 @@ + + + + + "हॉटस्पॉट से इंटरनेट नहीं चल रहा" + "डिवाइस इंटरनेट से कनेक्ट नहीं हो पा रहे" + "हॉटस्पॉट बंद करें" + "हॉटस्पॉट चालू है" + "रोमिंग के दौरान अतिरिक्त शुल्क लग सकता है" + "जारी रखें" + diff --git a/Tethering/res/values-mcc204-mnc04-hr/strings.xml b/Tethering/res/values-mcc204-mnc04-hr/strings.xml new file mode 100644 index 0000000000..41618afb2e --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-hr/strings.xml @@ -0,0 +1,25 @@ + + + + + "Žarišna točka nema pristup internetu" + "Uređaji se ne mogu povezati s internetom" + "Isključi žarišnu točku" + "Žarišna je točka uključena" + "U roamingu su mogući dodatni troškovi" + "Nastavi" + diff --git a/Tethering/res/values-mcc204-mnc04-hu/strings.xml b/Tethering/res/values-mcc204-mnc04-hu/strings.xml new file mode 100644 index 0000000000..39b7a6975b --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-hu/strings.xml @@ -0,0 +1,25 @@ + + + + + "A hotspot nem csatlakozik az internethez" + "Az eszközök nem tudnak csatlakozni az internethez" + "Hotspot kikapcsolása" + "A hotspot be van kapcsolva" + "Roaming során további díjak léphetnek fel" + "Tovább" + diff --git a/Tethering/res/values-mcc204-mnc04-hy/strings.xml b/Tethering/res/values-mcc204-mnc04-hy/strings.xml new file mode 100644 index 0000000000..c14ae10ad1 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-hy/strings.xml @@ -0,0 +1,25 @@ + + + + + "Թեժ կետը միացված չէ ինտերնետին" + "Սարքերը չեն կարողանում միանալ ինտերնետին" + "Անջատել թեժ կետը" + "Թեժ կետը միացված է" + "Ռոումինգում կարող են լրացուցիչ վճարներ գանձվել" + "Շարունակել" + diff --git a/Tethering/res/values-mcc204-mnc04-in/strings.xml b/Tethering/res/values-mcc204-mnc04-in/strings.xml new file mode 100644 index 0000000000..1243d22d19 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-in/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspot tidak memiliki koneksi internet" + "Perangkat tidak dapat tersambung ke internet" + "Nonaktifkan hotspot" + "Hotspot aktif" + "Biaya tambahan mungkin berlaku saat roaming" + "Lanjutkan" + diff --git a/Tethering/res/values-mcc204-mnc04-is/strings.xml b/Tethering/res/values-mcc204-mnc04-is/strings.xml new file mode 100644 index 0000000000..82a7d01234 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-is/strings.xml @@ -0,0 +1,25 @@ + + + + + "Heitur reitur er ekki nettengdur" + "Tæki geta ekki tengst við internetið" + "Slökkva á heitum reit" + "Kveikt er á heitum reit" + "Viðbótargjöld kunna að eiga við í reiki" + "Halda áfram" + diff --git a/Tethering/res/values-mcc204-mnc04-it/strings.xml b/Tethering/res/values-mcc204-mnc04-it/strings.xml new file mode 100644 index 0000000000..a0f52dc89b --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-it/strings.xml @@ -0,0 +1,25 @@ + + + + + "L\'hotspot non ha accesso a Internet" + "I dispositivi non possono connettersi a Internet" + "Disattiva l\'hotspot" + "Hotspot attivo" + "Potrebbero essere applicati costi aggiuntivi durante il roaming" + "Continua" + diff --git a/Tethering/res/values-mcc204-mnc04-iw/strings.xml b/Tethering/res/values-mcc204-mnc04-iw/strings.xml new file mode 100644 index 0000000000..80807bc232 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-iw/strings.xml @@ -0,0 +1,25 @@ + + + + + "לנקודה לשיתוף אינטרנט אין חיבור לאינטרנט" + "המכשירים לא יכולים להתחבר לאינטרנט" + "כיבוי הנקודה לשיתוף אינטרנט" + "הנקודה לשיתוף אינטרנט פועלת" + "ייתכנו חיובים נוספים בעת נדידה" + "המשך" + diff --git a/Tethering/res/values-mcc204-mnc04-ja/strings.xml b/Tethering/res/values-mcc204-mnc04-ja/strings.xml new file mode 100644 index 0000000000..0e21a7f322 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-ja/strings.xml @@ -0,0 +1,25 @@ + + + + + "アクセス ポイントがインターネットに接続されていません" + "デバイスをインターネットに接続できません" + "アクセス ポイントを OFF にする" + "アクセス ポイント: ON" + "ローミング時に追加料金が発生することがあります" + "続行" + diff --git a/Tethering/res/values-mcc204-mnc04-ka/strings.xml b/Tethering/res/values-mcc204-mnc04-ka/strings.xml new file mode 100644 index 0000000000..6d3b548744 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-ka/strings.xml @@ -0,0 +1,25 @@ + + + + + "უსადენო ქსელს არ აქვს ინტერნეტზე წვდომა" + "მოწყობილობები ვერ უკავშირდება ინტერნეტს" + "გამორთეთ უსადენო ქსელი" + "უსადენო ქსელი ჩართულია" + "როუმინგის გამოყენებისას შეიძლება ჩამოგეჭრათ დამატებითი საფასური" + "გაგრძელება" + diff --git a/Tethering/res/values-mcc204-mnc04-kk/strings.xml b/Tethering/res/values-mcc204-mnc04-kk/strings.xml new file mode 100644 index 0000000000..985fc3ff99 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-kk/strings.xml @@ -0,0 +1,25 @@ + + + + + "Хотспотта интернет жоқ" + "Құрылғылар интернетке қосылмайды" + "Хотспотты өшіру" + "Хотспот қосулы" + "Роуминг кезінде қосымша ақы алынуы мүмкін." + "Жалғастыру" + diff --git a/Tethering/res/values-mcc204-mnc04-km/strings.xml b/Tethering/res/values-mcc204-mnc04-km/strings.xml new file mode 100644 index 0000000000..03b5cb6e4b --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-km/strings.xml @@ -0,0 +1,25 @@ + + + + + "ហតស្ប៉ត​មិនមាន​អ៊ីនធឺណិត​ទេ" + "ឧបករណ៍​មិនអាច​ភ្ជាប់​អ៊ីនធឺណិត​បានទេ" + "បិទ​ហតស្ប៉ត" + "ហតស្ប៉ត​ត្រូវបានបើក" + "អាចមាន​ការគិតថ្លៃ​បន្ថែម នៅពេល​រ៉ូមីង" + "បន្ត" + diff --git a/Tethering/res/values-mcc204-mnc04-kn/strings.xml b/Tethering/res/values-mcc204-mnc04-kn/strings.xml new file mode 100644 index 0000000000..f0adad8e21 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-kn/strings.xml @@ -0,0 +1,25 @@ + + + + + "ಹಾಟ್‌ಸ್ಪಾಟ್ ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಸಂಪರ್ಕವನ್ನು ಹೊಂದಿಲ್ಲ" + "ಇಂಟರ್ನೆಟ್‌ಗೆ ಸಂಪರ್ಕಗೊಳ್ಳಲು ಸಾಧನಗಳಿಗೆ ಸಾಧ್ಯವಾಗುತ್ತಿಲ್ಲ" + "ಹಾಟ್‌ಸ್ಪಾಟ್ ಆಫ್‌ ಮಾಡಿ" + "ಹಾಟ್‌ಸ್ಪಾಟ್ ಆನ್ ಆಗಿದೆ" + "ರೋಮಿಂಗ್‌ನಲ್ಲಿರುವಾಗ ಹೆಚ್ಚುವರಿ ಶುಲ್ಕಗಳು ಅನ್ವಯವಾಗಬಹುದು" + "ಮುಂದುವರಿಸಿ" + diff --git a/Tethering/res/values-mcc204-mnc04-ko/strings.xml b/Tethering/res/values-mcc204-mnc04-ko/strings.xml new file mode 100644 index 0000000000..9218e9a09b --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-ko/strings.xml @@ -0,0 +1,25 @@ + + + + + "핫스팟이 인터넷에 연결되지 않음" + "기기를 인터넷에 연결할 수 없음" + "핫스팟 사용 중지" + "핫스팟 사용 중" + "로밍 중에는 추가 요금이 발생할 수 있습니다." + "계속" + diff --git a/Tethering/res/values-mcc204-mnc04-ky/strings.xml b/Tethering/res/values-mcc204-mnc04-ky/strings.xml new file mode 100644 index 0000000000..35a060aa24 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-ky/strings.xml @@ -0,0 +1,25 @@ + + + + + "Байланыш түйүнүндө Интернет жок" + "Түзмөктөр Интернетке туташпай жатат" + "Туташуу түйүнүн өчүрүү" + "Кошулуу түйүнү күйүк" + "Роумингде кошумча акы алынышы мүмкүн" + "Улантуу" + diff --git a/Tethering/res/values-mcc204-mnc04-lo/strings.xml b/Tethering/res/values-mcc204-mnc04-lo/strings.xml new file mode 100644 index 0000000000..1d9203b369 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-lo/strings.xml @@ -0,0 +1,25 @@ + + + + + "ຮັອດສະປອດບໍ່ມີອິນເຕີເນັດ" + "ອຸປະກອນບໍ່ສາມາດເຊື່ອມຕໍ່ອິນເຕີເນັດໄດ້" + "ປິດຮັອດສະປອດ" + "ຮັອດສະປອດເປີດຢູ່" + "ອາດມີຄ່າໃຊ້ຈ່າຍເພີ່ມເຕີມໃນລະຫວ່າງການໂຣມມິງ" + "ສືບຕໍ່" + diff --git a/Tethering/res/values-mcc204-mnc04-lt/strings.xml b/Tethering/res/values-mcc204-mnc04-lt/strings.xml new file mode 100644 index 0000000000..db5178bf2d --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-lt/strings.xml @@ -0,0 +1,25 @@ + + + + + "Nėra viešosios interneto prieigos taško interneto ryšio" + "Įrenginiams nepavyksta prisijungti prie interneto" + "Išjungti viešosios interneto prieigos tašką" + "Viešosios interneto prieigos taškas įjungtas" + "Veikiant tarptinkliniam ryšiui gali būti taikomi papildomi mokesčiai" + "Tęsti" + diff --git a/Tethering/res/values-mcc204-mnc04-lv/strings.xml b/Tethering/res/values-mcc204-mnc04-lv/strings.xml new file mode 100644 index 0000000000..c712173ca2 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-lv/strings.xml @@ -0,0 +1,25 @@ + + + + + "Tīklājam nav interneta savienojuma" + "Ierīces nevar izveidot savienojumu ar internetu" + "Izslēgt tīklāju" + "Tīklājs ir ieslēgts" + "Viesabonēšanas laikā var tikt piemērota papildu samaksa" + "Tālāk" + diff --git a/Tethering/res/values-mcc204-mnc04-mk/strings.xml b/Tethering/res/values-mcc204-mnc04-mk/strings.xml new file mode 100644 index 0000000000..aa4490912b --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-mk/strings.xml @@ -0,0 +1,25 @@ + + + + + "Точката на пристап нема интернет" + "Уредите не може да се поврзат на интернет" + "Исклучи ја точката на пристап" + "Точката на пристап е вклучена" + "При роаминг може да се наплатат дополнителни трошоци" + "Продолжи" + diff --git a/Tethering/res/values-mcc204-mnc04-ml/strings.xml b/Tethering/res/values-mcc204-mnc04-ml/strings.xml new file mode 100644 index 0000000000..d376fe5870 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-ml/strings.xml @@ -0,0 +1,25 @@ + + + + + "ഹോട്ട്സ്പോട്ടിൽ ഇന്റർനെറ്റ് ലഭ്യമല്ല" + "ഉപകരണങ്ങൾ ഇന്റർനെറ്റിലേക്ക് കണക്റ്റ് ചെയ്യാനാവില്ല" + "ഹോട്ട്‌സ്‌പോട്ട് ഓഫാക്കുക" + "ഹോട്ട്സ്പോട്ട് ഓണാണ്" + "റോമിംഗ് ചെയ്യുമ്പോൾ അധിക നിരക്കുകൾ ബാധകമായേക്കാം" + "തുടരുക" + diff --git a/Tethering/res/values-mcc204-mnc04-mn/strings.xml b/Tethering/res/values-mcc204-mnc04-mn/strings.xml new file mode 100644 index 0000000000..417213f543 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-mn/strings.xml @@ -0,0 +1,25 @@ + + + + + "Сүлжээний цэг дээр интернэт алга байна" + "Төхөөрөмжүүд нь интернэтэд холбогдох боломжгүй байна" + "Сүлжээний цэгийг унтраах" + "Сүлжээний цэг асаалттай байна" + "Роумингийн үеэр нэмэлт төлбөр нэхэмжилж болзошгүй" + "Үргэлжлүүлэх" + diff --git a/Tethering/res/values-mcc204-mnc04-mr/strings.xml b/Tethering/res/values-mcc204-mnc04-mr/strings.xml new file mode 100644 index 0000000000..2ed153fb17 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-mr/strings.xml @@ -0,0 +1,25 @@ + + + + + "हॉटस्पॉटला इंटरनेट नाही" + "डिव्हाइस इंटरनेटला कनेक्ट करू शकत नाहीत" + "हॉटस्पॉट बंद करा" + "हॉटस्पॉट सुरू आहे" + "रोमिंगदरम्यान अतिरिक्त शुल्क लागू होऊ शकतात" + "सुरू ठेवा" + diff --git a/Tethering/res/values-mcc204-mnc04-ms/strings.xml b/Tethering/res/values-mcc204-mnc04-ms/strings.xml new file mode 100644 index 0000000000..50817fd4a2 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-ms/strings.xml @@ -0,0 +1,25 @@ + + + + + "Tempat liputan tiada Internet" + "Peranti tidak dapat menyambung kepada Internet" + "Matikan tempat liputan" + "Tempat liputan dihidupkan" + "Caj tambahan mungkin digunakan semasa perayauan" + "Teruskan" + diff --git a/Tethering/res/values-mcc204-mnc04-my/strings.xml b/Tethering/res/values-mcc204-mnc04-my/strings.xml new file mode 100644 index 0000000000..c0d70e3d5f --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-my/strings.xml @@ -0,0 +1,25 @@ + + + + + "ဟော့စပေါ့တွင် အင်တာနက်မရှိပါ" + "စက်များက အင်တာနက်ချိတ်ဆက်၍ မရပါ" + "ဟော့စပေါ့ ပိတ်ရန်" + "ဟော့စပေါ့ ဖွင့်ထားသည်" + "ပြင်ပကွန်ရက်နှင့် ချိတ်ဆက်သည့်အခါ နောက်ထပ်ကျသင့်မှုများ ရှိနိုင်သည်" + "ရှေ့ဆက်ရန်" + diff --git a/Tethering/res/values-mcc204-mnc04-nb/strings.xml b/Tethering/res/values-mcc204-mnc04-nb/strings.xml new file mode 100644 index 0000000000..1e7f1c6d0a --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-nb/strings.xml @@ -0,0 +1,25 @@ + + + + + "Wi-Fi-sonen har ikke internettilgang" + "Enheter kan ikke koble til internett" + "Slå av Wi-Fi-sonen" + "Wi-Fi-sonen er på" + "Ytterligere kostnader kan påløpe under roaming" + "Fortsett" + diff --git a/Tethering/res/values-mcc204-mnc04-ne/strings.xml b/Tethering/res/values-mcc204-mnc04-ne/strings.xml new file mode 100644 index 0000000000..63ce155034 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-ne/strings.xml @@ -0,0 +1,25 @@ + + + + + "हटस्पटमा इन्टरनेट छैन" + "यन्त्रहरू इन्टरनेटमा कनेक्ट गर्न सकिएन" + "हटस्पट निष्क्रिय पार्नुहोस्" + "हटस्पट सक्रिय छ" + "रोमिङ सेवा प्रयोग गर्दा अतिरिक्त शुल्क लाग्न सक्छ" + "जारी राख्नुहोस्" + diff --git a/Tethering/res/values-mcc204-mnc04-nl/strings.xml b/Tethering/res/values-mcc204-mnc04-nl/strings.xml new file mode 100644 index 0000000000..bf14a0fced --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-nl/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspot heeft geen internet" + "Apparaten kunnen geen verbinding maken met internet" + "Hotspot uitschakelen" + "Hotspot is ingeschakeld" + "Er kunnen extra kosten voor roaming in rekening worden gebracht." + "Doorgaan" + diff --git a/Tethering/res/values-mcc204-mnc04-or/strings.xml b/Tethering/res/values-mcc204-mnc04-or/strings.xml new file mode 100644 index 0000000000..ab87b76caf --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-or/strings.xml @@ -0,0 +1,25 @@ + + + + + "ହଟସ୍ପଟରେ କୌଣସି ଇଣ୍ଟର୍ନେଟ୍ ସଂଯୋଗ ନାହିଁ" + "ଡିଭାଇସଗୁଡ଼ିକ ଇଣ୍ଟର୍ନେଟ୍ ସହ ସଂଯୋଗ କରାଯାଇପାରିବ ନାହିଁ" + "ହଟସ୍ପଟ ବନ୍ଦ କରନ୍ତୁ" + "ହଟସ୍ପଟ ଚାଲୁ ଅଛି" + "ରୋମିଂରେ ଥିବା ସମୟରେ ଅତିରିକ୍ତ ଶୁଳ୍କ ଲାଗୁ ହୋଇପାରେ" + "ଜାରି ରଖନ୍ତୁ" + diff --git a/Tethering/res/values-mcc204-mnc04-pa/strings.xml b/Tethering/res/values-mcc204-mnc04-pa/strings.xml new file mode 100644 index 0000000000..b09f285c2d --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-pa/strings.xml @@ -0,0 +1,25 @@ + + + + + "ਹੌਟਸਪੌਟ ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹੁੰਚ ਨਹੀਂ ਹੈ" + "ਡੀਵਾਈਸ ਇੰਟਰਨੈੱਟ ਨਾਲ ਕਨੈਕਟ ਨਹੀਂ ਹੋ ਸਕਦੇ" + "ਹੌਟਸਪੌਟ ਬੰਦ ਕਰੋ" + "ਹੌਟਸਪੌਟ ਚਾਲੂ ਹੈ" + "ਰੋਮਿੰਗ ਦੌਰਾਨ ਵਧੀਕ ਖਰਚੇ ਲਾਗੂ ਹੋ ਸਕਦੇ ਹਨ" + "ਜਾਰੀ ਰੱਖੋ" + diff --git a/Tethering/res/values-mcc204-mnc04-pl/strings.xml b/Tethering/res/values-mcc204-mnc04-pl/strings.xml new file mode 100644 index 0000000000..8becd0715f --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-pl/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspot nie ma internetu" + "Urządzenia nie mogą połączyć się z internetem" + "Wyłącz hotspot" + "Hotspot jest włączony" + "Podczas korzystania z roamingu mogą zostać naliczone dodatkowe opłaty" + "Dalej" + diff --git a/Tethering/res/values-mcc204-mnc04-pt-rBR/strings.xml b/Tethering/res/values-mcc204-mnc04-pt-rBR/strings.xml new file mode 100644 index 0000000000..8e01736f64 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-pt-rBR/strings.xml @@ -0,0 +1,25 @@ + + + + + "O ponto de acesso não tem conexão com a Internet" + "Não foi possível conectar os dispositivos à Internet" + "Desativar ponto de acesso" + "O ponto de acesso está ativado" + "Pode haver cobranças extras durante o roaming" + "Continuar" + diff --git a/Tethering/res/values-mcc204-mnc04-pt-rPT/strings.xml b/Tethering/res/values-mcc204-mnc04-pt-rPT/strings.xml new file mode 100644 index 0000000000..2356379e2f --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-pt-rPT/strings.xml @@ -0,0 +1,25 @@ + + + + + "A zona Wi-Fi não tem Internet" + "Não é possível ligar os dispositivos à Internet" + "Desativar zona Wi-Fi" + "A zona Wi-Fi está ativada" + "Podem aplicar-se custos adicionais em roaming." + "Continuar" + diff --git a/Tethering/res/values-mcc204-mnc04-pt/strings.xml b/Tethering/res/values-mcc204-mnc04-pt/strings.xml new file mode 100644 index 0000000000..8e01736f64 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-pt/strings.xml @@ -0,0 +1,25 @@ + + + + + "O ponto de acesso não tem conexão com a Internet" + "Não foi possível conectar os dispositivos à Internet" + "Desativar ponto de acesso" + "O ponto de acesso está ativado" + "Pode haver cobranças extras durante o roaming" + "Continuar" + diff --git a/Tethering/res/values-mcc204-mnc04-ro/strings.xml b/Tethering/res/values-mcc204-mnc04-ro/strings.xml new file mode 100644 index 0000000000..2e62bd611c --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-ro/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspotul nu are internet" + "Dispozitivele nu se pot conecta la internet" + "Dezactivați hotspotul" + "Hotspotul este activ" + "Se pot aplica taxe suplimentare pentru roaming" + "Continuați" + diff --git a/Tethering/res/values-mcc204-mnc04-ru/strings.xml b/Tethering/res/values-mcc204-mnc04-ru/strings.xml new file mode 100644 index 0000000000..a2b1640cb2 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-ru/strings.xml @@ -0,0 +1,25 @@ + + + + + "Точка доступа не подключена к Интернету" + "Устройства не могут подключаться к Интернету" + "Отключить точку доступа" + "Точка доступа включена" + "За использование услуг связи в роуминге может взиматься дополнительная плата." + "Продолжить" + diff --git a/Tethering/res/values-mcc204-mnc04-si/strings.xml b/Tethering/res/values-mcc204-mnc04-si/strings.xml new file mode 100644 index 0000000000..632748a3e8 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-si/strings.xml @@ -0,0 +1,25 @@ + + + + + "හොට්ස්පොට් හට අන්තර්ජාලය නැත" + "උපාංගවලට අන්තර්ජාලයට සම්බන්ධ විය නොහැකිය" + "හොට්ස්පොට් ක්‍රියාවිරහිත කරන්න" + "හොට්ස්පොට් ක්‍රියාත්මකයි" + "රෝමිං අතරතුර අමතර ගාස්තු අදාළ විය හැකිය" + "ඉදිරියට යන්න" + diff --git a/Tethering/res/values-mcc204-mnc04-sk/strings.xml b/Tethering/res/values-mcc204-mnc04-sk/strings.xml new file mode 100644 index 0000000000..247fc1b0e7 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-sk/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspot nemá internetové pripojenie" + "Zariadenia sa nedajú pripojiť k internetu" + "Vypnúť hotspot" + "Hotspot je zapnutý" + "Počas roamingu vám môžu byť účtované ďalšie poplatky" + "Pokračovať" + diff --git a/Tethering/res/values-mcc204-mnc04-sl/strings.xml b/Tethering/res/values-mcc204-mnc04-sl/strings.xml new file mode 100644 index 0000000000..ed22372197 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-sl/strings.xml @@ -0,0 +1,25 @@ + + + + + "Dostopna točka nima internetne povezave" + "Naprave ne morejo vzpostaviti internetne povezave" + "Izklopi dostopno točko" + "Dostopna točka je vklopljena" + "Med gostovanjem lahko nastanejo dodatni stroški" + "Naprej" + diff --git a/Tethering/res/values-mcc204-mnc04-sq/strings.xml b/Tethering/res/values-mcc204-mnc04-sq/strings.xml new file mode 100644 index 0000000000..4bfab6e474 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-sq/strings.xml @@ -0,0 +1,25 @@ + + + + + "Zona e qasjes për internet nuk ka internet" + "Pajisjet nuk mund të lidhen me internetin" + "Çaktivizo zonën e qasjes për internet" + "Zona e qasjes për internet është aktive" + "Mund të zbatohen tarifime shtesë kur je në roaming" + "Vazhdo" + diff --git a/Tethering/res/values-mcc204-mnc04-sr/strings.xml b/Tethering/res/values-mcc204-mnc04-sr/strings.xml new file mode 100644 index 0000000000..478d53a255 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-sr/strings.xml @@ -0,0 +1,25 @@ + + + + + "Хотспот нема приступ интернету" + "Уређаји не могу да се повежу на интернет" + "Искључи хотспот" + "Хотспот је укључен" + "Можда важе додатни трошкови у ромингу" + "Настави" + diff --git a/Tethering/res/values-mcc204-mnc04-sv/strings.xml b/Tethering/res/values-mcc204-mnc04-sv/strings.xml new file mode 100644 index 0000000000..a793ed6483 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-sv/strings.xml @@ -0,0 +1,25 @@ + + + + + "Surfzonen har ingen internetanslutning" + "Enheterna har ingen internetanslutning" + "Inaktivera surfzon" + "Surfzonen är aktiverad" + "Ytterligare avgifter kan tillkomma vid roaming" + "Fortsätt" + diff --git a/Tethering/res/values-mcc204-mnc04-sw/strings.xml b/Tethering/res/values-mcc204-mnc04-sw/strings.xml new file mode 100644 index 0000000000..18ee457d03 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-sw/strings.xml @@ -0,0 +1,25 @@ + + + + + "Mtandao pepe hauna intaneti" + "Vifaa vimeshindwa kuunganisha kwenye intaneti" + "Zima mtandao pepe" + "Mtandao pepe umewashwa" + "Huenda ukatozwa gharama za ziada ukitumia mitandao ya ng\'ambo" + "Endelea" + diff --git a/Tethering/res/values-mcc204-mnc04-ta/strings.xml b/Tethering/res/values-mcc204-mnc04-ta/strings.xml new file mode 100644 index 0000000000..7eebd6784a --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-ta/strings.xml @@ -0,0 +1,25 @@ + + + + + "ஹாட்ஸ்பாட்டில் இணையம் இல்லை" + "சாதனங்களால் இணையத்தில் இணைய இயலவில்லை" + "ஹாட்ஸ்பாட்டை ஆஃப் செய்" + "ஹாட்ஸ்பாட் ஆன் செய்யப்பட்டுள்ளது" + "ரோமிங்கின்போது கூடுதல் கட்டணங்கள் விதிக்கப்படக்கூடும்" + "தொடர்க" + diff --git a/Tethering/res/values-mcc204-mnc04-te/strings.xml b/Tethering/res/values-mcc204-mnc04-te/strings.xml new file mode 100644 index 0000000000..0986534fc7 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-te/strings.xml @@ -0,0 +1,25 @@ + + + + + "హాట్‌స్పాట్‌కు ఇంటర్నెట్ యాక్సెస్ లేదు" + "పరికరాలను ఇంటర్నెట్‌కి కనెక్ట్ చేయడం సాధ్యం కాదు" + "హాట్‌స్పాట్‌ని ఆఫ్ చేయండి" + "హాట్‌స్పాట్ ఆన్‌లో ఉంది" + "రోమింగ్‌లో ఉన్నప్పుడు అదనపు ఛార్జీలు వర్తించవచ్చు" + "కొనసాగించు" + diff --git a/Tethering/res/values-mcc204-mnc04-th/strings.xml b/Tethering/res/values-mcc204-mnc04-th/strings.xml new file mode 100644 index 0000000000..3837002b29 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-th/strings.xml @@ -0,0 +1,25 @@ + + + + + "ฮอตสปอตไม่ได้เชื่อมต่ออินเทอร์เน็ต" + "อุปกรณ์เชื่อมต่ออินเทอร์เน็ตไม่ได้" + "ปิดฮอตสปอต" + "ฮอตสปอตเปิดอยู่" + "อาจมีค่าใช้จ่ายเพิ่มเติมขณะโรมมิ่ง" + "ต่อไป" + diff --git a/Tethering/res/values-mcc204-mnc04-tl/strings.xml b/Tethering/res/values-mcc204-mnc04-tl/strings.xml new file mode 100644 index 0000000000..208f893447 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-tl/strings.xml @@ -0,0 +1,25 @@ + + + + + "Walang internet ang hotspot" + "Hindi makakonekta sa internet ang mga device" + "I-off ang hotspot" + "Naka-on ang hotspot" + "Posibleng magkaroon ng mga karagdagang singil habang nagro-roam" + "Ituloy" + diff --git a/Tethering/res/values-mcc204-mnc04-tr/strings.xml b/Tethering/res/values-mcc204-mnc04-tr/strings.xml new file mode 100644 index 0000000000..3482fafa2d --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-tr/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspot\'un internet bağlantısı yok" + "Cihazlar internete bağlanamıyor" + "Hotspot\'u kapat" + "Hotspot açık" + "Dolaşım sırasında ek ücretler uygulanabilir" + "Devam" + diff --git a/Tethering/res/values-mcc204-mnc04-uk/strings.xml b/Tethering/res/values-mcc204-mnc04-uk/strings.xml new file mode 100644 index 0000000000..dea311443f --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-uk/strings.xml @@ -0,0 +1,25 @@ + + + + + "Точка доступу не підключена до Інтернету" + "Не вдається підключити пристрої до Інтернету" + "Вимкнути точку доступу" + "Точку доступу ввімкнено" + "У роумінгу може стягуватися додаткова плата" + "Продовжити" + diff --git a/Tethering/res/values-mcc204-mnc04-ur/strings.xml b/Tethering/res/values-mcc204-mnc04-ur/strings.xml new file mode 100644 index 0000000000..09bc0c9eab --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-ur/strings.xml @@ -0,0 +1,25 @@ + + + + + "ہاٹ اسپاٹ میں انٹرنیٹ نہیں ہے" + "آلات انٹرنیٹ سے منسلک نہیں ہو سکتے" + "ہاٹ اسپاٹ آف کریں" + "ہاٹ اسپاٹ آن ہے" + "رومنگ کے دوران اضافی چارجز لاگو ہو سکتے ہیں" + "جاری رکھیں" + diff --git a/Tethering/res/values-mcc204-mnc04-uz/strings.xml b/Tethering/res/values-mcc204-mnc04-uz/strings.xml new file mode 100644 index 0000000000..715d34808b --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-uz/strings.xml @@ -0,0 +1,25 @@ + + + + + "Hotspot internetga ulanmagan" + "Qurilmalar internetga ulana olmayapti" + "Hotspotni faolsizlantirish" + "Hotspot yoniq" + "Rouming vaqtida qoʻshimcha haq olinishi mumkin" + "Davom etish" + diff --git a/Tethering/res/values-mcc204-mnc04-vi/strings.xml b/Tethering/res/values-mcc204-mnc04-vi/strings.xml new file mode 100644 index 0000000000..bf4ee1011b --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-vi/strings.xml @@ -0,0 +1,25 @@ + + + + + "Điểm phát sóng không có kết nối Internet" + "Các thiết bị không thể kết nối Internet" + "Tắt điểm phát sóng" + "Điểm phát sóng đang bật" + "Bạn có thể mất thêm phí dữ liệu khi chuyển vùng" + "Tiếp tục" + diff --git a/Tethering/res/values-mcc204-mnc04-zh-rCN/strings.xml b/Tethering/res/values-mcc204-mnc04-zh-rCN/strings.xml new file mode 100644 index 0000000000..cdb4224bf3 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-zh-rCN/strings.xml @@ -0,0 +1,25 @@ + + + + + "热点没有网络连接" + "设备无法连接到互联网" + "关闭热点" + "热点已开启" + "漫游时可能会产生额外的费用" + "继续" + diff --git a/Tethering/res/values-mcc204-mnc04-zh-rHK/strings.xml b/Tethering/res/values-mcc204-mnc04-zh-rHK/strings.xml new file mode 100644 index 0000000000..3bb52e491f --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-zh-rHK/strings.xml @@ -0,0 +1,25 @@ + + + + + "熱點沒有互聯網連線" + "裝置無法連線至互聯網" + "關閉熱點" + "已開啟熱點" + "漫遊時可能需要支付額外費用" + "繼續" + diff --git a/Tethering/res/values-mcc204-mnc04-zh-rTW/strings.xml b/Tethering/res/values-mcc204-mnc04-zh-rTW/strings.xml new file mode 100644 index 0000000000..298c3eac70 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-zh-rTW/strings.xml @@ -0,0 +1,25 @@ + + + + + "無線基地台沒有網際網路連線" + "裝置無法連上網際網路" + "關閉無線基地台" + "無線基地台已開啟" + "使用漫遊服務可能須支付額外費用" + "繼續" + diff --git a/Tethering/res/values-mcc204-mnc04-zu/strings.xml b/Tethering/res/values-mcc204-mnc04-zu/strings.xml new file mode 100644 index 0000000000..3dc0078834 --- /dev/null +++ b/Tethering/res/values-mcc204-mnc04-zu/strings.xml @@ -0,0 +1,25 @@ + + + + + "I-Hotspot ayina-inthanethi" + "Amadivayisi awakwazi ukuxhuma ku-inthanethi" + "Vala i-hotspot" + "I-Hotspot ivuliwe" + "Kungaba nezinkokhelo ezengeziwe uma uzula" + "Qhubeka" + diff --git a/Tethering/res/values-mcc310-mnc004-af/strings.xml b/Tethering/res/values-mcc310-mnc004-af/strings.xml new file mode 100644 index 0000000000..19d659c6ce --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-af/strings.xml @@ -0,0 +1,24 @@ + + + + + "Verbinding het nie internet nie" + "Toestelle kan nie koppel nie" + "Skakel verbinding af" + "Warmkol of verbinding is aan" + "Bykomende heffings kan geld terwyl jy swerf" + diff --git a/Tethering/res/values-mcc310-mnc004-am/strings.xml b/Tethering/res/values-mcc310-mnc004-am/strings.xml new file mode 100644 index 0000000000..8995430b4f --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-am/strings.xml @@ -0,0 +1,24 @@ + + + + + "ማስተሳሰር ምንም በይነመረብ የለውም" + "መሣሪያዎችን ማገናኘት አይቻልም" + "ማስተሳሰርን አጥፋ" + "መገናኛ ነጥብ ወይም ማስተሳሰር በርቷል" + "በሚያንዣብብበት ጊዜ ተጨማሪ ክፍያዎች ተፈጻሚ ሊሆኑ ይችላሉ" + diff --git a/Tethering/res/values-mcc310-mnc004-ar/strings.xml b/Tethering/res/values-mcc310-mnc004-ar/strings.xml new file mode 100644 index 0000000000..54f3b5389a --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-ar/strings.xml @@ -0,0 +1,24 @@ + + + + + "ما مِن اتصال بالإنترنت خلال التوصيل" + "تعذّر اتصال الأجهزة" + "إيقاف التوصيل" + "نقطة الاتصال أو التوصيل مفعّلان" + "قد يتم تطبيق رسوم إضافية أثناء التجوال." + diff --git a/Tethering/res/values-mcc310-mnc004-as/strings.xml b/Tethering/res/values-mcc310-mnc004-as/strings.xml new file mode 100644 index 0000000000..e215141c9e --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-as/strings.xml @@ -0,0 +1,24 @@ + + + + + "টে\'ডাৰিঙৰ ইণ্টাৰনেট নাই" + "ডিভাইচসমূহ সংযোগ কৰিব নোৱাৰি" + "টে\'ডাৰিং অফ কৰক" + "হটস্পট অথবা টে\'ডাৰিং অন আছে" + "ৰ\'মিঙত থাকিলে অতিৰিক্ত মাচুল প্ৰযোজ্য হ’ব পাৰে" + diff --git a/Tethering/res/values-mcc310-mnc004-az/strings.xml b/Tethering/res/values-mcc310-mnc004-az/strings.xml new file mode 100644 index 0000000000..1fd8e4c963 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-az/strings.xml @@ -0,0 +1,24 @@ + + + + + "Modemin internetə girişi yoxdur" + "Cihazları qoşmaq mümkün deyil" + "Modemi deaktiv edin" + "Hotspot və ya modem aktivdir" + "Rouminq zamanı əlavə ödənişlər tətbiq edilə bilər" + diff --git a/Tethering/res/values-mcc310-mnc004-b+sr+Latn/strings.xml b/Tethering/res/values-mcc310-mnc004-b+sr+Latn/strings.xml new file mode 100644 index 0000000000..1abe4f3aa3 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-b+sr+Latn/strings.xml @@ -0,0 +1,24 @@ + + + + + "Privezivanje nema pristup internetu" + "Povezivanje uređaja nije uspelo" + "Isključi privezivanje" + "Uključen je hotspot ili privezivanje" + "Možda važe dodatni troškovi u romingu" + diff --git a/Tethering/res/values-mcc310-mnc004-be/strings.xml b/Tethering/res/values-mcc310-mnc004-be/strings.xml new file mode 100644 index 0000000000..38dbd1e391 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-be/strings.xml @@ -0,0 +1,24 @@ + + + + + "Рэжым мадэма выкарыстоўваецца без доступу да інтэрнэту" + "Не ўдалося падключыць прылады" + "Выключыць рэжым мадэма" + "Хот-спот або рэжым мадэма ўключаны" + "Пры выкарыстанні роўмінгу можа спаганяцца дадатковая плата" + diff --git a/Tethering/res/values-mcc310-mnc004-bg/strings.xml b/Tethering/res/values-mcc310-mnc004-bg/strings.xml new file mode 100644 index 0000000000..04b44db5c1 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-bg/strings.xml @@ -0,0 +1,24 @@ + + + + + "Тетърингът няма връзка с интернет" + "Устройствата не могат да установят връзка" + "Изключване на тетъринга" + "Точката за достъп или тетърингът са включени" + "Възможно е да ви бъдат начислени допълнителни такси при роуминг" + diff --git a/Tethering/res/values-mcc310-mnc004-bn/strings.xml b/Tethering/res/values-mcc310-mnc004-bn/strings.xml new file mode 100644 index 0000000000..579d1be1c1 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-bn/strings.xml @@ -0,0 +1,24 @@ + + + + + "টিথারিং করার জন্য কোনও ইন্টারনেট কানেকশন নেই" + "ডিভাইস কানেক্ট করতে পারছে না" + "টিথারিং বন্ধ করুন" + "হটস্পট বা টিথারিং চালু আছে" + "রোমিংয়ের সময় অতিরিক্ত চার্জ করা হতে পারে" + diff --git a/Tethering/res/values-mcc310-mnc004-bs/strings.xml b/Tethering/res/values-mcc310-mnc004-bs/strings.xml new file mode 100644 index 0000000000..9ce3efe6c3 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-bs/strings.xml @@ -0,0 +1,24 @@ + + + + + "Povezivanje putem mobitela nema internet" + "Uređaji se ne mogu povezati" + "Isključi povezivanje putem mobitela" + "Pristupna tačka ili povezivanje putem mobitela je uključeno" + "Mogu nastati dodatni troškovi u romingu" + diff --git a/Tethering/res/values-mcc310-mnc004-ca/strings.xml b/Tethering/res/values-mcc310-mnc004-ca/strings.xml new file mode 100644 index 0000000000..46d4c35b9b --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-ca/strings.xml @@ -0,0 +1,24 @@ + + + + + "La compartició de xarxa no té accés a Internet" + "No es poden connectar els dispositius" + "Desactiva la compartició de xarxa" + "S\'ha activat el punt d\'accés Wi‑Fi o la compartició de xarxa" + "És possible que s\'apliquin costos addicionals en itinerància" + diff --git a/Tethering/res/values-mcc310-mnc004-cs/strings.xml b/Tethering/res/values-mcc310-mnc004-cs/strings.xml new file mode 100644 index 0000000000..cc13860b3d --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-cs/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering nemá připojení k internetu" + "Zařízení se nemůžou připojit" + "Vypnout tethering" + "Je zapnutý hotspot nebo tethering" + "Při roamingu mohou být účtovány dodatečné poplatky" + diff --git a/Tethering/res/values-mcc310-mnc004-da/strings.xml b/Tethering/res/values-mcc310-mnc004-da/strings.xml new file mode 100644 index 0000000000..92c3ae1156 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-da/strings.xml @@ -0,0 +1,24 @@ + + + + + "Netdeling har ingen internetforbindelse" + "Enheder kan ikke oprette forbindelse" + "Deaktiver netdeling" + "Hotspot eller netdeling er aktiveret" + "Der opkræves muligvis yderligere gebyrer ved roaming" + diff --git a/Tethering/res/values-mcc310-mnc004-de/strings.xml b/Tethering/res/values-mcc310-mnc004-de/strings.xml new file mode 100644 index 0000000000..967eb4db2e --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-de/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering hat keinen Internetzugriff" + "Geräte können sich nicht verbinden" + "Tethering deaktivieren" + "Hotspot oder Tethering ist aktiviert" + "Für das Roaming können zusätzliche Gebühren anfallen" + diff --git a/Tethering/res/values-mcc310-mnc004-el/strings.xml b/Tethering/res/values-mcc310-mnc004-el/strings.xml new file mode 100644 index 0000000000..5fb497451f --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-el/strings.xml @@ -0,0 +1,24 @@ + + + + + "Η σύνδεση δεν έχει πρόσβαση στο διαδίκτυο" + "Δεν είναι δυνατή η σύνδεση των συσκευών" + "Απενεργοποιήστε τη σύνδεση" + "Ενεργό σημείο πρόσβασης Wi-Fi ή ενεργή σύνδεση" + "Ενδέχεται να ισχύουν επιπλέον χρεώσεις κατά την περιαγωγή." + diff --git a/Tethering/res/values-mcc310-mnc004-en-rAU/strings.xml b/Tethering/res/values-mcc310-mnc004-en-rAU/strings.xml new file mode 100644 index 0000000000..45647f93f2 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-en-rAU/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering has no Internet" + "Devices can’t connect" + "Turn off tethering" + "Hotspot or tethering is on" + "Additional charges may apply while roaming" + diff --git a/Tethering/res/values-mcc310-mnc004-en-rCA/strings.xml b/Tethering/res/values-mcc310-mnc004-en-rCA/strings.xml new file mode 100644 index 0000000000..45647f93f2 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-en-rCA/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering has no Internet" + "Devices can’t connect" + "Turn off tethering" + "Hotspot or tethering is on" + "Additional charges may apply while roaming" + diff --git a/Tethering/res/values-mcc310-mnc004-en-rGB/strings.xml b/Tethering/res/values-mcc310-mnc004-en-rGB/strings.xml new file mode 100644 index 0000000000..45647f93f2 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-en-rGB/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering has no Internet" + "Devices can’t connect" + "Turn off tethering" + "Hotspot or tethering is on" + "Additional charges may apply while roaming" + diff --git a/Tethering/res/values-mcc310-mnc004-en-rIN/strings.xml b/Tethering/res/values-mcc310-mnc004-en-rIN/strings.xml new file mode 100644 index 0000000000..45647f93f2 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-en-rIN/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering has no Internet" + "Devices can’t connect" + "Turn off tethering" + "Hotspot or tethering is on" + "Additional charges may apply while roaming" + diff --git a/Tethering/res/values-mcc310-mnc004-en-rXC/strings.xml b/Tethering/res/values-mcc310-mnc004-en-rXC/strings.xml new file mode 100644 index 0000000000..7877074afc --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-en-rXC/strings.xml @@ -0,0 +1,24 @@ + + + + + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‎‏‏‏‎‎‏‏‏‎‎‏‎‎‏‏‎‏‎‎‎‏‎‏‎‏‏‏‏‏‎‎‏‏‎‎‎‎‏‎‏‏‏‏‎‏‎‏‎‎‎‏‏‏‎‏‎‎‎Tethering has no internet‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‏‎‏‎‏‎‏‏‏‎‏‎‎‎‎‎‏‏‏‏‏‏‏‎‏‎‎‎‏‏‎‎‎‎‎‏‏‏‎‎‏‏‎‏‏‎‎‏‎‏‎‎‎‎‏‏‏‎Devices can’t connect‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‎‎‏‏‏‎‏‏‏‎‏‎‎‏‎‏‎‏‎‎‏‏‏‎‎‎‏‏‎‎‏‏‎‏‎‏‏‏‏‎‎‎‏‎‏‏‎‎‎‏‎‏‎‎‎‎Turn off tethering‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‎‏‏‏‎‏‏‎‏‎‏‎‏‎‏‏‎‎‎‎‏‎‎‎‏‏‎‎‎‎‎‎‎‎‏‎‎‏‏‏‏‏‏‏‏‏‎‎‎‏‏‎‏‎‎‏‏‏‎Hotspot or tethering is on‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‏‎‏‎‏‎‏‎‎‎‏‏‏‏‏‏‏‎‏‏‎‏‏‏‏‎‎‏‏‏‏‏‎‏‏‎‎‎‎‏‏‎‎‎‏‏‏‏‏‎‏‎‏‎‏‏‏‏‎‎Additional charges may apply while roaming‎‏‎‎‏‎" + diff --git a/Tethering/res/values-mcc310-mnc004-es-rUS/strings.xml b/Tethering/res/values-mcc310-mnc004-es-rUS/strings.xml new file mode 100644 index 0000000000..08edd81a6b --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-es-rUS/strings.xml @@ -0,0 +1,24 @@ + + + + + "La conexión mediante dispositivo móvil no tiene Internet" + "No se pueden conectar los dispositivos" + "Desactivar conexión mediante dispositivo móvil" + "Se activó el hotspot o la conexión mediante dispositivo móvil" + "Es posible que se apliquen cargos adicionales por roaming" + diff --git a/Tethering/res/values-mcc310-mnc004-es/strings.xml b/Tethering/res/values-mcc310-mnc004-es/strings.xml new file mode 100644 index 0000000000..79f51d00e2 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-es/strings.xml @@ -0,0 +1,24 @@ + + + + + "La conexión no se puede compartir, porque no hay acceso a Internet" + "Los dispositivos no se pueden conectar" + "Desactivar conexión compartida" + "Punto de acceso o conexión compartida activados" + "Puede que se apliquen cargos adicionales en itinerancia" + diff --git a/Tethering/res/values-mcc310-mnc004-et/strings.xml b/Tethering/res/values-mcc310-mnc004-et/strings.xml new file mode 100644 index 0000000000..2da5f8a6d6 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-et/strings.xml @@ -0,0 +1,24 @@ + + + + + "Jagamisel puudub internetiühendus" + "Seadmed ei saa ühendust luua" + "Lülita jagamine välja" + "Kuumkoht või jagamine on sisse lülitatud" + "Rändluse kasutamisega võivad kaasneda lisatasud" + diff --git a/Tethering/res/values-mcc310-mnc004-eu/strings.xml b/Tethering/res/values-mcc310-mnc004-eu/strings.xml new file mode 100644 index 0000000000..2073f2806c --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-eu/strings.xml @@ -0,0 +1,24 @@ + + + + + "Konexioa partekatzeko aukerak ez du Interneteko konexiorik" + "Ezin dira konektatu gailuak" + "Desaktibatu konexioa partekatzeko aukera" + "Wifi-gunea edo konexioa partekatzeko aukera aktibatuta dago" + "Baliteke kostu gehigarriak ordaindu behar izatea ibiltaritzan" + diff --git a/Tethering/res/values-mcc310-mnc004-fa/strings.xml b/Tethering/res/values-mcc310-mnc004-fa/strings.xml new file mode 100644 index 0000000000..e21b2a0852 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-fa/strings.xml @@ -0,0 +1,24 @@ + + + + + "«اشتراک‌گذاری اینترنت» به اینترنت دسترسی ندارد" + "دستگاه‌ها متصل نمی‌شوند" + "خاموش کردن «اشتراک‌گذاری اینترنت»" + "«نقطه اتصال» یا «اشتراک‌گذاری اینترنت» روشن است" + "ممکن است درحین فراگردی تغییرات دیگر اعمال شود" + diff --git a/Tethering/res/values-mcc310-mnc004-fi/strings.xml b/Tethering/res/values-mcc310-mnc004-fi/strings.xml new file mode 100644 index 0000000000..88b0b13eb4 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-fi/strings.xml @@ -0,0 +1,24 @@ + + + + + "Ei jaettavaa internetyhteyttä" + "Laitteet eivät voi muodostaa yhteyttä" + "Laita yhteyden jakaminen pois päältä" + "Hotspot tai yhteyden jakaminen on päällä" + "Roaming voi aiheuttaa lisämaksuja" + diff --git a/Tethering/res/values-mcc310-mnc004-fr-rCA/strings.xml b/Tethering/res/values-mcc310-mnc004-fr-rCA/strings.xml new file mode 100644 index 0000000000..3b781bc8db --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-fr-rCA/strings.xml @@ -0,0 +1,24 @@ + + + + + "Le partage de connexion n\'est pas connecté à Internet" + "Impossible de connecter les appareils" + "Désactiver le partage de connexion" + "Le point d\'accès ou le partage de connexion est activé" + "En itinérance, des frais supplémentaires peuvent s\'appliquer" + diff --git a/Tethering/res/values-mcc310-mnc004-fr/strings.xml b/Tethering/res/values-mcc310-mnc004-fr/strings.xml new file mode 100644 index 0000000000..51d7203c36 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-fr/strings.xml @@ -0,0 +1,24 @@ + + + + + "Aucune connexion à Internet n\'est disponible pour le partage de connexion" + "Impossible de connecter les appareils" + "Désactiver le partage de connexion" + "Le point d\'accès ou le partage de connexion est activé" + "En itinérance, des frais supplémentaires peuvent s\'appliquer" + diff --git a/Tethering/res/values-mcc310-mnc004-gl/strings.xml b/Tethering/res/values-mcc310-mnc004-gl/strings.xml new file mode 100644 index 0000000000..008ccb475d --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-gl/strings.xml @@ -0,0 +1,24 @@ + + + + + "A conexión compartida non ten Internet" + "Non se puideron conectar os dispositivos" + "Desactivar conexión compartida" + "Está activada a zona wifi ou a conexión compartida" + "Pódense aplicar cargos adicionais en itinerancia" + diff --git a/Tethering/res/values-mcc310-mnc004-gu/strings.xml b/Tethering/res/values-mcc310-mnc004-gu/strings.xml new file mode 100644 index 0000000000..f2e3b4df78 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-gu/strings.xml @@ -0,0 +1,24 @@ + + + + + "ઇન્ટરનેટ શેર કરવાની સુવિધામાં ઇન્ટરનેટ નથી" + "ડિવાઇસ કનેક્ટ કરી શકાતા નથી" + "ઇન્ટરનેટ શેર કરવાની સુવિધા બંધ કરો" + "હૉટસ્પૉટ અથવા ઇન્ટરનેટ શેર કરવાની સુવિધા ચાલુ છે" + "રોમિંગમાં વધારાના શુલ્ક લાગી શકે છે" + diff --git a/Tethering/res/values-mcc310-mnc004-hi/strings.xml b/Tethering/res/values-mcc310-mnc004-hi/strings.xml new file mode 100644 index 0000000000..b11839d760 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-hi/strings.xml @@ -0,0 +1,24 @@ + + + + + "टेदरिंग से इंटरनेट नहीं चल रहा" + "डिवाइस कनेक्ट नहीं हो पा रहे" + "टेदरिंग बंद करें" + "हॉटस्पॉट या टेदरिंग चालू है" + "रोमिंग के दौरान अतिरिक्त शुल्क लग सकता है" + diff --git a/Tethering/res/values-mcc310-mnc004-hr/strings.xml b/Tethering/res/values-mcc310-mnc004-hr/strings.xml new file mode 100644 index 0000000000..0a5aca25b1 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-hr/strings.xml @@ -0,0 +1,24 @@ + + + + + "Modemsko povezivanje nema internet" + "Uređaji se ne mogu povezati" + "Isključivanje modemskog povezivanja" + "Uključena je žarišna točka ili modemsko povezivanje" + "U roamingu su mogući dodatni troškovi" + diff --git a/Tethering/res/values-mcc310-mnc004-hu/strings.xml b/Tethering/res/values-mcc310-mnc004-hu/strings.xml new file mode 100644 index 0000000000..21c689a44e --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-hu/strings.xml @@ -0,0 +1,24 @@ + + + + + "Nincs internetkapcsolat az internet megosztásához" + "Az eszközök nem tudnak csatlakozni" + "Internetmegosztás kikapcsolása" + "A hotspot vagy az internetmegosztás be van kapcsolva" + "Roaming során további díjak léphetnek fel" + diff --git a/Tethering/res/values-mcc310-mnc004-hy/strings.xml b/Tethering/res/values-mcc310-mnc004-hy/strings.xml new file mode 100644 index 0000000000..689d92870e --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-hy/strings.xml @@ -0,0 +1,24 @@ + + + + + "Մոդեմի ռեժիմի կապը բացակայում է" + "Չհաջողվեց միացնել սարքը" + "Անջատել մոդեմի ռեժիմը" + "Թեժ կետը կամ մոդեմի ռեժիմը միացված է" + "Ռոումինգում կարող են լրացուցիչ վճարներ գանձվել" + diff --git a/Tethering/res/values-mcc310-mnc004-in/strings.xml b/Tethering/res/values-mcc310-mnc004-in/strings.xml new file mode 100644 index 0000000000..a5f4d19abf --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-in/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tidak ada koneksi internet di tethering" + "Perangkat tidak dapat terhubung" + "Nonaktifkan tethering" + "Hotspot atau tethering aktif" + "Biaya tambahan mungkin berlaku saat roaming" + diff --git a/Tethering/res/values-mcc310-mnc004-is/strings.xml b/Tethering/res/values-mcc310-mnc004-is/strings.xml new file mode 100644 index 0000000000..fc7e8aaf4e --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-is/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tjóðrun er ekki með internettengingu" + "Tæki geta ekki tengst" + "Slökkva á tjóðrun" + "Kveikt er á heitum reit eða tjóðrun" + "Viðbótargjöld kunna að eiga við í reiki" + diff --git a/Tethering/res/values-mcc310-mnc004-it/strings.xml b/Tethering/res/values-mcc310-mnc004-it/strings.xml new file mode 100644 index 0000000000..6456dd1b80 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-it/strings.xml @@ -0,0 +1,24 @@ + + + + + "Nessuna connessione a Internet per il tethering" + "Impossibile connettere i dispositivi" + "Disattiva il tethering" + "Hotspot o tethering attivi" + "Potrebbero essere applicati costi aggiuntivi durante il roaming" + diff --git a/Tethering/res/values-mcc310-mnc004-iw/strings.xml b/Tethering/res/values-mcc310-mnc004-iw/strings.xml new file mode 100644 index 0000000000..46b24bd3c5 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-iw/strings.xml @@ -0,0 +1,24 @@ + + + + + "אי אפשר להפעיל את תכונת שיתוף האינטרנט בין מכשירים כי אין חיבור לאינטרנט" + "למכשירים אין אפשרות להתחבר" + "השבתה של שיתוף האינטרנט בין מכשירים" + "תכונת הנקודה לשיתוף אינטרנט או תכונת שיתוף האינטרנט בין מכשירים פועלת" + "ייתכנו חיובים נוספים בעת נדידה" + diff --git a/Tethering/res/values-mcc310-mnc004-ja/strings.xml b/Tethering/res/values-mcc310-mnc004-ja/strings.xml new file mode 100644 index 0000000000..e6eb277b90 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-ja/strings.xml @@ -0,0 +1,24 @@ + + + + + "テザリングがインターネットに接続されていません" + "デバイスを接続できません" + "テザリングを OFF にする" + "アクセス ポイントまたはテザリングが ON です" + "ローミング時に追加料金が発生することがあります" + diff --git a/Tethering/res/values-mcc310-mnc004-ka/strings.xml b/Tethering/res/values-mcc310-mnc004-ka/strings.xml new file mode 100644 index 0000000000..aeddd7101d --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-ka/strings.xml @@ -0,0 +1,24 @@ + + + + + "ტეტერინგს არ აქვს ინტერნეტზე წვდომა" + "მოწყობილობები ვერ ახერხებენ დაკავშირებას" + "ტეტერინგის გამორთვა" + "ჩართულია უსადენო ქსელი ან ტეტერინგი" + "როუმინგის გამოყენებისას შეიძლება ჩამოგეჭრათ დამატებითი საფასური" + diff --git a/Tethering/res/values-mcc310-mnc004-kk/strings.xml b/Tethering/res/values-mcc310-mnc004-kk/strings.xml new file mode 100644 index 0000000000..255f0a276f --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-kk/strings.xml @@ -0,0 +1,24 @@ + + + + + "Тетеринг режимі интернет байланысынсыз пайдаланылуда" + "Құрылғыларды байланыстыру мүмкін емес" + "Тетерингіні өшіру" + "Хотспот немесе тетеринг қосулы" + "Роуминг кезінде қосымша ақы алынуы мүмкін." + diff --git a/Tethering/res/values-mcc310-mnc004-km/strings.xml b/Tethering/res/values-mcc310-mnc004-km/strings.xml new file mode 100644 index 0000000000..2bceb1cf77 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-km/strings.xml @@ -0,0 +1,24 @@ + + + + + "ការភ្ជាប់​មិនមានអ៊ីនធឺណិត​ទេ" + "មិនអាច​ភ្ជាប់ឧបករណ៍​បានទេ" + "បិទការភ្ជាប់" + "ហតស្ប៉ត ឬការភ្ជាប់​ត្រូវបានបើក" + "អាចមាន​ការគិតថ្លៃ​បន្ថែម នៅពេល​រ៉ូមីង" + diff --git a/Tethering/res/values-mcc310-mnc004-kn/strings.xml b/Tethering/res/values-mcc310-mnc004-kn/strings.xml new file mode 100644 index 0000000000..ed769305a6 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-kn/strings.xml @@ -0,0 +1,24 @@ + + + + + "ಟೆಥರಿಂಗ್‌ ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಕನೆಕ್ಷನ್ ಹೊಂದಿಲ್ಲ" + "ಸಾಧನಗಳನ್ನು ಕನೆಕ್ಟ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ" + "ಟೆಥರಿಂಗ್‌ ಆಫ್ ಮಾಡಿ" + "ಹಾಟ್‌ಸ್ಪಾಟ್ ಅಥವಾ ಟೆಥರಿಂಗ್‌ ಆನ್ ಆಗಿದೆ" + "ರೋಮಿಂಗ್‌ನಲ್ಲಿರುವಾಗ ಹೆಚ್ಚುವರಿ ಶುಲ್ಕಗಳು ಅನ್ವಯವಾಗಬಹುದು" + diff --git a/Tethering/res/values-mcc310-mnc004-ko/strings.xml b/Tethering/res/values-mcc310-mnc004-ko/strings.xml new file mode 100644 index 0000000000..6e504941eb --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-ko/strings.xml @@ -0,0 +1,24 @@ + + + + + "테더링으로 인터넷을 사용할 수 없음" + "기기에서 연결할 수 없음" + "테더링 사용 중지" + "핫스팟 또는 테더링 켜짐" + "로밍 중에는 추가 요금이 발생할 수 있습니다." + diff --git a/Tethering/res/values-mcc310-mnc004-ky/strings.xml b/Tethering/res/values-mcc310-mnc004-ky/strings.xml new file mode 100644 index 0000000000..d68128b9a5 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-ky/strings.xml @@ -0,0 +1,24 @@ + + + + + "Модем режими Интернети жок колдонулууда" + "Түзмөктөр туташпай жатат" + "Модем режимин өчүрүү" + "Байланыш түйүнү же модем режими күйүк" + "Роумингде кошумча акы алынышы мүмкүн" + diff --git a/Tethering/res/values-mcc310-mnc004-lo/strings.xml b/Tethering/res/values-mcc310-mnc004-lo/strings.xml new file mode 100644 index 0000000000..03e134a0fc --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-lo/strings.xml @@ -0,0 +1,24 @@ + + + + + "ການປ່ອຍສັນຍານບໍ່ມີອິນເຕີເນັດ" + "ອຸປະກອນບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້" + "ປິດການປ່ອຍສັນຍານ" + "ເປີດໃຊ້ຮັອດສະປອດ ຫຼື ການປ່ອຍສັນຍານຢູ່" + "ອາດມີຄ່າໃຊ້ຈ່າຍເພີ່ມເຕີມໃນລະຫວ່າງການໂຣມມິງ" + diff --git a/Tethering/res/values-mcc310-mnc004-lt/strings.xml b/Tethering/res/values-mcc310-mnc004-lt/strings.xml new file mode 100644 index 0000000000..652cedc6e6 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-lt/strings.xml @@ -0,0 +1,24 @@ + + + + + "Nėra įrenginio kaip modemo naudojimo interneto ryšio" + "Nepavyko susieti įrenginių" + "Išjungti įrenginio kaip modemo naudojimą" + "Įjungtas viešosios interneto prieigos taškas arba įrenginio kaip modemo naudojimas" + "Veikiant tarptinkliniam ryšiui gali būti taikomi papildomi mokesčiai" + diff --git a/Tethering/res/values-mcc310-mnc004-lv/strings.xml b/Tethering/res/values-mcc310-mnc004-lv/strings.xml new file mode 100644 index 0000000000..221972298c --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-lv/strings.xml @@ -0,0 +1,24 @@ + + + + + "Piesaistei nav interneta savienojuma" + "Nevar savienot ierīces" + "Izslēgt piesaisti" + "Ir ieslēgts tīklājs vai piesaiste" + "Viesabonēšanas laikā var tikt piemērota papildu samaksa" + diff --git a/Tethering/res/values-mcc310-mnc004-mk/strings.xml b/Tethering/res/values-mcc310-mnc004-mk/strings.xml new file mode 100644 index 0000000000..227f9e3466 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-mk/strings.xml @@ -0,0 +1,24 @@ + + + + + "Нема интернет преку мобилен" + "Уредите не може да се поврзат" + "Исклучи интернет преку мобилен" + "Точката на пристап или интернетот преку мобилен е вклучен" + "При роаминг може да се наплатат дополнителни трошоци" + diff --git a/Tethering/res/values-mcc310-mnc004-ml/strings.xml b/Tethering/res/values-mcc310-mnc004-ml/strings.xml new file mode 100644 index 0000000000..ec43885126 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-ml/strings.xml @@ -0,0 +1,24 @@ + + + + + "ടെതറിംഗിന് ഇന്റർനെറ്റ് ഇല്ല" + "ഉപകരണങ്ങൾ കണക്റ്റ് ചെയ്യാനാവില്ല" + "ടെതറിംഗ് ഓഫാക്കുക" + "ഹോട്ട്‌സ്‌പോട്ട് അല്ലെങ്കിൽ ടെതറിംഗ് ഓണാണ്" + "റോമിംഗ് ചെയ്യുമ്പോൾ അധിക നിരക്കുകൾ ബാധകമായേക്കാം" + diff --git a/Tethering/res/values-mcc310-mnc004-mn/strings.xml b/Tethering/res/values-mcc310-mnc004-mn/strings.xml new file mode 100644 index 0000000000..e263573799 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-mn/strings.xml @@ -0,0 +1,24 @@ + + + + + "Модемд интернэт алга байна" + "Төхөөрөмжүүд холбогдох боломжгүй байна" + "Модем болгохыг унтраах" + "Сүлжээний цэг эсвэл модем болгох асаалттай байна" + "Роумингийн үеэр нэмэлт төлбөр нэхэмжилж болзошгүй" + diff --git a/Tethering/res/values-mcc310-mnc004-mr/strings.xml b/Tethering/res/values-mcc310-mnc004-mr/strings.xml new file mode 100644 index 0000000000..adf845d078 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-mr/strings.xml @@ -0,0 +1,24 @@ + + + + + "टेदरिंगला इंटरनेट नाही" + "डिव्हाइस कनेक्ट होऊ शकत नाहीत" + "टेदरिंग बंद करा" + "हॉटस्पॉट किंवा टेदरिंग सुरू आहे" + "रोमिंगदरम्यान अतिरिक्त शुल्क लागू होऊ शकतात" + diff --git a/Tethering/res/values-mcc310-mnc004-ms/strings.xml b/Tethering/res/values-mcc310-mnc004-ms/strings.xml new file mode 100644 index 0000000000..f65c451e4c --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-ms/strings.xml @@ -0,0 +1,24 @@ + + + + + "Penambatan tiada Internet" + "Peranti tidak dapat disambungkan" + "Matikan penambatan" + "Tempat liputan atau penambatan dihidupkan" + "Caj tambahan mungkin digunakan semasa perayauan" + diff --git a/Tethering/res/values-mcc310-mnc004-my/strings.xml b/Tethering/res/values-mcc310-mnc004-my/strings.xml new file mode 100644 index 0000000000..4118e775cd --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-my/strings.xml @@ -0,0 +1,24 @@ + + + + + "မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်းတွင် အင်တာနက် မရှိပါ" + "စက်များ ချိတ်ဆက်၍ မရပါ" + "မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း ပိတ်ရန်" + "ဟော့စပေါ့ (သို့) မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း ဖွင့်ထားသည်" + "ပြင်ပကွန်ရက်နှင့် ချိတ်ဆက်သည့်အခါ နောက်ထပ်ကျသင့်မှုများ ရှိနိုင်သည်" + diff --git a/Tethering/res/values-mcc310-mnc004-nb/strings.xml b/Tethering/res/values-mcc310-mnc004-nb/strings.xml new file mode 100644 index 0000000000..36853583ce --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-nb/strings.xml @@ -0,0 +1,24 @@ + + + + + "Internettdeling har ikke internettilgang" + "Enhetene kan ikke koble til" + "Slå av internettdeling" + "Wi-Fi-sone eller internettdeling er på" + "Ytterligere kostnader kan påløpe under roaming" + diff --git a/Tethering/res/values-mcc310-mnc004-ne/strings.xml b/Tethering/res/values-mcc310-mnc004-ne/strings.xml new file mode 100644 index 0000000000..d074f15699 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-ne/strings.xml @@ -0,0 +1,24 @@ + + + + + "टेदरिङमार्फत इन्टरनेट कनेक्सन प्राप्त हुन सकेन" + "यन्त्रहरू कनेक्ट गर्न सकिएन" + "टेदरिङ निष्क्रिय पार्नुहोस्" + "हटस्पट वा टेदरिङ सक्रिय छ" + "रोमिङ सेवा प्रयोग गर्दा अतिरिक्त शुल्क लाग्न सक्छ" + diff --git a/Tethering/res/values-mcc310-mnc004-nl/strings.xml b/Tethering/res/values-mcc310-mnc004-nl/strings.xml new file mode 100644 index 0000000000..1d888942f4 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-nl/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering heeft geen internet" + "Apparaten kunnen niet worden verbonden" + "Tethering uitschakelen" + "Hotspot of tethering is ingeschakeld" + "Er kunnen extra kosten voor roaming in rekening worden gebracht." + diff --git a/Tethering/res/values-mcc310-mnc004-or/strings.xml b/Tethering/res/values-mcc310-mnc004-or/strings.xml new file mode 100644 index 0000000000..8038815fe8 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-or/strings.xml @@ -0,0 +1,24 @@ + + + + + "ଟିଥରିଂ ପାଇଁ କୌଣସି ଇଣ୍ଟର୍ନେଟ୍ ସଂଯୋଗ ନାହିଁ" + "ଡିଭାଇସଗୁଡ଼ିକ ସଂଯୋଗ କରାଯାଇପାରିବ ନାହିଁ" + "ଟିଥରିଂ ବନ୍ଦ କରନ୍ତୁ" + "ହଟସ୍ପଟ୍ କିମ୍ବା ଟିଥରିଂ ଚାଲୁ ଅଛି" + "ରୋମିଂରେ ଥିବା ସମୟରେ ଅତିରିକ୍ତ ଶୁଳ୍କ ଲାଗୁ ହୋଇପାରେ" + diff --git a/Tethering/res/values-mcc310-mnc004-pa/strings.xml b/Tethering/res/values-mcc310-mnc004-pa/strings.xml new file mode 100644 index 0000000000..819833eab0 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-pa/strings.xml @@ -0,0 +1,24 @@ + + + + + "ਟੈਦਰਿੰਗ ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹੁੰਚ ਨਹੀਂ ਹੈ" + "ਡੀਵਾਈਸ ਕਨੈਕਟ ਨਹੀਂ ਕੀਤੇ ਜਾ ਸਕਦੇ" + "ਟੈਦਰਿੰਗ ਬੰਦ ਕਰੋ" + "ਹੌਟਸਪੌਟ ਜਾਂ ਟੈਦਰਿੰਗ ਚਾਲੂ ਹੈ" + "ਰੋਮਿੰਗ ਦੌਰਾਨ ਵਧੀਕ ਖਰਚੇ ਲਾਗੂ ਹੋ ਸਕਦੇ ਹਨ" + diff --git a/Tethering/res/values-mcc310-mnc004-pl/strings.xml b/Tethering/res/values-mcc310-mnc004-pl/strings.xml new file mode 100644 index 0000000000..65e4380e39 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-pl/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering nie ma internetu" + "Urządzenia nie mogą się połączyć" + "Wyłącz tethering" + "Hotspot lub tethering jest włączony" + "Podczas korzystania z roamingu mogą zostać naliczone dodatkowe opłaty" + diff --git a/Tethering/res/values-mcc310-mnc004-pt-rBR/strings.xml b/Tethering/res/values-mcc310-mnc004-pt-rBR/strings.xml new file mode 100644 index 0000000000..d8866170c1 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-pt-rBR/strings.xml @@ -0,0 +1,24 @@ + + + + + "O tethering não tem Internet" + "Não é possível conectar os dispositivos" + "Desativar o tethering" + "Ponto de acesso ou tethering ativado" + "Pode haver cobranças extras durante o roaming" + diff --git a/Tethering/res/values-mcc310-mnc004-pt-rPT/strings.xml b/Tethering/res/values-mcc310-mnc004-pt-rPT/strings.xml new file mode 100644 index 0000000000..bfd45ca0a3 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-pt-rPT/strings.xml @@ -0,0 +1,24 @@ + + + + + "A ligação (à Internet) via telemóvel não tem Internet" + "Não é possível ligar os dispositivos" + "Desativar ligação (à Internet) via telemóvel" + "A zona Wi-Fi ou a ligação (à Internet) via telemóvel está ativada" + "Podem aplicar-se custos adicionais em roaming." + diff --git a/Tethering/res/values-mcc310-mnc004-pt/strings.xml b/Tethering/res/values-mcc310-mnc004-pt/strings.xml new file mode 100644 index 0000000000..d8866170c1 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-pt/strings.xml @@ -0,0 +1,24 @@ + + + + + "O tethering não tem Internet" + "Não é possível conectar os dispositivos" + "Desativar o tethering" + "Ponto de acesso ou tethering ativado" + "Pode haver cobranças extras durante o roaming" + diff --git a/Tethering/res/values-mcc310-mnc004-ro/strings.xml b/Tethering/res/values-mcc310-mnc004-ro/strings.xml new file mode 100644 index 0000000000..8d87a9e516 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-ro/strings.xml @@ -0,0 +1,24 @@ + + + + + "Procesul de tethering nu are internet" + "Dispozitivele nu se pot conecta" + "Dezactivați procesul de tethering" + "S-a activat hotspotul sau tethering" + "Se pot aplica taxe suplimentare pentru roaming" + diff --git a/Tethering/res/values-mcc310-mnc004-ru/strings.xml b/Tethering/res/values-mcc310-mnc004-ru/strings.xml new file mode 100644 index 0000000000..dbdb9ebe49 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-ru/strings.xml @@ -0,0 +1,24 @@ + + + + + "Режим модема используется без доступа к Интернету" + "Невозможно подключить устройства." + "Отключить режим модема" + "Включены точка доступа или режим модема" + "За использование услуг связи в роуминге может взиматься дополнительная плата." + diff --git a/Tethering/res/values-mcc310-mnc004-si/strings.xml b/Tethering/res/values-mcc310-mnc004-si/strings.xml new file mode 100644 index 0000000000..d8301e41c2 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-si/strings.xml @@ -0,0 +1,24 @@ + + + + + "ටෙදරින් හට අන්තර්ජාලය නැත" + "උපාංගවලට සම්බන්ධ විය නොහැකිය" + "ටෙදරින් ක්‍රියාවිරහිත කරන්න" + "හොට්ස්පොට් හෝ ටෙදරින් ක්‍රියාත්මකයි" + "රෝමිං අතරතුර අමතර ගාස්තු අදාළ විය හැකිය" + diff --git a/Tethering/res/values-mcc310-mnc004-sk/strings.xml b/Tethering/res/values-mcc310-mnc004-sk/strings.xml new file mode 100644 index 0000000000..bef71363f4 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-sk/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering nemá internetové pripojenie" + "Zariadenia sa nemôžu pripojiť" + "Vypnúť tethering" + "Je zapnutý hotspot alebo tethering" + "Počas roamingu vám môžu byť účtované ďalšie poplatky" + diff --git a/Tethering/res/values-mcc310-mnc004-sl/strings.xml b/Tethering/res/values-mcc310-mnc004-sl/strings.xml new file mode 100644 index 0000000000..3202c62e8a --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-sl/strings.xml @@ -0,0 +1,24 @@ + + + + + "Internetna povezava prek mobilnega telefona ni vzpostavljena" + "Napravi se ne moreta povezati" + "Izklopi internetno povezavo prek mobilnega telefona" + "Dostopna točka ali internetna povezava prek mobilnega telefona je vklopljena" + "Med gostovanjem lahko nastanejo dodatni stroški" + diff --git a/Tethering/res/values-mcc310-mnc004-sq/strings.xml b/Tethering/res/values-mcc310-mnc004-sq/strings.xml new file mode 100644 index 0000000000..37f6ad2868 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-sq/strings.xml @@ -0,0 +1,24 @@ + + + + + "Ndarja e internetit nuk ka internet" + "Pajisjet nuk mund të lidhen" + "Çaktivizo ndarjen e internetit" + "Zona e qasjes për internet ose ndarja e internetit është aktive" + "Mund të zbatohen tarifime shtesë kur je në roaming" + diff --git a/Tethering/res/values-mcc310-mnc004-sr/strings.xml b/Tethering/res/values-mcc310-mnc004-sr/strings.xml new file mode 100644 index 0000000000..5566d03ed1 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-sr/strings.xml @@ -0,0 +1,24 @@ + + + + + "Привезивање нема приступ интернету" + "Повезивање уређаја није успело" + "Искључи привезивање" + "Укључен је хотспот или привезивање" + "Можда важе додатни трошкови у ромингу" + diff --git a/Tethering/res/values-mcc310-mnc004-sv/strings.xml b/Tethering/res/values-mcc310-mnc004-sv/strings.xml new file mode 100644 index 0000000000..9765acd0cf --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-sv/strings.xml @@ -0,0 +1,24 @@ + + + + + "Det finns ingen internetanslutning för internetdelningen" + "Enheterna kan inte anslutas" + "Inaktivera internetdelning" + "Surfzon eller internetdelning har aktiverats" + "Ytterligare avgifter kan tillkomma vid roaming" + diff --git a/Tethering/res/values-mcc310-mnc004-sw/strings.xml b/Tethering/res/values-mcc310-mnc004-sw/strings.xml new file mode 100644 index 0000000000..cf850c9cd2 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-sw/strings.xml @@ -0,0 +1,24 @@ + + + + + "Kipengele cha kusambaza mtandao hakina intaneti" + "Imeshindwa kuunganisha vifaa" + "Zima kipengele cha kusambaza mtandao" + "Umewasha kipengele cha kusambaza mtandao au mtandao pepe" + "Huenda ukatozwa gharama za ziada ukitumia mitandao ya ng\'ambo" + diff --git a/Tethering/res/values-mcc310-mnc004-ta/strings.xml b/Tethering/res/values-mcc310-mnc004-ta/strings.xml new file mode 100644 index 0000000000..f4b15aab19 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-ta/strings.xml @@ -0,0 +1,24 @@ + + + + + "இணைப்பு முறைக்கு இணைய இணைப்பு இல்லை" + "சாதனங்களால் இணைய முடியவில்லை" + "இணைப்பு முறையை ஆஃப் செய்" + "ஹாட்ஸ்பாட் அல்லது இணைப்பு முறை ஆன் செய்யப்பட்டுள்ளது" + "ரோமிங்கின்போது கூடுதல் கட்டணங்கள் விதிக்கப்படக்கூடும்" + diff --git a/Tethering/res/values-mcc310-mnc004-te/strings.xml b/Tethering/res/values-mcc310-mnc004-te/strings.xml new file mode 100644 index 0000000000..937d34d520 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-te/strings.xml @@ -0,0 +1,24 @@ + + + + + "టెథరింగ్ చేయడానికి ఇంటర్నెట్ కనెక్షన్ లేదు" + "పరికరాలు కనెక్ట్ అవ్వడం లేదు" + "టెథరింగ్‌ను ఆఫ్ చేయండి" + "హాట్‌స్పాట్ లేదా టెథరింగ్ ఆన్‌లో ఉంది" + "రోమింగ్‌లో ఉన్నప్పుడు అదనపు ఛార్జీలు వర్తించవచ్చు" + diff --git a/Tethering/res/values-mcc310-mnc004-th/strings.xml b/Tethering/res/values-mcc310-mnc004-th/strings.xml new file mode 100644 index 0000000000..f781fae525 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-th/strings.xml @@ -0,0 +1,24 @@ + + + + + "การเชื่อมต่ออินเทอร์เน็ตผ่านมือถือไม่มีอินเทอร์เน็ต" + "อุปกรณ์เชื่อมต่อไม่ได้" + "ปิดการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือ" + "ฮอตสปอตหรือการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือเปิดอยู่" + "อาจมีค่าใช้จ่ายเพิ่มเติมขณะโรมมิ่ง" + diff --git a/Tethering/res/values-mcc310-mnc004-tl/strings.xml b/Tethering/res/values-mcc310-mnc004-tl/strings.xml new file mode 100644 index 0000000000..8d5d465373 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-tl/strings.xml @@ -0,0 +1,24 @@ + + + + + "Walang internet ang pag-tether" + "Hindi makakonekta ang mga device" + "I-off ang pag-tether" + "Naka-on ang Hotspot o pag-tether" + "Posibleng magkaroon ng mga karagdagang singil habang nagro-roam" + diff --git a/Tethering/res/values-mcc310-mnc004-tr/strings.xml b/Tethering/res/values-mcc310-mnc004-tr/strings.xml new file mode 100644 index 0000000000..80cab33ac0 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-tr/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering\'in internet bağlantısı yok" + "Cihazlar bağlanamıyor" + "Tethering\'i kapat" + "Hotspot veya tethering açık" + "Dolaşım sırasında ek ücretler uygulanabilir" + diff --git a/Tethering/res/values-mcc310-mnc004-uk/strings.xml b/Tethering/res/values-mcc310-mnc004-uk/strings.xml new file mode 100644 index 0000000000..c05932a5ae --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-uk/strings.xml @@ -0,0 +1,24 @@ + + + + + "Телефон, який використовується як модем, не підключений до Інтернету" + "Не вдається підключити пристрої" + "Вимкнути використання телефона як модема" + "Увімкнено точку доступу або використання телефона як модема" + "У роумінгу може стягуватися додаткова плата" + diff --git a/Tethering/res/values-mcc310-mnc004-ur/strings.xml b/Tethering/res/values-mcc310-mnc004-ur/strings.xml new file mode 100644 index 0000000000..d820eee8ba --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-ur/strings.xml @@ -0,0 +1,24 @@ + + + + + "ٹیدرنگ میں انٹرنیٹ نہیں ہے" + "آلات منسلک نہیں ہو سکتے" + "ٹیدرنگ آف کریں" + "ہاٹ اسپاٹ یا ٹیدرنگ آن ہے" + "رومنگ کے دوران اضافی چارجز لاگو ہو سکتے ہیں" + diff --git a/Tethering/res/values-mcc310-mnc004-uz/strings.xml b/Tethering/res/values-mcc310-mnc004-uz/strings.xml new file mode 100644 index 0000000000..726148aaee --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-uz/strings.xml @@ -0,0 +1,24 @@ + + + + + "Modem internetga ulanmagan" + "Qurilmalar ulanmadi" + "Modem rejimini faolsizlantirish" + "Hotspot yoki modem rejimi yoniq" + "Rouming vaqtida qoʻshimcha haq olinishi mumkin" + diff --git a/Tethering/res/values-mcc310-mnc004-vi/strings.xml b/Tethering/res/values-mcc310-mnc004-vi/strings.xml new file mode 100644 index 0000000000..b7cb0456b6 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-vi/strings.xml @@ -0,0 +1,24 @@ + + + + + "Không có Internet để chia sẻ kết Internet" + "Các thiết bị không thể kết nối" + "Tắt tính năng chia sẻ Internet" + "Điểm phát sóng hoặc tính năng chia sẻ Internet đang bật" + "Bạn có thể mất thêm phí dữ liệu khi chuyển vùng" + diff --git a/Tethering/res/values-mcc310-mnc004-zh-rCN/strings.xml b/Tethering/res/values-mcc310-mnc004-zh-rCN/strings.xml new file mode 100644 index 0000000000..af91afff9a --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-zh-rCN/strings.xml @@ -0,0 +1,24 @@ + + + + + "共享网络未连接到互联网" + "设备无法连接" + "关闭网络共享" + "热点或网络共享已开启" + "漫游时可能会产生额外的费用" + diff --git a/Tethering/res/values-mcc310-mnc004-zh-rHK/strings.xml b/Tethering/res/values-mcc310-mnc004-zh-rHK/strings.xml new file mode 100644 index 0000000000..28e6b80c01 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-zh-rHK/strings.xml @@ -0,0 +1,24 @@ + + + + + "無法透過網絡共享連線至互聯網" + "裝置無法連接" + "關閉網絡共享" + "熱點或網絡共享已開啟" + "漫遊時可能需要支付額外費用" + diff --git a/Tethering/res/values-mcc310-mnc004-zh-rTW/strings.xml b/Tethering/res/values-mcc310-mnc004-zh-rTW/strings.xml new file mode 100644 index 0000000000..528a1e5292 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-zh-rTW/strings.xml @@ -0,0 +1,24 @@ + + + + + "無法透過網路共用連上網際網路" + "裝置無法連線" + "關閉網路共用" + "無線基地台或網路共用已開啟" + "使用漫遊服務可能須支付額外費用" + diff --git a/Tethering/res/values-mcc310-mnc004-zu/strings.xml b/Tethering/res/values-mcc310-mnc004-zu/strings.xml new file mode 100644 index 0000000000..11eb666219 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004-zu/strings.xml @@ -0,0 +1,24 @@ + + + + + "Ukusebenzisa ifoni njengemodemu akunayo i-inthanethi" + "Amadivayisi awakwazi ukuxhumeka" + "Vala ukusebenzisa ifoni njengemodemu" + "I-hotspot noma ukusebenzisa ifoni njengemodemu kuvuliwe" + "Kungaba nezinkokhelo ezengeziwe uma uzula" + diff --git a/Tethering/res/values-mcc310-mnc004/config.xml b/Tethering/res/values-mcc310-mnc004/config.xml new file mode 100644 index 0000000000..5c5be0466a --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004/config.xml @@ -0,0 +1,23 @@ + + + + + 5000 + + + true + \ No newline at end of file diff --git a/Tethering/res/values-mcc310-mnc004/strings.xml b/Tethering/res/values-mcc310-mnc004/strings.xml new file mode 100644 index 0000000000..ce9ff60807 --- /dev/null +++ b/Tethering/res/values-mcc310-mnc004/strings.xml @@ -0,0 +1,28 @@ + + + + + Tethering has no internet + + Devices can\u2019t connect + + Turn off tethering + + + Hotspot or tethering is on + + Additional charges may apply while roaming + diff --git a/Tethering/res/values-mcc311-mnc480-af/strings.xml b/Tethering/res/values-mcc311-mnc480-af/strings.xml new file mode 100644 index 0000000000..9bfa5317a9 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-af/strings.xml @@ -0,0 +1,24 @@ + + + + + "Verbinding het nie internet nie" + "Toestelle kan nie koppel nie" + "Skakel verbinding af" + "Warmkol of verbinding is aan" + "Bykomende heffings kan geld terwyl jy swerf" + diff --git a/Tethering/res/values-mcc311-mnc480-am/strings.xml b/Tethering/res/values-mcc311-mnc480-am/strings.xml new file mode 100644 index 0000000000..5949dfa776 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-am/strings.xml @@ -0,0 +1,24 @@ + + + + + "ማስተሳሰር ምንም በይነመረብ የለውም" + "መሣሪያዎችን ማገናኘት አይቻልም" + "ማስተሳሰርን አጥፋ" + "መገናኛ ነጥብ ወይም ማስተሳሰር በርቷል" + "በሚያንዣብብበት ጊዜ ተጨማሪ ክፍያዎች ተፈጻሚ ሊሆኑ ይችላሉ" + diff --git a/Tethering/res/values-mcc311-mnc480-ar/strings.xml b/Tethering/res/values-mcc311-mnc480-ar/strings.xml new file mode 100644 index 0000000000..8467f9b1f5 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-ar/strings.xml @@ -0,0 +1,24 @@ + + + + + "ما مِن اتصال بالإنترنت خلال التوصيل" + "تعذّر اتصال الأجهزة" + "إيقاف التوصيل" + "نقطة الاتصال أو التوصيل مفعّلان" + "قد يتم تطبيق رسوم إضافية أثناء التجوال." + diff --git a/Tethering/res/values-mcc311-mnc480-as/strings.xml b/Tethering/res/values-mcc311-mnc480-as/strings.xml new file mode 100644 index 0000000000..9776bd89da --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-as/strings.xml @@ -0,0 +1,24 @@ + + + + + "টে\'ডাৰিঙৰ ইণ্টাৰনেট নাই" + "ডিভাইচসমূহ সংযোগ কৰিব নোৱাৰি" + "টে\'ডাৰিং অফ কৰক" + "হটস্পট অথবা টে\'ডাৰিং অন আছে" + "ৰ\'মিঙত থাকিলে অতিৰিক্ত মাচুল প্ৰযোজ্য হ’ব পাৰে" + diff --git a/Tethering/res/values-mcc311-mnc480-az/strings.xml b/Tethering/res/values-mcc311-mnc480-az/strings.xml new file mode 100644 index 0000000000..e6d3eaf9f0 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-az/strings.xml @@ -0,0 +1,24 @@ + + + + + "Modemin internetə girişi yoxdur" + "Cihazları qoşmaq mümkün deyil" + "Modemi deaktiv edin" + "Hotspot və ya modem aktivdir" + "Rouminq zamanı əlavə ödənişlər tətbiq edilə bilər" + diff --git a/Tethering/res/values-mcc311-mnc480-b+sr+Latn/strings.xml b/Tethering/res/values-mcc311-mnc480-b+sr+Latn/strings.xml new file mode 100644 index 0000000000..4c8a1df8ee --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-b+sr+Latn/strings.xml @@ -0,0 +1,24 @@ + + + + + "Privezivanje nema pristup internetu" + "Povezivanje uređaja nije uspelo" + "Isključi privezivanje" + "Uključen je hotspot ili privezivanje" + "Možda važe dodatni troškovi u romingu" + diff --git a/Tethering/res/values-mcc311-mnc480-be/strings.xml b/Tethering/res/values-mcc311-mnc480-be/strings.xml new file mode 100644 index 0000000000..edfa41e1ff --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-be/strings.xml @@ -0,0 +1,24 @@ + + + + + "Рэжым мадэма выкарыстоўваецца без доступу да інтэрнэту" + "Не ўдалося падключыць прылады" + "Выключыць рэжым мадэма" + "Хот-спот або рэжым мадэма ўключаны" + "Пры выкарыстанні роўмінгу можа спаганяцца дадатковая плата" + diff --git a/Tethering/res/values-mcc311-mnc480-bg/strings.xml b/Tethering/res/values-mcc311-mnc480-bg/strings.xml new file mode 100644 index 0000000000..f56398196f --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-bg/strings.xml @@ -0,0 +1,24 @@ + + + + + "Тетърингът няма връзка с интернет" + "Устройствата не могат да установят връзка" + "Изключване на тетъринга" + "Точката за достъп или тетърингът са включени" + "Възможно е да ви бъдат начислени допълнителни такси при роуминг" + diff --git a/Tethering/res/values-mcc311-mnc480-bn/strings.xml b/Tethering/res/values-mcc311-mnc480-bn/strings.xml new file mode 100644 index 0000000000..d8ecd2e988 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-bn/strings.xml @@ -0,0 +1,24 @@ + + + + + "টিথারিং করার জন্য কোনও ইন্টারনেট কানেকশন নেই" + "ডিভাইস কানেক্ট করতে পারছে না" + "টিথারিং বন্ধ করুন" + "হটস্পট বা টিথারিং চালু আছে" + "রোমিংয়ের সময় অতিরিক্ত চার্জ করা হতে পারে" + diff --git a/Tethering/res/values-mcc311-mnc480-bs/strings.xml b/Tethering/res/values-mcc311-mnc480-bs/strings.xml new file mode 100644 index 0000000000..b85fd5e285 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-bs/strings.xml @@ -0,0 +1,24 @@ + + + + + "Povezivanje putem mobitela nema internet" + "Uređaji se ne mogu povezati" + "Isključi povezivanje putem mobitela" + "Pristupna tačka ili povezivanje putem mobitela je uključeno" + "Mogu nastati dodatni troškovi u romingu" + diff --git a/Tethering/res/values-mcc311-mnc480-ca/strings.xml b/Tethering/res/values-mcc311-mnc480-ca/strings.xml new file mode 100644 index 0000000000..a3572151be --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-ca/strings.xml @@ -0,0 +1,24 @@ + + + + + "La compartició de xarxa no té accés a Internet" + "No es poden connectar els dispositius" + "Desactiva la compartició de xarxa" + "S\'ha activat el punt d\'accés Wi‑Fi o la compartició de xarxa" + "És possible que s\'apliquin costos addicionals en itinerància" + diff --git a/Tethering/res/values-mcc311-mnc480-cs/strings.xml b/Tethering/res/values-mcc311-mnc480-cs/strings.xml new file mode 100644 index 0000000000..91196be9e5 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-cs/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering nemá připojení k internetu" + "Zařízení se nemůžou připojit" + "Vypnout tethering" + "Je zapnutý hotspot nebo tethering" + "Při roamingu mohou být účtovány dodatečné poplatky" + diff --git a/Tethering/res/values-mcc311-mnc480-da/strings.xml b/Tethering/res/values-mcc311-mnc480-da/strings.xml new file mode 100644 index 0000000000..196890011d --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-da/strings.xml @@ -0,0 +1,24 @@ + + + + + "Netdeling har ingen internetforbindelse" + "Enheder kan ikke oprette forbindelse" + "Deaktiver netdeling" + "Hotspot eller netdeling er aktiveret" + "Der opkræves muligvis yderligere gebyrer ved roaming" + diff --git a/Tethering/res/values-mcc311-mnc480-de/strings.xml b/Tethering/res/values-mcc311-mnc480-de/strings.xml new file mode 100644 index 0000000000..eb3f8c52c0 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-de/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering hat keinen Internetzugriff" + "Geräte können sich nicht verbinden" + "Tethering deaktivieren" + "Hotspot oder Tethering ist aktiviert" + "Für das Roaming können zusätzliche Gebühren anfallen" + diff --git a/Tethering/res/values-mcc311-mnc480-el/strings.xml b/Tethering/res/values-mcc311-mnc480-el/strings.xml new file mode 100644 index 0000000000..56c3d81b63 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-el/strings.xml @@ -0,0 +1,24 @@ + + + + + "Η σύνδεση δεν έχει πρόσβαση στο διαδίκτυο" + "Δεν είναι δυνατή η σύνδεση των συσκευών" + "Απενεργοποιήστε τη σύνδεση" + "Ενεργό σημείο πρόσβασης Wi-Fi ή ενεργή σύνδεση" + "Ενδέχεται να ισχύουν επιπλέον χρεώσεις κατά την περιαγωγή." + diff --git a/Tethering/res/values-mcc311-mnc480-en-rAU/strings.xml b/Tethering/res/values-mcc311-mnc480-en-rAU/strings.xml new file mode 100644 index 0000000000..dd1a1971cd --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-en-rAU/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering has no Internet" + "Devices can’t connect" + "Turn off tethering" + "Hotspot or tethering is on" + "Additional charges may apply while roaming" + diff --git a/Tethering/res/values-mcc311-mnc480-en-rCA/strings.xml b/Tethering/res/values-mcc311-mnc480-en-rCA/strings.xml new file mode 100644 index 0000000000..dd1a1971cd --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-en-rCA/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering has no Internet" + "Devices can’t connect" + "Turn off tethering" + "Hotspot or tethering is on" + "Additional charges may apply while roaming" + diff --git a/Tethering/res/values-mcc311-mnc480-en-rGB/strings.xml b/Tethering/res/values-mcc311-mnc480-en-rGB/strings.xml new file mode 100644 index 0000000000..dd1a1971cd --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-en-rGB/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering has no Internet" + "Devices can’t connect" + "Turn off tethering" + "Hotspot or tethering is on" + "Additional charges may apply while roaming" + diff --git a/Tethering/res/values-mcc311-mnc480-en-rIN/strings.xml b/Tethering/res/values-mcc311-mnc480-en-rIN/strings.xml new file mode 100644 index 0000000000..dd1a1971cd --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-en-rIN/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering has no Internet" + "Devices can’t connect" + "Turn off tethering" + "Hotspot or tethering is on" + "Additional charges may apply while roaming" + diff --git a/Tethering/res/values-mcc311-mnc480-en-rXC/strings.xml b/Tethering/res/values-mcc311-mnc480-en-rXC/strings.xml new file mode 100644 index 0000000000..d3347aae20 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-en-rXC/strings.xml @@ -0,0 +1,24 @@ + + + + + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‎‎‎‎‎‏‎‎‏‏‏‏‎‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‎‏‎‏‏‎‏‏‏‎‎‏‎‏‎‎‎‏‎‎‎Tethering has no internet‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‏‎‎‏‎‏‎‎‏‎‎‏‏‏‎‏‏‎‏‎‏‎‏‎‎‎‏‎‎‎‎‎‏‏‏‏‎‏‎‎‎‎‏‎‏‏‎‏‏‎‏‎‎‏‏‏‏‏‎Devices can’t connect‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‎‎‏‏‎‎‏‏‏‎‏‎‏‎‎‎‏‏‏‎‎‏‏‏‏‎‎‏‏‏‏‏‏‎‎‎‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‎‎‎Turn off tethering‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‎‏‏‏‎‎‏‎‎‏‎‏‎‏‏‏‎‏‎‎‏‏‏‏‏‏‎‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‏‎‏‎‎‎‏‎‎‎‎‏‏‎Hotspot or tethering is on‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‏‏‎‏‎‏‎‏‎‎‎‏‎‏‎‏‏‎‏‎‎‎‏‏‏‏‎‎‏‏‏‏‎‎‎‏‎‎‎‎‏‏‎‏‎‏‎‎‏‏‎‎‏‏‎Additional charges may apply while roaming‎‏‎‎‏‎" + diff --git a/Tethering/res/values-mcc311-mnc480-es-rUS/strings.xml b/Tethering/res/values-mcc311-mnc480-es-rUS/strings.xml new file mode 100644 index 0000000000..2f0504f07d --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-es-rUS/strings.xml @@ -0,0 +1,24 @@ + + + + + "La conexión mediante dispositivo móvil no tiene Internet" + "No se pueden conectar los dispositivos" + "Desactivar conexión mediante dispositivo móvil" + "Se activó el hotspot o la conexión mediante dispositivo móvil" + "Es posible que se apliquen cargos adicionales por roaming" + diff --git a/Tethering/res/values-mcc311-mnc480-es/strings.xml b/Tethering/res/values-mcc311-mnc480-es/strings.xml new file mode 100644 index 0000000000..2d8f882425 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-es/strings.xml @@ -0,0 +1,24 @@ + + + + + "La conexión no se puede compartir, porque no hay acceso a Internet" + "Los dispositivos no se pueden conectar" + "Desactivar conexión compartida" + "Punto de acceso o conexión compartida activados" + "Puede que se apliquen cargos adicionales en itinerancia" + diff --git a/Tethering/res/values-mcc311-mnc480-et/strings.xml b/Tethering/res/values-mcc311-mnc480-et/strings.xml new file mode 100644 index 0000000000..8493c47071 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-et/strings.xml @@ -0,0 +1,24 @@ + + + + + "Jagamisel puudub internetiühendus" + "Seadmed ei saa ühendust luua" + "Lülita jagamine välja" + "Kuumkoht või jagamine on sisse lülitatud" + "Rändluse kasutamisega võivad kaasneda lisatasud" + diff --git a/Tethering/res/values-mcc311-mnc480-eu/strings.xml b/Tethering/res/values-mcc311-mnc480-eu/strings.xml new file mode 100644 index 0000000000..33bccab3e8 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-eu/strings.xml @@ -0,0 +1,24 @@ + + + + + "Konexioa partekatzeko aukerak ez du Interneteko konexiorik" + "Ezin dira konektatu gailuak" + "Desaktibatu konexioa partekatzeko aukera" + "Wifi-gunea edo konexioa partekatzeko aukera aktibatuta dago" + "Baliteke kostu gehigarriak ordaindu behar izatea ibiltaritzan" + diff --git a/Tethering/res/values-mcc311-mnc480-fa/strings.xml b/Tethering/res/values-mcc311-mnc480-fa/strings.xml new file mode 100644 index 0000000000..cf8a0cc277 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-fa/strings.xml @@ -0,0 +1,24 @@ + + + + + "«اشتراک‌گذاری اینترنت» به اینترنت دسترسی ندارد" + "دستگاه‌ها متصل نمی‌شوند" + "خاموش کردن «اشتراک‌گذاری اینترنت»" + "«نقطه اتصال» یا «اشتراک‌گذاری اینترنت» روشن است" + "ممکن است درحین فراگردی تغییرات دیگر اعمال شود" + diff --git a/Tethering/res/values-mcc311-mnc480-fi/strings.xml b/Tethering/res/values-mcc311-mnc480-fi/strings.xml new file mode 100644 index 0000000000..6a3ab806db --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-fi/strings.xml @@ -0,0 +1,24 @@ + + + + + "Ei jaettavaa internetyhteyttä" + "Laitteet eivät voi muodostaa yhteyttä" + "Laita yhteyden jakaminen pois päältä" + "Hotspot tai yhteyden jakaminen on päällä" + "Roaming voi aiheuttaa lisämaksuja" + diff --git a/Tethering/res/values-mcc311-mnc480-fr-rCA/strings.xml b/Tethering/res/values-mcc311-mnc480-fr-rCA/strings.xml new file mode 100644 index 0000000000..ffb9bf6047 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-fr-rCA/strings.xml @@ -0,0 +1,24 @@ + + + + + "Le partage de connexion n\'est pas connecté à Internet" + "Impossible de connecter les appareils" + "Désactiver le partage de connexion" + "Le point d\'accès ou le partage de connexion est activé" + "En itinérance, des frais supplémentaires peuvent s\'appliquer" + diff --git a/Tethering/res/values-mcc311-mnc480-fr/strings.xml b/Tethering/res/values-mcc311-mnc480-fr/strings.xml new file mode 100644 index 0000000000..768bce3f0a --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-fr/strings.xml @@ -0,0 +1,24 @@ + + + + + "Aucune connexion à Internet n\'est disponible pour le partage de connexion" + "Impossible de connecter les appareils" + "Désactiver le partage de connexion" + "Le point d\'accès ou le partage de connexion est activé" + "En itinérance, des frais supplémentaires peuvent s\'appliquer" + diff --git a/Tethering/res/values-mcc311-mnc480-gl/strings.xml b/Tethering/res/values-mcc311-mnc480-gl/strings.xml new file mode 100644 index 0000000000..0c4195a7ca --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-gl/strings.xml @@ -0,0 +1,24 @@ + + + + + "A conexión compartida non ten Internet" + "Non se puideron conectar os dispositivos" + "Desactivar conexión compartida" + "Está activada a zona wifi ou a conexión compartida" + "Pódense aplicar cargos adicionais en itinerancia" + diff --git a/Tethering/res/values-mcc311-mnc480-gu/strings.xml b/Tethering/res/values-mcc311-mnc480-gu/strings.xml new file mode 100644 index 0000000000..e9d33a7db2 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-gu/strings.xml @@ -0,0 +1,24 @@ + + + + + "ઇન્ટરનેટ શેર કરવાની સુવિધામાં ઇન્ટરનેટ નથી" + "ડિવાઇસ કનેક્ટ કરી શકાતા નથી" + "ઇન્ટરનેટ શેર કરવાની સુવિધા બંધ કરો" + "હૉટસ્પૉટ અથવા ઇન્ટરનેટ શેર કરવાની સુવિધા ચાલુ છે" + "રોમિંગમાં વધારાના શુલ્ક લાગી શકે છે" + diff --git a/Tethering/res/values-mcc311-mnc480-hi/strings.xml b/Tethering/res/values-mcc311-mnc480-hi/strings.xml new file mode 100644 index 0000000000..aa418ac5d3 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-hi/strings.xml @@ -0,0 +1,24 @@ + + + + + "टेदरिंग से इंटरनेट नहीं चल रहा" + "डिवाइस कनेक्ट नहीं हो पा रहे" + "टेदरिंग बंद करें" + "हॉटस्पॉट या टेदरिंग चालू है" + "रोमिंग के दौरान अतिरिक्त शुल्क लग सकता है" + diff --git a/Tethering/res/values-mcc311-mnc480-hr/strings.xml b/Tethering/res/values-mcc311-mnc480-hr/strings.xml new file mode 100644 index 0000000000..51c524afbc --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-hr/strings.xml @@ -0,0 +1,24 @@ + + + + + "Modemsko povezivanje nema internet" + "Uređaji se ne mogu povezati" + "Isključivanje modemskog povezivanja" + "Uključena je žarišna točka ili modemsko povezivanje" + "U roamingu su mogući dodatni troškovi" + diff --git a/Tethering/res/values-mcc311-mnc480-hu/strings.xml b/Tethering/res/values-mcc311-mnc480-hu/strings.xml new file mode 100644 index 0000000000..164e45edd1 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-hu/strings.xml @@ -0,0 +1,24 @@ + + + + + "Nincs internetkapcsolat az internet megosztásához" + "Az eszközök nem tudnak csatlakozni" + "Internetmegosztás kikapcsolása" + "A hotspot vagy az internetmegosztás be van kapcsolva" + "Roaming során további díjak léphetnek fel" + diff --git a/Tethering/res/values-mcc311-mnc480-hy/strings.xml b/Tethering/res/values-mcc311-mnc480-hy/strings.xml new file mode 100644 index 0000000000..e76c0a4c80 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-hy/strings.xml @@ -0,0 +1,24 @@ + + + + + "Մոդեմի ռեժիմի կապը բացակայում է" + "Չհաջողվեց միացնել սարքը" + "Անջատել մոդեմի ռեժիմը" + "Թեժ կետը կամ մոդեմի ռեժիմը միացված է" + "Ռոումինգում կարող են լրացուցիչ վճարներ գանձվել" + diff --git a/Tethering/res/values-mcc311-mnc480-in/strings.xml b/Tethering/res/values-mcc311-mnc480-in/strings.xml new file mode 100644 index 0000000000..2b817f8abd --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-in/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tidak ada koneksi internet di tethering" + "Perangkat tidak dapat terhubung" + "Nonaktifkan tethering" + "Hotspot atau tethering aktif" + "Biaya tambahan mungkin berlaku saat roaming" + diff --git a/Tethering/res/values-mcc311-mnc480-is/strings.xml b/Tethering/res/values-mcc311-mnc480-is/strings.xml new file mode 100644 index 0000000000..a338d9c7ca --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-is/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tjóðrun er ekki með internettengingu" + "Tæki geta ekki tengst" + "Slökkva á tjóðrun" + "Kveikt er á heitum reit eða tjóðrun" + "Viðbótargjöld kunna að eiga við í reiki" + diff --git a/Tethering/res/values-mcc311-mnc480-it/strings.xml b/Tethering/res/values-mcc311-mnc480-it/strings.xml new file mode 100644 index 0000000000..77769c2ac5 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-it/strings.xml @@ -0,0 +1,24 @@ + + + + + "Nessuna connessione a Internet per il tethering" + "Impossibile connettere i dispositivi" + "Disattiva il tethering" + "Hotspot o tethering attivi" + "Potrebbero essere applicati costi aggiuntivi durante il roaming" + diff --git a/Tethering/res/values-mcc311-mnc480-iw/strings.xml b/Tethering/res/values-mcc311-mnc480-iw/strings.xml new file mode 100644 index 0000000000..5267b51264 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-iw/strings.xml @@ -0,0 +1,24 @@ + + + + + "אי אפשר להפעיל את תכונת שיתוף האינטרנט בין מכשירים כי אין חיבור לאינטרנט" + "למכשירים אין אפשרות להתחבר" + "השבתה של שיתוף האינטרנט בין מכשירים" + "תכונת הנקודה לשיתוף אינטרנט או תכונת שיתוף האינטרנט בין מכשירים פועלת" + "ייתכנו חיובים נוספים בעת נדידה" + diff --git a/Tethering/res/values-mcc311-mnc480-ja/strings.xml b/Tethering/res/values-mcc311-mnc480-ja/strings.xml new file mode 100644 index 0000000000..66a9a6dd35 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-ja/strings.xml @@ -0,0 +1,24 @@ + + + + + "テザリングがインターネットに接続されていません" + "デバイスを接続できません" + "テザリングを OFF にする" + "アクセス ポイントまたはテザリングが ON です" + "ローミング時に追加料金が発生することがあります" + diff --git a/Tethering/res/values-mcc311-mnc480-ka/strings.xml b/Tethering/res/values-mcc311-mnc480-ka/strings.xml new file mode 100644 index 0000000000..d8ad880849 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-ka/strings.xml @@ -0,0 +1,24 @@ + + + + + "ტეტერინგს არ აქვს ინტერნეტზე წვდომა" + "მოწყობილობები ვერ ახერხებენ დაკავშირებას" + "ტეტერინგის გამორთვა" + "ჩართულია უსადენო ქსელი ან ტეტერინგი" + "როუმინგის გამოყენებისას შეიძლება ჩამოგეჭრათ დამატებითი საფასური" + diff --git a/Tethering/res/values-mcc311-mnc480-kk/strings.xml b/Tethering/res/values-mcc311-mnc480-kk/strings.xml new file mode 100644 index 0000000000..1ddd6b419b --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-kk/strings.xml @@ -0,0 +1,24 @@ + + + + + "Тетеринг режимі интернет байланысынсыз пайдаланылуда" + "Құрылғыларды байланыстыру мүмкін емес" + "Тетерингіні өшіру" + "Хотспот немесе тетеринг қосулы" + "Роуминг кезінде қосымша ақы алынуы мүмкін." + diff --git a/Tethering/res/values-mcc311-mnc480-km/strings.xml b/Tethering/res/values-mcc311-mnc480-km/strings.xml new file mode 100644 index 0000000000..cf5a1379cc --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-km/strings.xml @@ -0,0 +1,24 @@ + + + + + "ការភ្ជាប់​មិនមានអ៊ីនធឺណិត​ទេ" + "មិនអាច​ភ្ជាប់ឧបករណ៍​បានទេ" + "បិទការភ្ជាប់" + "ហតស្ប៉ត ឬការភ្ជាប់​ត្រូវបានបើក" + "អាចមាន​ការគិតថ្លៃ​បន្ថែម នៅពេល​រ៉ូមីង" + diff --git a/Tethering/res/values-mcc311-mnc480-kn/strings.xml b/Tethering/res/values-mcc311-mnc480-kn/strings.xml new file mode 100644 index 0000000000..68ae68bc19 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-kn/strings.xml @@ -0,0 +1,24 @@ + + + + + "ಟೆಥರಿಂಗ್‌ ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಕನೆಕ್ಷನ್ ಹೊಂದಿಲ್ಲ" + "ಸಾಧನಗಳನ್ನು ಕನೆಕ್ಟ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ" + "ಟೆಥರಿಂಗ್‌ ಆಫ್ ಮಾಡಿ" + "ಹಾಟ್‌ಸ್ಪಾಟ್ ಅಥವಾ ಟೆಥರಿಂಗ್‌ ಆನ್ ಆಗಿದೆ" + "ರೋಮಿಂಗ್‌ನಲ್ಲಿರುವಾಗ ಹೆಚ್ಚುವರಿ ಶುಲ್ಕಗಳು ಅನ್ವಯವಾಗಬಹುದು" + diff --git a/Tethering/res/values-mcc311-mnc480-ko/strings.xml b/Tethering/res/values-mcc311-mnc480-ko/strings.xml new file mode 100644 index 0000000000..17185ba2d0 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-ko/strings.xml @@ -0,0 +1,24 @@ + + + + + "테더링으로 인터넷을 사용할 수 없음" + "기기에서 연결할 수 없음" + "테더링 사용 중지" + "핫스팟 또는 테더링 켜짐" + "로밍 중에는 추가 요금이 발생할 수 있습니다." + diff --git a/Tethering/res/values-mcc311-mnc480-ky/strings.xml b/Tethering/res/values-mcc311-mnc480-ky/strings.xml new file mode 100644 index 0000000000..6a9fb9810c --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-ky/strings.xml @@ -0,0 +1,24 @@ + + + + + "Модем режими Интернети жок колдонулууда" + "Түзмөктөр туташпай жатат" + "Модем режимин өчүрүү" + "Байланыш түйүнү же модем режими күйүк" + "Роумингде кошумча акы алынышы мүмкүн" + diff --git a/Tethering/res/values-mcc311-mnc480-lo/strings.xml b/Tethering/res/values-mcc311-mnc480-lo/strings.xml new file mode 100644 index 0000000000..bcc4b57626 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-lo/strings.xml @@ -0,0 +1,24 @@ + + + + + "ການປ່ອຍສັນຍານບໍ່ມີອິນເຕີເນັດ" + "ອຸປະກອນບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້" + "ປິດການປ່ອຍສັນຍານ" + "ເປີດໃຊ້ຮັອດສະປອດ ຫຼື ການປ່ອຍສັນຍານຢູ່" + "ອາດມີຄ່າໃຊ້ຈ່າຍເພີ່ມເຕີມໃນລະຫວ່າງການໂຣມມິງ" + diff --git a/Tethering/res/values-mcc311-mnc480-lt/strings.xml b/Tethering/res/values-mcc311-mnc480-lt/strings.xml new file mode 100644 index 0000000000..011c2c11fb --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-lt/strings.xml @@ -0,0 +1,24 @@ + + + + + "Nėra įrenginio kaip modemo naudojimo interneto ryšio" + "Nepavyko susieti įrenginių" + "Išjungti įrenginio kaip modemo naudojimą" + "Įjungtas viešosios interneto prieigos taškas arba įrenginio kaip modemo naudojimas" + "Veikiant tarptinkliniam ryšiui gali būti taikomi papildomi mokesčiai" + diff --git a/Tethering/res/values-mcc311-mnc480-lv/strings.xml b/Tethering/res/values-mcc311-mnc480-lv/strings.xml new file mode 100644 index 0000000000..5cb2f3b7aa --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-lv/strings.xml @@ -0,0 +1,24 @@ + + + + + "Piesaistei nav interneta savienojuma" + "Nevar savienot ierīces" + "Izslēgt piesaisti" + "Ir ieslēgts tīklājs vai piesaiste" + "Viesabonēšanas laikā var tikt piemērota papildu samaksa" + diff --git a/Tethering/res/values-mcc311-mnc480-mk/strings.xml b/Tethering/res/values-mcc311-mnc480-mk/strings.xml new file mode 100644 index 0000000000..4cbfd887c5 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-mk/strings.xml @@ -0,0 +1,24 @@ + + + + + "Нема интернет преку мобилен" + "Уредите не може да се поврзат" + "Исклучи интернет преку мобилен" + "Точката на пристап или интернетот преку мобилен е вклучен" + "При роаминг може да се наплатат дополнителни трошоци" + diff --git a/Tethering/res/values-mcc311-mnc480-ml/strings.xml b/Tethering/res/values-mcc311-mnc480-ml/strings.xml new file mode 100644 index 0000000000..9cf4eaf34a --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-ml/strings.xml @@ -0,0 +1,24 @@ + + + + + "ടെതറിംഗിന് ഇന്റർനെറ്റ് ഇല്ല" + "ഉപകരണങ്ങൾ കണക്റ്റ് ചെയ്യാനാവില്ല" + "ടെതറിംഗ് ഓഫാക്കുക" + "ഹോട്ട്‌സ്‌പോട്ട് അല്ലെങ്കിൽ ടെതറിംഗ് ഓണാണ്" + "റോമിംഗ് ചെയ്യുമ്പോൾ അധിക നിരക്കുകൾ ബാധകമായേക്കാം" + diff --git a/Tethering/res/values-mcc311-mnc480-mn/strings.xml b/Tethering/res/values-mcc311-mnc480-mn/strings.xml new file mode 100644 index 0000000000..47c82c14d9 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-mn/strings.xml @@ -0,0 +1,24 @@ + + + + + "Модемд интернэт алга байна" + "Төхөөрөмжүүд холбогдох боломжгүй байна" + "Модем болгохыг унтраах" + "Сүлжээний цэг эсвэл модем болгох асаалттай байна" + "Роумингийн үеэр нэмэлт төлбөр нэхэмжилж болзошгүй" + diff --git a/Tethering/res/values-mcc311-mnc480-mr/strings.xml b/Tethering/res/values-mcc311-mnc480-mr/strings.xml new file mode 100644 index 0000000000..ad9e809ab2 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-mr/strings.xml @@ -0,0 +1,24 @@ + + + + + "टेदरिंगला इंटरनेट नाही" + "डिव्हाइस कनेक्ट होऊ शकत नाहीत" + "टेदरिंग बंद करा" + "हॉटस्पॉट किंवा टेदरिंग सुरू आहे" + "रोमिंगदरम्यान अतिरिक्त शुल्क लागू होऊ शकतात" + diff --git a/Tethering/res/values-mcc311-mnc480-ms/strings.xml b/Tethering/res/values-mcc311-mnc480-ms/strings.xml new file mode 100644 index 0000000000..e708cb8717 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-ms/strings.xml @@ -0,0 +1,24 @@ + + + + + "Penambatan tiada Internet" + "Peranti tidak dapat disambungkan" + "Matikan penambatan" + "Tempat liputan atau penambatan dihidupkan" + "Caj tambahan mungkin digunakan semasa perayauan" + diff --git a/Tethering/res/values-mcc311-mnc480-my/strings.xml b/Tethering/res/values-mcc311-mnc480-my/strings.xml new file mode 100644 index 0000000000..ba5462250b --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-my/strings.xml @@ -0,0 +1,24 @@ + + + + + "မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်းတွင် အင်တာနက် မရှိပါ" + "စက်များ ချိတ်ဆက်၍ မရပါ" + "မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း ပိတ်ရန်" + "ဟော့စပေါ့ (သို့) မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း ဖွင့်ထားသည်" + "ပြင်ပကွန်ရက်နှင့် ချိတ်ဆက်သည့်အခါ နောက်ထပ်ကျသင့်မှုများ ရှိနိုင်သည်" + diff --git a/Tethering/res/values-mcc311-mnc480-nb/strings.xml b/Tethering/res/values-mcc311-mnc480-nb/strings.xml new file mode 100644 index 0000000000..57db484a25 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-nb/strings.xml @@ -0,0 +1,24 @@ + + + + + "Internettdeling har ikke internettilgang" + "Enhetene kan ikke koble til" + "Slå av internettdeling" + "Wi-Fi-sone eller internettdeling er på" + "Ytterligere kostnader kan påløpe under roaming" + diff --git a/Tethering/res/values-mcc311-mnc480-ne/strings.xml b/Tethering/res/values-mcc311-mnc480-ne/strings.xml new file mode 100644 index 0000000000..1503244f50 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-ne/strings.xml @@ -0,0 +1,24 @@ + + + + + "टेदरिङमार्फत इन्टरनेट कनेक्सन प्राप्त हुन सकेन" + "यन्त्रहरू कनेक्ट गर्न सकिएन" + "टेदरिङ निष्क्रिय पार्नुहोस्" + "हटस्पट वा टेदरिङ सक्रिय छ" + "रोमिङ सेवा प्रयोग गर्दा अतिरिक्त शुल्क लाग्न सक्छ" + diff --git a/Tethering/res/values-mcc311-mnc480-nl/strings.xml b/Tethering/res/values-mcc311-mnc480-nl/strings.xml new file mode 100644 index 0000000000..b08133f4e5 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-nl/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering heeft geen internet" + "Apparaten kunnen niet worden verbonden" + "Tethering uitschakelen" + "Hotspot of tethering is ingeschakeld" + "Er kunnen extra kosten voor roaming in rekening worden gebracht." + diff --git a/Tethering/res/values-mcc311-mnc480-or/strings.xml b/Tethering/res/values-mcc311-mnc480-or/strings.xml new file mode 100644 index 0000000000..1ad4ca354a --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-or/strings.xml @@ -0,0 +1,24 @@ + + + + + "ଟିଥରିଂ ପାଇଁ କୌଣସି ଇଣ୍ଟର୍ନେଟ୍ ସଂଯୋଗ ନାହିଁ" + "ଡିଭାଇସଗୁଡ଼ିକ ସଂଯୋଗ କରାଯାଇପାରିବ ନାହିଁ" + "ଟିଥରିଂ ବନ୍ଦ କରନ୍ତୁ" + "ହଟସ୍ପଟ୍ କିମ୍ବା ଟିଥରିଂ ଚାଲୁ ଅଛି" + "ରୋମିଂରେ ଥିବା ସମୟରେ ଅତିରିକ୍ତ ଶୁଳ୍କ ଲାଗୁ ହୋଇପାରେ" + diff --git a/Tethering/res/values-mcc311-mnc480-pa/strings.xml b/Tethering/res/values-mcc311-mnc480-pa/strings.xml new file mode 100644 index 0000000000..88def563d8 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-pa/strings.xml @@ -0,0 +1,24 @@ + + + + + "ਟੈਦਰਿੰਗ ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹੁੰਚ ਨਹੀਂ ਹੈ" + "ਡੀਵਾਈਸ ਕਨੈਕਟ ਨਹੀਂ ਕੀਤੇ ਜਾ ਸਕਦੇ" + "ਟੈਦਰਿੰਗ ਬੰਦ ਕਰੋ" + "ਹੌਟਸਪੌਟ ਜਾਂ ਟੈਦਰਿੰਗ ਚਾਲੂ ਹੈ" + "ਰੋਮਿੰਗ ਦੌਰਾਨ ਵਧੀਕ ਖਰਚੇ ਲਾਗੂ ਹੋ ਸਕਦੇ ਹਨ" + diff --git a/Tethering/res/values-mcc311-mnc480-pl/strings.xml b/Tethering/res/values-mcc311-mnc480-pl/strings.xml new file mode 100644 index 0000000000..f9890abdc2 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-pl/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering nie ma internetu" + "Urządzenia nie mogą się połączyć" + "Wyłącz tethering" + "Hotspot lub tethering jest włączony" + "Podczas korzystania z roamingu mogą zostać naliczone dodatkowe opłaty" + diff --git a/Tethering/res/values-mcc311-mnc480-pt-rBR/strings.xml b/Tethering/res/values-mcc311-mnc480-pt-rBR/strings.xml new file mode 100644 index 0000000000..ce3b88479f --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-pt-rBR/strings.xml @@ -0,0 +1,24 @@ + + + + + "O tethering não tem Internet" + "Não é possível conectar os dispositivos" + "Desativar o tethering" + "Ponto de acesso ou tethering ativado" + "Pode haver cobranças extras durante o roaming" + diff --git a/Tethering/res/values-mcc311-mnc480-pt-rPT/strings.xml b/Tethering/res/values-mcc311-mnc480-pt-rPT/strings.xml new file mode 100644 index 0000000000..7e883ea576 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-pt-rPT/strings.xml @@ -0,0 +1,24 @@ + + + + + "A ligação (à Internet) via telemóvel não tem Internet" + "Não é possível ligar os dispositivos" + "Desativar ligação (à Internet) via telemóvel" + "A zona Wi-Fi ou a ligação (à Internet) via telemóvel está ativada" + "Podem aplicar-se custos adicionais em roaming." + diff --git a/Tethering/res/values-mcc311-mnc480-pt/strings.xml b/Tethering/res/values-mcc311-mnc480-pt/strings.xml new file mode 100644 index 0000000000..ce3b88479f --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-pt/strings.xml @@ -0,0 +1,24 @@ + + + + + "O tethering não tem Internet" + "Não é possível conectar os dispositivos" + "Desativar o tethering" + "Ponto de acesso ou tethering ativado" + "Pode haver cobranças extras durante o roaming" + diff --git a/Tethering/res/values-mcc311-mnc480-ro/strings.xml b/Tethering/res/values-mcc311-mnc480-ro/strings.xml new file mode 100644 index 0000000000..1009417316 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-ro/strings.xml @@ -0,0 +1,24 @@ + + + + + "Procesul de tethering nu are internet" + "Dispozitivele nu se pot conecta" + "Dezactivați procesul de tethering" + "S-a activat hotspotul sau tethering" + "Se pot aplica taxe suplimentare pentru roaming" + diff --git a/Tethering/res/values-mcc311-mnc480-ru/strings.xml b/Tethering/res/values-mcc311-mnc480-ru/strings.xml new file mode 100644 index 0000000000..88683bed95 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-ru/strings.xml @@ -0,0 +1,24 @@ + + + + + "Режим модема используется без доступа к Интернету" + "Невозможно подключить устройства." + "Отключить режим модема" + "Включены точка доступа или режим модема" + "За использование услуг связи в роуминге может взиматься дополнительная плата." + diff --git a/Tethering/res/values-mcc311-mnc480-si/strings.xml b/Tethering/res/values-mcc311-mnc480-si/strings.xml new file mode 100644 index 0000000000..176bcdb797 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-si/strings.xml @@ -0,0 +1,24 @@ + + + + + "ටෙදරින් හට අන්තර්ජාලය නැත" + "උපාංගවලට සම්බන්ධ විය නොහැකිය" + "ටෙදරින් ක්‍රියාවිරහිත කරන්න" + "හොට්ස්පොට් හෝ ටෙදරින් ක්‍රියාත්මකයි" + "රෝමිං අතරතුර අමතර ගාස්තු අදාළ විය හැකිය" + diff --git a/Tethering/res/values-mcc311-mnc480-sk/strings.xml b/Tethering/res/values-mcc311-mnc480-sk/strings.xml new file mode 100644 index 0000000000..b9e2127fa8 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-sk/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering nemá internetové pripojenie" + "Zariadenia sa nemôžu pripojiť" + "Vypnúť tethering" + "Je zapnutý hotspot alebo tethering" + "Počas roamingu vám môžu byť účtované ďalšie poplatky" + diff --git a/Tethering/res/values-mcc311-mnc480-sl/strings.xml b/Tethering/res/values-mcc311-mnc480-sl/strings.xml new file mode 100644 index 0000000000..e8140e686a --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-sl/strings.xml @@ -0,0 +1,24 @@ + + + + + "Internetna povezava prek mobilnega telefona ni vzpostavljena" + "Napravi se ne moreta povezati" + "Izklopi internetno povezavo prek mobilnega telefona" + "Dostopna točka ali internetna povezava prek mobilnega telefona je vklopljena" + "Med gostovanjem lahko nastanejo dodatni stroški" + diff --git a/Tethering/res/values-mcc311-mnc480-sq/strings.xml b/Tethering/res/values-mcc311-mnc480-sq/strings.xml new file mode 100644 index 0000000000..61e698d6e8 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-sq/strings.xml @@ -0,0 +1,24 @@ + + + + + "Ndarja e internetit nuk ka internet" + "Pajisjet nuk mund të lidhen" + "Çaktivizo ndarjen e internetit" + "Zona e qasjes për internet ose ndarja e internetit është aktive" + "Mund të zbatohen tarifime shtesë kur je në roaming" + diff --git a/Tethering/res/values-mcc311-mnc480-sr/strings.xml b/Tethering/res/values-mcc311-mnc480-sr/strings.xml new file mode 100644 index 0000000000..b4c411c354 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-sr/strings.xml @@ -0,0 +1,24 @@ + + + + + "Привезивање нема приступ интернету" + "Повезивање уређаја није успело" + "Искључи привезивање" + "Укључен је хотспот или привезивање" + "Можда важе додатни трошкови у ромингу" + diff --git a/Tethering/res/values-mcc311-mnc480-sv/strings.xml b/Tethering/res/values-mcc311-mnc480-sv/strings.xml new file mode 100644 index 0000000000..4f543e47b9 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-sv/strings.xml @@ -0,0 +1,24 @@ + + + + + "Det finns ingen internetanslutning för internetdelningen" + "Enheterna kan inte anslutas" + "Inaktivera internetdelning" + "Surfzon eller internetdelning har aktiverats" + "Ytterligare avgifter kan tillkomma vid roaming" + diff --git a/Tethering/res/values-mcc311-mnc480-sw/strings.xml b/Tethering/res/values-mcc311-mnc480-sw/strings.xml new file mode 100644 index 0000000000..ac347ab485 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-sw/strings.xml @@ -0,0 +1,24 @@ + + + + + "Kipengele cha kusambaza mtandao hakina intaneti" + "Imeshindwa kuunganisha vifaa" + "Zima kipengele cha kusambaza mtandao" + "Umewasha kipengele cha kusambaza mtandao au mtandao pepe" + "Huenda ukatozwa gharama za ziada ukitumia mitandao ya ng\'ambo" + diff --git a/Tethering/res/values-mcc311-mnc480-ta/strings.xml b/Tethering/res/values-mcc311-mnc480-ta/strings.xml new file mode 100644 index 0000000000..2ea2467e58 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-ta/strings.xml @@ -0,0 +1,24 @@ + + + + + "இணைப்பு முறைக்கு இணைய இணைப்பு இல்லை" + "சாதனங்களால் இணைய முடியவில்லை" + "இணைப்பு முறையை ஆஃப் செய்" + "ஹாட்ஸ்பாட் அல்லது இணைப்பு முறை ஆன் செய்யப்பட்டுள்ளது" + "ரோமிங்கின்போது கூடுதல் கட்டணங்கள் விதிக்கப்படக்கூடும்" + diff --git a/Tethering/res/values-mcc311-mnc480-te/strings.xml b/Tethering/res/values-mcc311-mnc480-te/strings.xml new file mode 100644 index 0000000000..9360297dd8 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-te/strings.xml @@ -0,0 +1,24 @@ + + + + + "టెథరింగ్ చేయడానికి ఇంటర్నెట్ కనెక్షన్ లేదు" + "పరికరాలు కనెక్ట్ అవ్వడం లేదు" + "టెథరింగ్‌ను ఆఫ్ చేయండి" + "హాట్‌స్పాట్ లేదా టెథరింగ్ ఆన్‌లో ఉంది" + "రోమింగ్‌లో ఉన్నప్పుడు అదనపు ఛార్జీలు వర్తించవచ్చు" + diff --git a/Tethering/res/values-mcc311-mnc480-th/strings.xml b/Tethering/res/values-mcc311-mnc480-th/strings.xml new file mode 100644 index 0000000000..9c4d7e08f2 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-th/strings.xml @@ -0,0 +1,24 @@ + + + + + "การเชื่อมต่ออินเทอร์เน็ตผ่านมือถือไม่มีอินเทอร์เน็ต" + "อุปกรณ์เชื่อมต่อไม่ได้" + "ปิดการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือ" + "ฮอตสปอตหรือการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือเปิดอยู่" + "อาจมีค่าใช้จ่ายเพิ่มเติมขณะโรมมิ่ง" + diff --git a/Tethering/res/values-mcc311-mnc480-tl/strings.xml b/Tethering/res/values-mcc311-mnc480-tl/strings.xml new file mode 100644 index 0000000000..a7c78a5932 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-tl/strings.xml @@ -0,0 +1,24 @@ + + + + + "Walang internet ang pag-tether" + "Hindi makakonekta ang mga device" + "I-off ang pag-tether" + "Naka-on ang Hotspot o pag-tether" + "Posibleng magkaroon ng mga karagdagang singil habang nagro-roam" + diff --git a/Tethering/res/values-mcc311-mnc480-tr/strings.xml b/Tethering/res/values-mcc311-mnc480-tr/strings.xml new file mode 100644 index 0000000000..93da2c3f79 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-tr/strings.xml @@ -0,0 +1,24 @@ + + + + + "Tethering\'in internet bağlantısı yok" + "Cihazlar bağlanamıyor" + "Tethering\'i kapat" + "Hotspot veya tethering açık" + "Dolaşım sırasında ek ücretler uygulanabilir" + diff --git a/Tethering/res/values-mcc311-mnc480-uk/strings.xml b/Tethering/res/values-mcc311-mnc480-uk/strings.xml new file mode 100644 index 0000000000..ee0dcd2c4b --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-uk/strings.xml @@ -0,0 +1,24 @@ + + + + + "Телефон, який використовується як модем, не підключений до Інтернету" + "Не вдається підключити пристрої" + "Вимкнути використання телефона як модема" + "Увімкнено точку доступу або використання телефона як модема" + "У роумінгу може стягуватися додаткова плата" + diff --git a/Tethering/res/values-mcc311-mnc480-ur/strings.xml b/Tethering/res/values-mcc311-mnc480-ur/strings.xml new file mode 100644 index 0000000000..41cd28eef9 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-ur/strings.xml @@ -0,0 +1,24 @@ + + + + + "ٹیدرنگ میں انٹرنیٹ نہیں ہے" + "آلات منسلک نہیں ہو سکتے" + "ٹیدرنگ آف کریں" + "ہاٹ اسپاٹ یا ٹیدرنگ آن ہے" + "رومنگ کے دوران اضافی چارجز لاگو ہو سکتے ہیں" + diff --git a/Tethering/res/values-mcc311-mnc480-uz/strings.xml b/Tethering/res/values-mcc311-mnc480-uz/strings.xml new file mode 100644 index 0000000000..c847bc943b --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-uz/strings.xml @@ -0,0 +1,24 @@ + + + + + "Modem internetga ulanmagan" + "Qurilmalar ulanmadi" + "Modem rejimini faolsizlantirish" + "Hotspot yoki modem rejimi yoniq" + "Rouming vaqtida qoʻshimcha haq olinishi mumkin" + diff --git a/Tethering/res/values-mcc311-mnc480-vi/strings.xml b/Tethering/res/values-mcc311-mnc480-vi/strings.xml new file mode 100644 index 0000000000..a74326f09e --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-vi/strings.xml @@ -0,0 +1,24 @@ + + + + + "Không có Internet để chia sẻ kết Internet" + "Các thiết bị không thể kết nối" + "Tắt tính năng chia sẻ Internet" + "Điểm phát sóng hoặc tính năng chia sẻ Internet đang bật" + "Bạn có thể mất thêm phí dữ liệu khi chuyển vùng" + diff --git a/Tethering/res/values-mcc311-mnc480-zh-rCN/strings.xml b/Tethering/res/values-mcc311-mnc480-zh-rCN/strings.xml new file mode 100644 index 0000000000..d7370036e3 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-zh-rCN/strings.xml @@ -0,0 +1,24 @@ + + + + + "共享网络未连接到互联网" + "设备无法连接" + "关闭网络共享" + "热点或网络共享已开启" + "漫游时可能会产生额外的费用" + diff --git a/Tethering/res/values-mcc311-mnc480-zh-rHK/strings.xml b/Tethering/res/values-mcc311-mnc480-zh-rHK/strings.xml new file mode 100644 index 0000000000..f378a9dc2c --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-zh-rHK/strings.xml @@ -0,0 +1,24 @@ + + + + + "無法透過網絡共享連線至互聯網" + "裝置無法連接" + "關閉網絡共享" + "熱點或網絡共享已開啟" + "漫遊時可能需要支付額外費用" + diff --git a/Tethering/res/values-mcc311-mnc480-zh-rTW/strings.xml b/Tethering/res/values-mcc311-mnc480-zh-rTW/strings.xml new file mode 100644 index 0000000000..cd653df1da --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-zh-rTW/strings.xml @@ -0,0 +1,24 @@ + + + + + "無法透過網路共用連上網際網路" + "裝置無法連線" + "關閉網路共用" + "無線基地台或網路共用已開啟" + "使用漫遊服務可能須支付額外費用" + diff --git a/Tethering/res/values-mcc311-mnc480-zu/strings.xml b/Tethering/res/values-mcc311-mnc480-zu/strings.xml new file mode 100644 index 0000000000..32f6df56f1 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480-zu/strings.xml @@ -0,0 +1,24 @@ + + + + + "Ukusebenzisa ifoni njengemodemu akunayo i-inthanethi" + "Amadivayisi awakwazi ukuxhumeka" + "Vala ukusebenzisa ifoni njengemodemu" + "I-hotspot noma ukusebenzisa ifoni njengemodemu kuvuliwe" + "Kungaba nezinkokhelo ezengeziwe uma uzula" + diff --git a/Tethering/res/values-mcc311-mnc480/config.xml b/Tethering/res/values-mcc311-mnc480/config.xml new file mode 100644 index 0000000000..5c5be0466a --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480/config.xml @@ -0,0 +1,23 @@ + + + + + 5000 + + + true + \ No newline at end of file diff --git a/Tethering/res/values-mcc311-mnc480/strings.xml b/Tethering/res/values-mcc311-mnc480/strings.xml new file mode 100644 index 0000000000..ce9ff60807 --- /dev/null +++ b/Tethering/res/values-mcc311-mnc480/strings.xml @@ -0,0 +1,28 @@ + + + + + Tethering has no internet + + Devices can\u2019t connect + + Turn off tethering + + + Hotspot or tethering is on + + Additional charges may apply while roaming + diff --git a/Tethering/res/values-mk/strings.xml b/Tethering/res/values-mk/strings.xml new file mode 100644 index 0000000000..9ad9b9a589 --- /dev/null +++ b/Tethering/res/values-mk/strings.xml @@ -0,0 +1,29 @@ + + + + + "Активно е врзување или точка на пристап" + "Допрете за поставување." + "Врзувањето е оневозможено" + "Контактирајте со администраторот за детали" + "Статус на точката на пристап и врзувањето" + + + + + + diff --git a/Tethering/res/values-ml/strings.xml b/Tethering/res/values-ml/strings.xml new file mode 100644 index 0000000000..9db79ce220 --- /dev/null +++ b/Tethering/res/values-ml/strings.xml @@ -0,0 +1,29 @@ + + + + + "ടെതറിംഗ് അല്ലെങ്കിൽ ഹോട്ട്സ്‌പോട്ട് സജീവമാണ്" + "സജ്ജീകരിക്കാൻ ടാപ്പ് ചെയ്യുക." + "ടെതറിംഗ് പ്രവർത്തനരഹിതമാക്കിയിരിക്കുന്നു" + "വിശദാംശങ്ങൾക്ക് നിങ്ങളുടെ അഡ്മിനെ ബന്ധപ്പെടുക" + "ഹോട്ട്‌സ്പോട്ടിന്റെയും ടെതറിംഗിന്റെയും നില" + + + + + + diff --git a/Tethering/res/values-mn/strings.xml b/Tethering/res/values-mn/strings.xml new file mode 100644 index 0000000000..42d1edbace --- /dev/null +++ b/Tethering/res/values-mn/strings.xml @@ -0,0 +1,29 @@ + + + + + "Модем болгох эсвэл сүлжээний цэг идэвхтэй байна" + "Тохируулахын тулд товшино уу." + "Модем болгохыг идэвхгүй болгосон" + "Дэлгэрэнгүй мэдээлэл авахын тулд админтайгаа холбогдоно уу" + "Сүлжээний цэг болон модем болгох төлөв" + + + + + + diff --git a/Tethering/res/values-mr/strings.xml b/Tethering/res/values-mr/strings.xml new file mode 100644 index 0000000000..13995b6b8a --- /dev/null +++ b/Tethering/res/values-mr/strings.xml @@ -0,0 +1,29 @@ + + + + + "टेदरिंग किंवा हॉटस्पॉट अ‍ॅक्टिव्ह आहे" + "सेट करण्यासाठी टॅप करा." + "टेदरिंग बंद केले आहे" + "तपशीलांसाठी तुमच्या ॲडमिनशी संपर्क साधा" + "हॉटस्पॉट आणि टेदरिंगची स्थिती" + + + + + + diff --git a/Tethering/res/values-ms/strings.xml b/Tethering/res/values-ms/strings.xml new file mode 100644 index 0000000000..d6a67f37b1 --- /dev/null +++ b/Tethering/res/values-ms/strings.xml @@ -0,0 +1,29 @@ + + + + + "Penambatan atau tempat liputan aktif" + "Ketik untuk membuat persediaan." + "Penambatan dilumpuhkan" + "Hubungi pentadbir anda untuk mendapatkan maklumat lanjut" + "Status tempat liputan & penambatan" + + + + + + diff --git a/Tethering/res/values-my/strings.xml b/Tethering/res/values-my/strings.xml new file mode 100644 index 0000000000..49f6b88a75 --- /dev/null +++ b/Tethering/res/values-my/strings.xml @@ -0,0 +1,29 @@ + + + + + "မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း သို့မဟုတ် ဟော့စပေါ့ ဖွင့်ထားသည်" + "စနစ်ထည့်သွင်းရန် တို့ပါ။" + "မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်းကို ပိတ်ထားသည်" + "အသေးစိတ်အတွက် သင့်စီမံခန့်ခွဲသူကို ဆက်သွယ်ပါ" + "ဟော့စပေါ့နှင့် မိုဘိုင်းဖုန်းသုံး ချိတ်ဆက်မျှဝေခြင်း အခြေအနေ" + + + + + + diff --git a/Tethering/res/values-nb/strings.xml b/Tethering/res/values-nb/strings.xml new file mode 100644 index 0000000000..9594e0a70a --- /dev/null +++ b/Tethering/res/values-nb/strings.xml @@ -0,0 +1,29 @@ + + + + + "Internettdeling eller Wi-Fi-sone er aktiv" + "Trykk for å konfigurere." + "Internettdeling er slått av" + "Ta kontakt med administratoren din for å få mer informasjon" + "Status for Wi-Fi-sone og internettdeling" + + + + + + diff --git a/Tethering/res/values-ne/strings.xml b/Tethering/res/values-ne/strings.xml new file mode 100644 index 0000000000..72ae3a80a9 --- /dev/null +++ b/Tethering/res/values-ne/strings.xml @@ -0,0 +1,29 @@ + + + + + "टेदरिङ वा हटस्पट सक्रिय छ" + "सेटअप गर्न ट्याप गर्नुहोस्।" + "टेदरिङ सुविधा असक्षम पारिएको छ" + "विवरणहरूका लागि आफ्ना प्रशासकलाई सम्पर्क गर्नुहोस्" + "हटस्पट तथा टेदरिङको स्थिति" + + + + + + diff --git a/Tethering/res/values-nl/strings.xml b/Tethering/res/values-nl/strings.xml new file mode 100644 index 0000000000..18b2bbfc76 --- /dev/null +++ b/Tethering/res/values-nl/strings.xml @@ -0,0 +1,29 @@ + + + + + "Tethering of hotspot actief" + "Tik om in te stellen." + "Tethering is uitgeschakeld" + "Neem contact op met je beheerder voor meer informatie" + "Status van hotspot en tethering" + + + + + + diff --git a/Tethering/res/values-or/strings.xml b/Tethering/res/values-or/strings.xml new file mode 100644 index 0000000000..a15a6db42a --- /dev/null +++ b/Tethering/res/values-or/strings.xml @@ -0,0 +1,29 @@ + + + + + "ଟିଥେରିଂ କିମ୍ୱା ହଟସ୍ପଟ୍ ସକ୍ରିୟ ଅଛି" + "ସେଟ୍ ଅପ୍ କରିବାକୁ ଟାପ୍ କରନ୍ତୁ।" + "ଟିଥେରିଂ ଅକ୍ଷମ କରାଯାଇଛି" + "ବିବରଣୀଗୁଡ଼ିକ ପାଇଁ ଆପଣଙ୍କ ଆଡମିନଙ୍କ ସହ ଯୋଗାଯୋଗ କରନ୍ତୁ" + "ହଟସ୍ପଟ୍ ଓ ଟିଥେରିଂ ସ୍ଥିତି" + + + + + + diff --git a/Tethering/res/values-pa/strings.xml b/Tethering/res/values-pa/strings.xml new file mode 100644 index 0000000000..a8235e423e --- /dev/null +++ b/Tethering/res/values-pa/strings.xml @@ -0,0 +1,29 @@ + + + + + "ਟੈਦਰਿੰਗ ਜਾਂ ਹੌਟਸਪੌਟ ਕਿਰਿਆਸ਼ੀਲ" + "ਸੈੱਟਅੱਪ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ।" + "ਟੈਦਰਿੰਗ ਨੂੰ ਬੰਦ ਕੀਤਾ ਗਿਆ ਹੈ" + "ਵੇਰਵਿਆਂ ਲਈ ਆਪਣੇ ਪ੍ਰਸ਼ਾਸਕ ਨਾਲ ਸੰਪਰਕ ਕਰੋ" + "ਹੌਟਸਪੌਟ ਅਤੇ ਟੈਦਰਿੰਗ ਦੀ ਸਥਿਤੀ" + + + + + + diff --git a/Tethering/res/values-pl/strings.xml b/Tethering/res/values-pl/strings.xml new file mode 100644 index 0000000000..ccb017d43f --- /dev/null +++ b/Tethering/res/values-pl/strings.xml @@ -0,0 +1,29 @@ + + + + + "Aktywny tethering lub punkt dostępu" + "Kliknij, by skonfigurować" + "Tethering został wyłączony" + "Aby uzyskać szczegółowe informacje, skontaktuj się z administratorem" + "Hotspot i tethering – stan" + + + + + + diff --git a/Tethering/res/values-pt-rBR/strings.xml b/Tethering/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000000..a0a4745f93 --- /dev/null +++ b/Tethering/res/values-pt-rBR/strings.xml @@ -0,0 +1,29 @@ + + + + + "Ponto de acesso ou tethering ativo" + "Toque para configurar." + "Tethering desativado" + "Fale com seu administrador para saber detalhes" + "Status de ponto de acesso e tethering" + + + + + + diff --git a/Tethering/res/values-pt-rPT/strings.xml b/Tethering/res/values-pt-rPT/strings.xml new file mode 100644 index 0000000000..e3f03fcc69 --- /dev/null +++ b/Tethering/res/values-pt-rPT/strings.xml @@ -0,0 +1,29 @@ + + + + + "Ligação (à Internet) via telemóvel ou zona Wi-Fi ativas" + "Toque para configurar." + "A ligação (à Internet) via telemóvel está desativada." + "Contacte o administrador para obter detalhes." + "Estado da zona Wi-Fi e da ligação (à Internet) via telemóvel" + + + + + + diff --git a/Tethering/res/values-pt/strings.xml b/Tethering/res/values-pt/strings.xml new file mode 100644 index 0000000000..a0a4745f93 --- /dev/null +++ b/Tethering/res/values-pt/strings.xml @@ -0,0 +1,29 @@ + + + + + "Ponto de acesso ou tethering ativo" + "Toque para configurar." + "Tethering desativado" + "Fale com seu administrador para saber detalhes" + "Status de ponto de acesso e tethering" + + + + + + diff --git a/Tethering/res/values-ro/strings.xml b/Tethering/res/values-ro/strings.xml new file mode 100644 index 0000000000..5706a4a69c --- /dev/null +++ b/Tethering/res/values-ro/strings.xml @@ -0,0 +1,29 @@ + + + + + "Tethering sau hotspot activ" + "Atingeți ca să configurați." + "Tetheringul este dezactivat" + "Contactați administratorul pentru detalii" + "Starea hotspotului și a tetheringului" + + + + + + diff --git a/Tethering/res/values-ru/strings.xml b/Tethering/res/values-ru/strings.xml new file mode 100644 index 0000000000..7cb6f7db3f --- /dev/null +++ b/Tethering/res/values-ru/strings.xml @@ -0,0 +1,29 @@ + + + + + "Включен режим модема или точка доступа" + "Нажмите, чтобы настроить." + "Использование телефона в качестве модема запрещено" + "Чтобы узнать подробности, обратитесь к администратору." + "Статус хот-спота и режима модема" + + + + + + diff --git a/Tethering/res/values-si/strings.xml b/Tethering/res/values-si/strings.xml new file mode 100644 index 0000000000..ec34c22de7 --- /dev/null +++ b/Tethering/res/values-si/strings.xml @@ -0,0 +1,29 @@ + + + + + "ටෙදරින් හෝ හොට්ස්පොට් සක්‍රීයයි" + "පිහිටුවීමට තට්ටු කරන්න." + "ටෙදරින් අබල කර ඇත" + "විස්තර සඳහා ඔබගේ පරිපාලක අමතන්න" + "හොට්ස්පොට් & ටෙදරින් තත්ත්වය" + + + + + + diff --git a/Tethering/res/values-sk/strings.xml b/Tethering/res/values-sk/strings.xml new file mode 100644 index 0000000000..43e787c84f --- /dev/null +++ b/Tethering/res/values-sk/strings.xml @@ -0,0 +1,29 @@ + + + + + "Tethering alebo prístupový bod je aktívny" + "Klepnutím prejdete na nastavenie." + "Tethering je deaktivovaný" + "O podrobnosti požiadajte svojho správcu" + "Stav hotspotu a tetheringu" + + + + + + diff --git a/Tethering/res/values-sl/strings.xml b/Tethering/res/values-sl/strings.xml new file mode 100644 index 0000000000..59433626a1 --- /dev/null +++ b/Tethering/res/values-sl/strings.xml @@ -0,0 +1,29 @@ + + + + + "Povezava z internetom prek mobilnega telefona ali dostopna točka je aktivna" + "Dotaknite se, če želite nastaviti." + "Povezava z internetom prek mobilnega telefona je onemogočena" + "Za podrobnosti se obrnite na skrbnika" + "Stanje dostopne točke in povezave z internetom prek mobilnega telefona" + + + + + + diff --git a/Tethering/res/values-sq/strings.xml b/Tethering/res/values-sq/strings.xml new file mode 100644 index 0000000000..21e11558bb --- /dev/null +++ b/Tethering/res/values-sq/strings.xml @@ -0,0 +1,29 @@ + + + + + "Ndarja e internetit ose zona e qasjes së internetit është aktive" + "Trokit për ta konfiguruar." + "Ndarja e internetit është çaktivizuar" + "Kontakto me administratorin për detaje" + "Statusi i zonës së qasjes dhe ndarjes së internetit" + + + + + + diff --git a/Tethering/res/values-sr/strings.xml b/Tethering/res/values-sr/strings.xml new file mode 100644 index 0000000000..e2e4dc6361 --- /dev/null +++ b/Tethering/res/values-sr/strings.xml @@ -0,0 +1,29 @@ + + + + + "Привезивање или хотспот је активан" + "Додирните да бисте подесили." + "Привезивање је онемогућено" + "Потражите детаље од администратора" + "Статус хотспота и привезивања" + + + + + + diff --git a/Tethering/res/values-sv/strings.xml b/Tethering/res/values-sv/strings.xml new file mode 100644 index 0000000000..72702c2858 --- /dev/null +++ b/Tethering/res/values-sv/strings.xml @@ -0,0 +1,29 @@ + + + + + "Internetdelning eller surfzon har aktiverats" + "Tryck om du vill konfigurera." + "Internetdelning har inaktiverats" + "Kontakta administratören om du vill veta mer" + "Trådlös surfzon och internetdelning har inaktiverats" + + + + + + diff --git a/Tethering/res/values-sw/strings.xml b/Tethering/res/values-sw/strings.xml new file mode 100644 index 0000000000..65e4aa8ceb --- /dev/null +++ b/Tethering/res/values-sw/strings.xml @@ -0,0 +1,29 @@ + + + + + "Kusambaza mtandao au mtandaopepe umewashwa" + "Gusa ili uweke mipangilio." + "Umezima kipengele cha kusambaza mtandao" + "Wasiliana na msimamizi wako ili upate maelezo zaidi" + "Mtandaopepe na hali ya kusambaza mtandao" + + + + + + diff --git a/Tethering/res/values-ta/strings.xml b/Tethering/res/values-ta/strings.xml new file mode 100644 index 0000000000..4aba62d4ab --- /dev/null +++ b/Tethering/res/values-ta/strings.xml @@ -0,0 +1,29 @@ + + + + + "டெதெரிங் அல்லது ஹாட்ஸ்பாட் இயங்குகிறது" + "அமைக்க, தட்டவும்." + "டெதெரிங் முடக்கப்பட்டுள்ளது" + "விவரங்களுக்கு உங்கள் நிர்வாகியைத் தொடர்புகொள்ளவும்" + "ஹாட்ஸ்பாட் & டெதெரிங் நிலை" + + + + + + diff --git a/Tethering/res/values-te/strings.xml b/Tethering/res/values-te/strings.xml new file mode 100644 index 0000000000..1f91791341 --- /dev/null +++ b/Tethering/res/values-te/strings.xml @@ -0,0 +1,29 @@ + + + + + "టెథరింగ్ లేదా హాట్‌స్పాట్ యాక్టివ్‌గా ఉంది" + "సెటప్ చేయడానికి ట్యాప్ చేయండి." + "టెథరింగ్ డిజేబుల్ చేయబడింది" + "వివరాల కోసం మీ అడ్మిన్‌ని సంప్రదించండి" + "హాట్‌స్పాట్ & టెథరింగ్ స్థితి" + + + + + + diff --git a/Tethering/res/values-th/strings.xml b/Tethering/res/values-th/strings.xml new file mode 100644 index 0000000000..44171c0db8 --- /dev/null +++ b/Tethering/res/values-th/strings.xml @@ -0,0 +1,29 @@ + + + + + "การเชื่อมต่ออินเทอร์เน็ตผ่านมือถือหรือฮอตสปอตทำงานอยู่" + "แตะเพื่อตั้งค่า" + "ปิดใช้การเชื่อมต่ออินเทอร์เน็ตผ่านมือถือแล้ว" + "ติดต่อผู้ดูแลระบบเพื่อขอรายละเอียด" + "สถานะฮอตสปอตและการเชื่อมต่ออินเทอร์เน็ตผ่านมือถือ" + + + + + + diff --git a/Tethering/res/values-tl/strings.xml b/Tethering/res/values-tl/strings.xml new file mode 100644 index 0000000000..7347dd3e62 --- /dev/null +++ b/Tethering/res/values-tl/strings.xml @@ -0,0 +1,29 @@ + + + + + "Aktibo ang pag-tether o hotspot" + "I-tap para i-set up." + "Naka-disable ang pag-tether" + "Makipag-ugnayan sa iyong admin para sa mga detalye" + "Status ng hotspot at pag-tether" + + + + + + diff --git a/Tethering/res/values-tr/strings.xml b/Tethering/res/values-tr/strings.xml new file mode 100644 index 0000000000..32030f1765 --- /dev/null +++ b/Tethering/res/values-tr/strings.xml @@ -0,0 +1,29 @@ + + + + + "Tethering veya hotspot etkin" + "Ayarlamak için dokunun." + "Tethering devre dışı bırakıldı" + "Ayrıntılı bilgi için yöneticinize başvurun" + "Hotspot ve tethering durumu" + + + + + + diff --git a/Tethering/res/values-uk/strings.xml b/Tethering/res/values-uk/strings.xml new file mode 100644 index 0000000000..1ca89b3f78 --- /dev/null +++ b/Tethering/res/values-uk/strings.xml @@ -0,0 +1,29 @@ + + + + + "Модем чи точка доступу активні" + "Натисніть, щоб налаштувати." + "Використання телефона як модема вимкнено" + "Щоб дізнатися більше, зв\'яжіться з адміністратором" + "Статус точки доступу та модема" + + + + + + diff --git a/Tethering/res/values-ur/strings.xml b/Tethering/res/values-ur/strings.xml new file mode 100644 index 0000000000..d72c7d4195 --- /dev/null +++ b/Tethering/res/values-ur/strings.xml @@ -0,0 +1,29 @@ + + + + + "ٹیدرنگ یا ہاٹ اسپاٹ فعال" + "سیٹ اپ کرنے کیلئے تھپتھپائیں۔" + "ٹیدرنگ غیر فعال ہے" + "تفصیلات کے لئے اپنے منتظم سے رابطہ کریں" + "ہاٹ اسپاٹ اور ٹیتھرنگ کا اسٹیٹس" + + + + + + diff --git a/Tethering/res/values-uz/strings.xml b/Tethering/res/values-uz/strings.xml new file mode 100644 index 0000000000..af3b2ebb35 --- /dev/null +++ b/Tethering/res/values-uz/strings.xml @@ -0,0 +1,29 @@ + + + + + "Modem rejimi yoki hotspot yoniq" + "Sozlash uchun bosing." + "Modem rejimi faolsizlantirildi" + "Tafsilotlari uchun administratoringizga murojaat qiling" + "Hotspot va modem rejimi holati" + + + + + + diff --git a/Tethering/res/values-vi/strings.xml b/Tethering/res/values-vi/strings.xml new file mode 100644 index 0000000000..21a0735922 --- /dev/null +++ b/Tethering/res/values-vi/strings.xml @@ -0,0 +1,29 @@ + + + + + "Tính năng chia sẻ Internet hoặc điểm phát sóng đang hoạt động" + "Hãy nhấn để thiết lập." + "Đã tắt tính năng chia sẻ Internet" + "Hãy liên hệ với quản trị viên của bạn để biết chi tiết" + "Trạng thái điểm phát sóng và chia sẻ Internet" + + + + + + diff --git a/Tethering/res/values-zh-rCN/strings.xml b/Tethering/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000000..98e3b4b46f --- /dev/null +++ b/Tethering/res/values-zh-rCN/strings.xml @@ -0,0 +1,29 @@ + + + + + "网络共享或热点已启用" + "点按即可设置。" + "网络共享已停用" + "如需了解详情,请与您的管理员联系" + "热点和网络共享状态" + + + + + + diff --git a/Tethering/res/values-zh-rHK/strings.xml b/Tethering/res/values-zh-rHK/strings.xml new file mode 100644 index 0000000000..9cafd42dd4 --- /dev/null +++ b/Tethering/res/values-zh-rHK/strings.xml @@ -0,0 +1,29 @@ + + + + + "網絡共享或熱點已啟用" + "輕按即可設定。" + "網絡共享已停用" + "請聯絡您的管理員以瞭解詳情" + "熱點和網絡共享狀態" + + + + + + diff --git a/Tethering/res/values-zh-rTW/strings.xml b/Tethering/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000000..50a50bf7a9 --- /dev/null +++ b/Tethering/res/values-zh-rTW/strings.xml @@ -0,0 +1,29 @@ + + + + + "網路共用或無線基地台已啟用" + "輕觸即可進行設定。" + "網路共用已停用" + "詳情請洽你的管理員" + "無線基地台與網路共用狀態" + + + + + + diff --git a/Tethering/res/values-zu/strings.xml b/Tethering/res/values-zu/strings.xml new file mode 100644 index 0000000000..f210f8726e --- /dev/null +++ b/Tethering/res/values-zu/strings.xml @@ -0,0 +1,29 @@ + + + + + "Ukusebenzisa njengemodemu noma i-hotspot ephathekayo kuvuliwe" + "Thepha ukuze usethe." + "Ukusebenzisa ifoni njengemodemu kukhutshaziwe" + "Xhumana nomphathi wakho ukuze uthole imininingwane" + "I-Hotspot nesimo sokusebenzisa ifoni njengemodemu" + + + + + + diff --git a/Tethering/res/values/config.xml b/Tethering/res/values/config.xml new file mode 100644 index 0000000000..4391006741 --- /dev/null +++ b/Tethering/res/values/config.xml @@ -0,0 +1,196 @@ + + + + + + + "usb\\d" + "rndis\\d" + + + + + + + + + "wlan\\d" + "softap\\d" + "ap_br_wlan\\d" + "ap_br_softap\\d" + + + + + "wigig\\d" + + + + + "p2p-p2p\\d-.*" + "p2p\\d" + + + + + "bt-pan" + + + + true + + + false + + + false + + + + + + + 5000 + + + + + + + true + + + + + + + + + + + + + + + + 24 + + + com.android.settings/.wifi.tether.TetherService + + + + -1 + + + + false + diff --git a/Tethering/res/values/overlayable.xml b/Tethering/res/values/overlayable.xml new file mode 100644 index 0000000000..0ee7a992ee --- /dev/null +++ b/Tethering/res/values/overlayable.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tethering/res/values/strings.xml b/Tethering/res/values/strings.xml new file mode 100644 index 0000000000..d63c7c5063 --- /dev/null +++ b/Tethering/res/values/strings.xml @@ -0,0 +1,47 @@ + + + + + + Tethering or hotspot active + + Tap to set up. + + + + Tethering is disabled + + Contact your admin for details + + + + Hotspot & tethering status + + + + + + + + + + + + + diff --git a/Tethering/src/android/net/dhcp/DhcpServerCallbacks.java b/Tethering/src/android/net/dhcp/DhcpServerCallbacks.java new file mode 100644 index 0000000000..9fda1257b4 --- /dev/null +++ b/Tethering/src/android/net/dhcp/DhcpServerCallbacks.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.dhcp; + +/** + * Convenience wrapper around IDhcpServerCallbacks.Stub that implements getInterfaceVersion(). + * @hide + */ +public abstract class DhcpServerCallbacks extends IDhcpServerCallbacks.Stub { + /** + * Get the version of the aidl interface implemented by the callbacks. + */ + @Override + public int getInterfaceVersion() { + return IDhcpServerCallbacks.VERSION; + } + + @Override + public String getInterfaceHash() { + return IDhcpServerCallbacks.HASH; + } +} diff --git a/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java b/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java new file mode 100644 index 0000000000..aaaec17bf9 --- /dev/null +++ b/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.dhcp; + +import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH; + +import android.net.LinkAddress; +import android.util.ArraySet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.net.Inet4Address; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +/** + * Subclass of {@link DhcpServingParamsParcel} with additional utility methods for building. + * + *

This utility class does not check for validity of the parameters: invalid parameters are + * reported by the receiving module when unparceling the parcel. + * + * @see DhcpServingParams + * @hide + */ +public class DhcpServingParamsParcelExt extends DhcpServingParamsParcel { + public static final int MTU_UNSET = 0; + + /** + * Set the server address and served prefix for the DHCP server. + * + *

This parameter is required. + */ + public DhcpServingParamsParcelExt setServerAddr(@NonNull LinkAddress serverAddr) { + this.serverAddr = inet4AddressToIntHTH((Inet4Address) serverAddr.getAddress()); + this.serverAddrPrefixLength = serverAddr.getPrefixLength(); + return this; + } + + /** + * Set the default routers to be advertised to DHCP clients. + * + *

Each router must be inside the served prefix. This may be an empty set, but it must + * always be set explicitly. + */ + public DhcpServingParamsParcelExt setDefaultRouters(@NonNull Set defaultRouters) { + this.defaultRouters = toIntArray(defaultRouters); + return this; + } + + /** + * Set the default routers to be advertised to DHCP clients. + * + *

Each router must be inside the served prefix. This may be an empty list of routers, + * but it must always be set explicitly. + */ + public DhcpServingParamsParcelExt setDefaultRouters(@NonNull Inet4Address... defaultRouters) { + return setDefaultRouters(newArraySet(defaultRouters)); + } + + /** + * Convenience method to build the parameters with no default router. + * + *

Equivalent to calling {@link #setDefaultRouters(Inet4Address...)} with no address. + */ + public DhcpServingParamsParcelExt setNoDefaultRouter() { + return setDefaultRouters(); + } + + /** + * Set the DNS servers to be advertised to DHCP clients. + * + *

This may be an empty set, but it must always be set explicitly. + */ + public DhcpServingParamsParcelExt setDnsServers(@NonNull Set dnsServers) { + this.dnsServers = toIntArray(dnsServers); + return this; + } + + /** + * Set the DNS servers to be advertised to DHCP clients. + * + *

This may be an empty list of servers, but it must always be set explicitly. + */ + public DhcpServingParamsParcelExt setDnsServers(@NonNull Inet4Address... dnsServers) { + return setDnsServers(newArraySet(dnsServers)); + } + + /** + * Convenience method to build the parameters with no DNS server. + * + *

Equivalent to calling {@link #setDnsServers(Inet4Address...)} with no address. + */ + public DhcpServingParamsParcelExt setNoDnsServer() { + return setDnsServers(); + } + + /** + * Set excluded addresses that the DHCP server is not allowed to assign to clients. + * + *

This parameter is optional. DNS servers and default routers are always excluded + * and do not need to be set here. + */ + public DhcpServingParamsParcelExt setExcludedAddrs(@NonNull Set excludedAddrs) { + this.excludedAddrs = toIntArray(excludedAddrs); + return this; + } + + /** + * Set excluded addresses that the DHCP server is not allowed to assign to clients. + * + *

This parameter is optional. DNS servers and default routers are always excluded + * and do not need to be set here. + */ + public DhcpServingParamsParcelExt setExcludedAddrs(@NonNull Inet4Address... excludedAddrs) { + return setExcludedAddrs(newArraySet(excludedAddrs)); + } + + /** + * Set the lease time for leases assigned by the DHCP server. + * + *

This parameter is required. + */ + public DhcpServingParamsParcelExt setDhcpLeaseTimeSecs(long dhcpLeaseTimeSecs) { + this.dhcpLeaseTimeSecs = dhcpLeaseTimeSecs; + return this; + } + + /** + * Set the link MTU to be advertised to DHCP clients. + * + *

If set to {@link #MTU_UNSET}, no MTU will be advertised to clients. This parameter + * is optional and defaults to {@link #MTU_UNSET}. + */ + public DhcpServingParamsParcelExt setLinkMtu(int linkMtu) { + this.linkMtu = linkMtu; + return this; + } + + /** + * Set whether the DHCP server should send the ANDROID_METERED vendor-specific option. + * + *

If not set, the default value is false. + */ + public DhcpServingParamsParcelExt setMetered(boolean metered) { + this.metered = metered; + return this; + } + + /** + * Set the client address to tell DHCP server only offer this address. + * The client's prefix length is the same as server's. + * + *

If not set, the default value is null. + */ + public DhcpServingParamsParcelExt setSingleClientAddr(@Nullable Inet4Address clientAddr) { + this.singleClientAddr = clientAddr == null ? 0 : inet4AddressToIntHTH(clientAddr); + return this; + } + + /** + * Set whether the DHCP server should request a new prefix from IpServer when receiving + * DHCPDECLINE message in certain particular link (e.g. there is only one downstream USB + * tethering client). If it's false, process DHCPDECLINE message as RFC2131#4.3.3 suggests. + * + *

If not set, the default value is false. + */ + public DhcpServingParamsParcelExt setChangePrefixOnDecline(boolean changePrefixOnDecline) { + this.changePrefixOnDecline = changePrefixOnDecline; + return this; + } + + private static int[] toIntArray(@NonNull Collection addrs) { + int[] res = new int[addrs.size()]; + int i = 0; + for (Inet4Address addr : addrs) { + res[i] = inet4AddressToIntHTH(addr); + i++; + } + return res; + } + + private static ArraySet newArraySet(Inet4Address... addrs) { + ArraySet addrSet = new ArraySet<>(addrs.length); + Collections.addAll(addrSet, addrs); + return addrSet; + } +} diff --git a/Tethering/src/android/net/ip/DadProxy.java b/Tethering/src/android/net/ip/DadProxy.java new file mode 100644 index 0000000000..e2976b7890 --- /dev/null +++ b/Tethering/src/android/net/ip/DadProxy.java @@ -0,0 +1,54 @@ +/* + * 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.ip; + +import android.net.util.InterfaceParams; +import android.os.Handler; + +import androidx.annotation.VisibleForTesting; + +/** + * Basic Duplicate address detection proxy. + * + * @hide + */ +public class DadProxy { + private static final String TAG = DadProxy.class.getSimpleName(); + + @VisibleForTesting + public static NeighborPacketForwarder naForwarder; + public static NeighborPacketForwarder nsForwarder; + + public DadProxy(Handler h, InterfaceParams tetheredIface) { + naForwarder = new NeighborPacketForwarder(h, tetheredIface, + NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT); + nsForwarder = new NeighborPacketForwarder(h, tetheredIface, + NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION); + } + + /** Stop NS/NA Forwarders. */ + public void stop() { + naForwarder.stop(); + nsForwarder.stop(); + } + + /** Set upstream iface on both forwarders. */ + public void setUpstreamIface(InterfaceParams upstreamIface) { + naForwarder.setUpstreamIface(upstreamIface); + nsForwarder.setUpstreamIface(upstreamIface); + } +} diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java new file mode 100644 index 0000000000..c45ce830e2 --- /dev/null +++ b/Tethering/src/android/net/ip/IpServer.java @@ -0,0 +1,1469 @@ +/* + * 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.ip; + +import static android.net.RouteInfo.RTN_UNICAST; +import static android.net.TetheringManager.TetheringRequest.checkStaticAddressConfiguration; +import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS; +import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH; +import static android.net.util.NetworkConstants.asByte; +import static android.net.util.PrefixUtils.asIpPrefix; +import static android.net.util.TetheringMessageBase.BASE_IPSERVER; +import static android.system.OsConstants.RT_SCOPE_UNIVERSE; + +import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH; + +import android.net.INetd; +import android.net.INetworkStackStatusCallback; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.MacAddress; +import android.net.RouteInfo; +import android.net.TetheredClient; +import android.net.TetheringManager; +import android.net.TetheringRequestParcel; +import android.net.dhcp.DhcpLeaseParcelable; +import android.net.dhcp.DhcpServerCallbacks; +import android.net.dhcp.DhcpServingParamsParcel; +import android.net.dhcp.DhcpServingParamsParcelExt; +import android.net.dhcp.IDhcpEventCallbacks; +import android.net.dhcp.IDhcpServer; +import android.net.ip.IpNeighborMonitor.NeighborEvent; +import android.net.ip.RouterAdvertisementDaemon.RaParams; +import android.net.shared.NetdUtils; +import android.net.shared.RouteUtils; +import android.net.util.InterfaceParams; +import android.net.util.InterfaceSet; +import android.net.util.PrefixUtils; +import android.net.util.SharedLog; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.util.Log; +import android.util.SparseArray; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.util.MessageUtils; +import com.android.internal.util.State; +import com.android.internal.util.StateMachine; +import com.android.networkstack.tethering.BpfCoordinator; +import com.android.networkstack.tethering.BpfCoordinator.ClientInfo; +import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule; +import com.android.networkstack.tethering.PrivateAddressCoordinator; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.Set; + +/** + * Provides the interface to IP-layer serving functionality for a given network + * interface, e.g. for tethering or "local-only hotspot" mode. + * + * @hide + */ +public class IpServer extends StateMachine { + public static final int STATE_UNAVAILABLE = 0; + public static final int STATE_AVAILABLE = 1; + public static final int STATE_TETHERED = 2; + public static final int STATE_LOCAL_ONLY = 3; + + /** Get string name of |state|.*/ + public static String getStateString(int state) { + switch (state) { + case STATE_UNAVAILABLE: return "UNAVAILABLE"; + case STATE_AVAILABLE: return "AVAILABLE"; + case STATE_TETHERED: return "TETHERED"; + case STATE_LOCAL_ONLY: return "LOCAL_ONLY"; + } + return "UNKNOWN: " + state; + } + + private static final byte DOUG_ADAMS = (byte) 42; + + // TODO: have PanService use some visible version of this constant + private static final String BLUETOOTH_IFACE_ADDR = "192.168.44.1/24"; + + // TODO: have this configurable + private static final int DHCP_LEASE_TIME_SECS = 3600; + + private static final MacAddress NULL_MAC_ADDRESS = MacAddress.fromString("00:00:00:00:00:00"); + + private static final String TAG = "IpServer"; + private static final boolean DBG = false; + private static final boolean VDBG = false; + private static final Class[] sMessageClasses = { + IpServer.class + }; + private static final SparseArray sMagicDecoderRing = + MessageUtils.findMessageNames(sMessageClasses); + + /** IpServer callback. */ + public static class Callback { + /** + * Notify that |who| has changed its tethering state. + * + * @param who the calling instance of IpServer + * @param state one of STATE_* + * @param lastError one of TetheringManager.TETHER_ERROR_* + */ + public void updateInterfaceState(IpServer who, int state, int lastError) { } + + /** + * Notify that |who| has new LinkProperties. + * + * @param who the calling instance of IpServer + * @param newLp the new LinkProperties to report + */ + public void updateLinkProperties(IpServer who, LinkProperties newLp) { } + + /** + * Notify that the DHCP leases changed in one of the IpServers. + */ + public void dhcpLeasesChanged() { } + + /** + * Request Tethering change. + * + * @param tetheringType the downstream type of this IpServer. + * @param enabled enable or disable tethering. + */ + public void requestEnableTethering(int tetheringType, boolean enabled) { } + } + + /** Capture IpServer dependencies, for injection. */ + public abstract static class Dependencies { + /** + * Create a DadProxy instance to be used by IpServer. + * To support multiple tethered interfaces concurrently DAD Proxy + * needs to be supported per IpServer instead of per upstream. + */ + public DadProxy getDadProxy(Handler handler, InterfaceParams ifParams) { + return new DadProxy(handler, ifParams); + } + + /** Create an IpNeighborMonitor to be used by this IpServer */ + public IpNeighborMonitor getIpNeighborMonitor(Handler handler, SharedLog log, + IpNeighborMonitor.NeighborEventConsumer consumer) { + return new IpNeighborMonitor(handler, log, consumer); + } + + /** Create a RouterAdvertisementDaemon instance to be used by IpServer.*/ + public RouterAdvertisementDaemon getRouterAdvertisementDaemon(InterfaceParams ifParams) { + return new RouterAdvertisementDaemon(ifParams); + } + + /** Get |ifName|'s interface information.*/ + public InterfaceParams getInterfaceParams(String ifName) { + return InterfaceParams.getByName(ifName); + } + + /** Create a DhcpServer instance to be used by IpServer. */ + public abstract void makeDhcpServer(String ifName, DhcpServingParamsParcel params, + DhcpServerCallbacks cb); + } + + // request from the user that it wants to tether + public static final int CMD_TETHER_REQUESTED = BASE_IPSERVER + 1; + // request from the user that it wants to untether + public static final int CMD_TETHER_UNREQUESTED = BASE_IPSERVER + 2; + // notification that this interface is down + public static final int CMD_INTERFACE_DOWN = BASE_IPSERVER + 3; + // notification from the {@link Tethering.TetherMainSM} that it had trouble enabling IP + // Forwarding + public static final int CMD_IP_FORWARDING_ENABLE_ERROR = BASE_IPSERVER + 4; + // notification from the {@link Tethering.TetherMainSM} SM that it had trouble disabling IP + // Forwarding + public static final int CMD_IP_FORWARDING_DISABLE_ERROR = BASE_IPSERVER + 5; + // notification from the {@link Tethering.TetherMainSM} SM that it had trouble starting + // tethering + public static final int CMD_START_TETHERING_ERROR = BASE_IPSERVER + 6; + // notification from the {@link Tethering.TetherMainSM} that it had trouble stopping tethering + public static final int CMD_STOP_TETHERING_ERROR = BASE_IPSERVER + 7; + // notification from the {@link Tethering.TetherMainSM} that it had trouble setting the DNS + // forwarders + public static final int CMD_SET_DNS_FORWARDERS_ERROR = BASE_IPSERVER + 8; + // the upstream connection has changed + public static final int CMD_TETHER_CONNECTION_CHANGED = BASE_IPSERVER + 9; + // new IPv6 tethering parameters need to be processed + public static final int CMD_IPV6_TETHER_UPDATE = BASE_IPSERVER + 10; + // new neighbor cache entry on our interface + public static final int CMD_NEIGHBOR_EVENT = BASE_IPSERVER + 11; + // request from DHCP server that it wants to have a new prefix + public static final int CMD_NEW_PREFIX_REQUEST = BASE_IPSERVER + 12; + // request from PrivateAddressCoordinator to restart tethering. + public static final int CMD_NOTIFY_PREFIX_CONFLICT = BASE_IPSERVER + 13; + + private final State mInitialState; + private final State mLocalHotspotState; + private final State mTetheredState; + private final State mUnavailableState; + private final State mWaitingForRestartState; + + private final SharedLog mLog; + private final INetd mNetd; + @NonNull + private final BpfCoordinator mBpfCoordinator; + private final Callback mCallback; + private final InterfaceController mInterfaceCtrl; + private final PrivateAddressCoordinator mPrivateAddressCoordinator; + + private final String mIfaceName; + private final int mInterfaceType; + private final LinkProperties mLinkProperties; + private final boolean mUsingLegacyDhcp; + private final boolean mUsingBpfOffload; + + private final Dependencies mDeps; + + private int mLastError; + private int mServingMode; + private InterfaceSet mUpstreamIfaceSet; // may change over time + private InterfaceParams mInterfaceParams; + // TODO: De-duplicate this with mLinkProperties above. Currently, these link + // properties are those selected by the IPv6TetheringCoordinator and relayed + // to us. By comparison, mLinkProperties contains the addresses and directly + // connected routes that have been formed from these properties iff. we have + // succeeded in configuring them and are able to announce them within Router + // Advertisements (otherwise, we do not add them to mLinkProperties at all). + private LinkProperties mLastIPv6LinkProperties; + private RouterAdvertisementDaemon mRaDaemon; + private DadProxy mDadProxy; + + // To be accessed only on the handler thread + private int mDhcpServerStartIndex = 0; + private IDhcpServer mDhcpServer; + private RaParams mLastRaParams; + + private LinkAddress mStaticIpv4ServerAddr; + private LinkAddress mStaticIpv4ClientAddr; + + @NonNull + private List mDhcpLeases = Collections.emptyList(); + + private int mLastIPv6UpstreamIfindex = 0; + + private class MyNeighborEventConsumer implements IpNeighborMonitor.NeighborEventConsumer { + public void accept(NeighborEvent e) { + sendMessage(CMD_NEIGHBOR_EVENT, e); + } + } + + private final IpNeighborMonitor mIpNeighborMonitor; + + private LinkAddress mIpv4Address; + + // TODO: Add a dependency object to pass the data members or variables from the tethering + // object. It helps to reduce the arguments of the constructor. + public IpServer( + String ifaceName, Looper looper, int interfaceType, SharedLog log, + INetd netd, @NonNull BpfCoordinator coordinator, Callback callback, + boolean usingLegacyDhcp, boolean usingBpfOffload, + PrivateAddressCoordinator addressCoordinator, Dependencies deps) { + super(ifaceName, looper); + mLog = log.forSubComponent(ifaceName); + mNetd = netd; + mBpfCoordinator = coordinator; + mCallback = callback; + mInterfaceCtrl = new InterfaceController(ifaceName, mNetd, mLog); + mIfaceName = ifaceName; + mInterfaceType = interfaceType; + mLinkProperties = new LinkProperties(); + mUsingLegacyDhcp = usingLegacyDhcp; + mUsingBpfOffload = usingBpfOffload; + mPrivateAddressCoordinator = addressCoordinator; + mDeps = deps; + resetLinkProperties(); + mLastError = TetheringManager.TETHER_ERROR_NO_ERROR; + mServingMode = STATE_AVAILABLE; + + mIpNeighborMonitor = mDeps.getIpNeighborMonitor(getHandler(), mLog, + new MyNeighborEventConsumer()); + + // IP neighbor monitor monitors the neighbor events for adding/removing offload + // forwarding rules per client. If BPF offload is not supported, don't start listening + // for neighbor events. See updateIpv6ForwardingRules, addIpv6ForwardingRule, + // removeIpv6ForwardingRule. + if (mUsingBpfOffload && !mIpNeighborMonitor.start()) { + mLog.e("Failed to create IpNeighborMonitor on " + mIfaceName); + } + + mInitialState = new InitialState(); + mLocalHotspotState = new LocalHotspotState(); + mTetheredState = new TetheredState(); + mUnavailableState = new UnavailableState(); + mWaitingForRestartState = new WaitingForRestartState(); + addState(mInitialState); + addState(mLocalHotspotState); + addState(mTetheredState); + addState(mWaitingForRestartState, mTetheredState); + addState(mUnavailableState); + + setInitialState(mInitialState); + } + + /** Interface name which IpServer served.*/ + public String interfaceName() { + return mIfaceName; + } + + /** + * Tethering downstream type. It would be one of TetheringManager#TETHERING_*. + */ + public int interfaceType() { + return mInterfaceType; + } + + /** Last error from this IpServer. */ + public int lastError() { + return mLastError; + } + + /** Serving mode is the current state of IpServer state machine. */ + public int servingMode() { + return mServingMode; + } + + /** The properties of the network link which IpServer is serving. */ + public LinkProperties linkProperties() { + return new LinkProperties(mLinkProperties); + } + + /** The address which IpServer is using. */ + public LinkAddress getAddress() { + return mIpv4Address; + } + + /** + * Get the latest list of DHCP leases that was reported. Must be called on the IpServer looper + * thread. + */ + public List getAllLeases() { + return Collections.unmodifiableList(mDhcpLeases); + } + + /** Stop this IpServer. After this is called this IpServer should not be used any more. */ + public void stop() { + sendMessage(CMD_INTERFACE_DOWN); + } + + /** + * Tethering is canceled. IpServer state machine will be available and wait for + * next tethering request. + */ + public void unwanted() { + sendMessage(CMD_TETHER_UNREQUESTED); + } + + /** Internals. */ + + private boolean startIPv4() { + return configureIPv4(true); + } + + /** + * Convenience wrapper around INetworkStackStatusCallback to run callbacks on the IpServer + * handler. + * + *

Different instances of this class can be created for each call to IDhcpServer methods, + * with different implementations of the callback, to differentiate handling of success/error in + * each call. + */ + private abstract class OnHandlerStatusCallback extends INetworkStackStatusCallback.Stub { + @Override + public void onStatusAvailable(int statusCode) { + getHandler().post(() -> callback(statusCode)); + } + + public abstract void callback(int statusCode); + + @Override + public int getInterfaceVersion() { + return this.VERSION; + } + + @Override + public String getInterfaceHash() { + return this.HASH; + } + } + + private class DhcpServerCallbacksImpl extends DhcpServerCallbacks { + private final int mStartIndex; + + private DhcpServerCallbacksImpl(int startIndex) { + mStartIndex = startIndex; + } + + @Override + public void onDhcpServerCreated(int statusCode, IDhcpServer server) throws RemoteException { + getHandler().post(() -> { + // We are on the handler thread: mDhcpServerStartIndex can be read safely. + if (mStartIndex != mDhcpServerStartIndex) { + // This start request is obsolete. Explicitly stop the DHCP server to shut + // down its thread. When the |server| binder token goes out of scope, the + // garbage collector will finalize it, which causes the network stack process + // garbage collector to collect the server itself. + try { + server.stop(null); + } catch (RemoteException e) { } + return; + } + + if (statusCode != STATUS_SUCCESS) { + mLog.e("Error obtaining DHCP server: " + statusCode); + handleError(); + return; + } + + mDhcpServer = server; + try { + mDhcpServer.startWithCallbacks(new OnHandlerStatusCallback() { + @Override + public void callback(int startStatusCode) { + if (startStatusCode != STATUS_SUCCESS) { + mLog.e("Error starting DHCP server: " + startStatusCode); + handleError(); + } + } + }, new DhcpEventCallback()); + } catch (RemoteException e) { + throw new IllegalStateException(e); + } + }); + } + + private void handleError() { + mLastError = TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR; + transitionTo(mInitialState); + } + } + + private class DhcpEventCallback extends IDhcpEventCallbacks.Stub { + @Override + public void onLeasesChanged(List leaseParcelables) { + final ArrayList leases = new ArrayList<>(); + for (DhcpLeaseParcelable lease : leaseParcelables) { + final LinkAddress address = new LinkAddress( + intToInet4AddressHTH(lease.netAddr), lease.prefixLength, + 0 /* flags */, RT_SCOPE_UNIVERSE /* as per RFC6724#3.2 */, + lease.expTime /* deprecationTime */, lease.expTime /* expirationTime */); + + final MacAddress macAddress; + try { + macAddress = MacAddress.fromBytes(lease.hwAddr); + } catch (IllegalArgumentException e) { + Log.wtf(TAG, "Invalid address received from DhcpServer: " + + Arrays.toString(lease.hwAddr)); + return; + } + + final TetheredClient.AddressInfo addressInfo = new TetheredClient.AddressInfo( + address, lease.hostname); + leases.add(new TetheredClient( + macAddress, + Collections.singletonList(addressInfo), + mInterfaceType)); + } + + getHandler().post(() -> { + mDhcpLeases = leases; + mCallback.dhcpLeasesChanged(); + }); + } + + @Override + public void onNewPrefixRequest(@NonNull final IpPrefix currentPrefix) { + Objects.requireNonNull(currentPrefix); + sendMessage(CMD_NEW_PREFIX_REQUEST, currentPrefix); + } + + @Override + public int getInterfaceVersion() { + return this.VERSION; + } + + @Override + public String getInterfaceHash() throws RemoteException { + return this.HASH; + } + } + + private RouteInfo getDirectConnectedRoute(@NonNull final LinkAddress ipv4Address) { + Objects.requireNonNull(ipv4Address); + return new RouteInfo(PrefixUtils.asIpPrefix(ipv4Address), null, mIfaceName, RTN_UNICAST); + } + + private DhcpServingParamsParcel makeServingParams(@NonNull final Inet4Address defaultRouter, + @NonNull final Inet4Address dnsServer, @NonNull LinkAddress serverAddr, + @Nullable Inet4Address clientAddr) { + final boolean changePrefixOnDecline = + (mInterfaceType == TetheringManager.TETHERING_NCM && clientAddr == null); + return new DhcpServingParamsParcelExt() + .setDefaultRouters(defaultRouter) + .setDhcpLeaseTimeSecs(DHCP_LEASE_TIME_SECS) + .setDnsServers(dnsServer) + .setServerAddr(serverAddr) + .setMetered(true) + .setSingleClientAddr(clientAddr) + .setChangePrefixOnDecline(changePrefixOnDecline); + // TODO: also advertise link MTU + } + + private boolean startDhcp(final LinkAddress serverLinkAddr, final LinkAddress clientLinkAddr) { + if (mUsingLegacyDhcp) { + return true; + } + + final Inet4Address addr = (Inet4Address) serverLinkAddr.getAddress(); + final Inet4Address clientAddr = clientLinkAddr == null ? null : + (Inet4Address) clientLinkAddr.getAddress(); + + final DhcpServingParamsParcel params = makeServingParams(addr /* defaultRouter */, + addr /* dnsServer */, serverLinkAddr, clientAddr); + mDhcpServerStartIndex++; + mDeps.makeDhcpServer( + mIfaceName, params, new DhcpServerCallbacksImpl(mDhcpServerStartIndex)); + return true; + } + + private void stopDhcp() { + // Make all previous start requests obsolete so servers are not started later + mDhcpServerStartIndex++; + + if (mDhcpServer != null) { + try { + mDhcpServer.stop(new OnHandlerStatusCallback() { + @Override + public void callback(int statusCode) { + if (statusCode != STATUS_SUCCESS) { + mLog.e("Error stopping DHCP server: " + statusCode); + mLastError = TetheringManager.TETHER_ERROR_DHCPSERVER_ERROR; + // Not much more we can do here + } + mDhcpLeases.clear(); + getHandler().post(mCallback::dhcpLeasesChanged); + } + }); + mDhcpServer = null; + } catch (RemoteException e) { + mLog.e("Error stopping DHCP server", e); + // Not much more we can do here + } + } + } + + private boolean configureDhcp(boolean enable, final LinkAddress serverAddr, + final LinkAddress clientAddr) { + if (enable) { + return startDhcp(serverAddr, clientAddr); + } else { + stopDhcp(); + return true; + } + } + + private void stopIPv4() { + configureIPv4(false); + // NOTE: All of configureIPv4() will be refactored out of existence + // into calls to InterfaceController, shared with startIPv4(). + mInterfaceCtrl.clearIPv4Address(); + mPrivateAddressCoordinator.releaseDownstream(this); + mIpv4Address = null; + mStaticIpv4ServerAddr = null; + mStaticIpv4ClientAddr = null; + } + + private boolean configureIPv4(boolean enabled) { + if (VDBG) Log.d(TAG, "configureIPv4(" + enabled + ")"); + + if (enabled) { + mIpv4Address = requestIpv4Address(true /* useLastAddress */); + } + + if (mIpv4Address == null) { + mLog.e("No available ipv4 address"); + return false; + } + + if (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) { + // BT configures the interface elsewhere: only start DHCP. + // TODO: make all tethering types behave the same way, and delete the bluetooth + // code that calls into NetworkManagementService directly. + return configureDhcp(enabled, mIpv4Address, null /* clientAddress */); + } + + final IpPrefix ipv4Prefix = asIpPrefix(mIpv4Address); + + final Boolean setIfaceUp; + if (mInterfaceType == TetheringManager.TETHERING_WIFI + || mInterfaceType == TetheringManager.TETHERING_WIFI_P2P + || mInterfaceType == TetheringManager.TETHERING_ETHERNET + || mInterfaceType == TetheringManager.TETHERING_WIGIG) { + // The WiFi and Ethernet stack has ownership of the interface up/down state. + // It is unclear whether the Bluetooth or USB stacks will manage their own + // state. + setIfaceUp = null; + } else { + setIfaceUp = enabled; + } + if (!mInterfaceCtrl.setInterfaceConfiguration(mIpv4Address, setIfaceUp)) { + mLog.e("Error configuring interface"); + if (!enabled) stopDhcp(); + return false; + } + + if (enabled) { + mLinkProperties.addLinkAddress(mIpv4Address); + mLinkProperties.addRoute(getDirectConnectedRoute(mIpv4Address)); + } else { + mLinkProperties.removeLinkAddress(mIpv4Address); + mLinkProperties.removeRoute(getDirectConnectedRoute(mIpv4Address)); + } + return configureDhcp(enabled, mIpv4Address, mStaticIpv4ClientAddr); + } + + private LinkAddress requestIpv4Address(final boolean useLastAddress) { + if (mStaticIpv4ServerAddr != null) return mStaticIpv4ServerAddr; + + if (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) { + return new LinkAddress(BLUETOOTH_IFACE_ADDR); + } + + return mPrivateAddressCoordinator.requestDownstreamAddress(this, useLastAddress); + } + + private boolean startIPv6() { + mInterfaceParams = mDeps.getInterfaceParams(mIfaceName); + if (mInterfaceParams == null) { + mLog.e("Failed to find InterfaceParams"); + stopIPv6(); + return false; + } + + mRaDaemon = mDeps.getRouterAdvertisementDaemon(mInterfaceParams); + if (!mRaDaemon.start()) { + stopIPv6(); + return false; + } + + // TODO: use ShimUtils instead of explicitly checking the version here. + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R || "S".equals(Build.VERSION.CODENAME) + || "T".equals(Build.VERSION.CODENAME)) { + // DAD Proxy starts forwarding packets after IPv6 upstream is present. + mDadProxy = mDeps.getDadProxy(getHandler(), mInterfaceParams); + } + + return true; + } + + private void stopIPv6() { + mInterfaceParams = null; + setRaParams(null); + + if (mRaDaemon != null) { + mRaDaemon.stop(); + mRaDaemon = null; + } + + if (mDadProxy != null) { + mDadProxy.stop(); + mDadProxy = null; + } + } + + // IPv6TetheringCoordinator sends updates with carefully curated IPv6-only + // LinkProperties. These have extraneous data filtered out and only the + // necessary prefixes included (per its prefix distribution policy). + // + // TODO: Evaluate using a data structure than is more directly suited to + // communicating only the relevant information. + private void updateUpstreamIPv6LinkProperties(LinkProperties v6only, int ttlAdjustment) { + if (mRaDaemon == null) return; + + // Avoid unnecessary work on spurious updates. + if (Objects.equals(mLastIPv6LinkProperties, v6only)) { + return; + } + + RaParams params = null; + String upstreamIface = null; + InterfaceParams upstreamIfaceParams = null; + int upstreamIfIndex = 0; + + if (v6only != null) { + upstreamIface = v6only.getInterfaceName(); + upstreamIfaceParams = mDeps.getInterfaceParams(upstreamIface); + if (upstreamIfaceParams != null) { + upstreamIfIndex = upstreamIfaceParams.index; + } + params = new RaParams(); + params.mtu = v6only.getMtu(); + params.hasDefaultRoute = v6only.hasIpv6DefaultRoute(); + + if (params.hasDefaultRoute) params.hopLimit = getHopLimit(upstreamIface, ttlAdjustment); + + for (LinkAddress linkAddr : v6only.getLinkAddresses()) { + if (linkAddr.getPrefixLength() != RFC7421_PREFIX_LENGTH) continue; + + final IpPrefix prefix = new IpPrefix( + linkAddr.getAddress(), linkAddr.getPrefixLength()); + params.prefixes.add(prefix); + + final Inet6Address dnsServer = getLocalDnsIpFor(prefix); + if (dnsServer != null) { + params.dnses.add(dnsServer); + } + } + } + + // Add upstream index to name mapping. See the comment of the interface mapping update in + // CMD_TETHER_CONNECTION_CHANGED. Adding the mapping update here to the avoid potential + // timing issue. It prevents that the IPv6 capability is updated later than + // CMD_TETHER_CONNECTION_CHANGED. + mBpfCoordinator.addUpstreamNameToLookupTable(upstreamIfIndex, upstreamIface); + + // If v6only is null, we pass in null to setRaParams(), which handles + // deprecation of any existing RA data. + + setRaParams(params); + mLastIPv6LinkProperties = v6only; + + updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, upstreamIfIndex, null); + mLastIPv6UpstreamIfindex = upstreamIfIndex; + if (mDadProxy != null) { + mDadProxy.setUpstreamIface(upstreamIfaceParams); + } + } + + private void removeRoutesFromLocalNetwork(@NonNull final List toBeRemoved) { + final int removalFailures = RouteUtils.removeRoutesFromLocalNetwork( + mNetd, toBeRemoved); + if (removalFailures > 0) { + mLog.e(String.format("Failed to remove %d IPv6 routes from local table.", + removalFailures)); + } + + for (RouteInfo route : toBeRemoved) mLinkProperties.removeRoute(route); + } + + private void addRoutesToLocalNetwork(@NonNull final List toBeAdded) { + try { + // It's safe to call networkAddInterface() even if + // the interface is already in the local_network. + mNetd.networkAddInterface(INetd.LOCAL_NET_ID, mIfaceName); + try { + // Add routes from local network. Note that adding routes that + // already exist does not cause an error (EEXIST is silently ignored). + RouteUtils.addRoutesToLocalNetwork(mNetd, mIfaceName, toBeAdded); + } catch (IllegalStateException e) { + mLog.e("Failed to add IPv4/v6 routes to local table: " + e); + return; + } + } catch (ServiceSpecificException | RemoteException e) { + mLog.e("Failed to add " + mIfaceName + " to local table: ", e); + return; + } + + for (RouteInfo route : toBeAdded) mLinkProperties.addRoute(route); + } + + private void configureLocalIPv6Routes( + HashSet deprecatedPrefixes, HashSet newPrefixes) { + // [1] Remove the routes that are deprecated. + if (!deprecatedPrefixes.isEmpty()) { + removeRoutesFromLocalNetwork(getLocalRoutesFor(mIfaceName, deprecatedPrefixes)); + } + + // [2] Add only the routes that have not previously been added. + if (newPrefixes != null && !newPrefixes.isEmpty()) { + HashSet addedPrefixes = (HashSet) newPrefixes.clone(); + if (mLastRaParams != null) { + addedPrefixes.removeAll(mLastRaParams.prefixes); + } + + if (!addedPrefixes.isEmpty()) { + addRoutesToLocalNetwork(getLocalRoutesFor(mIfaceName, addedPrefixes)); + } + } + } + + private void configureLocalIPv6Dns( + HashSet deprecatedDnses, HashSet newDnses) { + // TODO: Is this really necessary? Can we not fail earlier if INetd cannot be located? + if (mNetd == null) { + if (newDnses != null) newDnses.clear(); + mLog.e("No netd service instance available; not setting local IPv6 addresses"); + return; + } + + // [1] Remove deprecated local DNS IP addresses. + if (!deprecatedDnses.isEmpty()) { + for (Inet6Address dns : deprecatedDnses) { + if (!mInterfaceCtrl.removeAddress(dns, RFC7421_PREFIX_LENGTH)) { + mLog.e("Failed to remove local dns IP " + dns); + } + + mLinkProperties.removeLinkAddress(new LinkAddress(dns, RFC7421_PREFIX_LENGTH)); + } + } + + // [2] Add only the local DNS IP addresses that have not previously been added. + if (newDnses != null && !newDnses.isEmpty()) { + final HashSet addedDnses = (HashSet) newDnses.clone(); + if (mLastRaParams != null) { + addedDnses.removeAll(mLastRaParams.dnses); + } + + for (Inet6Address dns : addedDnses) { + if (!mInterfaceCtrl.addAddress(dns, RFC7421_PREFIX_LENGTH)) { + mLog.e("Failed to add local dns IP " + dns); + newDnses.remove(dns); + } + + mLinkProperties.addLinkAddress(new LinkAddress(dns, RFC7421_PREFIX_LENGTH)); + } + } + + try { + mNetd.tetherApplyDnsInterfaces(); + } catch (ServiceSpecificException | RemoteException e) { + mLog.e("Failed to update local DNS caching server"); + if (newDnses != null) newDnses.clear(); + } + } + + private void addIpv6ForwardingRule(Ipv6ForwardingRule rule) { + // Theoretically, we don't need this check because IP neighbor monitor doesn't start if BPF + // offload is disabled. Add this check just in case. + // TODO: Perhaps remove this protection check. + if (!mUsingBpfOffload) return; + + mBpfCoordinator.tetherOffloadRuleAdd(this, rule); + } + + private void removeIpv6ForwardingRule(Ipv6ForwardingRule rule) { + // TODO: Perhaps remove this protection check. + // See the related comment in #addIpv6ForwardingRule. + if (!mUsingBpfOffload) return; + + mBpfCoordinator.tetherOffloadRuleRemove(this, rule); + } + + private void clearIpv6ForwardingRules() { + if (!mUsingBpfOffload) return; + + mBpfCoordinator.tetherOffloadRuleClear(this); + } + + private void updateIpv6ForwardingRule(int newIfindex) { + // TODO: Perhaps remove this protection check. + // See the related comment in #addIpv6ForwardingRule. + if (!mUsingBpfOffload) return; + + mBpfCoordinator.tetherOffloadRuleUpdate(this, newIfindex); + } + + // Handles all updates to IPv6 forwarding rules. These can currently change only if the upstream + // changes or if a neighbor event is received. + private void updateIpv6ForwardingRules(int prevUpstreamIfindex, int upstreamIfindex, + NeighborEvent e) { + // If we no longer have an upstream, clear forwarding rules and do nothing else. + if (upstreamIfindex == 0) { + clearIpv6ForwardingRules(); + return; + } + + // If the upstream interface has changed, remove all rules and re-add them with the new + // upstream interface. + if (prevUpstreamIfindex != upstreamIfindex) { + updateIpv6ForwardingRule(upstreamIfindex); + } + + // If we're here to process a NeighborEvent, do so now. + // mInterfaceParams must be non-null or the event would not have arrived. + if (e == null) return; + if (!(e.ip instanceof Inet6Address) || e.ip.isMulticastAddress() + || e.ip.isLoopbackAddress() || e.ip.isLinkLocalAddress()) { + return; + } + + // When deleting rules, we still need to pass a non-null MAC, even though it's ignored. + // Do this here instead of in the Ipv6ForwardingRule constructor to ensure that we never + // add rules with a null MAC, only delete them. + MacAddress dstMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS; + Ipv6ForwardingRule rule = new Ipv6ForwardingRule(upstreamIfindex, + mInterfaceParams.index, (Inet6Address) e.ip, mInterfaceParams.macAddr, dstMac); + if (e.isValid()) { + addIpv6ForwardingRule(rule); + } else { + removeIpv6ForwardingRule(rule); + } + } + + // TODO: consider moving into BpfCoordinator. + private void updateClientInfoIpv4(NeighborEvent e) { + // TODO: Perhaps remove this protection check. + // See the related comment in #addIpv6ForwardingRule. + if (!mUsingBpfOffload) return; + + if (e == null) return; + if (!(e.ip instanceof Inet4Address) || e.ip.isMulticastAddress() + || e.ip.isLoopbackAddress() || e.ip.isLinkLocalAddress()) { + return; + } + + // When deleting clients, IpServer still need to pass a non-null MAC, even though it's + // ignored. Do this here instead of in the ClientInfo constructor to ensure that + // IpServer never add clients with a null MAC, only delete them. + final MacAddress clientMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS; + final ClientInfo clientInfo = new ClientInfo(mInterfaceParams.index, + mInterfaceParams.macAddr, (Inet4Address) e.ip, clientMac); + if (e.isValid()) { + mBpfCoordinator.tetherOffloadClientAdd(this, clientInfo); + } else { + // TODO: Delete all related offload rules which are using this client. + mBpfCoordinator.tetherOffloadClientRemove(this, clientInfo); + } + } + + private void handleNeighborEvent(NeighborEvent e) { + if (mInterfaceParams != null + && mInterfaceParams.index == e.ifindex + && mInterfaceParams.hasMacAddress) { + updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, mLastIPv6UpstreamIfindex, e); + updateClientInfoIpv4(e); + } + } + + private void handleNewPrefixRequest(@NonNull final IpPrefix currentPrefix) { + if (!currentPrefix.contains(mIpv4Address.getAddress()) + || currentPrefix.getPrefixLength() != mIpv4Address.getPrefixLength()) { + Log.e(TAG, "Invalid prefix: " + currentPrefix); + return; + } + + final LinkAddress deprecatedLinkAddress = mIpv4Address; + mIpv4Address = requestIpv4Address(false); + if (mIpv4Address == null) { + mLog.e("Fail to request a new downstream prefix"); + return; + } + final Inet4Address srvAddr = (Inet4Address) mIpv4Address.getAddress(); + + // Add new IPv4 address on the interface. + if (!mInterfaceCtrl.addAddress(srvAddr, currentPrefix.getPrefixLength())) { + mLog.e("Failed to add new IP " + srvAddr); + return; + } + + // Remove deprecated routes from local network. + removeRoutesFromLocalNetwork( + Collections.singletonList(getDirectConnectedRoute(deprecatedLinkAddress))); + mLinkProperties.removeLinkAddress(deprecatedLinkAddress); + + // Add new routes to local network. + addRoutesToLocalNetwork( + Collections.singletonList(getDirectConnectedRoute(mIpv4Address))); + mLinkProperties.addLinkAddress(mIpv4Address); + + // Update local DNS caching server with new IPv4 address, otherwise, dnsmasq doesn't + // listen on the interface configured with new IPv4 address, that results DNS validation + // failure of downstream client even if appropriate routes have been configured. + try { + mNetd.tetherApplyDnsInterfaces(); + } catch (ServiceSpecificException | RemoteException e) { + mLog.e("Failed to update local DNS caching server"); + return; + } + sendLinkProperties(); + + // Notify DHCP server that new prefix/route has been applied on IpServer. + final Inet4Address clientAddr = mStaticIpv4ClientAddr == null ? null : + (Inet4Address) mStaticIpv4ClientAddr.getAddress(); + final DhcpServingParamsParcel params = makeServingParams(srvAddr /* defaultRouter */, + srvAddr /* dnsServer */, mIpv4Address /* serverLinkAddress */, clientAddr); + try { + mDhcpServer.updateParams(params, new OnHandlerStatusCallback() { + @Override + public void callback(int statusCode) { + if (statusCode != STATUS_SUCCESS) { + mLog.e("Error updating DHCP serving params: " + statusCode); + } + } + }); + } catch (RemoteException e) { + mLog.e("Error updating DHCP serving params", e); + } + } + + private byte getHopLimit(String upstreamIface, int adjustTTL) { + try { + int upstreamHopLimit = Integer.parseUnsignedInt( + mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, upstreamIface, "hop_limit")); + upstreamHopLimit = upstreamHopLimit + adjustTTL; + // Cap the hop limit to 255. + return (byte) Integer.min(upstreamHopLimit, 255); + } catch (Exception e) { + mLog.e("Failed to find upstream interface hop limit", e); + } + return RaParams.DEFAULT_HOPLIMIT; + } + + private void setRaParams(RaParams newParams) { + if (mRaDaemon != null) { + final RaParams deprecatedParams = + RaParams.getDeprecatedRaParams(mLastRaParams, newParams); + + configureLocalIPv6Routes(deprecatedParams.prefixes, + (newParams != null) ? newParams.prefixes : null); + + configureLocalIPv6Dns(deprecatedParams.dnses, + (newParams != null) ? newParams.dnses : null); + + mRaDaemon.buildNewRa(deprecatedParams, newParams); + } + + mLastRaParams = newParams; + } + + private void logMessage(State state, int what) { + mLog.log(state.getName() + " got " + sMagicDecoderRing.get(what, Integer.toString(what))); + } + + private void sendInterfaceState(int newInterfaceState) { + mServingMode = newInterfaceState; + mCallback.updateInterfaceState(this, newInterfaceState, mLastError); + sendLinkProperties(); + } + + private void sendLinkProperties() { + mCallback.updateLinkProperties(this, new LinkProperties(mLinkProperties)); + } + + private void resetLinkProperties() { + mLinkProperties.clear(); + mLinkProperties.setInterfaceName(mIfaceName); + } + + private void maybeConfigureStaticIp(final TetheringRequestParcel request) { + // Ignore static address configuration if they are invalid or null. In theory, static + // addresses should not be invalid here because TetheringManager do not allow caller to + // specify invalid static address configuration. + if (request == null || request.localIPv4Address == null + || request.staticClientAddress == null || !checkStaticAddressConfiguration( + request.localIPv4Address, request.staticClientAddress)) { + return; + } + + mStaticIpv4ServerAddr = request.localIPv4Address; + mStaticIpv4ClientAddr = request.staticClientAddress; + } + + class InitialState extends State { + @Override + public void enter() { + sendInterfaceState(STATE_AVAILABLE); + } + + @Override + public boolean processMessage(Message message) { + logMessage(this, message.what); + switch (message.what) { + case CMD_TETHER_REQUESTED: + mLastError = TetheringManager.TETHER_ERROR_NO_ERROR; + switch (message.arg1) { + case STATE_LOCAL_ONLY: + maybeConfigureStaticIp((TetheringRequestParcel) message.obj); + transitionTo(mLocalHotspotState); + break; + case STATE_TETHERED: + maybeConfigureStaticIp((TetheringRequestParcel) message.obj); + transitionTo(mTetheredState); + break; + default: + mLog.e("Invalid tethering interface serving state specified."); + } + break; + case CMD_INTERFACE_DOWN: + transitionTo(mUnavailableState); + break; + case CMD_IPV6_TETHER_UPDATE: + updateUpstreamIPv6LinkProperties((LinkProperties) message.obj, message.arg1); + break; + default: + return NOT_HANDLED; + } + return HANDLED; + } + } + + private void startConntrackMonitoring() { + mBpfCoordinator.startMonitoring(this); + } + + private void stopConntrackMonitoring() { + mBpfCoordinator.stopMonitoring(this); + } + + class BaseServingState extends State { + @Override + public void enter() { + startConntrackMonitoring(); + + if (!startIPv4()) { + mLastError = TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR; + return; + } + + try { + NetdUtils.tetherInterface(mNetd, mIfaceName, asIpPrefix(mIpv4Address)); + } catch (RemoteException | ServiceSpecificException | IllegalStateException e) { + mLog.e("Error Tethering", e); + mLastError = TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR; + return; + } + + if (!startIPv6()) { + mLog.e("Failed to startIPv6"); + // TODO: Make this a fatal error once Bluetooth IPv6 is sorted. + return; + } + } + + @Override + public void exit() { + // Note that at this point, we're leaving the tethered state. We can fail any + // of these operations, but it doesn't really change that we have to try them + // all in sequence. + stopIPv6(); + + try { + NetdUtils.untetherInterface(mNetd, mIfaceName); + } catch (RemoteException | ServiceSpecificException e) { + mLastError = TetheringManager.TETHER_ERROR_UNTETHER_IFACE_ERROR; + mLog.e("Failed to untether interface: " + e); + } + + stopIPv4(); + stopConntrackMonitoring(); + + resetLinkProperties(); + } + + @Override + public boolean processMessage(Message message) { + logMessage(this, message.what); + switch (message.what) { + case CMD_TETHER_UNREQUESTED: + transitionTo(mInitialState); + if (DBG) Log.d(TAG, "Untethered (unrequested)" + mIfaceName); + break; + case CMD_INTERFACE_DOWN: + transitionTo(mUnavailableState); + if (DBG) Log.d(TAG, "Untethered (ifdown)" + mIfaceName); + break; + case CMD_IPV6_TETHER_UPDATE: + updateUpstreamIPv6LinkProperties((LinkProperties) message.obj, message.arg1); + sendLinkProperties(); + break; + case CMD_IP_FORWARDING_ENABLE_ERROR: + case CMD_IP_FORWARDING_DISABLE_ERROR: + case CMD_START_TETHERING_ERROR: + case CMD_STOP_TETHERING_ERROR: + case CMD_SET_DNS_FORWARDERS_ERROR: + mLastError = TetheringManager.TETHER_ERROR_INTERNAL_ERROR; + transitionTo(mInitialState); + break; + case CMD_NEW_PREFIX_REQUEST: + handleNewPrefixRequest((IpPrefix) message.obj); + break; + case CMD_NOTIFY_PREFIX_CONFLICT: + mLog.i("restart tethering: " + mInterfaceType); + mCallback.requestEnableTethering(mInterfaceType, false /* enabled */); + transitionTo(mWaitingForRestartState); + break; + default: + return false; + } + return true; + } + } + + // Handling errors in BaseServingState.enter() by transitioning is + // problematic because transitioning during a multi-state jump yields + // a Log.wtf(). Ultimately, there should be only one ServingState, + // and forwarding and NAT rules should be handled by a coordinating + // functional element outside of IpServer. + class LocalHotspotState extends BaseServingState { + @Override + public void enter() { + super.enter(); + if (mLastError != TetheringManager.TETHER_ERROR_NO_ERROR) { + transitionTo(mInitialState); + } + + if (DBG) Log.d(TAG, "Local hotspot " + mIfaceName); + sendInterfaceState(STATE_LOCAL_ONLY); + } + + @Override + public boolean processMessage(Message message) { + if (super.processMessage(message)) return true; + + logMessage(this, message.what); + switch (message.what) { + case CMD_TETHER_REQUESTED: + mLog.e("CMD_TETHER_REQUESTED while in local-only hotspot mode."); + break; + case CMD_TETHER_CONNECTION_CHANGED: + // Ignored in local hotspot state. + break; + default: + return false; + } + return true; + } + } + + // Handling errors in BaseServingState.enter() by transitioning is + // problematic because transitioning during a multi-state jump yields + // a Log.wtf(). Ultimately, there should be only one ServingState, + // and forwarding and NAT rules should be handled by a coordinating + // functional element outside of IpServer. + class TetheredState extends BaseServingState { + @Override + public void enter() { + super.enter(); + if (mLastError != TetheringManager.TETHER_ERROR_NO_ERROR) { + transitionTo(mInitialState); + } + + if (DBG) Log.d(TAG, "Tethered " + mIfaceName); + sendInterfaceState(STATE_TETHERED); + } + + @Override + public void exit() { + cleanupUpstream(); + super.exit(); + } + + private void cleanupUpstream() { + if (mUpstreamIfaceSet == null) return; + + for (String ifname : mUpstreamIfaceSet.ifnames) cleanupUpstreamInterface(ifname); + mUpstreamIfaceSet = null; + clearIpv6ForwardingRules(); + } + + private void cleanupUpstreamInterface(String upstreamIface) { + // Note that we don't care about errors here. + // Sometimes interfaces are gone before we get + // to remove their rules, which generates errors. + // Just do the best we can. + mBpfCoordinator.maybeDetachProgram(mIfaceName, upstreamIface); + try { + mNetd.ipfwdRemoveInterfaceForward(mIfaceName, upstreamIface); + } catch (RemoteException | ServiceSpecificException e) { + mLog.e("Exception in ipfwdRemoveInterfaceForward: " + e.toString()); + } + try { + mNetd.tetherRemoveForward(mIfaceName, upstreamIface); + } catch (RemoteException | ServiceSpecificException e) { + mLog.e("Exception in disableNat: " + e.toString()); + } + } + + @Override + public boolean processMessage(Message message) { + if (super.processMessage(message)) return true; + + logMessage(this, message.what); + switch (message.what) { + case CMD_TETHER_REQUESTED: + mLog.e("CMD_TETHER_REQUESTED while already tethering."); + break; + case CMD_TETHER_CONNECTION_CHANGED: + final InterfaceSet newUpstreamIfaceSet = (InterfaceSet) message.obj; + if (noChangeInUpstreamIfaceSet(newUpstreamIfaceSet)) { + if (VDBG) Log.d(TAG, "Connection changed noop - dropping"); + break; + } + + if (newUpstreamIfaceSet == null) { + cleanupUpstream(); + break; + } + + for (String removed : upstreamInterfacesRemoved(newUpstreamIfaceSet)) { + cleanupUpstreamInterface(removed); + } + + final Set added = upstreamInterfacesAdd(newUpstreamIfaceSet); + // This makes the call to cleanupUpstream() in the error + // path for any interface neatly cleanup all the interfaces. + mUpstreamIfaceSet = newUpstreamIfaceSet; + + for (String ifname : added) { + // Add upstream index to name mapping for the tether stats usage in the + // coordinator. Although this mapping could be added by both class + // Tethering and IpServer, adding mapping from IpServer guarantees that + // the mapping is added before adding forwarding rules. That is because + // there are different state machines in both classes. It is hard to + // guarantee the link property update order between multiple state machines. + // Note that both IPv4 and IPv6 interface may be added because + // Tethering::setUpstreamNetwork calls getTetheringInterfaces which merges + // IPv4 and IPv6 interface name (if any) into an InterfaceSet. The IPv6 + // capability may be updated later. In that case, IPv6 interface mapping is + // updated in updateUpstreamIPv6LinkProperties. + if (!ifname.startsWith("v4-")) { // ignore clat interfaces + final InterfaceParams upstreamIfaceParams = + mDeps.getInterfaceParams(ifname); + if (upstreamIfaceParams != null) { + mBpfCoordinator.addUpstreamNameToLookupTable( + upstreamIfaceParams.index, ifname); + } + } + + mBpfCoordinator.maybeAttachProgram(mIfaceName, ifname); + try { + mNetd.tetherAddForward(mIfaceName, ifname); + mNetd.ipfwdAddInterfaceForward(mIfaceName, ifname); + } catch (RemoteException | ServiceSpecificException e) { + mLog.e("Exception enabling NAT: " + e.toString()); + cleanupUpstream(); + mLastError = TetheringManager.TETHER_ERROR_ENABLE_FORWARDING_ERROR; + transitionTo(mInitialState); + return true; + } + } + break; + case CMD_NEIGHBOR_EVENT: + handleNeighborEvent((NeighborEvent) message.obj); + break; + default: + return false; + } + return true; + } + + private boolean noChangeInUpstreamIfaceSet(InterfaceSet newIfaces) { + if (mUpstreamIfaceSet == null && newIfaces == null) return true; + if (mUpstreamIfaceSet != null && newIfaces != null) { + return mUpstreamIfaceSet.equals(newIfaces); + } + return false; + } + + private Set upstreamInterfacesRemoved(InterfaceSet newIfaces) { + if (mUpstreamIfaceSet == null) return new HashSet<>(); + + final HashSet removed = new HashSet<>(mUpstreamIfaceSet.ifnames); + removed.removeAll(newIfaces.ifnames); + return removed; + } + + private Set upstreamInterfacesAdd(InterfaceSet newIfaces) { + final HashSet added = new HashSet<>(newIfaces.ifnames); + if (mUpstreamIfaceSet != null) added.removeAll(mUpstreamIfaceSet.ifnames); + return added; + } + } + + /** + * This state is terminal for the per interface state machine. At this + * point, the tethering main state machine should have removed this interface + * specific state machine from its list of possible recipients of + * tethering requests. The state machine itself will hang around until + * the garbage collector finds it. + */ + class UnavailableState extends State { + @Override + public void enter() { + mIpNeighborMonitor.stop(); + mLastError = TetheringManager.TETHER_ERROR_NO_ERROR; + sendInterfaceState(STATE_UNAVAILABLE); + } + } + + class WaitingForRestartState extends State { + @Override + public boolean processMessage(Message message) { + logMessage(this, message.what); + switch (message.what) { + case CMD_TETHER_UNREQUESTED: + transitionTo(mInitialState); + mLog.i("Untethered (unrequested) and restarting " + mIfaceName); + mCallback.requestEnableTethering(mInterfaceType, true /* enabled */); + break; + case CMD_INTERFACE_DOWN: + transitionTo(mUnavailableState); + mLog.i("Untethered (interface down) and restarting " + mIfaceName); + mCallback.requestEnableTethering(mInterfaceType, true /* enabled */); + break; + default: + return false; + } + return true; + } + } + + // Accumulate routes representing "prefixes to be assigned to the local + // interface", for subsequent modification of local_network routing. + private static ArrayList getLocalRoutesFor( + String ifname, HashSet prefixes) { + final ArrayList localRoutes = new ArrayList(); + for (IpPrefix ipp : prefixes) { + localRoutes.add(new RouteInfo(ipp, null, ifname, RTN_UNICAST)); + } + return localRoutes; + } + + // Given a prefix like 2001:db8::/64 return an address like 2001:db8::1. + private static Inet6Address getLocalDnsIpFor(IpPrefix localPrefix) { + final byte[] dnsBytes = localPrefix.getRawAddress(); + dnsBytes[dnsBytes.length - 1] = getRandomSanitizedByte(DOUG_ADAMS, asByte(0), asByte(1)); + try { + return Inet6Address.getByAddress(null, dnsBytes, 0); + } catch (UnknownHostException e) { + Log.wtf(TAG, "Failed to construct Inet6Address from: " + localPrefix); + return null; + } + } + + private static byte getRandomSanitizedByte(byte dflt, byte... excluded) { + final byte random = (byte) (new Random()).nextInt(); + for (int value : excluded) { + if (random == value) return dflt; + } + return random; + } +} diff --git a/Tethering/src/android/net/ip/NeighborPacketForwarder.java b/Tethering/src/android/net/ip/NeighborPacketForwarder.java new file mode 100644 index 0000000000..084743db03 --- /dev/null +++ b/Tethering/src/android/net/ip/NeighborPacketForwarder.java @@ -0,0 +1,181 @@ +/* + * 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.ip; + +import static android.system.OsConstants.AF_INET6; +import static android.system.OsConstants.AF_PACKET; +import static android.system.OsConstants.ETH_P_IPV6; +import static android.system.OsConstants.IPPROTO_RAW; +import static android.system.OsConstants.SOCK_DGRAM; +import static android.system.OsConstants.SOCK_NONBLOCK; +import static android.system.OsConstants.SOCK_RAW; + +import android.net.util.InterfaceParams; +import android.net.util.SocketUtils; +import android.net.util.TetheringUtils; +import android.os.Handler; +import android.system.ErrnoException; +import android.system.Os; +import android.util.Log; + +import com.android.net.module.util.PacketReader; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.Inet6Address; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Arrays; + +/** + * Basic IPv6 Neighbor Advertisement Forwarder. + * + * Forward NA packets from upstream iface to tethered iface + * and NS packets from tethered iface to upstream iface. + * + * @hide + */ +public class NeighborPacketForwarder extends PacketReader { + private final String mTag; + + private FileDescriptor mFd; + + // TODO: get these from NetworkStackConstants. + private static final int IPV6_ADDR_LEN = 16; + private static final int IPV6_DST_ADDR_OFFSET = 24; + private static final int IPV6_HEADER_LEN = 40; + private static final int ETH_HEADER_LEN = 14; + + private InterfaceParams mListenIfaceParams, mSendIfaceParams; + + private final int mType; + public static final int ICMPV6_NEIGHBOR_ADVERTISEMENT = 136; + public static final int ICMPV6_NEIGHBOR_SOLICITATION = 135; + + public NeighborPacketForwarder(Handler h, InterfaceParams tetheredInterface, int type) { + super(h); + mTag = NeighborPacketForwarder.class.getSimpleName() + "-" + + tetheredInterface.name + "-" + type; + mType = type; + + if (mType == ICMPV6_NEIGHBOR_ADVERTISEMENT) { + mSendIfaceParams = tetheredInterface; + } else { + mListenIfaceParams = tetheredInterface; + } + } + + /** Set new upstream iface and start/stop based on new params. */ + public void setUpstreamIface(InterfaceParams upstreamParams) { + final InterfaceParams oldUpstreamParams; + + if (mType == ICMPV6_NEIGHBOR_ADVERTISEMENT) { + oldUpstreamParams = mListenIfaceParams; + mListenIfaceParams = upstreamParams; + } else { + oldUpstreamParams = mSendIfaceParams; + mSendIfaceParams = upstreamParams; + } + + if (oldUpstreamParams == null && upstreamParams != null) { + start(); + } else if (oldUpstreamParams != null && upstreamParams == null) { + stop(); + } else if (oldUpstreamParams != null && upstreamParams != null + && oldUpstreamParams.index != upstreamParams.index) { + stop(); + start(); + } + } + + // TODO: move NetworkStackUtils.closeSocketQuietly to + // frameworks/libs/net/common/device/com/android/net/module/util/[someclass]. + private void closeSocketQuietly(FileDescriptor fd) { + try { + SocketUtils.closeSocket(fd); + } catch (IOException ignored) { + } + } + + @Override + protected FileDescriptor createFd() { + try { + // ICMPv6 packets from modem do not have eth header, so RAW socket cannot be used. + // To keep uniformity in both directions PACKET socket can be used. + mFd = Os.socket(AF_PACKET, SOCK_DGRAM | SOCK_NONBLOCK, 0); + + // TODO: convert setup*Socket to setupICMPv6BpfFilter with filter type? + if (mType == ICMPV6_NEIGHBOR_ADVERTISEMENT) { + TetheringUtils.setupNaSocket(mFd); + } else if (mType == ICMPV6_NEIGHBOR_SOLICITATION) { + TetheringUtils.setupNsSocket(mFd); + } + + SocketAddress bindAddress = SocketUtils.makePacketSocketAddress( + ETH_P_IPV6, mListenIfaceParams.index); + Os.bind(mFd, bindAddress); + } catch (ErrnoException | SocketException e) { + Log.wtf(mTag, "Failed to create socket", e); + closeSocketQuietly(mFd); + return null; + } + + return mFd; + } + + private Inet6Address getIpv6DestinationAddress(byte[] recvbuf) { + Inet6Address dstAddr; + try { + dstAddr = (Inet6Address) Inet6Address.getByAddress(Arrays.copyOfRange(recvbuf, + IPV6_DST_ADDR_OFFSET, IPV6_DST_ADDR_OFFSET + IPV6_ADDR_LEN)); + } catch (UnknownHostException | ClassCastException impossible) { + throw new AssertionError("16-byte array not valid IPv6 address?"); + } + return dstAddr; + } + + @Override + protected void handlePacket(byte[] recvbuf, int length) { + if (mSendIfaceParams == null) { + return; + } + + // The BPF filter should already have checked the length of the packet, but... + if (length < IPV6_HEADER_LEN) { + return; + } + Inet6Address destv6 = getIpv6DestinationAddress(recvbuf); + if (!destv6.isMulticastAddress()) { + return; + } + InetSocketAddress dest = new InetSocketAddress(destv6, 0); + + FileDescriptor fd = null; + try { + fd = Os.socket(AF_INET6, SOCK_RAW | SOCK_NONBLOCK, IPPROTO_RAW); + SocketUtils.bindSocketToInterface(fd, mSendIfaceParams.name); + + int ret = Os.sendto(fd, recvbuf, 0, length, 0, dest); + } catch (ErrnoException | SocketException e) { + Log.e(mTag, "handlePacket error: " + e); + } finally { + closeSocketQuietly(fd); + } + } +} diff --git a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java new file mode 100644 index 0000000000..543a5c722f --- /dev/null +++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java @@ -0,0 +1,659 @@ +/* + * 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.ip; + +import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH; +import static android.net.util.TetheringUtils.getAllNodesForScopeId; +import static android.system.OsConstants.AF_INET6; +import static android.system.OsConstants.IPPROTO_ICMPV6; +import static android.system.OsConstants.SOCK_RAW; +import static android.system.OsConstants.SOL_SOCKET; +import static android.system.OsConstants.SO_SNDTIMEO; + +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_RA_HEADER_LEN; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION; +import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU; +import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS; +import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK; +import static com.android.net.module.util.NetworkStackConstants.TAG_SYSTEM_NEIGHBOR; + +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.MacAddress; +import android.net.TrafficStats; +import android.net.util.InterfaceParams; +import android.net.util.SocketUtils; +import android.net.util.TetheringUtils; +import android.system.ErrnoException; +import android.system.Os; +import android.system.StructTimeval; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; +import com.android.net.module.util.structs.Icmpv6Header; +import com.android.net.module.util.structs.LlaOption; +import com.android.net.module.util.structs.MtuOption; +import com.android.net.module.util.structs.PrefixInformationOption; +import com.android.net.module.util.structs.RaHeader; +import com.android.net.module.util.structs.RdnssOption; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + + +/** + * Basic IPv6 Router Advertisement Daemon. + * + * TODO: + * + * - Rewrite using Handler (and friends) so that AlarmManager can deliver + * "kick" messages when it's time to send a multicast RA. + * + * @hide + */ +public class RouterAdvertisementDaemon { + private static final String TAG = RouterAdvertisementDaemon.class.getSimpleName(); + + // Summary of various timers and lifetimes. + private static final int MIN_RTR_ADV_INTERVAL_SEC = 300; + private static final int MAX_RTR_ADV_INTERVAL_SEC = 600; + // In general, router, prefix, and DNS lifetimes are all advised to be + // greater than or equal to 3 * MAX_RTR_ADV_INTERVAL. Here, we double + // that to allow for multicast packet loss. + // + // This MAX_RTR_ADV_INTERVAL_SEC and DEFAULT_LIFETIME are also consistent + // with the https://tools.ietf.org/html/rfc7772#section-4 discussion of + // "approximately 7 RAs per hour". + private static final int DEFAULT_LIFETIME = 6 * MAX_RTR_ADV_INTERVAL_SEC; + // From https://tools.ietf.org/html/rfc4861#section-10 . + private static final int MIN_DELAY_BETWEEN_RAS_SEC = 3; + // Both initial and final RAs, but also for changes in RA contents. + // From https://tools.ietf.org/html/rfc4861#section-10 . + private static final int MAX_URGENT_RTR_ADVERTISEMENTS = 5; + + private static final int DAY_IN_SECONDS = 86_400; + + private final InterfaceParams mInterface; + private final InetSocketAddress mAllNodes; + + // This lock is to protect the RA from being updated while being + // transmitted on another thread (multicast or unicast). + // + // TODO: This should be handled with a more RCU-like approach. + private final Object mLock = new Object(); + @GuardedBy("mLock") + private final byte[] mRA = new byte[IPV6_MIN_MTU]; + @GuardedBy("mLock") + private int mRaLength; + @GuardedBy("mLock") + private final DeprecatedInfoTracker mDeprecatedInfoTracker; + @GuardedBy("mLock") + private RaParams mRaParams; + + private volatile FileDescriptor mSocket; + private volatile MulticastTransmitter mMulticastTransmitter; + private volatile UnicastResponder mUnicastResponder; + + /** Encapsulate the RA parameters for RouterAdvertisementDaemon.*/ + public static class RaParams { + // Tethered traffic will have the hop limit properly decremented. + // Consequently, set the hoplimit greater by one than the upstream + // unicast hop limit. + // + // TODO: Dynamically pass down the IPV6_UNICAST_HOPS value from the + // upstream interface for more correct behaviour. + static final byte DEFAULT_HOPLIMIT = 65; + + public boolean hasDefaultRoute; + public byte hopLimit; + public int mtu; + public HashSet prefixes; + public HashSet dnses; + + public RaParams() { + hasDefaultRoute = false; + hopLimit = DEFAULT_HOPLIMIT; + mtu = IPV6_MIN_MTU; + prefixes = new HashSet(); + dnses = new HashSet(); + } + + public RaParams(RaParams other) { + hasDefaultRoute = other.hasDefaultRoute; + hopLimit = other.hopLimit; + mtu = other.mtu; + prefixes = (HashSet) other.prefixes.clone(); + dnses = (HashSet) other.dnses.clone(); + } + + /** + * Returns the subset of RA parameters that become deprecated when + * moving from announcing oldRa to announcing newRa. + * + * Currently only tracks differences in |prefixes| and |dnses|. + */ + public static RaParams getDeprecatedRaParams(RaParams oldRa, RaParams newRa) { + RaParams newlyDeprecated = new RaParams(); + + if (oldRa != null) { + for (IpPrefix ipp : oldRa.prefixes) { + if (newRa == null || !newRa.prefixes.contains(ipp)) { + newlyDeprecated.prefixes.add(ipp); + } + } + + for (Inet6Address dns : oldRa.dnses) { + if (newRa == null || !newRa.dnses.contains(dns)) { + newlyDeprecated.dnses.add(dns); + } + } + } + + return newlyDeprecated; + } + } + + private static class DeprecatedInfoTracker { + private final HashMap mPrefixes = new HashMap<>(); + private final HashMap mDnses = new HashMap<>(); + + Set getPrefixes() { + return mPrefixes.keySet(); + } + + void putPrefixes(Set prefixes) { + for (IpPrefix ipp : prefixes) { + mPrefixes.put(ipp, MAX_URGENT_RTR_ADVERTISEMENTS); + } + } + + void removePrefixes(Set prefixes) { + for (IpPrefix ipp : prefixes) { + mPrefixes.remove(ipp); + } + } + + Set getDnses() { + return mDnses.keySet(); + } + + void putDnses(Set dnses) { + for (Inet6Address dns : dnses) { + mDnses.put(dns, MAX_URGENT_RTR_ADVERTISEMENTS); + } + } + + void removeDnses(Set dnses) { + for (Inet6Address dns : dnses) { + mDnses.remove(dns); + } + } + + boolean isEmpty() { + return mPrefixes.isEmpty() && mDnses.isEmpty(); + } + + private boolean decrementCounters() { + boolean removed = decrementCounter(mPrefixes); + removed |= decrementCounter(mDnses); + return removed; + } + + private boolean decrementCounter(HashMap map) { + boolean removed = false; + + for (Iterator> it = map.entrySet().iterator(); + it.hasNext();) { + Map.Entry kv = it.next(); + if (kv.getValue() == 0) { + it.remove(); + removed = true; + } else { + kv.setValue(kv.getValue() - 1); + } + } + + return removed; + } + } + + public RouterAdvertisementDaemon(InterfaceParams ifParams) { + mInterface = ifParams; + mAllNodes = new InetSocketAddress(getAllNodesForScopeId(mInterface.index), 0); + mDeprecatedInfoTracker = new DeprecatedInfoTracker(); + } + + /** Build new RA.*/ + public void buildNewRa(RaParams deprecatedParams, RaParams newParams) { + synchronized (mLock) { + if (deprecatedParams != null) { + mDeprecatedInfoTracker.putPrefixes(deprecatedParams.prefixes); + mDeprecatedInfoTracker.putDnses(deprecatedParams.dnses); + } + + if (newParams != null) { + // Process information that is no longer deprecated. + mDeprecatedInfoTracker.removePrefixes(newParams.prefixes); + mDeprecatedInfoTracker.removeDnses(newParams.dnses); + } + + mRaParams = newParams; + assembleRaLocked(); + } + + maybeNotifyMulticastTransmitter(); + } + + /** Start router advertisement daemon. */ + public boolean start() { + if (!createSocket()) { + return false; + } + + mMulticastTransmitter = new MulticastTransmitter(); + mMulticastTransmitter.start(); + + mUnicastResponder = new UnicastResponder(); + mUnicastResponder.start(); + + return true; + } + + /** Stop router advertisement daemon. */ + public void stop() { + closeSocket(); + // Wake up mMulticastTransmitter thread to interrupt a potential 1 day sleep before + // the thread's termination. + maybeNotifyMulticastTransmitter(); + mMulticastTransmitter = null; + mUnicastResponder = null; + } + + @GuardedBy("mLock") + private void assembleRaLocked() { + final ByteBuffer ra = ByteBuffer.wrap(mRA); + ra.order(ByteOrder.BIG_ENDIAN); + + final boolean haveRaParams = (mRaParams != null); + boolean shouldSendRA = false; + + try { + putHeader(ra, haveRaParams && mRaParams.hasDefaultRoute, + haveRaParams ? mRaParams.hopLimit : RaParams.DEFAULT_HOPLIMIT); + putSlla(ra, mInterface.macAddr.toByteArray()); + mRaLength = ra.position(); + + // https://tools.ietf.org/html/rfc5175#section-4 says: + // + // "MUST NOT be added to a Router Advertisement message + // if no flags in the option are set." + // + // putExpandedFlagsOption(ra); + + if (haveRaParams) { + putMtu(ra, mRaParams.mtu); + mRaLength = ra.position(); + + for (IpPrefix ipp : mRaParams.prefixes) { + putPio(ra, ipp, DEFAULT_LIFETIME, DEFAULT_LIFETIME); + mRaLength = ra.position(); + shouldSendRA = true; + } + + if (mRaParams.dnses.size() > 0) { + putRdnss(ra, mRaParams.dnses, DEFAULT_LIFETIME); + mRaLength = ra.position(); + shouldSendRA = true; + } + } + + for (IpPrefix ipp : mDeprecatedInfoTracker.getPrefixes()) { + putPio(ra, ipp, 0, 0); + mRaLength = ra.position(); + shouldSendRA = true; + } + + final Set deprecatedDnses = mDeprecatedInfoTracker.getDnses(); + if (!deprecatedDnses.isEmpty()) { + putRdnss(ra, deprecatedDnses, 0); + mRaLength = ra.position(); + shouldSendRA = true; + } + } catch (BufferOverflowException e) { + // The packet up to mRaLength is valid, since it has been updated + // progressively as the RA was built. Log an error, and continue + // on as best as possible. + Log.e(TAG, "Could not construct new RA: " + e); + } + + // We have nothing worth announcing; indicate as much to maybeSendRA(). + if (!shouldSendRA) { + mRaLength = 0; + } + } + + private void maybeNotifyMulticastTransmitter() { + final MulticastTransmitter m = mMulticastTransmitter; + if (m != null) { + m.hup(); + } + } + + private static byte asByte(int value) { + return (byte) value; + } + private static short asShort(int value) { + return (short) value; + } + + private static void putHeader(ByteBuffer ra, boolean hasDefaultRoute, byte hopLimit) { + // RFC 4191 "high" preference, iff. advertising a default route. + final byte flags = hasDefaultRoute ? asByte(0x08) : asByte(0); + final short lifetime = hasDefaultRoute ? asShort(DEFAULT_LIFETIME) : asShort(0); + final Icmpv6Header icmpv6Header = + new Icmpv6Header(asByte(ICMPV6_ROUTER_ADVERTISEMENT) /* type */, + asByte(0) /* code */, asShort(0) /* checksum */); + final RaHeader raHeader = new RaHeader(hopLimit, flags, lifetime, 0 /* reachableTime */, + 0 /* retransTimer */); + icmpv6Header.writeToByteBuffer(ra); + raHeader.writeToByteBuffer(ra); + } + + private static void putSlla(ByteBuffer ra, byte[] slla) { + if (slla == null || slla.length != 6) { + // Only IEEE 802.3 6-byte addresses are supported. + return; + } + + final ByteBuffer sllaOption = LlaOption.build(asByte(ICMPV6_ND_OPTION_SLLA), + MacAddress.fromBytes(slla)); + ra.put(sllaOption); + } + + private static void putExpandedFlagsOption(ByteBuffer ra) { + /** + Router Advertisement Expanded Flags Option + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Bit fields available .. + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ... for assignment | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + final byte nd_option__efo = 26; + final byte efo_num_8octets = 1; + + ra.put(nd_option__efo) + .put(efo_num_8octets) + .putShort(asShort(0)) + .putInt(0); + } + + private static void putMtu(ByteBuffer ra, int mtu) { + final ByteBuffer mtuOption = MtuOption.build((mtu < IPV6_MIN_MTU) ? IPV6_MIN_MTU : mtu); + ra.put(mtuOption); + } + + private static void putPio(ByteBuffer ra, IpPrefix ipp, + int validTime, int preferredTime) { + final int prefixLength = ipp.getPrefixLength(); + if (prefixLength != 64) { + return; + } + + if (validTime < 0) validTime = 0; + if (preferredTime < 0) preferredTime = 0; + if (preferredTime > validTime) preferredTime = validTime; + + final ByteBuffer pioOption = PrefixInformationOption.build(ipp, + asByte(PIO_FLAG_ON_LINK | PIO_FLAG_AUTONOMOUS), validTime, preferredTime); + ra.put(pioOption); + } + + private static void putRio(ByteBuffer ra, IpPrefix ipp) { + /** + Route Information Option + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Prefix Length |Resvd|Prf|Resvd| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Route Lifetime | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Prefix (Variable Length) | + . . + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + final int prefixLength = ipp.getPrefixLength(); + if (prefixLength > 64) { + return; + } + final byte nd_option_rio = 24; + final byte rio_num_8octets = asByte( + (prefixLength == 0) ? 1 : (prefixLength <= 8) ? 2 : 3); + + final byte[] addr = ipp.getAddress().getAddress(); + ra.put(nd_option_rio) + .put(rio_num_8octets) + .put(asByte(prefixLength)) + .put(asByte(0x18)) + .putInt(DEFAULT_LIFETIME); + + // Rely upon an IpPrefix's address being properly zeroed. + if (prefixLength > 0) { + ra.put(addr, 0, (prefixLength <= 64) ? 8 : 16); + } + } + + private static void putRdnss(ByteBuffer ra, Set dnses, int lifetime) { + final HashSet filteredDnses = new HashSet<>(); + for (Inet6Address dns : dnses) { + if ((new LinkAddress(dns, RFC7421_PREFIX_LENGTH)).isGlobalPreferred()) { + filteredDnses.add(dns); + } + } + if (filteredDnses.isEmpty()) return; + + final Inet6Address[] dnsesArray = + filteredDnses.toArray(new Inet6Address[filteredDnses.size()]); + final ByteBuffer rdnssOption = RdnssOption.build(lifetime, dnsesArray); + // NOTE: If the full of list DNS servers doesn't fit in the packet, + // this code will cause a buffer overflow and the RA won't include + // this instance of the option at all. + // + // TODO: Consider looking at ra.remaining() to determine how many + // DNS servers will fit, and adding only those. + ra.put(rdnssOption); + } + + private boolean createSocket() { + final int send_timout_ms = 300; + + final int oldTag = TrafficStats.getAndSetThreadStatsTag(TAG_SYSTEM_NEIGHBOR); + try { + mSocket = Os.socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6); + // Setting SNDTIMEO is purely for defensive purposes. + Os.setsockoptTimeval( + mSocket, SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(send_timout_ms)); + SocketUtils.bindSocketToInterface(mSocket, mInterface.name); + TetheringUtils.setupRaSocket(mSocket, mInterface.index); + } catch (ErrnoException | IOException e) { + Log.e(TAG, "Failed to create RA daemon socket: " + e); + return false; + } finally { + TrafficStats.setThreadStatsTag(oldTag); + } + + return true; + } + + private void closeSocket() { + if (mSocket != null) { + try { + SocketUtils.closeSocket(mSocket); + } catch (IOException ignored) { } + } + mSocket = null; + } + + private boolean isSocketValid() { + final FileDescriptor s = mSocket; + return (s != null) && s.valid(); + } + + private boolean isSuitableDestination(InetSocketAddress dest) { + if (mAllNodes.equals(dest)) { + return true; + } + + final InetAddress destip = dest.getAddress(); + return (destip instanceof Inet6Address) + && destip.isLinkLocalAddress() + && (((Inet6Address) destip).getScopeId() == mInterface.index); + } + + private void maybeSendRA(InetSocketAddress dest) { + if (dest == null || !isSuitableDestination(dest)) { + dest = mAllNodes; + } + + try { + synchronized (mLock) { + if (mRaLength < ICMPV6_RA_HEADER_LEN) { + // No actual RA to send. + return; + } + Os.sendto(mSocket, mRA, 0, mRaLength, 0, dest); + } + Log.d(TAG, "RA sendto " + dest.getAddress().getHostAddress()); + } catch (ErrnoException | SocketException e) { + if (isSocketValid()) { + Log.e(TAG, "sendto error: " + e); + } + } + } + + private final class UnicastResponder extends Thread { + private final InetSocketAddress mSolicitor = new InetSocketAddress(0); + // The recycled buffer for receiving Router Solicitations from clients. + // If the RS is larger than IPV6_MIN_MTU the packets are truncated. + // This is fine since currently only byte 0 is examined anyway. + private final byte[] mSolicitation = new byte[IPV6_MIN_MTU]; + + @Override + public void run() { + while (isSocketValid()) { + try { + // Blocking receive. + final int rval = Os.recvfrom( + mSocket, mSolicitation, 0, mSolicitation.length, 0, mSolicitor); + // Do the least possible amount of validation. + if (rval < 1 || mSolicitation[0] != asByte(ICMPV6_ROUTER_SOLICITATION)) { + continue; + } + } catch (ErrnoException | SocketException e) { + if (isSocketValid()) { + Log.e(TAG, "recvfrom error: " + e); + } + continue; + } + + maybeSendRA(mSolicitor); + } + } + } + + // TODO: Consider moving this to run on a provided Looper as a Handler, + // with WakeupMessage-style messages providing the timer driven input. + private final class MulticastTransmitter extends Thread { + private final Random mRandom = new Random(); + private final AtomicInteger mUrgentAnnouncements = new AtomicInteger(0); + + @Override + public void run() { + while (isSocketValid()) { + try { + Thread.sleep(getNextMulticastTransmitDelayMs()); + } catch (InterruptedException ignored) { + // Stop sleeping, immediately send an RA, and continue. + } + + maybeSendRA(mAllNodes); + synchronized (mLock) { + if (mDeprecatedInfoTracker.decrementCounters()) { + // At least one deprecated PIO has been removed; + // reassemble the RA. + assembleRaLocked(); + } + } + } + } + + public void hup() { + // Set to one fewer that the desired number, because as soon as + // the thread interrupt is processed we immediately send an RA + // and mUrgentAnnouncements is not examined until the subsequent + // sleep interval computation (i.e. this way we send 3 and not 4). + mUrgentAnnouncements.set(MAX_URGENT_RTR_ADVERTISEMENTS - 1); + interrupt(); + } + + private int getNextMulticastTransmitDelaySec() { + boolean deprecationInProgress = false; + synchronized (mLock) { + if (mRaLength < ICMPV6_RA_HEADER_LEN) { + // No actual RA to send; just sleep for 1 day. + return DAY_IN_SECONDS; + } + deprecationInProgress = !mDeprecatedInfoTracker.isEmpty(); + } + + final int urgentPending = mUrgentAnnouncements.getAndDecrement(); + if ((urgentPending > 0) || deprecationInProgress) { + return MIN_DELAY_BETWEEN_RAS_SEC; + } + + return MIN_RTR_ADV_INTERVAL_SEC + mRandom.nextInt( + MAX_RTR_ADV_INTERVAL_SEC - MIN_RTR_ADV_INTERVAL_SEC); + } + + private long getNextMulticastTransmitDelayMs() { + return 1000 * (long) getNextMulticastTransmitDelaySec(); + } + } +} diff --git a/Tethering/src/android/net/util/InterfaceSet.java b/Tethering/src/android/net/util/InterfaceSet.java new file mode 100644 index 0000000000..758978711b --- /dev/null +++ b/Tethering/src/android/net/util/InterfaceSet.java @@ -0,0 +1,52 @@ +/* + * 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.util; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.StringJoiner; + + +/** + * @hide + */ +public class InterfaceSet { + public final Set ifnames; + + public InterfaceSet(String... names) { + final Set nameSet = new HashSet<>(); + for (String name : names) { + if (name != null) nameSet.add(name); + } + ifnames = Collections.unmodifiableSet(nameSet); + } + + @Override + public String toString() { + final StringJoiner sj = new StringJoiner(",", "[", "]"); + for (String ifname : ifnames) sj.add(ifname); + return sj.toString(); + } + + @Override + public boolean equals(Object obj) { + return obj != null + && obj instanceof InterfaceSet + && ifnames.equals(((InterfaceSet) obj).ifnames); + } +} diff --git a/Tethering/src/android/net/util/PrefixUtils.java b/Tethering/src/android/net/util/PrefixUtils.java new file mode 100644 index 0000000000..f203e9995f --- /dev/null +++ b/Tethering/src/android/net/util/PrefixUtils.java @@ -0,0 +1,88 @@ +/* + * 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.util; + +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + + +/** + * @hide + */ +public class PrefixUtils { + private static final IpPrefix[] MIN_NON_FORWARDABLE_PREFIXES = { + pfx("127.0.0.0/8"), // IPv4 loopback + pfx("169.254.0.0/16"), // IPv4 link-local, RFC3927#section-8 + pfx("::/3"), + pfx("fe80::/64"), // IPv6 link-local + pfx("fc00::/7"), // IPv6 ULA + pfx("ff02::/8"), // IPv6 link-local multicast + }; + + public static final IpPrefix DEFAULT_WIFI_P2P_PREFIX = pfx("192.168.49.0/24"); + + /** Get non forwardable prefixes. */ + public static Set getNonForwardablePrefixes() { + final HashSet prefixes = new HashSet<>(); + addNonForwardablePrefixes(prefixes); + return prefixes; + } + + /** Add non forwardable prefixes. */ + public static void addNonForwardablePrefixes(Set prefixes) { + Collections.addAll(prefixes, MIN_NON_FORWARDABLE_PREFIXES); + } + + /** Get local prefixes from |lp|. */ + public static Set localPrefixesFrom(LinkProperties lp) { + final HashSet localPrefixes = new HashSet<>(); + if (lp == null) return localPrefixes; + + for (LinkAddress addr : lp.getAllLinkAddresses()) { + if (addr.getAddress().isLinkLocalAddress()) continue; + localPrefixes.add(asIpPrefix(addr)); + } + // TODO: Add directly-connected routes as well (ones from which we did + // not also form a LinkAddress)? + + return localPrefixes; + } + + /** Convert LinkAddress |addr| to IpPrefix. */ + public static IpPrefix asIpPrefix(LinkAddress addr) { + return new IpPrefix(addr.getAddress(), addr.getPrefixLength()); + } + + /** Convert InetAddress |ip| to IpPrefix. */ + public static IpPrefix ipAddressAsPrefix(InetAddress ip) { + final int bitLength = (ip instanceof Inet4Address) + ? NetworkConstants.IPV4_ADDR_BITS + : NetworkConstants.IPV6_ADDR_BITS; + return new IpPrefix(ip, bitLength); + } + + private static IpPrefix pfx(String prefixStr) { + return new IpPrefix(prefixStr); + } +} diff --git a/Tethering/src/android/net/util/TetheringMessageBase.java b/Tethering/src/android/net/util/TetheringMessageBase.java new file mode 100644 index 0000000000..29c0a817b6 --- /dev/null +++ b/Tethering/src/android/net/util/TetheringMessageBase.java @@ -0,0 +1,25 @@ +/* + * 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.util; + +/** + * This class defines Message.what base addresses for various state machine. + */ +public class TetheringMessageBase { + public static final int BASE_MAIN_SM = 0; + public static final int BASE_IPSERVER = 100; + +} diff --git a/Tethering/src/android/net/util/TetheringUtils.java b/Tethering/src/android/net/util/TetheringUtils.java new file mode 100644 index 0000000000..29900d9beb --- /dev/null +++ b/Tethering/src/android/net/util/TetheringUtils.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net.util; + +import android.net.TetherStatsParcel; +import android.net.TetheringRequestParcel; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.networkstack.tethering.TetherStatsValue; + +import java.io.FileDescriptor; +import java.net.Inet6Address; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Objects; + +/** + * The classes and the methods for tethering utilization. + * + * {@hide} + */ +public class TetheringUtils { + static { + System.loadLibrary("tetherutilsjni"); + } + + public static final byte[] ALL_NODES = new byte[] { + (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 + }; + + /** + * Configures a socket for receiving and sending ICMPv6 neighbor advertisments. + * @param fd the socket's {@link FileDescriptor}. + */ + public static native void setupNaSocket(FileDescriptor fd) + throws SocketException; + + /** + * Configures a socket for receiving and sending ICMPv6 neighbor solicitations. + * @param fd the socket's {@link FileDescriptor}. + */ + public static native void setupNsSocket(FileDescriptor fd) + throws SocketException; + + /** + * The object which records offload Tx/Rx forwarded bytes/packets. + * TODO: Replace the inner class ForwardedStats of class OffloadHardwareInterface with + * this class as well. + */ + public static class ForwardedStats { + public final long rxBytes; + public final long rxPackets; + public final long txBytes; + public final long txPackets; + + public ForwardedStats() { + rxBytes = 0; + rxPackets = 0; + txBytes = 0; + txPackets = 0; + } + + public ForwardedStats(long rxBytes, long txBytes) { + this.rxBytes = rxBytes; + this.rxPackets = 0; + this.txBytes = txBytes; + this.txPackets = 0; + } + + public ForwardedStats(long rxBytes, long rxPackets, long txBytes, long txPackets) { + this.rxBytes = rxBytes; + this.rxPackets = rxPackets; + this.txBytes = txBytes; + this.txPackets = txPackets; + } + + public ForwardedStats(@NonNull TetherStatsParcel tetherStats) { + rxBytes = tetherStats.rxBytes; + rxPackets = tetherStats.rxPackets; + txBytes = tetherStats.txBytes; + txPackets = tetherStats.txPackets; + } + + public ForwardedStats(@NonNull TetherStatsValue tetherStats) { + rxBytes = tetherStats.rxBytes; + rxPackets = tetherStats.rxPackets; + txBytes = tetherStats.txBytes; + txPackets = tetherStats.txPackets; + } + + public ForwardedStats(@NonNull ForwardedStats other) { + rxBytes = other.rxBytes; + rxPackets = other.rxPackets; + txBytes = other.txBytes; + txPackets = other.txPackets; + } + + /** Add Tx/Rx bytes/packets and return the result as a new object. */ + @NonNull + public ForwardedStats add(@NonNull ForwardedStats other) { + return new ForwardedStats(rxBytes + other.rxBytes, rxPackets + other.rxPackets, + txBytes + other.txBytes, txPackets + other.txPackets); + } + + /** Subtract Tx/Rx bytes/packets and return the result as a new object. */ + @NonNull + public ForwardedStats subtract(@NonNull ForwardedStats other) { + // TODO: Perhaps throw an exception if any negative difference value just in case. + final long rxBytesDiff = Math.max(rxBytes - other.rxBytes, 0); + final long rxPacketsDiff = Math.max(rxPackets - other.rxPackets, 0); + final long txBytesDiff = Math.max(txBytes - other.txBytes, 0); + final long txPacketsDiff = Math.max(txPackets - other.txPackets, 0); + return new ForwardedStats(rxBytesDiff, rxPacketsDiff, txBytesDiff, txPacketsDiff); + } + + /** Returns the string representation of this object. */ + @NonNull + public String toString() { + return String.format("ForwardedStats(rxb: %d, rxp: %d, txb: %d, txp: %d)", rxBytes, + rxPackets, txBytes, txPackets); + } + } + + /** + * Configures a socket for receiving ICMPv6 router solicitations and sending advertisements. + * @param fd the socket's {@link FileDescriptor}. + * @param ifIndex the interface index. + */ + public static native void setupRaSocket(FileDescriptor fd, int ifIndex) + throws SocketException; + + /** + * Read s as an unsigned 16-bit integer. + */ + public static int uint16(short s) { + return s & 0xffff; + } + + /** Check whether two TetheringRequestParcels are the same. */ + public static boolean isTetheringRequestEquals(final TetheringRequestParcel request, + final TetheringRequestParcel otherRequest) { + if (request == otherRequest) return true; + + return request != null && otherRequest != null + && request.tetheringType == otherRequest.tetheringType + && Objects.equals(request.localIPv4Address, otherRequest.localIPv4Address) + && Objects.equals(request.staticClientAddress, otherRequest.staticClientAddress) + && request.exemptFromEntitlementCheck == otherRequest.exemptFromEntitlementCheck + && request.showProvisioningUi == otherRequest.showProvisioningUi + && request.connectivityScope == otherRequest.connectivityScope; + } + + /** Get inet6 address for all nodes given scope ID. */ + public static Inet6Address getAllNodesForScopeId(int scopeId) { + try { + return Inet6Address.getByAddress("ff02::1", ALL_NODES, scopeId); + } catch (UnknownHostException uhe) { + Log.wtf("TetheringUtils", "Failed to construct Inet6Address from " + + Arrays.toString(ALL_NODES) + " and scopedId " + scopeId); + return null; + } + } +} diff --git a/Tethering/src/android/net/util/VersionedBroadcastListener.java b/Tethering/src/android/net/util/VersionedBroadcastListener.java new file mode 100644 index 0000000000..e2804abd75 --- /dev/null +++ b/Tethering/src/android/net/util/VersionedBroadcastListener.java @@ -0,0 +1,106 @@ +/* + * 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.util; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.util.Log; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + + +/** + * A utility class that runs the provided callback on the provided handler when + * intents matching the provided filter arrive. Intents received by a stale + * receiver are safely ignored. + * + * Calls to startListening() and stopListening() must happen on the same thread. + * + * @hide + */ +public class VersionedBroadcastListener { + private static final boolean DBG = false; + + private final String mTag; + private final Context mContext; + private final Handler mHandler; + private final IntentFilter mFilter; + private final Consumer mCallback; + private final AtomicInteger mGenerationNumber; + private BroadcastReceiver mReceiver; + + public VersionedBroadcastListener(String tag, Context ctx, Handler handler, + IntentFilter filter, Consumer callback) { + mTag = tag; + mContext = ctx; + mHandler = handler; + mFilter = filter; + mCallback = callback; + mGenerationNumber = new AtomicInteger(0); + } + + /** Start listening to intent broadcast. */ + public void startListening() { + if (DBG) Log.d(mTag, "startListening"); + if (mReceiver != null) return; + + mReceiver = new Receiver(mTag, mGenerationNumber, mCallback); + mContext.registerReceiver(mReceiver, mFilter, null, mHandler); + } + + /** Stop listening to intent broadcast. */ + public void stopListening() { + if (DBG) Log.d(mTag, "stopListening"); + if (mReceiver == null) return; + + mGenerationNumber.incrementAndGet(); + mContext.unregisterReceiver(mReceiver); + mReceiver = null; + } + + private static class Receiver extends BroadcastReceiver { + public final String tag; + public final AtomicInteger atomicGenerationNumber; + public final Consumer callback; + // Used to verify this receiver is still current. + public final int generationNumber; + + Receiver(String tag, AtomicInteger atomicGenerationNumber, Consumer callback) { + this.tag = tag; + this.atomicGenerationNumber = atomicGenerationNumber; + this.callback = callback; + generationNumber = atomicGenerationNumber.incrementAndGet(); + } + + @Override + public void onReceive(Context context, Intent intent) { + final int currentGenerationNumber = atomicGenerationNumber.get(); + + if (DBG) { + Log.d(tag, "receiver generationNumber=" + generationNumber + + ", current generationNumber=" + currentGenerationNumber); + } + if (generationNumber != currentGenerationNumber) return; + + callback.accept(intent); + } + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java new file mode 100644 index 0000000000..8adcbd9ff9 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java @@ -0,0 +1,1577 @@ +/* + * 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.networkstack.tethering; + +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.ROAMING_NO; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; +import static android.net.NetworkStats.UID_TETHERING; +import static android.net.ip.ConntrackMonitor.ConntrackEvent; +import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED; +import static android.system.OsConstants.ETH_P_IP; +import static android.system.OsConstants.ETH_P_IPV6; + +import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM; +import static com.android.networkstack.tethering.BpfUtils.UPSTREAM; +import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS; + +import android.app.usage.NetworkStatsManager; +import android.net.INetd; +import android.net.LinkProperties; +import android.net.MacAddress; +import android.net.NetworkStats; +import android.net.NetworkStats.Entry; +import android.net.TetherOffloadRuleParcel; +import android.net.ip.ConntrackMonitor; +import android.net.ip.ConntrackMonitor.ConntrackEventConsumer; +import android.net.ip.IpServer; +import android.net.netlink.NetlinkConstants; +import android.net.netstats.provider.NetworkStatsProvider; +import android.net.util.InterfaceParams; +import android.net.util.SharedLog; +import android.net.util.TetheringUtils.ForwardedStats; +import android.os.Handler; +import android.system.ErrnoException; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.modules.utils.build.SdkLevel; +import com.android.net.module.util.NetworkStackConstants; +import com.android.net.module.util.Struct; +import com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * This coordinator is responsible for providing BPF offload relevant functionality. + * - Get tethering stats. + * - Set data limit. + * - Set global alert. + * - Add/remove forwarding rules. + * + * @hide + */ +public class BpfCoordinator { + // Ensure the JNI code is loaded. In production this will already have been loaded by + // TetherService, but for tests it needs to be either loaded here or loaded by every test. + // TODO: is there a better way? + static { + System.loadLibrary("tetherutilsjni"); + } + + private static final String TAG = BpfCoordinator.class.getSimpleName(); + private static final int DUMP_TIMEOUT_MS = 10_000; + private static final MacAddress NULL_MAC_ADDRESS = MacAddress.fromString( + "00:00:00:00:00:00"); + private static final String TETHER_DOWNSTREAM4_MAP_PATH = makeMapPath(DOWNSTREAM, 4); + private static final String TETHER_UPSTREAM4_MAP_PATH = makeMapPath(UPSTREAM, 4); + private static final String TETHER_DOWNSTREAM6_FS_PATH = makeMapPath(DOWNSTREAM, 6); + private static final String TETHER_UPSTREAM6_FS_PATH = makeMapPath(UPSTREAM, 6); + private static final String TETHER_STATS_MAP_PATH = makeMapPath("stats"); + private static final String TETHER_LIMIT_MAP_PATH = makeMapPath("limit"); + private static final String TETHER_ERROR_MAP_PATH = makeMapPath("error"); + private static final String TETHER_DEV_MAP_PATH = makeMapPath("dev"); + + /** The names of all the BPF counters defined in bpf_tethering.h. */ + public static final String[] sBpfCounterNames = getBpfCounterNames(); + + private static String makeMapPath(String which) { + return "/sys/fs/bpf/tethering/map_offload_tether_" + which + "_map"; + } + + private static String makeMapPath(boolean downstream, int ipVersion) { + return makeMapPath((downstream ? "downstream" : "upstream") + ipVersion); + } + + @VisibleForTesting + enum StatsType { + STATS_PER_IFACE, + STATS_PER_UID, + } + + @NonNull + private final Handler mHandler; + @NonNull + private final INetd mNetd; + @NonNull + private final SharedLog mLog; + @NonNull + private final Dependencies mDeps; + @NonNull + private final ConntrackMonitor mConntrackMonitor; + @Nullable + private final BpfTetherStatsProvider mStatsProvider; + @NonNull + private final BpfCoordinatorShim mBpfCoordinatorShim; + @NonNull + private final BpfConntrackEventConsumer mBpfConntrackEventConsumer; + + // True if BPF offload is supported, false otherwise. The BPF offload could be disabled by + // a runtime resource overlay package or device configuration. This flag is only initialized + // in the constructor because it is hard to unwind all existing change once device + // configuration is changed. Especially the forwarding rules. Keep the same setting + // to make it simpler. See also TetheringConfiguration. + private final boolean mIsBpfEnabled; + + // Tracks whether BPF tethering is started or not. This is set by tethering before it + // starts the first IpServer and is cleared by tethering shortly before the last IpServer + // is stopped. Note that rule updates (especially deletions, but sometimes additions as + // well) may arrive when this is false. If they do, they must be communicated to netd. + // Changes in data limits may also arrive when this is false, and if they do, they must + // also be communicated to netd. + private boolean mPollingStarted = false; + + // Tracking remaining alert quota. Unlike limit quota is subject to interface, the alert + // quota is interface independent and global for tether offload. + private long mRemainingAlertQuota = QUOTA_UNLIMITED; + + // Maps upstream interface index to offloaded traffic statistics. + // Always contains the latest total bytes/packets, since each upstream was started, received + // from the BPF maps for each interface. + private final SparseArray mStats = new SparseArray<>(); + + // Maps upstream interface names to interface quotas. + // Always contains the latest value received from the framework for each interface, regardless + // of whether offload is currently running (or is even supported) on that interface. Only + // includes interfaces that have a quota set. Note that this map is used for storing the quota + // which is set from the service. Because the service uses the interface name to present the + // interface, this map uses the interface name to be the mapping index. + private final HashMap mInterfaceQuotas = new HashMap<>(); + + // Maps upstream interface index to interface names. + // Store all interface name since boot. Used for lookup what interface name it is from the + // tether stats got from netd because netd reports interface index to present an interface. + // TODO: Remove the unused interface name. + private final SparseArray mInterfaceNames = new SparseArray<>(); + + // Map of downstream rule maps. Each of these maps represents the IPv6 forwarding rules for a + // given downstream. Each map: + // - Is owned by the IpServer that is responsible for that downstream. + // - Must only be modified by that IpServer. + // - Is created when the IpServer adds its first rule, and deleted when the IpServer deletes + // its last rule (or clears its rules). + // TODO: Perhaps seal the map and rule operations which communicates with netd into a class. + // TODO: Does this need to be a LinkedHashMap or can it just be a HashMap? Also, could it be + // a ConcurrentHashMap, in order to avoid the copies in tetherOffloadRuleClear + // and tetherOffloadRuleUpdate? + // TODO: Perhaps use one-dimensional map and access specific downstream rules via downstream + // index. For doing that, IpServer must guarantee that it always has a valid IPv6 downstream + // interface index while calling function to clear all rules. IpServer may be calling clear + // rules function without a valid IPv6 downstream interface index even if it may have one + // before. IpServer would need to call getInterfaceParams() in the constructor instead of when + // startIpv6() is called, and make mInterfaceParams final. + private final HashMap> + mIpv6ForwardingRules = new LinkedHashMap<>(); + + // Map of downstream client maps. Each of these maps represents the IPv4 clients for a given + // downstream. Needed to build IPv4 forwarding rules when conntrack events are received. + // Each map: + // - Is owned by the IpServer that is responsible for that downstream. + // - Must only be modified by that IpServer. + // - Is created when the IpServer adds its first client, and deleted when the IpServer deletes + // its last client. + // Note that relying on the client address for finding downstream is okay for now because the + // client address is unique. See PrivateAddressCoordinator#requestDownstreamAddress. + // TODO: Refactor if any possible that the client address is not unique. + private final HashMap> + mTetherClients = new HashMap<>(); + + // Set for which downstream is monitoring the conntrack netlink message. + private final Set mMonitoringIpServers = new HashSet<>(); + + // Map of upstream interface IPv4 address to interface index. + // TODO: consider making the key to be unique because the upstream address is not unique. It + // is okay for now because there have only one upstream generally. + private final HashMap mIpv4UpstreamIndices = new HashMap<>(); + + // Map for upstream and downstream pair. + private final HashMap> mForwardingPairs = new HashMap<>(); + + // Set for upstream and downstream device map. Used for caching BPF dev map status and + // reduce duplicate adding or removing map operations. Use LinkedHashSet because the test + // BpfCoordinatorTest needs predictable iteration order. + private final Set mDeviceMapSet = new LinkedHashSet<>(); + + // Runnable that used by scheduling next polling of stats. + private final Runnable mScheduledPollingTask = () -> { + updateForwardedStats(); + maybeSchedulePollingStats(); + }; + + // TODO: add BpfMap retrieving function. + @VisibleForTesting + public abstract static class Dependencies { + /** Get handler. */ + @NonNull public abstract Handler getHandler(); + + /** Get netd. */ + @NonNull public abstract INetd getNetd(); + + /** Get network stats manager. */ + @NonNull public abstract NetworkStatsManager getNetworkStatsManager(); + + /** Get shared log. */ + @NonNull public abstract SharedLog getSharedLog(); + + /** Get tethering configuration. */ + @Nullable public abstract TetheringConfiguration getTetherConfig(); + + /** Get conntrack monitor. */ + @NonNull public ConntrackMonitor getConntrackMonitor(ConntrackEventConsumer consumer) { + return new ConntrackMonitor(getHandler(), getSharedLog(), consumer); + } + + /** Get interface information for a given interface. */ + @NonNull public InterfaceParams getInterfaceParams(String ifName) { + return InterfaceParams.getByName(ifName); + } + + /** + * Check OS Build at least S. + * + * TODO: move to BpfCoordinatorShim once the test doesn't need the mocked OS build for + * testing different code flows concurrently. + */ + public boolean isAtLeastS() { + // TODO: consider using ShimUtils.isAtLeastS. + return SdkLevel.isAtLeastS(); + } + + /** Get downstream4 BPF map. */ + @Nullable public BpfMap getBpfDownstream4Map() { + if (!isAtLeastS()) return null; + try { + return new BpfMap<>(TETHER_DOWNSTREAM4_MAP_PATH, + BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class); + } catch (ErrnoException e) { + Log.e(TAG, "Cannot create downstream4 map: " + e); + return null; + } + } + + /** Get upstream4 BPF map. */ + @Nullable public BpfMap getBpfUpstream4Map() { + if (!isAtLeastS()) return null; + try { + return new BpfMap<>(TETHER_UPSTREAM4_MAP_PATH, + BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class); + } catch (ErrnoException e) { + Log.e(TAG, "Cannot create upstream4 map: " + e); + return null; + } + } + + /** Get downstream6 BPF map. */ + @Nullable public BpfMap getBpfDownstream6Map() { + if (!isAtLeastS()) return null; + try { + return new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, + BpfMap.BPF_F_RDWR, TetherDownstream6Key.class, Tether6Value.class); + } catch (ErrnoException e) { + Log.e(TAG, "Cannot create downstream6 map: " + e); + return null; + } + } + + /** Get upstream6 BPF map. */ + @Nullable public BpfMap getBpfUpstream6Map() { + if (!isAtLeastS()) return null; + try { + return new BpfMap<>(TETHER_UPSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR, + TetherUpstream6Key.class, Tether6Value.class); + } catch (ErrnoException e) { + Log.e(TAG, "Cannot create upstream6 map: " + e); + return null; + } + } + + /** Get stats BPF map. */ + @Nullable public BpfMap getBpfStatsMap() { + if (!isAtLeastS()) return null; + try { + return new BpfMap<>(TETHER_STATS_MAP_PATH, + BpfMap.BPF_F_RDWR, TetherStatsKey.class, TetherStatsValue.class); + } catch (ErrnoException e) { + Log.e(TAG, "Cannot create stats map: " + e); + return null; + } + } + + /** Get limit BPF map. */ + @Nullable public BpfMap getBpfLimitMap() { + if (!isAtLeastS()) return null; + try { + return new BpfMap<>(TETHER_LIMIT_MAP_PATH, + BpfMap.BPF_F_RDWR, TetherLimitKey.class, TetherLimitValue.class); + } catch (ErrnoException e) { + Log.e(TAG, "Cannot create limit map: " + e); + return null; + } + } + + /** Get dev BPF map. */ + @Nullable public BpfMap getBpfDevMap() { + if (!isAtLeastS()) return null; + try { + return new BpfMap<>(TETHER_DEV_MAP_PATH, + BpfMap.BPF_F_RDWR, TetherDevKey.class, TetherDevValue.class); + } catch (ErrnoException e) { + Log.e(TAG, "Cannot create dev map: " + e); + return null; + } + } + } + + @VisibleForTesting + public BpfCoordinator(@NonNull Dependencies deps) { + mDeps = deps; + mHandler = mDeps.getHandler(); + mNetd = mDeps.getNetd(); + mLog = mDeps.getSharedLog().forSubComponent(TAG); + mIsBpfEnabled = isBpfEnabled(); + + // The conntrack consummer needs to be initialized in BpfCoordinator constructor because it + // have to access the data members of BpfCoordinator which is not a static class. The + // consumer object is also needed for initializing the conntrack monitor which may be + // mocked for testing. + mBpfConntrackEventConsumer = new BpfConntrackEventConsumer(); + mConntrackMonitor = mDeps.getConntrackMonitor(mBpfConntrackEventConsumer); + + BpfTetherStatsProvider provider = new BpfTetherStatsProvider(); + try { + mDeps.getNetworkStatsManager().registerNetworkStatsProvider( + getClass().getSimpleName(), provider); + } catch (RuntimeException e) { + // TODO: Perhaps not allow to use BPF offload because the reregistration failure + // implied that no data limit could be applies on a metered upstream if any. + Log.wtf(TAG, "Cannot register offload stats provider: " + e); + provider = null; + } + mStatsProvider = provider; + + mBpfCoordinatorShim = BpfCoordinatorShim.getBpfCoordinatorShim(deps); + if (!mBpfCoordinatorShim.isInitialized()) { + mLog.e("Bpf shim not initialized"); + } + } + + /** + * Start BPF tethering offload stats polling when the first upstream is started. + * Note that this can be only called on handler thread. + * TODO: Perhaps check BPF support before starting. + * TODO: Start the stats polling only if there is any client on the downstream. + */ + public void startPolling() { + if (mPollingStarted) return; + + if (!isUsingBpf()) { + mLog.i("BPF is not using"); + return; + } + + mPollingStarted = true; + maybeSchedulePollingStats(); + + mLog.i("Polling started"); + } + + /** + * Stop BPF tethering offload stats polling. + * The data limit cleanup and the tether stats maps cleanup are not implemented here. + * These cleanups rely on all IpServers calling #tetherOffloadRuleRemove. After the + * last rule is removed from the upstream, #tetherOffloadRuleRemove does the cleanup + * functionality. + * Note that this can be only called on handler thread. + */ + public void stopPolling() { + if (!mPollingStarted) return; + + // Stop scheduled polling tasks and poll the latest stats from BPF maps. + if (mHandler.hasCallbacks(mScheduledPollingTask)) { + mHandler.removeCallbacks(mScheduledPollingTask); + } + updateForwardedStats(); + mPollingStarted = false; + + mLog.i("Polling stopped"); + } + + private boolean isUsingBpf() { + return mIsBpfEnabled && mBpfCoordinatorShim.isInitialized(); + } + + /** + * Start conntrack message monitoring. + * Note that this can be only called on handler thread. + * + * TODO: figure out a better logging for non-interesting conntrack message. + * For example, the following logging is an IPCTNL_MSG_CT_GET message but looks scary. + * +---------------------------------------------------------------------------+ + * | ERROR unparsable netlink msg: 1400000001010103000000000000000002000000 | + * +------------------+--------------------------------------------------------+ + * | | struct nlmsghdr | + * | 14000000 | length = 20 | + * | 0101 | type = NFNL_SUBSYS_CTNETLINK << 8 | IPCTNL_MSG_CT_GET | + * | 0103 | flags | + * | 00000000 | seqno = 0 | + * | 00000000 | pid = 0 | + * | | struct nfgenmsg | + * | 02 | nfgen_family = AF_INET | + * | 00 | version = NFNETLINK_V0 | + * | 0000 | res_id | + * +------------------+--------------------------------------------------------+ + * See NetlinkMonitor#handlePacket, NetlinkMessage#parseNfMessage. + */ + public void startMonitoring(@NonNull final IpServer ipServer) { + // TODO: Wrap conntrackMonitor starting function into mBpfCoordinatorShim. + if (!isUsingBpf() || !mDeps.isAtLeastS()) return; + + if (mMonitoringIpServers.contains(ipServer)) { + Log.wtf(TAG, "The same downstream " + ipServer.interfaceName() + + " should not start monitoring twice."); + return; + } + + if (mMonitoringIpServers.isEmpty()) { + mConntrackMonitor.start(); + mLog.i("Monitoring started"); + } + + mMonitoringIpServers.add(ipServer); + } + + /** + * Stop conntrack event monitoring. + * Note that this can be only called on handler thread. + */ + public void stopMonitoring(@NonNull final IpServer ipServer) { + // TODO: Wrap conntrackMonitor stopping function into mBpfCoordinatorShim. + if (!isUsingBpf() || !mDeps.isAtLeastS()) return; + + mMonitoringIpServers.remove(ipServer); + + if (!mMonitoringIpServers.isEmpty()) return; + + mConntrackMonitor.stop(); + mLog.i("Monitoring stopped"); + } + + /** + * Add forwarding rule. After adding the first rule on a given upstream, must add the data + * limit on the given upstream. + * Note that this can be only called on handler thread. + */ + public void tetherOffloadRuleAdd( + @NonNull final IpServer ipServer, @NonNull final Ipv6ForwardingRule rule) { + if (!isUsingBpf()) return; + + // TODO: Perhaps avoid to add a duplicate rule. + if (!mBpfCoordinatorShim.tetherOffloadRuleAdd(rule)) return; + + if (!mIpv6ForwardingRules.containsKey(ipServer)) { + mIpv6ForwardingRules.put(ipServer, new LinkedHashMap()); + } + LinkedHashMap rules = mIpv6ForwardingRules.get(ipServer); + + // Add upstream and downstream interface index to dev map. + maybeAddDevMap(rule.upstreamIfindex, rule.downstreamIfindex); + + // When the first rule is added to an upstream, setup upstream forwarding and data limit. + maybeSetLimit(rule.upstreamIfindex); + + if (!isAnyRuleFromDownstreamToUpstream(rule.downstreamIfindex, rule.upstreamIfindex)) { + final int downstream = rule.downstreamIfindex; + final int upstream = rule.upstreamIfindex; + // TODO: support upstream forwarding on non-point-to-point interfaces. + // TODO: get the MTU from LinkProperties and update the rules when it changes. + if (!mBpfCoordinatorShim.startUpstreamIpv6Forwarding(downstream, upstream, rule.srcMac, + NULL_MAC_ADDRESS, NULL_MAC_ADDRESS, NetworkStackConstants.ETHER_MTU)) { + mLog.e("Failed to enable upstream IPv6 forwarding from " + + mInterfaceNames.get(downstream) + " to " + mInterfaceNames.get(upstream)); + } + } + + // Must update the adding rule after calling #isAnyRuleOnUpstream because it needs to + // check if it is about adding a first rule for a given upstream. + rules.put(rule.address, rule); + } + + /** + * Remove forwarding rule. After removing the last rule on a given upstream, must clear + * data limit, update the last tether stats and remove the tether stats in the BPF maps. + * Note that this can be only called on handler thread. + */ + public void tetherOffloadRuleRemove( + @NonNull final IpServer ipServer, @NonNull final Ipv6ForwardingRule rule) { + if (!isUsingBpf()) return; + + if (!mBpfCoordinatorShim.tetherOffloadRuleRemove(rule)) return; + + LinkedHashMap rules = mIpv6ForwardingRules.get(ipServer); + if (rules == null) return; + + // Must remove rules before calling #isAnyRuleOnUpstream because it needs to check if + // the last rule is removed for a given upstream. If no rule is removed, return early. + // Avoid unnecessary work on a non-existent rule which may have never been added or + // removed already. + if (rules.remove(rule.address) == null) return; + + // Remove the downstream entry if it has no more rule. + if (rules.isEmpty()) { + mIpv6ForwardingRules.remove(ipServer); + } + + // If no more rules between this upstream and downstream, stop upstream forwarding. + if (!isAnyRuleFromDownstreamToUpstream(rule.downstreamIfindex, rule.upstreamIfindex)) { + final int downstream = rule.downstreamIfindex; + final int upstream = rule.upstreamIfindex; + if (!mBpfCoordinatorShim.stopUpstreamIpv6Forwarding(downstream, upstream, + rule.srcMac)) { + mLog.e("Failed to disable upstream IPv6 forwarding from " + + mInterfaceNames.get(downstream) + " to " + mInterfaceNames.get(upstream)); + } + } + + // Do cleanup functionality if there is no more rule on the given upstream. + maybeClearLimit(rule.upstreamIfindex); + } + + /** + * Clear all forwarding rules for a given downstream. + * Note that this can be only called on handler thread. + */ + public void tetherOffloadRuleClear(@NonNull final IpServer ipServer) { + if (!isUsingBpf()) return; + + final LinkedHashMap rules = mIpv6ForwardingRules.get( + ipServer); + if (rules == null) return; + + // Need to build a rule list because the rule map may be changed in the iteration. + for (final Ipv6ForwardingRule rule : new ArrayList(rules.values())) { + tetherOffloadRuleRemove(ipServer, rule); + } + } + + /** + * Update existing forwarding rules to new upstream for a given downstream. + * Note that this can be only called on handler thread. + */ + public void tetherOffloadRuleUpdate(@NonNull final IpServer ipServer, int newUpstreamIfindex) { + if (!isUsingBpf()) return; + + final LinkedHashMap rules = mIpv6ForwardingRules.get( + ipServer); + if (rules == null) return; + + // Need to build a rule list because the rule map may be changed in the iteration. + // First remove all the old rules, then add all the new rules. This is because the upstream + // forwarding code in tetherOffloadRuleAdd cannot support rules on two upstreams at the + // same time. Deleting the rules first ensures that upstream forwarding is disabled on the + // old upstream when the last rule is removed from it, and re-enabled on the new upstream + // when the first rule is added to it. + // TODO: Once the IPv6 client processing code has moved from IpServer to BpfCoordinator, do + // something smarter. + final ArrayList rulesCopy = new ArrayList<>(rules.values()); + for (final Ipv6ForwardingRule rule : rulesCopy) { + // Remove the old rule before adding the new one because the map uses the same key for + // both rules. Reversing the processing order causes that the new rule is removed as + // unexpected. + // TODO: Add new rule first to reduce the latency which has no rule. + tetherOffloadRuleRemove(ipServer, rule); + } + for (final Ipv6ForwardingRule rule : rulesCopy) { + tetherOffloadRuleAdd(ipServer, rule.onNewUpstream(newUpstreamIfindex)); + } + } + + /** + * Add upstream name to lookup table. The lookup table is used for tether stats interface name + * lookup because the netd only reports interface index in BPF tether stats but the service + * expects the interface name in NetworkStats object. + * Note that this can be only called on handler thread. + */ + public void addUpstreamNameToLookupTable(int upstreamIfindex, @NonNull String upstreamIface) { + if (!isUsingBpf()) return; + + if (upstreamIfindex == 0 || TextUtils.isEmpty(upstreamIface)) return; + + // The same interface index to name mapping may be added by different IpServer objects or + // re-added by reconnection on the same upstream interface. Ignore the duplicate one. + final String iface = mInterfaceNames.get(upstreamIfindex); + if (iface == null) { + mInterfaceNames.put(upstreamIfindex, upstreamIface); + } else if (!TextUtils.equals(iface, upstreamIface)) { + Log.wtf(TAG, "The upstream interface name " + upstreamIface + + " is different from the existing interface name " + + iface + " for index " + upstreamIfindex); + } + } + + /** + * Add downstream client. + */ + public void tetherOffloadClientAdd(@NonNull final IpServer ipServer, + @NonNull final ClientInfo client) { + if (!isUsingBpf()) return; + + if (!mTetherClients.containsKey(ipServer)) { + mTetherClients.put(ipServer, new HashMap()); + } + + HashMap clients = mTetherClients.get(ipServer); + clients.put(client.clientAddress, client); + } + + /** + * Remove downstream client. + */ + public void tetherOffloadClientRemove(@NonNull final IpServer ipServer, + @NonNull final ClientInfo client) { + if (!isUsingBpf()) return; + + HashMap clients = mTetherClients.get(ipServer); + if (clients == null) return; + + // If no rule is removed, return early. Avoid unnecessary work on a non-existent rule + // which may have never been added or removed already. + if (clients.remove(client.clientAddress) == null) return; + + // Remove the downstream entry if it has no more rule. + if (clients.isEmpty()) { + mTetherClients.remove(ipServer); + } + } + + /** + * Call when UpstreamNetworkState may be changed. + * If upstream has ipv4 for tethering, update this new UpstreamNetworkState to map. The + * upstream interface index and its address mapping is prepared for building IPv4 + * offload rule. + * + * TODO: Delete the unused upstream interface mapping. + * TODO: Support ether ip upstream interface. + */ + public void addUpstreamIfindexToMap(LinkProperties lp) { + if (!mPollingStarted) return; + + // This will not work on a network that is using 464xlat because hasIpv4Address will not be + // true. + // TODO: need to consider 464xlat. + if (lp == null || !lp.hasIpv4Address()) return; + + // Support raw ip upstream interface only. + final InterfaceParams params = mDeps.getInterfaceParams(lp.getInterfaceName()); + if (params == null || params.hasMacAddress) return; + + Collection addresses = lp.getAddresses(); + for (InetAddress addr: addresses) { + if (addr instanceof Inet4Address) { + Inet4Address i4addr = (Inet4Address) addr; + if (!i4addr.isAnyLocalAddress() && !i4addr.isLinkLocalAddress() + && !i4addr.isLoopbackAddress() && !i4addr.isMulticastAddress()) { + mIpv4UpstreamIndices.put(i4addr, params.index); + } + } + } + } + + /** + * Attach BPF program + * + * TODO: consider error handling if the attach program failed. + */ + public void maybeAttachProgram(@NonNull String intIface, @NonNull String extIface) { + if (forwardingPairExists(intIface, extIface)) return; + + boolean firstDownstreamForThisUpstream = !isAnyForwardingPairOnUpstream(extIface); + forwardingPairAdd(intIface, extIface); + + mBpfCoordinatorShim.attachProgram(intIface, UPSTREAM); + // Attach if the upstream is the first time to be used in a forwarding pair. + if (firstDownstreamForThisUpstream) { + mBpfCoordinatorShim.attachProgram(extIface, DOWNSTREAM); + } + } + + /** + * Detach BPF program + */ + public void maybeDetachProgram(@NonNull String intIface, @NonNull String extIface) { + forwardingPairRemove(intIface, extIface); + + // Detaching program may fail because the interface has been removed already. + mBpfCoordinatorShim.detachProgram(intIface); + // Detach if no more forwarding pair is using the upstream. + if (!isAnyForwardingPairOnUpstream(extIface)) { + mBpfCoordinatorShim.detachProgram(extIface); + } + } + + // TODO: make mInterfaceNames accessible to the shim and move this code to there. + private String getIfName(long ifindex) { + return mInterfaceNames.get((int) ifindex, Long.toString(ifindex)); + } + + /** + * Dump information. + * Block the function until all the data are dumped on the handler thread or timed-out. The + * reason is that dumpsys invokes this function on the thread of caller and the data may only + * be allowed to be accessed on the handler thread. + */ + public void dump(@NonNull IndentingPrintWriter pw) { + pw.println("mIsBpfEnabled: " + mIsBpfEnabled); + pw.println("Polling " + (mPollingStarted ? "started" : "not started")); + pw.println("Stats provider " + (mStatsProvider != null + ? "registered" : "not registered")); + pw.println("Upstream quota: " + mInterfaceQuotas.toString()); + pw.println("Polling interval: " + getPollingInterval() + " ms"); + pw.println("Bpf shim: " + mBpfCoordinatorShim.toString()); + + pw.println("Forwarding stats:"); + pw.increaseIndent(); + if (mStats.size() == 0) { + pw.println(""); + } else { + dumpStats(pw); + } + pw.decreaseIndent(); + + pw.println("Forwarding rules:"); + pw.increaseIndent(); + dumpIpv6UpstreamRules(pw); + dumpIpv6ForwardingRules(pw); + dumpIpv4ForwardingRules(pw); + pw.decreaseIndent(); + + pw.println("Device map:"); + pw.increaseIndent(); + dumpDevmap(pw); + pw.decreaseIndent(); + + pw.println(); + pw.println("Forwarding counters:"); + pw.increaseIndent(); + dumpCounters(pw); + pw.decreaseIndent(); + } + + private void dumpStats(@NonNull IndentingPrintWriter pw) { + for (int i = 0; i < mStats.size(); i++) { + final int upstreamIfindex = mStats.keyAt(i); + final ForwardedStats stats = mStats.get(upstreamIfindex); + pw.println(String.format("%d(%s) - %s", upstreamIfindex, mInterfaceNames.get( + upstreamIfindex), stats.toString())); + } + } + + private void dumpIpv6ForwardingRules(@NonNull IndentingPrintWriter pw) { + if (mIpv6ForwardingRules.size() == 0) { + pw.println("No IPv6 rules"); + return; + } + + for (Map.Entry> entry : + mIpv6ForwardingRules.entrySet()) { + IpServer ipServer = entry.getKey(); + // The rule downstream interface index is paired with the interface name from + // IpServer#interfaceName. See #startIPv6, #updateIpv6ForwardingRules in IpServer. + final String downstreamIface = ipServer.interfaceName(); + pw.println("[" + downstreamIface + "]: iif(iface) oif(iface) v6addr srcmac dstmac"); + + pw.increaseIndent(); + LinkedHashMap rules = entry.getValue(); + for (Ipv6ForwardingRule rule : rules.values()) { + final int upstreamIfindex = rule.upstreamIfindex; + pw.println(String.format("%d(%s) %d(%s) %s %s %s", upstreamIfindex, + mInterfaceNames.get(upstreamIfindex), rule.downstreamIfindex, + downstreamIface, rule.address.getHostAddress(), rule.srcMac, rule.dstMac)); + } + pw.decreaseIndent(); + } + } + + private String ipv6UpstreamRuletoString(TetherUpstream6Key key, Tether6Value value) { + return String.format("%d(%s) %s -> %d(%s) %04x %s %s", + key.iif, getIfName(key.iif), key.dstMac, value.oif, getIfName(value.oif), + value.ethProto, value.ethSrcMac, value.ethDstMac); + } + + private void dumpIpv6UpstreamRules(IndentingPrintWriter pw) { + try (BpfMap map = mDeps.getBpfUpstream6Map()) { + if (map == null) { + pw.println("No IPv6 upstream"); + return; + } + if (map.isEmpty()) { + pw.println("No IPv6 upstream rules"); + return; + } + map.forEach((k, v) -> pw.println(ipv6UpstreamRuletoString(k, v))); + } catch (ErrnoException e) { + pw.println("Error dumping IPv6 upstream map: " + e); + } + } + + private String ipv4RuleToString(Tether4Key key, Tether4Value value) { + final String private4, public4, dst4; + try { + private4 = InetAddress.getByAddress(key.src4).getHostAddress(); + dst4 = InetAddress.getByAddress(key.dst4).getHostAddress(); + public4 = InetAddress.getByAddress(value.src46).getHostAddress(); + } catch (UnknownHostException impossible) { + throw new AssertionError("4-byte array not valid IPv4 address!"); + } + return String.format("[%s] %d(%s) %s:%d -> %d(%s) %s:%d -> %s:%d", + key.dstMac, key.iif, getIfName(key.iif), private4, key.srcPort, + value.oif, getIfName(value.oif), + public4, value.srcPort, dst4, key.dstPort); + } + + private void dumpIpv4ForwardingRules(IndentingPrintWriter pw) { + try (BpfMap map = mDeps.getBpfUpstream4Map()) { + if (map == null) { + pw.println("No IPv4 support"); + return; + } + if (map.isEmpty()) { + pw.println("No IPv4 rules"); + return; + } + pw.println("IPv4: [inDstMac] iif(iface) src -> nat -> dst"); + pw.increaseIndent(); + map.forEach((k, v) -> pw.println(ipv4RuleToString(k, v))); + } catch (ErrnoException e) { + pw.println("Error dumping IPv4 map: " + e); + } + pw.decreaseIndent(); + } + + /** + * Simple struct that only contains a u32. Must be public because Struct needs access to it. + * TODO: make this a public inner class of Struct so anyone can use it as, e.g., Struct.U32? + */ + public static class U32Struct extends Struct { + @Struct.Field(order = 0, type = Struct.Type.U32) + public long val; + } + + private void dumpCounters(@NonNull IndentingPrintWriter pw) { + if (!mDeps.isAtLeastS()) { + pw.println("No counter support"); + return; + } + try (BpfMap map = new BpfMap<>(TETHER_ERROR_MAP_PATH, + BpfMap.BPF_F_RDONLY, U32Struct.class, U32Struct.class)) { + + map.forEach((k, v) -> { + String counterName; + try { + counterName = sBpfCounterNames[(int) k.val]; + } catch (IndexOutOfBoundsException e) { + // Should never happen because this code gets the counter name from the same + // include file as the BPF program that increments the counter. + Log.wtf(TAG, "Unknown tethering counter type " + k.val); + counterName = Long.toString(k.val); + } + if (v.val > 0) pw.println(String.format("%s: %d", counterName, v.val)); + }); + } catch (ErrnoException e) { + pw.println("Error dumping counter map: " + e); + } + } + + private void dumpDevmap(@NonNull IndentingPrintWriter pw) { + try (BpfMap map = mDeps.getBpfDevMap()) { + if (map == null) { + pw.println("No devmap support"); + return; + } + if (map.isEmpty()) { + pw.println("No interface index"); + return; + } + pw.println("ifindex (iface) -> ifindex (iface)"); + pw.increaseIndent(); + map.forEach((k, v) -> { + // Only get upstream interface name. Just do the best to make the index readable. + // TODO: get downstream interface name because the index is either upstrema or + // downstream interface in dev map. + pw.println(String.format("%d (%s) -> %d (%s)", k.ifIndex, getIfName(k.ifIndex), + v.ifIndex, getIfName(v.ifIndex))); + }); + } catch (ErrnoException e) { + pw.println("Error dumping dev map: " + e); + } + pw.decreaseIndent(); + } + + /** IPv6 forwarding rule class. */ + public static class Ipv6ForwardingRule { + // The upstream6 and downstream6 rules are built as the following tables. Only raw ip + // upstream interface is supported. + // TODO: support ether ip upstream interface. + // + // NAT network topology: + // + // public network (rawip) private network + // | UE | + // +------------+ V +------------+------------+ V +------------+ + // | Sever +---------+ Upstream | Downstream +---------+ Client | + // +------------+ +------------+------------+ +------------+ + // + // upstream6 key and value: + // + // +------+-------------+ + // | TetherUpstream6Key | + // +------+------+------+ + // |field |iif |dstMac| + // | | | | + // +------+------+------+ + // |value |downst|downst| + // | |ream |ream | + // +------+------+------+ + // + // +------+----------------------------------+ + // | |Tether6Value | + // +------+------+------+------+------+------+ + // |field |oif |ethDst|ethSrc|ethPro|pmtu | + // | | |mac |mac |to | | + // +------+------+------+------+------+------+ + // |value |upstre|-- |-- |ETH_P_|1500 | + // | |am | | |IP | | + // +------+------+------+------+------+------+ + // + // downstream6 key and value: + // + // +------+--------------------+ + // | |TetherDownstream6Key| + // +------+------+------+------+ + // |field |iif |dstMac|neigh6| + // | | | | | + // +------+------+------+------+ + // |value |upstre|-- |client| + // | |am | | | + // +------+------+------+------+ + // + // +------+----------------------------------+ + // | |Tether6Value | + // +------+------+------+------+------+------+ + // |field |oif |ethDst|ethSrc|ethPro|pmtu | + // | | |mac |mac |to | | + // +------+------+------+------+------+------+ + // |value |downst|client|downst|ETH_P_|1500 | + // | |ream | |ream |IP | | + // +------+------+------+------+------+------+ + // + public final int upstreamIfindex; + public final int downstreamIfindex; + + // TODO: store a ClientInfo object instead of storing address, srcMac, and dstMac directly. + @NonNull + public final Inet6Address address; + @NonNull + public final MacAddress srcMac; + @NonNull + public final MacAddress dstMac; + + public Ipv6ForwardingRule(int upstreamIfindex, int downstreamIfIndex, + @NonNull Inet6Address address, @NonNull MacAddress srcMac, + @NonNull MacAddress dstMac) { + this.upstreamIfindex = upstreamIfindex; + this.downstreamIfindex = downstreamIfIndex; + this.address = address; + this.srcMac = srcMac; + this.dstMac = dstMac; + } + + /** Return a new rule object which updates with new upstream index. */ + @NonNull + public Ipv6ForwardingRule onNewUpstream(int newUpstreamIfindex) { + return new Ipv6ForwardingRule(newUpstreamIfindex, downstreamIfindex, address, srcMac, + dstMac); + } + + /** + * Don't manipulate TetherOffloadRuleParcel directly because implementing onNewUpstream() + * would be error-prone due to generated stable AIDL classes not having a copy constructor. + */ + @NonNull + public TetherOffloadRuleParcel toTetherOffloadRuleParcel() { + final TetherOffloadRuleParcel parcel = new TetherOffloadRuleParcel(); + parcel.inputInterfaceIndex = upstreamIfindex; + parcel.outputInterfaceIndex = downstreamIfindex; + parcel.destination = address.getAddress(); + parcel.prefixLength = 128; + parcel.srcL2Address = srcMac.toByteArray(); + parcel.dstL2Address = dstMac.toByteArray(); + return parcel; + } + + /** + * Return a TetherDownstream6Key object built from the rule. + */ + @NonNull + public TetherDownstream6Key makeTetherDownstream6Key() { + return new TetherDownstream6Key(upstreamIfindex, NULL_MAC_ADDRESS, + address.getAddress()); + } + + /** + * Return a Tether6Value object built from the rule. + */ + @NonNull + public Tether6Value makeTether6Value() { + return new Tether6Value(downstreamIfindex, dstMac, srcMac, ETH_P_IPV6, + NetworkStackConstants.ETHER_MTU); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Ipv6ForwardingRule)) return false; + Ipv6ForwardingRule that = (Ipv6ForwardingRule) o; + return this.upstreamIfindex == that.upstreamIfindex + && this.downstreamIfindex == that.downstreamIfindex + && Objects.equals(this.address, that.address) + && Objects.equals(this.srcMac, that.srcMac) + && Objects.equals(this.dstMac, that.dstMac); + } + + @Override + public int hashCode() { + // TODO: if this is ever used in production code, don't pass ifindices + // to Objects.hash() to avoid autoboxing overhead. + return Objects.hash(upstreamIfindex, downstreamIfindex, address, srcMac, dstMac); + } + } + + /** Tethering client information class. */ + public static class ClientInfo { + public final int downstreamIfindex; + + @NonNull + public final MacAddress downstreamMac; + @NonNull + public final Inet4Address clientAddress; + @NonNull + public final MacAddress clientMac; + + public ClientInfo(int downstreamIfindex, + @NonNull MacAddress downstreamMac, @NonNull Inet4Address clientAddress, + @NonNull MacAddress clientMac) { + this.downstreamIfindex = downstreamIfindex; + this.downstreamMac = downstreamMac; + this.clientAddress = clientAddress; + this.clientMac = clientMac; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ClientInfo)) return false; + ClientInfo that = (ClientInfo) o; + return this.downstreamIfindex == that.downstreamIfindex + && Objects.equals(this.downstreamMac, that.downstreamMac) + && Objects.equals(this.clientAddress, that.clientAddress) + && Objects.equals(this.clientMac, that.clientMac); + } + + @Override + public int hashCode() { + return Objects.hash(downstreamIfindex, downstreamMac, clientAddress, clientMac); + } + + @Override + public String toString() { + return String.format("downstream: %d (%s), client: %s (%s)", + downstreamIfindex, downstreamMac, clientAddress, clientMac); + } + } + + /** + * A BPF tethering stats provider to provide network statistics to the system. + * Note that this class' data may only be accessed on the handler thread. + */ + @VisibleForTesting + class BpfTetherStatsProvider extends NetworkStatsProvider { + // The offloaded traffic statistics per interface that has not been reported since the + // last call to pushTetherStats. Only the interfaces that were ever tethering upstreams + // and has pending tether stats delta are included in this NetworkStats object. + private NetworkStats mIfaceStats = new NetworkStats(0L, 0); + + // The same stats as above, but counts network stats per uid. + private NetworkStats mUidStats = new NetworkStats(0L, 0); + + @Override + public void onRequestStatsUpdate(int token) { + mHandler.post(() -> pushTetherStats()); + } + + @Override + public void onSetAlert(long quotaBytes) { + mHandler.post(() -> updateAlertQuota(quotaBytes)); + } + + @Override + public void onSetLimit(@NonNull String iface, long quotaBytes) { + if (quotaBytes < QUOTA_UNLIMITED) { + throw new IllegalArgumentException("invalid quota value " + quotaBytes); + } + + mHandler.post(() -> { + final Long curIfaceQuota = mInterfaceQuotas.get(iface); + + if (null == curIfaceQuota && QUOTA_UNLIMITED == quotaBytes) return; + + if (quotaBytes == QUOTA_UNLIMITED) { + mInterfaceQuotas.remove(iface); + } else { + mInterfaceQuotas.put(iface, quotaBytes); + } + maybeUpdateDataLimit(iface); + }); + } + + @VisibleForTesting + void pushTetherStats() { + try { + // The token is not used for now. See b/153606961. + notifyStatsUpdated(0 /* token */, mIfaceStats, mUidStats); + + // Clear the accumulated tether stats delta after reported. Note that create a new + // empty object because NetworkStats#clear is @hide. + mIfaceStats = new NetworkStats(0L, 0); + mUidStats = new NetworkStats(0L, 0); + } catch (RuntimeException e) { + mLog.e("Cannot report network stats: ", e); + } + } + + private void accumulateDiff(@NonNull NetworkStats ifaceDiff, + @NonNull NetworkStats uidDiff) { + mIfaceStats = mIfaceStats.add(ifaceDiff); + mUidStats = mUidStats.add(uidDiff); + } + } + + @Nullable + private ClientInfo getClientInfo(@NonNull Inet4Address clientAddress) { + for (HashMap clients : mTetherClients.values()) { + for (ClientInfo client : clients.values()) { + if (clientAddress.equals(client.clientAddress)) { + return client; + } + } + } + return null; + } + + // Support raw ip only. + // TODO: add ether ip support. + // TODO: parse CTA_PROTOINFO of conntrack event in ConntrackMonitor. For TCP, only add rules + // while TCP status is established. + @VisibleForTesting + class BpfConntrackEventConsumer implements ConntrackEventConsumer { + @NonNull + private Tether4Key makeTetherUpstream4Key( + @NonNull ConntrackEvent e, @NonNull ClientInfo c) { + return new Tether4Key(c.downstreamIfindex, c.downstreamMac, + e.tupleOrig.protoNum, e.tupleOrig.srcIp.getAddress(), + e.tupleOrig.dstIp.getAddress(), e.tupleOrig.srcPort, e.tupleOrig.dstPort); + } + + @NonNull + private Tether4Key makeTetherDownstream4Key( + @NonNull ConntrackEvent e, @NonNull ClientInfo c, int upstreamIndex) { + return new Tether4Key(upstreamIndex, NULL_MAC_ADDRESS /* dstMac (rawip) */, + e.tupleReply.protoNum, e.tupleReply.srcIp.getAddress(), + e.tupleReply.dstIp.getAddress(), e.tupleReply.srcPort, e.tupleReply.dstPort); + } + + @NonNull + private Tether4Value makeTetherUpstream4Value(@NonNull ConntrackEvent e, + int upstreamIndex) { + return new Tether4Value(upstreamIndex, + NULL_MAC_ADDRESS /* ethDstMac (rawip) */, + NULL_MAC_ADDRESS /* ethSrcMac (rawip) */, ETH_P_IP, + NetworkStackConstants.ETHER_MTU, toIpv4MappedAddressBytes(e.tupleReply.dstIp), + toIpv4MappedAddressBytes(e.tupleReply.srcIp), e.tupleReply.dstPort, + e.tupleReply.srcPort, 0 /* lastUsed, filled by bpf prog only */); + } + + @NonNull + private Tether4Value makeTetherDownstream4Value(@NonNull ConntrackEvent e, + @NonNull ClientInfo c, int upstreamIndex) { + return new Tether4Value(c.downstreamIfindex, + c.clientMac, c.downstreamMac, ETH_P_IP, NetworkStackConstants.ETHER_MTU, + toIpv4MappedAddressBytes(e.tupleOrig.dstIp), + toIpv4MappedAddressBytes(e.tupleOrig.srcIp), + e.tupleOrig.dstPort, e.tupleOrig.srcPort, + 0 /* lastUsed, filled by bpf prog only */); + } + + @NonNull + private byte[] toIpv4MappedAddressBytes(Inet4Address ia4) { + final byte[] addr4 = ia4.getAddress(); + final byte[] addr6 = new byte[16]; + addr6[10] = (byte) 0xff; + addr6[11] = (byte) 0xff; + addr6[12] = addr4[0]; + addr6[13] = addr4[1]; + addr6[14] = addr4[2]; + addr6[15] = addr4[3]; + return addr6; + } + + public void accept(ConntrackEvent e) { + final ClientInfo tetherClient = getClientInfo(e.tupleOrig.srcIp); + if (tetherClient == null) return; + + final Integer upstreamIndex = mIpv4UpstreamIndices.get(e.tupleReply.dstIp); + if (upstreamIndex == null) return; + + final Tether4Key upstream4Key = makeTetherUpstream4Key(e, tetherClient); + final Tether4Key downstream4Key = makeTetherDownstream4Key(e, tetherClient, + upstreamIndex); + + if (e.msgType == (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8 + | NetlinkConstants.IPCTNL_MSG_CT_DELETE)) { + mBpfCoordinatorShim.tetherOffloadRuleRemove(UPSTREAM, upstream4Key); + mBpfCoordinatorShim.tetherOffloadRuleRemove(DOWNSTREAM, downstream4Key); + maybeClearLimit(upstreamIndex); + return; + } + + final Tether4Value upstream4Value = makeTetherUpstream4Value(e, upstreamIndex); + final Tether4Value downstream4Value = makeTetherDownstream4Value(e, tetherClient, + upstreamIndex); + + maybeAddDevMap(upstreamIndex, tetherClient.downstreamIfindex); + maybeSetLimit(upstreamIndex); + mBpfCoordinatorShim.tetherOffloadRuleAdd(UPSTREAM, upstream4Key, upstream4Value); + mBpfCoordinatorShim.tetherOffloadRuleAdd(DOWNSTREAM, downstream4Key, downstream4Value); + } + } + + private boolean isBpfEnabled() { + final TetheringConfiguration config = mDeps.getTetherConfig(); + return (config != null) ? config.isBpfOffloadEnabled() : true /* default value */; + } + + private int getInterfaceIndexFromRules(@NonNull String ifName) { + for (LinkedHashMap rules : mIpv6ForwardingRules + .values()) { + for (Ipv6ForwardingRule rule : rules.values()) { + final int upstreamIfindex = rule.upstreamIfindex; + if (TextUtils.equals(ifName, mInterfaceNames.get(upstreamIfindex))) { + return upstreamIfindex; + } + } + } + return 0; + } + + private long getQuotaBytes(@NonNull String iface) { + final Long limit = mInterfaceQuotas.get(iface); + final long quotaBytes = (limit != null) ? limit : QUOTA_UNLIMITED; + + return quotaBytes; + } + + private boolean sendDataLimitToBpfMap(int ifIndex, long quotaBytes) { + if (ifIndex == 0) { + Log.wtf(TAG, "Invalid interface index."); + return false; + } + + return mBpfCoordinatorShim.tetherOffloadSetInterfaceQuota(ifIndex, quotaBytes); + } + + // Handle the data limit update from the service which is the stats provider registered for. + private void maybeUpdateDataLimit(@NonNull String iface) { + // Set data limit only on a given upstream which has at least one rule. If we can't get + // an interface index for a given interface name, it means either there is no rule for + // a given upstream or the interface name is not an upstream which is monitored by the + // coordinator. + final int ifIndex = getInterfaceIndexFromRules(iface); + if (ifIndex == 0) return; + + final long quotaBytes = getQuotaBytes(iface); + sendDataLimitToBpfMap(ifIndex, quotaBytes); + } + + // Handle the data limit update while adding forwarding rules. + private boolean updateDataLimit(int ifIndex) { + final String iface = mInterfaceNames.get(ifIndex); + if (iface == null) { + mLog.e("Fail to get the interface name for index " + ifIndex); + return false; + } + final long quotaBytes = getQuotaBytes(iface); + return sendDataLimitToBpfMap(ifIndex, quotaBytes); + } + + private void maybeSetLimit(int upstreamIfindex) { + if (isAnyRuleOnUpstream(upstreamIfindex) + || mBpfCoordinatorShim.isAnyIpv4RuleOnUpstream(upstreamIfindex)) { + return; + } + + // If failed to set a data limit, probably should not use this upstream, because + // the upstream may not want to blow through the data limit that was told to apply. + // TODO: Perhaps stop the coordinator. + boolean success = updateDataLimit(upstreamIfindex); + if (!success) { + final String iface = mInterfaceNames.get(upstreamIfindex); + mLog.e("Setting data limit for " + iface + " failed."); + } + } + + // TODO: This should be also called while IpServer wants to clear all IPv4 rules. Relying on + // conntrack event can't cover this case. + private void maybeClearLimit(int upstreamIfindex) { + if (isAnyRuleOnUpstream(upstreamIfindex) + || mBpfCoordinatorShim.isAnyIpv4RuleOnUpstream(upstreamIfindex)) { + return; + } + + final TetherStatsValue statsValue = + mBpfCoordinatorShim.tetherOffloadGetAndClearStats(upstreamIfindex); + if (statsValue == null) { + Log.wtf(TAG, "Fail to cleanup tether stats for upstream index " + upstreamIfindex); + return; + } + + SparseArray tetherStatsList = new SparseArray(); + tetherStatsList.put(upstreamIfindex, statsValue); + + // Update the last stats delta and delete the local cache for a given upstream. + updateQuotaAndStatsFromSnapshot(tetherStatsList); + mStats.remove(upstreamIfindex); + } + + // TODO: Rename to isAnyIpv6RuleOnUpstream and define an isAnyRuleOnUpstream method that called + // both isAnyIpv6RuleOnUpstream and mBpfCoordinatorShim.isAnyIpv4RuleOnUpstream. + private boolean isAnyRuleOnUpstream(int upstreamIfindex) { + for (LinkedHashMap rules : mIpv6ForwardingRules + .values()) { + for (Ipv6ForwardingRule rule : rules.values()) { + if (upstreamIfindex == rule.upstreamIfindex) return true; + } + } + return false; + } + + private boolean isAnyRuleFromDownstreamToUpstream(int downstreamIfindex, int upstreamIfindex) { + for (LinkedHashMap rules : mIpv6ForwardingRules + .values()) { + for (Ipv6ForwardingRule rule : rules.values()) { + if (downstreamIfindex == rule.downstreamIfindex + && upstreamIfindex == rule.upstreamIfindex) { + return true; + } + } + } + return false; + } + + // TODO: remove the index from map while the interface has been removed because the map size + // is 64 entries. See packages\modules\Connectivity\Tethering\bpf_progs\offload.c. + private void maybeAddDevMap(int upstreamIfindex, int downstreamIfindex) { + for (Integer index : new Integer[] {upstreamIfindex, downstreamIfindex}) { + if (mDeviceMapSet.contains(index)) continue; + if (mBpfCoordinatorShim.addDevMap(index)) mDeviceMapSet.add(index); + } + } + + private void forwardingPairAdd(@NonNull String intIface, @NonNull String extIface) { + if (!mForwardingPairs.containsKey(extIface)) { + mForwardingPairs.put(extIface, new HashSet()); + } + mForwardingPairs.get(extIface).add(intIface); + } + + private void forwardingPairRemove(@NonNull String intIface, @NonNull String extIface) { + HashSet downstreams = mForwardingPairs.get(extIface); + if (downstreams == null) return; + if (!downstreams.remove(intIface)) return; + + if (downstreams.isEmpty()) { + mForwardingPairs.remove(extIface); + } + } + + private boolean forwardingPairExists(@NonNull String intIface, @NonNull String extIface) { + if (!mForwardingPairs.containsKey(extIface)) return false; + + return mForwardingPairs.get(extIface).contains(intIface); + } + + private boolean isAnyForwardingPairOnUpstream(@NonNull String extIface) { + return mForwardingPairs.containsKey(extIface); + } + + @NonNull + private NetworkStats buildNetworkStats(@NonNull StatsType type, int ifIndex, + @NonNull final ForwardedStats diff) { + NetworkStats stats = new NetworkStats(0L, 0); + final String iface = mInterfaceNames.get(ifIndex); + if (iface == null) { + // TODO: Use Log.wtf once the coordinator owns full control of tether stats from netd. + // For now, netd may add the empty stats for the upstream which is not monitored by + // the coordinator. Silently ignore it. + return stats; + } + final int uid = (type == StatsType.STATS_PER_UID) ? UID_TETHERING : UID_ALL; + // Note that the argument 'metered', 'roaming' and 'defaultNetwork' are not recorded for + // network stats snapshot. See NetworkStatsRecorder#recordSnapshotLocked. + return stats.addEntry(new Entry(iface, uid, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, diff.rxBytes, diff.rxPackets, + diff.txBytes, diff.txPackets, 0L /* operations */)); + } + + private void updateAlertQuota(long newQuota) { + if (newQuota < QUOTA_UNLIMITED) { + throw new IllegalArgumentException("invalid quota value " + newQuota); + } + if (mRemainingAlertQuota == newQuota) return; + + mRemainingAlertQuota = newQuota; + if (mRemainingAlertQuota == 0) { + mLog.i("onAlertReached"); + if (mStatsProvider != null) mStatsProvider.notifyAlertReached(); + } + } + + private void updateQuotaAndStatsFromSnapshot( + @NonNull final SparseArray tetherStatsList) { + long usedAlertQuota = 0; + for (int i = 0; i < tetherStatsList.size(); i++) { + final Integer ifIndex = tetherStatsList.keyAt(i); + final TetherStatsValue tetherStats = tetherStatsList.valueAt(i); + final ForwardedStats curr = new ForwardedStats(tetherStats); + final ForwardedStats base = mStats.get(ifIndex); + final ForwardedStats diff = (base != null) ? curr.subtract(base) : curr; + usedAlertQuota += diff.rxBytes + diff.txBytes; + + // Update the local cache for counting tether stats delta. + mStats.put(ifIndex, curr); + + // Update the accumulated tether stats delta to the stats provider for the service + // querying. + if (mStatsProvider != null) { + try { + mStatsProvider.accumulateDiff( + buildNetworkStats(StatsType.STATS_PER_IFACE, ifIndex, diff), + buildNetworkStats(StatsType.STATS_PER_UID, ifIndex, diff)); + } catch (ArrayIndexOutOfBoundsException e) { + Log.wtf(TAG, "Fail to update the accumulated stats delta for interface index " + + ifIndex + " : ", e); + } + } + } + + if (mRemainingAlertQuota > 0 && usedAlertQuota > 0) { + // Trim to zero if overshoot. + final long newQuota = Math.max(mRemainingAlertQuota - usedAlertQuota, 0); + updateAlertQuota(newQuota); + } + + // TODO: Count the used limit quota for notifying data limit reached. + } + + private void updateForwardedStats() { + final SparseArray tetherStatsList = + mBpfCoordinatorShim.tetherOffloadGetStats(); + + if (tetherStatsList == null) { + mLog.e("Problem fetching tethering stats"); + return; + } + + updateQuotaAndStatsFromSnapshot(tetherStatsList); + } + + @VisibleForTesting + int getPollingInterval() { + // The valid range of interval is DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS..max_long. + // Ignore the config value is less than the minimum polling interval. Note that the + // minimum interval definition is invoked as OffloadController#isPollingStatsNeeded does. + // TODO: Perhaps define a minimum polling interval constant. + final TetheringConfiguration config = mDeps.getTetherConfig(); + final int configInterval = (config != null) ? config.getOffloadPollInterval() : 0; + return Math.max(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, configInterval); + } + + private void maybeSchedulePollingStats() { + if (!mPollingStarted) return; + + if (mHandler.hasCallbacks(mScheduledPollingTask)) { + mHandler.removeCallbacks(mScheduledPollingTask); + } + + mHandler.postDelayed(mScheduledPollingTask, getPollingInterval()); + } + + // Return forwarding rule map. This is used for testing only. + // Note that this can be only called on handler thread. + @NonNull + @VisibleForTesting + final HashMap> + getForwardingRulesForTesting() { + return mIpv6ForwardingRules; + } + + // Return upstream interface name map. This is used for testing only. + // Note that this can be only called on handler thread. + @NonNull + @VisibleForTesting + final SparseArray getInterfaceNamesForTesting() { + return mInterfaceNames; + } + + // Return BPF conntrack event consumer. This is used for testing only. + // Note that this can be only called on handler thread. + @NonNull + @VisibleForTesting + final BpfConntrackEventConsumer getBpfConntrackEventConsumerForTesting() { + return mBpfConntrackEventConsumer; + } + + private static native String[] getBpfCounterNames(); +} diff --git a/Tethering/src/com/android/networkstack/tethering/BpfMap.java b/Tethering/src/com/android/networkstack/tethering/BpfMap.java new file mode 100644 index 0000000000..1363dc5150 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/BpfMap.java @@ -0,0 +1,288 @@ +/* + * 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.networkstack.tethering; + +import static android.system.OsConstants.EEXIST; +import static android.system.OsConstants.ENOENT; + +import android.system.ErrnoException; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.Struct; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.BiConsumer; + +/** + * BpfMap is a key -> value mapping structure that is designed to maintained the bpf map entries. + * This is a wrapper class of in-kernel data structure. The in-kernel data can be read/written by + * passing syscalls with map file descriptor. + * + * @param the key of the map. + * @param the value of the map. + */ +public class BpfMap implements AutoCloseable { + static { + System.loadLibrary("tetherutilsjni"); + } + + // Following definitions from kernel include/uapi/linux/bpf.h + public static final int BPF_F_RDWR = 0; + public static final int BPF_F_RDONLY = 1 << 3; + public static final int BPF_F_WRONLY = 1 << 4; + + public static final int BPF_MAP_TYPE_HASH = 1; + + private static final int BPF_F_NO_PREALLOC = 1; + + private static final int BPF_ANY = 0; + private static final int BPF_NOEXIST = 1; + private static final int BPF_EXIST = 2; + + private final int mMapFd; + private final Class mKeyClass; + private final Class mValueClass; + private final int mKeySize; + private final int mValueSize; + + /** + * Create a BpfMap map wrapper with "path" of filesystem. + * + * @param flag the access mode, one of BPF_F_RDWR, BPF_F_RDONLY, or BPF_F_WRONLY. + * @throws ErrnoException if the BPF map associated with {@code path} cannot be retrieved. + * @throws NullPointerException if {@code path} is null. + */ + public BpfMap(@NonNull final String path, final int flag, final Class key, + final Class value) throws ErrnoException, NullPointerException { + mMapFd = bpfFdGet(path, flag); + + mKeyClass = key; + mValueClass = value; + mKeySize = Struct.getSize(key); + mValueSize = Struct.getSize(value); + } + + /** + * Constructor for testing only. + * The derived class implements an internal mocked map. It need to implement all functions + * which are related with the native BPF map because the BPF map handler is not initialized. + * See BpfCoordinatorTest#TestBpfMap. + */ + @VisibleForTesting + protected BpfMap(final Class key, final Class value) { + mMapFd = -1; + mKeyClass = key; + mValueClass = value; + mKeySize = Struct.getSize(key); + mValueSize = Struct.getSize(value); + } + + /** + * Update an existing or create a new key -> value entry in an eBbpf map. + * (use insertOrReplaceEntry() if you need to know whether insert or replace happened) + */ + public void updateEntry(K key, V value) throws ErrnoException { + writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_ANY); + } + + /** + * If the key does not exist in the map, insert key -> value entry into eBpf map. + * Otherwise IllegalStateException will be thrown. + */ + public void insertEntry(K key, V value) + throws ErrnoException, IllegalStateException { + try { + writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST); + } catch (ErrnoException e) { + if (e.errno == EEXIST) throw new IllegalStateException(key + " already exists"); + + throw e; + } + } + + /** + * If the key already exists in the map, replace its value. Otherwise NoSuchElementException + * will be thrown. + */ + public void replaceEntry(K key, V value) + throws ErrnoException, NoSuchElementException { + try { + writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_EXIST); + } catch (ErrnoException e) { + if (e.errno == ENOENT) throw new NoSuchElementException(key + " not found"); + + throw e; + } + } + + /** + * Update an existing or create a new key -> value entry in an eBbpf map. + * Returns true if inserted, false if replaced. + * (use updateEntry() if you don't care whether insert or replace happened) + * Note: see inline comment below if running concurrently with delete operations. + */ + public boolean insertOrReplaceEntry(K key, V value) + throws ErrnoException { + try { + writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST); + return true; /* insert succeeded */ + } catch (ErrnoException e) { + if (e.errno != EEXIST) throw e; + } + try { + writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_EXIST); + return false; /* replace succeeded */ + } catch (ErrnoException e) { + if (e.errno != ENOENT) throw e; + } + /* If we reach here somebody deleted after our insert attempt and before our replace: + * this implies a race happened. The kernel bpf delete interface only takes a key, + * and not the value, so we can safely pretend the replace actually succeeded and + * was immediately followed by the other thread's delete, since the delete cannot + * observe the potential change to the value. + */ + return false; /* pretend replace succeeded */ + } + + /** Remove existing key from eBpf map. Return false if map was not modified. */ + public boolean deleteEntry(K key) throws ErrnoException { + return deleteMapEntry(mMapFd, key.writeToBytes()); + } + + /** Returns {@code true} if this map contains no elements. */ + public boolean isEmpty() throws ErrnoException { + return getFirstKey() == null; + } + + private K getNextKeyInternal(@Nullable K key) throws ErrnoException { + final byte[] rawKey = getNextRawKey( + key == null ? null : key.writeToBytes()); + if (rawKey == null) return null; + + final ByteBuffer buffer = ByteBuffer.wrap(rawKey); + buffer.order(ByteOrder.nativeOrder()); + return Struct.parse(mKeyClass, buffer); + } + + /** + * Get the next key of the passed-in key. If the passed-in key is not found, return the first + * key. If the passed-in key is the last one, return null. + * + * TODO: consider allowing null passed-in key. + */ + public K getNextKey(@NonNull K key) throws ErrnoException { + Objects.requireNonNull(key); + return getNextKeyInternal(key); + } + + private byte[] getNextRawKey(@Nullable final byte[] key) throws ErrnoException { + byte[] nextKey = new byte[mKeySize]; + if (getNextMapKey(mMapFd, key, nextKey)) return nextKey; + + return null; + } + + /** Get the first key of eBpf map. */ + public K getFirstKey() throws ErrnoException { + return getNextKeyInternal(null); + } + + /** Check whether a key exists in the map. */ + public boolean containsKey(@NonNull K key) throws ErrnoException { + Objects.requireNonNull(key); + + final byte[] rawValue = getRawValue(key.writeToBytes()); + return rawValue != null; + } + + /** Retrieve a value from the map. Return null if there is no such key. */ + public V getValue(@NonNull K key) throws ErrnoException { + Objects.requireNonNull(key); + final byte[] rawValue = getRawValue(key.writeToBytes()); + + if (rawValue == null) return null; + + final ByteBuffer buffer = ByteBuffer.wrap(rawValue); + buffer.order(ByteOrder.nativeOrder()); + return Struct.parse(mValueClass, buffer); + } + + private byte[] getRawValue(final byte[] key) throws ErrnoException { + byte[] value = new byte[mValueSize]; + if (findMapEntry(mMapFd, key, value)) return value; + + return null; + } + + /** + * Iterate through the map and handle each key -> value retrieved base on the given BiConsumer. + * The given BiConsumer may to delete the passed-in entry, but is not allowed to perform any + * other structural modifications to the map, such as adding entries or deleting other entries. + * Otherwise, iteration will result in undefined behaviour. + */ + public void forEach(BiConsumer action) throws ErrnoException { + @Nullable K nextKey = getFirstKey(); + + while (nextKey != null) { + @NonNull final K curKey = nextKey; + @NonNull final V value = getValue(curKey); + + nextKey = getNextKey(curKey); + action.accept(curKey, value); + } + } + + @Override + public void close() throws ErrnoException { + closeMap(mMapFd); + } + + /** + * Clears the map. The map may already be empty. + * + * @throws ErrnoException if the map is already closed, if an error occurred during iteration, + * or if a non-ENOENT error occurred when deleting a key. + */ + public void clear() throws ErrnoException { + K key = getFirstKey(); + while (key != null) { + deleteEntry(key); // ignores ENOENT. + key = getFirstKey(); + } + } + + private static native int closeMap(int fd) throws ErrnoException; + + private native int bpfFdGet(String path, int mode) throws ErrnoException, NullPointerException; + + private native void writeToMapEntry(int fd, byte[] key, byte[] value, int flags) + throws ErrnoException; + + private native boolean deleteMapEntry(int fd, byte[] key) throws ErrnoException; + + // If key is found, the operation returns true and the nextKey would reference to the next + // element. If key is not found, the operation returns true and the nextKey would reference to + // the first element. If key is the last element, false is returned. + private native boolean getNextMapKey(int fd, byte[] key, byte[] nextKey) throws ErrnoException; + + private native boolean findMapEntry(int fd, byte[] key, byte[] value) throws ErrnoException; +} diff --git a/Tethering/src/com/android/networkstack/tethering/BpfUtils.java b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java new file mode 100644 index 0000000000..0b44249458 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.networkstack.tethering; + +import static android.system.OsConstants.ETH_P_IP; +import static android.system.OsConstants.ETH_P_IPV6; + +import android.net.util.InterfaceParams; + +import androidx.annotation.NonNull; + +import java.io.IOException; + +/** + * The classes and the methods for BPF utilization. + * + * {@hide} + */ +public class BpfUtils { + static { + System.loadLibrary("tetherutilsjni"); + } + + // For better code clarity when used for 'bool ingress' parameter. + static final boolean EGRESS = false; + static final boolean INGRESS = true; + + // For better code clarify when used for 'bool downstream' parameter. + // + // This is talking about the direction of travel of the offloaded packets. + // + // Upstream means packets heading towards the internet/uplink (upload), + // thus for tethering this is attached to ingress on the downstream interface, + // while for clat this is attached to egress on the v4-* clat interface. + // + // Downstream means packets coming from the internet/uplink (download), thus + // for both clat and tethering this is attached to ingress on the upstream interface. + static final boolean DOWNSTREAM = true; + static final boolean UPSTREAM = false; + + // The priority of clat/tether hooks - smaller is higher priority. + // TC tether is higher priority then TC clat to match XDP winning over TC. + // Sync from system/netd/server/OffloadUtils.h. + static final short PRIO_TETHER6 = 1; + static final short PRIO_TETHER4 = 2; + // note that the above must be lower than PRIO_CLAT from netd's OffloadUtils.cpp + + private static String makeProgPath(boolean downstream, int ipVersion, boolean ether) { + String path = "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_" + + (downstream ? "downstream" : "upstream") + + ipVersion + "_" + + (ether ? "ether" : "rawip"); + return path; + } + + /** + * Attach BPF program + * + * TODO: use interface index to replace interface name. + */ + public static void attachProgram(@NonNull String iface, boolean downstream) + throws IOException { + final InterfaceParams params = InterfaceParams.getByName(iface); + if (params == null) { + throw new IOException("Fail to get interface params for interface " + iface); + } + + boolean ether; + try { + ether = isEthernet(iface); + } catch (IOException e) { + throw new IOException("isEthernet(" + params.index + "[" + iface + "]) failure: " + e); + } + + try { + // tc filter add dev .. ingress prio 1 protocol ipv6 bpf object-pinned /sys/fs/bpf/... + // direct-action + tcFilterAddDevBpf(params.index, INGRESS, PRIO_TETHER6, (short) ETH_P_IPV6, + makeProgPath(downstream, 6, ether)); + } catch (IOException e) { + throw new IOException("tc filter add dev (" + params.index + "[" + iface + + "]) ingress prio PRIO_TETHER6 protocol ipv6 failure: " + e); + } + + try { + // tc filter add dev .. ingress prio 2 protocol ip bpf object-pinned /sys/fs/bpf/... + // direct-action + tcFilterAddDevBpf(params.index, INGRESS, PRIO_TETHER4, (short) ETH_P_IP, + makeProgPath(downstream, 4, ether)); + } catch (IOException e) { + throw new IOException("tc filter add dev (" + params.index + "[" + iface + + "]) ingress prio PRIO_TETHER4 protocol ip failure: " + e); + } + } + + /** + * Detach BPF program + * + * TODO: use interface index to replace interface name. + */ + public static void detachProgram(@NonNull String iface) throws IOException { + final InterfaceParams params = InterfaceParams.getByName(iface); + if (params == null) { + throw new IOException("Fail to get interface params for interface " + iface); + } + + try { + // tc filter del dev .. ingress prio 1 protocol ipv6 + tcFilterDelDev(params.index, INGRESS, PRIO_TETHER6, (short) ETH_P_IPV6); + } catch (IOException e) { + throw new IOException("tc filter del dev (" + params.index + "[" + iface + + "]) ingress prio PRIO_TETHER6 protocol ipv6 failure: " + e); + } + + try { + // tc filter del dev .. ingress prio 2 protocol ip + tcFilterDelDev(params.index, INGRESS, PRIO_TETHER4, (short) ETH_P_IP); + } catch (IOException e) { + throw new IOException("tc filter del dev (" + params.index + "[" + iface + + "]) ingress prio PRIO_TETHER4 protocol ip failure: " + e); + } + } + + private static native boolean isEthernet(String iface) throws IOException; + + private static native void tcFilterAddDevBpf(int ifIndex, boolean ingress, short prio, + short proto, String bpfProgPath) throws IOException; + + private static native void tcFilterDelDev(int ifIndex, boolean ingress, short prio, + short proto) throws IOException; +} diff --git a/Tethering/src/com/android/networkstack/tethering/ConnectedClientsTracker.java b/Tethering/src/com/android/networkstack/tethering/ConnectedClientsTracker.java new file mode 100644 index 0000000000..8a96988ae1 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/ConnectedClientsTracker.java @@ -0,0 +1,183 @@ +/* + * 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.networkstack.tethering; + +import static android.net.TetheringManager.TETHERING_WIFI; + +import android.net.MacAddress; +import android.net.TetheredClient; +import android.net.TetheredClient.AddressInfo; +import android.net.ip.IpServer; +import android.net.wifi.WifiClient; +import android.os.SystemClock; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Tracker for clients connected to downstreams. + * + *

This class is not thread safe, it is intended to be used only from the tethering handler + * thread. + */ +public class ConnectedClientsTracker { + private final Clock mClock; + + @NonNull + private List mLastWifiClients = Collections.emptyList(); + @NonNull + private List mLastTetheredClients = Collections.emptyList(); + + @VisibleForTesting + static class Clock { + public long elapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + } + + public ConnectedClientsTracker() { + this(new Clock()); + } + + @VisibleForTesting + ConnectedClientsTracker(Clock clock) { + mClock = clock; + } + + /** + * Update the tracker with new connected clients. + * + *

The new list can be obtained through {@link #getLastTetheredClients()}. + * @param ipServers The IpServers used to assign addresses to clients. + * @param wifiClients The list of L2-connected WiFi clients. Null for no change since last + * update. + * @return True if the list of clients changed since the last calculation. + */ + public boolean updateConnectedClients( + Iterable ipServers, @Nullable List wifiClients) { + final long now = mClock.elapsedRealtime(); + + if (wifiClients != null) { + mLastWifiClients = wifiClients; + } + final Set wifiClientMacs = getClientMacs(mLastWifiClients); + + // Build the list of non-expired leases from all IpServers, grouped by mac address + final Map clientsMap = new HashMap<>(); + for (IpServer server : ipServers) { + for (TetheredClient client : server.getAllLeases()) { + if (client.getTetheringType() == TETHERING_WIFI + && !wifiClientMacs.contains(client.getMacAddress())) { + // Skip leases of WiFi clients that are not (or no longer) L2-connected + continue; + } + final TetheredClient prunedClient = pruneExpired(client, now); + if (prunedClient == null) continue; // All addresses expired + + addLease(clientsMap, prunedClient); + } + } + + // TODO: add IPv6 addresses from netlink + + // Add connected WiFi clients that do not have any known address + for (MacAddress client : wifiClientMacs) { + if (clientsMap.containsKey(client)) continue; + clientsMap.put(client, new TetheredClient( + client, Collections.emptyList() /* addresses */, TETHERING_WIFI)); + } + + final HashSet clients = new HashSet<>(clientsMap.values()); + final boolean clientsChanged = clients.size() != mLastTetheredClients.size() + || !clients.containsAll(mLastTetheredClients); + mLastTetheredClients = Collections.unmodifiableList(new ArrayList<>(clients)); + return clientsChanged; + } + + private static void addLease(Map clientsMap, TetheredClient lease) { + final TetheredClient aggregateClient = clientsMap.getOrDefault( + lease.getMacAddress(), lease); + if (aggregateClient == lease) { + // This is the first lease with this mac address + clientsMap.put(lease.getMacAddress(), lease); + return; + } + + // Only add the address info; this assumes that the tethering type is the same when the mac + // address is the same. If a client is connected through different tethering types with the + // same mac address, connected clients callbacks will report all of its addresses under only + // one of these tethering types. This keeps the API simple considering that such a scenario + // would really be a rare edge case. + clientsMap.put(lease.getMacAddress(), aggregateClient.addAddresses(lease)); + } + + /** + * Get the last list of tethered clients, as calculated in {@link #updateConnectedClients}. + * + *

The returned list is immutable. + */ + @NonNull + public List getLastTetheredClients() { + return mLastTetheredClients; + } + + private static boolean hasExpiredAddress(List addresses, long now) { + for (AddressInfo info : addresses) { + if (info.getExpirationTime() <= now) { + return true; + } + } + return false; + } + + @Nullable + private static TetheredClient pruneExpired(TetheredClient client, long now) { + final List addresses = client.getAddresses(); + if (addresses.size() == 0) return null; + if (!hasExpiredAddress(addresses, now)) return client; + + final ArrayList newAddrs = new ArrayList<>(addresses.size() - 1); + for (AddressInfo info : addresses) { + if (info.getExpirationTime() > now) { + newAddrs.add(info); + } + } + + if (newAddrs.size() == 0) { + return null; + } + return new TetheredClient(client.getMacAddress(), newAddrs, client.getTetheringType()); + } + + @NonNull + private static Set getClientMacs(@NonNull List clients) { + final Set macs = new HashSet<>(clients.size()); + for (WifiClient c : clients) { + macs.add(c.getMacAddress()); + } + return macs; + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java new file mode 100644 index 0000000000..60fcfd0437 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java @@ -0,0 +1,639 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import static android.net.TetheringConstants.EXTRA_ADD_TETHER_TYPE; +import static android.net.TetheringConstants.EXTRA_PROVISION_CALLBACK; +import static android.net.TetheringConstants.EXTRA_RUN_PROVISION; +import static android.net.TetheringConstants.EXTRA_TETHER_PROVISIONING_RESPONSE; +import static android.net.TetheringConstants.EXTRA_TETHER_SILENT_PROVISIONING_ACTION; +import static android.net.TetheringConstants.EXTRA_TETHER_SUBID; +import static android.net.TetheringConstants.EXTRA_TETHER_UI_PROVISIONING_APP_NAME; +import static android.net.TetheringManager.TETHERING_BLUETOOTH; +import static android.net.TetheringManager.TETHERING_ETHERNET; +import static android.net.TetheringManager.TETHERING_INVALID; +import static android.net.TetheringManager.TETHERING_USB; +import static android.net.TetheringManager.TETHERING_WIFI; +import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN; +import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR; +import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.util.SharedLog; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcel; +import android.os.PersistableBundle; +import android.os.ResultReceiver; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.provider.Settings; +import android.telephony.CarrierConfigManager; +import android.util.SparseIntArray; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.PrintWriter; +import java.util.BitSet; + +/** + * Re-check tethering provisioning for enabled downstream tether types. + * Reference TetheringManager.TETHERING_{@code *} for each tether type. + * + * All methods of this class must be accessed from the thread of tethering + * state machine. + * @hide + */ +public class EntitlementManager { + private static final String TAG = EntitlementManager.class.getSimpleName(); + private static final boolean DBG = false; + + @VisibleForTesting + protected static final String DISABLE_PROVISIONING_SYSPROP_KEY = "net.tethering.noprovisioning"; + private static final String ACTION_PROVISIONING_ALARM = + "com.android.networkstack.tethering.PROVISIONING_RECHECK_ALARM"; + + private final ComponentName mSilentProvisioningService; + private static final int MS_PER_HOUR = 60 * 60 * 1000; + private static final int DUMP_TIMEOUT = 10_000; + + // The BitSet is the bit map of each enabled downstream types, ex: + // {@link TetheringManager.TETHERING_WIFI} + // {@link TetheringManager.TETHERING_USB} + // {@link TetheringManager.TETHERING_BLUETOOTH} + private final BitSet mCurrentDownstreams; + private final BitSet mExemptedDownstreams; + private final Context mContext; + private final SharedLog mLog; + private final SparseIntArray mEntitlementCacheValue; + private final Handler mHandler; + // Key: TetheringManager.TETHERING_*(downstream). + // Value: TetheringManager.TETHER_ERROR_{NO_ERROR or PROVISION_FAILED}(provisioning result). + private final SparseIntArray mCurrentEntitlementResults; + private final Runnable mPermissionChangeCallback; + private PendingIntent mProvisioningRecheckAlarm; + private boolean mLastCellularUpstreamPermitted = true; + private boolean mUsingCellularAsUpstream = false; + private boolean mNeedReRunProvisioningUi = false; + private OnUiEntitlementFailedListener mListener; + private TetheringConfigurationFetcher mFetcher; + + public EntitlementManager(Context ctx, Handler h, SharedLog log, + Runnable callback) { + mContext = ctx; + mLog = log.forSubComponent(TAG); + mCurrentDownstreams = new BitSet(); + mExemptedDownstreams = new BitSet(); + mCurrentEntitlementResults = new SparseIntArray(); + mEntitlementCacheValue = new SparseIntArray(); + mPermissionChangeCallback = callback; + mHandler = h; + mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_PROVISIONING_ALARM), + null, mHandler); + mSilentProvisioningService = ComponentName.unflattenFromString( + mContext.getResources().getString(R.string.config_wifi_tether_enable)); + } + + public void setOnUiEntitlementFailedListener(final OnUiEntitlementFailedListener listener) { + mListener = listener; + } + + /** Callback fired when UI entitlement failed. */ + public interface OnUiEntitlementFailedListener { + /** + * Ui entitlement check fails in |downstream|. + * + * @param downstream tethering type from TetheringManager.TETHERING_{@code *}. + */ + void onUiEntitlementFailed(int downstream); + } + + public void setTetheringConfigurationFetcher(final TetheringConfigurationFetcher fetcher) { + mFetcher = fetcher; + } + + /** Interface to fetch TetheringConfiguration. */ + public interface TetheringConfigurationFetcher { + /** + * Fetch current tethering configuration. This will be called to ensure whether entitlement + * check is needed. + * @return TetheringConfiguration instance. + */ + TetheringConfiguration fetchTetheringConfiguration(); + } + + /** + * Check if cellular upstream is permitted. + */ + public boolean isCellularUpstreamPermitted() { + final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration(); + + return isCellularUpstreamPermitted(config); + } + + private boolean isCellularUpstreamPermitted(final TetheringConfiguration config) { + if (!isTetherProvisioningRequired(config)) return true; + + // If provisioning is required and EntitlementManager doesn't know any downstreams, cellular + // upstream should not be enabled. Enable cellular upstream for exempted downstreams only + // when there is no non-exempted downstream. + if (mCurrentDownstreams.isEmpty()) return !mExemptedDownstreams.isEmpty(); + + return mCurrentEntitlementResults.indexOfValue(TETHER_ERROR_NO_ERROR) > -1; + } + + /** + * Set exempted downstream type. If there is only exempted downstream type active, + * corresponding entitlement check will not be run and cellular upstream will be permitted + * by default. If a privileged app enables tethering without a provisioning check, and then + * another app enables tethering of the same type but does not disable the provisioning check, + * then the downstream immediately loses exempt status and a provisioning check is run. + * If any non-exempted downstream type is active, the cellular upstream will be gated by the + * result of entitlement check from non-exempted downstreams. If entitlement check is still + * in progress on non-exempt downstreams, ceullar upstream would default be disabled. When any + * non-exempted downstream gets positive entitlement result, ceullar upstream will be enabled. + */ + public void setExemptedDownstreamType(final int type) { + mExemptedDownstreams.set(type, true); + } + + /** + * This is called when tethering starts. + * Launch provisioning app if upstream is cellular. + * + * @param downstreamType tethering type from TetheringManager.TETHERING_{@code *} + * @param showProvisioningUi a boolean indicating whether to show the + * provisioning app UI if there is one. + */ + public void startProvisioningIfNeeded(int downstreamType, boolean showProvisioningUi) { + if (!isValidDownstreamType(downstreamType)) return; + + mCurrentDownstreams.set(downstreamType, true); + + mExemptedDownstreams.set(downstreamType, false); + + final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration(); + if (!isTetherProvisioningRequired(config)) return; + + // If upstream is not cellular, provisioning app would not be launched + // till upstream change to cellular. + if (mUsingCellularAsUpstream) { + if (showProvisioningUi) { + runUiTetherProvisioning(downstreamType, config); + } else { + runSilentTetherProvisioning(downstreamType, config); + } + mNeedReRunProvisioningUi = false; + } else { + mNeedReRunProvisioningUi |= showProvisioningUi; + } + } + + /** + * Tell EntitlementManager that a given type of tethering has been disabled + * + * @param type tethering type from TetheringManager.TETHERING_{@code *} + */ + public void stopProvisioningIfNeeded(int downstreamType) { + if (!isValidDownstreamType(downstreamType)) return; + + mCurrentDownstreams.set(downstreamType, false); + // There are lurking bugs where the notion of "provisioning required" or + // "tethering supported" may change without without tethering being notified properly. + // Remove the mapping all the time no matter provisioning is required or not. + removeDownstreamMapping(downstreamType); + mExemptedDownstreams.set(downstreamType, false); + } + + /** + * Notify EntitlementManager if upstream is cellular or not. + * + * @param isCellular whether tethering upstream is cellular. + */ + public void notifyUpstream(boolean isCellular) { + if (DBG) { + mLog.i("notifyUpstream: " + isCellular + + ", mLastCellularUpstreamPermitted: " + mLastCellularUpstreamPermitted + + ", mNeedReRunProvisioningUi: " + mNeedReRunProvisioningUi); + } + mUsingCellularAsUpstream = isCellular; + + if (mUsingCellularAsUpstream) { + final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration(); + maybeRunProvisioning(config); + } + } + + /** Run provisioning if needed */ + public void maybeRunProvisioning() { + final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration(); + maybeRunProvisioning(config); + } + + private void maybeRunProvisioning(final TetheringConfiguration config) { + if (mCurrentDownstreams.isEmpty() || !isTetherProvisioningRequired(config)) { + return; + } + + // Whenever any entitlement value changes, all downstreams will re-evaluate whether they + // are allowed. Therefore even if the silent check here ends in a failure and the UI later + // yields success, then the downstream that got a failure will re-evaluate as a result of + // the change and get the new correct value. + for (int downstream = mCurrentDownstreams.nextSetBit(0); downstream >= 0; + downstream = mCurrentDownstreams.nextSetBit(downstream + 1)) { + if (mCurrentEntitlementResults.indexOfKey(downstream) < 0) { + if (mNeedReRunProvisioningUi) { + mNeedReRunProvisioningUi = false; + runUiTetherProvisioning(downstream, config); + } else { + runSilentTetherProvisioning(downstream, config); + } + } + } + } + + /** + * Check if the device requires a provisioning check in order to enable tethering. + * + * @param config an object that encapsulates the various tethering configuration elements. + * @return a boolean - {@code true} indicating tether provisioning is required by the carrier. + */ + @VisibleForTesting + protected boolean isTetherProvisioningRequired(final TetheringConfiguration config) { + if (SystemProperties.getBoolean(DISABLE_PROVISIONING_SYSPROP_KEY, false) + || config.provisioningApp.length == 0) { + return false; + } + if (carrierConfigAffirmsEntitlementCheckNotRequired(config)) { + return false; + } + return (config.provisioningApp.length == 2); + } + + /** + * Re-check tethering provisioning for all enabled tether types. + * Reference TetheringManager.TETHERING_{@code *} for each tether type. + * + * @param config an object that encapsulates the various tethering configuration elements. + * Note: this method is only called from @{link Tethering.TetherMainSM} on the handler thread. + * If there are new callers from different threads, the logic should move to + * @{link Tethering.TetherMainSM} handler to avoid race conditions. + */ + public void reevaluateSimCardProvisioning(final TetheringConfiguration config) { + if (DBG) mLog.i("reevaluateSimCardProvisioning"); + + if (!mHandler.getLooper().isCurrentThread()) { + // Except for test, this log should not appear in normal flow. + mLog.log("reevaluateSimCardProvisioning() don't run in TetherMainSM thread"); + } + mEntitlementCacheValue.clear(); + mCurrentEntitlementResults.clear(); + + // TODO: refine provisioning check to isTetherProvisioningRequired() ?? + if (!config.hasMobileHotspotProvisionApp() + || carrierConfigAffirmsEntitlementCheckNotRequired(config)) { + evaluateCellularPermission(config); + return; + } + + if (mUsingCellularAsUpstream) { + maybeRunProvisioning(config); + } + } + + /** + * Get carrier configuration bundle. + * @param config an object that encapsulates the various tethering configuration elements. + * */ + public PersistableBundle getCarrierConfig(final TetheringConfiguration config) { + final CarrierConfigManager configManager = (CarrierConfigManager) mContext + .getSystemService(Context.CARRIER_CONFIG_SERVICE); + if (configManager == null) return null; + + final PersistableBundle carrierConfig = configManager.getConfigForSubId( + config.activeDataSubId); + + if (CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfig)) { + return carrierConfig; + } + + return null; + } + + // The logic here is aimed solely at confirming that a CarrierConfig exists + // and affirms that entitlement checks are not required. + // + // TODO: find a better way to express this, or alter the checking process + // entirely so that this is more intuitive. + private boolean carrierConfigAffirmsEntitlementCheckNotRequired( + final TetheringConfiguration config) { + // Check carrier config for entitlement checks + final PersistableBundle carrierConfig = getCarrierConfig(config); + if (carrierConfig == null) return false; + + // A CarrierConfigManager was found and it has a config. + final boolean isEntitlementCheckRequired = carrierConfig.getBoolean( + CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL); + return !isEntitlementCheckRequired; + } + + /** + * Run no UI tethering provisioning check. + * @param type tethering type from TetheringManager.TETHERING_{@code *} + * @param subId default data subscription ID. + */ + @VisibleForTesting + protected Intent runSilentTetherProvisioning(int type, final TetheringConfiguration config) { + if (DBG) mLog.i("runSilentTetherProvisioning: " + type); + // For silent provisioning, settings would stop tethering when entitlement fail. + ResultReceiver receiver = buildProxyReceiver(type, false/* notifyFail */, null); + + Intent intent = new Intent(); + intent.putExtra(EXTRA_ADD_TETHER_TYPE, type); + intent.putExtra(EXTRA_RUN_PROVISION, true); + intent.putExtra(EXTRA_TETHER_SILENT_PROVISIONING_ACTION, config.provisioningAppNoUi); + intent.putExtra(EXTRA_TETHER_PROVISIONING_RESPONSE, config.provisioningResponse); + intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver); + intent.putExtra(EXTRA_TETHER_SUBID, config.activeDataSubId); + intent.setComponent(mSilentProvisioningService); + // Only admin user can change tethering and SilentTetherProvisioning don't need to + // show UI, it is fine to always start setting's background service as system user. + mContext.startService(intent); + return intent; + } + + private void runUiTetherProvisioning(int type, final TetheringConfiguration config) { + ResultReceiver receiver = buildProxyReceiver(type, true/* notifyFail */, null); + runUiTetherProvisioning(type, config, receiver); + } + + /** + * Run the UI-enabled tethering provisioning check. + * @param type tethering type from TetheringManager.TETHERING_{@code *} + * @param subId default data subscription ID. + * @param receiver to receive entitlement check result. + */ + @VisibleForTesting + protected Intent runUiTetherProvisioning(int type, final TetheringConfiguration config, + ResultReceiver receiver) { + if (DBG) mLog.i("runUiTetherProvisioning: " + type); + + Intent intent = new Intent(Settings.ACTION_TETHER_PROVISIONING_UI); + intent.putExtra(EXTRA_ADD_TETHER_TYPE, type); + intent.putExtra(EXTRA_TETHER_UI_PROVISIONING_APP_NAME, config.provisioningApp); + intent.putExtra(EXTRA_PROVISION_CALLBACK, receiver); + intent.putExtra(EXTRA_TETHER_SUBID, config.activeDataSubId); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + // Only launch entitlement UI for system user. Entitlement UI should not appear for other + // user because only admin user is allowed to change tethering. + mContext.startActivity(intent); + return intent; + } + + // Not needed to check if this don't run on the handler thread because it's private. + private void scheduleProvisioningRechecks(final TetheringConfiguration config) { + if (mProvisioningRecheckAlarm == null) { + final int period = config.provisioningCheckPeriod; + if (period <= 0) return; + + Intent intent = new Intent(ACTION_PROVISIONING_ALARM); + mProvisioningRecheckAlarm = PendingIntent.getBroadcast(mContext, 0, intent, + PendingIntent.FLAG_IMMUTABLE); + AlarmManager alarmManager = (AlarmManager) mContext.getSystemService( + Context.ALARM_SERVICE); + long periodMs = period * MS_PER_HOUR; + long firstAlarmTime = SystemClock.elapsedRealtime() + periodMs; + alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME, firstAlarmTime, periodMs, + mProvisioningRecheckAlarm); + } + } + + private void cancelTetherProvisioningRechecks() { + if (mProvisioningRecheckAlarm != null) { + AlarmManager alarmManager = (AlarmManager) mContext.getSystemService( + Context.ALARM_SERVICE); + alarmManager.cancel(mProvisioningRecheckAlarm); + mProvisioningRecheckAlarm = null; + } + } + + private void evaluateCellularPermission(final TetheringConfiguration config) { + final boolean permitted = isCellularUpstreamPermitted(config); + + if (DBG) { + mLog.i("Cellular permission change from " + mLastCellularUpstreamPermitted + + " to " + permitted); + } + + if (mLastCellularUpstreamPermitted != permitted) { + mLog.log("Cellular permission change: " + permitted); + mPermissionChangeCallback.run(); + } + // Only schedule periodic re-check when tether is provisioned + // and the result is ok. + if (permitted && mCurrentEntitlementResults.size() > 0) { + scheduleProvisioningRechecks(config); + } else { + cancelTetherProvisioningRechecks(); + } + mLastCellularUpstreamPermitted = permitted; + } + + /** + * Add the mapping between provisioning result and tethering type. + * Notify UpstreamNetworkMonitor if Cellular permission changes. + * + * @param type tethering type from TetheringManager.TETHERING_{@code *} + * @param resultCode Provisioning result + */ + protected void addDownstreamMapping(int type, int resultCode) { + mLog.i("addDownstreamMapping: " + type + ", result: " + resultCode + + " ,TetherTypeRequested: " + mCurrentDownstreams.get(type)); + if (!mCurrentDownstreams.get(type)) return; + + mCurrentEntitlementResults.put(type, resultCode); + final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration(); + evaluateCellularPermission(config); + } + + /** + * Remove the mapping for input tethering type. + * @param type tethering type from TetheringManager.TETHERING_{@code *} + */ + protected void removeDownstreamMapping(int type) { + mLog.i("removeDownstreamMapping: " + type); + mCurrentEntitlementResults.delete(type); + final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration(); + evaluateCellularPermission(config); + } + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (ACTION_PROVISIONING_ALARM.equals(intent.getAction())) { + mLog.log("Received provisioning alarm"); + final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration(); + reevaluateSimCardProvisioning(config); + } + } + }; + + private static boolean isValidDownstreamType(int type) { + switch (type) { + case TETHERING_BLUETOOTH: + case TETHERING_ETHERNET: + case TETHERING_USB: + case TETHERING_WIFI: + return true; + default: + return false; + } + } + + /** + * Dump the infromation of EntitlementManager. + * @param pw {@link PrintWriter} is used to print formatted + */ + public void dump(PrintWriter pw) { + pw.print("isCellularUpstreamPermitted: "); + pw.println(isCellularUpstreamPermitted()); + for (int type = mCurrentDownstreams.nextSetBit(0); type >= 0; + type = mCurrentDownstreams.nextSetBit(type + 1)) { + pw.print("Type: "); + pw.print(typeString(type)); + if (mCurrentEntitlementResults.indexOfKey(type) > -1) { + pw.print(", Value: "); + pw.println(errorString(mCurrentEntitlementResults.get(type))); + } else { + pw.println(", Value: empty"); + } + } + pw.print("Exempted: ["); + for (int type = mExemptedDownstreams.nextSetBit(0); type >= 0; + type = mExemptedDownstreams.nextSetBit(type + 1)) { + pw.print(typeString(type)); + pw.print(", "); + } + pw.println("]"); + } + + private static String typeString(int type) { + switch (type) { + case TETHERING_BLUETOOTH: return "TETHERING_BLUETOOTH"; + case TETHERING_INVALID: return "TETHERING_INVALID"; + case TETHERING_USB: return "TETHERING_USB"; + case TETHERING_WIFI: return "TETHERING_WIFI"; + default: + return String.format("TETHERING UNKNOWN TYPE (%d)", type); + } + } + + private static String errorString(int value) { + switch (value) { + case TETHER_ERROR_ENTITLEMENT_UNKNOWN: return "TETHER_ERROR_ENTITLEMENT_UNKONWN"; + case TETHER_ERROR_NO_ERROR: return "TETHER_ERROR_NO_ERROR"; + case TETHER_ERROR_PROVISIONING_FAILED: return "TETHER_ERROR_PROVISIONING_FAILED"; + default: + return String.format("UNKNOWN ERROR (%d)", value); + } + } + + private ResultReceiver buildProxyReceiver(int type, boolean notifyFail, + final ResultReceiver receiver) { + ResultReceiver rr = new ResultReceiver(mHandler) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + int updatedCacheValue = updateEntitlementCacheValue(type, resultCode); + addDownstreamMapping(type, updatedCacheValue); + if (updatedCacheValue == TETHER_ERROR_PROVISIONING_FAILED && notifyFail) { + mListener.onUiEntitlementFailed(type); + } + if (receiver != null) receiver.send(updatedCacheValue, null); + } + }; + + return writeToParcel(rr); + } + + // Instances of ResultReceiver need to be public classes for remote processes to be able + // to load them (otherwise, ClassNotFoundException). For private classes, this method + // performs a trick : round-trip parceling any instance of ResultReceiver will return a + // vanilla instance of ResultReceiver sharing the binder token with the original receiver. + // The binder token has a reference to the original instance of the private class and will + // still call its methods, and can be sent over. However it cannot be used for anything + // else than sending over a Binder call. + // While round-trip parceling is not great, there is currently no other way of generating + // a vanilla instance of ResultReceiver because all its fields are private. + private ResultReceiver writeToParcel(final ResultReceiver receiver) { + Parcel parcel = Parcel.obtain(); + receiver.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ResultReceiver receiverForSending = ResultReceiver.CREATOR.createFromParcel(parcel); + parcel.recycle(); + return receiverForSending; + } + + /** + * Update the last entitlement value to internal cache + * + * @param type tethering type from TetheringManager.TETHERING_{@code *} + * @param resultCode last entitlement value + * @return the last updated entitlement value + */ + private int updateEntitlementCacheValue(int type, int resultCode) { + if (DBG) { + mLog.i("updateEntitlementCacheValue: " + type + ", result: " + resultCode); + } + if (resultCode == TETHER_ERROR_NO_ERROR) { + mEntitlementCacheValue.put(type, resultCode); + return resultCode; + } else { + mEntitlementCacheValue.put(type, TETHER_ERROR_PROVISIONING_FAILED); + return TETHER_ERROR_PROVISIONING_FAILED; + } + } + + /** Get the last value of the tethering entitlement check. */ + public void requestLatestTetheringEntitlementResult(int downstream, ResultReceiver receiver, + boolean showEntitlementUi) { + if (!isValidDownstreamType(downstream)) { + receiver.send(TETHER_ERROR_ENTITLEMENT_UNKNOWN, null); + return; + } + + final TetheringConfiguration config = mFetcher.fetchTetheringConfiguration(); + if (!isTetherProvisioningRequired(config)) { + receiver.send(TETHER_ERROR_NO_ERROR, null); + return; + } + + final int cacheValue = mEntitlementCacheValue.get( + downstream, TETHER_ERROR_ENTITLEMENT_UNKNOWN); + if (cacheValue == TETHER_ERROR_NO_ERROR || !showEntitlementUi) { + receiver.send(cacheValue, null); + } else { + ResultReceiver proxy = buildProxyReceiver(downstream, false/* notifyFail */, receiver); + runUiTetherProvisioning(downstream, config, proxy); + } + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java b/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java new file mode 100644 index 0000000000..f3dcaa2529 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/IPv6TetheringCoordinator.java @@ -0,0 +1,328 @@ +/* + * 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.networkstack.tethering; + +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.RouteInfo; +import android.net.ip.IpServer; +import android.net.util.NetworkConstants; +import android.net.util.SharedLog; +import android.util.Log; + +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Random; + + +/** + * IPv6 tethering is rather different from IPv4 owing to the absence of NAT. + * This coordinator is responsible for evaluating the dedicated prefixes + * assigned to the device and deciding how to divvy them up among downstream + * interfaces. + * + * @hide + */ +public class IPv6TetheringCoordinator { + private static final String TAG = IPv6TetheringCoordinator.class.getSimpleName(); + private static final boolean DBG = false; + private static final boolean VDBG = false; + + private static class Downstream { + public final IpServer ipServer; + public final int mode; // IpServer.STATE_* + // Used to append to a ULA /48, constructing a ULA /64 for local use. + public final short subnetId; + + Downstream(IpServer ipServer, int mode, short subnetId) { + this.ipServer = ipServer; + this.mode = mode; + this.subnetId = subnetId; + } + } + + private final ArrayList mNotifyList; + private final SharedLog mLog; + // NOTE: mActiveDownstreams is a list and not a hash data structure because + // we keep active downstreams in arrival order. This is done so /64s can + // be parceled out on a "first come, first served" basis and a /64 used by + // a downstream that is no longer active can be redistributed to any next + // waiting active downstream (again, in arrival order). + private final LinkedList mActiveDownstreams; + private final byte[] mUniqueLocalPrefix; + private short mNextSubnetId; + private UpstreamNetworkState mUpstreamNetworkState; + + public IPv6TetheringCoordinator(ArrayList notifyList, SharedLog log) { + mNotifyList = notifyList; + mLog = log.forSubComponent(TAG); + mActiveDownstreams = new LinkedList<>(); + mUniqueLocalPrefix = generateUniqueLocalPrefix(); + mNextSubnetId = 0; + } + + /** Add active downstream to ipv6 tethering candidate list. */ + public void addActiveDownstream(IpServer downstream, int mode) { + if (findDownstream(downstream) == null) { + // Adding a new downstream appends it to the list. Adding a + // downstream a second time without first removing it has no effect. + // We never change the mode of a downstream except by first removing + // it and then re-adding it (with its new mode specified); + if (mActiveDownstreams.offer(new Downstream(downstream, mode, mNextSubnetId))) { + // Make sure subnet IDs are always positive. They are appended + // to a ULA /48 to make a ULA /64 for local use. + mNextSubnetId = (short) Math.max(0, mNextSubnetId + 1); + } + updateIPv6TetheringInterfaces(); + } + } + + /** Remove downstream from ipv6 tethering candidate list. */ + public void removeActiveDownstream(IpServer downstream) { + stopIPv6TetheringOn(downstream); + if (mActiveDownstreams.remove(findDownstream(downstream))) { + updateIPv6TetheringInterfaces(); + } + + // When tethering is stopping we can reset the subnet counter. + if (mNotifyList.isEmpty()) { + if (!mActiveDownstreams.isEmpty()) { + Log.wtf(TAG, "Tethering notify list empty, IPv6 downstreams non-empty."); + } + mNextSubnetId = 0; + } + } + + /** + * Call when UpstreamNetworkState may be changed. + * If upstream has ipv6 for tethering, update this new UpstreamNetworkState + * to IpServer. Otherwise stop ipv6 tethering on downstream interfaces. + */ + public void updateUpstreamNetworkState(UpstreamNetworkState ns) { + if (VDBG) { + Log.d(TAG, "updateUpstreamNetworkState: " + toDebugString(ns)); + } + if (TetheringInterfaceUtils.getIPv6Interface(ns) == null) { + stopIPv6TetheringOnAllInterfaces(); + setUpstreamNetworkState(null); + return; + } + + if (mUpstreamNetworkState != null + && !ns.network.equals(mUpstreamNetworkState.network)) { + stopIPv6TetheringOnAllInterfaces(); + } + + setUpstreamNetworkState(ns); + updateIPv6TetheringInterfaces(); + } + + private void stopIPv6TetheringOnAllInterfaces() { + for (IpServer ipServer : mNotifyList) { + stopIPv6TetheringOn(ipServer); + } + } + + private void setUpstreamNetworkState(UpstreamNetworkState ns) { + if (ns == null) { + mUpstreamNetworkState = null; + } else { + // Make a deep copy of the parts we need. + mUpstreamNetworkState = new UpstreamNetworkState( + new LinkProperties(ns.linkProperties), + new NetworkCapabilities(ns.networkCapabilities), + new Network(ns.network)); + } + + mLog.log("setUpstreamNetworkState: " + toDebugString(mUpstreamNetworkState)); + } + + private void updateIPv6TetheringInterfaces() { + for (IpServer ipServer : mNotifyList) { + final LinkProperties lp = getInterfaceIPv6LinkProperties(ipServer); + ipServer.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, getTtlAdjustment(), 0, lp); + break; + } + } + + private int getTtlAdjustment() { + if (mUpstreamNetworkState == null || mUpstreamNetworkState.networkCapabilities == null) { + return 0; + } + + // If upstream is cellular, set the TTL in Router Advertisements to "network-set TTL" - 1 + // for carrier requirement. + if (mUpstreamNetworkState.networkCapabilities.hasTransport( + NetworkCapabilities.TRANSPORT_CELLULAR)) { + return -1; + } + + // For other non-cellular upstream, set TTL as "network-set TTL" + 1 to preventing arbitrary + // distinction between tethered and untethered traffic. + return 1; + } + + private LinkProperties getInterfaceIPv6LinkProperties(IpServer ipServer) { + final Downstream ds = findDownstream(ipServer); + if (ds == null) return null; + + if (ds.mode == IpServer.STATE_LOCAL_ONLY) { + // Build a Unique Locally-assigned Prefix configuration. + return getUniqueLocalConfig(mUniqueLocalPrefix, ds.subnetId); + } + + // This downstream is in IpServer.STATE_TETHERED mode. + if (mUpstreamNetworkState == null || mUpstreamNetworkState.linkProperties == null) { + return null; + } + + // NOTE: Here, in future, we would have policies to decide how to divvy + // up the available dedicated prefixes among downstream interfaces. + // At this time we have no such mechanism--we only support tethering + // IPv6 toward the oldest (first requested) active downstream. + + final Downstream currentActive = mActiveDownstreams.peek(); + if (currentActive != null && currentActive.ipServer == ipServer) { + final LinkProperties lp = getIPv6OnlyLinkProperties( + mUpstreamNetworkState.linkProperties); + if (lp.hasIpv6DefaultRoute() && lp.hasGlobalIpv6Address()) { + return lp; + } + } + + return null; + } + + Downstream findDownstream(IpServer ipServer) { + for (Downstream ds : mActiveDownstreams) { + if (ds.ipServer == ipServer) return ds; + } + return null; + } + + private static LinkProperties getIPv6OnlyLinkProperties(LinkProperties lp) { + final LinkProperties v6only = new LinkProperties(); + if (lp == null) { + return v6only; + } + + // NOTE: At this time we don't copy over any information about any + // stacked links. No current stacked link configuration has IPv6. + + v6only.setInterfaceName(lp.getInterfaceName()); + + v6only.setMtu(lp.getMtu()); + + for (LinkAddress linkAddr : lp.getLinkAddresses()) { + if (linkAddr.isGlobalPreferred() && linkAddr.getPrefixLength() == 64) { + v6only.addLinkAddress(linkAddr); + } + } + + for (RouteInfo routeInfo : lp.getRoutes()) { + final IpPrefix destination = routeInfo.getDestination(); + if ((destination.getAddress() instanceof Inet6Address) + && (destination.getPrefixLength() <= 64)) { + v6only.addRoute(routeInfo); + } + } + + for (InetAddress dnsServer : lp.getDnsServers()) { + if (isIPv6GlobalAddress(dnsServer)) { + // For now we include ULAs. + v6only.addDnsServer(dnsServer); + } + } + + v6only.setDomains(lp.getDomains()); + + return v6only; + } + + // TODO: Delete this and switch to LinkAddress#isGlobalPreferred once we + // announce our own IPv6 address as DNS server. + private static boolean isIPv6GlobalAddress(InetAddress ip) { + return (ip instanceof Inet6Address) + && !ip.isAnyLocalAddress() + && !ip.isLoopbackAddress() + && !ip.isLinkLocalAddress() + && !ip.isSiteLocalAddress() + && !ip.isMulticastAddress(); + } + + private static LinkProperties getUniqueLocalConfig(byte[] ulp, short subnetId) { + final LinkProperties lp = new LinkProperties(); + + final IpPrefix local48 = makeUniqueLocalPrefix(ulp, (short) 0, 48); + lp.addRoute(new RouteInfo(local48, null, null, RouteInfo.RTN_UNICAST)); + + final IpPrefix local64 = makeUniqueLocalPrefix(ulp, subnetId, 64); + // Because this is a locally-generated ULA, we don't have an upstream + // address. But because the downstream IP address management code gets + // its prefix from the upstream's IP address, we create a fake one here. + lp.addLinkAddress(new LinkAddress(local64.getAddress(), 64)); + + lp.setMtu(NetworkConstants.ETHER_MTU); + return lp; + } + + private static IpPrefix makeUniqueLocalPrefix(byte[] in6addr, short subnetId, int prefixlen) { + final byte[] bytes = Arrays.copyOf(in6addr, in6addr.length); + bytes[7] = (byte) (subnetId >> 8); + bytes[8] = (byte) subnetId; + final InetAddress addr; + try { + addr = InetAddress.getByAddress(bytes); + } catch (UnknownHostException e) { + throw new IllegalStateException("Invalid address length: " + bytes.length, e); + } + return new IpPrefix(addr, prefixlen); + } + + // Generates a Unique Locally-assigned Prefix: + // + // https://tools.ietf.org/html/rfc4193#section-3.1 + // + // The result is a /48 that can be used for local-only communications. + private static byte[] generateUniqueLocalPrefix() { + final byte[] ulp = new byte[6]; // 6 = 48bits / 8bits/byte + (new Random()).nextBytes(ulp); + + final byte[] in6addr = Arrays.copyOf(ulp, NetworkConstants.IPV6_ADDR_LEN); + in6addr[0] = (byte) 0xfd; // fc00::/7 and L=1 + + return in6addr; + } + + private static String toDebugString(UpstreamNetworkState ns) { + if (ns == null) { + return "UpstreamNetworkState{null}"; + } + return ns.toString(); + } + + private static void stopIPv6TetheringOn(IpServer ipServer) { + ipServer.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, null); + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadController.java b/Tethering/src/com/android/networkstack/tethering/OffloadController.java new file mode 100644 index 0000000000..beb18219b7 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/OffloadController.java @@ -0,0 +1,884 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.ROAMING_NO; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; +import static android.net.NetworkStats.UID_TETHERING; +import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED; +import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED; + +import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0; +import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_1; +import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_NONE; +import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.usage.NetworkStatsManager; +import android.content.ContentResolver; +import android.net.InetAddresses; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.NetworkStats; +import android.net.NetworkStats.Entry; +import android.net.RouteInfo; +import android.net.netlink.ConntrackMessage; +import android.net.netlink.NetlinkConstants; +import android.net.netlink.NetlinkSocket; +import android.net.netstats.provider.NetworkStatsProvider; +import android.net.util.SharedLog; +import android.os.Handler; +import android.provider.Settings; +import android.system.ErrnoException; +import android.system.OsConstants; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.networkstack.tethering.OffloadHardwareInterface.ForwardedStats; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A class to encapsulate the business logic of programming the tethering + * hardware offload interface. + * + * @hide + */ +public class OffloadController { + private static final String TAG = OffloadController.class.getSimpleName(); + private static final boolean DBG = false; + private static final String ANYIP = "0.0.0.0"; + private static final ForwardedStats EMPTY_STATS = new ForwardedStats(); + + @VisibleForTesting + enum StatsType { + STATS_PER_IFACE, + STATS_PER_UID, + } + + private enum UpdateType { IF_NEEDED, FORCE }; + + private final Handler mHandler; + private final OffloadHardwareInterface mHwInterface; + private final ContentResolver mContentResolver; + @Nullable + private final OffloadTetheringStatsProvider mStatsProvider; + private final SharedLog mLog; + private final HashMap mDownstreams; + private boolean mConfigInitialized; + @OffloadHardwareInterface.OffloadHalVersion + private int mControlHalVersion; + private LinkProperties mUpstreamLinkProperties; + // The complete set of offload-exempt prefixes passed in via Tethering from + // all upstream and downstream sources. + private Set mExemptPrefixes; + // A strictly "smaller" set of prefixes, wherein offload-approved prefixes + // (e.g. downstream on-link prefixes) have been removed and replaced with + // prefixes representing only the locally-assigned IP addresses. + private Set mLastLocalPrefixStrs; + + // Maps upstream interface names to offloaded traffic statistics. + // Always contains the latest value received from the hardware for each interface, regardless of + // whether offload is currently running on that interface. + private ConcurrentHashMap mForwardedStats = + new ConcurrentHashMap<>(16, 0.75F, 1); + + private static class InterfaceQuota { + public final long warningBytes; + public final long limitBytes; + + public static InterfaceQuota MAX_VALUE = new InterfaceQuota(Long.MAX_VALUE, Long.MAX_VALUE); + + InterfaceQuota(long warningBytes, long limitBytes) { + this.warningBytes = warningBytes; + this.limitBytes = limitBytes; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof InterfaceQuota)) return false; + InterfaceQuota that = (InterfaceQuota) o; + return warningBytes == that.warningBytes + && limitBytes == that.limitBytes; + } + + @Override + public int hashCode() { + return (int) (warningBytes * 3 + limitBytes * 5); + } + + @Override + public String toString() { + return "InterfaceQuota{" + "warning=" + warningBytes + ", limit=" + limitBytes + '}'; + } + } + + // Maps upstream interface names to interface quotas. + // Always contains the latest value received from the framework for each interface, regardless + // of whether offload is currently running (or is even supported) on that interface. Only + // includes upstream interfaces that have a quota set. + private HashMap mInterfaceQuotas = new HashMap<>(); + + // Tracking remaining alert quota. Unlike limit quota is subject to interface, the alert + // quota is interface independent and global for tether offload. Note that this is only + // accessed on the handler thread and in the constructor. + private long mRemainingAlertQuota = QUOTA_UNLIMITED; + // Runnable that used to schedule the next stats poll. + private final Runnable mScheduledPollingTask = () -> { + updateStatsForCurrentUpstream(); + maybeSchedulePollingStats(); + }; + + private int mNatUpdateCallbacksReceived; + private int mNatUpdateNetlinkErrors; + + @NonNull + private final Dependencies mDeps; + + // TODO: Put more parameters in constructor into dependency object. + interface Dependencies { + @NonNull + TetheringConfiguration getTetherConfig(); + } + + public OffloadController(Handler h, OffloadHardwareInterface hwi, + ContentResolver contentResolver, NetworkStatsManager nsm, SharedLog log, + @NonNull Dependencies deps) { + mHandler = h; + mHwInterface = hwi; + mContentResolver = contentResolver; + mLog = log.forSubComponent(TAG); + mDownstreams = new HashMap<>(); + mExemptPrefixes = new HashSet<>(); + mLastLocalPrefixStrs = new HashSet<>(); + OffloadTetheringStatsProvider provider = new OffloadTetheringStatsProvider(); + try { + nsm.registerNetworkStatsProvider(getClass().getSimpleName(), provider); + } catch (RuntimeException e) { + Log.wtf(TAG, "Cannot register offload stats provider: " + e); + provider = null; + } + mStatsProvider = provider; + mDeps = deps; + } + + /** Start hardware offload. */ + public boolean start() { + if (started()) return true; + + if (isOffloadDisabled()) { + mLog.i("tethering offload disabled"); + return false; + } + + if (!mConfigInitialized) { + mConfigInitialized = mHwInterface.initOffloadConfig(); + if (!mConfigInitialized) { + mLog.i("tethering offload config not supported"); + stop(); + return false; + } + } + + mControlHalVersion = mHwInterface.initOffloadControl( + // OffloadHardwareInterface guarantees that these callback + // methods are called on the handler passed to it, which is the + // same as mHandler, as coordinated by the setup in Tethering. + new OffloadHardwareInterface.ControlCallback() { + @Override + public void onStarted() { + if (!started()) return; + mLog.log("onStarted"); + } + + @Override + public void onStoppedError() { + if (!started()) return; + mLog.log("onStoppedError"); + } + + @Override + public void onStoppedUnsupported() { + if (!started()) return; + mLog.log("onStoppedUnsupported"); + // Poll for statistics and trigger a sweep of tethering + // stats by observers. This might not succeed, but it's + // worth trying anyway. We need to do this because from + // this point on we continue with software forwarding, + // and we need to synchronize stats and limits between + // software and hardware forwarding. + updateStatsForAllUpstreams(); + if (mStatsProvider != null) mStatsProvider.pushTetherStats(); + } + + @Override + public void onSupportAvailable() { + if (!started()) return; + mLog.log("onSupportAvailable"); + + // [1] Poll for statistics and trigger a sweep of stats + // by observers. We need to do this to ensure that any + // limits set take into account any software tethering + // traffic that has been happening in the meantime. + updateStatsForAllUpstreams(); + if (mStatsProvider != null) mStatsProvider.pushTetherStats(); + // [2] (Re)Push all state. + computeAndPushLocalPrefixes(UpdateType.FORCE); + pushAllDownstreamState(); + pushUpstreamParameters(null); + } + + @Override + public void onStoppedLimitReached() { + if (!started()) return; + mLog.log("onStoppedLimitReached"); + + // We cannot reliably determine on which interface the limit was reached, + // because the HAL interface does not specify it. We cannot just use the + // current upstream, because that might have changed since the time that + // the HAL queued the callback. + // TODO: rev the HAL so that it provides an interface name. + + updateStatsForCurrentUpstream(); + if (mStatsProvider != null) { + mStatsProvider.pushTetherStats(); + // Push stats to service does not cause the service react to it + // immediately. Inform the service about limit reached. + mStatsProvider.notifyLimitReached(); + } + } + + @Override + public void onWarningReached() { + if (!started()) return; + mLog.log("onWarningReached"); + + updateStatsForCurrentUpstream(); + if (mStatsProvider != null) { + mStatsProvider.pushTetherStats(); + mStatsProvider.notifyWarningReached(); + } + } + + @Override + public void onNatTimeoutUpdate(int proto, + String srcAddr, int srcPort, + String dstAddr, int dstPort) { + if (!started()) return; + updateNatTimeout(proto, srcAddr, srcPort, dstAddr, dstPort); + } + }); + + final boolean isStarted = started(); + if (!isStarted) { + mLog.i("tethering offload control not supported"); + stop(); + } else { + mLog.log("tethering offload started, version: " + + OffloadHardwareInterface.halVerToString(mControlHalVersion)); + mNatUpdateCallbacksReceived = 0; + mNatUpdateNetlinkErrors = 0; + maybeSchedulePollingStats(); + } + return isStarted; + } + + /** Stop hardware offload. */ + public void stop() { + // Completely stops tethering offload. After this method is called, it is no longer safe to + // call any HAL method, no callbacks from the hardware will be delivered, and any in-flight + // callbacks must be ignored. Offload may be started again by calling start(). + final boolean wasStarted = started(); + updateStatsForCurrentUpstream(); + mUpstreamLinkProperties = null; + mHwInterface.stopOffloadControl(); + mControlHalVersion = OFFLOAD_HAL_VERSION_NONE; + mConfigInitialized = false; + if (mHandler.hasCallbacks(mScheduledPollingTask)) { + mHandler.removeCallbacks(mScheduledPollingTask); + } + if (wasStarted) mLog.log("tethering offload stopped"); + } + + private boolean started() { + return mConfigInitialized && mControlHalVersion != OFFLOAD_HAL_VERSION_NONE; + } + + @VisibleForTesting + class OffloadTetheringStatsProvider extends NetworkStatsProvider { + // These stats must only ever be touched on the handler thread. + @NonNull + private NetworkStats mIfaceStats = new NetworkStats(0L, 0); + @NonNull + private NetworkStats mUidStats = new NetworkStats(0L, 0); + + /** + * A helper function that collect tether stats from local hashmap. Note that this does not + * invoke binder call. + */ + @VisibleForTesting + @NonNull + NetworkStats getTetherStats(@NonNull StatsType how) { + NetworkStats stats = new NetworkStats(0L, 0); + final int uid = (how == StatsType.STATS_PER_UID) ? UID_TETHERING : UID_ALL; + + for (final Map.Entry kv : mForwardedStats.entrySet()) { + final ForwardedStats value = kv.getValue(); + final Entry entry = new Entry(kv.getKey(), uid, SET_DEFAULT, TAG_NONE, METERED_NO, + ROAMING_NO, DEFAULT_NETWORK_NO, value.rxBytes, 0L, value.txBytes, 0L, 0L); + stats = stats.addEntry(entry); + } + + return stats; + } + + @Override + public void onSetLimit(String iface, long quotaBytes) { + onSetWarningAndLimit(iface, QUOTA_UNLIMITED, quotaBytes); + } + + @Override + public void onSetWarningAndLimit(@NonNull String iface, + long warningBytes, long limitBytes) { + // Listen for all iface is necessary since upstream might be changed after limit + // is set. + mHandler.post(() -> { + final InterfaceQuota curIfaceQuota = mInterfaceQuotas.get(iface); + final InterfaceQuota newIfaceQuota = new InterfaceQuota( + warningBytes == QUOTA_UNLIMITED ? Long.MAX_VALUE : warningBytes, + limitBytes == QUOTA_UNLIMITED ? Long.MAX_VALUE : limitBytes); + + // If the quota is set to unlimited, the value set to HAL is Long.MAX_VALUE, + // which is ~8.4 x 10^6 TiB, no one can actually reach it. Thus, it is not + // useful to set it multiple times. + // Otherwise, the quota needs to be updated to tell HAL to re-count from now even + // if the quota is the same as the existing one. + if (null == curIfaceQuota && InterfaceQuota.MAX_VALUE.equals(newIfaceQuota)) { + return; + } + + if (InterfaceQuota.MAX_VALUE.equals(newIfaceQuota)) { + mInterfaceQuotas.remove(iface); + } else { + mInterfaceQuotas.put(iface, newIfaceQuota); + } + maybeUpdateDataWarningAndLimit(iface); + }); + } + + /** + * Push stats to service, but does not cause a force polling. Note that this can only be + * called on the handler thread. + */ + public void pushTetherStats() { + // TODO: remove the accumulated stats and report the diff from HAL directly. + final NetworkStats ifaceDiff = + getTetherStats(StatsType.STATS_PER_IFACE).subtract(mIfaceStats); + final NetworkStats uidDiff = + getTetherStats(StatsType.STATS_PER_UID).subtract(mUidStats); + try { + notifyStatsUpdated(0 /* token */, ifaceDiff, uidDiff); + mIfaceStats = mIfaceStats.add(ifaceDiff); + mUidStats = mUidStats.add(uidDiff); + } catch (RuntimeException e) { + mLog.e("Cannot report network stats: ", e); + } + } + + @Override + public void onRequestStatsUpdate(int token) { + // Do not attempt to update stats by querying the offload HAL + // synchronously from a different thread than the Handler thread. http://b/64771555. + mHandler.post(() -> { + updateStatsForCurrentUpstream(); + pushTetherStats(); + }); + } + + @Override + public void onSetAlert(long quotaBytes) { + // Ignore set alert calls from HAL V1.1 since the hardware supports set warning now. + // Thus, the software polling mechanism is not needed. + if (!useStatsPolling()) { + return; + } + // Post it to handler thread since it access remaining quota bytes. + mHandler.post(() -> { + updateAlertQuota(quotaBytes); + maybeSchedulePollingStats(); + }); + } + } + + private String currentUpstreamInterface() { + return (mUpstreamLinkProperties != null) + ? mUpstreamLinkProperties.getInterfaceName() : null; + } + + private void maybeUpdateStats(String iface) { + if (TextUtils.isEmpty(iface)) { + return; + } + + // Always called on the handler thread. + // + // Use get()/put() instead of updating ForwardedStats in place because we can be called + // concurrently with getTetherStats. In combination with the guarantees provided by + // ConcurrentHashMap, this ensures that getTetherStats always gets the most recent copy of + // the stats for each interface, and does not observe partial writes where rxBytes is + // updated and txBytes is not. + ForwardedStats diff = mHwInterface.getForwardedStats(iface); + final long usedAlertQuota = diff.rxBytes + diff.txBytes; + ForwardedStats base = mForwardedStats.get(iface); + if (base != null) { + diff.add(base); + } + + // Update remaining alert quota if it is still positive. + if (mRemainingAlertQuota > 0 && usedAlertQuota > 0) { + // Trim to zero if overshoot. + final long newQuota = Math.max(mRemainingAlertQuota - usedAlertQuota, 0); + updateAlertQuota(newQuota); + } + + mForwardedStats.put(iface, diff); + // diff is a new object, just created by getForwardedStats(). Therefore, anyone reading from + // mForwardedStats (i.e., any caller of getTetherStats) will see the new stats immediately. + } + + /** + * Update remaining alert quota, fire the {@link NetworkStatsProvider#notifyAlertReached()} + * callback when it reaches zero. This can be invoked either from service setting the alert, or + * {@code maybeUpdateStats} when updating stats. Note that this can be only called on + * handler thread. + * + * @param newQuota non-negative value to indicate the new quota, or + * {@link NetworkStatsProvider#QUOTA_UNLIMITED} to indicate there is no + * quota. + */ + private void updateAlertQuota(long newQuota) { + if (newQuota < QUOTA_UNLIMITED) { + throw new IllegalArgumentException("invalid quota value " + newQuota); + } + if (mRemainingAlertQuota == newQuota) return; + + mRemainingAlertQuota = newQuota; + if (mRemainingAlertQuota == 0) { + mLog.i("notifyAlertReached"); + if (mStatsProvider != null) mStatsProvider.notifyAlertReached(); + } + } + + /** + * Schedule polling if needed, this will be stopped if offload has been + * stopped or remaining quota reaches zero or upstream is empty. + * Note that this can be only called on handler thread. + */ + private void maybeSchedulePollingStats() { + if (!isPollingStatsNeeded()) return; + + if (mHandler.hasCallbacks(mScheduledPollingTask)) { + mHandler.removeCallbacks(mScheduledPollingTask); + } + mHandler.postDelayed(mScheduledPollingTask, + mDeps.getTetherConfig().getOffloadPollInterval()); + } + + private boolean isPollingStatsNeeded() { + return started() && mRemainingAlertQuota > 0 + && useStatsPolling() + && !TextUtils.isEmpty(currentUpstreamInterface()) + && mDeps.getTetherConfig() != null + && mDeps.getTetherConfig().getOffloadPollInterval() + >= DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS; + } + + private boolean useStatsPolling() { + return mControlHalVersion == OFFLOAD_HAL_VERSION_1_0; + } + + private boolean maybeUpdateDataWarningAndLimit(String iface) { + // setDataLimit or setDataWarningAndLimit may only be called while offload is occurring + // on this upstream. + if (!started() || !TextUtils.equals(iface, currentUpstreamInterface())) { + return true; + } + + final InterfaceQuota quota = mInterfaceQuotas.getOrDefault(iface, InterfaceQuota.MAX_VALUE); + final boolean ret; + if (mControlHalVersion >= OFFLOAD_HAL_VERSION_1_1) { + ret = mHwInterface.setDataWarningAndLimit(iface, quota.warningBytes, quota.limitBytes); + } else { + ret = mHwInterface.setDataLimit(iface, quota.limitBytes); + } + return ret; + } + + private void updateStatsForCurrentUpstream() { + maybeUpdateStats(currentUpstreamInterface()); + } + + private void updateStatsForAllUpstreams() { + // In practice, there should only ever be a single digit number of + // upstream interfaces over the lifetime of an active tethering session. + // Roughly speaking, imagine a very ambitious one or two of each of the + // following interface types: [ "rmnet_data", "wlan", "eth", "rndis" ]. + for (Map.Entry kv : mForwardedStats.entrySet()) { + maybeUpdateStats(kv.getKey()); + } + } + + /** Set current tethering upstream LinkProperties. */ + public void setUpstreamLinkProperties(LinkProperties lp) { + if (!started() || Objects.equals(mUpstreamLinkProperties, lp)) return; + + final String prevUpstream = currentUpstreamInterface(); + + mUpstreamLinkProperties = (lp != null) ? new LinkProperties(lp) : null; + // Make sure we record this interface in the ForwardedStats map. + final String iface = currentUpstreamInterface(); + if (!TextUtils.isEmpty(iface)) mForwardedStats.putIfAbsent(iface, EMPTY_STATS); + + maybeSchedulePollingStats(); + + // TODO: examine return code and decide what to do if programming + // upstream parameters fails (probably just wait for a subsequent + // onOffloadEvent() callback to tell us offload is available again and + // then reapply all state). + computeAndPushLocalPrefixes(UpdateType.IF_NEEDED); + pushUpstreamParameters(prevUpstream); + } + + /** Set local prefixes. */ + public void setLocalPrefixes(Set localPrefixes) { + mExemptPrefixes = localPrefixes; + + if (!started()) return; + computeAndPushLocalPrefixes(UpdateType.IF_NEEDED); + } + + /** Update current downstream LinkProperties. */ + public void notifyDownstreamLinkProperties(LinkProperties lp) { + final String ifname = lp.getInterfaceName(); + final LinkProperties oldLp = mDownstreams.put(ifname, new LinkProperties(lp)); + if (Objects.equals(oldLp, lp)) return; + + if (!started()) return; + pushDownstreamState(oldLp, lp); + } + + private void pushDownstreamState(LinkProperties oldLp, LinkProperties newLp) { + final String ifname = newLp.getInterfaceName(); + final List oldRoutes = + (oldLp != null) ? oldLp.getRoutes() : Collections.EMPTY_LIST; + final List newRoutes = newLp.getRoutes(); + + // For each old route, if not in new routes: remove. + for (RouteInfo ri : oldRoutes) { + if (shouldIgnoreDownstreamRoute(ri)) continue; + if (!newRoutes.contains(ri)) { + mHwInterface.removeDownstreamPrefix(ifname, ri.getDestination().toString()); + } + } + + // For each new route, if not in old routes: add. + for (RouteInfo ri : newRoutes) { + if (shouldIgnoreDownstreamRoute(ri)) continue; + if (!oldRoutes.contains(ri)) { + mHwInterface.addDownstreamPrefix(ifname, ri.getDestination().toString()); + } + } + } + + private void pushAllDownstreamState() { + for (LinkProperties lp : mDownstreams.values()) { + pushDownstreamState(null, lp); + } + } + + /** Remove downstream interface from offload hardware. */ + public void removeDownstreamInterface(String ifname) { + final LinkProperties lp = mDownstreams.remove(ifname); + if (lp == null) return; + + if (!started()) return; + + for (RouteInfo route : lp.getRoutes()) { + if (shouldIgnoreDownstreamRoute(route)) continue; + mHwInterface.removeDownstreamPrefix(ifname, route.getDestination().toString()); + } + } + + private boolean isOffloadDisabled() { + final int defaultDisposition = mHwInterface.getDefaultTetherOffloadDisabled(); + return (Settings.Global.getInt( + mContentResolver, TETHER_OFFLOAD_DISABLED, defaultDisposition) != 0); + } + + private boolean pushUpstreamParameters(String prevUpstream) { + final String iface = currentUpstreamInterface(); + + if (TextUtils.isEmpty(iface)) { + final boolean rval = mHwInterface.setUpstreamParameters("", ANYIP, ANYIP, null); + // Update stats after we've told the hardware to stop forwarding so + // we don't miss packets. + maybeUpdateStats(prevUpstream); + return rval; + } + + // A stacked interface cannot be an upstream for hardware offload. + // Consequently, we examine only the primary interface name, look at + // getAddresses() rather than getAllAddresses(), and check getRoutes() + // rather than getAllRoutes(). + final ArrayList v6gateways = new ArrayList<>(); + String v4addr = null; + String v4gateway = null; + + for (InetAddress ip : mUpstreamLinkProperties.getAddresses()) { + if (ip instanceof Inet4Address) { + v4addr = ip.getHostAddress(); + break; + } + } + + // Find the gateway addresses of all default routes of either address family. + for (RouteInfo ri : mUpstreamLinkProperties.getRoutes()) { + if (!ri.hasGateway()) continue; + + final String gateway = ri.getGateway().getHostAddress(); + final InetAddress address = ri.getDestination().getAddress(); + if (ri.isDefaultRoute() && address instanceof Inet4Address) { + v4gateway = gateway; + } else if (ri.isDefaultRoute() && address instanceof Inet6Address) { + v6gateways.add(gateway); + } + } + + boolean success = mHwInterface.setUpstreamParameters( + iface, v4addr, v4gateway, (v6gateways.isEmpty() ? null : v6gateways)); + + if (!success) { + return success; + } + + // Update stats after we've told the hardware to change routing so we don't miss packets. + maybeUpdateStats(prevUpstream); + + // Data limits can only be set once offload is running on the upstream. + success = maybeUpdateDataWarningAndLimit(iface); + if (!success) { + // If we failed to set a data limit, don't use this upstream, because we don't want to + // blow through the data limit that we were told to apply. + mLog.log("Setting data limit for " + iface + " failed, disabling offload."); + stop(); + } + + return success; + } + + private boolean computeAndPushLocalPrefixes(UpdateType how) { + final boolean force = (how == UpdateType.FORCE); + final Set localPrefixStrs = computeLocalPrefixStrings( + mExemptPrefixes, mUpstreamLinkProperties); + if (!force && mLastLocalPrefixStrs.equals(localPrefixStrs)) return true; + + mLastLocalPrefixStrs = localPrefixStrs; + return mHwInterface.setLocalPrefixes(new ArrayList<>(localPrefixStrs)); + } + + // TODO: Factor in downstream LinkProperties once that information is available. + private static Set computeLocalPrefixStrings( + Set localPrefixes, LinkProperties upstreamLinkProperties) { + // Create an editable copy. + final Set prefixSet = new HashSet<>(localPrefixes); + + // TODO: If a downstream interface (not currently passed in) is reusing + // the /64 of the upstream (64share) then: + // + // [a] remove that /64 from the local prefixes + // [b] add in /128s for IP addresses on the downstream interface + // [c] add in /128s for IP addresses on the upstream interface + // + // Until downstream information is available here, simply add /128s from + // the upstream network; they'll just be redundant with their /64. + if (upstreamLinkProperties != null) { + for (LinkAddress linkAddr : upstreamLinkProperties.getLinkAddresses()) { + if (!linkAddr.isGlobalPreferred()) continue; + final InetAddress ip = linkAddr.getAddress(); + if (!(ip instanceof Inet6Address)) continue; + prefixSet.add(new IpPrefix(ip, 128)); + } + } + + final HashSet localPrefixStrs = new HashSet<>(); + for (IpPrefix pfx : prefixSet) localPrefixStrs.add(pfx.toString()); + return localPrefixStrs; + } + + private static boolean shouldIgnoreDownstreamRoute(RouteInfo route) { + // Ignore any link-local routes. + final IpPrefix destination = route.getDestination(); + final LinkAddress linkAddr = new LinkAddress(destination.getAddress(), + destination.getPrefixLength()); + if (!linkAddr.isGlobalPreferred()) return true; + + return false; + } + + /** Dump information. */ + public void dump(IndentingPrintWriter pw) { + if (isOffloadDisabled()) { + pw.println("Offload disabled"); + return; + } + final boolean isStarted = started(); + pw.println("Offload HALs " + (isStarted ? "started" : "not started")); + pw.println("Offload Control HAL version: " + + OffloadHardwareInterface.halVerToString(mControlHalVersion)); + LinkProperties lp = mUpstreamLinkProperties; + String upstream = (lp != null) ? lp.getInterfaceName() : null; + pw.println("Current upstream: " + upstream); + pw.println("Exempt prefixes: " + mLastLocalPrefixStrs); + pw.println("NAT timeout update callbacks received during the " + + (isStarted ? "current" : "last") + + " offload session: " + + mNatUpdateCallbacksReceived); + pw.println("NAT timeout update netlink errors during the " + + (isStarted ? "current" : "last") + + " offload session: " + + mNatUpdateNetlinkErrors); + } + + private void updateNatTimeout( + int proto, String srcAddr, int srcPort, String dstAddr, int dstPort) { + final String protoName = protoNameFor(proto); + if (protoName == null) { + mLog.e("Unknown NAT update callback protocol: " + proto); + return; + } + + final Inet4Address src = parseIPv4Address(srcAddr); + if (src == null) { + mLog.e("Failed to parse IPv4 address: " + srcAddr); + return; + } + + if (!isValidUdpOrTcpPort(srcPort)) { + mLog.e("Invalid src port: " + srcPort); + return; + } + + final Inet4Address dst = parseIPv4Address(dstAddr); + if (dst == null) { + mLog.e("Failed to parse IPv4 address: " + dstAddr); + return; + } + + if (!isValidUdpOrTcpPort(dstPort)) { + mLog.e("Invalid dst port: " + dstPort); + return; + } + + mNatUpdateCallbacksReceived++; + final String natDescription = String.format("%s (%s, %s) -> (%s, %s)", + protoName, srcAddr, srcPort, dstAddr, dstPort); + if (DBG) { + mLog.log("NAT timeout update: " + natDescription); + } + + final int timeoutSec = connectionTimeoutUpdateSecondsFor(proto); + final byte[] msg = ConntrackMessage.newIPv4TimeoutUpdateRequest( + proto, src, srcPort, dst, dstPort, timeoutSec); + + try { + NetlinkSocket.sendOneShotKernelMessage(OsConstants.NETLINK_NETFILTER, msg); + } catch (ErrnoException e) { + mNatUpdateNetlinkErrors++; + mLog.e("Error updating NAT conntrack entry >" + natDescription + "<: " + e + + ", msg: " + NetlinkConstants.hexify(msg)); + mLog.log("NAT timeout update callbacks received: " + mNatUpdateCallbacksReceived); + mLog.log("NAT timeout update netlink errors: " + mNatUpdateNetlinkErrors); + } + } + + private static Inet4Address parseIPv4Address(String addrString) { + try { + final InetAddress ip = InetAddresses.parseNumericAddress(addrString); + // TODO: Consider other sanitization steps here, including perhaps: + // not eql to 0.0.0.0 + // not within 169.254.0.0/16 + // not within ::ffff:0.0.0.0/96 + // not within ::/96 + // et cetera. + if (ip instanceof Inet4Address) { + return (Inet4Address) ip; + } + } catch (IllegalArgumentException iae) { } + return null; + } + + private static String protoNameFor(int proto) { + // OsConstants values are not constant expressions; no switch statement. + if (proto == OsConstants.IPPROTO_UDP) { + return "UDP"; + } else if (proto == OsConstants.IPPROTO_TCP) { + return "TCP"; + } + return null; + } + + private static int connectionTimeoutUpdateSecondsFor(int proto) { + // TODO: Replace this with more thoughtful work, perhaps reading from + // and maybe writing to any required + // + // /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_* + // /proc/sys/net/netfilter/nf_conntrack_udp_timeout{,_stream} + // + // entries. TBD. + if (proto == OsConstants.IPPROTO_TCP) { + // Cf. /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established + return 432000; + } else { + // Cf. /proc/sys/net/netfilter/nf_conntrack_udp_timeout_stream + return 180; + } + } + + private static boolean isValidUdpOrTcpPort(int port) { + return port > 0 && port < 65536; + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java new file mode 100644 index 0000000000..e3ac660910 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java @@ -0,0 +1,695 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import static android.net.netlink.StructNlMsgHdr.NLM_F_DUMP; +import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST; +import static android.net.util.TetheringUtils.uint16; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.hardware.tetheroffload.config.V1_0.IOffloadConfig; +import android.hardware.tetheroffload.control.V1_0.IOffloadControl; +import android.hardware.tetheroffload.control.V1_0.NatTimeoutUpdate; +import android.hardware.tetheroffload.control.V1_0.NetworkProtocol; +import android.hardware.tetheroffload.control.V1_0.OffloadCallbackEvent; +import android.hardware.tetheroffload.control.V1_1.ITetheringOffloadCallback; +import android.net.netlink.NetlinkSocket; +import android.net.netlink.StructNfGenMsg; +import android.net.netlink.StructNlMsgHdr; +import android.net.util.SharedLog; +import android.net.util.SocketUtils; +import android.os.Handler; +import android.os.NativeHandle; +import android.os.RemoteException; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; +import android.util.Log; +import android.util.Pair; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.SocketAddress; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.NoSuchElementException; + + +/** + * Capture tethering dependencies, for injection. + * + * @hide + */ +public class OffloadHardwareInterface { + private static final String TAG = OffloadHardwareInterface.class.getSimpleName(); + private static final String YIELDS = " -> "; + // Change this value to control whether tether offload is enabled or + // disabled by default in the absence of an explicit Settings value. + // See accompanying unittest to distinguish 0 from non-0 values. + private static final int DEFAULT_TETHER_OFFLOAD_DISABLED = 0; + private static final String NO_INTERFACE_NAME = ""; + private static final String NO_IPV4_ADDRESS = ""; + private static final String NO_IPV4_GATEWAY = ""; + // Reference kernel/uapi/linux/netfilter/nfnetlink_compat.h + public static final int NF_NETLINK_CONNTRACK_NEW = 1; + public static final int NF_NETLINK_CONNTRACK_UPDATE = 2; + public static final int NF_NETLINK_CONNTRACK_DESTROY = 4; + // Reference libnetfilter_conntrack/linux_nfnetlink_conntrack.h + public static final short NFNL_SUBSYS_CTNETLINK = 1; + public static final short IPCTNL_MSG_CT_NEW = 0; + public static final short IPCTNL_MSG_CT_GET = 1; + + private final long NETLINK_MESSAGE_TIMEOUT_MS = 500; + + private final Handler mHandler; + private final SharedLog mLog; + private final Dependencies mDeps; + private IOffloadControl mOffloadControl; + + // TODO: Use major-minor version control to prevent from defining new constants. + static final int OFFLOAD_HAL_VERSION_NONE = 0; + static final int OFFLOAD_HAL_VERSION_1_0 = 1; + static final int OFFLOAD_HAL_VERSION_1_1 = 2; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = "OFFLOAD_HAL_VERSION_", value = { + OFFLOAD_HAL_VERSION_NONE, + OFFLOAD_HAL_VERSION_1_0, + OFFLOAD_HAL_VERSION_1_1 + }) + public @interface OffloadHalVersion {} + @OffloadHalVersion + private int mOffloadControlVersion = OFFLOAD_HAL_VERSION_NONE; + + @NonNull + static String halVerToString(int version) { + switch(version) { + case OFFLOAD_HAL_VERSION_1_0: + return "1.0"; + case OFFLOAD_HAL_VERSION_1_1: + return "1.1"; + case OFFLOAD_HAL_VERSION_NONE: + return "None"; + default: + throw new IllegalArgumentException("Unsupported version int " + version); + } + + } + + private TetheringOffloadCallback mTetheringOffloadCallback; + private ControlCallback mControlCallback; + + /** The callback to notify status of offload management process. */ + public static class ControlCallback { + /** Offload started. */ + public void onStarted() {} + /** + * Offload stopped because an error has occurred in lower layer. + */ + public void onStoppedError() {} + /** + * Offload stopped because the device has moved to a bearer on which hardware offload is + * not supported. Subsequent calls to setUpstreamParameters and add/removeDownstream will + * likely fail and cannot be presumed to be saved inside of the hardware management process. + * Upon receiving #onSupportAvailable(), the caller should reprogram the hardware to begin + * offload again. + */ + public void onStoppedUnsupported() {} + /** Indicate that offload is able to proivde support for this time. */ + public void onSupportAvailable() {} + /** Offload stopped because of usage limit reached. */ + public void onStoppedLimitReached() {} + /** Indicate that data warning quota is reached. */ + public void onWarningReached() {} + + /** Indicate to update NAT timeout. */ + public void onNatTimeoutUpdate(int proto, + String srcAddr, int srcPort, + String dstAddr, int dstPort) {} + } + + /** The object which records Tx/Rx forwarded bytes. */ + public static class ForwardedStats { + public long rxBytes; + public long txBytes; + + public ForwardedStats() { + rxBytes = 0; + txBytes = 0; + } + + @VisibleForTesting + public ForwardedStats(long rxBytes, long txBytes) { + this.rxBytes = rxBytes; + this.txBytes = txBytes; + } + + /** Add Tx/Rx bytes. */ + public void add(ForwardedStats other) { + rxBytes += other.rxBytes; + txBytes += other.txBytes; + } + + /** Returns the string representation of this object. */ + public String toString() { + return String.format("rx:%s tx:%s", rxBytes, txBytes); + } + } + + public OffloadHardwareInterface(Handler h, SharedLog log) { + this(h, log, new Dependencies(log)); + } + + OffloadHardwareInterface(Handler h, SharedLog log, Dependencies deps) { + mHandler = h; + mLog = log.forSubComponent(TAG); + mDeps = deps; + } + + /** Capture OffloadHardwareInterface dependencies, for injection. */ + static class Dependencies { + private final SharedLog mLog; + + Dependencies(SharedLog log) { + mLog = log; + } + + public IOffloadConfig getOffloadConfig() { + try { + return IOffloadConfig.getService(true /*retry*/); + } catch (RemoteException | NoSuchElementException e) { + mLog.e("getIOffloadConfig error " + e); + return null; + } + } + + @NonNull + public Pair getOffloadControl() { + IOffloadControl hal = null; + int version = OFFLOAD_HAL_VERSION_NONE; + try { + hal = android.hardware.tetheroffload.control + .V1_1.IOffloadControl.getService(true /*retry*/); + version = OFFLOAD_HAL_VERSION_1_1; + } catch (NoSuchElementException e) { + // Unsupported by device. + } catch (RemoteException e) { + mLog.e("Unable to get offload control " + OFFLOAD_HAL_VERSION_1_1); + } + if (hal == null) { + try { + hal = IOffloadControl.getService(true /*retry*/); + version = OFFLOAD_HAL_VERSION_1_0; + } catch (NoSuchElementException e) { + // Unsupported by device. + } catch (RemoteException e) { + mLog.e("Unable to get offload control " + OFFLOAD_HAL_VERSION_1_0); + } + } + return new Pair(hal, version); + } + + public NativeHandle createConntrackSocket(final int groups) { + final FileDescriptor fd; + try { + fd = NetlinkSocket.forProto(OsConstants.NETLINK_NETFILTER); + } catch (ErrnoException e) { + mLog.e("Unable to create conntrack socket " + e); + return null; + } + + final SocketAddress sockAddr = SocketUtils.makeNetlinkSocketAddress(0, groups); + try { + Os.bind(fd, sockAddr); + } catch (ErrnoException | SocketException e) { + mLog.e("Unable to bind conntrack socket for groups " + groups + " error: " + e); + try { + SocketUtils.closeSocket(fd); + } catch (IOException ie) { + // Nothing we can do here + } + return null; + } + try { + Os.connect(fd, sockAddr); + } catch (ErrnoException | SocketException e) { + mLog.e("connect to kernel fail for groups " + groups + " error: " + e); + try { + SocketUtils.closeSocket(fd); + } catch (IOException ie) { + // Nothing we can do here + } + return null; + } + + return new NativeHandle(fd, true); + } + } + + /** Get default value indicating whether offload is supported. */ + public int getDefaultTetherOffloadDisabled() { + return DEFAULT_TETHER_OFFLOAD_DISABLED; + } + + /** + * Offload management process need to know conntrack rules to support NAT, but it may not have + * permission to create netlink netfilter sockets. Create two netlink netfilter sockets and + * share them with offload management process. + */ + public boolean initOffloadConfig() { + final IOffloadConfig offloadConfig = mDeps.getOffloadConfig(); + if (offloadConfig == null) { + mLog.e("Could not find IOffloadConfig service"); + return false; + } + // Per the IConfigOffload definition: + // + // h1 provides a file descriptor bound to the following netlink groups + // (NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY). + // + // h2 provides a file descriptor bound to the following netlink groups + // (NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY). + final NativeHandle h1 = mDeps.createConntrackSocket( + NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY); + if (h1 == null) return false; + + sendIpv4NfGenMsg(h1, (short) ((NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_GET), + (short) (NLM_F_REQUEST | NLM_F_DUMP)); + + final NativeHandle h2 = mDeps.createConntrackSocket( + NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY); + if (h2 == null) { + closeFdInNativeHandle(h1); + return false; + } + + final CbResults results = new CbResults(); + try { + offloadConfig.setHandles(h1, h2, + (boolean success, String errMsg) -> { + results.mSuccess = success; + results.mErrMsg = errMsg; + }); + } catch (RemoteException e) { + record("initOffloadConfig, setHandles fail", e); + return false; + } + // Explicitly close FDs. + closeFdInNativeHandle(h1); + closeFdInNativeHandle(h2); + + record("initOffloadConfig, setHandles results:", results); + return results.mSuccess; + } + + @VisibleForTesting + public void sendIpv4NfGenMsg(@NonNull NativeHandle handle, short type, short flags) { + final int length = StructNlMsgHdr.STRUCT_SIZE + StructNfGenMsg.STRUCT_SIZE; + final byte[] msg = new byte[length]; + final ByteBuffer byteBuffer = ByteBuffer.wrap(msg); + byteBuffer.order(ByteOrder.nativeOrder()); + + final StructNlMsgHdr nlh = new StructNlMsgHdr(); + nlh.nlmsg_len = length; + nlh.nlmsg_type = type; + nlh.nlmsg_flags = flags; + nlh.nlmsg_seq = 0; + nlh.pack(byteBuffer); + + // Header needs to be added to buffer since a generic netlink request is being sent. + final StructNfGenMsg nfh = new StructNfGenMsg((byte) OsConstants.AF_INET); + nfh.pack(byteBuffer); + + try { + NetlinkSocket.sendMessage(handle.getFileDescriptor(), msg, 0 /* offset */, length, + NETLINK_MESSAGE_TIMEOUT_MS); + } catch (ErrnoException | InterruptedIOException e) { + mLog.e("Unable to send netfilter message, error: " + e); + } + } + + private void closeFdInNativeHandle(final NativeHandle h) { + try { + h.close(); + } catch (IOException | IllegalStateException e) { + // IllegalStateException means fd is already closed, do nothing here. + // Also nothing we can do if IOException. + } + } + + /** + * Initialize the tethering offload HAL. + * + * @return one of {@code OFFLOAD_HAL_VERSION_*} represents the HAL version, or + * {@link #OFFLOAD_HAL_VERSION_NONE} if failed. + */ + public int initOffloadControl(ControlCallback controlCb) { + mControlCallback = controlCb; + + if (mOffloadControl == null) { + final Pair halAndVersion = mDeps.getOffloadControl(); + mOffloadControl = halAndVersion.first; + mOffloadControlVersion = halAndVersion.second; + if (mOffloadControl == null) { + mLog.e("tethering IOffloadControl.getService() returned null"); + return OFFLOAD_HAL_VERSION_NONE; + } + mLog.i("tethering offload control version " + + halVerToString(mOffloadControlVersion) + " is supported."); + } + + final String logmsg = String.format("initOffloadControl(%s)", + (controlCb == null) ? "null" + : "0x" + Integer.toHexString(System.identityHashCode(controlCb))); + + mTetheringOffloadCallback = new TetheringOffloadCallback( + mHandler, mControlCallback, mLog, mOffloadControlVersion); + final CbResults results = new CbResults(); + try { + mOffloadControl.initOffload( + mTetheringOffloadCallback, + (boolean success, String errMsg) -> { + results.mSuccess = success; + results.mErrMsg = errMsg; + }); + } catch (RemoteException e) { + record(logmsg, e); + return OFFLOAD_HAL_VERSION_NONE; + } + + record(logmsg, results); + return results.mSuccess ? mOffloadControlVersion : OFFLOAD_HAL_VERSION_NONE; + } + + /** Stop IOffloadControl. */ + public void stopOffloadControl() { + if (mOffloadControl != null) { + try { + mOffloadControl.stopOffload( + (boolean success, String errMsg) -> { + if (!success) mLog.e("stopOffload failed: " + errMsg); + }); + } catch (RemoteException e) { + mLog.e("failed to stopOffload: " + e); + } + } + mOffloadControl = null; + mTetheringOffloadCallback = null; + mControlCallback = null; + mLog.log("stopOffloadControl()"); + } + + /** Get Tx/Rx usage from last query. */ + public ForwardedStats getForwardedStats(String upstream) { + final String logmsg = String.format("getForwardedStats(%s)", upstream); + + final ForwardedStats stats = new ForwardedStats(); + try { + mOffloadControl.getForwardedStats( + upstream, + (long rxBytes, long txBytes) -> { + stats.rxBytes = (rxBytes > 0) ? rxBytes : 0; + stats.txBytes = (txBytes > 0) ? txBytes : 0; + }); + } catch (RemoteException e) { + record(logmsg, e); + return stats; + } + + return stats; + } + + /** Set local prefixes to offload management process. */ + public boolean setLocalPrefixes(ArrayList localPrefixes) { + final String logmsg = String.format("setLocalPrefixes([%s])", + String.join(",", localPrefixes)); + + final CbResults results = new CbResults(); + try { + mOffloadControl.setLocalPrefixes(localPrefixes, + (boolean success, String errMsg) -> { + results.mSuccess = success; + results.mErrMsg = errMsg; + }); + } catch (RemoteException e) { + record(logmsg, e); + return false; + } + + record(logmsg, results); + return results.mSuccess; + } + + /** Set data limit value to offload management process. */ + public boolean setDataLimit(String iface, long limit) { + + final String logmsg = String.format("setDataLimit(%s, %d)", iface, limit); + + final CbResults results = new CbResults(); + try { + mOffloadControl.setDataLimit( + iface, limit, + (boolean success, String errMsg) -> { + results.mSuccess = success; + results.mErrMsg = errMsg; + }); + } catch (RemoteException e) { + record(logmsg, e); + return false; + } + + record(logmsg, results); + return results.mSuccess; + } + + /** Set data warning and limit value to offload management process. */ + public boolean setDataWarningAndLimit(String iface, long warning, long limit) { + if (mOffloadControlVersion < OFFLOAD_HAL_VERSION_1_1) { + throw new IllegalArgumentException( + "setDataWarningAndLimit is not supported below HAL V1.1"); + } + final String logmsg = + String.format("setDataWarningAndLimit(%s, %d, %d)", iface, warning, limit); + + final CbResults results = new CbResults(); + try { + ((android.hardware.tetheroffload.control.V1_1.IOffloadControl) mOffloadControl) + .setDataWarningAndLimit( + iface, warning, limit, + (boolean success, String errMsg) -> { + results.mSuccess = success; + results.mErrMsg = errMsg; + }); + } catch (RemoteException e) { + record(logmsg, e); + return false; + } + + record(logmsg, results); + return results.mSuccess; + } + + /** Set upstream parameters to offload management process. */ + public boolean setUpstreamParameters( + String iface, String v4addr, String v4gateway, ArrayList v6gws) { + iface = (iface != null) ? iface : NO_INTERFACE_NAME; + v4addr = (v4addr != null) ? v4addr : NO_IPV4_ADDRESS; + v4gateway = (v4gateway != null) ? v4gateway : NO_IPV4_GATEWAY; + v6gws = (v6gws != null) ? v6gws : new ArrayList<>(); + + final String logmsg = String.format("setUpstreamParameters(%s, %s, %s, [%s])", + iface, v4addr, v4gateway, String.join(",", v6gws)); + + final CbResults results = new CbResults(); + try { + mOffloadControl.setUpstreamParameters( + iface, v4addr, v4gateway, v6gws, + (boolean success, String errMsg) -> { + results.mSuccess = success; + results.mErrMsg = errMsg; + }); + } catch (RemoteException e) { + record(logmsg, e); + return false; + } + + record(logmsg, results); + return results.mSuccess; + } + + /** Add downstream prefix to offload management process. */ + public boolean addDownstreamPrefix(String ifname, String prefix) { + final String logmsg = String.format("addDownstreamPrefix(%s, %s)", ifname, prefix); + + final CbResults results = new CbResults(); + try { + mOffloadControl.addDownstream(ifname, prefix, + (boolean success, String errMsg) -> { + results.mSuccess = success; + results.mErrMsg = errMsg; + }); + } catch (RemoteException e) { + record(logmsg, e); + return false; + } + + record(logmsg, results); + return results.mSuccess; + } + + /** Remove downstream prefix from offload management process. */ + public boolean removeDownstreamPrefix(String ifname, String prefix) { + final String logmsg = String.format("removeDownstreamPrefix(%s, %s)", ifname, prefix); + + final CbResults results = new CbResults(); + try { + mOffloadControl.removeDownstream(ifname, prefix, + (boolean success, String errMsg) -> { + results.mSuccess = success; + results.mErrMsg = errMsg; + }); + } catch (RemoteException e) { + record(logmsg, e); + return false; + } + + record(logmsg, results); + return results.mSuccess; + } + + private void record(String msg, Throwable t) { + mLog.e(msg + YIELDS + "exception: " + t); + } + + private void record(String msg, CbResults results) { + final String logmsg = msg + YIELDS + results; + if (!results.mSuccess) { + mLog.e(logmsg); + } else { + mLog.log(logmsg); + } + } + + private static class TetheringOffloadCallback extends ITetheringOffloadCallback.Stub { + public final Handler handler; + public final ControlCallback controlCb; + public final SharedLog log; + private final int mOffloadControlVersion; + + TetheringOffloadCallback( + Handler h, ControlCallback cb, SharedLog sharedLog, int offloadControlVersion) { + handler = h; + controlCb = cb; + log = sharedLog; + this.mOffloadControlVersion = offloadControlVersion; + } + + private void handleOnEvent(int event) { + switch (event) { + case OffloadCallbackEvent.OFFLOAD_STARTED: + controlCb.onStarted(); + break; + case OffloadCallbackEvent.OFFLOAD_STOPPED_ERROR: + controlCb.onStoppedError(); + break; + case OffloadCallbackEvent.OFFLOAD_STOPPED_UNSUPPORTED: + controlCb.onStoppedUnsupported(); + break; + case OffloadCallbackEvent.OFFLOAD_SUPPORT_AVAILABLE: + controlCb.onSupportAvailable(); + break; + case OffloadCallbackEvent.OFFLOAD_STOPPED_LIMIT_REACHED: + controlCb.onStoppedLimitReached(); + break; + case android.hardware.tetheroffload.control + .V1_1.OffloadCallbackEvent.OFFLOAD_WARNING_REACHED: + controlCb.onWarningReached(); + break; + default: + log.e("Unsupported OffloadCallbackEvent: " + event); + } + } + + @Override + public void onEvent(int event) { + // The implementation should never call onEvent()) if the event is already reported + // through newer callback. + if (mOffloadControlVersion > OFFLOAD_HAL_VERSION_1_0) { + Log.wtf(TAG, "onEvent(" + event + ") fired on HAL " + + halVerToString(mOffloadControlVersion)); + } + handler.post(() -> { + handleOnEvent(event); + }); + } + + @Override + public void onEvent_1_1(int event) { + if (mOffloadControlVersion < OFFLOAD_HAL_VERSION_1_1) { + Log.wtf(TAG, "onEvent_1_1(" + event + ") fired on HAL " + + halVerToString(mOffloadControlVersion)); + return; + } + handler.post(() -> { + handleOnEvent(event); + }); + } + + @Override + public void updateTimeout(NatTimeoutUpdate params) { + handler.post(() -> { + controlCb.onNatTimeoutUpdate( + networkProtocolToOsConstant(params.proto), + params.src.addr, uint16(params.src.port), + params.dst.addr, uint16(params.dst.port)); + }); + } + } + + private static int networkProtocolToOsConstant(int proto) { + switch (proto) { + case NetworkProtocol.TCP: return OsConstants.IPPROTO_TCP; + case NetworkProtocol.UDP: return OsConstants.IPPROTO_UDP; + default: + // The caller checks this value and will log an error. Just make + // sure it won't collide with valid OsContants.IPPROTO_* values. + return -Math.abs(proto); + } + } + + private static class CbResults { + boolean mSuccess; + String mErrMsg; + + @Override + public String toString() { + if (mSuccess) { + return "ok"; + } else { + return "fail: " + mErrMsg; + } + } + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java new file mode 100644 index 0000000000..4f616cdff0 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java @@ -0,0 +1,416 @@ +/* + * 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.networkstack.tethering; + +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.TetheringManager.TETHERING_BLUETOOTH; +import static android.net.TetheringManager.TETHERING_WIFI_P2P; +import static android.net.util.PrefixUtils.asIpPrefix; + +import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH; +import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH; +import static com.android.net.module.util.Inet4AddressUtils.prefixLengthToV4NetmaskIntHTH; + +import static java.util.Arrays.asList; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.Network; +import android.net.ip.IpServer; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.SparseArray; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +/** + * This class coordinate IP addresses conflict problem. + * + * Tethering downstream IP addresses may conflict with network assigned addresses. This + * coordinator is responsible for recording all of network assigned addresses and dispatched + * free address to downstream interfaces. + * + * This class is not thread-safe and should be accessed on the same tethering internal thread. + * @hide + */ +public class PrivateAddressCoordinator { + public static final int PREFIX_LENGTH = 24; + + // Upstream monitor would be stopped when tethering is down. When tethering restart, downstream + // address may be requested before coordinator get current upstream notification. To ensure + // coordinator do not select conflict downstream prefix, mUpstreamPrefixMap would not be cleared + // when tethering is down. Instead tethering would remove all deprecated upstreams from + // mUpstreamPrefixMap when tethering is starting. See #maybeRemoveDeprecatedUpstreams(). + private final ArrayMap> mUpstreamPrefixMap; + private final ArraySet mDownstreams; + private static final String LEGACY_WIFI_P2P_IFACE_ADDRESS = "192.168.49.1/24"; + private static final String LEGACY_BLUETOOTH_IFACE_ADDRESS = "192.168.44.1/24"; + private final List mTetheringPrefixes; + private final ConnectivityManager mConnectivityMgr; + private final TetheringConfiguration mConfig; + // keyed by downstream type(TetheringManager.TETHERING_*). + private final SparseArray mCachedAddresses; + + public PrivateAddressCoordinator(Context context, TetheringConfiguration config) { + mDownstreams = new ArraySet<>(); + mUpstreamPrefixMap = new ArrayMap<>(); + mConnectivityMgr = (ConnectivityManager) context.getSystemService( + Context.CONNECTIVITY_SERVICE); + mConfig = config; + mCachedAddresses = new SparseArray<>(); + // Reserved static addresses for bluetooth and wifi p2p. + mCachedAddresses.put(TETHERING_BLUETOOTH, new LinkAddress(LEGACY_BLUETOOTH_IFACE_ADDRESS)); + mCachedAddresses.put(TETHERING_WIFI_P2P, new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS)); + + mTetheringPrefixes = new ArrayList<>(Arrays.asList(new IpPrefix("192.168.0.0/16"))); + if (config.isSelectAllPrefixRangeEnabled()) { + mTetheringPrefixes.add(new IpPrefix("172.16.0.0/12")); + mTetheringPrefixes.add(new IpPrefix("10.0.0.0/8")); + } + } + + /** + * Record a new upstream IpPrefix which may conflict with tethering downstreams. + * The downstreams will be notified if a conflict is found. When updateUpstreamPrefix is called, + * UpstreamNetworkState must have an already populated LinkProperties. + */ + public void updateUpstreamPrefix(final UpstreamNetworkState ns) { + // Do not support VPN as upstream. Normally, networkCapabilities is not expected to be null, + // but just checking to be sure. + if (ns.networkCapabilities != null && ns.networkCapabilities.hasTransport(TRANSPORT_VPN)) { + removeUpstreamPrefix(ns.network); + return; + } + + final ArrayList ipv4Prefixes = getIpv4Prefixes( + ns.linkProperties.getAllLinkAddresses()); + if (ipv4Prefixes.isEmpty()) { + removeUpstreamPrefix(ns.network); + return; + } + + mUpstreamPrefixMap.put(ns.network, ipv4Prefixes); + handleMaybePrefixConflict(ipv4Prefixes); + } + + private ArrayList getIpv4Prefixes(final List linkAddresses) { + final ArrayList list = new ArrayList<>(); + for (LinkAddress address : linkAddresses) { + if (!address.isIpv4()) continue; + + list.add(asIpPrefix(address)); + } + + return list; + } + + private void handleMaybePrefixConflict(final List prefixes) { + for (IpServer downstream : mDownstreams) { + final IpPrefix target = getDownstreamPrefix(downstream); + + for (IpPrefix source : prefixes) { + if (isConflictPrefix(source, target)) { + downstream.sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT); + break; + } + } + } + } + + /** Remove IpPrefix records corresponding to input network. */ + public void removeUpstreamPrefix(final Network network) { + mUpstreamPrefixMap.remove(network); + } + + /** + * Maybe remove deprecated upstream records, this would be called once tethering started without + * any exiting tethered downstream. + */ + public void maybeRemoveDeprecatedUpstreams() { + if (mUpstreamPrefixMap.isEmpty()) return; + + // Remove all upstreams that are no longer valid networks + final Set toBeRemoved = new HashSet<>(mUpstreamPrefixMap.keySet()); + toBeRemoved.removeAll(asList(mConnectivityMgr.getAllNetworks())); + + mUpstreamPrefixMap.removeAll(toBeRemoved); + } + + /** + * Pick a random available address and mark its prefix as in use for the provided IpServer, + * returns null if there is no available address. + */ + @Nullable + public LinkAddress requestDownstreamAddress(final IpServer ipServer, boolean useLastAddress) { + if (mConfig.shouldEnableWifiP2pDedicatedIp() + && ipServer.interfaceType() == TETHERING_WIFI_P2P) { + return new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS); + } + + final LinkAddress cachedAddress = mCachedAddresses.get(ipServer.interfaceType()); + if (useLastAddress && cachedAddress != null + && !isConflictWithUpstream(asIpPrefix(cachedAddress))) { + mDownstreams.add(ipServer); + return cachedAddress; + } + + for (IpPrefix prefixRange : mTetheringPrefixes) { + final LinkAddress newAddress = chooseDownstreamAddress(prefixRange); + if (newAddress != null) { + mDownstreams.add(ipServer); + mCachedAddresses.put(ipServer.interfaceType(), newAddress); + return newAddress; + } + } + + // No available address. + return null; + } + + private int getPrefixBaseAddress(final IpPrefix prefix) { + return inet4AddressToIntHTH((Inet4Address) prefix.getAddress()); + } + + /** + * Check whether input prefix conflict with upstream prefixes or in-use downstream prefixes. + * If yes, return one of them. + */ + private IpPrefix getConflictPrefix(final IpPrefix prefix) { + final IpPrefix upstream = getConflictWithUpstream(prefix); + if (upstream != null) return upstream; + + return getInUseDownstreamPrefix(prefix); + } + + // Get the next non-conflict sub prefix. E.g: To get next sub prefix from 10.0.0.0/8, if the + // previously selected prefix is 10.20.42.0/24(subPrefix: 0.20.42.0) and the conflicting prefix + // is 10.16.0.0/20 (10.16.0.0 ~ 10.16.15.255), then the max address under subPrefix is + // 0.16.15.255 and the next subPrefix is 0.16.16.255/24 (0.16.15.255 + 0.0.1.0). + // Note: the sub address 0.0.0.255 here is fine to be any value that it will be replaced as + // selected random sub address later. + private int getNextSubPrefix(final IpPrefix conflictPrefix, final int prefixRangeMask) { + final int suffixMask = ~prefixLengthToV4NetmaskIntHTH(conflictPrefix.getPrefixLength()); + // The largest offset within the prefix assignment block that still conflicts with + // conflictPrefix. + final int maxConflict = + (getPrefixBaseAddress(conflictPrefix) | suffixMask) & ~prefixRangeMask; + + final int prefixMask = prefixLengthToV4NetmaskIntHTH(PREFIX_LENGTH); + // Pick a sub prefix a full prefix (1 << (32 - PREFIX_LENGTH) addresses) greater than + // maxConflict. This ensures that the selected prefix never overlaps with conflictPrefix. + // There is no need to mask the result with PREFIX_LENGTH bits because this is done by + // findAvailablePrefixFromRange when it constructs the prefix. + return maxConflict + (1 << (32 - PREFIX_LENGTH)); + } + + private LinkAddress chooseDownstreamAddress(final IpPrefix prefixRange) { + // The netmask of the prefix assignment block (e.g., 0xfff00000 for 172.16.0.0/12). + final int prefixRangeMask = prefixLengthToV4NetmaskIntHTH(prefixRange.getPrefixLength()); + + // The zero address in the block (e.g., 0xac100000 for 172.16.0.0/12). + final int baseAddress = getPrefixBaseAddress(prefixRange); + + // The subnet mask corresponding to PREFIX_LENGTH. + final int prefixMask = prefixLengthToV4NetmaskIntHTH(PREFIX_LENGTH); + + // The offset within prefixRange of a randomly-selected prefix of length PREFIX_LENGTH. + // This may not be the prefix of the address returned by this method: + // - If it is already in use, the method will return an address in another prefix. + // - If all prefixes within prefixRange are in use, the method will return null. For + // example, for a /24 prefix within 172.26.0.0/12, this will be a multiple of 256 in + // [0, 1048576). In other words, a random 32-bit number with mask 0x000fff00. + // + // prefixRangeMask is required to ensure no wrapping. For example, consider: + // - prefixRange 127.0.0.0/8 + // - randomPrefixStart 127.255.255.0 + // - A conflicting prefix of 127.255.254.0/23 + // In this case without prefixRangeMask, getNextSubPrefix would return 128.0.0.0, which + // means the "start < end" check in findAvailablePrefixFromRange would not reject the prefix + // because Java doesn't have unsigned integers, so 128.0.0.0 = 0x80000000 = -2147483648 + // is less than 127.0.0.0 = 0x7f000000 = 2130706432. + // + // Additionally, it makes debug output easier to read by making the numbers smaller. + final int randomPrefixStart = getRandomInt() & ~prefixRangeMask & prefixMask; + + // A random offset within the prefix. Used to determine the local address once the prefix + // is selected. It does not result in an IPv4 address ending in .0, .1, or .255 + // For a PREFIX_LENGTH of 255, this is a number between 2 and 254. + final int subAddress = getSanitizedSubAddr(~prefixMask); + + // Find a prefix length PREFIX_LENGTH between randomPrefixStart and the end of the block, + // such that the prefix does not conflict with any upstream. + IpPrefix downstreamPrefix = findAvailablePrefixFromRange( + randomPrefixStart, (~prefixRangeMask) + 1, baseAddress, prefixRangeMask); + if (downstreamPrefix != null) return getLinkAddress(downstreamPrefix, subAddress); + + // If that failed, do the same, but between 0 and randomPrefixStart. + downstreamPrefix = findAvailablePrefixFromRange( + 0, randomPrefixStart, baseAddress, prefixRangeMask); + + return getLinkAddress(downstreamPrefix, subAddress); + } + + private LinkAddress getLinkAddress(final IpPrefix prefix, final int subAddress) { + if (prefix == null) return null; + + final InetAddress address = intToInet4AddressHTH(getPrefixBaseAddress(prefix) | subAddress); + return new LinkAddress(address, PREFIX_LENGTH); + } + + private IpPrefix findAvailablePrefixFromRange(final int start, final int end, + final int baseAddress, final int prefixRangeMask) { + int newSubPrefix = start; + while (newSubPrefix < end) { + final InetAddress address = intToInet4AddressHTH(baseAddress | newSubPrefix); + final IpPrefix prefix = new IpPrefix(address, PREFIX_LENGTH); + + final IpPrefix conflictPrefix = getConflictPrefix(prefix); + + if (conflictPrefix == null) return prefix; + + newSubPrefix = getNextSubPrefix(conflictPrefix, prefixRangeMask); + } + + return null; + } + + /** Get random int which could be used to generate random address. */ + @VisibleForTesting + public int getRandomInt() { + return (new Random()).nextInt(); + } + + /** Get random subAddress and avoid selecting x.x.x.0, x.x.x.1 and x.x.x.255 address. */ + private int getSanitizedSubAddr(final int subAddrMask) { + final int randomSubAddr = getRandomInt() & subAddrMask; + // If prefix length > 30, the selecting speace would be less than 4 which may be hard to + // avoid 3 consecutive address. + if (PREFIX_LENGTH > 30) return randomSubAddr; + + // TODO: maybe it is not necessary to avoid .0, .1 and .255 address because tethering + // address would not be conflicted. This code only works because PREFIX_LENGTH is not longer + // than 24 + final int candidate = randomSubAddr & 0xff; + if (candidate == 0 || candidate == 1 || candidate == 255) { + return (randomSubAddr & 0xfffffffc) + 2; + } + + return randomSubAddr; + } + + /** Release downstream record for IpServer. */ + public void releaseDownstream(final IpServer ipServer) { + mDownstreams.remove(ipServer); + } + + /** Clear current upstream prefixes records. */ + public void clearUpstreamPrefixes() { + mUpstreamPrefixMap.clear(); + } + + private IpPrefix getConflictWithUpstream(final IpPrefix prefix) { + for (int i = 0; i < mUpstreamPrefixMap.size(); i++) { + final List list = mUpstreamPrefixMap.valueAt(i); + for (IpPrefix upstream : list) { + if (isConflictPrefix(prefix, upstream)) return upstream; + } + } + return null; + } + + private boolean isConflictWithUpstream(final IpPrefix prefix) { + return getConflictWithUpstream(prefix) != null; + } + + private boolean isConflictPrefix(final IpPrefix prefix1, final IpPrefix prefix2) { + if (prefix2.getPrefixLength() < prefix1.getPrefixLength()) { + return prefix2.contains(prefix1.getAddress()); + } + + return prefix1.contains(prefix2.getAddress()); + } + + // InUse Prefixes are prefixes of mCachedAddresses which are active downstream addresses, last + // downstream addresses(reserved for next time) and static addresses(e.g. bluetooth, wifi p2p). + private IpPrefix getInUseDownstreamPrefix(final IpPrefix prefix) { + for (int i = 0; i < mCachedAddresses.size(); i++) { + final IpPrefix downstream = asIpPrefix(mCachedAddresses.valueAt(i)); + if (isConflictPrefix(prefix, downstream)) return downstream; + } + + // IpServer may use manually-defined address (mStaticIpv4ServerAddr) which does not include + // in mCachedAddresses. + for (IpServer downstream : mDownstreams) { + final IpPrefix target = getDownstreamPrefix(downstream); + + if (isConflictPrefix(prefix, target)) return target; + } + + return null; + } + + @NonNull + private IpPrefix getDownstreamPrefix(final IpServer downstream) { + final LinkAddress address = downstream.getAddress(); + + return asIpPrefix(address); + } + + void dump(final IndentingPrintWriter pw) { + pw.println("mTetheringPrefixes:"); + pw.increaseIndent(); + for (IpPrefix prefix : mTetheringPrefixes) { + pw.println(prefix); + } + pw.decreaseIndent(); + + pw.println("mUpstreamPrefixMap:"); + pw.increaseIndent(); + for (int i = 0; i < mUpstreamPrefixMap.size(); i++) { + pw.println(mUpstreamPrefixMap.keyAt(i) + " - " + mUpstreamPrefixMap.valueAt(i)); + } + pw.decreaseIndent(); + + pw.println("mDownstreams:"); + pw.increaseIndent(); + for (IpServer ipServer : mDownstreams) { + pw.println(ipServer.interfaceType() + " - " + ipServer.getAddress()); + } + pw.decreaseIndent(); + + pw.println("mCachedAddresses:"); + pw.increaseIndent(); + for (int i = 0; i < mCachedAddresses.size(); i++) { + pw.println(mCachedAddresses.keyAt(i) + " - " + mCachedAddresses.valueAt(i)); + } + pw.decreaseIndent(); + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/Tether4Key.java b/Tethering/src/com/android/networkstack/tethering/Tether4Key.java new file mode 100644 index 0000000000..a01ea34c68 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/Tether4Key.java @@ -0,0 +1,81 @@ +/* + * 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.networkstack.tethering; + +import android.net.MacAddress; + +import androidx.annotation.NonNull; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +import java.net.Inet4Address; +import java.net.UnknownHostException; +import java.util.Objects; + +/** Key type for downstream & upstream IPv4 forwarding maps. */ +public class Tether4Key extends Struct { + @Field(order = 0, type = Type.U32) + public final long iif; + + @Field(order = 1, type = Type.EUI48) + public final MacAddress dstMac; + + @Field(order = 2, type = Type.U8, padding = 1) + public final short l4proto; + + @Field(order = 3, type = Type.ByteArray, arraysize = 4) + public final byte[] src4; + + @Field(order = 4, type = Type.ByteArray, arraysize = 4) + public final byte[] dst4; + + @Field(order = 5, type = Type.UBE16) + public final int srcPort; + + @Field(order = 6, type = Type.UBE16) + public final int dstPort; + + public Tether4Key(final long iif, @NonNull final MacAddress dstMac, final short l4proto, + final byte[] src4, final byte[] dst4, final int srcPort, + final int dstPort) { + Objects.requireNonNull(dstMac); + + this.iif = iif; + this.dstMac = dstMac; + this.l4proto = l4proto; + this.src4 = src4; + this.dst4 = dst4; + this.srcPort = srcPort; + this.dstPort = dstPort; + } + + @Override + public String toString() { + try { + return String.format( + "iif: %d, dstMac: %s, l4proto: %d, src4: %s, dst4: %s, " + + "srcPort: %d, dstPort: %d", + iif, dstMac, l4proto, + Inet4Address.getByAddress(src4), Inet4Address.getByAddress(dst4), + Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort)); + } catch (UnknownHostException | IllegalArgumentException e) { + return String.format("Invalid IP address", e); + } + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/Tether4Value.java b/Tethering/src/com/android/networkstack/tethering/Tether4Value.java new file mode 100644 index 0000000000..03a226ce45 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/Tether4Value.java @@ -0,0 +1,97 @@ +/* + * 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.networkstack.tethering; + +import android.net.MacAddress; + +import androidx.annotation.NonNull; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Objects; + +/** Value type for downstream & upstream IPv4 forwarding maps. */ +public class Tether4Value extends Struct { + @Field(order = 0, type = Type.U32) + public final long oif; + + // The ethhdr struct which is defined in uapi/linux/if_ether.h + @Field(order = 1, type = Type.EUI48) + public final MacAddress ethDstMac; + @Field(order = 2, type = Type.EUI48) + public final MacAddress ethSrcMac; + @Field(order = 3, type = Type.UBE16) + public final int ethProto; // Packet type ID field. + + @Field(order = 4, type = Type.U16) + public final int pmtu; + + @Field(order = 5, type = Type.ByteArray, arraysize = 16) + public final byte[] src46; + + @Field(order = 6, type = Type.ByteArray, arraysize = 16) + public final byte[] dst46; + + @Field(order = 7, type = Type.UBE16) + public final int srcPort; + + @Field(order = 8, type = Type.UBE16) + public final int dstPort; + + // TODO: consider using U64. + @Field(order = 9, type = Type.U63) + public final long lastUsed; + + public Tether4Value(final long oif, @NonNull final MacAddress ethDstMac, + @NonNull final MacAddress ethSrcMac, final int ethProto, final int pmtu, + final byte[] src46, final byte[] dst46, final int srcPort, + final int dstPort, final long lastUsed) { + Objects.requireNonNull(ethDstMac); + Objects.requireNonNull(ethSrcMac); + + this.oif = oif; + this.ethDstMac = ethDstMac; + this.ethSrcMac = ethSrcMac; + this.ethProto = ethProto; + this.pmtu = pmtu; + this.src46 = src46; + this.dst46 = dst46; + this.srcPort = srcPort; + this.dstPort = dstPort; + this.lastUsed = lastUsed; + } + + @Override + public String toString() { + try { + return String.format( + "oif: %d, ethDstMac: %s, ethSrcMac: %s, ethProto: %d, pmtu: %d, " + + "src46: %s, dst46: %s, srcPort: %d, dstPort: %d, " + + "lastUsed: %d", + oif, ethDstMac, ethSrcMac, ethProto, pmtu, + InetAddress.getByAddress(src46), InetAddress.getByAddress(dst46), + Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort), + lastUsed); + } catch (UnknownHostException | IllegalArgumentException e) { + return String.format("Invalid IP address", e); + } + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/Tether6Value.java b/Tethering/src/com/android/networkstack/tethering/Tether6Value.java new file mode 100644 index 0000000000..b3107fdd74 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/Tether6Value.java @@ -0,0 +1,60 @@ +/* + * 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.networkstack.tethering; + +import android.net.MacAddress; + +import androidx.annotation.NonNull; + +import com.android.net.module.util.Struct; + +import java.util.Objects; + +/** Value type for downstream and upstream IPv6 forwarding maps. */ +public class Tether6Value extends Struct { + @Field(order = 0, type = Type.S32) + public final int oif; // The output interface index. + + // The ethhdr struct which is defined in uapi/linux/if_ether.h + @Field(order = 1, type = Type.EUI48) + public final MacAddress ethDstMac; // The destination mac address. + @Field(order = 2, type = Type.EUI48) + public final MacAddress ethSrcMac; // The source mac address. + @Field(order = 3, type = Type.UBE16) + public final int ethProto; // Packet type ID field. + + @Field(order = 4, type = Type.U16) + public final int pmtu; // The maximum L3 output path/route mtu. + + public Tether6Value(final int oif, @NonNull final MacAddress ethDstMac, + @NonNull final MacAddress ethSrcMac, final int ethProto, final int pmtu) { + Objects.requireNonNull(ethSrcMac); + Objects.requireNonNull(ethDstMac); + + this.oif = oif; + this.ethDstMac = ethDstMac; + this.ethSrcMac = ethSrcMac; + this.ethProto = ethProto; + this.pmtu = pmtu; + } + + @Override + public String toString() { + return String.format("oif: %d, dstMac: %s, srcMac: %s, proto: %d, pmtu: %d", oif, + ethDstMac, ethSrcMac, ethProto, pmtu); + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherDevKey.java b/Tethering/src/com/android/networkstack/tethering/TetherDevKey.java new file mode 100644 index 0000000000..4283c1b131 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherDevKey.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +/** The key of BpfMap which is used for mapping interface index. */ +public class TetherDevKey extends Struct { + @Field(order = 0, type = Type.U32) + public final long ifIndex; // interface index + + public TetherDevKey(final long ifIndex) { + this.ifIndex = ifIndex; + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherDevValue.java b/Tethering/src/com/android/networkstack/tethering/TetherDevValue.java new file mode 100644 index 0000000000..1cd99b5d8e --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherDevValue.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +/** The key of BpfMap which is used for mapping interface index. */ +public class TetherDevValue extends Struct { + @Field(order = 0, type = Type.U32) + public final long ifIndex; // interface index + + public TetherDevValue(final long ifIndex) { + this.ifIndex = ifIndex; + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherDownstream6Key.java b/Tethering/src/com/android/networkstack/tethering/TetherDownstream6Key.java new file mode 100644 index 0000000000..a08ad4ab03 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherDownstream6Key.java @@ -0,0 +1,69 @@ +/* + * 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.networkstack.tethering; + +import android.net.MacAddress; + +import androidx.annotation.NonNull; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Objects; + +/** The key of BpfMap which is used for bpf offload. */ +public class TetherDownstream6Key extends Struct { + @Field(order = 0, type = Type.U32) + public final long iif; // The input interface index. + + @Field(order = 1, type = Type.EUI48, padding = 2) + public final MacAddress dstMac; // Destination ethernet mac address (zeroed iff rawip ingress). + + @Field(order = 2, type = Type.ByteArray, arraysize = 16) + public final byte[] neigh6; // The destination IPv6 address. + + public TetherDownstream6Key(final long iif, @NonNull final MacAddress dstMac, + final byte[] neigh6) { + Objects.requireNonNull(dstMac); + + try { + final Inet6Address unused = (Inet6Address) InetAddress.getByAddress(neigh6); + } catch (ClassCastException | UnknownHostException e) { + throw new IllegalArgumentException("Invalid IPv6 address: " + + Arrays.toString(neigh6)); + } + this.iif = iif; + this.dstMac = dstMac; + this.neigh6 = neigh6; + } + + @Override + public String toString() { + try { + return String.format("iif: %d, dstMac: %s, neigh: %s", iif, dstMac, + Inet6Address.getByAddress(neigh6)); + } catch (UnknownHostException e) { + // Should not happen because construtor already verify neigh6. + throw new IllegalStateException("Invalid TetherDownstream6Key"); + } + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java b/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java new file mode 100644 index 0000000000..bc9bb474a2 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java @@ -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 com.android.networkstack.tethering; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +/** The key of BpfMap which is used for tethering per-interface limit. */ +public class TetherLimitKey extends Struct { + @Field(order = 0, type = Type.U32) + public final long ifindex; // upstream interface index + + public TetherLimitKey(final long ifindex) { + this.ifindex = ifindex; + } + + // TODO: remove equals, hashCode and toString once aosp/1536721 is merged. + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + + if (!(obj instanceof TetherLimitKey)) return false; + + final TetherLimitKey that = (TetherLimitKey) obj; + + return ifindex == that.ifindex; + } + + @Override + public int hashCode() { + return Long.hashCode(ifindex); + } + + @Override + public String toString() { + return String.format("ifindex: %d", ifindex); + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java b/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java new file mode 100644 index 0000000000..ed7e7d4324 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java @@ -0,0 +1,57 @@ +/* + * 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.networkstack.tethering; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +/** The value of BpfMap which is used for tethering per-interface limit. */ +public class TetherLimitValue extends Struct { + // Use the signed long variable to store the int64 limit on limit BPF map. + // S64 is enough for each interface limit even at 5Gbps for ~468 years. + // 2^63 / (5 * 1000 * 1000 * 1000) * 8 / 86400 / 365 = 468. + // Note that QUOTA_UNLIMITED (-1) indicates there is no limit. + @Field(order = 0, type = Type.S64) + public final long limit; + + public TetherLimitValue(final long limit) { + this.limit = limit; + } + + // TODO: remove equals, hashCode and toString once aosp/1536721 is merged. + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + + if (!(obj instanceof TetherLimitValue)) return false; + + final TetherLimitValue that = (TetherLimitValue) obj; + + return limit == that.limit; + } + + @Override + public int hashCode() { + return Long.hashCode(limit); + } + + @Override + public String toString() { + return String.format("limit: %d", limit); + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java b/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java new file mode 100644 index 0000000000..5442480a01 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java @@ -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 com.android.networkstack.tethering; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +/** The key of BpfMap which is used for tethering stats. */ +public class TetherStatsKey extends Struct { + @Field(order = 0, type = Type.U32) + public final long ifindex; // upstream interface index + + public TetherStatsKey(final long ifindex) { + this.ifindex = ifindex; + } + + // TODO: remove equals, hashCode and toString once aosp/1536721 is merged. + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + + if (!(obj instanceof TetherStatsKey)) return false; + + final TetherStatsKey that = (TetherStatsKey) obj; + + return ifindex == that.ifindex; + } + + @Override + public int hashCode() { + return Long.hashCode(ifindex); + } + + @Override + public String toString() { + return String.format("ifindex: %d", ifindex); + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java b/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java new file mode 100644 index 0000000000..844d2e8f7e --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java @@ -0,0 +1,80 @@ +/* + * 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.networkstack.tethering; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +/** The key of BpfMap which is used for tethering stats. */ +public class TetherStatsValue extends Struct { + // Use the signed long variable to store the uint64 stats from stats BPF map. + // U63 is enough for each data element even at 5Gbps for ~468 years. + // 2^63 / (5 * 1000 * 1000 * 1000) * 8 / 86400 / 365 = 468. + @Field(order = 0, type = Type.U63) + public final long rxPackets; + @Field(order = 1, type = Type.U63) + public final long rxBytes; + @Field(order = 2, type = Type.U63) + public final long rxErrors; + @Field(order = 3, type = Type.U63) + public final long txPackets; + @Field(order = 4, type = Type.U63) + public final long txBytes; + @Field(order = 5, type = Type.U63) + public final long txErrors; + + public TetherStatsValue(final long rxPackets, final long rxBytes, final long rxErrors, + final long txPackets, final long txBytes, final long txErrors) { + this.rxPackets = rxPackets; + this.rxBytes = rxBytes; + this.rxErrors = rxErrors; + this.txPackets = txPackets; + this.txBytes = txBytes; + this.txErrors = txErrors; + } + + // TODO: remove equals, hashCode and toString once aosp/1536721 is merged. + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + + if (!(obj instanceof TetherStatsValue)) return false; + + final TetherStatsValue that = (TetherStatsValue) obj; + + return rxPackets == that.rxPackets + && rxBytes == that.rxBytes + && rxErrors == that.rxErrors + && txPackets == that.txPackets + && txBytes == that.txBytes + && txErrors == that.txErrors; + } + + @Override + public int hashCode() { + return Long.hashCode(rxPackets) ^ Long.hashCode(rxBytes) ^ Long.hashCode(rxErrors) + ^ Long.hashCode(txPackets) ^ Long.hashCode(txBytes) ^ Long.hashCode(txErrors); + } + + @Override + public String toString() { + return String.format("rxPackets: %s, rxBytes: %s, rxErrors: %s, txPackets: %s, " + + "txBytes: %s, txErrors: %s", rxPackets, rxBytes, rxErrors, txPackets, + txBytes, txErrors); + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java b/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java new file mode 100644 index 0000000000..5893885a76 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java @@ -0,0 +1,41 @@ +/* + * 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.networkstack.tethering; + +import android.net.MacAddress; + +import androidx.annotation.NonNull; + +import com.android.net.module.util.Struct; + +import java.util.Objects; + +/** Key type for upstream IPv6 forwarding map. */ +public class TetherUpstream6Key extends Struct { + @Field(order = 0, type = Type.S32) + public final int iif; // The input interface index. + + @Field(order = 1, type = Type.EUI48, padding = 2) + public final MacAddress dstMac; // Destination ethernet mac address (zeroed iff rawip ingress). + + public TetherUpstream6Key(int iif, @NonNull final MacAddress dstMac) { + Objects.requireNonNull(dstMac); + + this.iif = iif; + this.dstMac = dstMac; + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java new file mode 100644 index 0000000000..759638083f --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java @@ -0,0 +1,2494 @@ +/* + * 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 com.android.networkstack.tethering; + +import static android.Manifest.permission.NETWORK_SETTINGS; +import static android.Manifest.permission.NETWORK_STACK; +import static android.content.pm.PackageManager.GET_ACTIVITIES; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.hardware.usb.UsbManager.USB_CONFIGURED; +import static android.hardware.usb.UsbManager.USB_CONNECTED; +import static android.hardware.usb.UsbManager.USB_FUNCTION_NCM; +import static android.hardware.usb.UsbManager.USB_FUNCTION_RNDIS; +import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED; +import static android.net.ConnectivityManager.CONNECTIVITY_ACTION; +import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO; +import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; +import static android.net.TetheringManager.ACTION_TETHER_STATE_CHANGED; +import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL; +import static android.net.TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY; +import static android.net.TetheringManager.EXTRA_ACTIVE_TETHER; +import static android.net.TetheringManager.EXTRA_AVAILABLE_TETHER; +import static android.net.TetheringManager.EXTRA_ERRORED_TETHER; +import static android.net.TetheringManager.TETHERING_BLUETOOTH; +import static android.net.TetheringManager.TETHERING_ETHERNET; +import static android.net.TetheringManager.TETHERING_INVALID; +import static android.net.TetheringManager.TETHERING_NCM; +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.TETHERING_WIGIG; +import static android.net.TetheringManager.TETHER_ERROR_INTERNAL_ERROR; +import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR; +import static android.net.TetheringManager.TETHER_ERROR_SERVICE_UNAVAIL; +import static android.net.TetheringManager.TETHER_ERROR_UNAVAIL_IFACE; +import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE; +import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_TYPE; +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 android.net.TetheringManager.toIfaces; +import static android.net.util.TetheringMessageBase.BASE_MAIN_SM; +import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME; +import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE; +import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE; +import static android.net.wifi.WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR; +import static android.net.wifi.WifiManager.IFACE_IP_MODE_LOCAL_ONLY; +import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED; +import static android.net.wifi.WifiManager.IFACE_IP_MODE_UNSPECIFIED; +import static android.net.wifi.WifiManager.WIFI_AP_STATE_DISABLED; +import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED; +import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; + +import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE; +import static com.android.networkstack.tethering.UpstreamNetworkMonitor.isCellular; + +import android.app.usage.NetworkStatsManager; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothPan; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothProfile.ServiceListener; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.hardware.usb.UsbManager; +import android.net.ConnectivityManager; +import android.net.EthernetManager; +import android.net.IIntResultListener; +import android.net.INetd; +import android.net.ITetheringEventCallback; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.TetherStatesParcel; +import android.net.TetheredClient; +import android.net.TetheringCallbackStartedParcel; +import android.net.TetheringConfigurationParcel; +import android.net.TetheringInterface; +import android.net.TetheringManager.TetheringRequest; +import android.net.TetheringRequestParcel; +import android.net.ip.IpServer; +import android.net.shared.NetdUtils; +import android.net.util.InterfaceSet; +import android.net.util.PrefixUtils; +import android.net.util.SharedLog; +import android.net.util.TetheringUtils; +import android.net.util.VersionedBroadcastListener; +import android.net.wifi.WifiClient; +import android.net.wifi.WifiManager; +import android.net.wifi.p2p.WifiP2pGroup; +import android.net.wifi.p2p.WifiP2pInfo; +import android.net.wifi.p2p.WifiP2pManager; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ServiceSpecificException; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.telephony.PhoneStateListener; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; +import android.util.SparseArray; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.MessageUtils; +import com.android.internal.util.State; +import com.android.internal.util.StateMachine; +import com.android.net.module.util.BaseNetdUnsolicitedEventListener; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * + * This class holds much of the business logic to allow Android devices + * to act as IP gateways via USB, BT, and WiFi interfaces. + */ +public class Tethering { + + private static final String TAG = Tethering.class.getSimpleName(); + private static final boolean DBG = false; + private static final boolean VDBG = false; + + private static final Class[] sMessageClasses = { + Tethering.class, TetherMainSM.class, IpServer.class + }; + private static final SparseArray sMagicDecoderRing = + MessageUtils.findMessageNames(sMessageClasses); + + private static final int DUMP_TIMEOUT_MS = 10_000; + + // Keep in sync with NETID_UNSET in system/netd/include/netid_client.h + private static final int NETID_UNSET = 0; + + private static class TetherState { + public final IpServer ipServer; + public int lastState; + public int lastError; + + TetherState(IpServer ipServer) { + this.ipServer = ipServer; + // Assume all state machines start out available and with no errors. + lastState = IpServer.STATE_AVAILABLE; + lastError = TETHER_ERROR_NO_ERROR; + } + + public boolean isCurrentlyServing() { + switch (lastState) { + case IpServer.STATE_TETHERED: + case IpServer.STATE_LOCAL_ONLY: + return true; + default: + return false; + } + } + } + + /** + * Cookie added when registering {@link android.net.TetheringManager.TetheringEventCallback}. + */ + private static class CallbackCookie { + public final boolean hasListClientsPermission; + + private CallbackCookie(boolean hasListClientsPermission) { + this.hasListClientsPermission = hasListClientsPermission; + } + } + + private final SharedLog mLog = new SharedLog(TAG); + private final RemoteCallbackList mTetheringEventCallbacks = + new RemoteCallbackList<>(); + // Currently active tethering requests per tethering type. Only one of each type can be + // requested at a time. After a tethering type is requested, the map keeps tethering parameters + // to be used after the interface comes up asynchronously. + private final SparseArray mActiveTetheringRequests = + new SparseArray<>(); + + private final Context mContext; + private final ArrayMap mTetherStates; + private final BroadcastReceiver mStateReceiver; + private final Looper mLooper; + private final TetherMainSM mTetherMainSM; + private final OffloadController mOffloadController; + private final UpstreamNetworkMonitor mUpstreamNetworkMonitor; + // TODO: Figure out how to merge this and other downstream-tracking objects + // into a single coherent structure. + private final HashSet mForwardedDownstreams; + private final VersionedBroadcastListener mCarrierConfigChange; + private final TetheringDependencies mDeps; + private final EntitlementManager mEntitlementMgr; + private final Handler mHandler; + private final INetd mNetd; + private final NetdCallback mNetdCallback; + private final UserRestrictionActionListener mTetheringRestriction; + private final ActiveDataSubIdListener mActiveDataSubIdListener; + private final ConnectedClientsTracker mConnectedClientsTracker; + private final TetheringThreadExecutor mExecutor; + private final TetheringNotificationUpdater mNotificationUpdater; + private final UserManager mUserManager; + private final BpfCoordinator mBpfCoordinator; + private final PrivateAddressCoordinator mPrivateAddressCoordinator; + private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID; + + private volatile TetheringConfiguration mConfig; + private InterfaceSet mCurrentUpstreamIfaceSet; + + private boolean mRndisEnabled; // track the RNDIS function enabled state + // True iff. WiFi tethering should be started when soft AP is ready. + private boolean mWifiTetherRequested; + private Network mTetherUpstream; + private TetherStatesParcel mTetherStatesParcel; + private boolean mDataSaverEnabled = false; + private String mWifiP2pTetherInterface = null; + private int mOffloadStatus = TETHER_HARDWARE_OFFLOAD_STOPPED; + + private EthernetManager.TetheredInterfaceRequest mEthernetIfaceRequest; + private String mConfiguredEthernetIface; + private EthernetCallback mEthernetCallback; + + public Tethering(TetheringDependencies deps) { + mLog.mark("Tethering.constructed"); + mDeps = deps; + mContext = mDeps.getContext(); + mNetd = mDeps.getINetd(mContext); + mLooper = mDeps.getTetheringLooper(); + mNotificationUpdater = mDeps.getNotificationUpdater(mContext, mLooper); + + mTetherStates = new ArrayMap<>(); + mConnectedClientsTracker = new ConnectedClientsTracker(); + + mTetherMainSM = new TetherMainSM("TetherMain", mLooper, deps); + mTetherMainSM.start(); + + mHandler = mTetherMainSM.getHandler(); + mOffloadController = mDeps.getOffloadController(mHandler, mLog, + new OffloadController.Dependencies() { + + @Override + public TetheringConfiguration getTetherConfig() { + return mConfig; + } + }); + mUpstreamNetworkMonitor = mDeps.getUpstreamNetworkMonitor(mContext, mTetherMainSM, mLog, + TetherMainSM.EVENT_UPSTREAM_CALLBACK); + mForwardedDownstreams = new HashSet<>(); + + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_CARRIER_CONFIG_CHANGED); + // EntitlementManager will send EVENT_UPSTREAM_PERMISSION_CHANGED when cellular upstream + // permission is changed according to entitlement check result. + mEntitlementMgr = mDeps.getEntitlementManager(mContext, mHandler, mLog, + () -> mTetherMainSM.sendMessage( + TetherMainSM.EVENT_UPSTREAM_PERMISSION_CHANGED)); + mEntitlementMgr.setOnUiEntitlementFailedListener((int downstream) -> { + mLog.log("OBSERVED UiEnitlementFailed"); + stopTethering(downstream); + }); + mEntitlementMgr.setTetheringConfigurationFetcher(() -> { + return mConfig; + }); + + mCarrierConfigChange = new VersionedBroadcastListener( + "CarrierConfigChangeListener", mContext, mHandler, filter, + (Intent ignored) -> { + mLog.log("OBSERVED carrier config change"); + updateConfiguration(); + mEntitlementMgr.reevaluateSimCardProvisioning(mConfig); + }); + + mStateReceiver = new StateReceiver(); + + mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + mTetheringRestriction = new UserRestrictionActionListener( + mUserManager, this, mNotificationUpdater); + mExecutor = new TetheringThreadExecutor(mHandler); + mActiveDataSubIdListener = new ActiveDataSubIdListener(mExecutor); + mNetdCallback = new NetdCallback(); + + // Load tethering configuration. + updateConfiguration(); + // It is OK for the configuration to be passed to the PrivateAddressCoordinator at + // construction time because the only part of the configuration it uses is + // shouldEnableWifiP2pDedicatedIp(), and currently do not support changing that. + mPrivateAddressCoordinator = mDeps.getPrivateAddressCoordinator(mContext, mConfig); + + // Must be initialized after tethering configuration is loaded because BpfCoordinator + // constructor needs to use the configuration. + mBpfCoordinator = mDeps.getBpfCoordinator( + new BpfCoordinator.Dependencies() { + @NonNull + public Handler getHandler() { + return mHandler; + } + + @NonNull + public INetd getNetd() { + return mNetd; + } + + @NonNull + public NetworkStatsManager getNetworkStatsManager() { + return mContext.getSystemService(NetworkStatsManager.class); + } + + @NonNull + public SharedLog getSharedLog() { + return mLog; + } + + @Nullable + public TetheringConfiguration getTetherConfig() { + return mConfig; + } + }); + + startStateMachineUpdaters(); + } + + /** + * Start to register callbacks. + * Call this function when tethering is ready to handle callback events. + */ + private void startStateMachineUpdaters() { + try { + mNetd.registerUnsolicitedEventListener(mNetdCallback); + } catch (RemoteException e) { + mLog.e("Unable to register netd UnsolicitedEventListener"); + } + mCarrierConfigChange.startListening(); + mContext.getSystemService(TelephonyManager.class).listen(mActiveDataSubIdListener, + PhoneStateListener.LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE); + + IntentFilter filter = new IntentFilter(); + filter.addAction(UsbManager.ACTION_USB_STATE); + filter.addAction(CONNECTIVITY_ACTION); + filter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION); + filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); + filter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION); + filter.addAction(UserManager.ACTION_USER_RESTRICTIONS_CHANGED); + filter.addAction(ACTION_RESTRICT_BACKGROUND_CHANGED); + mContext.registerReceiver(mStateReceiver, filter, null, mHandler); + + final IntentFilter noUpstreamFilter = new IntentFilter(); + noUpstreamFilter.addAction(TetheringNotificationUpdater.ACTION_DISABLE_TETHERING); + mContext.registerReceiver( + mStateReceiver, noUpstreamFilter, PERMISSION_MAINLINE_NETWORK_STACK, mHandler); + + final WifiManager wifiManager = getWifiManager(); + if (wifiManager != null) { + wifiManager.registerSoftApCallback(mExecutor, new TetheringSoftApCallback()); + } + + startTrackDefaultNetwork(); + } + + private class TetheringThreadExecutor implements Executor { + private final Handler mTetherHandler; + TetheringThreadExecutor(Handler handler) { + mTetherHandler = handler; + } + @Override + public void execute(Runnable command) { + if (!mTetherHandler.post(command)) { + throw new RejectedExecutionException(mTetherHandler + " is shutting down"); + } + } + } + + private class ActiveDataSubIdListener extends PhoneStateListener { + ActiveDataSubIdListener(Executor executor) { + super(executor); + } + + @Override + public void onActiveDataSubscriptionIdChanged(int subId) { + mLog.log("OBSERVED active data subscription change, from " + mActiveDataSubId + + " to " + subId); + if (subId == mActiveDataSubId) return; + + mActiveDataSubId = subId; + updateConfiguration(); + mNotificationUpdater.onActiveDataSubscriptionIdChanged(subId); + // To avoid launching unexpected provisioning checks, ignore re-provisioning + // when no CarrierConfig loaded yet. Assume reevaluateSimCardProvisioning() + // will be triggered again when CarrierConfig is loaded. + if (mEntitlementMgr.getCarrierConfig(mConfig) != null) { + mEntitlementMgr.reevaluateSimCardProvisioning(mConfig); + } else { + mLog.log("IGNORED reevaluate provisioning, no carrier config loaded"); + } + } + } + + private WifiManager getWifiManager() { + return (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + } + + // NOTE: This is always invoked on the mLooper thread. + private void updateConfiguration() { + mConfig = mDeps.generateTetheringConfiguration(mContext, mLog, mActiveDataSubId); + mUpstreamNetworkMonitor.setUpstreamConfig(mConfig.chooseUpstreamAutomatically, + mConfig.isDunRequired); + reportConfigurationChanged(mConfig.toStableParcelable()); + } + + private void maybeDunSettingChanged() { + final boolean isDunRequired = TetheringConfiguration.checkDunRequired(mContext); + if (isDunRequired == mConfig.isDunRequired) return; + updateConfiguration(); + } + + private class NetdCallback extends BaseNetdUnsolicitedEventListener { + @Override + public void onInterfaceChanged(String ifName, boolean up) { + mHandler.post(() -> interfaceStatusChanged(ifName, up)); + } + + @Override + public void onInterfaceLinkStateChanged(String ifName, boolean up) { + mHandler.post(() -> interfaceLinkStateChanged(ifName, up)); + } + + @Override + public void onInterfaceAdded(String ifName) { + mHandler.post(() -> interfaceAdded(ifName)); + } + + @Override + public void onInterfaceRemoved(String ifName) { + mHandler.post(() -> interfaceRemoved(ifName)); + } + } + + private class TetheringSoftApCallback implements WifiManager.SoftApCallback { + // TODO: Remove onStateChanged override when this method has default on + // WifiManager#SoftApCallback interface. + // Wifi listener for state change of the soft AP + @Override + public void onStateChanged(final int state, final int failureReason) { + // Nothing + } + + // Called by wifi when the number of soft AP clients changed. + @Override + public void onConnectedClientsChanged(final List clients) { + updateConnectedClients(clients); + } + } + + void interfaceStatusChanged(String iface, boolean up) { + // Never called directly: only called from interfaceLinkStateChanged. + // See NetlinkHandler.cpp: notifyInterfaceChanged. + if (VDBG) Log.d(TAG, "interfaceStatusChanged " + iface + ", " + up); + if (up) { + maybeTrackNewInterfaceLocked(iface); + } else { + if (ifaceNameToType(iface) == TETHERING_BLUETOOTH + || ifaceNameToType(iface) == TETHERING_WIGIG) { + stopTrackingInterfaceLocked(iface); + } else { + // Ignore usb0 down after enabling RNDIS. + // We will handle disconnect in interfaceRemoved. + // Similarly, ignore interface down for WiFi. We monitor WiFi AP status + // through the WifiManager.WIFI_AP_STATE_CHANGED_ACTION intent. + if (VDBG) Log.d(TAG, "ignore interface down for " + iface); + } + } + } + + void interfaceLinkStateChanged(String iface, boolean up) { + interfaceStatusChanged(iface, up); + } + + private int ifaceNameToType(String iface) { + final TetheringConfiguration cfg = mConfig; + + if (cfg.isWifi(iface)) { + return TETHERING_WIFI; + } else if (cfg.isWigig(iface)) { + return TETHERING_WIGIG; + } else if (cfg.isWifiP2p(iface)) { + return TETHERING_WIFI_P2P; + } else if (cfg.isUsb(iface)) { + return TETHERING_USB; + } else if (cfg.isBluetooth(iface)) { + return TETHERING_BLUETOOTH; + } else if (cfg.isNcm(iface)) { + return TETHERING_NCM; + } + return TETHERING_INVALID; + } + + void interfaceAdded(String iface) { + if (VDBG) Log.d(TAG, "interfaceAdded " + iface); + maybeTrackNewInterfaceLocked(iface); + } + + void interfaceRemoved(String iface) { + if (VDBG) Log.d(TAG, "interfaceRemoved " + iface); + stopTrackingInterfaceLocked(iface); + } + + void startTethering(final TetheringRequestParcel request, final IIntResultListener listener) { + mHandler.post(() -> { + final TetheringRequestParcel unfinishedRequest = mActiveTetheringRequests.get( + request.tetheringType); + // If tethering is already enabled with a different request, + // disable before re-enabling. + if (unfinishedRequest != null + && !TetheringUtils.isTetheringRequestEquals(unfinishedRequest, request)) { + enableTetheringInternal(request.tetheringType, false /* disabled */, null); + mEntitlementMgr.stopProvisioningIfNeeded(request.tetheringType); + } + mActiveTetheringRequests.put(request.tetheringType, request); + + if (request.exemptFromEntitlementCheck) { + mEntitlementMgr.setExemptedDownstreamType(request.tetheringType); + } else { + mEntitlementMgr.startProvisioningIfNeeded(request.tetheringType, + request.showProvisioningUi); + } + enableTetheringInternal(request.tetheringType, true /* enabled */, listener); + }); + } + + void stopTethering(int type) { + mHandler.post(() -> { + mActiveTetheringRequests.remove(type); + + enableTetheringInternal(type, false /* disabled */, null); + mEntitlementMgr.stopProvisioningIfNeeded(type); + }); + } + + /** + * Enables or disables tethering for the given type. If provisioning is required, it will + * schedule provisioning rechecks for the specified interface. + */ + private void enableTetheringInternal(int type, boolean enable, + final IIntResultListener listener) { + int result = TETHER_ERROR_NO_ERROR; + switch (type) { + case TETHERING_WIFI: + result = setWifiTethering(enable); + break; + case TETHERING_USB: + result = setUsbTethering(enable); + break; + case TETHERING_BLUETOOTH: + setBluetoothTethering(enable, listener); + break; + case TETHERING_NCM: + result = setNcmTethering(enable); + break; + case TETHERING_ETHERNET: + result = setEthernetTethering(enable); + break; + default: + Log.w(TAG, "Invalid tether type."); + result = TETHER_ERROR_UNKNOWN_TYPE; + } + + // The result of Bluetooth tethering will be sent by #setBluetoothTethering. + if (type != TETHERING_BLUETOOTH) { + sendTetherResult(listener, result, type); + } + } + + private void sendTetherResult(final IIntResultListener listener, final int result, + final int type) { + if (listener != null) { + try { + listener.onResult(result); + } catch (RemoteException e) { } + } + + // If changing tethering fail, remove corresponding request + // no matter who trigger the start/stop. + if (result != TETHER_ERROR_NO_ERROR) mActiveTetheringRequests.remove(type); + } + + private int setWifiTethering(final boolean enable) { + final long ident = Binder.clearCallingIdentity(); + try { + final WifiManager mgr = getWifiManager(); + if (mgr == null) { + mLog.e("setWifiTethering: failed to get WifiManager!"); + return TETHER_ERROR_SERVICE_UNAVAIL; + } + if ((enable && mgr.startTetheredHotspot(null /* use existing softap config */)) + || (!enable && mgr.stopSoftAp())) { + mWifiTetherRequested = enable; + return TETHER_ERROR_NO_ERROR; + } + } finally { + Binder.restoreCallingIdentity(ident); + } + + return TETHER_ERROR_INTERNAL_ERROR; + } + + private void setBluetoothTethering(final boolean enable, final IIntResultListener listener) { + final BluetoothAdapter adapter = mDeps.getBluetoothAdapter(); + if (adapter == null || !adapter.isEnabled()) { + Log.w(TAG, "Tried to enable bluetooth tethering with null or disabled adapter. null: " + + (adapter == null)); + sendTetherResult(listener, TETHER_ERROR_SERVICE_UNAVAIL, TETHERING_BLUETOOTH); + return; + } + + adapter.getProfileProxy(mContext, new ServiceListener() { + @Override + public void onServiceDisconnected(int profile) { } + + @Override + public void onServiceConnected(int profile, BluetoothProfile proxy) { + // Clear identify is fine because caller already pass tethering permission at + // ConnectivityService#startTethering()(or stopTethering) before the control comes + // here. Bluetooth will check tethering permission again that there is + // Context#getOpPackageName() under BluetoothPan#setBluetoothTethering() to get + // caller's package name for permission check. + // Calling BluetoothPan#setBluetoothTethering() here means the package name always + // be system server. If calling identity is not cleared, that package's uid might + // not match calling uid and end up in permission denied. + final long identityToken = Binder.clearCallingIdentity(); + try { + ((BluetoothPan) proxy).setBluetoothTethering(enable); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + // TODO: Enabling bluetooth tethering can fail asynchronously here. + // We should figure out a way to bubble up that failure instead of sending success. + final int result = (((BluetoothPan) proxy).isTetheringOn() == enable) + ? TETHER_ERROR_NO_ERROR + : TETHER_ERROR_INTERNAL_ERROR; + sendTetherResult(listener, result, TETHERING_BLUETOOTH); + adapter.closeProfileProxy(BluetoothProfile.PAN, proxy); + } + }, BluetoothProfile.PAN); + } + + private int setEthernetTethering(final boolean enable) { + final EthernetManager em = (EthernetManager) mContext.getSystemService( + Context.ETHERNET_SERVICE); + if (enable) { + if (mEthernetCallback != null) { + Log.d(TAG, "Ethernet tethering already started"); + return TETHER_ERROR_NO_ERROR; + } + + mEthernetCallback = new EthernetCallback(); + mEthernetIfaceRequest = em.requestTetheredInterface(mExecutor, mEthernetCallback); + } else { + stopEthernetTetheringLocked(); + } + return TETHER_ERROR_NO_ERROR; + } + + private void stopEthernetTetheringLocked() { + if (mConfiguredEthernetIface != null) { + stopTrackingInterfaceLocked(mConfiguredEthernetIface); + mConfiguredEthernetIface = null; + } + if (mEthernetCallback != null) { + mEthernetIfaceRequest.release(); + mEthernetCallback = null; + mEthernetIfaceRequest = null; + } + } + + private class EthernetCallback implements EthernetManager.TetheredInterfaceCallback { + @Override + public void onAvailable(String iface) { + if (this != mEthernetCallback) { + // Ethernet callback arrived after Ethernet tethering stopped. Ignore. + return; + } + maybeTrackNewInterfaceLocked(iface, TETHERING_ETHERNET); + changeInterfaceState(iface, getRequestedState(TETHERING_ETHERNET)); + mConfiguredEthernetIface = iface; + } + + @Override + public void onUnavailable() { + if (this != mEthernetCallback) { + // onAvailable called after stopping Ethernet tethering. + return; + } + stopEthernetTetheringLocked(); + } + } + + void tether(String iface, int requestedState, final IIntResultListener listener) { + mHandler.post(() -> { + try { + listener.onResult(tether(iface, requestedState)); + } catch (RemoteException e) { } + }); + } + + private int tether(String iface, int requestedState) { + if (DBG) Log.d(TAG, "Tethering " + iface); + TetherState tetherState = mTetherStates.get(iface); + if (tetherState == null) { + Log.e(TAG, "Tried to Tether an unknown iface: " + iface + ", ignoring"); + return TETHER_ERROR_UNKNOWN_IFACE; + } + // Ignore the error status of the interface. If the interface is available, + // the errors are referring to past tethering attempts anyway. + if (tetherState.lastState != IpServer.STATE_AVAILABLE) { + Log.e(TAG, "Tried to Tether an unavailable iface: " + iface + ", ignoring"); + return TETHER_ERROR_UNAVAIL_IFACE; + } + // NOTE: If a CMD_TETHER_REQUESTED message is already in the TISM's queue but not yet + // processed, this will be a no-op and it will not return an error. + // + // This code cannot race with untether() because they both run on the handler thread. + final int type = tetherState.ipServer.interfaceType(); + final TetheringRequestParcel request = mActiveTetheringRequests.get(type, null); + if (request != null) { + mActiveTetheringRequests.delete(type); + } + tetherState.ipServer.sendMessage(IpServer.CMD_TETHER_REQUESTED, requestedState, 0, + request); + return TETHER_ERROR_NO_ERROR; + } + + void untether(String iface, final IIntResultListener listener) { + mHandler.post(() -> { + try { + listener.onResult(untether(iface)); + } catch (RemoteException e) { + } + }); + } + + int untether(String iface) { + if (DBG) Log.d(TAG, "Untethering " + iface); + TetherState tetherState = mTetherStates.get(iface); + if (tetherState == null) { + Log.e(TAG, "Tried to Untether an unknown iface :" + iface + ", ignoring"); + return TETHER_ERROR_UNKNOWN_IFACE; + } + if (!tetherState.isCurrentlyServing()) { + Log.e(TAG, "Tried to untether an inactive iface :" + iface + ", ignoring"); + return TETHER_ERROR_UNAVAIL_IFACE; + } + tetherState.ipServer.sendMessage(IpServer.CMD_TETHER_UNREQUESTED); + return TETHER_ERROR_NO_ERROR; + } + + void untetherAll() { + stopTethering(TETHERING_WIFI); + stopTethering(TETHERING_WIFI_P2P); + stopTethering(TETHERING_USB); + stopTethering(TETHERING_BLUETOOTH); + stopTethering(TETHERING_ETHERNET); + } + + @VisibleForTesting + int getLastErrorForTest(String iface) { + TetherState tetherState = mTetherStates.get(iface); + if (tetherState == null) { + Log.e(TAG, "Tried to getLastErrorForTest on an unknown iface :" + iface + + ", ignoring"); + return TETHER_ERROR_UNKNOWN_IFACE; + } + return tetherState.lastError; + } + + private boolean isProvisioningNeededButUnavailable() { + return isTetherProvisioningRequired() && !doesEntitlementPackageExist(); + } + + boolean isTetherProvisioningRequired() { + final TetheringConfiguration cfg = mConfig; + return mEntitlementMgr.isTetherProvisioningRequired(cfg); + } + + private boolean doesEntitlementPackageExist() { + // provisioningApp must contain package and class name. + if (mConfig.provisioningApp.length != 2) { + return false; + } + + final PackageManager pm = mContext.getPackageManager(); + try { + pm.getPackageInfo(mConfig.provisioningApp[0], GET_ACTIVITIES); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + return true; + } + + private int getRequestedState(int type) { + final TetheringRequestParcel request = mActiveTetheringRequests.get(type); + + // The request could have been deleted before we had a chance to complete it. + // If so, assume that the scope is the default scope for this tethering type. + // This likely doesn't matter - if the request has been deleted, then tethering is + // likely going to be stopped soon anyway. + final int connectivityScope = (request != null) + ? request.connectivityScope + : TetheringRequest.getDefaultConnectivityScope(type); + + return connectivityScope == CONNECTIVITY_SCOPE_LOCAL + ? IpServer.STATE_LOCAL_ONLY + : IpServer.STATE_TETHERED; + } + + // TODO: Figure out how to update for local hotspot mode interfaces. + private void sendTetherStateChangedBroadcast() { + if (!isTetheringSupported()) return; + + final ArrayList available = new ArrayList<>(); + final ArrayList tethered = new ArrayList<>(); + final ArrayList localOnly = new ArrayList<>(); + final ArrayList errored = new ArrayList<>(); + final ArrayList lastErrors = new ArrayList<>(); + + final TetheringConfiguration cfg = mConfig; + + int downstreamTypesMask = DOWNSTREAM_NONE; + for (int i = 0; i < mTetherStates.size(); i++) { + final TetherState tetherState = mTetherStates.valueAt(i); + final int type = tetherState.ipServer.interfaceType(); + final String iface = mTetherStates.keyAt(i); + final TetheringInterface tetheringIface = new TetheringInterface(type, iface); + if (tetherState.lastError != TETHER_ERROR_NO_ERROR) { + errored.add(tetheringIface); + lastErrors.add(tetherState.lastError); + } else if (tetherState.lastState == IpServer.STATE_AVAILABLE) { + available.add(tetheringIface); + } else if (tetherState.lastState == IpServer.STATE_LOCAL_ONLY) { + localOnly.add(tetheringIface); + } else if (tetherState.lastState == IpServer.STATE_TETHERED) { + if (cfg.isUsb(iface)) { + downstreamTypesMask |= (1 << TETHERING_USB); + } else if (cfg.isWifi(iface)) { + downstreamTypesMask |= (1 << TETHERING_WIFI); + } else if (cfg.isBluetooth(iface)) { + downstreamTypesMask |= (1 << TETHERING_BLUETOOTH); + } + tethered.add(tetheringIface); + } + } + + mTetherStatesParcel = buildTetherStatesParcel(available, localOnly, tethered, errored, + lastErrors); + reportTetherStateChanged(mTetherStatesParcel); + + mContext.sendStickyBroadcastAsUser(buildStateChangeIntent(available, localOnly, tethered, + errored), UserHandle.ALL); + if (DBG) { + Log.d(TAG, String.format( + "reportTetherStateChanged %s=[%s] %s=[%s] %s=[%s] %s=[%s]", + "avail", TextUtils.join(",", available), + "local_only", TextUtils.join(",", localOnly), + "tether", TextUtils.join(",", tethered), + "error", TextUtils.join(",", errored))); + } + + mNotificationUpdater.onDownstreamChanged(downstreamTypesMask); + } + + private TetherStatesParcel buildTetherStatesParcel( + final ArrayList available, + final ArrayList localOnly, + final ArrayList tethered, + final ArrayList errored, + final ArrayList lastErrors) { + final TetherStatesParcel parcel = new TetherStatesParcel(); + + parcel.availableList = available.toArray(new TetheringInterface[0]); + parcel.tetheredList = tethered.toArray(new TetheringInterface[0]); + parcel.localOnlyList = localOnly.toArray(new TetheringInterface[0]); + parcel.erroredIfaceList = errored.toArray(new TetheringInterface[0]); + parcel.lastErrorList = new int[lastErrors.size()]; + for (int i = 0; i < lastErrors.size(); i++) { + parcel.lastErrorList[i] = lastErrors.get(i); + } + + return parcel; + } + + private Intent buildStateChangeIntent(final ArrayList available, + final ArrayList localOnly, + final ArrayList tethered, + final ArrayList errored) { + final Intent bcast = new Intent(ACTION_TETHER_STATE_CHANGED); + bcast.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + + bcast.putStringArrayListExtra(EXTRA_AVAILABLE_TETHER, toIfaces(available)); + bcast.putStringArrayListExtra(EXTRA_ACTIVE_LOCAL_ONLY, toIfaces(localOnly)); + bcast.putStringArrayListExtra(EXTRA_ACTIVE_TETHER, toIfaces(tethered)); + bcast.putStringArrayListExtra(EXTRA_ERRORED_TETHER, toIfaces(errored)); + + return bcast; + } + + private class StateReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context content, Intent intent) { + final String action = intent.getAction(); + if (action == null) return; + + if (action.equals(UsbManager.ACTION_USB_STATE)) { + handleUsbAction(intent); + } else if (action.equals(CONNECTIVITY_ACTION)) { + handleConnectivityAction(intent); + } else if (action.equals(WifiManager.WIFI_AP_STATE_CHANGED_ACTION)) { + handleWifiApAction(intent); + } else if (action.equals(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)) { + handleWifiP2pAction(intent); + } else if (action.equals(Intent.ACTION_CONFIGURATION_CHANGED)) { + mLog.log("OBSERVED configuration changed"); + updateConfiguration(); + } else if (action.equals(UserManager.ACTION_USER_RESTRICTIONS_CHANGED)) { + mLog.log("OBSERVED user restrictions changed"); + handleUserRestrictionAction(); + } else if (action.equals(ACTION_RESTRICT_BACKGROUND_CHANGED)) { + mLog.log("OBSERVED data saver changed"); + handleDataSaverChanged(); + } else if (action.equals(TetheringNotificationUpdater.ACTION_DISABLE_TETHERING)) { + untetherAll(); + } + } + + private void handleConnectivityAction(Intent intent) { + final NetworkInfo networkInfo = + (NetworkInfo) intent.getParcelableExtra(EXTRA_NETWORK_INFO); + if (networkInfo == null + || networkInfo.getDetailedState() == NetworkInfo.DetailedState.FAILED) { + return; + } + + if (VDBG) Log.d(TAG, "Tethering got CONNECTIVITY_ACTION: " + networkInfo.toString()); + mTetherMainSM.sendMessage(TetherMainSM.CMD_UPSTREAM_CHANGED); + } + + private void handleUsbAction(Intent intent) { + final boolean usbConnected = intent.getBooleanExtra(USB_CONNECTED, false); + final boolean usbConfigured = intent.getBooleanExtra(USB_CONFIGURED, false); + final boolean rndisEnabled = intent.getBooleanExtra(USB_FUNCTION_RNDIS, false); + final boolean ncmEnabled = intent.getBooleanExtra(USB_FUNCTION_NCM, false); + + mLog.log(String.format("USB bcast connected:%s configured:%s rndis:%s", + usbConnected, usbConfigured, rndisEnabled)); + + // There are three types of ACTION_USB_STATE: + // + // - DISCONNECTED (USB_CONNECTED and USB_CONFIGURED are 0) + // Meaning: USB connection has ended either because of + // software reset or hard unplug. + // + // - CONNECTED (USB_CONNECTED is 1, USB_CONFIGURED is 0) + // Meaning: the first stage of USB protocol handshake has + // occurred but it is not complete. + // + // - CONFIGURED (USB_CONNECTED and USB_CONFIGURED are 1) + // Meaning: the USB handshake is completely done and all the + // functions are ready to use. + // + // For more explanation, see b/62552150 . + if (!usbConnected && mRndisEnabled) { + // Turn off tethering if it was enabled and there is a disconnect. + tetherMatchingInterfaces(IpServer.STATE_AVAILABLE, TETHERING_USB); + mEntitlementMgr.stopProvisioningIfNeeded(TETHERING_USB); + } else if (usbConfigured && rndisEnabled) { + // Tether if rndis is enabled and usb is configured. + final int state = getRequestedState(TETHERING_USB); + tetherMatchingInterfaces(state, TETHERING_USB); + } else if (usbConnected && ncmEnabled) { + final int state = getRequestedState(TETHERING_NCM); + tetherMatchingInterfaces(state, TETHERING_NCM); + } + mRndisEnabled = usbConfigured && rndisEnabled; + } + + private void handleWifiApAction(Intent intent) { + final int curState = intent.getIntExtra(EXTRA_WIFI_AP_STATE, WIFI_AP_STATE_DISABLED); + final String ifname = intent.getStringExtra(EXTRA_WIFI_AP_INTERFACE_NAME); + final int ipmode = intent.getIntExtra(EXTRA_WIFI_AP_MODE, IFACE_IP_MODE_UNSPECIFIED); + + switch (curState) { + case WifiManager.WIFI_AP_STATE_ENABLING: + // We can see this state on the way to both enabled and failure states. + break; + case WifiManager.WIFI_AP_STATE_ENABLED: + enableWifiIpServingLocked(ifname, ipmode); + break; + case WifiManager.WIFI_AP_STATE_DISABLING: + // We can see this state on the way to disabled. + break; + case WifiManager.WIFI_AP_STATE_DISABLED: + case WifiManager.WIFI_AP_STATE_FAILED: + default: + disableWifiIpServingLocked(ifname, curState); + break; + } + } + + private boolean isGroupOwner(WifiP2pGroup group) { + return group != null && group.isGroupOwner() + && !TextUtils.isEmpty(group.getInterface()); + } + + private void handleWifiP2pAction(Intent intent) { + if (mConfig.isWifiP2pLegacyTetheringMode()) return; + + final WifiP2pInfo p2pInfo = + (WifiP2pInfo) intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO); + final WifiP2pGroup group = + (WifiP2pGroup) intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP); + + if (VDBG) { + Log.d(TAG, "WifiP2pAction: P2pInfo: " + p2pInfo + " Group: " + group); + } + + // if no group is formed, bring it down if needed. + if (p2pInfo == null || !p2pInfo.groupFormed) { + disableWifiP2pIpServingLockedIfNeeded(mWifiP2pTetherInterface); + mWifiP2pTetherInterface = null; + return; + } + + // If there is a group but the device is not the owner, bail out. + if (!isGroupOwner(group)) return; + + // If already serving from the correct interface, nothing to do. + if (group.getInterface().equals(mWifiP2pTetherInterface)) return; + + // If already serving from another interface, turn it down first. + if (!TextUtils.isEmpty(mWifiP2pTetherInterface)) { + mLog.w("P2P tethered interface " + mWifiP2pTetherInterface + + "is different from current interface " + + group.getInterface() + ", re-tether it"); + disableWifiP2pIpServingLockedIfNeeded(mWifiP2pTetherInterface); + } + + // Finally bring up serving on the new interface + mWifiP2pTetherInterface = group.getInterface(); + enableWifiIpServingLocked(mWifiP2pTetherInterface, IFACE_IP_MODE_LOCAL_ONLY); + } + + private void handleUserRestrictionAction() { + mTetheringRestriction.onUserRestrictionsChanged(); + } + + private void handleDataSaverChanged() { + final ConnectivityManager connMgr = (ConnectivityManager) mContext.getSystemService( + Context.CONNECTIVITY_SERVICE); + final boolean isDataSaverEnabled = connMgr.getRestrictBackgroundStatus() + != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; + + if (mDataSaverEnabled == isDataSaverEnabled) return; + + mDataSaverEnabled = isDataSaverEnabled; + if (mDataSaverEnabled) { + untetherAll(); + } + } + } + + @VisibleForTesting + SparseArray getActiveTetheringRequests() { + return mActiveTetheringRequests; + } + + @VisibleForTesting + boolean isTetheringActive() { + return getTetheredIfaces().length > 0; + } + + @VisibleForTesting + protected static class UserRestrictionActionListener { + private final UserManager mUserMgr; + private final Tethering mTethering; + private final TetheringNotificationUpdater mNotificationUpdater; + public boolean mDisallowTethering; + + public UserRestrictionActionListener(@NonNull UserManager um, @NonNull Tethering tethering, + @NonNull TetheringNotificationUpdater updater) { + mUserMgr = um; + mTethering = tethering; + mNotificationUpdater = updater; + mDisallowTethering = false; + } + + public void onUserRestrictionsChanged() { + // getUserRestrictions gets restriction for this process' user, which is the primary + // user. This is fine because DISALLOW_CONFIG_TETHERING can only be set on the primary + // user. See UserManager.DISALLOW_CONFIG_TETHERING. + final Bundle restrictions = mUserMgr.getUserRestrictions(); + final boolean newlyDisallowed = + restrictions.getBoolean(UserManager.DISALLOW_CONFIG_TETHERING); + final boolean prevDisallowed = mDisallowTethering; + mDisallowTethering = newlyDisallowed; + + final boolean tetheringDisallowedChanged = (newlyDisallowed != prevDisallowed); + if (!tetheringDisallowedChanged) { + return; + } + + if (!newlyDisallowed) { + // Clear the restricted notification when user is allowed to have tethering + // function. + mNotificationUpdater.tetheringRestrictionLifted(); + return; + } + + if (mTethering.isTetheringActive()) { + // Restricted notification is shown when tethering function is disallowed on + // user's device. + mNotificationUpdater.notifyTetheringDisabledByRestriction(); + + // Untether from all downstreams since tethering is disallowed. + mTethering.untetherAll(); + } + // TODO(b/148139325): send tetheringSupported on restriction change + } + } + + private void disableWifiIpServingLockedCommon(int tetheringType, String ifname, int apState) { + mLog.log("Canceling WiFi tethering request -" + + " type=" + tetheringType + + " interface=" + ifname + + " state=" + apState); + + if (!TextUtils.isEmpty(ifname)) { + final TetherState ts = mTetherStates.get(ifname); + if (ts != null) { + ts.ipServer.unwanted(); + return; + } + } + + for (int i = 0; i < mTetherStates.size(); i++) { + final IpServer ipServer = mTetherStates.valueAt(i).ipServer; + if (ipServer.interfaceType() == tetheringType) { + ipServer.unwanted(); + return; + } + } + + mLog.log("Error disabling Wi-Fi IP serving; " + + (TextUtils.isEmpty(ifname) ? "no interface name specified" + : "specified interface: " + ifname)); + } + + private void disableWifiIpServingLocked(String ifname, int apState) { + // Regardless of whether we requested this transition, the AP has gone + // down. Don't try to tether again unless we're requested to do so. + // TODO: Remove this altogether, once Wi-Fi reliably gives us an + // interface name with every broadcast. + mWifiTetherRequested = false; + + disableWifiIpServingLockedCommon(TETHERING_WIFI, ifname, apState); + } + + private void disableWifiP2pIpServingLockedIfNeeded(String ifname) { + if (TextUtils.isEmpty(ifname)) return; + + disableWifiIpServingLockedCommon(TETHERING_WIFI_P2P, ifname, /* fake */ 0); + } + + private void enableWifiIpServingLocked(String ifname, int wifiIpMode) { + // Map wifiIpMode values to IpServer.Callback serving states, inferring + // from mWifiTetherRequested as a final "best guess". + final int ipServingMode; + switch (wifiIpMode) { + case IFACE_IP_MODE_TETHERED: + ipServingMode = IpServer.STATE_TETHERED; + break; + case IFACE_IP_MODE_LOCAL_ONLY: + ipServingMode = IpServer.STATE_LOCAL_ONLY; + break; + default: + mLog.e("Cannot enable IP serving in unknown WiFi mode: " + wifiIpMode); + return; + } + + if (!TextUtils.isEmpty(ifname)) { + maybeTrackNewInterfaceLocked(ifname); + changeInterfaceState(ifname, ipServingMode); + } else { + mLog.e(String.format( + "Cannot enable IP serving in mode %s on missing interface name", + ipServingMode)); + } + } + + // TODO: Consider renaming to something more accurate in its description. + // This method: + // - allows requesting either tethering or local hotspot serving states + // - handles both enabling and disabling serving states + // - only tethers the first matching interface in listInterfaces() + // order of a given type + private void tetherMatchingInterfaces(int requestedState, int interfaceType) { + if (VDBG) { + Log.d(TAG, "tetherMatchingInterfaces(" + requestedState + ", " + interfaceType + ")"); + } + + String[] ifaces = null; + try { + ifaces = mNetd.interfaceGetList(); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Error listing Interfaces", e); + return; + } + String chosenIface = null; + if (ifaces != null) { + for (String iface : ifaces) { + if (ifaceNameToType(iface) == interfaceType) { + chosenIface = iface; + break; + } + } + } + if (chosenIface == null) { + Log.e(TAG, "could not find iface of type " + interfaceType); + return; + } + + changeInterfaceState(chosenIface, requestedState); + } + + private void changeInterfaceState(String ifname, int requestedState) { + final int result; + switch (requestedState) { + case IpServer.STATE_UNAVAILABLE: + case IpServer.STATE_AVAILABLE: + result = untether(ifname); + break; + case IpServer.STATE_TETHERED: + case IpServer.STATE_LOCAL_ONLY: + result = tether(ifname, requestedState); + break; + default: + Log.wtf(TAG, "Unknown interface state: " + requestedState); + return; + } + if (result != TETHER_ERROR_NO_ERROR) { + Log.e(TAG, "unable start or stop tethering on iface " + ifname); + return; + } + } + + TetheringConfiguration getTetheringConfiguration() { + return mConfig; + } + + boolean hasTetherableConfiguration() { + final TetheringConfiguration cfg = mConfig; + final boolean hasDownstreamConfiguration = + (cfg.tetherableUsbRegexs.length != 0) + || (cfg.tetherableWifiRegexs.length != 0) + || (cfg.tetherableBluetoothRegexs.length != 0); + final boolean hasUpstreamConfiguration = !cfg.preferredUpstreamIfaceTypes.isEmpty() + || cfg.chooseUpstreamAutomatically; + + return hasDownstreamConfiguration && hasUpstreamConfiguration; + } + + void setUsbTethering(boolean enable, IIntResultListener listener) { + mHandler.post(() -> { + try { + listener.onResult(setUsbTethering(enable)); + } catch (RemoteException e) { } + }); + } + + private int setUsbTethering(boolean enable) { + if (VDBG) Log.d(TAG, "setUsbTethering(" + enable + ")"); + UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE); + if (usbManager == null) { + mLog.e("setUsbTethering: failed to get UsbManager!"); + return TETHER_ERROR_SERVICE_UNAVAIL; + } + usbManager.setCurrentFunctions(enable ? UsbManager.FUNCTION_RNDIS + : UsbManager.FUNCTION_NONE); + return TETHER_ERROR_NO_ERROR; + } + + private int setNcmTethering(boolean enable) { + if (VDBG) Log.d(TAG, "setNcmTethering(" + enable + ")"); + UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE); + usbManager.setCurrentFunctions(enable ? UsbManager.FUNCTION_NCM : UsbManager.FUNCTION_NONE); + return TETHER_ERROR_NO_ERROR; + } + + // TODO review API - figure out how to delete these entirely. + String[] getTetheredIfaces() { + ArrayList list = new ArrayList(); + for (int i = 0; i < mTetherStates.size(); i++) { + TetherState tetherState = mTetherStates.valueAt(i); + if (tetherState.lastState == IpServer.STATE_TETHERED) { + list.add(mTetherStates.keyAt(i)); + } + } + return list.toArray(new String[list.size()]); + } + + String[] getTetherableIfacesForTest() { + ArrayList list = new ArrayList(); + for (int i = 0; i < mTetherStates.size(); i++) { + TetherState tetherState = mTetherStates.valueAt(i); + if (tetherState.lastState == IpServer.STATE_AVAILABLE) { + list.add(mTetherStates.keyAt(i)); + } + } + return list.toArray(new String[list.size()]); + } + + private void logMessage(State state, int what) { + mLog.log(state.getName() + " got " + sMagicDecoderRing.get(what, Integer.toString(what))); + } + + private boolean upstreamWanted() { + if (!mForwardedDownstreams.isEmpty()) return true; + return mWifiTetherRequested; + } + + // Needed because the canonical source of upstream truth is just the + // upstream interface set, |mCurrentUpstreamIfaceSet|. + private boolean pertainsToCurrentUpstream(UpstreamNetworkState ns) { + if (ns != null && ns.linkProperties != null && mCurrentUpstreamIfaceSet != null) { + for (String ifname : ns.linkProperties.getAllInterfaceNames()) { + if (mCurrentUpstreamIfaceSet.ifnames.contains(ifname)) { + return true; + } + } + } + return false; + } + + class TetherMainSM extends StateMachine { + // an interface SM has requested Tethering/Local Hotspot + static final int EVENT_IFACE_SERVING_STATE_ACTIVE = BASE_MAIN_SM + 1; + // an interface SM has unrequested Tethering/Local Hotspot + static final int EVENT_IFACE_SERVING_STATE_INACTIVE = BASE_MAIN_SM + 2; + // upstream connection change - do the right thing + static final int CMD_UPSTREAM_CHANGED = BASE_MAIN_SM + 3; + // we don't have a valid upstream conn, check again after a delay + static final int CMD_RETRY_UPSTREAM = BASE_MAIN_SM + 4; + // Events from NetworkCallbacks that we process on the main state + // machine thread on behalf of the UpstreamNetworkMonitor. + static final int EVENT_UPSTREAM_CALLBACK = BASE_MAIN_SM + 5; + // we treated the error and want now to clear it + static final int CMD_CLEAR_ERROR = BASE_MAIN_SM + 6; + static final int EVENT_IFACE_UPDATE_LINKPROPERTIES = BASE_MAIN_SM + 7; + // Events from EntitlementManager to choose upstream again. + static final int EVENT_UPSTREAM_PERMISSION_CHANGED = BASE_MAIN_SM + 8; + private final State mInitialState; + private final State mTetherModeAliveState; + + private final State mSetIpForwardingEnabledErrorState; + private final State mSetIpForwardingDisabledErrorState; + private final State mStartTetheringErrorState; + private final State mStopTetheringErrorState; + private final State mSetDnsForwardersErrorState; + + // This list is a little subtle. It contains all the interfaces that currently are + // requesting tethering, regardless of whether these interfaces are still members of + // mTetherStates. This allows us to maintain the following predicates: + // + // 1) mTetherStates contains the set of all currently existing, tetherable, link state up + // interfaces. + // 2) mNotifyList contains all state machines that may have outstanding tethering state + // that needs to be torn down. + // 3) Use mNotifyList for predictable ordering order for ConnectedClientsTracker. + // + // Because we excise interfaces immediately from mTetherStates, we must maintain mNotifyList + // so that the garbage collector does not clean up the state machine before it has a chance + // to tear itself down. + private final ArrayList mNotifyList; + private final IPv6TetheringCoordinator mIPv6TetheringCoordinator; + private final OffloadWrapper mOffload; + + private static final int UPSTREAM_SETTLE_TIME_MS = 10000; + + TetherMainSM(String name, Looper looper, TetheringDependencies deps) { + super(name, looper); + + mInitialState = new InitialState(); + mTetherModeAliveState = new TetherModeAliveState(); + mSetIpForwardingEnabledErrorState = new SetIpForwardingEnabledErrorState(); + mSetIpForwardingDisabledErrorState = new SetIpForwardingDisabledErrorState(); + mStartTetheringErrorState = new StartTetheringErrorState(); + mStopTetheringErrorState = new StopTetheringErrorState(); + mSetDnsForwardersErrorState = new SetDnsForwardersErrorState(); + + addState(mInitialState); + addState(mTetherModeAliveState); + addState(mSetIpForwardingEnabledErrorState); + addState(mSetIpForwardingDisabledErrorState); + addState(mStartTetheringErrorState); + addState(mStopTetheringErrorState); + addState(mSetDnsForwardersErrorState); + + mNotifyList = new ArrayList<>(); + mIPv6TetheringCoordinator = deps.getIPv6TetheringCoordinator(mNotifyList, mLog); + mOffload = new OffloadWrapper(); + + setInitialState(mInitialState); + } + + /** + * Returns all downstreams that are serving clients, regardless of they are actually + * tethered or localOnly. This must be called on the tethering thread (not thread-safe). + */ + @NonNull + public List getAllDownstreams() { + return mNotifyList; + } + + class InitialState extends State { + @Override + public boolean processMessage(Message message) { + logMessage(this, message.what); + switch (message.what) { + case EVENT_IFACE_SERVING_STATE_ACTIVE: { + final IpServer who = (IpServer) message.obj; + if (VDBG) Log.d(TAG, "Tether Mode requested by " + who); + handleInterfaceServingStateActive(message.arg1, who); + transitionTo(mTetherModeAliveState); + break; + } + case EVENT_IFACE_SERVING_STATE_INACTIVE: { + final IpServer who = (IpServer) message.obj; + if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who); + handleInterfaceServingStateInactive(who); + break; + } + case EVENT_IFACE_UPDATE_LINKPROPERTIES: + // Silently ignore these for now. + break; + default: + return NOT_HANDLED; + } + return HANDLED; + } + } + + protected boolean turnOnMainTetherSettings() { + final TetheringConfiguration cfg = mConfig; + try { + mNetd.ipfwdEnableForwarding(TAG); + } catch (RemoteException | ServiceSpecificException e) { + mLog.e(e); + transitionTo(mSetIpForwardingEnabledErrorState); + return false; + } + + // TODO: Randomize DHCPv4 ranges, especially in hotspot mode. + // Legacy DHCP server is disabled if passed an empty ranges array + final String[] dhcpRanges = cfg.enableLegacyDhcpServer + ? cfg.legacyDhcpRanges : new String[0]; + try { + NetdUtils.tetherStart(mNetd, true /** usingLegacyDnsProxy */, dhcpRanges); + } catch (RemoteException | ServiceSpecificException e) { + try { + // Stop and retry. + mNetd.tetherStop(); + NetdUtils.tetherStart(mNetd, true /** usingLegacyDnsProxy */, dhcpRanges); + } catch (RemoteException | ServiceSpecificException ee) { + mLog.e(ee); + transitionTo(mStartTetheringErrorState); + return false; + } + } + mLog.log("SET main tether settings: ON"); + return true; + } + + protected boolean turnOffMainTetherSettings() { + try { + mNetd.tetherStop(); + } catch (RemoteException | ServiceSpecificException e) { + mLog.e(e); + transitionTo(mStopTetheringErrorState); + return false; + } + try { + mNetd.ipfwdDisableForwarding(TAG); + } catch (RemoteException | ServiceSpecificException e) { + mLog.e(e); + transitionTo(mSetIpForwardingDisabledErrorState); + return false; + } + transitionTo(mInitialState); + mLog.log("SET main tether settings: OFF"); + return true; + } + + protected void chooseUpstreamType(boolean tryCell) { + // We rebuild configuration on ACTION_CONFIGURATION_CHANGED, but we + // do not currently know how to watch for changes in DUN settings. + maybeDunSettingChanged(); + + final TetheringConfiguration config = mConfig; + final UpstreamNetworkState ns = (config.chooseUpstreamAutomatically) + ? mUpstreamNetworkMonitor.getCurrentPreferredUpstream() + : mUpstreamNetworkMonitor.selectPreferredUpstreamType( + config.preferredUpstreamIfaceTypes); + + if (ns == null) { + if (tryCell) { + mUpstreamNetworkMonitor.setTryCell(true); + // We think mobile should be coming up; don't set a retry. + } else { + sendMessageDelayed(CMD_RETRY_UPSTREAM, UPSTREAM_SETTLE_TIME_MS); + } + } else if (!isCellular(ns)) { + mUpstreamNetworkMonitor.setTryCell(false); + } + + setUpstreamNetwork(ns); + final Network newUpstream = (ns != null) ? ns.network : null; + if (mTetherUpstream != newUpstream) { + mTetherUpstream = newUpstream; + mUpstreamNetworkMonitor.setCurrentUpstream(mTetherUpstream); + reportUpstreamChanged(ns); + } + } + + protected void setUpstreamNetwork(UpstreamNetworkState ns) { + InterfaceSet ifaces = null; + if (ns != null) { + // Find the interface with the default IPv4 route. It may be the + // interface described by linkProperties, or one of the interfaces + // stacked on top of it. + mLog.i("Looking for default routes on: " + ns.linkProperties); + ifaces = TetheringInterfaceUtils.getTetheringInterfaces(ns); + mLog.i("Found upstream interface(s): " + ifaces); + } + + if (ifaces != null) { + setDnsForwarders(ns.network, ns.linkProperties); + } + notifyDownstreamsOfNewUpstreamIface(ifaces); + if (ns != null && pertainsToCurrentUpstream(ns)) { + // If we already have UpstreamNetworkState for this network update it immediately. + handleNewUpstreamNetworkState(ns); + } else if (mCurrentUpstreamIfaceSet == null) { + // There are no available upstream networks. + handleNewUpstreamNetworkState(null); + } + } + + protected void setDnsForwarders(final Network network, final LinkProperties lp) { + // TODO: Set v4 and/or v6 DNS per available connectivity. + final Collection dnses = lp.getDnsServers(); + // TODO: Properly support the absence of DNS servers. + final String[] dnsServers; + if (dnses != null && !dnses.isEmpty()) { + dnsServers = new String[dnses.size()]; + int i = 0; + for (InetAddress dns : dnses) { + dnsServers[i++] = dns.getHostAddress(); + } + } else { + dnsServers = mConfig.defaultIPv4DNS; + } + final int netId = (network != null) ? network.getNetId() : NETID_UNSET; + try { + mNetd.tetherDnsSet(netId, dnsServers); + mLog.log(String.format( + "SET DNS forwarders: network=%s dnsServers=%s", + network, Arrays.toString(dnsServers))); + } catch (RemoteException | ServiceSpecificException e) { + // TODO: Investigate how this can fail and what exactly + // happens if/when such failures occur. + mLog.e("setting DNS forwarders failed, " + e); + transitionTo(mSetDnsForwardersErrorState); + } + } + + protected void notifyDownstreamsOfNewUpstreamIface(InterfaceSet ifaces) { + mCurrentUpstreamIfaceSet = ifaces; + for (IpServer ipServer : mNotifyList) { + ipServer.sendMessage(IpServer.CMD_TETHER_CONNECTION_CHANGED, ifaces); + } + } + + protected void handleNewUpstreamNetworkState(UpstreamNetworkState ns) { + mIPv6TetheringCoordinator.updateUpstreamNetworkState(ns); + mOffload.updateUpstreamNetworkState(ns); + + // TODO: Delete all related offload rules which are using this upstream. + if (ns != null) { + // Add upstream index to the map. The upstream interface index is required while + // the conntrack event builds the offload rules. + mBpfCoordinator.addUpstreamIfindexToMap(ns.linkProperties); + } + } + + private void handleInterfaceServingStateActive(int mode, IpServer who) { + if (mNotifyList.indexOf(who) < 0) { + mNotifyList.add(who); + mIPv6TetheringCoordinator.addActiveDownstream(who, mode); + } + + if (mode == IpServer.STATE_TETHERED) { + // No need to notify OffloadController just yet as there are no + // "offload-able" prefixes to pass along. This will handled + // when the TISM informs Tethering of its LinkProperties. + mForwardedDownstreams.add(who); + } else { + mOffload.excludeDownstreamInterface(who.interfaceName()); + mForwardedDownstreams.remove(who); + } + + // If this is a Wi-Fi interface, notify WifiManager of the active serving state. + if (who.interfaceType() == TETHERING_WIFI) { + final WifiManager mgr = getWifiManager(); + final String iface = who.interfaceName(); + switch (mode) { + case IpServer.STATE_TETHERED: + mgr.updateInterfaceIpState(iface, IFACE_IP_MODE_TETHERED); + break; + case IpServer.STATE_LOCAL_ONLY: + mgr.updateInterfaceIpState(iface, IFACE_IP_MODE_LOCAL_ONLY); + break; + default: + Log.wtf(TAG, "Unknown active serving mode: " + mode); + break; + } + } + } + + private void handleInterfaceServingStateInactive(IpServer who) { + mNotifyList.remove(who); + mIPv6TetheringCoordinator.removeActiveDownstream(who); + mOffload.excludeDownstreamInterface(who.interfaceName()); + mForwardedDownstreams.remove(who); + updateConnectedClients(null /* wifiClients */); + + // If this is a Wi-Fi interface, tell WifiManager of any errors + // or the inactive serving state. + if (who.interfaceType() == TETHERING_WIFI) { + final WifiManager mgr = getWifiManager(); + final String iface = who.interfaceName(); + if (mgr == null) { + Log.wtf(TAG, "Skipping WifiManager notification about inactive tethering"); + } else if (who.lastError() != TETHER_ERROR_NO_ERROR) { + mgr.updateInterfaceIpState(iface, IFACE_IP_MODE_CONFIGURATION_ERROR); + } else { + mgr.updateInterfaceIpState(iface, IFACE_IP_MODE_UNSPECIFIED); + } + } + } + + @VisibleForTesting + void handleUpstreamNetworkMonitorCallback(int arg1, Object o) { + if (arg1 == UpstreamNetworkMonitor.NOTIFY_LOCAL_PREFIXES) { + mOffload.sendOffloadExemptPrefixes((Set) o); + return; + } + + final UpstreamNetworkState ns = (UpstreamNetworkState) o; + switch (arg1) { + case UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES: + mPrivateAddressCoordinator.updateUpstreamPrefix(ns); + break; + case UpstreamNetworkMonitor.EVENT_ON_LOST: + mPrivateAddressCoordinator.removeUpstreamPrefix(ns.network); + break; + } + + if (mConfig.chooseUpstreamAutomatically + && arg1 == UpstreamNetworkMonitor.EVENT_DEFAULT_SWITCHED) { + chooseUpstreamType(true); + return; + } + + if (ns == null || !pertainsToCurrentUpstream(ns)) { + // TODO: In future, this is where upstream evaluation and selection + // could be handled for notifications which include sufficient data. + // For example, after CONNECTIVITY_ACTION listening is removed, here + // is where we could observe a Wi-Fi network becoming available and + // passing validation. + if (mCurrentUpstreamIfaceSet == null) { + // If we have no upstream interface, try to run through upstream + // selection again. If, for example, IPv4 connectivity has shown up + // after IPv6 (e.g., 464xlat became available) we want the chance to + // notice and act accordingly. + chooseUpstreamType(false); + } + return; + } + + switch (arg1) { + case UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES: + if (ns.network.equals(mTetherUpstream)) { + mNotificationUpdater.onUpstreamCapabilitiesChanged(ns.networkCapabilities); + } + handleNewUpstreamNetworkState(ns); + break; + case UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES: + chooseUpstreamType(false); + break; + case UpstreamNetworkMonitor.EVENT_ON_LOST: + // TODO: Re-evaluate possible upstreams. Currently upstream + // reevaluation is triggered via received CONNECTIVITY_ACTION + // broadcasts that result in being passed a + // TetherMainSM.CMD_UPSTREAM_CHANGED. + handleNewUpstreamNetworkState(null); + break; + default: + mLog.e("Unknown arg1 value: " + arg1); + break; + } + } + + class TetherModeAliveState extends State { + boolean mUpstreamWanted = false; + boolean mTryCell = true; + + @Override + public void enter() { + // If turning on main tether settings fails, we have already + // transitioned to an error state; exit early. + if (!turnOnMainTetherSettings()) { + return; + } + + mPrivateAddressCoordinator.maybeRemoveDeprecatedUpstreams(); + mUpstreamNetworkMonitor.startObserveAllNetworks(); + + // TODO: De-duplicate with updateUpstreamWanted() below. + if (upstreamWanted()) { + mUpstreamWanted = true; + mOffload.start(); + chooseUpstreamType(true); + mTryCell = false; + } + + // TODO: Check the upstream interface if it is managed by BPF offload. + mBpfCoordinator.startPolling(); + } + + @Override + public void exit() { + mOffload.stop(); + mUpstreamNetworkMonitor.stop(); + notifyDownstreamsOfNewUpstreamIface(null); + handleNewUpstreamNetworkState(null); + if (mTetherUpstream != null) { + mTetherUpstream = null; + reportUpstreamChanged(null); + } + mBpfCoordinator.stopPolling(); + } + + private boolean updateUpstreamWanted() { + final boolean previousUpstreamWanted = mUpstreamWanted; + mUpstreamWanted = upstreamWanted(); + if (mUpstreamWanted != previousUpstreamWanted) { + if (mUpstreamWanted) { + mOffload.start(); + } else { + mOffload.stop(); + } + } + return previousUpstreamWanted; + } + + @Override + public boolean processMessage(Message message) { + logMessage(this, message.what); + boolean retValue = true; + switch (message.what) { + case EVENT_IFACE_SERVING_STATE_ACTIVE: { + IpServer who = (IpServer) message.obj; + if (VDBG) Log.d(TAG, "Tether Mode requested by " + who); + handleInterfaceServingStateActive(message.arg1, who); + who.sendMessage(IpServer.CMD_TETHER_CONNECTION_CHANGED, + mCurrentUpstreamIfaceSet); + // If there has been a change and an upstream is now + // desired, kick off the selection process. + final boolean previousUpstreamWanted = updateUpstreamWanted(); + if (!previousUpstreamWanted && mUpstreamWanted) { + chooseUpstreamType(true); + } + break; + } + case EVENT_IFACE_SERVING_STATE_INACTIVE: { + IpServer who = (IpServer) message.obj; + if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who); + handleInterfaceServingStateInactive(who); + + if (mNotifyList.isEmpty()) { + // This transitions us out of TetherModeAliveState, + // either to InitialState or an error state. + turnOffMainTetherSettings(); + break; + } + + if (DBG) { + Log.d(TAG, "TetherModeAlive still has " + mNotifyList.size() + + " live requests:"); + for (IpServer o : mNotifyList) { + Log.d(TAG, " " + o); + } + } + // If there has been a change and an upstream is no + // longer desired, release any mobile requests. + final boolean previousUpstreamWanted = updateUpstreamWanted(); + if (previousUpstreamWanted && !mUpstreamWanted) { + mUpstreamNetworkMonitor.setTryCell(false); + } + break; + } + case EVENT_IFACE_UPDATE_LINKPROPERTIES: { + final LinkProperties newLp = (LinkProperties) message.obj; + if (message.arg1 == IpServer.STATE_TETHERED) { + mOffload.updateDownstreamLinkProperties(newLp); + } else { + mOffload.excludeDownstreamInterface(newLp.getInterfaceName()); + } + break; + } + case EVENT_UPSTREAM_PERMISSION_CHANGED: + case CMD_UPSTREAM_CHANGED: + updateUpstreamWanted(); + if (!mUpstreamWanted) break; + + // Need to try DUN immediately if Wi-Fi goes down. + chooseUpstreamType(true); + mTryCell = false; + break; + case CMD_RETRY_UPSTREAM: + updateUpstreamWanted(); + if (!mUpstreamWanted) break; + + chooseUpstreamType(mTryCell); + mTryCell = !mTryCell; + break; + case EVENT_UPSTREAM_CALLBACK: { + updateUpstreamWanted(); + if (mUpstreamWanted) { + handleUpstreamNetworkMonitorCallback(message.arg1, message.obj); + } + break; + } + default: + retValue = false; + break; + } + return retValue; + } + } + + class ErrorState extends State { + private int mErrorNotification; + + @Override + public boolean processMessage(Message message) { + boolean retValue = true; + switch (message.what) { + case EVENT_IFACE_SERVING_STATE_ACTIVE: + IpServer who = (IpServer) message.obj; + who.sendMessage(mErrorNotification); + break; + case CMD_CLEAR_ERROR: + mErrorNotification = TETHER_ERROR_NO_ERROR; + transitionTo(mInitialState); + break; + default: + retValue = false; + } + return retValue; + } + + void notify(int msgType) { + mErrorNotification = msgType; + for (IpServer ipServer : mNotifyList) { + ipServer.sendMessage(msgType); + } + } + + } + + class SetIpForwardingEnabledErrorState extends ErrorState { + @Override + public void enter() { + Log.e(TAG, "Error in setIpForwardingEnabled"); + notify(IpServer.CMD_IP_FORWARDING_ENABLE_ERROR); + } + } + + class SetIpForwardingDisabledErrorState extends ErrorState { + @Override + public void enter() { + Log.e(TAG, "Error in setIpForwardingDisabled"); + notify(IpServer.CMD_IP_FORWARDING_DISABLE_ERROR); + } + } + + class StartTetheringErrorState extends ErrorState { + @Override + public void enter() { + Log.e(TAG, "Error in startTethering"); + notify(IpServer.CMD_START_TETHERING_ERROR); + try { + mNetd.ipfwdDisableForwarding(TAG); + } catch (RemoteException | ServiceSpecificException e) { } + } + } + + class StopTetheringErrorState extends ErrorState { + @Override + public void enter() { + Log.e(TAG, "Error in stopTethering"); + notify(IpServer.CMD_STOP_TETHERING_ERROR); + try { + mNetd.ipfwdDisableForwarding(TAG); + } catch (RemoteException | ServiceSpecificException e) { } + } + } + + class SetDnsForwardersErrorState extends ErrorState { + @Override + public void enter() { + Log.e(TAG, "Error in setDnsForwarders"); + notify(IpServer.CMD_SET_DNS_FORWARDERS_ERROR); + try { + mNetd.tetherStop(); + } catch (RemoteException | ServiceSpecificException e) { } + try { + mNetd.ipfwdDisableForwarding(TAG); + } catch (RemoteException | ServiceSpecificException e) { } + } + } + + // A wrapper class to handle multiple situations where several calls to + // the OffloadController need to happen together. + // + // TODO: This suggests that the interface between OffloadController and + // Tethering is in need of improvement. Refactor these calls into the + // OffloadController implementation. + class OffloadWrapper { + public void start() { + final int status = mOffloadController.start() ? TETHER_HARDWARE_OFFLOAD_STARTED + : TETHER_HARDWARE_OFFLOAD_FAILED; + updateOffloadStatus(status); + sendOffloadExemptPrefixes(); + } + + public void stop() { + mOffloadController.stop(); + updateOffloadStatus(TETHER_HARDWARE_OFFLOAD_STOPPED); + } + + public void updateUpstreamNetworkState(UpstreamNetworkState ns) { + mOffloadController.setUpstreamLinkProperties( + (ns != null) ? ns.linkProperties : null); + } + + public void updateDownstreamLinkProperties(LinkProperties newLp) { + // Update the list of offload-exempt prefixes before adding + // new prefixes on downstream interfaces to the offload HAL. + sendOffloadExemptPrefixes(); + mOffloadController.notifyDownstreamLinkProperties(newLp); + } + + public void excludeDownstreamInterface(String ifname) { + // This and other interfaces may be in local-only hotspot mode; + // resend all local prefixes to the OffloadController. + sendOffloadExemptPrefixes(); + mOffloadController.removeDownstreamInterface(ifname); + } + + public void sendOffloadExemptPrefixes() { + sendOffloadExemptPrefixes(mUpstreamNetworkMonitor.getLocalPrefixes()); + } + + public void sendOffloadExemptPrefixes(final Set localPrefixes) { + // Add in well-known minimum set. + PrefixUtils.addNonForwardablePrefixes(localPrefixes); + // Add tragically hardcoded prefixes. + localPrefixes.add(PrefixUtils.DEFAULT_WIFI_P2P_PREFIX); + + // Maybe add prefixes or addresses for downstreams, depending on + // the IP serving mode of each. + for (IpServer ipServer : mNotifyList) { + final LinkProperties lp = ipServer.linkProperties(); + + switch (ipServer.servingMode()) { + case IpServer.STATE_UNAVAILABLE: + case IpServer.STATE_AVAILABLE: + // No usable LinkProperties in these states. + continue; + case IpServer.STATE_TETHERED: + // Only add IPv4 /32 and IPv6 /128 prefixes. The + // directly-connected prefixes will be sent as + // downstream "offload-able" prefixes. + for (LinkAddress addr : lp.getAllLinkAddresses()) { + final InetAddress ip = addr.getAddress(); + if (ip.isLinkLocalAddress()) continue; + localPrefixes.add(PrefixUtils.ipAddressAsPrefix(ip)); + } + break; + case IpServer.STATE_LOCAL_ONLY: + // Add prefixes covering all local IPs. + localPrefixes.addAll(PrefixUtils.localPrefixesFrom(lp)); + break; + } + } + + mOffloadController.setLocalPrefixes(localPrefixes); + } + + private void updateOffloadStatus(final int newStatus) { + if (newStatus == mOffloadStatus) return; + + mOffloadStatus = newStatus; + reportOffloadStatusChanged(mOffloadStatus); + } + } + } + + private void startTrackDefaultNetwork() { + mUpstreamNetworkMonitor.startTrackDefaultNetwork(mEntitlementMgr); + } + + /** Get the latest value of the tethering entitlement check. */ + void requestLatestTetheringEntitlementResult(int type, ResultReceiver receiver, + boolean showEntitlementUi) { + if (receiver == null) return; + + mHandler.post(() -> { + mEntitlementMgr.requestLatestTetheringEntitlementResult(type, receiver, + showEntitlementUi); + }); + } + + /** Register tethering event callback */ + void registerTetheringEventCallback(ITetheringEventCallback callback) { + final boolean hasListPermission = + hasCallingPermission(NETWORK_SETTINGS) + || hasCallingPermission(PERMISSION_MAINLINE_NETWORK_STACK) + || hasCallingPermission(NETWORK_STACK); + mHandler.post(() -> { + mTetheringEventCallbacks.register(callback, new CallbackCookie(hasListPermission)); + final TetheringCallbackStartedParcel parcel = new TetheringCallbackStartedParcel(); + parcel.tetheringSupported = isTetheringSupported(); + parcel.upstreamNetwork = mTetherUpstream; + parcel.config = mConfig.toStableParcelable(); + parcel.states = + mTetherStatesParcel != null ? mTetherStatesParcel : emptyTetherStatesParcel(); + parcel.tetheredClients = hasListPermission + ? mConnectedClientsTracker.getLastTetheredClients() + : Collections.emptyList(); + parcel.offloadStatus = mOffloadStatus; + try { + callback.onCallbackStarted(parcel); + } catch (RemoteException e) { + // Not really very much to do here. + } + }); + } + + private TetherStatesParcel emptyTetherStatesParcel() { + final TetherStatesParcel parcel = new TetherStatesParcel(); + parcel.availableList = new TetheringInterface[0]; + parcel.tetheredList = new TetheringInterface[0]; + parcel.localOnlyList = new TetheringInterface[0]; + parcel.erroredIfaceList = new TetheringInterface[0]; + parcel.lastErrorList = new int[0]; + + return parcel; + } + + private boolean hasCallingPermission(@NonNull String permission) { + return mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED; + } + + /** Unregister tethering event callback */ + void unregisterTetheringEventCallback(ITetheringEventCallback callback) { + mHandler.post(() -> { + mTetheringEventCallbacks.unregister(callback); + }); + } + + private void reportUpstreamChanged(UpstreamNetworkState ns) { + final int length = mTetheringEventCallbacks.beginBroadcast(); + final Network network = (ns != null) ? ns.network : null; + final NetworkCapabilities capabilities = (ns != null) ? ns.networkCapabilities : null; + try { + for (int i = 0; i < length; i++) { + try { + mTetheringEventCallbacks.getBroadcastItem(i).onUpstreamChanged(network); + } catch (RemoteException e) { + // Not really very much to do here. + } + } + } finally { + mTetheringEventCallbacks.finishBroadcast(); + } + // Need to notify capabilities change after upstream network changed because new network's + // capabilities should be checked every time. + mNotificationUpdater.onUpstreamCapabilitiesChanged(capabilities); + } + + private void reportConfigurationChanged(TetheringConfigurationParcel config) { + final int length = mTetheringEventCallbacks.beginBroadcast(); + try { + for (int i = 0; i < length; i++) { + try { + mTetheringEventCallbacks.getBroadcastItem(i).onConfigurationChanged(config); + // TODO(b/148139325): send tetheringSupported on configuration change + } catch (RemoteException e) { + // Not really very much to do here. + } + } + } finally { + mTetheringEventCallbacks.finishBroadcast(); + } + } + + private void reportTetherStateChanged(TetherStatesParcel states) { + final int length = mTetheringEventCallbacks.beginBroadcast(); + try { + for (int i = 0; i < length; i++) { + try { + mTetheringEventCallbacks.getBroadcastItem(i).onTetherStatesChanged(states); + } catch (RemoteException e) { + // Not really very much to do here. + } + } + } finally { + mTetheringEventCallbacks.finishBroadcast(); + } + } + + private void reportTetherClientsChanged(List clients) { + final int length = mTetheringEventCallbacks.beginBroadcast(); + try { + for (int i = 0; i < length; i++) { + try { + final CallbackCookie cookie = + (CallbackCookie) mTetheringEventCallbacks.getBroadcastCookie(i); + if (!cookie.hasListClientsPermission) continue; + mTetheringEventCallbacks.getBroadcastItem(i).onTetherClientsChanged(clients); + } catch (RemoteException e) { + // Not really very much to do here. + } + } + } finally { + mTetheringEventCallbacks.finishBroadcast(); + } + } + + private void reportOffloadStatusChanged(final int status) { + final int length = mTetheringEventCallbacks.beginBroadcast(); + try { + for (int i = 0; i < length; i++) { + try { + mTetheringEventCallbacks.getBroadcastItem(i).onOffloadStatusChanged(status); + } catch (RemoteException e) { + // Not really very much to do here. + } + } + } finally { + mTetheringEventCallbacks.finishBroadcast(); + } + } + + // if ro.tether.denied = true we default to no tethering + // gservices could set the secure setting to 1 though to enable it on a build where it + // had previously been turned off. + boolean isTetheringSupported() { + final int defaultVal = mDeps.isTetheringDenied() ? 0 : 1; + final boolean tetherSupported = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.TETHER_SUPPORTED, defaultVal) != 0; + final boolean tetherEnabledInSettings = tetherSupported + && !mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_TETHERING); + + return tetherEnabledInSettings && hasTetherableConfiguration() + && !isProvisioningNeededButUnavailable(); + } + + private void dumpBpf(IndentingPrintWriter pw) { + pw.println("BPF offload:"); + pw.increaseIndent(); + mBpfCoordinator.dump(pw); + pw.decreaseIndent(); + } + + void doDump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, @Nullable String[] args) { + // Binder.java closes the resource for us. + @SuppressWarnings("resource") final IndentingPrintWriter pw = new IndentingPrintWriter( + writer, " "); + + if (argsContain(args, "bpf")) { + dumpBpf(pw); + return; + } + + pw.println("Tethering:"); + pw.increaseIndent(); + + pw.println("Configuration:"); + pw.increaseIndent(); + final TetheringConfiguration cfg = mConfig; + cfg.dump(pw); + pw.decreaseIndent(); + + pw.println("Entitlement:"); + pw.increaseIndent(); + mEntitlementMgr.dump(pw); + pw.decreaseIndent(); + + pw.println("Tether state:"); + pw.increaseIndent(); + for (int i = 0; i < mTetherStates.size(); i++) { + final String iface = mTetherStates.keyAt(i); + final TetherState tetherState = mTetherStates.valueAt(i); + pw.print(iface + " - "); + + switch (tetherState.lastState) { + case IpServer.STATE_UNAVAILABLE: + pw.print("UnavailableState"); + break; + case IpServer.STATE_AVAILABLE: + pw.print("AvailableState"); + break; + case IpServer.STATE_TETHERED: + pw.print("TetheredState"); + break; + case IpServer.STATE_LOCAL_ONLY: + pw.print("LocalHotspotState"); + break; + default: + pw.print("UnknownState"); + break; + } + pw.println(" - lastError = " + tetherState.lastError); + } + pw.println("Upstream wanted: " + upstreamWanted()); + pw.println("Current upstream interface(s): " + mCurrentUpstreamIfaceSet); + pw.decreaseIndent(); + + pw.println("Hardware offload:"); + pw.increaseIndent(); + mOffloadController.dump(pw); + pw.decreaseIndent(); + + dumpBpf(pw); + + pw.println("Private address coordinator:"); + pw.increaseIndent(); + mPrivateAddressCoordinator.dump(pw); + pw.decreaseIndent(); + + pw.println("Log:"); + pw.increaseIndent(); + if (argsContain(args, "--short")) { + pw.println(""); + } else { + mLog.dump(fd, pw, args); + } + pw.decreaseIndent(); + + pw.decreaseIndent(); + } + + void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, @Nullable String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PERMISSION_GRANTED) { + writer.println("Permission Denial: can't dump."); + return; + } + + final CountDownLatch latch = new CountDownLatch(1); + + // Don't crash the system if something in doDump throws an exception, but try to propagate + // the exception to the caller. + AtomicReference exceptionRef = new AtomicReference<>(); + mHandler.post(() -> { + try { + doDump(fd, writer, args); + } catch (RuntimeException e) { + exceptionRef.set(e); + } + latch.countDown(); + }); + + try { + if (!latch.await(DUMP_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + writer.println("Dump timeout after " + DUMP_TIMEOUT_MS + "ms"); + return; + } + } catch (InterruptedException e) { + exceptionRef.compareAndSet(null, new IllegalStateException("Dump interrupted", e)); + } + + final RuntimeException e = exceptionRef.get(); + if (e != null) throw e; + } + + private static boolean argsContain(String[] args, String target) { + for (String arg : args) { + if (target.equals(arg)) return true; + } + return false; + } + + private void updateConnectedClients(final List wifiClients) { + if (mConnectedClientsTracker.updateConnectedClients(mTetherMainSM.getAllDownstreams(), + wifiClients)) { + reportTetherClientsChanged(mConnectedClientsTracker.getLastTetheredClients()); + } + } + + private IpServer.Callback makeControlCallback() { + return new IpServer.Callback() { + @Override + public void updateInterfaceState(IpServer who, int state, int lastError) { + notifyInterfaceStateChange(who, state, lastError); + } + + @Override + public void updateLinkProperties(IpServer who, LinkProperties newLp) { + notifyLinkPropertiesChanged(who, newLp); + } + + @Override + public void dhcpLeasesChanged() { + updateConnectedClients(null /* wifiClients */); + } + + @Override + public void requestEnableTethering(int tetheringType, boolean enabled) { + enableTetheringInternal(tetheringType, enabled, null); + } + }; + } + + // TODO: Move into TetherMainSM. + private void notifyInterfaceStateChange(IpServer who, int state, int error) { + final String iface = who.interfaceName(); + final TetherState tetherState = mTetherStates.get(iface); + if (tetherState != null && tetherState.ipServer.equals(who)) { + tetherState.lastState = state; + tetherState.lastError = error; + } else { + if (DBG) Log.d(TAG, "got notification from stale iface " + iface); + } + + mLog.log(String.format("OBSERVED iface=%s state=%s error=%s", iface, state, error)); + + // If TetherMainSM is in ErrorState, TetherMainSM stays there. + // Thus we give a chance for TetherMainSM to recover to InitialState + // by sending CMD_CLEAR_ERROR + if (error == TETHER_ERROR_INTERNAL_ERROR) { + mTetherMainSM.sendMessage(TetherMainSM.CMD_CLEAR_ERROR, who); + } + int which; + switch (state) { + case IpServer.STATE_UNAVAILABLE: + case IpServer.STATE_AVAILABLE: + which = TetherMainSM.EVENT_IFACE_SERVING_STATE_INACTIVE; + break; + case IpServer.STATE_TETHERED: + case IpServer.STATE_LOCAL_ONLY: + which = TetherMainSM.EVENT_IFACE_SERVING_STATE_ACTIVE; + break; + default: + Log.wtf(TAG, "Unknown interface state: " + state); + return; + } + mTetherMainSM.sendMessage(which, state, 0, who); + sendTetherStateChangedBroadcast(); + } + + private void notifyLinkPropertiesChanged(IpServer who, LinkProperties newLp) { + final String iface = who.interfaceName(); + final int state; + final TetherState tetherState = mTetherStates.get(iface); + if (tetherState != null && tetherState.ipServer.equals(who)) { + state = tetherState.lastState; + } else { + mLog.log("got notification from stale iface " + iface); + return; + } + + mLog.log(String.format( + "OBSERVED LinkProperties update iface=%s state=%s lp=%s", + iface, IpServer.getStateString(state), newLp)); + final int which = TetherMainSM.EVENT_IFACE_UPDATE_LINKPROPERTIES; + mTetherMainSM.sendMessage(which, state, 0, newLp); + } + + private void maybeTrackNewInterfaceLocked(final String iface) { + // If we don't care about this type of interface, ignore. + final int interfaceType = ifaceNameToType(iface); + if (interfaceType == TETHERING_INVALID) { + mLog.log(iface + " is not a tetherable iface, ignoring"); + return; + } + + final PackageManager pm = mContext.getPackageManager(); + if ((interfaceType == TETHERING_WIFI || interfaceType == TETHERING_WIGIG) + && !pm.hasSystemFeature(PackageManager.FEATURE_WIFI)) { + mLog.log(iface + " is not tetherable, because WiFi feature is disabled"); + return; + } + if (interfaceType == TETHERING_WIFI_P2P + && !pm.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)) { + mLog.log(iface + " is not tetherable, because WiFi Direct feature is disabled"); + return; + } + + maybeTrackNewInterfaceLocked(iface, interfaceType); + } + + private void maybeTrackNewInterfaceLocked(final String iface, int interfaceType) { + // If we have already started a TISM for this interface, skip. + if (mTetherStates.containsKey(iface)) { + mLog.log("active iface (" + iface + ") reported as added, ignoring"); + return; + } + + mLog.log("adding TetheringInterfaceStateMachine for: " + iface); + final TetherState tetherState = new TetherState( + new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator, + makeControlCallback(), mConfig.enableLegacyDhcpServer, + mConfig.isBpfOffloadEnabled(), mPrivateAddressCoordinator, + mDeps.getIpServerDependencies())); + mTetherStates.put(iface, tetherState); + tetherState.ipServer.start(); + } + + private void stopTrackingInterfaceLocked(final String iface) { + final TetherState tetherState = mTetherStates.get(iface); + if (tetherState == null) { + mLog.log("attempting to remove unknown iface (" + iface + "), ignoring"); + return; + } + tetherState.ipServer.stop(); + mLog.log("removing TetheringInterfaceStateMachine for: " + iface); + mTetherStates.remove(iface); + } + + private static String[] copy(String[] strarray) { + return Arrays.copyOf(strarray, strarray.length); + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java new file mode 100644 index 0000000000..2beeeb8bca --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java @@ -0,0 +1,547 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import static android.content.Context.TELEPHONY_SERVICE; +import static android.net.ConnectivityManager.TYPE_ETHERNET; +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_MOBILE_DUN; +import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI; +import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; + +import android.content.Context; +import android.content.res.Resources; +import android.net.TetheringConfigurationParcel; +import android.net.util.SharedLog; +import android.provider.DeviceConfig; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.modules.utils.build.SdkLevel; +import com.android.net.module.util.DeviceConfigUtils; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.StringJoiner; + +/** + * A utility class to encapsulate the various tethering configuration elements. + * + * This configuration data includes elements describing upstream properties + * (preferred and required types of upstream connectivity as well as default + * DNS servers to use if none are available) and downstream properties (such + * as regular expressions use to match suitable downstream interfaces and the + * DHCPv4 ranges to use). + * + * @hide + */ +public class TetheringConfiguration { + private static final String TAG = TetheringConfiguration.class.getSimpleName(); + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private static final String TETHERING_MODULE_NAME = "com.android.tethering"; + + // Default ranges used for the legacy DHCP server. + // USB is 192.168.42.1 and 255.255.255.0 + // Wifi is 192.168.43.1 and 255.255.255.0 + // BT is limited to max default of 5 connections. 192.168.44.1 to 192.168.48.1 + // with 255.255.255.0 + // P2P is 192.168.49.1 and 255.255.255.0 + private static final String[] LEGACY_DHCP_DEFAULT_RANGE = { + "192.168.42.2", "192.168.42.254", "192.168.43.2", "192.168.43.254", + "192.168.44.2", "192.168.44.254", "192.168.45.2", "192.168.45.254", + "192.168.46.2", "192.168.46.254", "192.168.47.2", "192.168.47.254", + "192.168.48.2", "192.168.48.254", "192.168.49.2", "192.168.49.254", + }; + + private static final String[] DEFAULT_IPV4_DNS = {"8.8.4.4", "8.8.8.8"}; + + /** + * Override enabling BPF offload configuration for tethering. + */ + public static final String OVERRIDE_TETHER_ENABLE_BPF_OFFLOAD = + "override_tether_enable_bpf_offload"; + + /** + * Use the old dnsmasq DHCP server for tethering instead of the framework implementation. + */ + public static final String TETHER_ENABLE_LEGACY_DHCP_SERVER = + "tether_enable_legacy_dhcp_server"; + + public static final String USE_LEGACY_WIFI_P2P_DEDICATED_IP = + "use_legacy_wifi_p2p_dedicated_ip"; + + /** + * Flag use to enable select all prefix ranges feature. + * TODO: Remove this flag if there are no problems after M-2020-12 rolls out. + */ + public static final String TETHER_ENABLE_SELECT_ALL_PREFIX_RANGES = + "tether_enable_select_all_prefix_ranges"; + + /** + * Experiment flag to force choosing upstreams automatically. + * + * This setting is intended to help force-enable the feature on OEM devices that disabled it + * via resource overlays, and later noticed issues. To that end, it overrides + * config_tether_upstream_automatic when set to true. + * + * This flag is enabled if !=0 and less than the module APK version: see + * {@link DeviceConfigUtils#isFeatureEnabled}. It is also ignored after R, as later devices + * should just set config_tether_upstream_automatic to true instead. + */ + public static final String TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION = + "tether_force_upstream_automatic_version"; + + /** + * Default value that used to periodic polls tether offload stats from tethering offload HAL + * to make the data warnings work. + */ + public static final int DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS = 5000; + + public final String[] tetherableUsbRegexs; + public final String[] tetherableWifiRegexs; + public final String[] tetherableWigigRegexs; + public final String[] tetherableWifiP2pRegexs; + public final String[] tetherableBluetoothRegexs; + public final String[] tetherableNcmRegexs; + public final boolean isDunRequired; + public final boolean chooseUpstreamAutomatically; + public final Collection preferredUpstreamIfaceTypes; + public final String[] legacyDhcpRanges; + public final String[] defaultIPv4DNS; + public final boolean enableLegacyDhcpServer; + + public final String[] provisioningApp; + public final String provisioningAppNoUi; + public final int provisioningCheckPeriod; + public final String provisioningResponse; + + public final int activeDataSubId; + + private final int mOffloadPollInterval; + // TODO: Add to TetheringConfigurationParcel if required. + private final boolean mEnableBpfOffload; + private final boolean mEnableWifiP2pDedicatedIp; + + private final boolean mEnableSelectAllPrefixRange; + + public TetheringConfiguration(Context ctx, SharedLog log, int id) { + final SharedLog configLog = log.forSubComponent("config"); + + activeDataSubId = id; + Resources res = getResources(ctx, activeDataSubId); + + tetherableUsbRegexs = getResourceStringArray(res, R.array.config_tether_usb_regexs); + tetherableNcmRegexs = getResourceStringArray(res, R.array.config_tether_ncm_regexs); + // TODO: Evaluate deleting this altogether now that Wi-Fi always passes + // us an interface name. Careful consideration needs to be given to + // implications for Settings and for provisioning checks. + tetherableWifiRegexs = getResourceStringArray(res, R.array.config_tether_wifi_regexs); + tetherableWigigRegexs = getResourceStringArray(res, R.array.config_tether_wigig_regexs); + tetherableWifiP2pRegexs = getResourceStringArray( + res, R.array.config_tether_wifi_p2p_regexs); + tetherableBluetoothRegexs = getResourceStringArray( + res, R.array.config_tether_bluetooth_regexs); + + isDunRequired = checkDunRequired(ctx); + + final boolean forceAutomaticUpstream = !SdkLevel.isAtLeastS() + && isFeatureEnabled(ctx, TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION); + chooseUpstreamAutomatically = forceAutomaticUpstream || getResourceBoolean( + res, R.bool.config_tether_upstream_automatic, false /** defaultValue */); + preferredUpstreamIfaceTypes = getUpstreamIfaceTypes(res, isDunRequired); + + legacyDhcpRanges = getLegacyDhcpRanges(res); + defaultIPv4DNS = copy(DEFAULT_IPV4_DNS); + mEnableBpfOffload = getEnableBpfOffload(res); + enableLegacyDhcpServer = getEnableLegacyDhcpServer(res); + + provisioningApp = getResourceStringArray(res, R.array.config_mobile_hotspot_provision_app); + provisioningAppNoUi = getResourceString(res, + R.string.config_mobile_hotspot_provision_app_no_ui); + provisioningCheckPeriod = getResourceInteger(res, + R.integer.config_mobile_hotspot_provision_check_period, + 0 /* No periodic re-check */); + provisioningResponse = getResourceString(res, + R.string.config_mobile_hotspot_provision_response); + + mOffloadPollInterval = getResourceInteger(res, + R.integer.config_tether_offload_poll_interval, + DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); + + mEnableWifiP2pDedicatedIp = getResourceBoolean(res, + R.bool.config_tether_enable_legacy_wifi_p2p_dedicated_ip, + false /* defaultValue */); + + // Flags should normally not be booleans, but this is a kill-switch flag that is only used + // to turn off the feature, so binary rollback problems do not apply. + mEnableSelectAllPrefixRange = getDeviceConfigBoolean( + TETHER_ENABLE_SELECT_ALL_PREFIX_RANGES, true /* defaultValue */); + + configLog.log(toString()); + } + + /** Check whether input interface belong to usb.*/ + public boolean isUsb(String iface) { + return matchesDownstreamRegexs(iface, tetherableUsbRegexs); + } + + /** Check whether input interface belong to wifi.*/ + public boolean isWifi(String iface) { + return matchesDownstreamRegexs(iface, tetherableWifiRegexs); + } + + /** Check whether input interface belong to wigig.*/ + public boolean isWigig(String iface) { + return matchesDownstreamRegexs(iface, tetherableWigigRegexs); + } + + /** Check whether this interface is Wifi P2P interface. */ + public boolean isWifiP2p(String iface) { + return matchesDownstreamRegexs(iface, tetherableWifiP2pRegexs); + } + + /** Check whether using legacy mode for wifi P2P. */ + public boolean isWifiP2pLegacyTetheringMode() { + return (tetherableWifiP2pRegexs == null || tetherableWifiP2pRegexs.length == 0); + } + + /** Check whether input interface belong to bluetooth.*/ + public boolean isBluetooth(String iface) { + return matchesDownstreamRegexs(iface, tetherableBluetoothRegexs); + } + + /** Check if interface is ncm */ + public boolean isNcm(String iface) { + return matchesDownstreamRegexs(iface, tetherableNcmRegexs); + } + + /** Check whether no ui entitlement application is available.*/ + public boolean hasMobileHotspotProvisionApp() { + return !TextUtils.isEmpty(provisioningAppNoUi); + } + + /** Check whether dedicated wifi p2p address is enabled. */ + public boolean shouldEnableWifiP2pDedicatedIp() { + return mEnableWifiP2pDedicatedIp; + } + + /** Does the dumping.*/ + public void dump(PrintWriter pw) { + pw.print("activeDataSubId: "); + pw.println(activeDataSubId); + + dumpStringArray(pw, "tetherableUsbRegexs", tetherableUsbRegexs); + dumpStringArray(pw, "tetherableWifiRegexs", tetherableWifiRegexs); + dumpStringArray(pw, "tetherableWifiP2pRegexs", tetherableWifiP2pRegexs); + dumpStringArray(pw, "tetherableBluetoothRegexs", tetherableBluetoothRegexs); + dumpStringArray(pw, "tetherableNcmRegexs", tetherableNcmRegexs); + + pw.print("isDunRequired: "); + pw.println(isDunRequired); + + pw.print("chooseUpstreamAutomatically: "); + pw.println(chooseUpstreamAutomatically); + pw.print("legacyPreredUpstreamIfaceTypes: "); + pw.println(Arrays.toString(toIntArray(preferredUpstreamIfaceTypes))); + + dumpStringArray(pw, "legacyDhcpRanges", legacyDhcpRanges); + dumpStringArray(pw, "defaultIPv4DNS", defaultIPv4DNS); + + pw.print("offloadPollInterval: "); + pw.println(mOffloadPollInterval); + + dumpStringArray(pw, "provisioningApp", provisioningApp); + pw.print("provisioningAppNoUi: "); + pw.println(provisioningAppNoUi); + + pw.print("enableBpfOffload: "); + pw.println(mEnableBpfOffload); + + pw.print("enableLegacyDhcpServer: "); + pw.println(enableLegacyDhcpServer); + + pw.print("enableWifiP2pDedicatedIp: "); + pw.println(mEnableWifiP2pDedicatedIp); + + pw.print("mEnableSelectAllPrefixRange: "); + pw.println(mEnableSelectAllPrefixRange); + } + + /** Returns the string representation of this object.*/ + public String toString() { + final StringJoiner sj = new StringJoiner(" "); + sj.add(String.format("activeDataSubId:%d", activeDataSubId)); + sj.add(String.format("tetherableUsbRegexs:%s", makeString(tetherableUsbRegexs))); + sj.add(String.format("tetherableWifiRegexs:%s", makeString(tetherableWifiRegexs))); + sj.add(String.format("tetherableWifiP2pRegexs:%s", makeString(tetherableWifiP2pRegexs))); + sj.add(String.format("tetherableBluetoothRegexs:%s", + makeString(tetherableBluetoothRegexs))); + sj.add(String.format("isDunRequired:%s", isDunRequired)); + sj.add(String.format("chooseUpstreamAutomatically:%s", chooseUpstreamAutomatically)); + sj.add(String.format("offloadPollInterval:%d", mOffloadPollInterval)); + sj.add(String.format("preferredUpstreamIfaceTypes:%s", + toIntArray(preferredUpstreamIfaceTypes))); + sj.add(String.format("provisioningApp:%s", makeString(provisioningApp))); + sj.add(String.format("provisioningAppNoUi:%s", provisioningAppNoUi)); + sj.add(String.format("enableBpfOffload:%s", mEnableBpfOffload)); + sj.add(String.format("enableLegacyDhcpServer:%s", enableLegacyDhcpServer)); + return String.format("TetheringConfiguration{%s}", sj.toString()); + } + + private static void dumpStringArray(PrintWriter pw, String label, String[] values) { + pw.print(label); + pw.print(": "); + + if (values != null) { + final StringJoiner sj = new StringJoiner(", ", "[", "]"); + for (String value : values) sj.add(value); + pw.print(sj.toString()); + } else { + pw.print("null"); + } + + pw.println(); + } + + private static String makeString(String[] strings) { + if (strings == null) return "null"; + final StringJoiner sj = new StringJoiner(",", "[", "]"); + for (String s : strings) sj.add(s); + return sj.toString(); + } + + /** Check whether dun is required. */ + public static boolean checkDunRequired(Context ctx) { + final TelephonyManager tm = (TelephonyManager) ctx.getSystemService(TELEPHONY_SERVICE); + // TelephonyManager would uses the active data subscription, which should be the one used + // by tethering. + return (tm != null) ? tm.isTetheringApnRequired() : false; + } + + public int getOffloadPollInterval() { + return mOffloadPollInterval; + } + + public boolean isBpfOffloadEnabled() { + return mEnableBpfOffload; + } + + public boolean isSelectAllPrefixRangeEnabled() { + return mEnableSelectAllPrefixRange; + } + + private static Collection getUpstreamIfaceTypes(Resources res, boolean dunRequired) { + final int[] ifaceTypes = res.getIntArray(R.array.config_tether_upstream_types); + final ArrayList upstreamIfaceTypes = new ArrayList<>(ifaceTypes.length); + for (int i : ifaceTypes) { + switch (i) { + case TYPE_MOBILE: + case TYPE_MOBILE_HIPRI: + if (dunRequired) continue; + break; + case TYPE_MOBILE_DUN: + if (!dunRequired) continue; + break; + } + upstreamIfaceTypes.add(i); + } + + // Fix up upstream interface types for DUN or mobile. NOTE: independent + // of the value of |dunRequired|, cell data of one form or another is + // *always* an upstream, regardless of the upstream interface types + // specified by configuration resources. + if (dunRequired) { + appendIfNotPresent(upstreamIfaceTypes, TYPE_MOBILE_DUN); + } else { + // Do not modify if a cellular interface type is already present in the + // upstream interface types. Add TYPE_MOBILE and TYPE_MOBILE_HIPRI if no + // cellular interface types are found in the upstream interface types. + // This preserves backwards compatibility and prevents the DUN and default + // mobile types incorrectly appearing together, which could happen on + // previous releases in the common case where checkDunRequired returned + // DUN_UNSPECIFIED. + if (!containsOneOf(upstreamIfaceTypes, TYPE_MOBILE, TYPE_MOBILE_HIPRI)) { + upstreamIfaceTypes.add(TYPE_MOBILE); + upstreamIfaceTypes.add(TYPE_MOBILE_HIPRI); + } + } + + // Always make sure our good friend Ethernet is present. + // TODO: consider unilaterally forcing this at the front. + prependIfNotPresent(upstreamIfaceTypes, TYPE_ETHERNET); + + return upstreamIfaceTypes; + } + + private static boolean matchesDownstreamRegexs(String iface, String[] regexs) { + for (String regex : regexs) { + if (iface.matches(regex)) return true; + } + return false; + } + + private static String[] getLegacyDhcpRanges(Resources res) { + final String[] fromResource = getResourceStringArray(res, R.array.config_tether_dhcp_range); + if ((fromResource.length > 0) && (fromResource.length % 2 == 0)) { + return fromResource; + } + return copy(LEGACY_DHCP_DEFAULT_RANGE); + } + + private static String getResourceString(Resources res, final int resId) { + try { + return res.getString(resId); + } catch (Resources.NotFoundException e) { + return ""; + } + } + + private static boolean getResourceBoolean(Resources res, int resId, boolean defaultValue) { + try { + return res.getBoolean(resId); + } catch (Resources.NotFoundException e404) { + return defaultValue; + } + } + + private static String[] getResourceStringArray(Resources res, int resId) { + try { + final String[] strArray = res.getStringArray(resId); + return (strArray != null) ? strArray : EMPTY_STRING_ARRAY; + } catch (Resources.NotFoundException e404) { + return EMPTY_STRING_ARRAY; + } + } + + private static int getResourceInteger(Resources res, int resId, int defaultValue) { + try { + return res.getInteger(resId); + } catch (Resources.NotFoundException e404) { + return defaultValue; + } + } + + private boolean getEnableBpfOffload(final Resources res) { + // Get BPF offload config + // Priority 1: Device config + // Priority 2: Resource config + // Priority 3: Default value + final boolean defaultValue = getResourceBoolean( + res, R.bool.config_tether_enable_bpf_offload, true /** default value */); + + return getDeviceConfigBoolean(OVERRIDE_TETHER_ENABLE_BPF_OFFLOAD, defaultValue); + } + + private boolean getEnableLegacyDhcpServer(final Resources res) { + return getResourceBoolean( + res, R.bool.config_tether_enable_legacy_dhcp_server, false /** defaultValue */) + || getDeviceConfigBoolean( + TETHER_ENABLE_LEGACY_DHCP_SERVER, false /** defaultValue */); + } + + private boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) { + // Due to the limitation of static mock for testing, using #getDeviceConfigProperty instead + // of DeviceConfig#getBoolean. If using #getBoolean here, the test can't know that the + // returned boolean value comes from device config or default value (because of null + // property string). See the test case testBpfOffload{*} in TetheringConfigurationTest.java. + final String value = getDeviceConfigProperty(name); + return value != null ? Boolean.parseBoolean(value) : defaultValue; + } + + @VisibleForTesting + protected String getDeviceConfigProperty(String name) { + return DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, name); + } + + @VisibleForTesting + protected boolean isFeatureEnabled(Context ctx, String featureVersionFlag) { + return DeviceConfigUtils.isFeatureEnabled(ctx, NAMESPACE_CONNECTIVITY, featureVersionFlag, + TETHERING_MODULE_NAME, false /* defaultEnabled */); + } + + private Resources getResources(Context ctx, int subId) { + if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + return getResourcesForSubIdWrapper(ctx, subId); + } else { + return ctx.getResources(); + } + } + + @VisibleForTesting + protected Resources getResourcesForSubIdWrapper(Context ctx, int subId) { + return SubscriptionManager.getResourcesForSubId(ctx, subId); + } + + private static String[] copy(String[] strarray) { + return Arrays.copyOf(strarray, strarray.length); + } + + private static void prependIfNotPresent(ArrayList list, int value) { + if (list.contains(value)) return; + list.add(0, value); + } + + private static void appendIfNotPresent(ArrayList list, int value) { + if (list.contains(value)) return; + list.add(value); + } + + private static boolean containsOneOf(ArrayList list, Integer... values) { + for (Integer value : values) { + if (list.contains(value)) return true; + } + return false; + } + + private static int[] toIntArray(Collection values) { + final int[] result = new int[values.size()]; + int index = 0; + for (Integer value : values) { + result[index++] = value; + } + return result; + } + + /** + * Convert this TetheringConfiguration to a TetheringConfigurationParcel. + */ + public TetheringConfigurationParcel toStableParcelable() { + final TetheringConfigurationParcel parcel = new TetheringConfigurationParcel(); + parcel.subId = activeDataSubId; + parcel.tetherableUsbRegexs = tetherableUsbRegexs; + parcel.tetherableWifiRegexs = tetherableWifiRegexs; + parcel.tetherableBluetoothRegexs = tetherableBluetoothRegexs; + parcel.isDunRequired = isDunRequired; + parcel.chooseUpstreamAutomatically = chooseUpstreamAutomatically; + + parcel.preferredUpstreamIfaceTypes = toIntArray(preferredUpstreamIfaceTypes); + + parcel.legacyDhcpRanges = legacyDhcpRanges; + parcel.defaultIPv4DNS = defaultIPv4DNS; + parcel.enableLegacyDhcpServer = enableLegacyDhcpServer; + parcel.provisioningApp = provisioningApp; + parcel.provisioningAppNoUi = provisioningAppNoUi; + parcel.provisioningCheckPeriod = provisioningCheckPeriod; + return parcel; + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java new file mode 100644 index 0000000000..7df9475153 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import android.app.usage.NetworkStatsManager; +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.net.INetd; +import android.net.ip.IpServer; +import android.net.util.SharedLog; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.SystemProperties; +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +import com.android.internal.util.StateMachine; + +import java.util.ArrayList; + + +/** + * Capture tethering dependencies, for injection. + * + * @hide + */ +public abstract class TetheringDependencies { + /** + * Get a reference to the BpfCoordinator to be used by tethering. + */ + public @NonNull BpfCoordinator getBpfCoordinator( + @NonNull BpfCoordinator.Dependencies deps) { + return new BpfCoordinator(deps); + } + + /** + * Get a reference to the offload hardware interface to be used by tethering. + */ + public OffloadHardwareInterface getOffloadHardwareInterface(Handler h, SharedLog log) { + return new OffloadHardwareInterface(h, log); + } + + /** + * Get a reference to the offload controller to be used by tethering. + */ + @NonNull + public OffloadController getOffloadController(@NonNull Handler h, + @NonNull SharedLog log, @NonNull OffloadController.Dependencies deps) { + final NetworkStatsManager statsManager = + (NetworkStatsManager) getContext().getSystemService(Context.NETWORK_STATS_SERVICE); + return new OffloadController(h, getOffloadHardwareInterface(h, log), + getContext().getContentResolver(), statsManager, log, deps); + } + + + /** + * Get a reference to the UpstreamNetworkMonitor to be used by tethering. + */ + public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx, StateMachine target, + SharedLog log, int what) { + return new UpstreamNetworkMonitor(ctx, target, log, what); + } + + /** + * Get a reference to the IPv6TetheringCoordinator to be used by tethering. + */ + public IPv6TetheringCoordinator getIPv6TetheringCoordinator( + ArrayList notifyList, SharedLog log) { + return new IPv6TetheringCoordinator(notifyList, log); + } + + /** + * Get dependencies to be used by IpServer. + */ + public abstract IpServer.Dependencies getIpServerDependencies(); + + /** + * Indicates whether tethering is supported on the device. + */ + public boolean isTetheringSupported() { + return true; + } + + /** + * Get a reference to the EntitlementManager to be used by tethering. + */ + public EntitlementManager getEntitlementManager(Context ctx, Handler h, SharedLog log, + Runnable callback) { + return new EntitlementManager(ctx, h, log, callback); + } + + /** + * Generate a new TetheringConfiguration according to input sub Id. + */ + public TetheringConfiguration generateTetheringConfiguration(Context ctx, SharedLog log, + int subId) { + return new TetheringConfiguration(ctx, log, subId); + } + + /** + * Get a reference to INetd to be used by tethering. + */ + public INetd getINetd(Context context) { + return INetd.Stub.asInterface( + (IBinder) context.getSystemService(Context.NETD_SERVICE)); + } + + /** + * Get a reference to the TetheringNotificationUpdater to be used by tethering. + */ + public TetheringNotificationUpdater getNotificationUpdater(@NonNull final Context ctx, + @NonNull final Looper looper) { + return new TetheringNotificationUpdater(ctx, looper); + } + + /** + * Get tethering thread looper. + */ + public abstract Looper getTetheringLooper(); + + /** + * Get Context of TetheringSerice. + */ + public abstract Context getContext(); + + /** + * Get a reference to BluetoothAdapter to be used by tethering. + */ + public abstract BluetoothAdapter getBluetoothAdapter(); + + /** + * Get SystemProperties which indicate whether tethering is denied. + */ + public boolean isTetheringDenied() { + return TextUtils.equals(SystemProperties.get("ro.tether.denied"), "true"); + } + + /** + * Get a reference to PrivateAddressCoordinator to be used by Tethering. + */ + public PrivateAddressCoordinator getPrivateAddressCoordinator(Context ctx, + TetheringConfiguration cfg) { + return new PrivateAddressCoordinator(ctx, cfg); + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringInterfaceUtils.java b/Tethering/src/com/android/networkstack/tethering/TetheringInterfaceUtils.java new file mode 100644 index 0000000000..ff38f717a1 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetheringInterfaceUtils.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import android.annotation.Nullable; +import android.net.LinkProperties; +import android.net.NetworkCapabilities; +import android.net.RouteInfo; +import android.net.util.InterfaceSet; + +import com.android.net.module.util.NetUtils; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * @hide + */ +public final class TetheringInterfaceUtils { + private static final InetAddress IN6ADDR_ANY = getByAddress( + new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + private static final InetAddress INADDR_ANY = getByAddress(new byte[] {0, 0, 0, 0}); + + /** + * Get upstream interfaces for tethering based on default routes for IPv4/IPv6. + * @return null if there is no usable interface, or a set of at least one interface otherwise. + */ + public static @Nullable InterfaceSet getTetheringInterfaces(UpstreamNetworkState ns) { + if (ns == null) { + return null; + } + + final LinkProperties lp = ns.linkProperties; + final String if4 = getInterfaceForDestination(lp, INADDR_ANY); + final String if6 = getIPv6Interface(ns); + + return (if4 == null && if6 == null) ? null : new InterfaceSet(if4, if6); + } + + /** + * Get the upstream interface for IPv6 tethering. + * @return null if there is no usable interface, or the interface name otherwise. + */ + public static @Nullable String getIPv6Interface(UpstreamNetworkState ns) { + // Broadly speaking: + // + // [1] does the upstream have an IPv6 default route? + // + // and + // + // [2] does the upstream have one or more global IPv6 /64s + // dedicated to this device? + // + // In lieu of Prefix Delegation and other evaluation of whether a + // prefix may or may not be dedicated to this device, for now just + // check whether the upstream is TRANSPORT_CELLULAR. This works + // because "[t]he 3GPP network allocates each default bearer a unique + // /64 prefix", per RFC 6459, Section 5.2. + final boolean canTether = + (ns != null) && (ns.network != null) + && (ns.linkProperties != null) && (ns.networkCapabilities != null) + // At least one upstream DNS server: + && ns.linkProperties.hasIpv6DnsServer() + // Minimal amount of IPv6 provisioning: + && ns.linkProperties.hasGlobalIpv6Address() + // Temporary approximation of "dedicated prefix": + && ns.networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR); + + return canTether + ? getInterfaceForDestination(ns.linkProperties, IN6ADDR_ANY) + : null; + } + + private static String getInterfaceForDestination(LinkProperties lp, InetAddress dst) { + final RouteInfo ri = (lp != null) + ? NetUtils.selectBestRoute(lp.getAllRoutes(), dst) + : null; + return (ri != null) ? ri.getInterface() : null; + } + + private static InetAddress getByAddress(final byte[] addr) { + try { + return InetAddress.getByAddress(null, addr); + } catch (UnknownHostException e) { + throw new AssertionError("illegal address length" + addr.length); + } + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java b/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java new file mode 100644 index 0000000000..a0198cc9c1 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetheringNotificationUpdater.java @@ -0,0 +1,362 @@ +/* + * 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.networkstack.tethering; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; +import static android.text.TextUtils.isEmpty; + +import android.app.Notification; +import android.app.Notification.Action; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.net.NetworkCapabilities; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.UserHandle; +import android.provider.Settings; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.SparseArray; + +import androidx.annotation.DrawableRes; +import androidx.annotation.IntDef; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A class to display tethering-related notifications. + * + *

This class is not thread safe, it is intended to be used only from the tethering handler + * thread. However the constructor is an exception, as it is called on another thread ; + * therefore for thread safety all members of this class MUST either be final or initialized + * to their default value (0, false or null). + * + * @hide + */ +public class TetheringNotificationUpdater { + private static final String TAG = TetheringNotificationUpdater.class.getSimpleName(); + private static final String CHANNEL_ID = "TETHERING_STATUS"; + private static final String WIFI_DOWNSTREAM = "WIFI"; + private static final String USB_DOWNSTREAM = "USB"; + private static final String BLUETOOTH_DOWNSTREAM = "BT"; + @VisibleForTesting + static final String ACTION_DISABLE_TETHERING = + "com.android.server.connectivity.tethering.DISABLE_TETHERING"; + private static final boolean NOTIFY_DONE = true; + private static final boolean NO_NOTIFY = false; + @VisibleForTesting + static final int EVENT_SHOW_NO_UPSTREAM = 1; + // Id to update and cancel restricted notification. Must be unique within the tethering app. + @VisibleForTesting + static final int RESTRICTED_NOTIFICATION_ID = 1001; + // Id to update and cancel no upstream notification. Must be unique within the tethering app. + @VisibleForTesting + static final int NO_UPSTREAM_NOTIFICATION_ID = 1002; + // Id to update and cancel roaming notification. Must be unique within the tethering app. + @VisibleForTesting + static final int ROAMING_NOTIFICATION_ID = 1003; + @VisibleForTesting + static final int NO_ICON_ID = 0; + @VisibleForTesting + static final int DOWNSTREAM_NONE = 0; + // Refer to TelephonyManager#getSimCarrierId for more details about carrier id. + @VisibleForTesting + static final int VERIZON_CARRIER_ID = 1839; + private final Context mContext; + private final NotificationManager mNotificationManager; + private final NotificationChannel mChannel; + private final Handler mHandler; + + // WARNING : the constructor is called on a different thread. Thread safety therefore + // relies on these values being initialized to 0, false or null, and not any other value. If you + // need to change this, you will need to change the thread where the constructor is invoked, or + // to introduce synchronization. + // Downstream type is one of ConnectivityManager.TETHERING_* constants, 0 1 or 2. + // This value has to be made 1 2 and 4, and OR'd with the others. + private int mDownstreamTypesMask = DOWNSTREAM_NONE; + private boolean mNoUpstream = false; + private boolean mRoaming = false; + + // WARNING : this value is not able to being initialized to 0 and must have volatile because + // telephony service is not guaranteed that is up before tethering service starts. If telephony + // is up later than tethering, TetheringNotificationUpdater will use incorrect and valid + // subscription id(0) to query resources. Therefore, initialized subscription id must be + // INVALID_SUBSCRIPTION_ID. + private volatile int mActiveDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + RESTRICTED_NOTIFICATION_ID, + NO_UPSTREAM_NOTIFICATION_ID, + ROAMING_NOTIFICATION_ID + }) + @interface NotificationId {} + + private static final class MccMncOverrideInfo { + public final String visitedMccMnc; + public final int homeMcc; + public final int homeMnc; + MccMncOverrideInfo(String visitedMccMnc, int mcc, int mnc) { + this.visitedMccMnc = visitedMccMnc; + this.homeMcc = mcc; + this.homeMnc = mnc; + } + } + + private static final SparseArray sCarrierIdToMccMnc = new SparseArray<>(); + + static { + sCarrierIdToMccMnc.put(VERIZON_CARRIER_ID, new MccMncOverrideInfo("20404", 311, 480)); + } + + public TetheringNotificationUpdater(@NonNull final Context context, + @NonNull final Looper looper) { + mContext = context; + mNotificationManager = (NotificationManager) context.createContextAsUser(UserHandle.ALL, 0) + .getSystemService(Context.NOTIFICATION_SERVICE); + mChannel = new NotificationChannel( + CHANNEL_ID, + context.getResources().getString(R.string.notification_channel_tethering_status), + NotificationManager.IMPORTANCE_LOW); + mNotificationManager.createNotificationChannel(mChannel); + mHandler = new NotificationHandler(looper); + } + + private class NotificationHandler extends Handler { + NotificationHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case EVENT_SHOW_NO_UPSTREAM: + notifyTetheringNoUpstream(); + break; + } + } + } + + /** Called when downstream has changed */ + public void onDownstreamChanged(@IntRange(from = 0, to = 7) final int downstreamTypesMask) { + updateActiveNotifications( + mActiveDataSubId, downstreamTypesMask, mNoUpstream, mRoaming); + } + + /** Called when active data subscription id changed */ + public void onActiveDataSubscriptionIdChanged(final int subId) { + updateActiveNotifications(subId, mDownstreamTypesMask, mNoUpstream, mRoaming); + } + + /** Called when upstream network capabilities changed */ + public void onUpstreamCapabilitiesChanged(@Nullable final NetworkCapabilities capabilities) { + final boolean isNoUpstream = (capabilities == null); + final boolean isRoaming = capabilities != null + && !capabilities.hasCapability(NET_CAPABILITY_NOT_ROAMING); + updateActiveNotifications( + mActiveDataSubId, mDownstreamTypesMask, isNoUpstream, isRoaming); + } + + @NonNull + @VisibleForTesting + final Handler getHandler() { + return mHandler; + } + + @NonNull + @VisibleForTesting + Resources getResourcesForSubId(@NonNull final Context context, final int subId) { + final Resources res = SubscriptionManager.getResourcesForSubId(context, subId); + final TelephonyManager tm = + ((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE)) + .createForSubscriptionId(mActiveDataSubId); + final int carrierId = tm.getSimCarrierId(); + final String mccmnc = tm.getSimOperator(); + final MccMncOverrideInfo overrideInfo = sCarrierIdToMccMnc.get(carrierId); + if (overrideInfo != null && overrideInfo.visitedMccMnc.equals(mccmnc)) { + // Re-configure MCC/MNC value to specific carrier to get right resources. + final Configuration config = res.getConfiguration(); + config.mcc = overrideInfo.homeMcc; + config.mnc = overrideInfo.homeMnc; + return context.createConfigurationContext(config).getResources(); + } + return res; + } + + private void updateActiveNotifications(final int subId, final int downstreamTypes, + final boolean noUpstream, final boolean isRoaming) { + final boolean tetheringActiveChanged = + (downstreamTypes == DOWNSTREAM_NONE) != (mDownstreamTypesMask == DOWNSTREAM_NONE); + final boolean subIdChanged = subId != mActiveDataSubId; + final boolean upstreamChanged = noUpstream != mNoUpstream; + final boolean roamingChanged = isRoaming != mRoaming; + final boolean updateAll = tetheringActiveChanged || subIdChanged; + mActiveDataSubId = subId; + mDownstreamTypesMask = downstreamTypes; + mNoUpstream = noUpstream; + mRoaming = isRoaming; + + if (updateAll || upstreamChanged) updateNoUpstreamNotification(); + if (updateAll || roamingChanged) updateRoamingNotification(); + } + + private void updateNoUpstreamNotification() { + final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE; + + if (tetheringInactive || !mNoUpstream || setupNoUpstreamNotification() == NO_NOTIFY) { + clearNotification(NO_UPSTREAM_NOTIFICATION_ID); + mHandler.removeMessages(EVENT_SHOW_NO_UPSTREAM); + } + } + + private void updateRoamingNotification() { + final boolean tetheringInactive = mDownstreamTypesMask == DOWNSTREAM_NONE; + + if (tetheringInactive || !mRoaming || setupRoamingNotification() == NO_NOTIFY) { + clearNotification(ROAMING_NOTIFICATION_ID); + } + } + + @VisibleForTesting + void tetheringRestrictionLifted() { + clearNotification(RESTRICTED_NOTIFICATION_ID); + } + + private void clearNotification(@NotificationId final int id) { + mNotificationManager.cancel(null /* tag */, id); + } + + @VisibleForTesting + static String getSettingsPackageName(@NonNull final PackageManager pm) { + final Intent settingsIntent = new Intent(Settings.ACTION_SETTINGS); + final ComponentName settingsComponent = settingsIntent.resolveActivity(pm); + return settingsComponent != null + ? settingsComponent.getPackageName() : "com.android.settings"; + } + + @VisibleForTesting + void notifyTetheringDisabledByRestriction() { + final Resources res = getResourcesForSubId(mContext, mActiveDataSubId); + final String title = res.getString(R.string.disable_tether_notification_title); + final String message = res.getString(R.string.disable_tether_notification_message); + if (isEmpty(title) || isEmpty(message)) return; + + final PendingIntent pi = PendingIntent.getActivity( + mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */), + 0 /* requestCode */, + new Intent(Settings.ACTION_TETHER_SETTINGS) + .setPackage(getSettingsPackageName(mContext.getPackageManager())) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + PendingIntent.FLAG_IMMUTABLE, + null /* options */); + + showNotification(R.drawable.stat_sys_tether_general, title, message, + RESTRICTED_NOTIFICATION_ID, false /* ongoing */, pi, new Action[0]); + } + + private void notifyTetheringNoUpstream() { + final Resources res = getResourcesForSubId(mContext, mActiveDataSubId); + final String title = res.getString(R.string.no_upstream_notification_title); + final String message = res.getString(R.string.no_upstream_notification_message); + final String disableButton = + res.getString(R.string.no_upstream_notification_disable_button); + if (isEmpty(title) || isEmpty(message) || isEmpty(disableButton)) return; + + final Intent intent = new Intent(ACTION_DISABLE_TETHERING); + intent.setPackage(mContext.getPackageName()); + final PendingIntent pi = PendingIntent.getBroadcast( + mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */), + 0 /* requestCode */, + intent, + PendingIntent.FLAG_IMMUTABLE); + final Action action = new Action.Builder(NO_ICON_ID, disableButton, pi).build(); + + showNotification(R.drawable.stat_sys_tether_general, title, message, + NO_UPSTREAM_NOTIFICATION_ID, true /* ongoing */, null /* pendingIntent */, action); + } + + private boolean setupRoamingNotification() { + final Resources res = getResourcesForSubId(mContext, mActiveDataSubId); + final boolean upstreamRoamingNotification = + res.getBoolean(R.bool.config_upstream_roaming_notification); + + if (!upstreamRoamingNotification) return NO_NOTIFY; + + final String title = res.getString(R.string.upstream_roaming_notification_title); + final String message = res.getString(R.string.upstream_roaming_notification_message); + if (isEmpty(title) || isEmpty(message)) return NO_NOTIFY; + + final PendingIntent pi = PendingIntent.getActivity( + mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */), + 0 /* requestCode */, + new Intent(Settings.ACTION_TETHER_SETTINGS) + .setPackage(getSettingsPackageName(mContext.getPackageManager())) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + PendingIntent.FLAG_IMMUTABLE, + null /* options */); + + showNotification(R.drawable.stat_sys_tether_general, title, message, + ROAMING_NOTIFICATION_ID, true /* ongoing */, pi, new Action[0]); + return NOTIFY_DONE; + } + + private boolean setupNoUpstreamNotification() { + final Resources res = getResourcesForSubId(mContext, mActiveDataSubId); + final int delayToShowUpstreamNotification = + res.getInteger(R.integer.delay_to_show_no_upstream_after_no_backhaul); + + if (delayToShowUpstreamNotification < 0) return NO_NOTIFY; + + mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_SHOW_NO_UPSTREAM), + delayToShowUpstreamNotification); + return NOTIFY_DONE; + } + + private void showNotification(@DrawableRes final int iconId, @NonNull final String title, + @NonNull final String message, @NotificationId final int id, final boolean ongoing, + @Nullable PendingIntent pi, @NonNull final Action... actions) { + final Notification notification = + new Notification.Builder(mContext, mChannel.getId()) + .setSmallIcon(iconId) + .setContentTitle(title) + .setContentText(message) + .setOngoing(ongoing) + .setColor(mContext.getColor( + android.R.color.system_notification_accent_color)) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setCategory(Notification.CATEGORY_STATUS) + .setContentIntent(pi) + .setActions(actions) + .build(); + + mNotificationManager.notify(null /* tag */, id, notification); + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java new file mode 100644 index 0000000000..722ec8f90d --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java @@ -0,0 +1,371 @@ +/* + * 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.networkstack.tethering; + +import static android.Manifest.permission.ACCESS_NETWORK_STATE; +import static android.Manifest.permission.NETWORK_STACK; +import static android.Manifest.permission.TETHER_PRIVILEGED; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; +import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION; +import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION; +import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR; +import static android.net.TetheringManager.TETHER_ERROR_UNSUPPORTED; +import static android.net.dhcp.IDhcpServer.STATUS_UNKNOWN_ERROR; + +import android.app.Service; +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.content.Intent; +import android.net.IIntResultListener; +import android.net.INetworkStackConnector; +import android.net.ITetheringConnector; +import android.net.ITetheringEventCallback; +import android.net.NetworkStack; +import android.net.TetheringRequestParcel; +import android.net.dhcp.DhcpServerCallbacks; +import android.net.dhcp.DhcpServingParamsParcel; +import android.net.ip.IpServer; +import android.os.Binder; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.networkstack.apishim.SettingsShimImpl; +import com.android.networkstack.apishim.common.SettingsShim; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Android service used to manage tethering. + * + *

The service returns a binder for the system server to communicate with the tethering. + */ +public class TetheringService extends Service { + private static final String TAG = TetheringService.class.getSimpleName(); + + private TetheringConnector mConnector; + private SettingsShim mSettingsShim; + + @Override + public void onCreate() { + final TetheringDependencies deps = makeTetheringDependencies(); + // The Tethering object needs a fully functional context to start, so this can't be done + // in the constructor. + mConnector = new TetheringConnector(makeTethering(deps), TetheringService.this); + + mSettingsShim = SettingsShimImpl.newInstance(); + } + + /** + * Make a reference to Tethering object. + */ + @VisibleForTesting + public Tethering makeTethering(TetheringDependencies deps) { + return new Tethering(deps); + } + + @NonNull + @Override + public IBinder onBind(Intent intent) { + return mConnector; + } + + private static class TetheringConnector extends ITetheringConnector.Stub { + private final TetheringService mService; + private final Tethering mTethering; + + TetheringConnector(Tethering tether, TetheringService service) { + mTethering = tether; + mService = service; + } + + @Override + public void tether(String iface, String callerPkg, String callingAttributionTag, + IIntResultListener listener) { + if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return; + + mTethering.tether(iface, IpServer.STATE_TETHERED, listener); + } + + @Override + public void untether(String iface, String callerPkg, String callingAttributionTag, + IIntResultListener listener) { + if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return; + + mTethering.untether(iface, listener); + } + + @Override + public void setUsbTethering(boolean enable, String callerPkg, String callingAttributionTag, + IIntResultListener listener) { + if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return; + + mTethering.setUsbTethering(enable, listener); + } + + @Override + public void startTethering(TetheringRequestParcel request, String callerPkg, + String callingAttributionTag, IIntResultListener listener) { + if (checkAndNotifyCommonError(callerPkg, + callingAttributionTag, + request.exemptFromEntitlementCheck /* onlyAllowPrivileged */, + listener)) { + return; + } + + mTethering.startTethering(request, listener); + } + + @Override + public void stopTethering(int type, String callerPkg, String callingAttributionTag, + IIntResultListener listener) { + if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return; + + try { + mTethering.stopTethering(type); + listener.onResult(TETHER_ERROR_NO_ERROR); + } catch (RemoteException e) { } + } + + @Override + public void requestLatestTetheringEntitlementResult(int type, ResultReceiver receiver, + boolean showEntitlementUi, String callerPkg, String callingAttributionTag) { + if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, receiver)) return; + + mTethering.requestLatestTetheringEntitlementResult(type, receiver, showEntitlementUi); + } + + @Override + public void registerTetheringEventCallback(ITetheringEventCallback callback, + String callerPkg) { + try { + if (!hasTetherAccessPermission()) { + callback.onCallbackStopped(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION); + return; + } + mTethering.registerTetheringEventCallback(callback); + } catch (RemoteException e) { } + } + + @Override + public void unregisterTetheringEventCallback(ITetheringEventCallback callback, + String callerPkg) { + try { + if (!hasTetherAccessPermission()) { + callback.onCallbackStopped(TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION); + return; + } + mTethering.unregisterTetheringEventCallback(callback); + } catch (RemoteException e) { } + } + + @Override + public void stopAllTethering(String callerPkg, String callingAttributionTag, + IIntResultListener listener) { + if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return; + + try { + mTethering.untetherAll(); + listener.onResult(TETHER_ERROR_NO_ERROR); + } catch (RemoteException e) { } + } + + @Override + public void isTetheringSupported(String callerPkg, String callingAttributionTag, + IIntResultListener listener) { + if (checkAndNotifyCommonError(callerPkg, callingAttributionTag, listener)) return; + + try { + listener.onResult(TETHER_ERROR_NO_ERROR); + } catch (RemoteException e) { } + } + + @Override + protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, + @Nullable String[] args) { + mTethering.dump(fd, writer, args); + } + + private boolean checkAndNotifyCommonError(final String callerPkg, + final String callingAttributionTag, final IIntResultListener listener) { + return checkAndNotifyCommonError(callerPkg, callingAttributionTag, + false /* onlyAllowPrivileged */, listener); + } + + private boolean checkAndNotifyCommonError(final String callerPkg, + final String callingAttributionTag, final boolean onlyAllowPrivileged, + final IIntResultListener listener) { + try { + if (!hasTetherChangePermission(callerPkg, callingAttributionTag, + onlyAllowPrivileged)) { + listener.onResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION); + return true; + } + if (!mTethering.isTetheringSupported()) { + listener.onResult(TETHER_ERROR_UNSUPPORTED); + return true; + } + } catch (RemoteException e) { + return true; + } + + return false; + } + + private boolean checkAndNotifyCommonError(final String callerPkg, + final String callingAttributionTag, final ResultReceiver receiver) { + if (!hasTetherChangePermission(callerPkg, callingAttributionTag, + false /* onlyAllowPrivileged */)) { + receiver.send(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION, null); + return true; + } + if (!mTethering.isTetheringSupported()) { + receiver.send(TETHER_ERROR_UNSUPPORTED, null); + return true; + } + + return false; + } + + private boolean hasNetworkStackPermission() { + return checkCallingOrSelfPermission(NETWORK_STACK) + || checkCallingOrSelfPermission(PERMISSION_MAINLINE_NETWORK_STACK); + } + + private boolean hasTetherPrivilegedPermission() { + return checkCallingOrSelfPermission(TETHER_PRIVILEGED); + } + + private boolean checkCallingOrSelfPermission(final String permission) { + return mService.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED; + } + + private boolean hasTetherChangePermission(final String callerPkg, + final String callingAttributionTag, final boolean onlyAllowPrivileged) { + if (onlyAllowPrivileged && !hasNetworkStackPermission()) return false; + + if (hasTetherPrivilegedPermission()) return true; + + if (mTethering.isTetherProvisioningRequired()) return false; + + int uid = Binder.getCallingUid(); + + // If callerPkg's uid is not same as Binder.getCallingUid(), + // checkAndNoteWriteSettingsOperation will return false and the operation will be + // denied. + return mService.checkAndNoteWriteSettingsOperation(mService, uid, callerPkg, + callingAttributionTag, false /* throwException */); + } + + private boolean hasTetherAccessPermission() { + if (hasTetherPrivilegedPermission()) return true; + + return mService.checkCallingOrSelfPermission( + ACCESS_NETWORK_STATE) == PERMISSION_GRANTED; + } + } + + /** + * Check if the package is a allowed to write settings. This also accounts that such an access + * happened. + * + * @return {@code true} iff the package is allowed to write settings. + */ + @VisibleForTesting + boolean checkAndNoteWriteSettingsOperation(@NonNull Context context, int uid, + @NonNull String callingPackage, @Nullable String callingAttributionTag, + boolean throwException) { + return mSettingsShim.checkAndNoteWriteSettingsOperation(context, uid, callingPackage, + callingAttributionTag, throwException); + } + + /** + * An injection method for testing. + */ + @VisibleForTesting + public TetheringDependencies makeTetheringDependencies() { + return new TetheringDependencies() { + @Override + public Looper getTetheringLooper() { + final HandlerThread tetherThread = new HandlerThread("android.tethering"); + tetherThread.start(); + return tetherThread.getLooper(); + } + + @Override + public Context getContext() { + return TetheringService.this; + } + + @Override + public IpServer.Dependencies getIpServerDependencies() { + return new IpServer.Dependencies() { + @Override + public void makeDhcpServer(String ifName, DhcpServingParamsParcel params, + DhcpServerCallbacks cb) { + try { + final INetworkStackConnector service = getNetworkStackConnector(); + if (service == null) return; + + service.makeDhcpServer(ifName, params, cb); + } catch (RemoteException e) { + Log.e(TAG, "Fail to make dhcp server"); + try { + cb.onDhcpServerCreated(STATUS_UNKNOWN_ERROR, null); + } catch (RemoteException re) { } + } + } + }; + } + + // TODO: replace this by NetworkStackClient#getRemoteConnector after refactoring + // networkStackClient. + static final int NETWORKSTACK_TIMEOUT_MS = 60_000; + private INetworkStackConnector getNetworkStackConnector() { + IBinder connector; + try { + final long before = System.currentTimeMillis(); + while ((connector = NetworkStack.getService()) == null) { + if (System.currentTimeMillis() - before > NETWORKSTACK_TIMEOUT_MS) { + Log.wtf(TAG, "Timeout, fail to get INetworkStackConnector"); + return null; + } + Thread.sleep(200); + } + } catch (InterruptedException e) { + Log.wtf(TAG, "Interrupted, fail to get INetworkStackConnector"); + return null; + } + return INetworkStackConnector.Stub.asInterface(connector); + } + + @Override + public BluetoothAdapter getBluetoothAdapter() { + return BluetoothAdapter.getDefaultAdapter(); + } + }; + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java new file mode 100644 index 0000000000..e615334bbb --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java @@ -0,0 +1,690 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import static android.net.ConnectivityManager.TYPE_BLUETOOTH; +import static android.net.ConnectivityManager.TYPE_ETHERNET; +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_MOBILE_DUN; +import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI; +import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.IpPrefix; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.util.PrefixUtils; +import android.net.util.SharedLog; +import android.os.Handler; +import android.util.Log; +import android.util.SparseIntArray; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.StateMachine; +import com.android.networkstack.apishim.ConnectivityManagerShimImpl; +import com.android.networkstack.apishim.common.ConnectivityManagerShim; +import com.android.networkstack.apishim.common.UnsupportedApiLevelException; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + + +/** + * A class to centralize all the network and link properties information + * pertaining to the current and any potential upstream network. + * + * The owner of UNM gets it to register network callbacks by calling the + * following methods : + * Calling #startTrackDefaultNetwork() to track the system default network. + * Calling #startObserveAllNetworks() to observe all networks. Listening all + * networks is necessary while the expression of preferred upstreams remains + * a list of legacy connectivity types. In future, this can be revisited. + * Calling #setTryCell() to request bringing up mobile DUN or HIPRI. + * + * The methods and data members of this class are only to be accessed and + * modified from the tethering main state machine thread. Any other + * access semantics would necessitate the addition of locking. + * + * TODO: Move upstream selection logic here. + * + * All callback methods are run on the same thread as the specified target + * state machine. This class does not require locking when accessed from this + * thread. Access from other threads is not advised. + * + * @hide + */ +public class UpstreamNetworkMonitor { + private static final String TAG = UpstreamNetworkMonitor.class.getSimpleName(); + private static final boolean DBG = false; + private static final boolean VDBG = false; + + public static final int EVENT_ON_CAPABILITIES = 1; + public static final int EVENT_ON_LINKPROPERTIES = 2; + public static final int EVENT_ON_LOST = 3; + public static final int EVENT_DEFAULT_SWITCHED = 4; + public static final int NOTIFY_LOCAL_PREFIXES = 10; + // This value is used by deprecated preferredUpstreamIfaceTypes selection which is default + // disabled. + @VisibleForTesting + public static final int TYPE_NONE = -1; + + private static final int CALLBACK_LISTEN_ALL = 1; + private static final int CALLBACK_DEFAULT_INTERNET = 2; + private static final int CALLBACK_MOBILE_REQUEST = 3; + + private static final SparseIntArray sLegacyTypeToTransport = new SparseIntArray(); + static { + sLegacyTypeToTransport.put(TYPE_MOBILE, NetworkCapabilities.TRANSPORT_CELLULAR); + sLegacyTypeToTransport.put(TYPE_MOBILE_DUN, NetworkCapabilities.TRANSPORT_CELLULAR); + sLegacyTypeToTransport.put(TYPE_MOBILE_HIPRI, NetworkCapabilities.TRANSPORT_CELLULAR); + sLegacyTypeToTransport.put(TYPE_WIFI, NetworkCapabilities.TRANSPORT_WIFI); + sLegacyTypeToTransport.put(TYPE_BLUETOOTH, NetworkCapabilities.TRANSPORT_BLUETOOTH); + sLegacyTypeToTransport.put(TYPE_ETHERNET, NetworkCapabilities.TRANSPORT_ETHERNET); + } + + private final Context mContext; + private final SharedLog mLog; + private final StateMachine mTarget; + private final Handler mHandler; + private final int mWhat; + private final HashMap mNetworkMap = new HashMap<>(); + private HashSet mLocalPrefixes; + private ConnectivityManager mCM; + private EntitlementManager mEntitlementMgr; + private NetworkCallback mListenAllCallback; + private NetworkCallback mDefaultNetworkCallback; + private NetworkCallback mMobileNetworkCallback; + + /** Whether Tethering has requested a cellular upstream. */ + private boolean mTryCell; + /** Whether the carrier requires DUN. */ + private boolean mDunRequired; + /** Whether automatic upstream selection is enabled. */ + private boolean mAutoUpstream; + + // Whether the current default upstream is mobile or not. + private boolean mIsDefaultCellularUpstream; + // The current system default network (not really used yet). + private Network mDefaultInternetNetwork; + // The current upstream network used for tethering. + private Network mTetheringUpstreamNetwork; + + public UpstreamNetworkMonitor(Context ctx, StateMachine tgt, SharedLog log, int what) { + mContext = ctx; + mTarget = tgt; + mHandler = mTarget.getHandler(); + mLog = log.forSubComponent(TAG); + mWhat = what; + mLocalPrefixes = new HashSet<>(); + mIsDefaultCellularUpstream = false; + mCM = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE); + } + + /** + * Tracking the system default network. This method should be only called once when system is + * ready, and the callback is never unregistered. + * + * @param entitle a EntitlementManager object to communicate between EntitlementManager and + * UpstreamNetworkMonitor + */ + public void startTrackDefaultNetwork(EntitlementManager entitle) { + if (mDefaultNetworkCallback != null) { + Log.wtf(TAG, "default network callback is already registered"); + return; + } + ConnectivityManagerShim mCmShim = ConnectivityManagerShimImpl.newInstance(mContext); + mDefaultNetworkCallback = new UpstreamNetworkCallback(CALLBACK_DEFAULT_INTERNET); + try { + mCmShim.registerSystemDefaultNetworkCallback(mDefaultNetworkCallback, mHandler); + } catch (UnsupportedApiLevelException e) { + Log.wtf(TAG, "registerSystemDefaultNetworkCallback is not supported"); + return; + } + if (mEntitlementMgr == null) { + mEntitlementMgr = entitle; + } + } + + /** Listen all networks. */ + public void startObserveAllNetworks() { + stop(); + + final NetworkRequest listenAllRequest = new NetworkRequest.Builder() + .clearCapabilities().build(); + mListenAllCallback = new UpstreamNetworkCallback(CALLBACK_LISTEN_ALL); + cm().registerNetworkCallback(listenAllRequest, mListenAllCallback, mHandler); + } + + /** + * Stop tracking candidate tethering upstreams and release mobile network request. + * Note: this function is used when tethering is stopped because tethering do not need to + * choose upstream anymore. But it would not stop default network tracking because + * EntitlementManager may need to know default network to decide whether to request entitlement + * check even tethering is not active yet. + */ + public void stop() { + releaseMobileNetworkRequest(); + + releaseCallback(mListenAllCallback); + mListenAllCallback = null; + + mTetheringUpstreamNetwork = null; + mNetworkMap.clear(); + } + + private void reevaluateUpstreamRequirements(boolean tryCell, boolean autoUpstream, + boolean dunRequired) { + final boolean mobileRequestRequired = tryCell && (dunRequired || !autoUpstream); + final boolean dunRequiredChanged = (mDunRequired != dunRequired); + + mTryCell = tryCell; + mDunRequired = dunRequired; + mAutoUpstream = autoUpstream; + + if (mobileRequestRequired && !mobileNetworkRequested()) { + registerMobileNetworkRequest(); + } else if (mobileNetworkRequested() && !mobileRequestRequired) { + releaseMobileNetworkRequest(); + } else if (mobileNetworkRequested() && dunRequiredChanged) { + releaseMobileNetworkRequest(); + if (mobileRequestRequired) { + registerMobileNetworkRequest(); + } + } + } + + /** + * Informs UpstreamNetworkMonitor that a cellular upstream is desired. + * + * This may result in filing a NetworkRequest for DUN if it is required, or for MOBILE_HIPRI if + * automatic upstream selection is disabled and MOBILE_HIPRI is the preferred upstream. + */ + public void setTryCell(boolean tryCell) { + reevaluateUpstreamRequirements(tryCell, mAutoUpstream, mDunRequired); + } + + /** Informs UpstreamNetworkMonitor of upstream configuration parameters. */ + public void setUpstreamConfig(boolean autoUpstream, boolean dunRequired) { + reevaluateUpstreamRequirements(mTryCell, autoUpstream, dunRequired); + } + + /** Whether mobile network is requested. */ + public boolean mobileNetworkRequested() { + return (mMobileNetworkCallback != null); + } + + /** Request mobile network if mobile upstream is permitted. */ + private void registerMobileNetworkRequest() { + if (!isCellularUpstreamPermitted()) { + mLog.i("registerMobileNetworkRequest() is not permitted"); + releaseMobileNetworkRequest(); + return; + } + if (mMobileNetworkCallback != null) { + mLog.e("registerMobileNetworkRequest() already registered"); + return; + } + + final NetworkRequest mobileUpstreamRequest; + if (mDunRequired) { + mobileUpstreamRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_DUN) + .removeCapability(NET_CAPABILITY_NOT_RESTRICTED) + .addTransportType(TRANSPORT_CELLULAR).build(); + } else { + mobileUpstreamRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET) + .addTransportType(TRANSPORT_CELLULAR).build(); + } + + // The existing default network and DUN callbacks will be notified. + // Therefore, to avoid duplicate notifications, we only register a no-op. + mMobileNetworkCallback = new UpstreamNetworkCallback(CALLBACK_MOBILE_REQUEST); + + // The following use of the legacy type system cannot be removed until + // upstream selection no longer finds networks by legacy type. + // See also http://b/34364553 . + final int legacyType = mDunRequired ? TYPE_MOBILE_DUN : TYPE_MOBILE_HIPRI; + + // TODO: Change the timeout from 0 (no onUnavailable callback) to some + // moderate callback timeout. This might be useful for updating some UI. + // Additionally, we log a message to aid in any subsequent debugging. + mLog.i("requesting mobile upstream network: " + mobileUpstreamRequest + + " mTryCell=" + mTryCell + " mAutoUpstream=" + mAutoUpstream + + " mDunRequired=" + mDunRequired); + + cm().requestNetwork(mobileUpstreamRequest, 0, legacyType, mHandler, + mMobileNetworkCallback); + } + + /** Release mobile network request. */ + private void releaseMobileNetworkRequest() { + if (mMobileNetworkCallback == null) return; + + cm().unregisterNetworkCallback(mMobileNetworkCallback); + mMobileNetworkCallback = null; + } + + // So many TODOs here, but chief among them is: make this functionality an + // integral part of this class such that whenever a higher priority network + // becomes available and useful we (a) file a request to keep it up as + // necessary and (b) change all upstream tracking state accordingly (by + // passing LinkProperties up to Tethering). + /** + * Select the first available network from |perferredTypes|. + */ + public UpstreamNetworkState selectPreferredUpstreamType(Iterable preferredTypes) { + final TypeStatePair typeStatePair = findFirstAvailableUpstreamByType( + mNetworkMap.values(), preferredTypes, isCellularUpstreamPermitted()); + + mLog.log("preferred upstream type: " + typeStatePair.type); + + switch (typeStatePair.type) { + case TYPE_MOBILE_DUN: + case TYPE_MOBILE_HIPRI: + // Tethering just selected mobile upstream in spite of the default network being + // not mobile. This can happen because of the priority list. + // Notify EntitlementManager to check permission for using mobile upstream. + if (!mIsDefaultCellularUpstream) { + mEntitlementMgr.maybeRunProvisioning(); + } + break; + } + + return typeStatePair.ns; + } + + /** + * Get current preferred upstream network. If default network is cellular and DUN is required, + * preferred upstream would be DUN otherwise preferred upstream is the same as default network. + * Returns null if no current upstream is available. + */ + public UpstreamNetworkState getCurrentPreferredUpstream() { + final UpstreamNetworkState dfltState = (mDefaultInternetNetwork != null) + ? mNetworkMap.get(mDefaultInternetNetwork) + : null; + if (isNetworkUsableAndNotCellular(dfltState)) return dfltState; + + if (!isCellularUpstreamPermitted()) return null; + + if (!mDunRequired) return dfltState; + + // Find a DUN network. Note that code in Tethering causes a DUN request + // to be filed, but this might be moved into this class in future. + return findFirstDunNetwork(mNetworkMap.values()); + } + + /** Tell UpstreamNetworkMonitor which network is the current upstream of tethering. */ + public void setCurrentUpstream(Network upstream) { + mTetheringUpstreamNetwork = upstream; + } + + /** Return local prefixes. */ + public Set getLocalPrefixes() { + return (Set) mLocalPrefixes.clone(); + } + + private boolean isCellularUpstreamPermitted() { + if (mEntitlementMgr != null) { + return mEntitlementMgr.isCellularUpstreamPermitted(); + } else { + // This flow should only happens in testing. + return true; + } + } + + private void handleAvailable(Network network) { + if (mNetworkMap.containsKey(network)) return; + + if (VDBG) Log.d(TAG, "onAvailable for " + network); + mNetworkMap.put(network, new UpstreamNetworkState(null, null, network)); + } + + private void handleNetCap(Network network, NetworkCapabilities newNc) { + final UpstreamNetworkState prev = mNetworkMap.get(network); + if (prev == null || newNc.equals(prev.networkCapabilities)) { + // Ignore notifications about networks for which we have not yet + // received onAvailable() (should never happen) and any duplicate + // notifications (e.g. matching more than one of our callbacks). + return; + } + + if (VDBG) { + Log.d(TAG, String.format("EVENT_ON_CAPABILITIES for %s: %s", + network, newNc)); + } + + mNetworkMap.put(network, new UpstreamNetworkState( + prev.linkProperties, newNc, network)); + // TODO: If sufficient information is available to select a more + // preferable upstream, do so now and notify the target. + notifyTarget(EVENT_ON_CAPABILITIES, network); + } + + private @Nullable UpstreamNetworkState updateLinkProperties(@NonNull Network network, + LinkProperties newLp) { + final UpstreamNetworkState prev = mNetworkMap.get(network); + if (prev == null || newLp.equals(prev.linkProperties)) { + // Ignore notifications about networks for which we have not yet + // received onAvailable() (should never happen) and any duplicate + // notifications (e.g. matching more than one of our callbacks). + // + // Also, it can happen that onLinkPropertiesChanged is called after + // onLost removed the state from mNetworkMap. This appears to be due + // to a bug in disconnectAndDestroyNetwork, which calls + // nai.clatd.update() after the onLost callbacks. + // TODO: fix the bug and make this method void. + return null; + } + + if (VDBG) { + Log.d(TAG, String.format("EVENT_ON_LINKPROPERTIES for %s: %s", + network, newLp)); + } + + final UpstreamNetworkState ns = new UpstreamNetworkState(newLp, prev.networkCapabilities, + network); + mNetworkMap.put(network, ns); + return ns; + } + + private void handleLinkProp(Network network, LinkProperties newLp) { + final UpstreamNetworkState ns = updateLinkProperties(network, newLp); + if (ns != null) { + notifyTarget(EVENT_ON_LINKPROPERTIES, ns); + } + } + + private void handleLost(Network network) { + // There are few TODOs within ConnectivityService's rematching code + // pertaining to spurious onLost() notifications. + // + // TODO: simplify this, probably if favor of code that: + // - selects a new upstream if mTetheringUpstreamNetwork has + // been lost (by any callback) + // - deletes the entry from the map only when the LISTEN_ALL + // callback gets notified. + + if (!mNetworkMap.containsKey(network)) { + // Ignore loss of networks about which we had not previously + // learned any information or for which we have already processed + // an onLost() notification. + return; + } + + if (VDBG) Log.d(TAG, "EVENT_ON_LOST for " + network); + + // TODO: If sufficient information is available to select a more + // preferable upstream, do so now and notify the target. Likewise, + // if the current upstream network is gone, notify the target of the + // fact that we now have no upstream at all. + notifyTarget(EVENT_ON_LOST, mNetworkMap.remove(network)); + } + + private void maybeHandleNetworkSwitch(@NonNull Network network) { + if (Objects.equals(mDefaultInternetNetwork, network)) return; + + final UpstreamNetworkState ns = mNetworkMap.get(network); + if (ns == null) { + // Can never happen unless there is a bug in ConnectivityService. Entries are only + // removed from mNetworkMap when receiving onLost, and onLost for a given network can + // never be followed by any other callback on that network. + Log.wtf(TAG, "maybeHandleNetworkSwitch: no UpstreamNetworkState for " + network); + return; + } + + // Default network changed. Update local data and notify tethering. + Log.d(TAG, "New default Internet network: " + network); + mDefaultInternetNetwork = network; + notifyTarget(EVENT_DEFAULT_SWITCHED, ns); + } + + private void recomputeLocalPrefixes() { + final HashSet localPrefixes = allLocalPrefixes(mNetworkMap.values()); + if (!mLocalPrefixes.equals(localPrefixes)) { + mLocalPrefixes = localPrefixes; + notifyTarget(NOTIFY_LOCAL_PREFIXES, localPrefixes.clone()); + } + } + + // Fetch (and cache) a ConnectivityManager only if and when we need one. + private ConnectivityManager cm() { + if (mCM == null) { + // MUST call the String variant to be able to write unittests. + mCM = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + } + return mCM; + } + + /** + * A NetworkCallback class that handles information of interest directly + * in the thread on which it is invoked. To avoid locking, this MUST be + * run on the same thread as the target state machine's handler. + */ + private class UpstreamNetworkCallback extends NetworkCallback { + private final int mCallbackType; + + UpstreamNetworkCallback(int callbackType) { + mCallbackType = callbackType; + } + + @Override + public void onAvailable(Network network) { + handleAvailable(network); + } + + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities newNc) { + if (mCallbackType == CALLBACK_DEFAULT_INTERNET) { + // mDefaultInternetNetwork is not updated here because upstream selection must only + // run when the LinkProperties have been updated as well as the capabilities. If + // this callback is due to a default network switch, then the system will invoke + // onLinkPropertiesChanged right after this method and mDefaultInternetNetwork will + // be updated then. + // + // Technically, not updating here isn't necessary, because the notifications to + // Tethering sent by notifyTarget are messages sent to a state machine running on + // the same thread as this method, and so cannot arrive until after this method has + // returned. However, it is not a good idea to rely on that because fact that + // Tethering uses multiple state machines running on the same thread is a major + // source of race conditions and something that should be fixed. + // + // TODO: is it correct that this code always updates EntitlementManager? + // This code runs when the default network connects or changes capabilities, but the + // default network might not be the tethering upstream. + final boolean newIsCellular = isCellular(newNc); + if (mIsDefaultCellularUpstream != newIsCellular) { + mIsDefaultCellularUpstream = newIsCellular; + mEntitlementMgr.notifyUpstream(newIsCellular); + } + return; + } + + handleNetCap(network, newNc); + } + + @Override + public void onLinkPropertiesChanged(Network network, LinkProperties newLp) { + if (mCallbackType == CALLBACK_DEFAULT_INTERNET) { + updateLinkProperties(network, newLp); + // When the default network callback calls onLinkPropertiesChanged, it means that + // all the network information for the default network is known (because + // onLinkPropertiesChanged is called after onAvailable and onCapabilitiesChanged). + // Inform tethering that the default network might have changed. + maybeHandleNetworkSwitch(network); + return; + } + + handleLinkProp(network, newLp); + // Any non-LISTEN_ALL callback will necessarily concern a network that will + // also match the LISTEN_ALL callback by construction of the LISTEN_ALL callback. + // So it's not useful to do this work for non-LISTEN_ALL callbacks. + if (mCallbackType == CALLBACK_LISTEN_ALL) { + recomputeLocalPrefixes(); + } + } + + @Override + public void onLost(Network network) { + if (mCallbackType == CALLBACK_DEFAULT_INTERNET) { + mDefaultInternetNetwork = null; + mIsDefaultCellularUpstream = false; + mEntitlementMgr.notifyUpstream(false); + Log.d(TAG, "Lost default Internet network: " + network); + notifyTarget(EVENT_DEFAULT_SWITCHED, null); + return; + } + + handleLost(network); + // Any non-LISTEN_ALL callback will necessarily concern a network that will + // also match the LISTEN_ALL callback by construction of the LISTEN_ALL callback. + // So it's not useful to do this work for non-LISTEN_ALL callbacks. + if (mCallbackType == CALLBACK_LISTEN_ALL) { + recomputeLocalPrefixes(); + } + } + } + + private void releaseCallback(NetworkCallback cb) { + if (cb != null) cm().unregisterNetworkCallback(cb); + } + + private void notifyTarget(int which, Network network) { + notifyTarget(which, mNetworkMap.get(network)); + } + + private void notifyTarget(int which, Object obj) { + mTarget.sendMessage(mWhat, which, 0, obj); + } + + private static class TypeStatePair { + public int type = TYPE_NONE; + public UpstreamNetworkState ns = null; + } + + private static TypeStatePair findFirstAvailableUpstreamByType( + Iterable netStates, Iterable preferredTypes, + boolean isCellularUpstreamPermitted) { + final TypeStatePair result = new TypeStatePair(); + + for (int type : preferredTypes) { + NetworkCapabilities nc; + try { + nc = networkCapabilitiesForType(type); + } catch (IllegalArgumentException iae) { + Log.e(TAG, "No NetworkCapabilities mapping for legacy type: " + type); + continue; + } + if (!isCellularUpstreamPermitted && isCellular(nc)) { + continue; + } + + for (UpstreamNetworkState value : netStates) { + if (!nc.satisfiedByNetworkCapabilities(value.networkCapabilities)) { + continue; + } + + result.type = type; + result.ns = value; + return result; + } + } + + return result; + } + + private static HashSet allLocalPrefixes(Iterable netStates) { + final HashSet prefixSet = new HashSet<>(); + + for (UpstreamNetworkState ns : netStates) { + final LinkProperties lp = ns.linkProperties; + if (lp == null) continue; + prefixSet.addAll(PrefixUtils.localPrefixesFrom(lp)); + } + + return prefixSet; + } + + /** Check whether upstream is cellular. */ + static boolean isCellular(UpstreamNetworkState ns) { + return (ns != null) && isCellular(ns.networkCapabilities); + } + + private static boolean isCellular(NetworkCapabilities nc) { + return (nc != null) && nc.hasTransport(TRANSPORT_CELLULAR) + && nc.hasCapability(NET_CAPABILITY_NOT_VPN); + } + + private static boolean hasCapability(UpstreamNetworkState ns, int netCap) { + return (ns != null) && (ns.networkCapabilities != null) + && ns.networkCapabilities.hasCapability(netCap); + } + + private static boolean isNetworkUsableAndNotCellular(UpstreamNetworkState ns) { + return (ns != null) && (ns.networkCapabilities != null) && (ns.linkProperties != null) + && !isCellular(ns.networkCapabilities); + } + + private static UpstreamNetworkState findFirstDunNetwork( + Iterable netStates) { + for (UpstreamNetworkState ns : netStates) { + if (isCellular(ns) && hasCapability(ns, NET_CAPABILITY_DUN)) return ns; + } + + return null; + } + + /** + * Given a legacy type (TYPE_WIFI, ...) returns the corresponding NetworkCapabilities instance. + * This function is used for deprecated legacy type and be disabled by default. + */ + @VisibleForTesting + public static NetworkCapabilities networkCapabilitiesForType(int type) { + final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder(); + + // Map from type to transports. + final int notFound = -1; + final int transport = sLegacyTypeToTransport.get(type, notFound); + if (transport == notFound) { + throw new IllegalArgumentException("unknown legacy type: " + type); + } + builder.addTransportType(transport); + + if (type == TYPE_MOBILE_DUN) { + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_DUN); + // DUN is restricted network, see NetworkCapabilities#FORCE_RESTRICTED_CAPABILITIES. + builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED); + } else { + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + } + return builder.build(); + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkState.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkState.java new file mode 100644 index 0000000000..bab9f84cf7 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkState.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.networkstack.tethering; + +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; + +import androidx.annotation.NonNull; + +/** + * Snapshot of tethering upstream network state. + */ +public class UpstreamNetworkState { + /** {@link LinkProperties}. */ + public final LinkProperties linkProperties; + /** {@link NetworkCapabilities}. */ + public final NetworkCapabilities networkCapabilities; + /** {@link Network}. */ + public final Network network; + + /** Constructs a new UpstreamNetworkState. */ + public UpstreamNetworkState(LinkProperties linkProperties, + NetworkCapabilities networkCapabilities, Network network) { + this.linkProperties = linkProperties; + this.networkCapabilities = networkCapabilities; + this.network = network; + } + + @NonNull + @Override + public String toString() { + return String.format("UpstreamNetworkState{%s, %s, %s}", + network == null ? "null" : network, + networkCapabilities == null ? "null" : networkCapabilities, + linkProperties == null ? "null" : linkProperties); + } +} diff --git a/Tethering/tests/Android.bp b/Tethering/tests/Android.bp new file mode 100644 index 0000000000..8f31c577a6 --- /dev/null +++ b/Tethering/tests/Android.bp @@ -0,0 +1,28 @@ +// +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +filegroup { + name: "TetheringTestsJarJarRules", + srcs: ["jarjar-rules.txt"], + visibility: [ + "//frameworks/base/packages/Tethering/tests:__subpackages__", + "//packages/modules/Connectivity/Tethering/tests:__subpackages__", + ] +} diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp new file mode 100644 index 0000000000..351b9f4f5a --- /dev/null +++ b/Tethering/tests/integration/Android.bp @@ -0,0 +1,110 @@ +// +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_defaults { + name: "TetheringIntegrationTestsDefaults", + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + min_sdk_version: "30", + static_libs: [ + "NetworkStackApiStableLib", + "androidx.test.rules", + "mockito-target-extended-minus-junit4", + "net-tests-utils", + "testables", + ], + libs: [ + "android.test.runner", + "android.test.base", + "android.test.mock", + ], + jni_libs: [ + // For mockito extended + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + jarjar_rules: ":NetworkStackJarJarRules", +} + +android_library { + name: "TetheringIntegrationTestsLatestSdkLib", + target_sdk_version: "30", + platform_apis: true, + defaults: ["TetheringIntegrationTestsDefaults"], + visibility: [ + "//packages/modules/Connectivity/tests/cts/tethering", + "//packages/modules/Connectivity/Tethering/tests/mts", + ] +} + +android_library { + name: "TetheringIntegrationTestsLib", + target_sdk_version: "current", + platform_apis: true, + defaults: ["TetheringIntegrationTestsDefaults"], + visibility: [ + "//packages/modules/Connectivity/tests/cts/tethering", + "//packages/modules/Connectivity/Tethering/tests/mts", + ] +} + +android_test { + name: "TetheringIntegrationTests", + platform_apis: true, + defaults: ["TetheringIntegrationTestsDefaults"], + test_suites: [ + "device-tests", + "mts", + ], + compile_multilib: "both", +} + +// Special version of the tethering tests that includes all tests necessary for code coverage +// purposes. This is currently the union of TetheringTests, TetheringIntegrationTests and +// NetworkStackTests. +android_test { + name: "TetheringCoverageTests", + platform_apis: true, + min_sdk_version: "30", + target_sdk_version: "30", + test_suites: ["device-tests", "mts"], + test_config: "AndroidTest_Coverage.xml", + defaults: ["libnetworkstackutilsjni_deps"], + static_libs: [ + "modules-utils-native-coverage-listener", + "NetdStaticLibTestsLib", + "NetworkStaticLibTestsLib", + "NetworkStackTestsLib", + "TetheringTestsLatestSdkLib", + "TetheringIntegrationTestsLatestSdkLib", + ], + jni_libs: [ + // For mockito extended + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + // For NetworkStackUtils included in NetworkStackBase + "libnetworkstackutilsjni", + "libtetherutilsjni", + ], + jarjar_rules: ":TetheringTestsJarJarRules", + compile_multilib: "both", + manifest: "AndroidManifest_coverage.xml", +} diff --git a/Tethering/tests/integration/AndroidManifest.xml b/Tethering/tests/integration/AndroidManifest.xml new file mode 100644 index 0000000000..fddfaad29f --- /dev/null +++ b/Tethering/tests/integration/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/Tethering/tests/integration/AndroidManifest_coverage.xml b/Tethering/tests/integration/AndroidManifest_coverage.xml new file mode 100644 index 0000000000..06de00d785 --- /dev/null +++ b/Tethering/tests/integration/AndroidManifest_coverage.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/Tethering/tests/integration/AndroidTest_Coverage.xml b/Tethering/tests/integration/AndroidTest_Coverage.xml new file mode 100644 index 0000000000..33c5b3d8ec --- /dev/null +++ b/Tethering/tests/integration/AndroidTest_Coverage.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java new file mode 100644 index 0000000000..f1ddc6dffa --- /dev/null +++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java @@ -0,0 +1,706 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import static android.Manifest.permission.ACCESS_NETWORK_STATE; +import static android.Manifest.permission.MANAGE_TEST_NETWORKS; +import static android.Manifest.permission.NETWORK_SETTINGS; +import static android.Manifest.permission.TETHER_PRIVILEGED; +import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL; +import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL; +import static android.net.TetheringManager.TETHERING_ETHERNET; +import static android.system.OsConstants.IPPROTO_ICMPV6; + +import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA; +import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT; + +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.assumeFalse; +import static org.junit.Assume.assumeTrue; + +import android.app.UiAutomation; +import android.content.Context; +import android.net.EthernetManager.TetheredInterfaceCallback; +import android.net.EthernetManager.TetheredInterfaceRequest; +import android.net.TetheringManager.StartTetheringCallback; +import android.net.TetheringManager.TetheringEventCallback; +import android.net.TetheringManager.TetheringRequest; +import android.net.dhcp.DhcpAckPacket; +import android.net.dhcp.DhcpOfferPacket; +import android.net.dhcp.DhcpPacket; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.system.Os; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.MediumTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.structs.EthernetHeader; +import com.android.net.module.util.structs.Icmpv6Header; +import com.android.net.module.util.structs.Ipv6Header; +import com.android.testutils.HandlerUtils; +import com.android.testutils.TapPacketReader; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.FileDescriptor; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class EthernetTetheringTest { + + private static final String TAG = EthernetTetheringTest.class.getSimpleName(); + private static final int TIMEOUT_MS = 5000; + private static final int PACKET_READ_TIMEOUT_MS = 100; + private static final int DHCP_DISCOVER_ATTEMPTS = 10; + private static final byte[] DHCP_REQUESTED_PARAMS = new byte[] { + DhcpPacket.DHCP_SUBNET_MASK, + DhcpPacket.DHCP_ROUTER, + DhcpPacket.DHCP_DNS_SERVER, + DhcpPacket.DHCP_LEASE_TIME, + }; + private static final String DHCP_HOSTNAME = "testhostname"; + + private final Context mContext = InstrumentationRegistry.getContext(); + private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class); + private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class); + + private TestNetworkInterface mTestIface; + private HandlerThread mHandlerThread; + private Handler mHandler; + private TapPacketReader mTapPacketReader; + + private TetheredInterfaceRequester mTetheredInterfaceRequester; + private MyTetheringEventCallback mTetheringEventCallback; + + private UiAutomation mUiAutomation = + InstrumentationRegistry.getInstrumentation().getUiAutomation(); + private boolean mRunTests; + + @Before + public void setUp() throws Exception { + // Needed to create a TestNetworkInterface, to call requestTetheredInterface, and to receive + // tethered client callbacks. + mUiAutomation.adoptShellPermissionIdentity( + MANAGE_TEST_NETWORKS, NETWORK_SETTINGS, TETHER_PRIVILEGED, ACCESS_NETWORK_STATE); + mRunTests = mTm.isTetheringSupported() && mEm != null; + assumeTrue(mRunTests); + + mHandlerThread = new HandlerThread(getClass().getSimpleName()); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + mTetheredInterfaceRequester = new TetheredInterfaceRequester(mHandler, mEm); + } + + private void cleanUp() throws Exception { + mTm.stopTethering(TETHERING_ETHERNET); + if (mTetheringEventCallback != null) { + mTetheringEventCallback.awaitInterfaceUntethered(); + mTetheringEventCallback.unregister(); + mTetheringEventCallback = null; + } + if (mTapPacketReader != null) { + TapPacketReader reader = mTapPacketReader; + mHandler.post(() -> reader.stop()); + mTapPacketReader = null; + } + mHandlerThread.quitSafely(); + mTetheredInterfaceRequester.release(); + mEm.setIncludeTestInterfaces(false); + maybeDeleteTestInterface(); + } + + @After + public void tearDown() throws Exception { + try { + if (mRunTests) cleanUp(); + } finally { + mUiAutomation.dropShellPermissionIdentity(); + } + } + + @Test + public void testVirtualEthernetAlreadyExists() throws Exception { + // This test requires manipulating packets. Skip if there is a physical Ethernet connected. + assumeFalse(mEm.isAvailable()); + + mTestIface = createTestInterface(); + // This must be done now because as soon as setIncludeTestInterfaces(true) is called, the + // interface will be placed in client mode, which will delete the link-local address. + // At that point NetworkInterface.getByName() will cease to work on the interface, because + // starting in R NetworkInterface can no longer see interfaces without IP addresses. + int mtu = getMTU(mTestIface); + + Log.d(TAG, "Including test interfaces"); + mEm.setIncludeTestInterfaces(true); + + final String iface = mTetheredInterfaceRequester.getInterface(); + assertEquals("TetheredInterfaceCallback for unexpected interface", + mTestIface.getInterfaceName(), iface); + + checkVirtualEthernet(mTestIface, mtu); + } + + @Test + public void testVirtualEthernet() throws Exception { + // This test requires manipulating packets. Skip if there is a physical Ethernet connected. + assumeFalse(mEm.isAvailable()); + + CompletableFuture futureIface = mTetheredInterfaceRequester.requestInterface(); + + mEm.setIncludeTestInterfaces(true); + + mTestIface = createTestInterface(); + + final String iface = futureIface.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + assertEquals("TetheredInterfaceCallback for unexpected interface", + mTestIface.getInterfaceName(), iface); + + checkVirtualEthernet(mTestIface, getMTU(mTestIface)); + } + + @Test + public void testStaticIpv4() throws Exception { + assumeFalse(mEm.isAvailable()); + + mEm.setIncludeTestInterfaces(true); + + mTestIface = createTestInterface(); + + final String iface = mTetheredInterfaceRequester.getInterface(); + assertEquals("TetheredInterfaceCallback for unexpected interface", + mTestIface.getInterfaceName(), iface); + + assertInvalidStaticIpv4Request(iface, null, null); + assertInvalidStaticIpv4Request(iface, "2001:db8::1/64", "2001:db8:2::/64"); + assertInvalidStaticIpv4Request(iface, "192.0.2.2/28", "2001:db8:2::/28"); + assertInvalidStaticIpv4Request(iface, "2001:db8:2::/28", "192.0.2.2/28"); + assertInvalidStaticIpv4Request(iface, "192.0.2.2/28", null); + assertInvalidStaticIpv4Request(iface, null, "192.0.2.2/28"); + assertInvalidStaticIpv4Request(iface, "192.0.2.3/27", "192.0.2.2/28"); + + final String localAddr = "192.0.2.3/28"; + final String clientAddr = "192.0.2.2/28"; + mTetheringEventCallback = enableEthernetTethering(iface, + requestWithStaticIpv4(localAddr, clientAddr)); + + mTetheringEventCallback.awaitInterfaceTethered(); + assertInterfaceHasIpAddress(iface, localAddr); + + byte[] client1 = MacAddress.fromString("1:2:3:4:5:6").toByteArray(); + byte[] client2 = MacAddress.fromString("a:b:c:d:e:f").toByteArray(); + + FileDescriptor fd = mTestIface.getFileDescriptor().getFileDescriptor(); + mTapPacketReader = makePacketReader(fd, getMTU(mTestIface)); + DhcpResults dhcpResults = runDhcp(fd, client1); + assertEquals(new LinkAddress(clientAddr), dhcpResults.ipAddress); + + try { + runDhcp(fd, client2); + fail("Only one client should get an IP address"); + } catch (TimeoutException expected) { } + + } + + private static boolean isRouterAdvertisement(byte[] pkt) { + if (pkt == null) return false; + + ByteBuffer buf = ByteBuffer.wrap(pkt); + + final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf); + if (ethHdr.etherType != ETHER_TYPE_IPV6) return false; + + final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf); + if (ipv6Hdr.nextHeader != (byte) IPPROTO_ICMPV6) return false; + + final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf); + return icmpv6Hdr.type == (short) ICMPV6_ROUTER_ADVERTISEMENT; + } + + private static void expectRouterAdvertisement(TapPacketReader reader, String iface, + long timeoutMs) { + final long deadline = SystemClock.uptimeMillis() + timeoutMs; + do { + byte[] pkt = reader.popPacket(timeoutMs); + if (isRouterAdvertisement(pkt)) return; + timeoutMs = deadline - SystemClock.uptimeMillis(); + } while (timeoutMs > 0); + fail("Did not receive router advertisement on " + iface + " after " + + timeoutMs + "ms idle"); + } + + private static void expectLocalOnlyAddresses(String iface) throws Exception { + final List interfaceAddresses = + NetworkInterface.getByName(iface).getInterfaceAddresses(); + + boolean foundIpv6Ula = false; + for (InterfaceAddress ia : interfaceAddresses) { + final InetAddress addr = ia.getAddress(); + if (isIPv6ULA(addr)) { + foundIpv6Ula = true; + } + final int prefixlen = ia.getNetworkPrefixLength(); + final LinkAddress la = new LinkAddress(addr, prefixlen); + if (la.isIpv6() && la.isGlobalPreferred()) { + fail("Found global IPv6 address on local-only interface: " + interfaceAddresses); + } + } + + assertTrue("Did not find IPv6 ULA on local-only interface " + iface, + foundIpv6Ula); + } + + @Test + public void testLocalOnlyTethering() throws Exception { + assumeFalse(mEm.isAvailable()); + + mEm.setIncludeTestInterfaces(true); + + mTestIface = createTestInterface(); + + final String iface = mTetheredInterfaceRequester.getInterface(); + assertEquals("TetheredInterfaceCallback for unexpected interface", + mTestIface.getInterfaceName(), iface); + + final TetheringRequest request = new TetheringRequest.Builder(TETHERING_ETHERNET) + .setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build(); + mTetheringEventCallback = enableEthernetTethering(iface, request); + mTetheringEventCallback.awaitInterfaceLocalOnly(); + + // makePacketReader only works after tethering is started, because until then the interface + // does not have an IP address, and unprivileged apps cannot see interfaces without IP + // addresses. This shouldn't be flaky because the TAP interface will buffer all packets even + // before the reader is started. + FileDescriptor fd = mTestIface.getFileDescriptor().getFileDescriptor(); + mTapPacketReader = makePacketReader(fd, getMTU(mTestIface)); + + expectRouterAdvertisement(mTapPacketReader, iface, 2000 /* timeoutMs */); + expectLocalOnlyAddresses(iface); + } + + private boolean isAdbOverNetwork() { + // If adb TCP port opened, this test may running by adb over network. + return (SystemProperties.getInt("persist.adb.tcp.port", -1) > -1) + || (SystemProperties.getInt("service.adb.tcp.port", -1) > -1); + } + + @Test + public void testPhysicalEthernet() throws Exception { + assumeTrue(mEm.isAvailable()); + // Do not run this test if adb is over network and ethernet is connected. + // It is likely the adb run over ethernet, the adb would break when ethernet is switching + // from client mode to server mode. See b/160389275. + assumeFalse(isAdbOverNetwork()); + + // Get an interface to use. + final String iface = mTetheredInterfaceRequester.getInterface(); + + // Enable Ethernet tethering and check that it starts. + mTetheringEventCallback = enableEthernetTethering(iface); + + // There is nothing more we can do on a physical interface without connecting an actual + // client, which is not possible in this test. + } + + private static final class MyTetheringEventCallback implements TetheringEventCallback { + private final TetheringManager mTm; + private final CountDownLatch mTetheringStartedLatch = new CountDownLatch(1); + private final CountDownLatch mTetheringStoppedLatch = new CountDownLatch(1); + private final CountDownLatch mLocalOnlyStartedLatch = new CountDownLatch(1); + private final CountDownLatch mLocalOnlyStoppedLatch = new CountDownLatch(1); + private final CountDownLatch mClientConnectedLatch = new CountDownLatch(1); + private final TetheringInterface mIface; + + private volatile boolean mInterfaceWasTethered = false; + private volatile boolean mInterfaceWasLocalOnly = false; + private volatile boolean mUnregistered = false; + private volatile Collection mClients = null; + + MyTetheringEventCallback(TetheringManager tm, String iface) { + mTm = tm; + mIface = new TetheringInterface(TETHERING_ETHERNET, iface); + } + + public void unregister() { + mTm.unregisterTetheringEventCallback(this); + mUnregistered = true; + } + @Override + public void onTetheredInterfacesChanged(List interfaces) { + fail("Should only call callback that takes a Set"); + } + + @Override + public void onTetheredInterfacesChanged(Set interfaces) { + // Ignore stale callbacks registered by previous test cases. + if (mUnregistered) return; + + if (!mInterfaceWasTethered && interfaces.contains(mIface)) { + // This interface is being tethered for the first time. + Log.d(TAG, "Tethering started: " + interfaces); + mInterfaceWasTethered = true; + mTetheringStartedLatch.countDown(); + } else if (mInterfaceWasTethered && !interfaces.contains(mIface)) { + Log.d(TAG, "Tethering stopped: " + interfaces); + mTetheringStoppedLatch.countDown(); + } + } + + @Override + public void onLocalOnlyInterfacesChanged(List interfaces) { + fail("Should only call callback that takes a Set"); + } + + @Override + public void onLocalOnlyInterfacesChanged(Set interfaces) { + // Ignore stale callbacks registered by previous test cases. + if (mUnregistered) return; + + if (!mInterfaceWasLocalOnly && interfaces.contains(mIface)) { + // This interface is being put into local-only mode for the first time. + Log.d(TAG, "Local-only started: " + interfaces); + mInterfaceWasLocalOnly = true; + mLocalOnlyStartedLatch.countDown(); + } else if (mInterfaceWasLocalOnly && !interfaces.contains(mIface)) { + Log.d(TAG, "Local-only stopped: " + interfaces); + mLocalOnlyStoppedLatch.countDown(); + } + } + + public void awaitInterfaceTethered() throws Exception { + assertTrue("Ethernet not tethered after " + TIMEOUT_MS + "ms", + mTetheringStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } + + public void awaitInterfaceLocalOnly() throws Exception { + assertTrue("Ethernet not local-only after " + TIMEOUT_MS + "ms", + mLocalOnlyStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } + + public void awaitInterfaceUntethered() throws Exception { + // Don't block teardown if the interface was never tethered. + // This is racy because the interface might become tethered right after this check, but + // that can only happen in tearDown if startTethering timed out, which likely means + // the test has already failed. + if (!mInterfaceWasTethered && !mInterfaceWasLocalOnly) return; + + if (mInterfaceWasTethered) { + assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms", + mTetheringStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } else if (mInterfaceWasLocalOnly) { + assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms", + mLocalOnlyStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } else { + fail(mIface + " cannot be both tethered and local-only. Update this test class."); + } + } + + @Override + public void onError(String ifName, int error) { + // Ignore stale callbacks registered by previous test cases. + if (mUnregistered) return; + + fail("TetheringEventCallback got error:" + error + " on iface " + ifName); + } + + @Override + public void onClientsChanged(Collection clients) { + // Ignore stale callbacks registered by previous test cases. + if (mUnregistered) return; + + Log.d(TAG, "Got clients changed: " + clients); + mClients = clients; + if (clients.size() > 0) { + mClientConnectedLatch.countDown(); + } + } + + public Collection awaitClientConnected() throws Exception { + assertTrue("Did not receive client connected callback after " + TIMEOUT_MS + "ms", + mClientConnectedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + return mClients; + } + } + + private MyTetheringEventCallback enableEthernetTethering(String iface, + TetheringRequest request) throws Exception { + MyTetheringEventCallback callback = new MyTetheringEventCallback(mTm, iface); + mTm.registerTetheringEventCallback(mHandler::post, callback); + + StartTetheringCallback startTetheringCallback = new StartTetheringCallback() { + @Override + public void onTetheringFailed(int resultCode) { + fail("Unexpectedly got onTetheringFailed"); + } + }; + Log.d(TAG, "Starting Ethernet tethering"); + mTm.startTethering(request, mHandler::post /* executor */, startTetheringCallback); + + final int connectivityType = request.getConnectivityScope(); + switch (connectivityType) { + case CONNECTIVITY_SCOPE_GLOBAL: + callback.awaitInterfaceTethered(); + break; + case CONNECTIVITY_SCOPE_LOCAL: + callback.awaitInterfaceLocalOnly(); + break; + default: + fail("Unexpected connectivity type requested: " + connectivityType); + } + + return callback; + } + + private MyTetheringEventCallback enableEthernetTethering(String iface) throws Exception { + return enableEthernetTethering(iface, + new TetheringRequest.Builder(TETHERING_ETHERNET) + .setShouldShowEntitlementUi(false).build()); + } + + private int getMTU(TestNetworkInterface iface) throws SocketException { + NetworkInterface nif = NetworkInterface.getByName(iface.getInterfaceName()); + assertNotNull("Can't get NetworkInterface object for " + iface.getInterfaceName(), nif); + return nif.getMTU(); + } + + private TapPacketReader makePacketReader(FileDescriptor fd, int mtu) { + final TapPacketReader reader = new TapPacketReader(mHandler, fd, mtu); + mHandler.post(() -> reader.start()); + HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS); + return reader; + } + + private void checkVirtualEthernet(TestNetworkInterface iface, int mtu) throws Exception { + FileDescriptor fd = iface.getFileDescriptor().getFileDescriptor(); + mTapPacketReader = makePacketReader(fd, mtu); + mTetheringEventCallback = enableEthernetTethering(iface.getInterfaceName()); + checkTetheredClientCallbacks(fd); + } + + private DhcpResults runDhcp(FileDescriptor fd, byte[] clientMacAddr) throws Exception { + // We have to retransmit DHCP requests because IpServer declares itself to be ready before + // its DhcpServer is actually started. TODO: fix this race and remove this loop. + DhcpPacket offerPacket = null; + for (int i = 0; i < DHCP_DISCOVER_ATTEMPTS; i++) { + Log.d(TAG, "Sending DHCP discover"); + sendDhcpDiscover(fd, clientMacAddr); + offerPacket = getNextDhcpPacket(); + if (offerPacket instanceof DhcpOfferPacket) break; + } + if (!(offerPacket instanceof DhcpOfferPacket)) { + throw new TimeoutException("No DHCPOFFER received on interface within timeout"); + } + + sendDhcpRequest(fd, offerPacket, clientMacAddr); + DhcpPacket ackPacket = getNextDhcpPacket(); + if (!(ackPacket instanceof DhcpAckPacket)) { + throw new TimeoutException("No DHCPACK received on interface within timeout"); + } + + return ackPacket.toDhcpResults(); + } + + private void checkTetheredClientCallbacks(FileDescriptor fd) throws Exception { + // Create a fake client. + byte[] clientMacAddr = new byte[6]; + new Random().nextBytes(clientMacAddr); + + DhcpResults dhcpResults = runDhcp(fd, clientMacAddr); + + final Collection clients = mTetheringEventCallback.awaitClientConnected(); + assertEquals(1, clients.size()); + final TetheredClient client = clients.iterator().next(); + + // Check the MAC address. + assertEquals(MacAddress.fromBytes(clientMacAddr), client.getMacAddress()); + assertEquals(TETHERING_ETHERNET, client.getTetheringType()); + + // Check the hostname. + assertEquals(1, client.getAddresses().size()); + TetheredClient.AddressInfo info = client.getAddresses().get(0); + assertEquals(DHCP_HOSTNAME, info.getHostname()); + + // Check the address is the one that was handed out in the DHCP ACK. + assertLinkAddressMatches(dhcpResults.ipAddress, info.getAddress()); + + // Check that the lifetime is correct +/- 10s. + final long now = SystemClock.elapsedRealtime(); + final long actualLeaseDuration = (info.getAddress().getExpirationTime() - now) / 1000; + final String msg = String.format("IP address should have lifetime of %d, got %d", + dhcpResults.leaseDuration, actualLeaseDuration); + assertTrue(msg, Math.abs(dhcpResults.leaseDuration - actualLeaseDuration) < 10); + } + + private DhcpPacket getNextDhcpPacket() throws ParseException { + byte[] packet; + while ((packet = mTapPacketReader.popPacket(PACKET_READ_TIMEOUT_MS)) != null) { + try { + return DhcpPacket.decodeFullPacket(packet, packet.length, DhcpPacket.ENCAP_L2); + } catch (DhcpPacket.ParseException e) { + // Not a DHCP packet. Continue. + } + } + return null; + } + + private static final class TetheredInterfaceRequester implements TetheredInterfaceCallback { + private final Handler mHandler; + private final EthernetManager mEm; + + private TetheredInterfaceRequest mRequest; + private final CompletableFuture mFuture = new CompletableFuture<>(); + + TetheredInterfaceRequester(Handler handler, EthernetManager em) { + mHandler = handler; + mEm = em; + } + + @Override + public void onAvailable(String iface) { + Log.d(TAG, "Ethernet interface available: " + iface); + mFuture.complete(iface); + } + + @Override + public void onUnavailable() { + mFuture.completeExceptionally(new IllegalStateException("onUnavailable received")); + } + + public CompletableFuture requestInterface() { + assertNull("BUG: more than one tethered interface request", mRequest); + Log.d(TAG, "Requesting tethered interface"); + mRequest = mEm.requestTetheredInterface(mHandler::post, this); + return mFuture; + } + + public String getInterface() throws Exception { + return requestInterface().get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + + public void release() { + if (mRequest != null) { + mFuture.obtrudeException(new IllegalStateException("Request already released")); + mRequest.release(); + mRequest = null; + } + } + } + + private void sendDhcpDiscover(FileDescriptor fd, byte[] macAddress) throws Exception { + ByteBuffer packet = DhcpPacket.buildDiscoverPacket(DhcpPacket.ENCAP_L2, + new Random().nextInt() /* transactionId */, (short) 0 /* secs */, + macAddress, false /* unicast */, DHCP_REQUESTED_PARAMS, + false /* rapid commit */, DHCP_HOSTNAME); + sendPacket(fd, packet); + } + + private void sendDhcpRequest(FileDescriptor fd, DhcpPacket offerPacket, byte[] macAddress) + throws Exception { + DhcpResults results = offerPacket.toDhcpResults(); + Inet4Address clientIp = (Inet4Address) results.ipAddress.getAddress(); + Inet4Address serverIdentifier = results.serverAddress; + ByteBuffer packet = DhcpPacket.buildRequestPacket(DhcpPacket.ENCAP_L2, + 0 /* transactionId */, (short) 0 /* secs */, DhcpPacket.INADDR_ANY /* clientIp */, + false /* broadcast */, macAddress, clientIp /* requestedIpAddress */, + serverIdentifier, DHCP_REQUESTED_PARAMS, DHCP_HOSTNAME); + sendPacket(fd, packet); + } + + private void sendPacket(FileDescriptor fd, ByteBuffer packet) throws Exception { + assertNotNull("Only tests on virtual interfaces can send packets", fd); + Os.write(fd, packet); + } + + public void assertLinkAddressMatches(LinkAddress l1, LinkAddress l2) { + // Check all fields except the deprecation and expiry times. + String msg = String.format("LinkAddresses do not match. expected: %s actual: %s", l1, l2); + assertTrue(msg, l1.isSameAddressAs(l2)); + assertEquals("LinkAddress flags do not match", l1.getFlags(), l2.getFlags()); + assertEquals("LinkAddress scope does not match", l1.getScope(), l2.getScope()); + } + + private TetheringRequest requestWithStaticIpv4(String local, String client) { + LinkAddress localAddr = local == null ? null : new LinkAddress(local); + LinkAddress clientAddr = client == null ? null : new LinkAddress(client); + return new TetheringRequest.Builder(TETHERING_ETHERNET) + .setStaticIpv4Addresses(localAddr, clientAddr) + .setShouldShowEntitlementUi(false).build(); + } + + private void assertInvalidStaticIpv4Request(String iface, String local, String client) + throws Exception { + try { + enableEthernetTethering(iface, requestWithStaticIpv4(local, client)); + fail("Unexpectedly accepted invalid IPv4 configuration: " + local + ", " + client); + } catch (IllegalArgumentException | NullPointerException expected) { } + } + + private void assertInterfaceHasIpAddress(String iface, String expected) throws Exception { + LinkAddress expectedAddr = new LinkAddress(expected); + NetworkInterface nif = NetworkInterface.getByName(iface); + for (InterfaceAddress ia : nif.getInterfaceAddresses()) { + final LinkAddress addr = new LinkAddress(ia.getAddress(), ia.getNetworkPrefixLength()); + if (expectedAddr.equals(addr)) { + return; + } + } + fail("Expected " + iface + " to have IP address " + expected + ", found " + + nif.getInterfaceAddresses()); + } + + private TestNetworkInterface createTestInterface() throws Exception { + TestNetworkManager tnm = mContext.getSystemService(TestNetworkManager.class); + TestNetworkInterface iface = tnm.createTapInterface(); + Log.d(TAG, "Created test interface " + iface.getInterfaceName()); + return iface; + } + + private void maybeDeleteTestInterface() throws Exception { + if (mTestIface != null) { + mTestIface.getFileDescriptor().close(); + Log.d(TAG, "Deleted test interface " + mTestIface.getInterfaceName()); + mTestIface = null; + } + } +} diff --git a/Tethering/tests/jarjar-rules.txt b/Tethering/tests/jarjar-rules.txt new file mode 100644 index 0000000000..9cb143e5aa --- /dev/null +++ b/Tethering/tests/jarjar-rules.txt @@ -0,0 +1,19 @@ +# Don't jar-jar the entire package because this test use some +# internal classes (like ArrayUtils in com.android.internal.util) +rule com.android.internal.util.BitUtils* com.android.networkstack.tethering.util.BitUtils@1 +rule com.android.internal.util.IndentingPrintWriter* com.android.networkstack.tethering.util.IndentingPrintWriter@1 +rule com.android.internal.util.IState* com.android.networkstack.tethering.util.IState@1 +rule com.android.internal.util.MessageUtils* com.android.networkstack.tethering.util.MessageUtils@1 +rule com.android.internal.util.State* com.android.networkstack.tethering.util.State@1 +rule com.android.internal.util.StateMachine* com.android.networkstack.tethering.util.StateMachine@1 +rule com.android.internal.util.TrafficStatsConstants* com.android.networkstack.tethering.util.TrafficStatsConstants@1 + +rule android.util.LocalLog* com.android.networkstack.tethering.util.LocalLog@1 + +# Classes from net-utils-framework-common +rule com.android.net.module.util.** com.android.networkstack.tethering.util.@1 + +# TODO: either stop using frameworks-base-testutils or remove the unit test classes it contains. +# TestableLooper from "testables" can be used instead of TestLooper from frameworks-base-testutils. +zap android.os.test.TestLooperTest* +zap com.android.test.filters.SelectTestTests* diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp new file mode 100644 index 0000000000..e51d531b2c --- /dev/null +++ b/Tethering/tests/mts/Android.bp @@ -0,0 +1,65 @@ +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test { + // This tests for functionality that is not required for devices that + // don't use Tethering mainline module. + name: "MtsTetheringTestLatestSdk", + + min_sdk_version: "30", + target_sdk_version: "30", + + libs: [ + "android.test.base", + ], + + srcs: [ + "src/**/*.java", + ], + + static_libs: [ + "androidx.test.rules", + // mockito-target-extended-minus-junit4 used in this lib have dependency with + // jni_libs libdexmakerjvmtiagent and libstaticjvmtiagent. + "cts-net-utils", + // This is needed for androidx.test.runner.AndroidJUnitRunner. + "ctstestrunner-axt", + "junit", + "junit-params", + ], + + jni_libs: [ + // For mockito extended which is pulled in from -net-utils -> net-tests-utils + // (mockito-target-extended-minus-junit4). + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + + defaults: ["framework-connectivity-test-defaults"], + + platform_apis: true, + + // Tag this module as a mts test artifact + test_suites: [ + "general-tests", + "mts-tethering", + ], + + // Include both the 32 and 64 bit versions + compile_multilib: "both", +} diff --git a/Tethering/tests/mts/AndroidManifest.xml b/Tethering/tests/mts/AndroidManifest.xml new file mode 100644 index 0000000000..6d2abcad42 --- /dev/null +++ b/Tethering/tests/mts/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + diff --git a/Tethering/tests/mts/AndroidTest.xml b/Tethering/tests/mts/AndroidTest.xml new file mode 100644 index 0000000000..4edd544acf --- /dev/null +++ b/Tethering/tests/mts/AndroidTest.xml @@ -0,0 +1,36 @@ + + + + diff --git a/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java b/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java new file mode 100644 index 0000000000..e0fcbfad28 --- /dev/null +++ b/Tethering/tests/mts/src/android/tethering/mts/TetheringModuleTest.java @@ -0,0 +1,185 @@ +/* + * 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.tethering.mts; + +import static android.Manifest.permission.MANAGE_TEST_NETWORKS; +import static android.Manifest.permission.NETWORK_SETTINGS; +import static android.Manifest.permission.READ_DEVICE_CONFIG; +import static android.Manifest.permission.TETHER_PRIVILEGED; +import static android.Manifest.permission.WRITE_SETTINGS; +import static android.net.TetheringManager.TETHERING_WIFI; +import static android.net.cts.util.CtsTetheringUtils.isWifiTetheringSupported; +import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; + +import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import android.app.UiAutomation; +import android.content.Context; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.TetheringInterface; +import android.net.TetheringManager; +import android.net.cts.util.CtsTetheringUtils; +import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback; +import android.provider.DeviceConfig; + +import androidx.annotation.NonNull; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.TestNetworkTracker; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class TetheringModuleTest { + private Context mContext; + private TetheringManager mTm; + private CtsTetheringUtils mCtsTetheringUtils; + + private UiAutomation mUiAutomation = + InstrumentationRegistry.getInstrumentation().getUiAutomation(); + + @Before + public void setUp() throws Exception { + mUiAutomation.adoptShellPermissionIdentity(MANAGE_TEST_NETWORKS, NETWORK_SETTINGS, + WRITE_SETTINGS, READ_DEVICE_CONFIG, TETHER_PRIVILEGED); + mContext = InstrumentationRegistry.getContext(); + mTm = mContext.getSystemService(TetheringManager.class); + mCtsTetheringUtils = new CtsTetheringUtils(mContext); + } + + @After + public void tearDown() throws Exception { + mUiAutomation.dropShellPermissionIdentity(); + } + + private static final String TETHER_ENABLE_SELECT_ALL_PREFIX_RANGES = + "tether_enable_select_all_prefix_ranges"; + @Test + public void testSwitchBasePrefixRangeWhenConflict() throws Exception { + assumeTrue(isFeatureEnabled(TETHER_ENABLE_SELECT_ALL_PREFIX_RANGES, true)); + + addressConflictTest(true); + } + + @Test + public void testSwitchPrefixRangeWhenConflict() throws Exception { + addressConflictTest(false); + } + + private void addressConflictTest(final boolean wholeRangeConflict) throws Exception { + final TestTetheringEventCallback tetherEventCallback = + mCtsTetheringUtils.registerTetheringEventCallback(); + + TestNetworkTracker tnt = null; + try { + tetherEventCallback.assumeTetheringSupported(); + assumeTrue(isWifiTetheringSupported(tetherEventCallback)); + tetherEventCallback.expectNoTetheringActive(); + + final TetheringInterface tetheredIface = + mCtsTetheringUtils.startWifiTethering(tetherEventCallback); + + assertNotNull(tetheredIface); + final String wifiTetheringIface = tetheredIface.getInterface(); + + NetworkInterface nif = NetworkInterface.getByName(wifiTetheringIface); + // Tethering downstream only have one ipv4 address. + final LinkAddress hotspotAddr = getFirstIpv4Address(nif); + assertNotNull(hotspotAddr); + + final IpPrefix testPrefix = getConflictingPrefix(hotspotAddr, wholeRangeConflict); + assertNotNull(testPrefix); + + tnt = setUpTestNetwork( + new LinkAddress(testPrefix.getAddress(), testPrefix.getPrefixLength())); + + tetherEventCallback.expectNoTetheringActive(); + final List wifiRegexs = + tetherEventCallback.getTetheringInterfaceRegexps().getTetherableWifiRegexs(); + + tetherEventCallback.expectTetheredInterfacesChanged(wifiRegexs, TETHERING_WIFI); + nif = NetworkInterface.getByName(wifiTetheringIface); + final LinkAddress newHotspotAddr = getFirstIpv4Address(nif); + assertNotNull(newHotspotAddr); + + assertFalse(testPrefix.containsPrefix( + new IpPrefix(newHotspotAddr.getAddress(), newHotspotAddr.getPrefixLength()))); + + mCtsTetheringUtils.stopWifiTethering(tetherEventCallback); + } finally { + if (tnt != null) { + tnt.teardown(); + } + mTm.stopAllTethering(); + mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback); + } + } + + private LinkAddress getFirstIpv4Address(final NetworkInterface nif) { + for (InterfaceAddress ia : nif.getInterfaceAddresses()) { + final LinkAddress addr = new LinkAddress(ia.getAddress(), ia.getNetworkPrefixLength()); + if (addr.isIpv4()) return addr; + } + return null; + } + + @NonNull + private IpPrefix getConflictingPrefix(final LinkAddress address, + final boolean wholeRangeConflict) { + if (!wholeRangeConflict) { + return new IpPrefix(address.getAddress(), address.getPrefixLength()); + } + + final ArrayList prefixPool = new ArrayList<>(Arrays.asList( + new IpPrefix("192.168.0.0/16"), + new IpPrefix("172.16.0.0/12"), + new IpPrefix("10.0.0.0/8"))); + + for (IpPrefix prefix : prefixPool) { + if (prefix.contains(address.getAddress())) return prefix; + } + + fail("Could not find sutiable conflict prefix"); + + // Never go here. + return null; + } + + private TestNetworkTracker setUpTestNetwork(final LinkAddress address) throws Exception { + return initTestNetwork(mContext, address, 10_000L /* test timeout ms*/); + + } + + public static boolean isFeatureEnabled(final String name, final boolean defaultValue) { + return DeviceConfig.getBoolean(NAMESPACE_CONNECTIVITY, name, defaultValue); + } +} diff --git a/Tethering/tests/privileged/Android.bp b/Tethering/tests/privileged/Android.bp new file mode 100644 index 0000000000..75fdd6ef23 --- /dev/null +++ b/Tethering/tests/privileged/Android.bp @@ -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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_defaults { + name: "TetheringPrivilegedTestsJniDefaults", + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + "libtetherutilsjni", + ], + jni_uses_sdk_apis: true, + visibility: ["//visibility:private"], +} + +android_test { + name: "TetheringPrivilegedTests", + defaults: [ + "TetheringPrivilegedTestsJniDefaults", + ], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + certificate: "networkstack", + platform_apis: true, + test_suites: [ + "device-tests", + "mts", + ], + static_libs: [ + "androidx.test.rules", + "net-tests-utils", + "TetheringApiCurrentLib", + ], + compile_multilib: "both", +} diff --git a/Tethering/tests/privileged/AndroidManifest.xml b/Tethering/tests/privileged/AndroidManifest.xml new file mode 100644 index 0000000000..49eba15d13 --- /dev/null +++ b/Tethering/tests/privileged/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + diff --git a/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java b/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java new file mode 100644 index 0000000000..a933e1b277 --- /dev/null +++ b/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java @@ -0,0 +1,323 @@ +/* + * 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.ip; + +import static android.system.OsConstants.IPPROTO_ICMPV6; + +import static com.android.net.module.util.IpUtils.icmpv6Checksum; +import static com.android.net.module.util.NetworkStackConstants.ETHER_SRC_ADDR_OFFSET; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.app.Instrumentation; +import android.content.Context; +import android.net.INetd; +import android.net.InetAddresses; +import android.net.MacAddress; +import android.net.util.InterfaceParams; +import android.net.util.TetheringUtils; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.TapPacketReader; +import com.android.testutils.TapPacketReaderRule; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; +import java.nio.ByteBuffer; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DadProxyTest { + private static final int DATA_BUFFER_LEN = 4096; + private static final int PACKET_TIMEOUT_MS = 2_000; // Long enough for DAD to succeed. + + // Start the readers manually on a common handler shared with DadProxy, for simplicity + @Rule + public final TapPacketReaderRule mUpstreamReader = new TapPacketReaderRule( + DATA_BUFFER_LEN, false /* autoStart */); + @Rule + public final TapPacketReaderRule mTetheredReader = new TapPacketReaderRule( + DATA_BUFFER_LEN, false /* autoStart */); + + private InterfaceParams mUpstreamParams, mTetheredParams; + private HandlerThread mHandlerThread; + private Handler mHandler; + private TapPacketReader mUpstreamPacketReader, mTetheredPacketReader; + + private static INetd sNetd; + + @BeforeClass + public static void setupOnce() { + System.loadLibrary("tetherutilsjni"); + + final Instrumentation inst = InstrumentationRegistry.getInstrumentation(); + final IBinder netdIBinder = + (IBinder) inst.getContext().getSystemService(Context.NETD_SERVICE); + sNetd = INetd.Stub.asInterface(netdIBinder); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mHandlerThread = new HandlerThread(getClass().getSimpleName()); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + + setupTapInterfaces(); + + // Looper must be prepared here since AndroidJUnitRunner runs tests on separate threads. + if (Looper.myLooper() == null) Looper.prepare(); + + DadProxy mProxy = setupProxy(); + } + + @After + public void tearDown() throws Exception { + mUpstreamReader.stop(); + mTetheredReader.stop(); + + if (mHandlerThread != null) { + mHandlerThread.quitSafely(); + mHandlerThread.join(PACKET_TIMEOUT_MS); + } + + if (mTetheredParams != null) { + sNetd.networkRemoveInterface(INetd.LOCAL_NET_ID, mTetheredParams.name); + } + if (mUpstreamParams != null) { + sNetd.networkRemoveInterface(INetd.LOCAL_NET_ID, mUpstreamParams.name); + } + } + + private void setupTapInterfaces() throws Exception { + // Create upstream test iface. + mUpstreamReader.start(mHandler); + final String upstreamIface = mUpstreamReader.iface.getInterfaceName(); + mUpstreamParams = InterfaceParams.getByName(upstreamIface); + assertNotNull(mUpstreamParams); + mUpstreamPacketReader = mUpstreamReader.getReader(); + + // Create tethered test iface. + mTetheredReader.start(mHandler); + final String tetheredIface = mTetheredReader.getIface().getInterfaceName(); + mTetheredParams = InterfaceParams.getByName(tetheredIface); + assertNotNull(mTetheredParams); + mTetheredPacketReader = mTetheredReader.getReader(); + } + + private static final int IPV6_HEADER_LEN = 40; + private static final int ETH_HEADER_LEN = 14; + private static final int ICMPV6_NA_NS_LEN = 24; + private static final int LL_TARGET_OPTION_LEN = 8; + private static final int ICMPV6_CHECKSUM_OFFSET = 2; + private static final int ETHER_TYPE_IPV6 = 0x86dd; + + private static ByteBuffer createDadPacket(int type) { + // Refer to buildArpPacket() + int icmpLen = ICMPV6_NA_NS_LEN + + (type == NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT + ? LL_TARGET_OPTION_LEN : 0); + final ByteBuffer buf = ByteBuffer.allocate(icmpLen + IPV6_HEADER_LEN + ETH_HEADER_LEN); + + // Ethernet header. + final MacAddress srcMac = MacAddress.fromString("33:33:ff:66:77:88"); + buf.put(srcMac.toByteArray()); + final MacAddress dstMac = MacAddress.fromString("01:02:03:04:05:06"); + buf.put(dstMac.toByteArray()); + buf.putShort((short) ETHER_TYPE_IPV6); + + // IPv6 header + byte[] version = {(byte) 0x60, 0x00, 0x00, 0x00}; + buf.put(version); // Version + buf.putShort((byte) icmpLen); // Length + buf.put((byte) IPPROTO_ICMPV6); // Next header + buf.put((byte) 0xff); // Hop limit + + final byte[] target = + InetAddresses.parseNumericAddress("fe80::1122:3344:5566:7788").getAddress(); + final byte[] src; + final byte[] dst; + if (type == NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION) { + src = InetAddresses.parseNumericAddress("::").getAddress(); + dst = InetAddresses.parseNumericAddress("ff02::1:ff66:7788").getAddress(); + } else { + src = target; + dst = TetheringUtils.ALL_NODES; + } + buf.put(src); + buf.put(dst); + + // ICMPv6 Header + buf.put((byte) type); // Type + buf.put((byte) 0x00); // Code + buf.putShort((short) 0); // Checksum + buf.putInt(0); // Reserved + buf.put(target); + + if (type == NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT) { + //NA packet has LL target address + //ICMPv6 Option + buf.put((byte) 0x02); // Type + buf.put((byte) 0x01); // Length + byte[] ll_target = MacAddress.fromString("01:02:03:04:05:06").toByteArray(); + buf.put(ll_target); + } + + // Populate checksum field + final int transportOffset = ETH_HEADER_LEN + IPV6_HEADER_LEN; + final short checksum = icmpv6Checksum(buf, ETH_HEADER_LEN, transportOffset, icmpLen); + buf.putShort(transportOffset + ICMPV6_CHECKSUM_OFFSET, checksum); + + buf.flip(); + return buf; + } + + private DadProxy setupProxy() throws Exception { + DadProxy proxy = new DadProxy(mHandler, mTetheredParams); + mHandler.post(() -> proxy.setUpstreamIface(mUpstreamParams)); + + // Upstream iface is added to local network to simplify test case. + // Otherwise the test needs to create and destroy a network for the upstream iface. + sNetd.networkAddInterface(INetd.LOCAL_NET_ID, mUpstreamParams.name); + sNetd.networkAddInterface(INetd.LOCAL_NET_ID, mTetheredParams.name); + + return proxy; + } + + // TODO: change to assert. + private boolean waitForPacket(ByteBuffer packet, TapPacketReader reader) { + byte[] p; + + while ((p = reader.popPacket(PACKET_TIMEOUT_MS)) != null) { + final ByteBuffer buffer = ByteBuffer.wrap(p); + + if (buffer.compareTo(packet) == 0) return true; + } + return false; + } + + private ByteBuffer copy(ByteBuffer buf) { + // There does not seem to be a way to copy ByteBuffers. ByteBuffer does not implement + // clone() and duplicate() copies the metadata but shares the contents. + return ByteBuffer.wrap(buf.array().clone()); + } + + private void updateDstMac(ByteBuffer buf, MacAddress mac) { + buf.put(mac.toByteArray()); + buf.rewind(); + } + private void updateSrcMac(ByteBuffer buf, InterfaceParams ifaceParams) { + buf.position(ETHER_SRC_ADDR_OFFSET); + buf.put(ifaceParams.macAddr.toByteArray()); + buf.rewind(); + } + + private void receivePacketAndMaybeExpectForwarded(boolean expectForwarded, + ByteBuffer in, TapPacketReader inReader, ByteBuffer out, TapPacketReader outReader) + throws IOException { + + inReader.sendResponse(in); + if (waitForPacket(out, outReader)) return; + + // When the test runs, DAD may be in progress, because the interface has just been created. + // If so, the DAD proxy will get EADDRNOTAVAIL when trying to send packets. It is not + // possible to work around this using IPV6_FREEBIND or IPV6_TRANSPARENT options because the + // kernel rawv6 code doesn't consider those options either when binding or when sending, and + // doesn't get the source address from the packet even in IPPROTO_RAW/HDRINCL mode (it only + // gets it from the socket or from cmsg). + // + // If DAD was in progress when the above was attempted, try again and expect the packet to + // be forwarded. Don't disable DAD in the test because if we did, the test would not notice + // if, for example, the DAD proxy code just crashed if it received EADDRNOTAVAIL. + final String msg = expectForwarded + ? "Did not receive expected packet even after waiting for DAD:" + : "Unexpectedly received packet:"; + + inReader.sendResponse(in); + assertEquals(msg, expectForwarded, waitForPacket(out, outReader)); + } + + private void receivePacketAndExpectForwarded(ByteBuffer in, TapPacketReader inReader, + ByteBuffer out, TapPacketReader outReader) throws IOException { + receivePacketAndMaybeExpectForwarded(true, in, inReader, out, outReader); + } + + private void receivePacketAndExpectNotForwarded(ByteBuffer in, TapPacketReader inReader, + ByteBuffer out, TapPacketReader outReader) throws IOException { + receivePacketAndMaybeExpectForwarded(false, in, inReader, out, outReader); + } + + @Test + public void testNaForwardingFromUpstreamToTether() throws Exception { + ByteBuffer na = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT); + + ByteBuffer out = copy(na); + updateDstMac(out, MacAddress.fromString("33:33:00:00:00:01")); + updateSrcMac(out, mTetheredParams); + + receivePacketAndExpectForwarded(na, mUpstreamPacketReader, out, mTetheredPacketReader); + } + + @Test + // TODO: remove test once DAD works in both directions. + public void testNaForwardingFromTetherToUpstream() throws Exception { + ByteBuffer na = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT); + + ByteBuffer out = copy(na); + updateDstMac(out, MacAddress.fromString("33:33:00:00:00:01")); + updateSrcMac(out, mTetheredParams); + + receivePacketAndExpectNotForwarded(na, mTetheredPacketReader, out, mUpstreamPacketReader); + } + + @Test + public void testNsForwardingFromTetherToUpstream() throws Exception { + ByteBuffer ns = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION); + + ByteBuffer out = copy(ns); + updateSrcMac(out, mUpstreamParams); + + receivePacketAndExpectForwarded(ns, mTetheredPacketReader, out, mUpstreamPacketReader); + } + + @Test + // TODO: remove test once DAD works in both directions. + public void testNsForwardingFromUpstreamToTether() throws Exception { + ByteBuffer ns = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION); + + ByteBuffer out = copy(ns); + updateSrcMac(ns, mUpstreamParams); + + receivePacketAndExpectNotForwarded(ns, mUpstreamPacketReader, out, mTetheredPacketReader); + } +} diff --git a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java new file mode 100644 index 0000000000..1d942146e1 --- /dev/null +++ b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java @@ -0,0 +1,344 @@ +/* + * 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.ip; + +import static android.net.RouteInfo.RTN_UNICAST; + +import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN; +import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_MTU; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_RDNSS; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_RA_HEADER_LEN; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT; +import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST; +import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN; +import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN; +import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS; +import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.app.Instrumentation; +import android.content.Context; +import android.net.INetd; +import android.net.IpPrefix; +import android.net.MacAddress; +import android.net.RouteInfo; +import android.net.ip.RouterAdvertisementDaemon.RaParams; +import android.net.shared.RouteUtils; +import android.net.util.InterfaceParams; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.net.module.util.Ipv6Utils; +import com.android.net.module.util.Struct; +import com.android.net.module.util.structs.EthernetHeader; +import com.android.net.module.util.structs.Icmpv6Header; +import com.android.net.module.util.structs.Ipv6Header; +import com.android.net.module.util.structs.LlaOption; +import com.android.net.module.util.structs.MtuOption; +import com.android.net.module.util.structs.PrefixInformationOption; +import com.android.net.module.util.structs.RaHeader; +import com.android.net.module.util.structs.RdnssOption; +import com.android.testutils.TapPacketReader; +import com.android.testutils.TapPacketReaderRule; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; + +import java.net.Inet6Address; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public final class RouterAdvertisementDaemonTest { + private static final String TAG = RouterAdvertisementDaemonTest.class.getSimpleName(); + private static final int DATA_BUFFER_LEN = 4096; + private static final int PACKET_TIMEOUT_MS = 5_000; + + @Rule + public final TapPacketReaderRule mTetheredReader = new TapPacketReaderRule( + DATA_BUFFER_LEN, false /* autoStart */); + + private InterfaceParams mTetheredParams; + private HandlerThread mHandlerThread; + private Handler mHandler; + private TapPacketReader mTetheredPacketReader; + private RouterAdvertisementDaemon mRaDaemon; + + private static INetd sNetd; + + @BeforeClass + public static void setupOnce() { + final Instrumentation inst = InstrumentationRegistry.getInstrumentation(); + final IBinder netdIBinder = + (IBinder) inst.getContext().getSystemService(Context.NETD_SERVICE); + sNetd = INetd.Stub.asInterface(netdIBinder); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mHandlerThread = new HandlerThread(getClass().getSimpleName()); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + + setupTapInterfaces(); + + // Looper must be prepared here since AndroidJUnitRunner runs tests on separate threads. + if (Looper.myLooper() == null) Looper.prepare(); + + mRaDaemon = new RouterAdvertisementDaemon(mTetheredParams); + sNetd.networkAddInterface(INetd.LOCAL_NET_ID, mTetheredParams.name); + } + + @After + public void tearDown() throws Exception { + mTetheredReader.stop(); + if (mHandlerThread != null) { + mHandlerThread.quitSafely(); + mHandlerThread.join(PACKET_TIMEOUT_MS); + } + + if (mTetheredParams != null) { + sNetd.networkRemoveInterface(INetd.LOCAL_NET_ID, mTetheredParams.name); + } + } + + private void setupTapInterfaces() { + // Create tethered test iface. + mTetheredReader.start(mHandler); + mTetheredParams = InterfaceParams.getByName(mTetheredReader.iface.getInterfaceName()); + assertNotNull(mTetheredParams); + mTetheredPacketReader = mTetheredReader.getReader(); + mHandler.post(mTetheredPacketReader::start); + } + + private class TestRaPacket { + final RaParams mNewParams, mOldParams; + + TestRaPacket(final RaParams oldParams, final RaParams newParams) { + mOldParams = oldParams; + mNewParams = newParams; + } + + public boolean isPacketMatched(final byte[] pkt, boolean multicast) throws Exception { + if (pkt.length < (ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_RA_HEADER_LEN)) { + return false; + } + final ByteBuffer buf = ByteBuffer.wrap(pkt); + + // Parse Ethernet header + final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf); + if (ethHdr.etherType != ETHER_TYPE_IPV6) return false; + + // Parse IPv6 header + final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf); + assertEquals((ipv6Hdr.vtf >> 28), 6 /* ip version*/); + + final int payLoadLength = pkt.length - ETHER_HEADER_LEN - IPV6_HEADER_LEN; + assertEquals(payLoadLength, ipv6Hdr.payloadLength); + + // Parse ICMPv6 header + final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf); + if (icmpv6Hdr.type != (short) ICMPV6_ROUTER_ADVERTISEMENT) return false; + + // Check whether IPv6 destination address is multicast or unicast + if (multicast) { + assertEquals(ipv6Hdr.dstIp, IPV6_ADDR_ALL_NODES_MULTICAST); + } else { + // The unicast IPv6 destination address in RA can be either link-local or global + // IPv6 address. This test only expects link-local address. + assertTrue(ipv6Hdr.dstIp.isLinkLocalAddress()); + } + + // Parse RA header + final RaHeader raHdr = Struct.parse(RaHeader.class, buf); + assertEquals(mNewParams.hopLimit, raHdr.hopLimit); + + while (buf.position() < pkt.length) { + final int currentPos = buf.position(); + final int type = Byte.toUnsignedInt(buf.get()); + final int length = Byte.toUnsignedInt(buf.get()); + switch (type) { + case ICMPV6_ND_OPTION_PIO: + // length is 4 because this test only expects one PIO included in the + // router advertisement packet. + assertEquals(4, length); + + final ByteBuffer pioBuf = ByteBuffer.wrap(buf.array(), currentPos, + Struct.getSize(PrefixInformationOption.class)); + final PrefixInformationOption pio = + Struct.parse(PrefixInformationOption.class, pioBuf); + assertEquals((byte) (PIO_FLAG_ON_LINK | PIO_FLAG_AUTONOMOUS), pio.flags); + + final InetAddress address = InetAddress.getByAddress(pio.prefix); + final IpPrefix prefix = new IpPrefix(address, pio.prefixLen); + if (mNewParams.prefixes.contains(prefix)) { + assertTrue(pio.validLifetime > 0); + assertTrue(pio.preferredLifetime > 0); + } else if (mOldParams != null && mOldParams.prefixes.contains(prefix)) { + assertEquals(0, pio.validLifetime); + assertEquals(0, pio.preferredLifetime); + } else { + fail("Unexpected prefix: " + prefix); + } + + // Move ByteBuffer position to the next option. + buf.position(currentPos + Struct.getSize(PrefixInformationOption.class)); + break; + case ICMPV6_ND_OPTION_MTU: + assertEquals(1, length); + + final ByteBuffer mtuBuf = ByteBuffer.wrap(buf.array(), currentPos, + Struct.getSize(MtuOption.class)); + final MtuOption mtu = Struct.parse(MtuOption.class, mtuBuf); + assertEquals(mNewParams.mtu, mtu.mtu); + + // Move ByteBuffer position to the next option. + buf.position(currentPos + Struct.getSize(MtuOption.class)); + break; + case ICMPV6_ND_OPTION_RDNSS: + final int rdnssHeaderLen = Struct.getSize(RdnssOption.class); + final ByteBuffer RdnssBuf = ByteBuffer.wrap(buf.array(), currentPos, + rdnssHeaderLen); + final RdnssOption rdnss = Struct.parse(RdnssOption.class, RdnssBuf); + final String msg = + rdnss.lifetime > 0 ? "Unknown dns" : "Unknown deprecated dns"; + final HashSet dnses = + rdnss.lifetime > 0 ? mNewParams.dnses : mOldParams.dnses; + assertNotNull(msg, dnses); + + // Check DNS servers included in this option. + buf.position(currentPos + rdnssHeaderLen); // skip the rdnss option header + final int numOfDnses = (length - 1) / 2; + for (int i = 0; i < numOfDnses; i++) { + byte[] rawAddress = new byte[IPV6_ADDR_LEN]; + buf.get(rawAddress); + final Inet6Address dns = + (Inet6Address) InetAddress.getByAddress(rawAddress); + if (!dnses.contains(dns)) fail("Unexpected dns: " + dns); + } + // Unnecessary to move ByteBuffer position here, since the position has been + // moved forward correctly after reading DNS servers from ByteBuffer. + break; + case ICMPV6_ND_OPTION_SLLA: + // Do nothing, just move ByteBuffer position to the next option. + buf.position(currentPos + Struct.getSize(LlaOption.class)); + break; + default: + fail("Unknown RA option type " + type); + } + } + return true; + } + } + + private RaParams createRaParams(final String ipv6Address) throws Exception { + final RaParams params = new RaParams(); + final Inet6Address address = (Inet6Address) InetAddress.getByName(ipv6Address); + params.dnses.add(address); + params.prefixes.add(new IpPrefix(address, 64)); + + return params; + } + + private boolean isRaPacket(final TestRaPacket testRa, boolean multicast) throws Exception { + byte[] packet; + while ((packet = mTetheredPacketReader.poll(PACKET_TIMEOUT_MS)) != null) { + if (testRa.isPacketMatched(packet, multicast)) { + return true; + } + } + return false; + } + + private void assertUnicastRaPacket(final TestRaPacket testRa) throws Exception { + assertTrue(isRaPacket(testRa, false /* multicast */)); + } + + private void assertMulticastRaPacket(final TestRaPacket testRa) throws Exception { + assertTrue(isRaPacket(testRa, true /* multicast */)); + } + + private ByteBuffer createRsPacket(final String srcIp) throws Exception { + final MacAddress dstMac = MacAddress.fromString("33:33:03:04:05:06"); + final MacAddress srcMac = mTetheredParams.macAddr; + final ByteBuffer slla = LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, srcMac); + + return Ipv6Utils.buildRsPacket(srcMac, dstMac, (Inet6Address) InetAddress.getByName(srcIp), + IPV6_ADDR_ALL_NODES_MULTICAST, slla); + } + + @Test + public void testUnSolicitRouterAdvertisement() throws Exception { + assertTrue(mRaDaemon.start()); + final RaParams params1 = createRaParams("2001:1122:3344::5566"); + mRaDaemon.buildNewRa(null, params1); + assertMulticastRaPacket(new TestRaPacket(null, params1)); + + final RaParams params2 = createRaParams("2006:3344:5566::7788"); + mRaDaemon.buildNewRa(params1, params2); + assertMulticastRaPacket(new TestRaPacket(params1, params2)); + } + + @Test + public void testSolicitRouterAdvertisement() throws Exception { + // Enable IPv6 forwarding is necessary, which makes kernel process RS correctly and + // create the neighbor entry for peer's link-layer address and IPv6 address. Otherwise, + // when device receives RS with IPv6 link-local address as source address, it has to + // initiate the address resolution first before responding the unicast RA. + sNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mTetheredParams.name, "forwarding", "1"); + + assertTrue(mRaDaemon.start()); + final RaParams params1 = createRaParams("2001:1122:3344::5566"); + mRaDaemon.buildNewRa(null, params1); + assertMulticastRaPacket(new TestRaPacket(null, params1)); + + // Add a default route "fe80::/64 -> ::" to local network, otherwise, device will fail to + // send the unicast RA out due to the ENETUNREACH error(No route to the peer's link-local + // address is present). + final String iface = mTetheredParams.name; + final RouteInfo linkLocalRoute = + new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST); + RouteUtils.addRoutesToLocalNetwork(sNetd, iface, List.of(linkLocalRoute)); + + final ByteBuffer rs = createRsPacket("fe80::1122:3344:5566:7788"); + mTetheredPacketReader.sendResponse(rs); + assertUnicastRaPacket(new TestRaPacket(null, params1)); + } +} diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java new file mode 100644 index 0000000000..830729d98f --- /dev/null +++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java @@ -0,0 +1,392 @@ +/* + * 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.networkstack.tethering; + +import static android.system.OsConstants.ETH_P_IPV6; + +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 android.net.MacAddress; +import android.os.Build; +import android.system.ErrnoException; +import android.system.OsConstants; +import android.util.ArrayMap; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetAddress; +import java.util.NoSuchElementException; +import java.util.concurrent.atomic.AtomicInteger; + + +@RunWith(AndroidJUnit4.class) +@IgnoreUpTo(Build.VERSION_CODES.R) +public final class BpfMapTest { + // Sync from packages/modules/Connectivity/Tethering/bpf_progs/offload.c. + private static final int TEST_MAP_SIZE = 16; + private static final String TETHER_DOWNSTREAM6_FS_PATH = + "/sys/fs/bpf/tethering/map_test_tether_downstream6_map"; + + private ArrayMap mTestData; + + private BpfMap mTestMap; + + @BeforeClass + public static void setupOnce() { + System.loadLibrary("tetherutilsjni"); + } + + @Before + public void setUp() throws Exception { + mTestData = new ArrayMap<>(); + mTestData.put(createTetherDownstream6Key(101, "00:00:00:00:00:aa", "2001:db8::1"), + createTether6Value(11, "00:00:00:00:00:0a", "11:11:11:00:00:0b", + ETH_P_IPV6, 1280)); + mTestData.put(createTetherDownstream6Key(102, "00:00:00:00:00:bb", "2001:db8::2"), + createTether6Value(22, "00:00:00:00:00:0c", "22:22:22:00:00:0d", + ETH_P_IPV6, 1400)); + mTestData.put(createTetherDownstream6Key(103, "00:00:00:00:00:cc", "2001:db8::3"), + createTether6Value(33, "00:00:00:00:00:0e", "33:33:33:00:00:0f", + ETH_P_IPV6, 1500)); + + initTestMap(); + } + + private void initTestMap() throws Exception { + mTestMap = new BpfMap<>( + TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR, + TetherDownstream6Key.class, Tether6Value.class); + + mTestMap.forEach((key, value) -> { + try { + assertTrue(mTestMap.deleteEntry(key)); + } catch (ErrnoException e) { + fail("Fail to delete the key " + key + ": " + e); + } + }); + assertNull(mTestMap.getFirstKey()); + assertTrue(mTestMap.isEmpty()); + } + + private TetherDownstream6Key createTetherDownstream6Key(long iif, String mac, + String address) throws Exception { + final MacAddress dstMac = MacAddress.fromString(mac); + final InetAddress ipv6Address = InetAddress.getByName(address); + + return new TetherDownstream6Key(iif, dstMac, ipv6Address.getAddress()); + } + + private Tether6Value createTether6Value(int oif, String src, String dst, int proto, int pmtu) { + final MacAddress srcMac = MacAddress.fromString(src); + final MacAddress dstMac = MacAddress.fromString(dst); + + return new Tether6Value(oif, dstMac, srcMac, proto, pmtu); + } + + @Test + public void testGetFd() throws Exception { + try (BpfMap readOnlyMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDONLY, + TetherDownstream6Key.class, Tether6Value.class)) { + assertNotNull(readOnlyMap); + try { + readOnlyMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0)); + fail("Writing RO map should throw ErrnoException"); + } catch (ErrnoException expected) { + assertEquals(OsConstants.EPERM, expected.errno); + } + } + try (BpfMap writeOnlyMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_WRONLY, + TetherDownstream6Key.class, Tether6Value.class)) { + assertNotNull(writeOnlyMap); + try { + writeOnlyMap.getFirstKey(); + fail("Reading WO map should throw ErrnoException"); + } catch (ErrnoException expected) { + assertEquals(OsConstants.EPERM, expected.errno); + } + } + try (BpfMap readWriteMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR, + TetherDownstream6Key.class, Tether6Value.class)) { + assertNotNull(readWriteMap); + } + } + + @Test + public void testIsEmpty() throws Exception { + assertNull(mTestMap.getFirstKey()); + assertTrue(mTestMap.isEmpty()); + + mTestMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0)); + assertFalse(mTestMap.isEmpty()); + + mTestMap.deleteEntry((mTestData.keyAt(0))); + assertTrue(mTestMap.isEmpty()); + } + + @Test + public void testGetFirstKey() throws Exception { + // getFirstKey on an empty map returns null. + assertFalse(mTestMap.containsKey(mTestData.keyAt(0))); + assertNull(mTestMap.getFirstKey()); + assertNull(mTestMap.getValue(mTestData.keyAt(0))); + + // getFirstKey on a non-empty map returns the first key. + mTestMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0)); + assertEquals(mTestData.keyAt(0), mTestMap.getFirstKey()); + } + + @Test + public void testGetNextKey() throws Exception { + // [1] If the passed-in key is not found on empty map, return null. + final TetherDownstream6Key nonexistentKey = + createTetherDownstream6Key(1234, "00:00:00:00:00:01", "2001:db8::10"); + assertNull(mTestMap.getNextKey(nonexistentKey)); + + // [2] If the passed-in key is null on empty map, throw NullPointerException. + try { + mTestMap.getNextKey(null); + fail("Getting next key with null key should throw NullPointerException"); + } catch (NullPointerException expected) { } + + // The BPF map has one entry now. + final ArrayMap resultMap = + new ArrayMap<>(); + mTestMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0)); + resultMap.put(mTestData.keyAt(0), mTestData.valueAt(0)); + + // [3] If the passed-in key is the last key, return null. + // Because there is only one entry in the map, the first key equals the last key. + final TetherDownstream6Key lastKey = mTestMap.getFirstKey(); + assertNull(mTestMap.getNextKey(lastKey)); + + // The BPF map has two entries now. + mTestMap.insertEntry(mTestData.keyAt(1), mTestData.valueAt(1)); + resultMap.put(mTestData.keyAt(1), mTestData.valueAt(1)); + + // [4] If the passed-in key is found, return the next key. + TetherDownstream6Key nextKey = mTestMap.getFirstKey(); + while (nextKey != null) { + if (resultMap.remove(nextKey).equals(nextKey)) { + fail("Unexpected result: " + nextKey); + } + nextKey = mTestMap.getNextKey(nextKey); + } + assertTrue(resultMap.isEmpty()); + + // [5] If the passed-in key is not found on non-empty map, return the first key. + assertEquals(mTestMap.getFirstKey(), mTestMap.getNextKey(nonexistentKey)); + + // [6] If the passed-in key is null on non-empty map, throw NullPointerException. + try { + mTestMap.getNextKey(null); + fail("Getting next key with null key should throw NullPointerException"); + } catch (NullPointerException expected) { } + } + + @Test + public void testUpdateEntry() throws Exception { + final TetherDownstream6Key key = mTestData.keyAt(0); + final Tether6Value value = mTestData.valueAt(0); + final Tether6Value value2 = mTestData.valueAt(1); + assertFalse(mTestMap.deleteEntry(key)); + + // updateEntry will create an entry if it does not exist already. + mTestMap.updateEntry(key, value); + assertTrue(mTestMap.containsKey(key)); + final Tether6Value result = mTestMap.getValue(key); + assertEquals(value, result); + + // updateEntry will update an entry that already exists. + mTestMap.updateEntry(key, value2); + assertTrue(mTestMap.containsKey(key)); + final Tether6Value result2 = mTestMap.getValue(key); + assertEquals(value2, result2); + + assertTrue(mTestMap.deleteEntry(key)); + assertFalse(mTestMap.containsKey(key)); + } + + @Test + public void testInsertOrReplaceEntry() throws Exception { + final TetherDownstream6Key key = mTestData.keyAt(0); + final Tether6Value value = mTestData.valueAt(0); + final Tether6Value value2 = mTestData.valueAt(1); + assertFalse(mTestMap.deleteEntry(key)); + + // insertOrReplaceEntry will create an entry if it does not exist already. + assertTrue(mTestMap.insertOrReplaceEntry(key, value)); + assertTrue(mTestMap.containsKey(key)); + final Tether6Value result = mTestMap.getValue(key); + assertEquals(value, result); + + // updateEntry will update an entry that already exists. + assertFalse(mTestMap.insertOrReplaceEntry(key, value2)); + assertTrue(mTestMap.containsKey(key)); + final Tether6Value result2 = mTestMap.getValue(key); + assertEquals(value2, result2); + + assertTrue(mTestMap.deleteEntry(key)); + assertFalse(mTestMap.containsKey(key)); + } + + @Test + public void testInsertReplaceEntry() throws Exception { + final TetherDownstream6Key key = mTestData.keyAt(0); + final Tether6Value value = mTestData.valueAt(0); + final Tether6Value value2 = mTestData.valueAt(1); + + try { + mTestMap.replaceEntry(key, value); + fail("Replacing non-existent key " + key + " should throw NoSuchElementException"); + } catch (NoSuchElementException expected) { } + assertFalse(mTestMap.containsKey(key)); + + mTestMap.insertEntry(key, value); + assertTrue(mTestMap.containsKey(key)); + final Tether6Value result = mTestMap.getValue(key); + assertEquals(value, result); + try { + mTestMap.insertEntry(key, value); + fail("Inserting existing key " + key + " should throw IllegalStateException"); + } catch (IllegalStateException expected) { } + + mTestMap.replaceEntry(key, value2); + assertTrue(mTestMap.containsKey(key)); + final Tether6Value result2 = mTestMap.getValue(key); + assertEquals(value2, result2); + } + + @Test + public void testIterateBpfMap() throws Exception { + final ArrayMap resultMap = + new ArrayMap<>(mTestData); + + for (int i = 0; i < resultMap.size(); i++) { + mTestMap.insertEntry(resultMap.keyAt(i), resultMap.valueAt(i)); + } + + mTestMap.forEach((key, value) -> { + if (!value.equals(resultMap.remove(key))) { + fail("Unexpected result: " + key + ", value: " + value); + } + }); + assertTrue(resultMap.isEmpty()); + } + + @Test + public void testIterateEmptyMap() throws Exception { + // Can't use an int because variables used in a lambda must be final. + final AtomicInteger count = new AtomicInteger(); + mTestMap.forEach((key, value) -> count.incrementAndGet()); + // Expect that the consumer was never called. + assertEquals(0, count.get()); + } + + @Test + public void testIterateDeletion() throws Exception { + final ArrayMap resultMap = + new ArrayMap<>(mTestData); + + for (int i = 0; i < resultMap.size(); i++) { + mTestMap.insertEntry(resultMap.keyAt(i), resultMap.valueAt(i)); + } + + // Can't use an int because variables used in a lambda must be final. + final AtomicInteger count = new AtomicInteger(); + mTestMap.forEach((key, value) -> { + try { + assertTrue(mTestMap.deleteEntry(key)); + } catch (ErrnoException e) { + fail("Fail to delete key " + key + ": " + e); + } + if (!value.equals(resultMap.remove(key))) { + fail("Unexpected result: " + key + ", value: " + value); + } + count.incrementAndGet(); + }); + assertEquals(3, count.get()); + assertTrue(resultMap.isEmpty()); + assertNull(mTestMap.getFirstKey()); + } + + @Test + public void testClear() throws Exception { + // Clear an empty map. + assertTrue(mTestMap.isEmpty()); + mTestMap.clear(); + + // Clear a map with some data in it. + final ArrayMap resultMap = + new ArrayMap<>(mTestData); + for (int i = 0; i < resultMap.size(); i++) { + mTestMap.insertEntry(resultMap.keyAt(i), resultMap.valueAt(i)); + } + assertFalse(mTestMap.isEmpty()); + mTestMap.clear(); + assertTrue(mTestMap.isEmpty()); + + // Clearing an already-closed map throws. + mTestMap.close(); + try { + mTestMap.clear(); + fail("clearing already-closed map should throw"); + } catch (ErrnoException expected) { + assertEquals(OsConstants.EBADF, expected.errno); + } + } + + @Test + public void testInsertOverflow() throws Exception { + final ArrayMap testData = + new ArrayMap<>(); + + // Build test data for TEST_MAP_SIZE + 1 entries. + for (int i = 1; i <= TEST_MAP_SIZE + 1; i++) { + testData.put( + createTetherDownstream6Key(i, "00:00:00:00:00:01", "2001:db8::1"), + createTether6Value(100, "de:ad:be:ef:00:01", "de:ad:be:ef:00:02", + ETH_P_IPV6, 1500)); + } + + // Insert #TEST_MAP_SIZE test entries to the map. The map has reached the limit. + for (int i = 0; i < TEST_MAP_SIZE; i++) { + mTestMap.insertEntry(testData.keyAt(i), testData.valueAt(i)); + } + + // The map won't allow inserting any more entries. + try { + mTestMap.insertEntry(testData.keyAt(TEST_MAP_SIZE), testData.valueAt(TEST_MAP_SIZE)); + fail("Writing too many entries should throw ErrnoException"); + } catch (ErrnoException expected) { + // Expect that can't insert the entry anymore because the number of elements in the + // map reached the limit. See man-pages/bpf. + assertEquals(OsConstants.E2BIG, expected.errno); + } + } +} diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java new file mode 100644 index 0000000000..57c28fc67c --- /dev/null +++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/ConntrackSocketTest.java @@ -0,0 +1,130 @@ +/* + * 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.networkstack.tethering; + +import static android.net.netlink.NetlinkSocket.DEFAULT_RECV_BUFSIZE; +import static android.net.netlink.StructNlMsgHdr.NLM_F_DUMP; +import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST; + +import static com.android.networkstack.tethering.OffloadHardwareInterface.IPCTNL_MSG_CT_GET; +import static com.android.networkstack.tethering.OffloadHardwareInterface.IPCTNL_MSG_CT_NEW; +import static com.android.networkstack.tethering.OffloadHardwareInterface.NFNL_SUBSYS_CTNETLINK; +import static com.android.networkstack.tethering.OffloadHardwareInterface.NF_NETLINK_CONNTRACK_DESTROY; +import static com.android.networkstack.tethering.OffloadHardwareInterface.NF_NETLINK_CONNTRACK_NEW; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.net.netlink.StructNlMsgHdr; +import android.net.util.SharedLog; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.NativeHandle; +import android.system.Os; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ConntrackSocketTest { + private static final long TIMEOUT = 500; + + private HandlerThread mHandlerThread; + private Handler mHandler; + private final SharedLog mLog = new SharedLog("privileged-test"); + + private OffloadHardwareInterface mOffloadHw; + private OffloadHardwareInterface.Dependencies mDeps; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mHandlerThread = new HandlerThread(getClass().getSimpleName()); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + + // Looper must be prepared here since AndroidJUnitRunner runs tests on separate threads. + if (Looper.myLooper() == null) Looper.prepare(); + + mDeps = new OffloadHardwareInterface.Dependencies(mLog); + mOffloadHw = new OffloadHardwareInterface(mHandler, mLog, mDeps); + } + + @Test + public void testIpv4ConntrackSocket() throws Exception { + // Set up server and connect. + final InetSocketAddress anyAddress = new InetSocketAddress( + InetAddress.getByName("127.0.0.1"), 0); + final ServerSocket serverSocket = new ServerSocket(); + serverSocket.bind(anyAddress); + final SocketAddress theAddress = serverSocket.getLocalSocketAddress(); + + // Make a connection to the server. + final Socket socket = new Socket(); + socket.connect(theAddress); + final Socket acceptedSocket = serverSocket.accept(); + + final NativeHandle handle = mDeps.createConntrackSocket( + NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY); + mOffloadHw.sendIpv4NfGenMsg(handle, + (short) ((NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_GET), + (short) (NLM_F_REQUEST | NLM_F_DUMP)); + + boolean foundConntrackEntry = false; + ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_RECV_BUFSIZE); + buffer.order(ByteOrder.nativeOrder()); + + try { + while (Os.read(handle.getFileDescriptor(), buffer) > 0) { + buffer.flip(); + + // TODO: ConntrackMessage should get a parse API like StructNlMsgHdr + // so we can confirm that the conntrack added is for the TCP connection above. + final StructNlMsgHdr nlmsghdr = StructNlMsgHdr.parse(buffer); + assertNotNull(nlmsghdr); + + // As long as 1 conntrack entry is found test case will pass, even if it's not + // the from the TCP connection above. + if (nlmsghdr.nlmsg_type == ((NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_NEW)) { + foundConntrackEntry = true; + break; + } + } + } finally { + socket.close(); + serverSocket.close(); + } + assertTrue("Did not receive any NFNL_SUBSYS_CTNETLINK/IPCTNL_MSG_CT_NEW message", + foundConntrackEntry); + } +} diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp new file mode 100644 index 0000000000..c6f19d7a16 --- /dev/null +++ b/Tethering/tests/unit/Android.bp @@ -0,0 +1,104 @@ +// +// Copyright (C) 2019 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Tests in this folder are included both in unit tests and CTS. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_library { + name: "TetheringCommonTests", + srcs: [ + "common/**/*.java", + "common/**/*.kt" + ], + static_libs: [ + "androidx.test.rules", + "net-tests-utils", + ], + // TODO(b/147200698) change sdk_version to module-current and remove framework-minus-apex + sdk_version: "core_platform", + libs: [ + "framework-minus-apex", + "framework-connectivity.impl", + "framework-tethering.impl", + ], + visibility: [ + "//packages/modules/Connectivity/tests/cts/tethering", + ], +} + +java_defaults { + name: "TetheringTestsDefaults", + min_sdk_version: "30", + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + static_libs: [ + "TetheringApiCurrentLib", + "TetheringCommonTests", + "androidx.test.rules", + "frameworks-base-testutils", + "mockito-target-extended-minus-junit4", + "net-tests-utils", + "testables", + ], + // TODO(b/147200698) change sdk_version to module-current and + // remove framework-minus-apex, ext, and framework-res + sdk_version: "core_platform", + libs: [ + "android.test.runner", + "android.test.base", + "android.test.mock", + "ext", + "framework-minus-apex", + "framework-res", + "framework-connectivity.impl", + "framework-tethering.impl", + "framework-wifi.stubs.module_lib", + ], + jni_libs: [ + // For mockito extended + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + "libtetherutilsjni", + ], +} + +// Library containing the unit tests. This is used by the coverage test target to pull in the +// unit test code. It is not currently used by the tests themselves because all the build +// configuration needed by the tests is in the TetheringTestsDefaults rule. +android_library { + name: "TetheringTestsLatestSdkLib", + defaults: ["TetheringTestsDefaults"], + target_sdk_version: "30", + visibility: [ + "//packages/modules/Connectivity/Tethering/tests/integration", + ] +} + +android_test { + name: "TetheringTests", + platform_apis: true, + test_suites: [ + "device-tests", + "mts", + ], + jarjar_rules: ":TetheringTestsJarJarRules", + defaults: ["TetheringTestsDefaults"], + compile_multilib: "both", +} diff --git a/Tethering/tests/unit/AndroidManifest.xml b/Tethering/tests/unit/AndroidManifest.xml new file mode 100644 index 0000000000..355342f643 --- /dev/null +++ b/Tethering/tests/unit/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + diff --git a/Tethering/tests/unit/common/android/net/TetheredClientTest.kt b/Tethering/tests/unit/common/android/net/TetheredClientTest.kt new file mode 100644 index 0000000000..55c59dd08f --- /dev/null +++ b/Tethering/tests/unit/common/android/net/TetheredClientTest.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 + +import android.net.InetAddresses.parseNumericAddress +import android.net.TetheredClient.AddressInfo +import android.net.TetheringManager.TETHERING_BLUETOOTH +import android.net.TetheringManager.TETHERING_USB +import android.system.OsConstants.RT_SCOPE_UNIVERSE +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.testutils.assertParcelSane +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +private val TEST_MACADDR = MacAddress.fromBytes(byteArrayOf(12, 23, 34, 45, 56, 67)) +private val TEST_OTHER_MACADDR = MacAddress.fromBytes(byteArrayOf(23, 34, 45, 56, 67, 78)) +private val TEST_ADDR1 = makeLinkAddress("192.168.113.3", prefixLength = 24, expTime = 123L) +private val TEST_ADDR2 = makeLinkAddress("fe80::1:2:3", prefixLength = 64, expTime = 456L) +private val TEST_HOSTNAME = "test_hostname" +private val TEST_OTHER_HOSTNAME = "test_other_hostname" +private val TEST_ADDRINFO1 = AddressInfo(TEST_ADDR1, TEST_HOSTNAME) +private val TEST_ADDRINFO2 = AddressInfo(TEST_ADDR2, null) + +private fun makeLinkAddress(addr: String, prefixLength: Int, expTime: Long) = LinkAddress( + parseNumericAddress(addr), + prefixLength, + 0 /* flags */, + RT_SCOPE_UNIVERSE, + expTime /* deprecationTime */, + expTime /* expirationTime */) + +@RunWith(AndroidJUnit4::class) +@SmallTest +class TetheredClientTest { + @Test + fun testParceling() { + assertParcelSane(TEST_ADDRINFO1, fieldCount = 2) + assertParcelSane(makeTestClient(), fieldCount = 3) + } + + @Test + fun testEquals() { + assertEquals(makeTestClient(), makeTestClient()) + + // Different mac address + assertNotEquals(makeTestClient(), TetheredClient( + TEST_OTHER_MACADDR, + listOf(TEST_ADDRINFO1, TEST_ADDRINFO2), + TETHERING_BLUETOOTH)) + + // Different hostname + assertNotEquals(makeTestClient(), TetheredClient( + TEST_MACADDR, + listOf(AddressInfo(TEST_ADDR1, TEST_OTHER_HOSTNAME), TEST_ADDRINFO2), + TETHERING_BLUETOOTH)) + + // Null hostname + assertNotEquals(makeTestClient(), TetheredClient( + TEST_MACADDR, + listOf(AddressInfo(TEST_ADDR1, null), TEST_ADDRINFO2), + TETHERING_BLUETOOTH)) + + // Missing address + assertNotEquals(makeTestClient(), TetheredClient( + TEST_MACADDR, + listOf(TEST_ADDRINFO2), + TETHERING_BLUETOOTH)) + + // Different type + assertNotEquals(makeTestClient(), TetheredClient( + TEST_MACADDR, + listOf(TEST_ADDRINFO1, TEST_ADDRINFO2), + TETHERING_USB)) + } + + @Test + fun testAddAddresses() { + val client1 = TetheredClient(TEST_MACADDR, listOf(TEST_ADDRINFO1), TETHERING_USB) + val client2 = TetheredClient(TEST_OTHER_MACADDR, listOf(TEST_ADDRINFO2), TETHERING_USB) + assertEquals(TetheredClient( + TEST_MACADDR, + listOf(TEST_ADDRINFO1, TEST_ADDRINFO2), + TETHERING_USB), client1.addAddresses(client2)) + } + + @Test + fun testGetters() { + assertEquals(TEST_MACADDR, makeTestClient().macAddress) + assertEquals(listOf(TEST_ADDRINFO1, TEST_ADDRINFO2), makeTestClient().addresses) + assertEquals(TETHERING_BLUETOOTH, makeTestClient().tetheringType) + } + + @Test + fun testAddressInfo_Getters() { + assertEquals(TEST_ADDR1, TEST_ADDRINFO1.address) + assertEquals(TEST_ADDR2, TEST_ADDRINFO2.address) + assertEquals(TEST_HOSTNAME, TEST_ADDRINFO1.hostname) + assertEquals(null, TEST_ADDRINFO2.hostname) + } + + private fun makeTestClient() = TetheredClient( + TEST_MACADDR, + listOf(TEST_ADDRINFO1, TEST_ADDRINFO2), + TETHERING_BLUETOOTH) +} \ No newline at end of file diff --git a/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java b/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java new file mode 100644 index 0000000000..a8857b2e5c --- /dev/null +++ b/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.dhcp; + +import static android.net.InetAddresses.parseNumericAddress; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.net.LinkAddress; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.Inet4Address; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DhcpServingParamsParcelExtTest { + private static final Inet4Address TEST_ADDRESS = inet4Addr("192.168.0.123"); + private static final Inet4Address TEST_CLIENT_ADDRESS = inet4Addr("192.168.0.42"); + private static final int TEST_ADDRESS_PARCELED = 0xc0a8007b; + private static final int TEST_CLIENT_ADDRESS_PARCELED = 0xc0a8002a; + private static final int TEST_PREFIX_LENGTH = 17; + private static final int TEST_LEASE_TIME_SECS = 120; + private static final int TEST_MTU = 1000; + private static final Set TEST_ADDRESS_SET = + new HashSet(Arrays.asList( + new Inet4Address[] {inet4Addr("192.168.1.123"), inet4Addr("192.168.1.124")})); + private static final Set TEST_ADDRESS_SET_PARCELED = + new HashSet(Arrays.asList(new Integer[] {0xc0a8017b, 0xc0a8017c})); + + private DhcpServingParamsParcelExt mParcel; + + @Before + public void setUp() { + mParcel = new DhcpServingParamsParcelExt(); + } + + @Test + public void testSetServerAddr() { + mParcel.setServerAddr(new LinkAddress(TEST_ADDRESS, TEST_PREFIX_LENGTH)); + + assertEquals(TEST_ADDRESS_PARCELED, mParcel.serverAddr); + assertEquals(TEST_PREFIX_LENGTH, mParcel.serverAddrPrefixLength); + } + + @Test + public void testSetDefaultRouters() { + mParcel.setDefaultRouters(TEST_ADDRESS_SET); + assertEquals(TEST_ADDRESS_SET_PARCELED, asSet(mParcel.defaultRouters)); + } + + @Test + public void testSetDnsServers() { + mParcel.setDnsServers(TEST_ADDRESS_SET); + assertEquals(TEST_ADDRESS_SET_PARCELED, asSet(mParcel.dnsServers)); + } + + @Test + public void testSetExcludedAddrs() { + mParcel.setExcludedAddrs(TEST_ADDRESS_SET); + assertEquals(TEST_ADDRESS_SET_PARCELED, asSet(mParcel.excludedAddrs)); + } + + @Test + public void testSetDhcpLeaseTimeSecs() { + mParcel.setDhcpLeaseTimeSecs(TEST_LEASE_TIME_SECS); + assertEquals(TEST_LEASE_TIME_SECS, mParcel.dhcpLeaseTimeSecs); + } + + @Test + public void testSetLinkMtu() { + mParcel.setLinkMtu(TEST_MTU); + assertEquals(TEST_MTU, mParcel.linkMtu); + } + + @Test + public void testSetMetered() { + mParcel.setMetered(true); + assertTrue(mParcel.metered); + mParcel.setMetered(false); + assertFalse(mParcel.metered); + } + + @Test + public void testSetClientAddr() { + mParcel.setSingleClientAddr(TEST_CLIENT_ADDRESS); + assertEquals(TEST_CLIENT_ADDRESS_PARCELED, mParcel.singleClientAddr); + } + + private static Inet4Address inet4Addr(String addr) { + return (Inet4Address) parseNumericAddress(addr); + } + + private static Set asSet(int[] ints) { + return IntStream.of(ints).boxed().collect(Collectors.toSet()); + } +} diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java new file mode 100644 index 0000000000..ce69cb30ee --- /dev/null +++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java @@ -0,0 +1,1455 @@ +/* + * 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.ip; + +import static android.net.INetd.IF_STATE_UP; +import static android.net.RouteInfo.RTN_UNICAST; +import static android.net.TetheringManager.TETHERING_BLUETOOTH; +import static android.net.TetheringManager.TETHERING_NCM; +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_ENABLE_FORWARDING_ERROR; +import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR; +import static android.net.TetheringManager.TETHER_ERROR_TETHER_IFACE_ERROR; +import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS; +import static android.net.ip.IpServer.STATE_AVAILABLE; +import static android.net.ip.IpServer.STATE_LOCAL_ONLY; +import static android.net.ip.IpServer.STATE_TETHERED; +import static android.net.ip.IpServer.STATE_UNAVAILABLE; +import static android.net.netlink.NetlinkConstants.RTM_DELNEIGH; +import static android.net.netlink.NetlinkConstants.RTM_NEWNEIGH; +import static android.net.netlink.StructNdMsg.NUD_FAILED; +import static android.net.netlink.StructNdMsg.NUD_REACHABLE; +import static android.net.netlink.StructNdMsg.NUD_STALE; +import static android.system.OsConstants.ETH_P_IPV6; + +import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.app.usage.NetworkStatsManager; +import android.net.INetd; +import android.net.InetAddresses; +import android.net.InterfaceConfigurationParcel; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.MacAddress; +import android.net.RouteInfo; +import android.net.TetherOffloadRuleParcel; +import android.net.TetherStatsParcel; +import android.net.dhcp.DhcpServerCallbacks; +import android.net.dhcp.DhcpServingParamsParcel; +import android.net.dhcp.IDhcpEventCallbacks; +import android.net.dhcp.IDhcpServer; +import android.net.dhcp.IDhcpServerCallbacks; +import android.net.ip.IpNeighborMonitor.NeighborEvent; +import android.net.ip.IpNeighborMonitor.NeighborEventConsumer; +import android.net.ip.RouterAdvertisementDaemon.RaParams; +import android.net.util.InterfaceParams; +import android.net.util.InterfaceSet; +import android.net.util.PrefixUtils; +import android.net.util.SharedLog; +import android.os.Build; +import android.os.Handler; +import android.os.RemoteException; +import android.os.test.TestLooper; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.net.module.util.NetworkStackConstants; +import com.android.networkstack.tethering.BpfCoordinator; +import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule; +import com.android.networkstack.tethering.BpfMap; +import com.android.networkstack.tethering.PrivateAddressCoordinator; +import com.android.networkstack.tethering.Tether4Key; +import com.android.networkstack.tethering.Tether4Value; +import com.android.networkstack.tethering.Tether6Value; +import com.android.networkstack.tethering.TetherDevKey; +import com.android.networkstack.tethering.TetherDevValue; +import com.android.networkstack.tethering.TetherDownstream6Key; +import com.android.networkstack.tethering.TetherLimitKey; +import com.android.networkstack.tethering.TetherLimitValue; +import com.android.networkstack.tethering.TetherStatsKey; +import com.android.networkstack.tethering.TetherStatsValue; +import com.android.networkstack.tethering.TetherUpstream6Key; +import com.android.networkstack.tethering.TetheringConfiguration; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.Captor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.Arrays; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class IpServerTest { + @Rule + public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(); + + private static final String IFACE_NAME = "testnet1"; + private static final String UPSTREAM_IFACE = "upstream0"; + private static final String UPSTREAM_IFACE2 = "upstream1"; + private static final int UPSTREAM_IFINDEX = 101; + private static final int UPSTREAM_IFINDEX2 = 102; + private static final String BLUETOOTH_IFACE_ADDR = "192.168.44.1"; + private static final int BLUETOOTH_DHCP_PREFIX_LENGTH = 24; + private static final int DHCP_LEASE_TIME_SECS = 3600; + private static final boolean DEFAULT_USING_BPF_OFFLOAD = true; + + private static final InterfaceParams TEST_IFACE_PARAMS = new InterfaceParams( + IFACE_NAME, 42 /* index */, MacAddress.ALL_ZEROS_ADDRESS, 1500 /* defaultMtu */); + private static final InterfaceParams UPSTREAM_IFACE_PARAMS = new InterfaceParams( + UPSTREAM_IFACE, UPSTREAM_IFINDEX, MacAddress.ALL_ZEROS_ADDRESS, 1500 /* defaultMtu */); + private static final InterfaceParams UPSTREAM_IFACE_PARAMS2 = new InterfaceParams( + UPSTREAM_IFACE2, UPSTREAM_IFINDEX2, MacAddress.ALL_ZEROS_ADDRESS, + 1500 /* defaultMtu */); + + private static final int MAKE_DHCPSERVER_TIMEOUT_MS = 1000; + + private final LinkAddress mTestAddress = new LinkAddress("192.168.42.5/24"); + private final IpPrefix mBluetoothPrefix = new IpPrefix("192.168.44.0/24"); + + @Mock private INetd mNetd; + @Mock private IpServer.Callback mCallback; + @Mock private SharedLog mSharedLog; + @Mock private IDhcpServer mDhcpServer; + @Mock private DadProxy mDadProxy; + @Mock private RouterAdvertisementDaemon mRaDaemon; + @Mock private IpNeighborMonitor mIpNeighborMonitor; + @Mock private IpServer.Dependencies mDependencies; + @Mock private PrivateAddressCoordinator mAddressCoordinator; + @Mock private NetworkStatsManager mStatsManager; + @Mock private TetheringConfiguration mTetherConfig; + @Mock private ConntrackMonitor mConntrackMonitor; + @Mock private BpfMap mBpfDownstream4Map; + @Mock private BpfMap mBpfUpstream4Map; + @Mock private BpfMap mBpfDownstream6Map; + @Mock private BpfMap mBpfUpstream6Map; + @Mock private BpfMap mBpfStatsMap; + @Mock private BpfMap mBpfLimitMap; + @Mock private BpfMap mBpfDevMap; + + @Captor private ArgumentCaptor mDhcpParamsCaptor; + + private final TestLooper mLooper = new TestLooper(); + private final ArgumentCaptor mLinkPropertiesCaptor = + ArgumentCaptor.forClass(LinkProperties.class); + private IpServer mIpServer; + private InterfaceConfigurationParcel mInterfaceConfiguration; + private NeighborEventConsumer mNeighborEventConsumer; + private BpfCoordinator mBpfCoordinator; + private BpfCoordinator.Dependencies mBpfDeps; + + private void initStateMachine(int interfaceType) throws Exception { + initStateMachine(interfaceType, false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD); + } + + private void initStateMachine(int interfaceType, boolean usingLegacyDhcp, + boolean usingBpfOffload) throws Exception { + when(mDependencies.getDadProxy(any(), any())).thenReturn(mDadProxy); + when(mDependencies.getRouterAdvertisementDaemon(any())).thenReturn(mRaDaemon); + when(mDependencies.getInterfaceParams(IFACE_NAME)).thenReturn(TEST_IFACE_PARAMS); + when(mDependencies.getInterfaceParams(UPSTREAM_IFACE)).thenReturn(UPSTREAM_IFACE_PARAMS); + when(mDependencies.getInterfaceParams(UPSTREAM_IFACE2)).thenReturn(UPSTREAM_IFACE_PARAMS2); + + mInterfaceConfiguration = new InterfaceConfigurationParcel(); + mInterfaceConfiguration.flags = new String[0]; + if (interfaceType == TETHERING_BLUETOOTH) { + mInterfaceConfiguration.ipv4Addr = BLUETOOTH_IFACE_ADDR; + mInterfaceConfiguration.prefixLength = BLUETOOTH_DHCP_PREFIX_LENGTH; + } + + ArgumentCaptor neighborCaptor = + ArgumentCaptor.forClass(NeighborEventConsumer.class); + doReturn(mIpNeighborMonitor).when(mDependencies).getIpNeighborMonitor(any(), any(), + neighborCaptor.capture()); + + mIpServer = new IpServer( + IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog, mNetd, mBpfCoordinator, + mCallback, usingLegacyDhcp, usingBpfOffload, mAddressCoordinator, mDependencies); + mIpServer.start(); + mNeighborEventConsumer = neighborCaptor.getValue(); + + // Starting the state machine always puts us in a consistent state and notifies + // the rest of the world that we've changed from an unknown to available state. + mLooper.dispatchAll(); + reset(mNetd, mCallback); + + when(mRaDaemon.start()).thenReturn(true); + } + + private void initTetheredStateMachine(int interfaceType, String upstreamIface) + throws Exception { + initTetheredStateMachine(interfaceType, upstreamIface, false, + DEFAULT_USING_BPF_OFFLOAD); + } + + private void initTetheredStateMachine(int interfaceType, String upstreamIface, + boolean usingLegacyDhcp, boolean usingBpfOffload) throws Exception { + initStateMachine(interfaceType, usingLegacyDhcp, usingBpfOffload); + dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED); + if (upstreamIface != null) { + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(upstreamIface); + dispatchTetherConnectionChanged(upstreamIface, lp, 0); + } + reset(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator); + when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn( + mTestAddress); + } + + private void setUpDhcpServer() throws Exception { + doAnswer(inv -> { + final IDhcpServerCallbacks cb = inv.getArgument(2); + new Thread(() -> { + try { + cb.onDhcpServerCreated(STATUS_SUCCESS, mDhcpServer); + } catch (RemoteException e) { + fail(e.getMessage()); + } + }).run(); + return null; + }).when(mDependencies).makeDhcpServer(any(), mDhcpParamsCaptor.capture(), any()); + } + + @Before public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + when(mSharedLog.forSubComponent(anyString())).thenReturn(mSharedLog); + when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn( + mTestAddress); + when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(true /* default value */); + + mBpfDeps = new BpfCoordinator.Dependencies() { + @NonNull + public Handler getHandler() { + return new Handler(mLooper.getLooper()); + } + + @NonNull + public INetd getNetd() { + return mNetd; + } + + @NonNull + public NetworkStatsManager getNetworkStatsManager() { + return mStatsManager; + } + + @NonNull + public SharedLog getSharedLog() { + return mSharedLog; + } + + @Nullable + public TetheringConfiguration getTetherConfig() { + return mTetherConfig; + } + + @NonNull + public ConntrackMonitor getConntrackMonitor( + ConntrackMonitor.ConntrackEventConsumer consumer) { + return mConntrackMonitor; + } + + @Nullable + public BpfMap getBpfDownstream4Map() { + return mBpfDownstream4Map; + } + + @Nullable + public BpfMap getBpfUpstream4Map() { + return mBpfUpstream4Map; + } + + @Nullable + public BpfMap getBpfDownstream6Map() { + return mBpfDownstream6Map; + } + + @Nullable + public BpfMap getBpfUpstream6Map() { + return mBpfUpstream6Map; + } + + @Nullable + public BpfMap getBpfStatsMap() { + return mBpfStatsMap; + } + + @Nullable + public BpfMap getBpfLimitMap() { + return mBpfLimitMap; + } + + @Nullable + public BpfMap getBpfDevMap() { + return mBpfDevMap; + } + }; + mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps)); + + setUpDhcpServer(); + } + + @Test + public void startsOutAvailable() { + when(mDependencies.getIpNeighborMonitor(any(), any(), any())) + .thenReturn(mIpNeighborMonitor); + mIpServer = new IpServer(IFACE_NAME, mLooper.getLooper(), TETHERING_BLUETOOTH, mSharedLog, + mNetd, mBpfCoordinator, mCallback, false /* usingLegacyDhcp */, + DEFAULT_USING_BPF_OFFLOAD, mAddressCoordinator, mDependencies); + mIpServer.start(); + mLooper.dispatchAll(); + verify(mCallback).updateInterfaceState( + mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR); + verify(mCallback).updateLinkProperties(eq(mIpServer), any(LinkProperties.class)); + verifyNoMoreInteractions(mCallback, mNetd); + } + + @Test + public void shouldDoNothingUntilRequested() throws Exception { + initStateMachine(TETHERING_BLUETOOTH); + final int [] noOp_commands = { + IpServer.CMD_TETHER_UNREQUESTED, + IpServer.CMD_IP_FORWARDING_ENABLE_ERROR, + IpServer.CMD_IP_FORWARDING_DISABLE_ERROR, + IpServer.CMD_START_TETHERING_ERROR, + IpServer.CMD_STOP_TETHERING_ERROR, + IpServer.CMD_SET_DNS_FORWARDERS_ERROR, + IpServer.CMD_TETHER_CONNECTION_CHANGED + }; + for (int command : noOp_commands) { + // None of these commands should trigger us to request action from + // the rest of the system. + dispatchCommand(command); + verifyNoMoreInteractions(mNetd, mCallback); + } + } + + @Test + public void handlesImmediateInterfaceDown() throws Exception { + initStateMachine(TETHERING_BLUETOOTH); + + dispatchCommand(IpServer.CMD_INTERFACE_DOWN); + verify(mCallback).updateInterfaceState( + mIpServer, STATE_UNAVAILABLE, TETHER_ERROR_NO_ERROR); + verify(mCallback).updateLinkProperties(eq(mIpServer), any(LinkProperties.class)); + verifyNoMoreInteractions(mNetd, mCallback); + } + + @Test + public void canBeTethered() throws Exception { + initStateMachine(TETHERING_BLUETOOTH); + + dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED); + InOrder inOrder = inOrder(mCallback, mNetd); + inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME); + inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME); + // One for ipv4 route, one for ipv6 link local route. + inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME), + any(), any()); + inOrder.verify(mCallback).updateInterfaceState( + mIpServer, STATE_TETHERED, TETHER_ERROR_NO_ERROR); + inOrder.verify(mCallback).updateLinkProperties( + eq(mIpServer), any(LinkProperties.class)); + verifyNoMoreInteractions(mNetd, mCallback); + } + + @Test + public void canUnrequestTethering() throws Exception { + initTetheredStateMachine(TETHERING_BLUETOOTH, null); + + dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED); + InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator); + inOrder.verify(mNetd).tetherApplyDnsInterfaces(); + inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME); + inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME); + inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName))); + inOrder.verify(mAddressCoordinator).releaseDownstream(any()); + inOrder.verify(mCallback).updateInterfaceState( + mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR); + inOrder.verify(mCallback).updateLinkProperties( + eq(mIpServer), any(LinkProperties.class)); + verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator); + } + + @Test + public void canBeTetheredAsUsb() throws Exception { + initStateMachine(TETHERING_USB); + + dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED); + InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator); + inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true)); + inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> + IFACE_NAME.equals(cfg.ifName) && assertContainsFlag(cfg.flags, IF_STATE_UP))); + inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME); + inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME); + inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME), + any(), any()); + inOrder.verify(mCallback).updateInterfaceState( + mIpServer, STATE_TETHERED, TETHER_ERROR_NO_ERROR); + inOrder.verify(mCallback).updateLinkProperties( + eq(mIpServer), mLinkPropertiesCaptor.capture()); + assertIPv4AddressAndDirectlyConnectedRoute(mLinkPropertiesCaptor.getValue()); + verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator); + } + + @Test + public void canBeTetheredAsWifiP2p() throws Exception { + initStateMachine(TETHERING_WIFI_P2P); + + dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY); + InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator); + inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true)); + inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> + IFACE_NAME.equals(cfg.ifName) && assertNotContainsFlag(cfg.flags, IF_STATE_UP))); + inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME); + inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME); + inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME), + any(), any()); + inOrder.verify(mCallback).updateInterfaceState( + mIpServer, STATE_LOCAL_ONLY, TETHER_ERROR_NO_ERROR); + inOrder.verify(mCallback).updateLinkProperties( + eq(mIpServer), mLinkPropertiesCaptor.capture()); + assertIPv4AddressAndDirectlyConnectedRoute(mLinkPropertiesCaptor.getValue()); + verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator); + } + + @Test + public void handlesFirstUpstreamChange() throws Exception { + initTetheredStateMachine(TETHERING_BLUETOOTH, null); + + // Telling the state machine about its upstream interface triggers + // a little more configuration. + dispatchTetherConnectionChanged(UPSTREAM_IFACE); + InOrder inOrder = inOrder(mNetd, mBpfCoordinator); + + // Add the forwarding pair . + inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, + UPSTREAM_IFACE); + inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE); + inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE); + inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE); + + verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator); + } + + @Test + public void handlesChangingUpstream() throws Exception { + initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE); + + dispatchTetherConnectionChanged(UPSTREAM_IFACE2); + InOrder inOrder = inOrder(mNetd, mBpfCoordinator); + + // Remove the forwarding pair . + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE); + inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE); + inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE); + + // Add the forwarding pair . + inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2, + UPSTREAM_IFACE2); + inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2); + inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2); + inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2); + + verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator); + } + + @Test + public void handlesChangingUpstreamNatFailure() throws Exception { + initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE); + + doThrow(RemoteException.class).when(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2); + + dispatchTetherConnectionChanged(UPSTREAM_IFACE2); + InOrder inOrder = inOrder(mNetd, mBpfCoordinator); + + // Remove the forwarding pair . + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE); + inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE); + inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE); + + // Add the forwarding pair and expect that failed on + // tetherAddForward. + inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2, + UPSTREAM_IFACE2); + inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2); + inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2); + + // Remove the forwarding pair to fallback. + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2); + inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2); + inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2); + } + + @Test + public void handlesChangingUpstreamInterfaceForwardingFailure() throws Exception { + initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE); + + doThrow(RemoteException.class).when(mNetd).ipfwdAddInterfaceForward( + IFACE_NAME, UPSTREAM_IFACE2); + + dispatchTetherConnectionChanged(UPSTREAM_IFACE2); + InOrder inOrder = inOrder(mNetd, mBpfCoordinator); + + // Remove the forwarding pair . + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE); + inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE); + inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE); + + // Add the forwarding pair and expect that failed on + // ipfwdAddInterfaceForward. + inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2, + UPSTREAM_IFACE2); + inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2); + inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2); + inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2); + + // Remove the forwarding pair to fallback. + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2); + inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2); + inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2); + } + + @Test + public void canUnrequestTetheringWithUpstream() throws Exception { + initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE); + + dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED); + InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator); + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE); + inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE); + inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE); + inOrder.verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer); + inOrder.verify(mNetd).tetherApplyDnsInterfaces(); + inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME); + inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME); + inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName))); + inOrder.verify(mAddressCoordinator).releaseDownstream(any()); + inOrder.verify(mBpfCoordinator).stopMonitoring(mIpServer); + inOrder.verify(mCallback).updateInterfaceState( + mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR); + inOrder.verify(mCallback).updateLinkProperties( + eq(mIpServer), any(LinkProperties.class)); + verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator); + } + + @Test + public void interfaceDownLeadsToUnavailable() throws Exception { + for (boolean shouldThrow : new boolean[]{true, false}) { + initTetheredStateMachine(TETHERING_USB, null); + + if (shouldThrow) { + doThrow(RemoteException.class).when(mNetd).tetherInterfaceRemove(IFACE_NAME); + } + dispatchCommand(IpServer.CMD_INTERFACE_DOWN); + InOrder usbTeardownOrder = inOrder(mNetd, mCallback); + // Currently IpServer interfaceSetCfg twice to stop IPv4. One just set interface down + // Another one is set IPv4 to 0.0.0.0/0 as clearng ipv4 address. + usbTeardownOrder.verify(mNetd, times(2)).interfaceSetCfg( + argThat(cfg -> IFACE_NAME.equals(cfg.ifName))); + usbTeardownOrder.verify(mCallback).updateInterfaceState( + mIpServer, STATE_UNAVAILABLE, TETHER_ERROR_NO_ERROR); + usbTeardownOrder.verify(mCallback).updateLinkProperties( + eq(mIpServer), mLinkPropertiesCaptor.capture()); + assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue()); + } + } + + @Test + public void usbShouldBeTornDownOnTetherError() throws Exception { + initStateMachine(TETHERING_USB); + + doThrow(RemoteException.class).when(mNetd).tetherInterfaceAdd(IFACE_NAME); + dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED); + InOrder usbTeardownOrder = inOrder(mNetd, mCallback); + usbTeardownOrder.verify(mNetd).interfaceSetCfg( + argThat(cfg -> IFACE_NAME.equals(cfg.ifName))); + usbTeardownOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME); + + usbTeardownOrder.verify(mNetd, times(2)).interfaceSetCfg( + argThat(cfg -> IFACE_NAME.equals(cfg.ifName))); + usbTeardownOrder.verify(mCallback).updateInterfaceState( + mIpServer, STATE_AVAILABLE, TETHER_ERROR_TETHER_IFACE_ERROR); + usbTeardownOrder.verify(mCallback).updateLinkProperties( + eq(mIpServer), mLinkPropertiesCaptor.capture()); + assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue()); + } + + @Test + public void shouldTearDownUsbOnUpstreamError() throws Exception { + initTetheredStateMachine(TETHERING_USB, null); + + doThrow(RemoteException.class).when(mNetd).tetherAddForward(anyString(), anyString()); + dispatchTetherConnectionChanged(UPSTREAM_IFACE); + InOrder usbTeardownOrder = inOrder(mNetd, mCallback); + usbTeardownOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE); + + usbTeardownOrder.verify(mNetd, times(2)).interfaceSetCfg( + argThat(cfg -> IFACE_NAME.equals(cfg.ifName))); + usbTeardownOrder.verify(mCallback).updateInterfaceState( + mIpServer, STATE_AVAILABLE, TETHER_ERROR_ENABLE_FORWARDING_ERROR); + usbTeardownOrder.verify(mCallback).updateLinkProperties( + eq(mIpServer), mLinkPropertiesCaptor.capture()); + assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue()); + } + + @Test + public void ignoresDuplicateUpstreamNotifications() throws Exception { + initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE); + + verifyNoMoreInteractions(mNetd, mCallback); + + for (int i = 0; i < 5; i++) { + dispatchTetherConnectionChanged(UPSTREAM_IFACE); + verifyNoMoreInteractions(mNetd, mCallback); + } + } + + @Test + public void startsDhcpServer() throws Exception { + initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE); + dispatchTetherConnectionChanged(UPSTREAM_IFACE); + + assertDhcpStarted(PrefixUtils.asIpPrefix(mTestAddress)); + } + + @Test + public void startsDhcpServerOnBluetooth() throws Exception { + initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE); + dispatchTetherConnectionChanged(UPSTREAM_IFACE); + + assertDhcpStarted(mBluetoothPrefix); + } + + @Test + public void startsDhcpServerOnWifiP2p() throws Exception { + initTetheredStateMachine(TETHERING_WIFI_P2P, UPSTREAM_IFACE); + dispatchTetherConnectionChanged(UPSTREAM_IFACE); + + assertDhcpStarted(PrefixUtils.asIpPrefix(mTestAddress)); + } + + @Test + public void startsDhcpServerOnNcm() throws Exception { + initStateMachine(TETHERING_NCM); + dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY); + dispatchTetherConnectionChanged(UPSTREAM_IFACE); + + assertDhcpStarted(new IpPrefix("192.168.42.0/24")); + } + + @Test + public void testOnNewPrefixRequest() throws Exception { + initStateMachine(TETHERING_NCM); + dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY); + + final IDhcpEventCallbacks eventCallbacks; + final ArgumentCaptor dhcpEventCbsCaptor = + ArgumentCaptor.forClass(IDhcpEventCallbacks.class); + verify(mDhcpServer, timeout(MAKE_DHCPSERVER_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), dhcpEventCbsCaptor.capture()); + eventCallbacks = dhcpEventCbsCaptor.getValue(); + assertDhcpStarted(new IpPrefix("192.168.42.0/24")); + + final ArgumentCaptor lpCaptor = + ArgumentCaptor.forClass(LinkProperties.class); + InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator); + inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(true)); + inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME); + // One for ipv4 route, one for ipv6 link local route. + inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME), + any(), any()); + inOrder.verify(mCallback).updateInterfaceState( + mIpServer, STATE_LOCAL_ONLY, TETHER_ERROR_NO_ERROR); + inOrder.verify(mCallback).updateLinkProperties(eq(mIpServer), lpCaptor.capture()); + verifyNoMoreInteractions(mCallback, mAddressCoordinator); + + // Simulate the DHCP server receives DHCPDECLINE on MirrorLink and then signals + // onNewPrefixRequest callback. + final LinkAddress newAddress = new LinkAddress("192.168.100.125/24"); + when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn( + newAddress); + eventCallbacks.onNewPrefixRequest(new IpPrefix("192.168.42.0/24")); + mLooper.dispatchAll(); + + inOrder.verify(mAddressCoordinator).requestDownstreamAddress(any(), eq(false)); + inOrder.verify(mNetd).tetherApplyDnsInterfaces(); + inOrder.verify(mCallback).updateLinkProperties(eq(mIpServer), lpCaptor.capture()); + verifyNoMoreInteractions(mCallback); + + final LinkProperties linkProperties = lpCaptor.getValue(); + final List linkAddresses = linkProperties.getLinkAddresses(); + assertEquals(1, linkProperties.getLinkAddresses().size()); + assertEquals(1, linkProperties.getRoutes().size()); + final IpPrefix prefix = new IpPrefix(linkAddresses.get(0).getAddress(), + linkAddresses.get(0).getPrefixLength()); + assertNotEquals(prefix, new IpPrefix("192.168.42.0/24")); + + verify(mDhcpServer).updateParams(mDhcpParamsCaptor.capture(), any()); + assertDhcpServingParams(mDhcpParamsCaptor.getValue(), prefix); + } + + @Test + public void doesNotStartDhcpServerIfDisabled() throws Exception { + initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, true /* usingLegacyDhcp */, + DEFAULT_USING_BPF_OFFLOAD); + dispatchTetherConnectionChanged(UPSTREAM_IFACE); + + verify(mDependencies, never()).makeDhcpServer(any(), any(), any()); + } + + private InetAddress addr(String addr) throws Exception { + return InetAddresses.parseNumericAddress(addr); + } + + private void recvNewNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) { + mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_NEWNEIGH, ifindex, addr, + nudState, mac)); + mLooper.dispatchAll(); + } + + private void recvDelNeigh(int ifindex, InetAddress addr, short nudState, MacAddress mac) { + mNeighborEventConsumer.accept(new NeighborEvent(0, RTM_DELNEIGH, ifindex, addr, + nudState, mac)); + mLooper.dispatchAll(); + } + + /** + * Custom ArgumentMatcher for TetherOffloadRuleParcel. This is needed because generated stable + * AIDL classes don't have equals(), so we cannot just use eq(). A custom assert, such as: + * + * private void checkFooCalled(StableParcelable p, ...) { + * ArgumentCaptor captor = ArgumentCaptor.forClass(FooParam.class); + * verify(mMock).foo(captor.capture()); + * Foo foo = captor.getValue(); + * assertFooMatchesExpectations(foo); + * } + * + * almost works, but not quite. This is because if the code under test calls foo() twice, the + * first call to checkFooCalled() matches both the calls, putting both calls into the captor, + * and then fails with TooManyActualInvocations. It also makes it harder to use other mockito + * features such as never(), inOrder(), etc. + * + * This approach isn't great because if the match fails, the error message is unhelpful + * (actual: "android.net.TetherOffloadRuleParcel@8c827b0" or some such), but at least it does + * work. + * + * TODO: consider making the error message more readable by adding a method that catching the + * AssertionFailedError and throwing a new assertion with more details. See + * NetworkMonitorTest#verifyNetworkTested. + * + * See ConnectivityServiceTest#assertRoutesAdded for an alternative approach which solves the + * TooManyActualInvocations problem described above by forcing the caller of the custom assert + * method to specify all expected invocations in one call. This is useful when the stable + * parcelable class being asserted on has a corresponding Java object (eg., RouteInfo and + * RouteInfoParcelable), and the caller can just pass in a list of them. It not useful here + * because there is no such object. + */ + private static class TetherOffloadRuleParcelMatcher implements + ArgumentMatcher { + public final int upstreamIfindex; + public final InetAddress dst; + public final MacAddress dstMac; + + TetherOffloadRuleParcelMatcher(int upstreamIfindex, InetAddress dst, MacAddress dstMac) { + this.upstreamIfindex = upstreamIfindex; + this.dst = dst; + this.dstMac = dstMac; + } + + public boolean matches(TetherOffloadRuleParcel parcel) { + return upstreamIfindex == parcel.inputInterfaceIndex + && (TEST_IFACE_PARAMS.index == parcel.outputInterfaceIndex) + && Arrays.equals(dst.getAddress(), parcel.destination) + && (128 == parcel.prefixLength) + && Arrays.equals(TEST_IFACE_PARAMS.macAddr.toByteArray(), parcel.srcL2Address) + && Arrays.equals(dstMac.toByteArray(), parcel.dstL2Address); + } + + public String toString() { + return String.format("TetherOffloadRuleParcelMatcher(%d, %s, %s", + upstreamIfindex, dst.getHostAddress(), dstMac); + } + } + + @NonNull + private static TetherOffloadRuleParcel matches( + int upstreamIfindex, InetAddress dst, MacAddress dstMac) { + return argThat(new TetherOffloadRuleParcelMatcher(upstreamIfindex, dst, dstMac)); + } + + @NonNull + private static Ipv6ForwardingRule makeForwardingRule( + int upstreamIfindex, @NonNull InetAddress dst, @NonNull MacAddress dstMac) { + return new Ipv6ForwardingRule(upstreamIfindex, TEST_IFACE_PARAMS.index, + (Inet6Address) dst, TEST_IFACE_PARAMS.macAddr, dstMac); + } + + @NonNull + private static TetherDownstream6Key makeDownstream6Key(int upstreamIfindex, + @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst) { + return new TetherDownstream6Key(upstreamIfindex, upstreamMac, dst.getAddress()); + } + + @NonNull + private static Tether6Value makeDownstream6Value(@NonNull final MacAddress dstMac) { + return new Tether6Value(TEST_IFACE_PARAMS.index, dstMac, + TEST_IFACE_PARAMS.macAddr, ETH_P_IPV6, NetworkStackConstants.ETHER_MTU); + } + + private T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) { + if (inOrder != null) { + return inOrder.verify(t); + } else { + return verify(t); + } + } + + private void verifyTetherOffloadRuleAdd(@Nullable InOrder inOrder, int upstreamIfindex, + @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst, + @NonNull final MacAddress dstMac) throws Exception { + if (mBpfDeps.isAtLeastS()) { + verifyWithOrder(inOrder, mBpfDownstream6Map).updateEntry( + makeDownstream6Key(upstreamIfindex, upstreamMac, dst), + makeDownstream6Value(dstMac)); + } else { + verifyWithOrder(inOrder, mNetd).tetherOffloadRuleAdd(matches(upstreamIfindex, dst, + dstMac)); + } + } + + private void verifyNeverTetherOffloadRuleAdd(int upstreamIfindex, + @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst, + @NonNull final MacAddress dstMac) throws Exception { + if (mBpfDeps.isAtLeastS()) { + verify(mBpfDownstream6Map, never()).updateEntry( + makeDownstream6Key(upstreamIfindex, upstreamMac, dst), + makeDownstream6Value(dstMac)); + } else { + verify(mNetd, never()).tetherOffloadRuleAdd(matches(upstreamIfindex, dst, dstMac)); + } + } + + private void verifyNeverTetherOffloadRuleAdd() throws Exception { + if (mBpfDeps.isAtLeastS()) { + verify(mBpfDownstream6Map, never()).updateEntry(any(), any()); + } else { + verify(mNetd, never()).tetherOffloadRuleAdd(any()); + } + } + + private void verifyTetherOffloadRuleRemove(@Nullable InOrder inOrder, int upstreamIfindex, + @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst, + @NonNull final MacAddress dstMac) throws Exception { + if (mBpfDeps.isAtLeastS()) { + verifyWithOrder(inOrder, mBpfDownstream6Map).deleteEntry(makeDownstream6Key( + upstreamIfindex, upstreamMac, dst)); + } else { + // |dstMac| is not required for deleting rules. Used bacause tetherOffloadRuleRemove + // uses a whole rule to be a argument. + // See system/netd/server/TetherController.cpp/TetherController#removeOffloadRule. + verifyWithOrder(inOrder, mNetd).tetherOffloadRuleRemove(matches(upstreamIfindex, dst, + dstMac)); + } + } + + private void verifyNeverTetherOffloadRuleRemove() throws Exception { + if (mBpfDeps.isAtLeastS()) { + verify(mBpfDownstream6Map, never()).deleteEntry(any()); + } else { + verify(mNetd, never()).tetherOffloadRuleRemove(any()); + } + } + + private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int upstreamIfindex) + throws Exception { + if (!mBpfDeps.isAtLeastS()) return; + final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index, + TEST_IFACE_PARAMS.macAddr); + final Tether6Value value = new Tether6Value(upstreamIfindex, + MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS, + ETH_P_IPV6, NetworkStackConstants.ETHER_MTU); + verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(key, value); + } + + private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder) + throws Exception { + if (!mBpfDeps.isAtLeastS()) return; + final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index, + TEST_IFACE_PARAMS.macAddr); + verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(key); + } + + private void verifyNoUpstreamIpv6ForwardingChange(@Nullable InOrder inOrder) throws Exception { + if (!mBpfDeps.isAtLeastS()) return; + if (inOrder != null) { + inOrder.verify(mBpfUpstream6Map, never()).deleteEntry(any()); + inOrder.verify(mBpfUpstream6Map, never()).insertEntry(any(), any()); + inOrder.verify(mBpfUpstream6Map, never()).updateEntry(any(), any()); + } else { + verify(mBpfUpstream6Map, never()).deleteEntry(any()); + verify(mBpfUpstream6Map, never()).insertEntry(any(), any()); + verify(mBpfUpstream6Map, never()).updateEntry(any(), any()); + } + } + + @NonNull + private static TetherStatsParcel buildEmptyTetherStatsParcel(int ifIndex) { + TetherStatsParcel parcel = new TetherStatsParcel(); + parcel.ifIndex = ifIndex; + return parcel; + } + + private void resetNetdBpfMapAndCoordinator() throws Exception { + reset(mNetd, mBpfDownstream6Map, mBpfUpstream6Map, mBpfCoordinator); + // When the last rule is removed, tetherOffloadGetAndClearStats will log a WTF (and + // potentially crash the test) if the stats map is empty. + when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]); + when(mNetd.tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX)) + .thenReturn(buildEmptyTetherStatsParcel(UPSTREAM_IFINDEX)); + when(mNetd.tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX2)) + .thenReturn(buildEmptyTetherStatsParcel(UPSTREAM_IFINDEX2)); + // When the last rule is removed, tetherOffloadGetAndClearStats will log a WTF (and + // potentially crash the test) if the stats map is empty. + final TetherStatsValue allZeros = new TetherStatsValue(0, 0, 0, 0, 0, 0); + when(mBpfStatsMap.getValue(new TetherStatsKey(UPSTREAM_IFINDEX))).thenReturn(allZeros); + when(mBpfStatsMap.getValue(new TetherStatsKey(UPSTREAM_IFINDEX2))).thenReturn(allZeros); + } + + @Test + public void addRemoveipv6ForwardingRules() throws Exception { + initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */, + DEFAULT_USING_BPF_OFFLOAD); + + final int myIfindex = TEST_IFACE_PARAMS.index; + final int notMyIfindex = myIfindex - 1; + + final InetAddress neighA = InetAddresses.parseNumericAddress("2001:db8::1"); + final InetAddress neighB = InetAddresses.parseNumericAddress("2001:db8::2"); + final InetAddress neighLL = InetAddresses.parseNumericAddress("fe80::1"); + final InetAddress neighMC = InetAddresses.parseNumericAddress("ff02::1234"); + final MacAddress macNull = MacAddress.fromString("00:00:00:00:00:00"); + final MacAddress macA = MacAddress.fromString("00:00:00:00:00:0a"); + final MacAddress macB = MacAddress.fromString("11:22:33:00:00:0b"); + + resetNetdBpfMapAndCoordinator(); + verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map); + + // TODO: Perhaps verify the interaction of tetherOffloadSetInterfaceQuota and + // tetherOffloadGetAndClearStats in netd while the rules are changed. + + // Events on other interfaces are ignored. + recvNewNeigh(notMyIfindex, neighA, NUD_REACHABLE, macA); + verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map); + + // Events on this interface are received and sent to netd. + recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA); + verify(mBpfCoordinator).tetherOffloadRuleAdd( + mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA)); + verifyTetherOffloadRuleAdd(null, + UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA); + verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX); + resetNetdBpfMapAndCoordinator(); + + recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB); + verify(mBpfCoordinator).tetherOffloadRuleAdd( + mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB)); + verifyTetherOffloadRuleAdd(null, + UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB); + verifyNoUpstreamIpv6ForwardingChange(null); + resetNetdBpfMapAndCoordinator(); + + // Link-local and multicast neighbors are ignored. + recvNewNeigh(myIfindex, neighLL, NUD_REACHABLE, macA); + verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map); + recvNewNeigh(myIfindex, neighMC, NUD_REACHABLE, macA); + verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map); + + // A neighbor that is no longer valid causes the rule to be removed. + // NUD_FAILED events do not have a MAC address. + recvNewNeigh(myIfindex, neighA, NUD_FAILED, null); + verify(mBpfCoordinator).tetherOffloadRuleRemove( + mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macNull)); + verifyTetherOffloadRuleRemove(null, + UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macNull); + verifyNoUpstreamIpv6ForwardingChange(null); + resetNetdBpfMapAndCoordinator(); + + // A neighbor that is deleted causes the rule to be removed. + recvDelNeigh(myIfindex, neighB, NUD_STALE, macB); + verify(mBpfCoordinator).tetherOffloadRuleRemove( + mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macNull)); + verifyTetherOffloadRuleRemove(null, + UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macNull); + verifyStopUpstreamIpv6Forwarding(null); + resetNetdBpfMapAndCoordinator(); + + // Upstream changes result in updating the rules. + recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA); + verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX); + recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB); + resetNetdBpfMapAndCoordinator(); + + InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map); + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(UPSTREAM_IFACE2); + dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp, -1); + verify(mBpfCoordinator).tetherOffloadRuleUpdate(mIpServer, UPSTREAM_IFINDEX2); + verifyTetherOffloadRuleRemove(inOrder, + UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA); + verifyTetherOffloadRuleRemove(inOrder, + UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB); + verifyStopUpstreamIpv6Forwarding(inOrder); + verifyTetherOffloadRuleAdd(inOrder, + UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA); + verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2); + verifyTetherOffloadRuleAdd(inOrder, + UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighB, macB); + verifyNoUpstreamIpv6ForwardingChange(inOrder); + resetNetdBpfMapAndCoordinator(); + + // When the upstream is lost, rules are removed. + dispatchTetherConnectionChanged(null, null, 0); + // Clear function is called two times by: + // - processMessage CMD_TETHER_CONNECTION_CHANGED for the upstream is lost. + // - processMessage CMD_IPV6_TETHER_UPDATE for the IPv6 upstream is lost. + // See dispatchTetherConnectionChanged. + verify(mBpfCoordinator, times(2)).tetherOffloadRuleClear(mIpServer); + verifyTetherOffloadRuleRemove(null, + UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA); + verifyTetherOffloadRuleRemove(null, + UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighB, macB); + verifyStopUpstreamIpv6Forwarding(inOrder); + resetNetdBpfMapAndCoordinator(); + + // If the upstream is IPv4-only, no rules are added. + dispatchTetherConnectionChanged(UPSTREAM_IFACE); + resetNetdBpfMapAndCoordinator(); + recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA); + // Clear function is called by #updateIpv6ForwardingRules for the IPv6 upstream is lost. + verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer); + verifyNoUpstreamIpv6ForwardingChange(null); + verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map); + + // Rules can be added again once upstream IPv6 connectivity is available. + lp.setInterfaceName(UPSTREAM_IFACE); + dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1); + recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB); + verify(mBpfCoordinator).tetherOffloadRuleAdd( + mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB)); + verifyTetherOffloadRuleAdd(null, + UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB); + verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX); + verify(mBpfCoordinator, never()).tetherOffloadRuleAdd( + mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA)); + verifyNeverTetherOffloadRuleAdd( + UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA); + + // If upstream IPv6 connectivity is lost, rules are removed. + resetNetdBpfMapAndCoordinator(); + dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0); + verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer); + verifyTetherOffloadRuleRemove(null, + UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB); + verifyStopUpstreamIpv6Forwarding(null); + + // When the interface goes down, rules are removed. + lp.setInterfaceName(UPSTREAM_IFACE); + dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1); + recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA); + recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB); + verify(mBpfCoordinator).tetherOffloadRuleAdd( + mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA)); + verifyTetherOffloadRuleAdd(null, + UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA); + verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX); + verify(mBpfCoordinator).tetherOffloadRuleAdd( + mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB)); + verifyTetherOffloadRuleAdd(null, + UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB); + resetNetdBpfMapAndCoordinator(); + + mIpServer.stop(); + mLooper.dispatchAll(); + verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer); + verifyTetherOffloadRuleRemove(null, + UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA); + verifyTetherOffloadRuleRemove(null, + UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB); + verifyStopUpstreamIpv6Forwarding(null); + verify(mIpNeighborMonitor).stop(); + resetNetdBpfMapAndCoordinator(); + } + + @Test + public void enableDisableUsingBpfOffload() throws Exception { + final int myIfindex = TEST_IFACE_PARAMS.index; + final InetAddress neigh = InetAddresses.parseNumericAddress("2001:db8::1"); + final MacAddress macA = MacAddress.fromString("00:00:00:00:00:0a"); + final MacAddress macNull = MacAddress.fromString("00:00:00:00:00:00"); + + // Expect that rules can be only added/removed when the BPF offload config is enabled. + // Note that the BPF offload disabled case is not a realistic test case. Because IP + // neighbor monitor doesn't start if BPF offload is disabled, there should have no + // neighbor event listening. This is used for testing the protection check just in case. + // TODO: Perhaps remove the BPF offload disabled case test once this check isn't needed + // anymore. + + // [1] Enable BPF offload. + // A neighbor that is added or deleted causes the rule to be added or removed. + initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */, + true /* usingBpfOffload */); + resetNetdBpfMapAndCoordinator(); + + recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA); + verify(mBpfCoordinator).tetherOffloadRuleAdd( + mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neigh, macA)); + verifyTetherOffloadRuleAdd(null, + UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neigh, macA); + verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX); + resetNetdBpfMapAndCoordinator(); + + recvDelNeigh(myIfindex, neigh, NUD_STALE, macA); + verify(mBpfCoordinator).tetherOffloadRuleRemove( + mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neigh, macNull)); + verifyTetherOffloadRuleRemove(null, + UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neigh, macNull); + verifyStopUpstreamIpv6Forwarding(null); + resetNetdBpfMapAndCoordinator(); + + // [2] Disable BPF offload. + // A neighbor that is added or deleted doesn’t cause the rule to be added or removed. + initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */, + false /* usingBpfOffload */); + resetNetdBpfMapAndCoordinator(); + + recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA); + verify(mBpfCoordinator, never()).tetherOffloadRuleAdd(any(), any()); + verifyNeverTetherOffloadRuleAdd(); + verifyNoUpstreamIpv6ForwardingChange(null); + resetNetdBpfMapAndCoordinator(); + + recvDelNeigh(myIfindex, neigh, NUD_STALE, macA); + verify(mBpfCoordinator, never()).tetherOffloadRuleRemove(any(), any()); + verifyNeverTetherOffloadRuleRemove(); + verifyNoUpstreamIpv6ForwardingChange(null); + resetNetdBpfMapAndCoordinator(); + } + + @Test + public void doesNotStartIpNeighborMonitorIfBpfOffloadDisabled() throws Exception { + initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */, + false /* usingBpfOffload */); + + // IP neighbor monitor doesn't start if BPF offload is disabled. + verify(mIpNeighborMonitor, never()).start(); + } + + private LinkProperties buildIpv6OnlyLinkProperties(final String iface) { + final LinkProperties linkProp = new LinkProperties(); + linkProp.setInterfaceName(iface); + linkProp.addLinkAddress(new LinkAddress("2001:db8::1/64")); + linkProp.addRoute(new RouteInfo(new IpPrefix("::/0"), null, iface, RTN_UNICAST)); + final InetAddress dns = InetAddresses.parseNumericAddress("2001:4860:4860::8888"); + linkProp.addDnsServer(dns); + + return linkProp; + } + + @Test + public void testAdjustTtlValue() throws Exception { + final ArgumentCaptor raParamsCaptor = + ArgumentCaptor.forClass(RaParams.class); + initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE); + verify(mRaDaemon).buildNewRa(any(), raParamsCaptor.capture()); + final RaParams noV6Params = raParamsCaptor.getValue(); + assertEquals(65, noV6Params.hopLimit); + reset(mRaDaemon); + + when(mNetd.getProcSysNet( + INetd.IPV6, INetd.CONF, UPSTREAM_IFACE, "hop_limit")).thenReturn("64"); + final LinkProperties lp = buildIpv6OnlyLinkProperties(UPSTREAM_IFACE); + dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, 1); + verify(mRaDaemon).buildNewRa(any(), raParamsCaptor.capture()); + final RaParams nonCellularParams = raParamsCaptor.getValue(); + assertEquals(65, nonCellularParams.hopLimit); + reset(mRaDaemon); + + dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0); + verify(mRaDaemon).buildNewRa(any(), raParamsCaptor.capture()); + final RaParams noUpstream = raParamsCaptor.getValue(); + assertEquals(65, nonCellularParams.hopLimit); + reset(mRaDaemon); + + dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1); + verify(mRaDaemon).buildNewRa(any(), raParamsCaptor.capture()); + final RaParams cellularParams = raParamsCaptor.getValue(); + assertEquals(63, cellularParams.hopLimit); + reset(mRaDaemon); + } + + @Test + public void testStopObsoleteDhcpServer() throws Exception { + final ArgumentCaptor cbCaptor = + ArgumentCaptor.forClass(DhcpServerCallbacks.class); + doNothing().when(mDependencies).makeDhcpServer(any(), mDhcpParamsCaptor.capture(), + cbCaptor.capture()); + initStateMachine(TETHERING_WIFI); + dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED); + verify(mDhcpServer, never()).startWithCallbacks(any(), any()); + + // No stop dhcp server because dhcp server is not created yet. + dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED); + verify(mDhcpServer, never()).stop(any()); + + // Stop obsolete dhcp server. + try { + final DhcpServerCallbacks cb = cbCaptor.getValue(); + cb.onDhcpServerCreated(STATUS_SUCCESS, mDhcpServer); + mLooper.dispatchAll(); + } catch (RemoteException e) { + fail(e.getMessage()); + } + verify(mDhcpServer).stop(any()); + } + + private void assertDhcpServingParams(final DhcpServingParamsParcel params, + final IpPrefix prefix) { + // Last address byte is random + assertTrue(prefix.contains(intToInet4AddressHTH(params.serverAddr))); + assertEquals(prefix.getPrefixLength(), params.serverAddrPrefixLength); + assertEquals(1, params.defaultRouters.length); + assertEquals(params.serverAddr, params.defaultRouters[0]); + assertEquals(1, params.dnsServers.length); + assertEquals(params.serverAddr, params.dnsServers[0]); + assertEquals(DHCP_LEASE_TIME_SECS, params.dhcpLeaseTimeSecs); + if (mIpServer.interfaceType() == TETHERING_NCM) { + assertTrue(params.changePrefixOnDecline); + } + } + + private void assertDhcpStarted(IpPrefix expectedPrefix) throws Exception { + verify(mDependencies, times(1)).makeDhcpServer(eq(IFACE_NAME), any(), any()); + verify(mDhcpServer, timeout(MAKE_DHCPSERVER_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); + assertDhcpServingParams(mDhcpParamsCaptor.getValue(), expectedPrefix); + } + + /** + * Send a command to the state machine under test, and run the event loop to idle. + * + * @param command One of the IpServer.CMD_* constants. + * @param arg1 An additional argument to pass. + */ + private void dispatchCommand(int command, int arg1) { + mIpServer.sendMessage(command, arg1); + mLooper.dispatchAll(); + } + + /** + * Send a command to the state machine under test, and run the event loop to idle. + * + * @param command One of the IpServer.CMD_* constants. + */ + private void dispatchCommand(int command) { + mIpServer.sendMessage(command); + mLooper.dispatchAll(); + } + + /** + * Special override to tell the state machine that the upstream interface has changed. + * + * @see #dispatchCommand(int) + * @param upstreamIface String name of upstream interface (or null) + * @param v6lp IPv6 LinkProperties of the upstream interface, or null for an IPv4-only upstream. + */ + private void dispatchTetherConnectionChanged(String upstreamIface, LinkProperties v6lp, + int ttlAdjustment) { + dispatchTetherConnectionChanged(upstreamIface); + mIpServer.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, ttlAdjustment, 0, v6lp); + mLooper.dispatchAll(); + } + + private void dispatchTetherConnectionChanged(String upstreamIface) { + final InterfaceSet ifs = (upstreamIface != null) ? new InterfaceSet(upstreamIface) : null; + mIpServer.sendMessage(IpServer.CMD_TETHER_CONNECTION_CHANGED, ifs); + mLooper.dispatchAll(); + } + + private void assertIPv4AddressAndDirectlyConnectedRoute(LinkProperties lp) { + // Find the first IPv4 LinkAddress. + LinkAddress addr4 = null; + for (LinkAddress addr : lp.getLinkAddresses()) { + if (!(addr.getAddress() instanceof Inet4Address)) continue; + addr4 = addr; + break; + } + assertNotNull("missing IPv4 address", addr4); + + final IpPrefix destination = new IpPrefix(addr4.getAddress(), addr4.getPrefixLength()); + // Assert the presence of the associated directly connected route. + final RouteInfo directlyConnected = new RouteInfo(destination, null, lp.getInterfaceName(), + RouteInfo.RTN_UNICAST); + assertTrue("missing directly connected route: '" + directlyConnected.toString() + "'", + lp.getRoutes().contains(directlyConnected)); + } + + private void assertNoAddressesNorRoutes(LinkProperties lp) { + assertTrue(lp.getLinkAddresses().isEmpty()); + assertTrue(lp.getRoutes().isEmpty()); + // We also check that interface name is non-empty, because we should + // never see an empty interface name in any LinkProperties update. + assertFalse(TextUtils.isEmpty(lp.getInterfaceName())); + } + + private boolean assertContainsFlag(String[] flags, String match) { + for (String flag : flags) { + if (flag.equals(match)) return true; + } + fail("Missing flag: " + match); + return false; + } + + private boolean assertNotContainsFlag(String[] flags, String match) { + for (String flag : flags) { + if (flag.equals(match)) { + fail("Unexpected flag: " + match); + return false; + } + } + return true; + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void dadProxyUpdates() throws Exception { + InOrder inOrder = inOrder(mDadProxy); + initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE); + inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS); + + // Add an upstream without IPv6. + dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0); + inOrder.verify(mDadProxy).setUpstreamIface(null); + + // Add IPv6 to the upstream. + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(UPSTREAM_IFACE); + dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, 0); + inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS); + + // Change upstream. + // New linkproperties is needed, otherwise changing the iface has no impact. + LinkProperties lp2 = new LinkProperties(); + lp2.setInterfaceName(UPSTREAM_IFACE2); + dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp2, 0); + inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS2); + + // Lose IPv6 on the upstream... + dispatchTetherConnectionChanged(UPSTREAM_IFACE2, null, 0); + inOrder.verify(mDadProxy).setUpstreamIface(null); + + // ... and regain it on a different upstream. + dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, 0); + inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS); + + // Lose upstream. + dispatchTetherConnectionChanged(null, null, 0); + inOrder.verify(mDadProxy).setUpstreamIface(null); + + // Regain upstream. + dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, 0); + inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS); + + // Stop tethering. + mIpServer.stop(); + mLooper.dispatchAll(); + } + + private void checkDadProxyEnabled(boolean expectEnabled) throws Exception { + initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE); + InOrder inOrder = inOrder(mDadProxy); + // Add IPv6 to the upstream. + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(UPSTREAM_IFACE); + if (expectEnabled) { + inOrder.verify(mDadProxy).setUpstreamIface(UPSTREAM_IFACE_PARAMS); + } else { + inOrder.verifyNoMoreInteractions(); + } + // Stop tethering. + mIpServer.stop(); + mLooper.dispatchAll(); + if (expectEnabled) { + inOrder.verify(mDadProxy).stop(); + } + else { + verify(mDependencies, never()).getDadProxy(any(), any()); + } + } + @Test @IgnoreAfter(Build.VERSION_CODES.R) + public void testDadProxyUpdates_DisabledUpToR() throws Exception { + checkDadProxyEnabled(false); + } + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testDadProxyUpdates_EnabledAfterR() throws Exception { + checkDadProxyEnabled(true); + } +} diff --git a/Tethering/tests/unit/src/android/net/util/InterfaceSetTest.java b/Tethering/tests/unit/src/android/net/util/InterfaceSetTest.java new file mode 100644 index 0000000000..ea084b6078 --- /dev/null +++ b/Tethering/tests/unit/src/android/net/util/InterfaceSetTest.java @@ -0,0 +1,61 @@ +/* + * 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.util; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class InterfaceSetTest { + @Test + public void testNullNamesIgnored() { + final InterfaceSet set = new InterfaceSet(null, "if1", null, "if2", null); + assertEquals(2, set.ifnames.size()); + assertTrue(set.ifnames.contains("if1")); + assertTrue(set.ifnames.contains("if2")); + } + + @Test + public void testToString() { + final InterfaceSet set = new InterfaceSet("if1", "if2"); + final String setString = set.toString(); + assertTrue(setString.equals("[if1,if2]") || setString.equals("[if2,if1]")); + } + + @Test + public void testToString_Empty() { + final InterfaceSet set = new InterfaceSet(null, null); + assertEquals("[]", set.toString()); + } + + @Test + public void testEquals() { + assertEquals(new InterfaceSet(null, "if1", "if2"), new InterfaceSet("if2", "if1")); + assertEquals(new InterfaceSet(null, null), new InterfaceSet()); + assertFalse(new InterfaceSet("if1", "if3").equals(new InterfaceSet("if1", "if2"))); + assertFalse(new InterfaceSet("if1", "if2").equals(new InterfaceSet("if1"))); + assertFalse(new InterfaceSet().equals(null)); + } +} diff --git a/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java b/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java new file mode 100644 index 0000000000..e5d0b1cd38 --- /dev/null +++ b/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net.util; + +import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL; +import static android.net.TetheringManager.TETHERING_USB; +import static android.net.TetheringManager.TETHERING_WIFI; +import static android.system.OsConstants.AF_UNIX; +import static android.system.OsConstants.EAGAIN; +import static android.system.OsConstants.SOCK_CLOEXEC; +import static android.system.OsConstants.SOCK_DGRAM; +import static android.system.OsConstants.SOCK_NONBLOCK; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import android.net.LinkAddress; +import android.net.MacAddress; +import android.net.TetheringRequestParcel; +import android.system.ErrnoException; +import android.system.Os; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.net.module.util.Ipv6Utils; +import com.android.net.module.util.NetworkStackConstants; +import com.android.net.module.util.Struct; +import com.android.net.module.util.structs.EthernetHeader; +import com.android.net.module.util.structs.Icmpv6Header; +import com.android.net.module.util.structs.Ipv6Header; +import com.android.testutils.MiscAsserts; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.FileDescriptor; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.nio.ByteBuffer; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class TetheringUtilsTest { + private static final LinkAddress TEST_SERVER_ADDR = new LinkAddress("192.168.43.1/24"); + private static final LinkAddress TEST_CLIENT_ADDR = new LinkAddress("192.168.43.5/24"); + private static final int PACKET_SIZE = 1500; + + private TetheringRequestParcel mTetheringRequest; + + @Before + public void setUp() { + mTetheringRequest = makeTetheringRequestParcel(); + } + + public TetheringRequestParcel makeTetheringRequestParcel() { + final TetheringRequestParcel request = new TetheringRequestParcel(); + request.tetheringType = TETHERING_WIFI; + request.localIPv4Address = TEST_SERVER_ADDR; + request.staticClientAddress = TEST_CLIENT_ADDR; + request.exemptFromEntitlementCheck = false; + request.showProvisioningUi = true; + return request; + } + + @Test + public void testIsTetheringRequestEquals() { + TetheringRequestParcel request = makeTetheringRequestParcel(); + + assertTrue(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, mTetheringRequest)); + assertTrue(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request)); + assertTrue(TetheringUtils.isTetheringRequestEquals(null, null)); + assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, null)); + assertFalse(TetheringUtils.isTetheringRequestEquals(null, mTetheringRequest)); + + request = makeTetheringRequestParcel(); + request.tetheringType = TETHERING_USB; + assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request)); + + request = makeTetheringRequestParcel(); + request.localIPv4Address = null; + request.staticClientAddress = null; + assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request)); + + request = makeTetheringRequestParcel(); + request.exemptFromEntitlementCheck = true; + assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request)); + + request = makeTetheringRequestParcel(); + request.showProvisioningUi = false; + assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request)); + + request = makeTetheringRequestParcel(); + request.connectivityScope = CONNECTIVITY_SCOPE_LOCAL; + assertFalse(TetheringUtils.isTetheringRequestEquals(mTetheringRequest, request)); + + MiscAsserts.assertFieldCountEquals(6, TetheringRequestParcel.class); + } + + // Writes the specified packet to a filedescriptor, skipping the Ethernet header. + // Needed because the Ipv6Utils methods for building packets always include the Ethernet header, + // but socket filters applied by TetheringUtils expect the packet to start from the IP header. + private int writePacket(FileDescriptor fd, ByteBuffer pkt) throws Exception { + pkt.flip(); + int offset = Struct.getSize(EthernetHeader.class); + int len = pkt.capacity() - offset; + return Os.write(fd, pkt.array(), offset, len); + } + + // Reads a packet from the filedescriptor. + private ByteBuffer readIpPacket(FileDescriptor fd) throws Exception { + ByteBuffer buf = ByteBuffer.allocate(PACKET_SIZE); + Os.read(fd, buf); + return buf; + } + + private interface SocketFilter { + void apply(FileDescriptor fd) throws Exception; + } + + private ByteBuffer checkIcmpSocketFilter(ByteBuffer passed, ByteBuffer dropped, + SocketFilter filter) throws Exception { + FileDescriptor in = new FileDescriptor(); + FileDescriptor out = new FileDescriptor(); + Os.socketpair(AF_UNIX, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, in, out); + + // Before the filter is applied, it doesn't drop anything. + int len = writePacket(out, dropped); + ByteBuffer received = readIpPacket(in); + assertEquals(len, received.position()); + + // Install the socket filter. Then write two packets, the first expected to be dropped and + // the second expected to be passed. Check that only the second makes it through. + filter.apply(in); + writePacket(out, dropped); + len = writePacket(out, passed); + received = readIpPacket(in); + assertEquals(len, received.position()); + received.flip(); + + // Check there are no more packets to read. + try { + readIpPacket(in); + } catch (ErrnoException expected) { + assertEquals(EAGAIN, expected.errno); + } + + return received; + } + + @Test + public void testIcmpSocketFilters() throws Exception { + MacAddress mac1 = MacAddress.fromString("11:22:33:44:55:66"); + MacAddress mac2 = MacAddress.fromString("aa:bb:cc:dd:ee:ff"); + Inet6Address ll1 = (Inet6Address) InetAddress.getByName("fe80::1"); + Inet6Address ll2 = (Inet6Address) InetAddress.getByName("fe80::abcd"); + Inet6Address allRouters = NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST; + + final ByteBuffer na = Ipv6Utils.buildNaPacket(mac1, mac2, ll1, ll2, 0, ll1); + final ByteBuffer ns = Ipv6Utils.buildNsPacket(mac1, mac2, ll1, ll2, ll1); + final ByteBuffer rs = Ipv6Utils.buildRsPacket(mac1, mac2, ll1, allRouters); + + ByteBuffer received = checkIcmpSocketFilter(na /* passed */, rs /* dropped */, + TetheringUtils::setupNaSocket); + + Struct.parse(Ipv6Header.class, received); // Skip IPv6 header. + Icmpv6Header icmpv6 = Struct.parse(Icmpv6Header.class, received); + assertEquals(NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT, icmpv6.type); + + received = checkIcmpSocketFilter(ns /* passed */, rs /* dropped */, + TetheringUtils::setupNsSocket); + + Struct.parse(Ipv6Header.class, received); // Skip IPv6 header. + icmpv6 = Struct.parse(Icmpv6Header.class, received); + assertEquals(NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION, icmpv6.type); + } +} diff --git a/Tethering/tests/unit/src/android/net/util/VersionedBroadcastListenerTest.java b/Tethering/tests/unit/src/android/net/util/VersionedBroadcastListenerTest.java new file mode 100644 index 0000000000..5a9b6e380e --- /dev/null +++ b/Tethering/tests/unit/src/android/net/util/VersionedBroadcastListenerTest.java @@ -0,0 +1,131 @@ +/* + * 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.util; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.reset; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.test.BroadcastInterceptingContext; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VersionedBroadcastListenerTest { + private static final String TAG = VersionedBroadcastListenerTest.class.getSimpleName(); + private static final String ACTION_TEST = "action.test.happy.broadcasts"; + + @Mock private Context mContext; + private BroadcastInterceptingContext mServiceContext; + private Handler mHandler; + private VersionedBroadcastListener mListener; + private int mCallbackCount; + + private void doCallback() { + mCallbackCount++; + } + + private class MockContext extends BroadcastInterceptingContext { + MockContext(Context base) { + super(base); + } + } + + @BeforeClass + public static void setUpBeforeClass() throws Exception { + if (Looper.myLooper() == null) { + Looper.prepare(); + } + } + + @Before public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + reset(mContext); + mServiceContext = new MockContext(mContext); + mHandler = new Handler(Looper.myLooper()); + mCallbackCount = 0; + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_TEST); + mListener = new VersionedBroadcastListener( + TAG, mServiceContext, mHandler, filter, (Intent intent) -> doCallback()); + } + + @After public void tearDown() throws Exception { + if (mListener != null) { + mListener.stopListening(); + mListener = null; + } + } + + private void sendBroadcast() { + final Intent intent = new Intent(ACTION_TEST); + mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + } + + @Test + public void testBasicListening() { + assertEquals(0, mCallbackCount); + mListener.startListening(); + for (int i = 0; i < 5; i++) { + sendBroadcast(); + assertEquals(i + 1, mCallbackCount); + } + mListener.stopListening(); + } + + @Test + public void testBroadcastsBeforeStartAreIgnored() { + assertEquals(0, mCallbackCount); + for (int i = 0; i < 5; i++) { + sendBroadcast(); + assertEquals(0, mCallbackCount); + } + + mListener.startListening(); + sendBroadcast(); + assertEquals(1, mCallbackCount); + } + + @Test + public void testBroadcastsAfterStopAreIgnored() { + mListener.startListening(); + sendBroadcast(); + assertEquals(1, mCallbackCount); + mListener.stopListening(); + + for (int i = 0; i < 5; i++) { + sendBroadcast(); + assertEquals(1, mCallbackCount); + } + } +} diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java new file mode 100644 index 0000000000..cc912f4dba --- /dev/null +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java @@ -0,0 +1,1497 @@ +/* + * 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.networkstack.tethering; + +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.ROAMING_NO; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; +import static android.net.NetworkStats.UID_TETHERING; +import static android.net.ip.ConntrackMonitor.ConntrackEvent; +import static android.net.netlink.ConntrackMessage.DYING_MASK; +import static android.net.netlink.ConntrackMessage.ESTABLISHED_MASK; +import static android.net.netlink.ConntrackMessage.Tuple; +import static android.net.netlink.ConntrackMessage.TupleIpv4; +import static android.net.netlink.ConntrackMessage.TupleProto; +import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_DELETE; +import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_NEW; +import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED; +import static android.system.OsConstants.ETH_P_IP; +import static android.system.OsConstants.ETH_P_IPV6; +import static android.system.OsConstants.IPPROTO_TCP; +import static android.system.OsConstants.IPPROTO_UDP; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker; +import static com.android.networkstack.tethering.BpfCoordinator.StatsType; +import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE; +import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_UID; +import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM; +import static com.android.networkstack.tethering.BpfUtils.UPSTREAM; +import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS; + +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.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.usage.NetworkStatsManager; +import android.net.INetd; +import android.net.InetAddresses; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.MacAddress; +import android.net.NetworkStats; +import android.net.TetherOffloadRuleParcel; +import android.net.TetherStatsParcel; +import android.net.ip.ConntrackMonitor; +import android.net.ip.ConntrackMonitor.ConntrackEventConsumer; +import android.net.ip.IpServer; +import android.net.netlink.NetlinkConstants; +import android.net.util.InterfaceParams; +import android.net.util.SharedLog; +import android.os.Build; +import android.os.Handler; +import android.os.test.TestLooper; +import android.system.ErrnoException; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.net.module.util.NetworkStackConstants; +import com.android.net.module.util.Struct; +import com.android.networkstack.tethering.BpfCoordinator.BpfConntrackEventConsumer; +import com.android.networkstack.tethering.BpfCoordinator.ClientInfo; +import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; +import com.android.testutils.TestableNetworkStatsProviderCbBinder; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class BpfCoordinatorTest { + @Rule + public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(); + + private static final int UPSTREAM_IFINDEX = 1001; + private static final int DOWNSTREAM_IFINDEX = 1002; + + private static final String UPSTREAM_IFACE = "rmnet0"; + + private static final MacAddress DOWNSTREAM_MAC = MacAddress.fromString("12:34:56:78:90:ab"); + private static final MacAddress MAC_A = MacAddress.fromString("00:00:00:00:00:0a"); + private static final MacAddress MAC_B = MacAddress.fromString("11:22:33:00:00:0b"); + + private static final InetAddress NEIGH_A = InetAddresses.parseNumericAddress("2001:db8::1"); + private static final InetAddress NEIGH_B = InetAddresses.parseNumericAddress("2001:db8::2"); + + private static final InterfaceParams UPSTREAM_IFACE_PARAMS = new InterfaceParams( + UPSTREAM_IFACE, UPSTREAM_IFINDEX, null /* macAddr, rawip */, + NetworkStackConstants.ETHER_MTU); + + // The test fake BPF map class is needed because the test has no privilege to access the BPF + // map. All member functions which eventually call JNI to access the real native BPF map need + // to be overridden. + // TODO: consider moving to an individual file. + private class TestBpfMap extends BpfMap { + private final HashMap mMap = new HashMap(); + + TestBpfMap(final Class key, final Class value) { + super(key, value); + } + + @Override + public void forEach(BiConsumer action) throws ErrnoException { + // TODO: consider using mocked #getFirstKey and #getNextKey to iterate. It helps to + // implement the entry deletion in the iteration if required. + for (Map.Entry entry : mMap.entrySet()) { + action.accept(entry.getKey(), entry.getValue()); + } + } + + @Override + public void updateEntry(K key, V value) throws ErrnoException { + mMap.put(key, value); + } + + @Override + public void insertEntry(K key, V value) throws ErrnoException, + IllegalArgumentException { + // The entry is created if and only if it doesn't exist. See BpfMap#insertEntry. + if (mMap.get(key) != null) { + throw new IllegalArgumentException(key + " already exist"); + } + mMap.put(key, value); + } + + @Override + public boolean deleteEntry(Struct key) throws ErrnoException { + return mMap.remove(key) != null; + } + + @Override + public V getValue(@NonNull K key) throws ErrnoException { + // Return value for a given key. Otherwise, return null without an error ENOENT. + // BpfMap#getValue treats that the entry is not found as no error. + return mMap.get(key); + } + + @Override + public void clear() throws ErrnoException { + // TODO: consider using mocked #getFirstKey and #deleteEntry to implement. + mMap.clear(); + } + }; + + @Mock private NetworkStatsManager mStatsManager; + @Mock private INetd mNetd; + @Mock private IpServer mIpServer; + @Mock private IpServer mIpServer2; + @Mock private TetheringConfiguration mTetherConfig; + @Mock private ConntrackMonitor mConntrackMonitor; + @Mock private BpfMap mBpfDownstream4Map; + @Mock private BpfMap mBpfUpstream4Map; + @Mock private BpfMap mBpfDownstream6Map; + @Mock private BpfMap mBpfUpstream6Map; + @Mock private BpfMap mBpfDevMap; + + // Late init since methods must be called by the thread that created this object. + private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb; + private BpfCoordinator.BpfTetherStatsProvider mTetherStatsProvider; + + // Late init since the object must be initialized by the BPF coordinator instance because + // it has to access the non-static function of BPF coordinator. + private BpfConntrackEventConsumer mConsumer; + + private final ArgumentCaptor mStringArrayCaptor = + ArgumentCaptor.forClass(ArrayList.class); + private final TestLooper mTestLooper = new TestLooper(); + private final TestBpfMap mBpfStatsMap = + spy(new TestBpfMap<>(TetherStatsKey.class, TetherStatsValue.class)); + private final TestBpfMap mBpfLimitMap = + spy(new TestBpfMap<>(TetherLimitKey.class, TetherLimitValue.class)); + private BpfCoordinator.Dependencies mDeps = + spy(new BpfCoordinator.Dependencies() { + @NonNull + public Handler getHandler() { + return new Handler(mTestLooper.getLooper()); + } + + @NonNull + public INetd getNetd() { + return mNetd; + } + + @NonNull + public NetworkStatsManager getNetworkStatsManager() { + return mStatsManager; + } + + @NonNull + public SharedLog getSharedLog() { + return new SharedLog("test"); + } + + @Nullable + public TetheringConfiguration getTetherConfig() { + return mTetherConfig; + } + + @NonNull + public ConntrackMonitor getConntrackMonitor(ConntrackEventConsumer consumer) { + return mConntrackMonitor; + } + + @Nullable + public BpfMap getBpfDownstream4Map() { + return mBpfDownstream4Map; + } + + @Nullable + public BpfMap getBpfUpstream4Map() { + return mBpfUpstream4Map; + } + + @Nullable + public BpfMap getBpfDownstream6Map() { + return mBpfDownstream6Map; + } + + @Nullable + public BpfMap getBpfUpstream6Map() { + return mBpfUpstream6Map; + } + + @Nullable + public BpfMap getBpfStatsMap() { + return mBpfStatsMap; + } + + @Nullable + public BpfMap getBpfLimitMap() { + return mBpfLimitMap; + } + + @Nullable + public BpfMap getBpfDevMap() { + return mBpfDevMap; + } + }); + + @Before public void setUp() { + MockitoAnnotations.initMocks(this); + when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(true /* default value */); + } + + private void waitForIdle() { + mTestLooper.dispatchAll(); + } + + // TODO: Remove unnecessary calling on R because the BPF map accessing has been moved into + // module. + private void setupFunctioningNetdInterface() throws Exception { + when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]); + } + + @NonNull + private BpfCoordinator makeBpfCoordinator() throws Exception { + final BpfCoordinator coordinator = new BpfCoordinator(mDeps); + + mConsumer = coordinator.getBpfConntrackEventConsumerForTesting(); + final ArgumentCaptor + tetherStatsProviderCaptor = + ArgumentCaptor.forClass(BpfCoordinator.BpfTetherStatsProvider.class); + verify(mStatsManager).registerNetworkStatsProvider(anyString(), + tetherStatsProviderCaptor.capture()); + mTetherStatsProvider = tetherStatsProviderCaptor.getValue(); + assertNotNull(mTetherStatsProvider); + mTetherStatsProviderCb = new TestableNetworkStatsProviderCbBinder(); + mTetherStatsProvider.setProviderCallbackBinder(mTetherStatsProviderCb); + + return coordinator; + } + + @NonNull + private static NetworkStats.Entry buildTestEntry(@NonNull StatsType how, + @NonNull String iface, long rxBytes, long rxPackets, long txBytes, long txPackets) { + return new NetworkStats.Entry(iface, how == STATS_PER_IFACE ? UID_ALL : UID_TETHERING, + SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes, + rxPackets, txBytes, txPackets, 0L); + } + + @NonNull + private static TetherStatsParcel buildTestTetherStatsParcel(@NonNull Integer ifIndex, + long rxBytes, long rxPackets, long txBytes, long txPackets) { + final TetherStatsParcel parcel = new TetherStatsParcel(); + parcel.ifIndex = ifIndex; + parcel.rxBytes = rxBytes; + parcel.rxPackets = rxPackets; + parcel.txBytes = txBytes; + parcel.txPackets = txPackets; + return parcel; + } + + // Update a stats entry or create if not exists. + private void updateStatsEntryToStatsMap(@NonNull TetherStatsParcel stats) throws Exception { + final TetherStatsKey key = new TetherStatsKey(stats.ifIndex); + final TetherStatsValue value = new TetherStatsValue(stats.rxPackets, stats.rxBytes, + 0L /* rxErrors */, stats.txPackets, stats.txBytes, 0L /* txErrors */); + mBpfStatsMap.updateEntry(key, value); + } + + private void updateStatsEntry(@NonNull TetherStatsParcel stats) throws Exception { + if (mDeps.isAtLeastS()) { + updateStatsEntryToStatsMap(stats); + } else { + when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[] {stats}); + } + } + + // Update specific tether stats list and wait for the stats cache is updated by polling thread + // in the coordinator. Beware of that it is only used for the default polling interval. + // Note that the mocked tetherOffloadGetStats of netd replaces all stats entries because it + // doesn't store the previous entries. + private void updateStatsEntriesAndWaitForUpdate(@NonNull TetherStatsParcel[] tetherStatsList) + throws Exception { + if (mDeps.isAtLeastS()) { + for (TetherStatsParcel stats : tetherStatsList) { + updateStatsEntry(stats); + } + } else { + when(mNetd.tetherOffloadGetStats()).thenReturn(tetherStatsList); + } + + mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); + waitForIdle(); + } + + // In tests, the stats need to be set before deleting the last rule. + // The reason is that BpfCoordinator#tetherOffloadRuleRemove reads the stats + // of the deleting interface after the last rule deleted. #tetherOffloadRuleRemove + // does the interface cleanup failed if there is no stats for the deleting interface. + // Note that the mocked tetherOffloadGetAndClearStats of netd replaces all stats entries + // because it doesn't store the previous entries. + private void updateStatsEntryForTetherOffloadGetAndClearStats(TetherStatsParcel stats) + throws Exception { + if (mDeps.isAtLeastS()) { + updateStatsEntryToStatsMap(stats); + } else { + when(mNetd.tetherOffloadGetAndClearStats(stats.ifIndex)).thenReturn(stats); + } + } + + private void clearStatsInvocations() { + if (mDeps.isAtLeastS()) { + clearInvocations(mBpfStatsMap); + } else { + clearInvocations(mNetd); + } + } + + private T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) { + if (inOrder != null) { + return inOrder.verify(t); + } else { + return verify(t); + } + } + + private void verifyTetherOffloadGetStats() throws Exception { + if (mDeps.isAtLeastS()) { + verify(mBpfStatsMap).forEach(any()); + } else { + verify(mNetd).tetherOffloadGetStats(); + } + } + + private void verifyNeverTetherOffloadGetStats() throws Exception { + if (mDeps.isAtLeastS()) { + verify(mBpfStatsMap, never()).forEach(any()); + } else { + verify(mNetd, never()).tetherOffloadGetStats(); + } + } + + private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int downstreamIfIndex, + MacAddress downstreamMac, int upstreamIfindex) throws Exception { + if (!mDeps.isAtLeastS()) return; + final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfIndex, downstreamMac); + final Tether6Value value = new Tether6Value(upstreamIfindex, + MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS, + ETH_P_IPV6, NetworkStackConstants.ETHER_MTU); + verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(key, value); + } + + private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int downstreamIfIndex, + MacAddress downstreamMac) + throws Exception { + if (!mDeps.isAtLeastS()) return; + final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfIndex, downstreamMac); + verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(key); + } + + private void verifyNoUpstreamIpv6ForwardingChange(@Nullable InOrder inOrder) throws Exception { + if (!mDeps.isAtLeastS()) return; + if (inOrder != null) { + inOrder.verify(mBpfUpstream6Map, never()).deleteEntry(any()); + inOrder.verify(mBpfUpstream6Map, never()).insertEntry(any(), any()); + inOrder.verify(mBpfUpstream6Map, never()).updateEntry(any(), any()); + } else { + verify(mBpfUpstream6Map, never()).deleteEntry(any()); + verify(mBpfUpstream6Map, never()).insertEntry(any(), any()); + verify(mBpfUpstream6Map, never()).updateEntry(any(), any()); + } + } + + private void verifyTetherOffloadRuleAdd(@Nullable InOrder inOrder, + @NonNull Ipv6ForwardingRule rule) throws Exception { + if (mDeps.isAtLeastS()) { + verifyWithOrder(inOrder, mBpfDownstream6Map).updateEntry( + rule.makeTetherDownstream6Key(), rule.makeTether6Value()); + } else { + verifyWithOrder(inOrder, mNetd).tetherOffloadRuleAdd(matches(rule)); + } + } + + private void verifyNeverTetherOffloadRuleAdd() throws Exception { + if (mDeps.isAtLeastS()) { + verify(mBpfDownstream6Map, never()).updateEntry(any(), any()); + } else { + verify(mNetd, never()).tetherOffloadRuleAdd(any()); + } + } + + private void verifyTetherOffloadRuleRemove(@Nullable InOrder inOrder, + @NonNull final Ipv6ForwardingRule rule) throws Exception { + if (mDeps.isAtLeastS()) { + verifyWithOrder(inOrder, mBpfDownstream6Map).deleteEntry( + rule.makeTetherDownstream6Key()); + } else { + verifyWithOrder(inOrder, mNetd).tetherOffloadRuleRemove(matches(rule)); + } + } + + private void verifyNeverTetherOffloadRuleRemove() throws Exception { + if (mDeps.isAtLeastS()) { + verify(mBpfDownstream6Map, never()).deleteEntry(any()); + } else { + verify(mNetd, never()).tetherOffloadRuleRemove(any()); + } + } + + private void verifyTetherOffloadSetInterfaceQuota(@Nullable InOrder inOrder, int ifIndex, + long quotaBytes, boolean isInit) throws Exception { + if (mDeps.isAtLeastS()) { + final TetherStatsKey key = new TetherStatsKey(ifIndex); + verifyWithOrder(inOrder, mBpfStatsMap).getValue(key); + if (isInit) { + verifyWithOrder(inOrder, mBpfStatsMap).insertEntry(key, new TetherStatsValue( + 0L /* rxPackets */, 0L /* rxBytes */, 0L /* rxErrors */, + 0L /* txPackets */, 0L /* txBytes */, 0L /* txErrors */)); + } + verifyWithOrder(inOrder, mBpfLimitMap).updateEntry(new TetherLimitKey(ifIndex), + new TetherLimitValue(quotaBytes)); + } else { + verifyWithOrder(inOrder, mNetd).tetherOffloadSetInterfaceQuota(ifIndex, quotaBytes); + } + } + + private void verifyNeverTetherOffloadSetInterfaceQuota(@NonNull InOrder inOrder) + throws Exception { + if (mDeps.isAtLeastS()) { + inOrder.verify(mBpfStatsMap, never()).getValue(any()); + inOrder.verify(mBpfStatsMap, never()).insertEntry(any(), any()); + inOrder.verify(mBpfLimitMap, never()).updateEntry(any(), any()); + } else { + inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong()); + } + } + + private void verifyTetherOffloadGetAndClearStats(@NonNull InOrder inOrder, int ifIndex) + throws Exception { + if (mDeps.isAtLeastS()) { + inOrder.verify(mBpfStatsMap).getValue(new TetherStatsKey(ifIndex)); + inOrder.verify(mBpfStatsMap).deleteEntry(new TetherStatsKey(ifIndex)); + inOrder.verify(mBpfLimitMap).deleteEntry(new TetherLimitKey(ifIndex)); + } else { + inOrder.verify(mNetd).tetherOffloadGetAndClearStats(ifIndex); + } + } + + // S+ and R api minimum tests. + // The following tests are used to provide minimum checking for the APIs on different flow. + // The auto merge is not enabled on mainline prod. The code flow R may be verified at the + // late stage by manual cherry pick. It is risky if the R code flow has broken and be found at + // the last minute. + // TODO: remove once presubmit tests on R even the code is submitted on S. + private void checkTetherOffloadRuleAddAndRemove(boolean usingApiS) throws Exception { + setupFunctioningNetdInterface(); + + // Replace Dependencies#isAtLeastS() for testing R and S+ BPF map apis. Note that |mDeps| + // must be mocked before calling #makeBpfCoordinator which use |mDeps| to initialize the + // coordinator. + doReturn(usingApiS).when(mDeps).isAtLeastS(); + final BpfCoordinator coordinator = makeBpfCoordinator(); + + final String mobileIface = "rmnet_data0"; + final Integer mobileIfIndex = 100; + coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface); + + // InOrder is required because mBpfStatsMap may be accessed by both + // BpfCoordinator#tetherOffloadRuleAdd and BpfCoordinator#tetherOffloadGetAndClearStats. + // The #verifyTetherOffloadGetAndClearStats can't distinguish who has ever called + // mBpfStatsMap#getValue and get a wrong calling count which counts all. + final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap); + final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A); + coordinator.tetherOffloadRuleAdd(mIpServer, rule); + verifyTetherOffloadRuleAdd(inOrder, rule); + verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED, + true /* isInit */); + + // Removing the last rule on current upstream immediately sends the cleanup stuff to netd. + updateStatsEntryForTetherOffloadGetAndClearStats( + buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0)); + coordinator.tetherOffloadRuleRemove(mIpServer, rule); + verifyTetherOffloadRuleRemove(inOrder, rule); + verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex); + } + + // TODO: remove once presubmit tests on R even the code is submitted on S. + @Test + public void testTetherOffloadRuleAddAndRemoveSdkR() throws Exception { + checkTetherOffloadRuleAddAndRemove(false /* R */); + } + + // TODO: remove once presubmit tests on R even the code is submitted on S. + @Test + public void testTetherOffloadRuleAddAndRemoveAtLeastSdkS() throws Exception { + checkTetherOffloadRuleAddAndRemove(true /* S+ */); + } + + // TODO: remove once presubmit tests on R even the code is submitted on S. + private void checkTetherOffloadGetStats(boolean usingApiS) throws Exception { + setupFunctioningNetdInterface(); + + doReturn(usingApiS).when(mDeps).isAtLeastS(); + final BpfCoordinator coordinator = makeBpfCoordinator(); + coordinator.startPolling(); + + final String mobileIface = "rmnet_data0"; + final Integer mobileIfIndex = 100; + coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface); + + updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] { + buildTestTetherStatsParcel(mobileIfIndex, 1000, 100, 2000, 200)}); + + final NetworkStats expectedIfaceStats = new NetworkStats(0L, 1) + .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 1000, 100, 2000, 200)); + + final NetworkStats expectedUidStats = new NetworkStats(0L, 1) + .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 1000, 100, 2000, 200)); + + mTetherStatsProvider.pushTetherStats(); + mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStats, expectedUidStats); + } + + // TODO: remove once presubmit tests on R even the code is submitted on S. + @Test + public void testTetherOffloadGetStatsSdkR() throws Exception { + checkTetherOffloadGetStats(false /* R */); + } + + // TODO: remove once presubmit tests on R even the code is submitted on S. + @Test + public void testTetherOffloadGetStatsAtLeastSdkS() throws Exception { + checkTetherOffloadGetStats(true /* S+ */); + } + + @Test + public void testGetForwardedStats() throws Exception { + setupFunctioningNetdInterface(); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + coordinator.startPolling(); + + final String wlanIface = "wlan0"; + final Integer wlanIfIndex = 100; + final String mobileIface = "rmnet_data0"; + final Integer mobileIfIndex = 101; + + // Add interface name to lookup table. In realistic case, the upstream interface name will + // be added by IpServer when IpServer has received with a new IPv6 upstream update event. + coordinator.addUpstreamNameToLookupTable(wlanIfIndex, wlanIface); + coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface); + + // [1] Both interface stats are changed. + // Setup the tether stats of wlan and mobile interface. Note that move forward the time of + // the looper to make sure the new tether stats has been updated by polling update thread. + updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] { + buildTestTetherStatsParcel(wlanIfIndex, 1000, 100, 2000, 200), + buildTestTetherStatsParcel(mobileIfIndex, 3000, 300, 4000, 400)}); + + final NetworkStats expectedIfaceStats = new NetworkStats(0L, 2) + .addEntry(buildTestEntry(STATS_PER_IFACE, wlanIface, 1000, 100, 2000, 200)) + .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 3000, 300, 4000, 400)); + + final NetworkStats expectedUidStats = new NetworkStats(0L, 2) + .addEntry(buildTestEntry(STATS_PER_UID, wlanIface, 1000, 100, 2000, 200)) + .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 3000, 300, 4000, 400)); + + // Force pushing stats update to verify the stats reported. + // TODO: Perhaps make #expectNotifyStatsUpdated to use test TetherStatsParcel object for + // verifying the notification. + mTetherStatsProvider.pushTetherStats(); + mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStats, expectedUidStats); + + // [2] Only one interface stats is changed. + // The tether stats of mobile interface is accumulated and The tether stats of wlan + // interface is the same. + updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] { + buildTestTetherStatsParcel(wlanIfIndex, 1000, 100, 2000, 200), + buildTestTetherStatsParcel(mobileIfIndex, 3010, 320, 4030, 440)}); + + final NetworkStats expectedIfaceStatsDiff = new NetworkStats(0L, 2) + .addEntry(buildTestEntry(STATS_PER_IFACE, wlanIface, 0, 0, 0, 0)) + .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 10, 20, 30, 40)); + + final NetworkStats expectedUidStatsDiff = new NetworkStats(0L, 2) + .addEntry(buildTestEntry(STATS_PER_UID, wlanIface, 0, 0, 0, 0)) + .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 10, 20, 30, 40)); + + // Force pushing stats update to verify that only diff of stats is reported. + mTetherStatsProvider.pushTetherStats(); + mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStatsDiff, + expectedUidStatsDiff); + + // [3] Stop coordinator. + // Shutdown the coordinator and clear the invocation history, especially the + // tetherOffloadGetStats() calls. + coordinator.stopPolling(); + clearStatsInvocations(); + + // Verify the polling update thread stopped. + mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); + waitForIdle(); + verifyNeverTetherOffloadGetStats(); + } + + @Test + public void testOnSetAlert() throws Exception { + setupFunctioningNetdInterface(); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + coordinator.startPolling(); + + final String mobileIface = "rmnet_data0"; + final Integer mobileIfIndex = 100; + coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface); + + // Verify that set quota to 0 will immediately triggers a callback. + mTetherStatsProvider.onSetAlert(0); + waitForIdle(); + mTetherStatsProviderCb.expectNotifyAlertReached(); + + // Verify that notifyAlertReached never fired if quota is not yet reached. + updateStatsEntry(buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0)); + mTetherStatsProvider.onSetAlert(100); + mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); + waitForIdle(); + mTetherStatsProviderCb.assertNoCallback(); + + // Verify that notifyAlertReached fired when quota is reached. + updateStatsEntry(buildTestTetherStatsParcel(mobileIfIndex, 50, 0, 50, 0)); + mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); + waitForIdle(); + mTetherStatsProviderCb.expectNotifyAlertReached(); + + // Verify that set quota with UNLIMITED won't trigger any callback. + mTetherStatsProvider.onSetAlert(QUOTA_UNLIMITED); + mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); + waitForIdle(); + mTetherStatsProviderCb.assertNoCallback(); + } + + // The custom ArgumentMatcher simply comes from IpServerTest. + // TODO: move both of them into a common utility class for reusing the code. + private static class TetherOffloadRuleParcelMatcher implements + ArgumentMatcher { + public final int upstreamIfindex; + public final int downstreamIfindex; + public final Inet6Address address; + public final MacAddress srcMac; + public final MacAddress dstMac; + + TetherOffloadRuleParcelMatcher(@NonNull Ipv6ForwardingRule rule) { + upstreamIfindex = rule.upstreamIfindex; + downstreamIfindex = rule.downstreamIfindex; + address = rule.address; + srcMac = rule.srcMac; + dstMac = rule.dstMac; + } + + public boolean matches(@NonNull TetherOffloadRuleParcel parcel) { + return upstreamIfindex == parcel.inputInterfaceIndex + && (downstreamIfindex == parcel.outputInterfaceIndex) + && Arrays.equals(address.getAddress(), parcel.destination) + && (128 == parcel.prefixLength) + && Arrays.equals(srcMac.toByteArray(), parcel.srcL2Address) + && Arrays.equals(dstMac.toByteArray(), parcel.dstL2Address); + } + + public String toString() { + return String.format("TetherOffloadRuleParcelMatcher(%d, %d, %s, %s, %s", + upstreamIfindex, downstreamIfindex, address.getHostAddress(), srcMac, dstMac); + } + } + + @NonNull + private TetherOffloadRuleParcel matches(@NonNull Ipv6ForwardingRule rule) { + return argThat(new TetherOffloadRuleParcelMatcher(rule)); + } + + @NonNull + private static Ipv6ForwardingRule buildTestForwardingRule( + int upstreamIfindex, @NonNull InetAddress address, @NonNull MacAddress dstMac) { + return new Ipv6ForwardingRule(upstreamIfindex, DOWNSTREAM_IFINDEX, (Inet6Address) address, + DOWNSTREAM_MAC, dstMac); + } + + @Test + public void testRuleMakeTetherDownstream6Key() throws Exception { + final Integer mobileIfIndex = 100; + final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A); + + final TetherDownstream6Key key = rule.makeTetherDownstream6Key(); + assertEquals(key.iif, (long) mobileIfIndex); + assertEquals(key.dstMac, MacAddress.ALL_ZEROS_ADDRESS); // rawip upstream + assertTrue(Arrays.equals(key.neigh6, NEIGH_A.getAddress())); + // iif (4) + dstMac(6) + padding(2) + neigh6 (16) = 28. + assertEquals(28, key.writeToBytes().length); + } + + @Test + public void testRuleMakeTether6Value() throws Exception { + final Integer mobileIfIndex = 100; + final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A); + + final Tether6Value value = rule.makeTether6Value(); + assertEquals(value.oif, DOWNSTREAM_IFINDEX); + assertEquals(value.ethDstMac, MAC_A); + assertEquals(value.ethSrcMac, DOWNSTREAM_MAC); + assertEquals(value.ethProto, ETH_P_IPV6); + assertEquals(value.pmtu, NetworkStackConstants.ETHER_MTU); + // oif (4) + ethDstMac (6) + ethSrcMac (6) + ethProto (2) + pmtu (2) = 20. + assertEquals(20, value.writeToBytes().length); + } + + @Test + public void testSetDataLimit() throws Exception { + setupFunctioningNetdInterface(); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + + final String mobileIface = "rmnet_data0"; + final Integer mobileIfIndex = 100; + coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface); + + // [1] Default limit. + // Set the unlimited quota as default if the service has never applied a data limit for a + // given upstream. Note that the data limit only be applied on an upstream which has rules. + final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A); + final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap); + coordinator.tetherOffloadRuleAdd(mIpServer, rule); + verifyTetherOffloadRuleAdd(inOrder, rule); + verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED, + true /* isInit */); + inOrder.verifyNoMoreInteractions(); + + // [2] Specific limit. + // Applying the data limit boundary {min, 1gb, max, infinity} on current upstream. + for (final long quota : new long[] {0, 1048576000, Long.MAX_VALUE, QUOTA_UNLIMITED}) { + mTetherStatsProvider.onSetLimit(mobileIface, quota); + waitForIdle(); + verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, quota, + false /* isInit */); + inOrder.verifyNoMoreInteractions(); + } + + // [3] Invalid limit. + // The valid range of quota is 0..max_int64 or -1 (unlimited). + final long invalidLimit = Long.MIN_VALUE; + try { + mTetherStatsProvider.onSetLimit(mobileIface, invalidLimit); + waitForIdle(); + fail("No exception thrown for invalid limit " + invalidLimit + "."); + } catch (IllegalArgumentException expected) { + assertEquals(expected.getMessage(), "invalid quota value " + invalidLimit); + } + } + + // TODO: Test the case in which the rules are changed from different IpServer objects. + @Test + public void testSetDataLimitOnRule6Change() throws Exception { + setupFunctioningNetdInterface(); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + + final String mobileIface = "rmnet_data0"; + final Integer mobileIfIndex = 100; + coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface); + + // Applying a data limit to the current upstream does not take any immediate action. + // The data limit could be only set on an upstream which has rules. + final long limit = 12345; + final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap); + mTetherStatsProvider.onSetLimit(mobileIface, limit); + waitForIdle(); + verifyNeverTetherOffloadSetInterfaceQuota(inOrder); + + // Adding the first rule on current upstream immediately sends the quota to netd. + final Ipv6ForwardingRule ruleA = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A); + coordinator.tetherOffloadRuleAdd(mIpServer, ruleA); + verifyTetherOffloadRuleAdd(inOrder, ruleA); + verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, limit, true /* isInit */); + inOrder.verifyNoMoreInteractions(); + + // Adding the second rule on current upstream does not send the quota to netd. + final Ipv6ForwardingRule ruleB = buildTestForwardingRule(mobileIfIndex, NEIGH_B, MAC_B); + coordinator.tetherOffloadRuleAdd(mIpServer, ruleB); + verifyTetherOffloadRuleAdd(inOrder, ruleB); + verifyNeverTetherOffloadSetInterfaceQuota(inOrder); + + // Removing the second rule on current upstream does not send the quota to netd. + coordinator.tetherOffloadRuleRemove(mIpServer, ruleB); + verifyTetherOffloadRuleRemove(inOrder, ruleB); + verifyNeverTetherOffloadSetInterfaceQuota(inOrder); + + // Removing the last rule on current upstream immediately sends the cleanup stuff to netd. + updateStatsEntryForTetherOffloadGetAndClearStats( + buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0)); + coordinator.tetherOffloadRuleRemove(mIpServer, ruleA); + verifyTetherOffloadRuleRemove(inOrder, ruleA); + verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testTetherOffloadRuleUpdateAndClear() throws Exception { + setupFunctioningNetdInterface(); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + + final String ethIface = "eth1"; + final String mobileIface = "rmnet_data0"; + final Integer ethIfIndex = 100; + final Integer mobileIfIndex = 101; + coordinator.addUpstreamNameToLookupTable(ethIfIndex, ethIface); + coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface); + + final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map, mBpfLimitMap, + mBpfStatsMap); + + // Before the rule test, here are the additional actions while the rules are changed. + // - After adding the first rule on a given upstream, the coordinator adds a data limit. + // If the service has never applied the data limit, set an unlimited quota as default. + // - After removing the last rule on a given upstream, the coordinator gets the last stats. + // Then, it clears the stats and the limit entry from BPF maps. + // See tetherOffloadRule{Add, Remove, Clear, Clean}. + + // [1] Adding rules on the upstream Ethernet. + // Note that the default data limit is applied after the first rule is added. + final Ipv6ForwardingRule ethernetRuleA = buildTestForwardingRule( + ethIfIndex, NEIGH_A, MAC_A); + final Ipv6ForwardingRule ethernetRuleB = buildTestForwardingRule( + ethIfIndex, NEIGH_B, MAC_B); + + coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleA); + verifyTetherOffloadRuleAdd(inOrder, ethernetRuleA); + verifyTetherOffloadSetInterfaceQuota(inOrder, ethIfIndex, QUOTA_UNLIMITED, + true /* isInit */); + verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, ethIfIndex); + coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleB); + verifyTetherOffloadRuleAdd(inOrder, ethernetRuleB); + + // [2] Update the existing rules from Ethernet to cellular. + final Ipv6ForwardingRule mobileRuleA = buildTestForwardingRule( + mobileIfIndex, NEIGH_A, MAC_A); + final Ipv6ForwardingRule mobileRuleB = buildTestForwardingRule( + mobileIfIndex, NEIGH_B, MAC_B); + updateStatsEntryForTetherOffloadGetAndClearStats( + buildTestTetherStatsParcel(ethIfIndex, 10, 20, 30, 40)); + + // Update the existing rules for upstream changes. The rules are removed and re-added one + // by one for updating upstream interface index by #tetherOffloadRuleUpdate. + coordinator.tetherOffloadRuleUpdate(mIpServer, mobileIfIndex); + verifyTetherOffloadRuleRemove(inOrder, ethernetRuleA); + verifyTetherOffloadRuleRemove(inOrder, ethernetRuleB); + verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC); + verifyTetherOffloadGetAndClearStats(inOrder, ethIfIndex); + verifyTetherOffloadRuleAdd(inOrder, mobileRuleA); + verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED, + true /* isInit */); + verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, + mobileIfIndex); + verifyTetherOffloadRuleAdd(inOrder, mobileRuleB); + + // [3] Clear all rules for a given IpServer. + updateStatsEntryForTetherOffloadGetAndClearStats( + buildTestTetherStatsParcel(mobileIfIndex, 50, 60, 70, 80)); + coordinator.tetherOffloadRuleClear(mIpServer); + verifyTetherOffloadRuleRemove(inOrder, mobileRuleA); + verifyTetherOffloadRuleRemove(inOrder, mobileRuleB); + verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC); + verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex); + + // [4] Force pushing stats update to verify that the last diff of stats is reported on all + // upstreams. + mTetherStatsProvider.pushTetherStats(); + mTetherStatsProviderCb.expectNotifyStatsUpdated( + new NetworkStats(0L, 2) + .addEntry(buildTestEntry(STATS_PER_IFACE, ethIface, 10, 20, 30, 40)) + .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 50, 60, 70, 80)), + new NetworkStats(0L, 2) + .addEntry(buildTestEntry(STATS_PER_UID, ethIface, 10, 20, 30, 40)) + .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 50, 60, 70, 80))); + } + + private void checkBpfDisabled() throws Exception { + // The caller may mock the global dependencies |mDeps| which is used in + // #makeBpfCoordinator for testing. + // See #testBpfDisabledbyNoBpfDownstream6Map. + final BpfCoordinator coordinator = makeBpfCoordinator(); + coordinator.startPolling(); + + // The tether stats polling task should not be scheduled. + mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); + waitForIdle(); + verifyNeverTetherOffloadGetStats(); + + // The interface name lookup table can't be added. + final String iface = "rmnet_data0"; + final Integer ifIndex = 100; + coordinator.addUpstreamNameToLookupTable(ifIndex, iface); + assertEquals(0, coordinator.getInterfaceNamesForTesting().size()); + + // The rule can't be added. + final InetAddress neigh = InetAddresses.parseNumericAddress("2001:db8::1"); + final MacAddress mac = MacAddress.fromString("00:00:00:00:00:0a"); + final Ipv6ForwardingRule rule = buildTestForwardingRule(ifIndex, neigh, mac); + coordinator.tetherOffloadRuleAdd(mIpServer, rule); + verifyNeverTetherOffloadRuleAdd(); + LinkedHashMap rules = + coordinator.getForwardingRulesForTesting().get(mIpServer); + assertNull(rules); + + // The rule can't be removed. This is not a realistic case because adding rule is not + // allowed. That implies no rule could be removed, cleared or updated. Verify these + // cases just in case. + rules = new LinkedHashMap(); + rules.put(rule.address, rule); + coordinator.getForwardingRulesForTesting().put(mIpServer, rules); + coordinator.tetherOffloadRuleRemove(mIpServer, rule); + verifyNeverTetherOffloadRuleRemove(); + rules = coordinator.getForwardingRulesForTesting().get(mIpServer); + assertNotNull(rules); + assertEquals(1, rules.size()); + + // The rule can't be cleared. + coordinator.tetherOffloadRuleClear(mIpServer); + verifyNeverTetherOffloadRuleRemove(); + rules = coordinator.getForwardingRulesForTesting().get(mIpServer); + assertNotNull(rules); + assertEquals(1, rules.size()); + + // The rule can't be updated. + coordinator.tetherOffloadRuleUpdate(mIpServer, rule.upstreamIfindex + 1 /* new */); + verifyNeverTetherOffloadRuleRemove(); + verifyNeverTetherOffloadRuleAdd(); + rules = coordinator.getForwardingRulesForTesting().get(mIpServer); + assertNotNull(rules); + assertEquals(1, rules.size()); + } + + @Test + public void testBpfDisabledbyConfig() throws Exception { + setupFunctioningNetdInterface(); + when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false); + + checkBpfDisabled(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testBpfDisabledbyNoBpfDownstream6Map() throws Exception { + setupFunctioningNetdInterface(); + doReturn(null).when(mDeps).getBpfDownstream6Map(); + + checkBpfDisabled(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testBpfDisabledbyNoBpfUpstream6Map() throws Exception { + setupFunctioningNetdInterface(); + doReturn(null).when(mDeps).getBpfUpstream6Map(); + + checkBpfDisabled(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testBpfDisabledbyNoBpfDownstream4Map() throws Exception { + setupFunctioningNetdInterface(); + doReturn(null).when(mDeps).getBpfDownstream4Map(); + + checkBpfDisabled(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testBpfDisabledbyNoBpfUpstream4Map() throws Exception { + setupFunctioningNetdInterface(); + doReturn(null).when(mDeps).getBpfUpstream4Map(); + + checkBpfDisabled(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testBpfDisabledbyNoBpfStatsMap() throws Exception { + setupFunctioningNetdInterface(); + doReturn(null).when(mDeps).getBpfStatsMap(); + + checkBpfDisabled(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testBpfDisabledbyNoBpfLimitMap() throws Exception { + setupFunctioningNetdInterface(); + doReturn(null).when(mDeps).getBpfLimitMap(); + + checkBpfDisabled(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testBpfMapClear() throws Exception { + setupFunctioningNetdInterface(); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + verify(mBpfDownstream4Map).clear(); + verify(mBpfUpstream4Map).clear(); + verify(mBpfDownstream6Map).clear(); + verify(mBpfUpstream6Map).clear(); + verify(mBpfStatsMap).clear(); + verify(mBpfLimitMap).clear(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testAttachDetachBpfProgram() throws Exception { + setupFunctioningNetdInterface(); + + // Static mocking for BpfUtils. + MockitoSession mockSession = ExtendedMockito.mockitoSession() + .mockStatic(BpfUtils.class) + .startMocking(); + try { + final String intIface1 = "wlan1"; + final String intIface2 = "rndis0"; + final String extIface = "rmnet_data0"; + final BpfUtils mockMarkerBpfUtils = staticMockMarker(BpfUtils.class); + final BpfCoordinator coordinator = makeBpfCoordinator(); + + // [1] Add the forwarding pair . Expect that attach both wlan1 and + // rmnet_data0. + coordinator.maybeAttachProgram(intIface1, extIface); + ExtendedMockito.verify(() -> BpfUtils.attachProgram(extIface, DOWNSTREAM)); + ExtendedMockito.verify(() -> BpfUtils.attachProgram(intIface1, UPSTREAM)); + ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); + ExtendedMockito.clearInvocations(mockMarkerBpfUtils); + + // [2] Add the forwarding pair again. Expect no more action. + coordinator.maybeAttachProgram(intIface1, extIface); + ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); + ExtendedMockito.clearInvocations(mockMarkerBpfUtils); + + // [3] Add the forwarding pair . Expect that attach rndis0 only. + coordinator.maybeAttachProgram(intIface2, extIface); + ExtendedMockito.verify(() -> BpfUtils.attachProgram(intIface2, UPSTREAM)); + ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); + ExtendedMockito.clearInvocations(mockMarkerBpfUtils); + + // [4] Remove the forwarding pair . Expect detach rndis0 only. + coordinator.maybeDetachProgram(intIface2, extIface); + ExtendedMockito.verify(() -> BpfUtils.detachProgram(intIface2)); + ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); + ExtendedMockito.clearInvocations(mockMarkerBpfUtils); + + // [5] Remove the forwarding pair . Expect that detach both wlan1 + // and rmnet_data0. + coordinator.maybeDetachProgram(intIface1, extIface); + ExtendedMockito.verify(() -> BpfUtils.detachProgram(extIface)); + ExtendedMockito.verify(() -> BpfUtils.detachProgram(intIface1)); + ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); + ExtendedMockito.clearInvocations(mockMarkerBpfUtils); + } finally { + mockSession.finishMocking(); + } + } + + @Test + public void testTetheringConfigSetPollingInterval() throws Exception { + setupFunctioningNetdInterface(); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + + // [1] The default polling interval. + coordinator.startPolling(); + assertEquals(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, coordinator.getPollingInterval()); + coordinator.stopPolling(); + + // [2] Expect the invalid polling interval isn't applied. The valid range of interval is + // DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS..max_long. + for (final int interval + : new int[] {0, 100, DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS - 1}) { + when(mTetherConfig.getOffloadPollInterval()).thenReturn(interval); + coordinator.startPolling(); + assertEquals(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, coordinator.getPollingInterval()); + coordinator.stopPolling(); + } + + // [3] Set a specific polling interval which is larger than default value. + // Use a large polling interval to avoid flaky test because the time forwarding + // approximation is used to verify the scheduled time of the polling thread. + final int pollingInterval = 100_000; + when(mTetherConfig.getOffloadPollInterval()).thenReturn(pollingInterval); + coordinator.startPolling(); + + // Expect the specific polling interval to be applied. + assertEquals(pollingInterval, coordinator.getPollingInterval()); + + // Start on a new polling time slot. + mTestLooper.moveTimeForward(pollingInterval); + waitForIdle(); + clearStatsInvocations(); + + // Move time forward to 90% polling interval time. Expect that the polling thread has not + // scheduled yet. + mTestLooper.moveTimeForward((long) (pollingInterval * 0.9)); + waitForIdle(); + verifyNeverTetherOffloadGetStats(); + + // Move time forward to the remaining 10% polling interval time. Expect that the polling + // thread has scheduled. + mTestLooper.moveTimeForward((long) (pollingInterval * 0.1)); + waitForIdle(); + verifyTetherOffloadGetStats(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testStartStopConntrackMonitoring() throws Exception { + setupFunctioningNetdInterface(); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + + // [1] Don't stop monitoring if it has never started. + coordinator.stopMonitoring(mIpServer); + verify(mConntrackMonitor, never()).start(); + + // [2] Start monitoring. + coordinator.startMonitoring(mIpServer); + verify(mConntrackMonitor).start(); + clearInvocations(mConntrackMonitor); + + // [3] Stop monitoring. + coordinator.stopMonitoring(mIpServer); + verify(mConntrackMonitor).stop(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + @IgnoreAfter(Build.VERSION_CODES.R) + // Only run this test on Android R. + public void testStartStopConntrackMonitoring_R() throws Exception { + setupFunctioningNetdInterface(); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + + coordinator.startMonitoring(mIpServer); + verify(mConntrackMonitor, never()).start(); + + coordinator.stopMonitoring(mIpServer); + verify(mConntrackMonitor, never()).stop(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testStartStopConntrackMonitoringWithTwoDownstreamIfaces() throws Exception { + setupFunctioningNetdInterface(); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + + // [1] Start monitoring at the first IpServer adding. + coordinator.startMonitoring(mIpServer); + verify(mConntrackMonitor).start(); + clearInvocations(mConntrackMonitor); + + // [2] Don't start monitoring at the second IpServer adding. + coordinator.startMonitoring(mIpServer2); + verify(mConntrackMonitor, never()).start(); + + // [3] Don't stop monitoring if any downstream interface exists. + coordinator.stopMonitoring(mIpServer2); + verify(mConntrackMonitor, never()).stop(); + + // [4] Stop monitoring if no downstream exists. + coordinator.stopMonitoring(mIpServer); + verify(mConntrackMonitor).stop(); + } + + // Test network topology: + // + // public network (rawip) private network + // | UE | + // +------------+ V +------------+------------+ V +------------+ + // | Sever +---------+ Upstream | Downstream +---------+ Client | + // +------------+ +------------+------------+ +------------+ + // remote ip public ip private ip + // 140.112.8.116:443 100.81.179.1:62449 192.168.80.12:62449 + // + private static final Inet4Address REMOTE_ADDR = + (Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116"); + private static final Inet4Address PUBLIC_ADDR = + (Inet4Address) InetAddresses.parseNumericAddress("100.81.179.1"); + private static final Inet4Address PRIVATE_ADDR = + (Inet4Address) InetAddresses.parseNumericAddress("192.168.80.12"); + + // IPv4-mapped IPv6 addresses + // Remote addrress ::ffff:140.112.8.116 + // Public addrress ::ffff:100.81.179.1 + // Private addrress ::ffff:192.168.80.12 + private static final byte[] REMOTE_ADDR_V4MAPPED_BYTES = new byte[] { + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff, + (byte) 0x8c, (byte) 0x70, (byte) 0x08, (byte) 0x74 }; + private static final byte[] PUBLIC_ADDR_V4MAPPED_BYTES = new byte[] { + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff, + (byte) 0x64, (byte) 0x51, (byte) 0xb3, (byte) 0x01 }; + private static final byte[] PRIVATE_ADDR_V4MAPPED_BYTES = new byte[] { + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff, + (byte) 0xc0, (byte) 0xa8, (byte) 0x50, (byte) 0x0c }; + + // Generally, public port and private port are the same in the NAT conntrack message. + // TODO: consider using different private port and public port for testing. + private static final short REMOTE_PORT = (short) 443; + private static final short PUBLIC_PORT = (short) 62449; + private static final short PRIVATE_PORT = (short) 62449; + + @NonNull + private Tether4Key makeUpstream4Key(int proto) { + if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) { + fail("Not support protocol " + proto); + } + return new Tether4Key(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, (short) proto, + PRIVATE_ADDR.getAddress(), REMOTE_ADDR.getAddress(), PRIVATE_PORT, REMOTE_PORT); + } + + @NonNull + private Tether4Key makeDownstream4Key(int proto) { + if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) { + fail("Not support protocol " + proto); + } + return new Tether4Key(UPSTREAM_IFINDEX, + MacAddress.ALL_ZEROS_ADDRESS /* dstMac (rawip) */, (short) proto, + REMOTE_ADDR.getAddress(), PUBLIC_ADDR.getAddress(), REMOTE_PORT, PUBLIC_PORT); + } + + @NonNull + private Tether4Value makeUpstream4Value() { + return new Tether4Value(UPSTREAM_IFINDEX, + MacAddress.ALL_ZEROS_ADDRESS /* ethDstMac (rawip) */, + MacAddress.ALL_ZEROS_ADDRESS /* ethSrcMac (rawip) */, ETH_P_IP, + NetworkStackConstants.ETHER_MTU, PUBLIC_ADDR_V4MAPPED_BYTES, + REMOTE_ADDR_V4MAPPED_BYTES, PUBLIC_PORT, REMOTE_PORT, 0 /* lastUsed */); + } + + @NonNull + private Tether4Value makeDownstream4Value() { + return new Tether4Value(DOWNSTREAM_IFINDEX, MAC_A /* client mac */, DOWNSTREAM_MAC, + ETH_P_IP, NetworkStackConstants.ETHER_MTU, REMOTE_ADDR_V4MAPPED_BYTES, + PRIVATE_ADDR_V4MAPPED_BYTES, REMOTE_PORT, PRIVATE_PORT, 0 /* lastUsed */); + } + + @NonNull + private ConntrackEvent makeTestConntrackEvent(short msgType, int proto) { + if (msgType != IPCTNL_MSG_CT_NEW && msgType != IPCTNL_MSG_CT_DELETE) { + fail("Not support message type " + msgType); + } + if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) { + fail("Not support protocol " + proto); + } + + final int status = (msgType == IPCTNL_MSG_CT_NEW) ? ESTABLISHED_MASK : DYING_MASK; + final int timeoutSec = (msgType == IPCTNL_MSG_CT_NEW) ? 100 /* nonzero, new */ + : 0 /* unused, delete */; + return new ConntrackEvent( + (short) (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8 | msgType), + new Tuple(new TupleIpv4(PRIVATE_ADDR, REMOTE_ADDR), + new TupleProto((byte) proto, PRIVATE_PORT, REMOTE_PORT)), + new Tuple(new TupleIpv4(REMOTE_ADDR, PUBLIC_ADDR), + new TupleProto((byte) proto, REMOTE_PORT, PUBLIC_PORT)), + status, + timeoutSec); + } + + private void setUpstreamInformationTo(final BpfCoordinator coordinator) { + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(UPSTREAM_IFACE); + lp.addLinkAddress(new LinkAddress(PUBLIC_ADDR, 32 /* prefix length */)); + coordinator.addUpstreamIfindexToMap(lp); + } + + private void setDownstreamAndClientInformationTo(final BpfCoordinator coordinator) { + final ClientInfo clientInfo = new ClientInfo(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, + PRIVATE_ADDR, MAC_A /* client mac */); + coordinator.tetherOffloadClientAdd(mIpServer, clientInfo); + } + + private void initBpfCoordinatorForRule4(final BpfCoordinator coordinator) throws Exception { + // Needed because addUpstreamIfindexToMap only updates upstream information when polling + // was started. + coordinator.startPolling(); + + // Needed because tetherOffloadRuleRemove of api31.BpfCoordinatorShimImpl only decreases + // the count while the entry is deleted. In the other words, deleteEntry returns true. + doReturn(true).when(mBpfDownstream4Map).deleteEntry(any()); + + // Needed because BpfCoordinator#addUpstreamIfindexToMap queries interface parameter for + // interface index. + doReturn(UPSTREAM_IFACE_PARAMS).when(mDeps).getInterfaceParams(UPSTREAM_IFACE); + + coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE); + setUpstreamInformationTo(coordinator); + setDownstreamAndClientInformationTo(coordinator); + } + + // TODO: Test the IPv4 and IPv6 exist concurrently. + // TODO: Test the IPv4 rule delete failed. + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testSetDataLimitOnRule4Change() throws Exception { + final BpfCoordinator coordinator = makeBpfCoordinator(); + initBpfCoordinatorForRule4(coordinator); + + // Applying a data limit to the current upstream does not take any immediate action. + // The data limit could be only set on an upstream which has rules. + final long limit = 12345; + final InOrder inOrder = inOrder(mNetd, mBpfUpstream4Map, mBpfDownstream4Map, mBpfLimitMap, + mBpfStatsMap); + mTetherStatsProvider.onSetLimit(UPSTREAM_IFACE, limit); + waitForIdle(); + verifyNeverTetherOffloadSetInterfaceQuota(inOrder); + + // Build TCP and UDP rules for testing. Note that the values of {TCP, UDP} are the same + // because the protocol is not an element of the value. Consider using different address + // or port to make them different for better testing. + // TODO: Make the values of {TCP, UDP} rules different. + final Tether4Key expectedUpstream4KeyTcp = makeUpstream4Key(IPPROTO_TCP); + final Tether4Key expectedDownstream4KeyTcp = makeDownstream4Key(IPPROTO_TCP); + final Tether4Value expectedUpstream4ValueTcp = makeUpstream4Value(); + final Tether4Value expectedDownstream4ValueTcp = makeDownstream4Value(); + + final Tether4Key expectedUpstream4KeyUdp = makeUpstream4Key(IPPROTO_UDP); + final Tether4Key expectedDownstream4KeyUdp = makeDownstream4Key(IPPROTO_UDP); + final Tether4Value expectedUpstream4ValueUdp = makeUpstream4Value(); + final Tether4Value expectedDownstream4ValueUdp = makeDownstream4Value(); + + // [1] Adding the first rule on current upstream immediately sends the quota. + mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_TCP)); + verifyTetherOffloadSetInterfaceQuota(inOrder, UPSTREAM_IFINDEX, limit, true /* isInit */); + inOrder.verify(mBpfUpstream4Map) + .insertEntry(eq(expectedUpstream4KeyTcp), eq(expectedUpstream4ValueTcp)); + inOrder.verify(mBpfDownstream4Map) + .insertEntry(eq(expectedDownstream4KeyTcp), eq(expectedDownstream4ValueTcp)); + inOrder.verifyNoMoreInteractions(); + + // [2] Adding the second rule on current upstream does not send the quota. + mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_UDP)); + verifyNeverTetherOffloadSetInterfaceQuota(inOrder); + inOrder.verify(mBpfUpstream4Map) + .insertEntry(eq(expectedUpstream4KeyUdp), eq(expectedUpstream4ValueUdp)); + inOrder.verify(mBpfDownstream4Map) + .insertEntry(eq(expectedDownstream4KeyUdp), eq(expectedDownstream4ValueUdp)); + inOrder.verifyNoMoreInteractions(); + + // [3] Removing the second rule on current upstream does not send the quota. + mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, IPPROTO_UDP)); + verifyNeverTetherOffloadSetInterfaceQuota(inOrder); + inOrder.verify(mBpfUpstream4Map).deleteEntry(eq(expectedUpstream4KeyUdp)); + inOrder.verify(mBpfDownstream4Map).deleteEntry(eq(expectedDownstream4KeyUdp)); + inOrder.verifyNoMoreInteractions(); + + // [4] Removing the last rule on current upstream immediately sends the cleanup stuff. + updateStatsEntryForTetherOffloadGetAndClearStats( + buildTestTetherStatsParcel(UPSTREAM_IFINDEX, 0, 0, 0, 0)); + mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, IPPROTO_TCP)); + inOrder.verify(mBpfUpstream4Map).deleteEntry(eq(expectedUpstream4KeyTcp)); + inOrder.verify(mBpfDownstream4Map).deleteEntry(eq(expectedDownstream4KeyTcp)); + verifyTetherOffloadGetAndClearStats(inOrder, UPSTREAM_IFINDEX); + inOrder.verifyNoMoreInteractions(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testAddDevMapRule6() throws Exception { + final BpfCoordinator coordinator = makeBpfCoordinator(); + + coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE); + final Ipv6ForwardingRule ruleA = buildTestForwardingRule(UPSTREAM_IFINDEX, NEIGH_A, MAC_A); + final Ipv6ForwardingRule ruleB = buildTestForwardingRule(UPSTREAM_IFINDEX, NEIGH_B, MAC_B); + + coordinator.tetherOffloadRuleAdd(mIpServer, ruleA); + verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)), + eq(new TetherDevValue(UPSTREAM_IFINDEX))); + verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)), + eq(new TetherDevValue(DOWNSTREAM_IFINDEX))); + clearInvocations(mBpfDevMap); + + coordinator.tetherOffloadRuleAdd(mIpServer, ruleB); + verify(mBpfDevMap, never()).updateEntry(any(), any()); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testAddDevMapRule4() throws Exception { + final BpfCoordinator coordinator = makeBpfCoordinator(); + initBpfCoordinatorForRule4(coordinator); + + mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_TCP)); + verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)), + eq(new TetherDevValue(UPSTREAM_IFINDEX))); + verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)), + eq(new TetherDevValue(DOWNSTREAM_IFINDEX))); + clearInvocations(mBpfDevMap); + + mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_UDP)); + verify(mBpfDevMap, never()).updateEntry(any(), any()); + } +} diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/ConnectedClientsTrackerTest.kt b/Tethering/tests/unit/src/com/android/networkstack/tethering/ConnectedClientsTrackerTest.kt new file mode 100644 index 0000000000..d915354b0c --- /dev/null +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/ConnectedClientsTrackerTest.kt @@ -0,0 +1,162 @@ +/* + * 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.networkstack.tethering + +import android.net.LinkAddress +import android.net.MacAddress +import android.net.TetheredClient +import android.net.TetheredClient.AddressInfo +import android.net.TetheringManager.TETHERING_USB +import android.net.TetheringManager.TETHERING_WIFI +import android.net.ip.IpServer +import android.net.wifi.WifiClient +import androidx.test.filters.SmallTest +import androidx.test.runner.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +@SmallTest +class ConnectedClientsTrackerTest { + + private val server1 = mock(IpServer::class.java) + private val server2 = mock(IpServer::class.java) + private val servers = listOf(server1, server2) + + private val clock = TestClock(1324L) + + private val client1Addr = MacAddress.fromString("01:23:45:67:89:0A") + private val client1 = TetheredClient(client1Addr, listOf( + makeAddrInfo("192.168.43.44/32", null /* hostname */, clock.time + 20)), + TETHERING_WIFI) + private val wifiClient1 = makeWifiClient(client1Addr) + private val client2Addr = MacAddress.fromString("02:34:56:78:90:AB") + private val client2Exp30AddrInfo = makeAddrInfo( + "192.168.43.45/32", "my_hostname", clock.time + 30) + private val client2 = TetheredClient(client2Addr, listOf( + client2Exp30AddrInfo, + makeAddrInfo("2001:db8:12::34/72", "other_hostname", clock.time + 10)), + TETHERING_WIFI) + private val wifiClient2 = makeWifiClient(client2Addr) + private val client3Addr = MacAddress.fromString("03:45:67:89:0A:BC") + private val client3 = TetheredClient(client3Addr, + listOf(makeAddrInfo("2001:db8:34::34/72", "other_other_hostname", clock.time + 10)), + TETHERING_USB) + + private fun makeAddrInfo(addr: String, hostname: String?, expTime: Long) = + LinkAddress(addr).let { + AddressInfo(LinkAddress(it.address, it.prefixLength, it.flags, it.scope, + expTime /* deprecationTime */, expTime /* expirationTime */), hostname) + } + + @Test + fun testUpdateConnectedClients() { + doReturn(emptyList()).`when`(server1).allLeases + doReturn(emptyList()).`when`(server2).allLeases + + val tracker = ConnectedClientsTracker(clock) + assertFalse(tracker.updateConnectedClients(servers, null)) + + // Obtain a lease for client 1 + doReturn(listOf(client1)).`when`(server1).allLeases + assertSameClients(listOf(client1), assertNewClients(tracker, servers, listOf(wifiClient1))) + + // Client 2 L2-connected, no lease yet + val client2WithoutAddr = TetheredClient(client2Addr, emptyList(), TETHERING_WIFI) + assertSameClients(listOf(client1, client2WithoutAddr), + assertNewClients(tracker, servers, listOf(wifiClient1, wifiClient2))) + + // Client 2 lease obtained + doReturn(listOf(client1, client2)).`when`(server1).allLeases + assertSameClients(listOf(client1, client2), assertNewClients(tracker, servers, null)) + + // Client 3 lease obtained + doReturn(listOf(client3)).`when`(server2).allLeases + assertSameClients(listOf(client1, client2, client3), + assertNewClients(tracker, servers, null)) + + // Client 2 L2-disconnected + assertSameClients(listOf(client1, client3), + assertNewClients(tracker, servers, listOf(wifiClient1))) + + // Client 1 L2-disconnected + assertSameClients(listOf(client3), assertNewClients(tracker, servers, emptyList())) + + // Client 1 comes back + assertSameClients(listOf(client1, client3), + assertNewClients(tracker, servers, listOf(wifiClient1))) + + // Leases lost, client 1 still L2-connected + doReturn(emptyList()).`when`(server1).allLeases + doReturn(emptyList()).`when`(server2).allLeases + assertSameClients(listOf(TetheredClient(client1Addr, emptyList(), TETHERING_WIFI)), + assertNewClients(tracker, servers, null)) + } + + @Test + fun testUpdateConnectedClients_LeaseExpiration() { + val tracker = ConnectedClientsTracker(clock) + doReturn(listOf(client1, client2)).`when`(server1).allLeases + doReturn(listOf(client3)).`when`(server2).allLeases + assertSameClients(listOf(client1, client2, client3), assertNewClients( + tracker, servers, listOf(wifiClient1, wifiClient2))) + + clock.time += 20 + // Client 3 has no remaining lease: removed + val expectedClients = listOf( + // Client 1 has no remaining lease but is L2-connected + TetheredClient(client1Addr, emptyList(), TETHERING_WIFI), + // Client 2 has some expired leases + TetheredClient( + client2Addr, + // Only the "t + 30" address is left, the "t + 10" address expired + listOf(client2Exp30AddrInfo), + TETHERING_WIFI)) + assertSameClients(expectedClients, assertNewClients(tracker, servers, null)) + } + + private fun assertNewClients( + tracker: ConnectedClientsTracker, + ipServers: Iterable, + wifiClients: List? + ): List { + assertTrue(tracker.updateConnectedClients(ipServers, wifiClients)) + return tracker.lastTetheredClients + } + + private fun assertSameClients(expected: List, actual: List) { + val expectedSet = HashSet(expected) + assertEquals(expected.size, expectedSet.size) + assertEquals(expectedSet, HashSet(actual)) + } + + private fun makeWifiClient(macAddr: MacAddress): WifiClient { + // Use a mock WifiClient as the constructor is not part of the WiFi module exported API. + return mock(WifiClient::class.java).apply { doReturn(macAddr).`when`(this).macAddress } + } + + private class TestClock(var time: Long) : ConnectedClientsTracker.Clock() { + override fun elapsedRealtime(): Long { + return time + } + } +} diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java new file mode 100644 index 0000000000..5ae4b43f41 --- /dev/null +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java @@ -0,0 +1,632 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import static android.net.TetheringConstants.EXTRA_ADD_TETHER_TYPE; +import static android.net.TetheringConstants.EXTRA_PROVISION_CALLBACK; +import static android.net.TetheringConstants.EXTRA_RUN_PROVISION; +import static android.net.TetheringConstants.EXTRA_TETHER_PROVISIONING_RESPONSE; +import static android.net.TetheringConstants.EXTRA_TETHER_SILENT_PROVISIONING_ACTION; +import static android.net.TetheringConstants.EXTRA_TETHER_SUBID; +import static android.net.TetheringConstants.EXTRA_TETHER_UI_PROVISIONING_APP_NAME; +import static android.net.TetheringManager.TETHERING_BLUETOOTH; +import static android.net.TetheringManager.TETHERING_ETHERNET; +import static android.net.TetheringManager.TETHERING_INVALID; +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_ERROR; +import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED; +import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; +import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ModuleInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.net.util.SharedLog; +import android.os.Bundle; +import android.os.Handler; +import android.os.PersistableBundle; +import android.os.ResultReceiver; +import android.os.SystemProperties; +import android.os.test.TestLooper; +import android.provider.DeviceConfig; +import android.provider.Settings; +import android.telephony.CarrierConfigManager; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.test.BroadcastInterceptingContext; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public final class EntitlementManagerTest { + + private static final String[] PROVISIONING_APP_NAME = {"some", "app"}; + private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app"; + private static final String PROVISIONING_APP_RESPONSE = "app_response"; + private static final String TEST_PACKAGE_NAME = "com.android.tethering.test"; + + @Mock private CarrierConfigManager mCarrierConfigManager; + @Mock private Context mContext; + @Mock private Resources mResources; + @Mock private SharedLog mLog; + @Mock private PackageManager mPm; + @Mock private EntitlementManager.OnUiEntitlementFailedListener mEntitlementFailedListener; + + // Like so many Android system APIs, these cannot be mocked because it is marked final. + // We have to use the real versions. + private final PersistableBundle mCarrierConfig = new PersistableBundle(); + private final TestLooper mLooper = new TestLooper(); + private Context mMockContext; + private Runnable mPermissionChangeCallback; + + private WrappedEntitlementManager mEnMgr; + private TetheringConfiguration mConfig; + private MockitoSession mMockingSession; + + private class MockContext extends BroadcastInterceptingContext { + MockContext(Context base) { + super(base); + } + + @Override + public Resources getResources() { + return mResources; + } + } + + public class WrappedEntitlementManager extends EntitlementManager { + public int fakeEntitlementResult = TETHER_ERROR_ENTITLEMENT_UNKNOWN; + public int uiProvisionCount = 0; + public int silentProvisionCount = 0; + + public WrappedEntitlementManager(Context ctx, Handler h, SharedLog log, + Runnable callback) { + super(ctx, h, log, callback); + } + + public void reset() { + fakeEntitlementResult = TETHER_ERROR_ENTITLEMENT_UNKNOWN; + uiProvisionCount = 0; + silentProvisionCount = 0; + } + + @Override + protected Intent runUiTetherProvisioning(int type, + final TetheringConfiguration config, final ResultReceiver receiver) { + Intent intent = super.runUiTetherProvisioning(type, config, receiver); + assertUiTetherProvisioningIntent(type, config, receiver, intent); + uiProvisionCount++; + receiver.send(fakeEntitlementResult, null); + return intent; + } + + private void assertUiTetherProvisioningIntent(int type, final TetheringConfiguration config, + final ResultReceiver receiver, final Intent intent) { + assertEquals(Settings.ACTION_TETHER_PROVISIONING_UI, intent.getAction()); + assertEquals(type, intent.getIntExtra(EXTRA_ADD_TETHER_TYPE, TETHERING_INVALID)); + final String[] appName = intent.getStringArrayExtra( + EXTRA_TETHER_UI_PROVISIONING_APP_NAME); + assertEquals(PROVISIONING_APP_NAME.length, appName.length); + for (int i = 0; i < PROVISIONING_APP_NAME.length; i++) { + assertEquals(PROVISIONING_APP_NAME[i], appName[i]); + } + assertEquals(receiver, intent.getParcelableExtra(EXTRA_PROVISION_CALLBACK)); + assertEquals(config.activeDataSubId, + intent.getIntExtra(EXTRA_TETHER_SUBID, INVALID_SUBSCRIPTION_ID)); + } + + @Override + protected Intent runSilentTetherProvisioning(int type, + final TetheringConfiguration config) { + Intent intent = super.runSilentTetherProvisioning(type, config); + assertSilentTetherProvisioning(type, config, intent); + silentProvisionCount++; + addDownstreamMapping(type, fakeEntitlementResult); + return intent; + } + + private void assertSilentTetherProvisioning(int type, final TetheringConfiguration config, + final Intent intent) { + assertEquals(type, intent.getIntExtra(EXTRA_ADD_TETHER_TYPE, TETHERING_INVALID)); + assertEquals(true, intent.getBooleanExtra(EXTRA_RUN_PROVISION, false)); + assertEquals(PROVISIONING_NO_UI_APP_NAME, + intent.getStringExtra(EXTRA_TETHER_SILENT_PROVISIONING_ACTION)); + assertEquals(PROVISIONING_APP_RESPONSE, + intent.getStringExtra(EXTRA_TETHER_PROVISIONING_RESPONSE)); + assertTrue(intent.hasExtra(EXTRA_PROVISION_CALLBACK)); + assertEquals(config.activeDataSubId, + intent.getIntExtra(EXTRA_TETHER_SUBID, INVALID_SUBSCRIPTION_ID)); + } + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mMockingSession = mockitoSession() + .initMocks(this) + .mockStatic(SystemProperties.class) + .mockStatic(DeviceConfig.class) + .strictness(Strictness.WARN) + .startMocking(); + // Don't disable tethering provisioning unless requested. + doReturn(false).when( + () -> SystemProperties.getBoolean( + eq(EntitlementManager.DISABLE_PROVISIONING_SYSPROP_KEY), anyBoolean())); + doReturn(null).when( + () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY), anyString())); + doReturn(mPm).when(mContext).getPackageManager(); + doReturn(TEST_PACKAGE_NAME).when(mContext).getPackageName(); + doReturn(new PackageInfo()).when(mPm).getPackageInfo(anyString(), anyInt()); + doReturn(new ModuleInfo()).when(mPm).getModuleInfo(anyString(), anyInt()); + + when(mResources.getStringArray(R.array.config_tether_dhcp_range)) + .thenReturn(new String[0]); + when(mResources.getStringArray(R.array.config_tether_usb_regexs)) + .thenReturn(new String[0]); + when(mResources.getStringArray(R.array.config_tether_wifi_regexs)) + .thenReturn(new String[0]); + when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs)) + .thenReturn(new String[0]); + when(mResources.getIntArray(R.array.config_tether_upstream_types)) + .thenReturn(new int[0]); + when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn( + false); + when(mResources.getString(R.string.config_wifi_tether_enable)).thenReturn(""); + when(mLog.forSubComponent(anyString())).thenReturn(mLog); + + mMockContext = new MockContext(mContext); + mPermissionChangeCallback = spy(() -> { }); + mEnMgr = new WrappedEntitlementManager(mMockContext, new Handler(mLooper.getLooper()), mLog, + mPermissionChangeCallback); + mEnMgr.setOnUiEntitlementFailedListener(mEntitlementFailedListener); + mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + mEnMgr.setTetheringConfigurationFetcher(() -> { + return mConfig; + }); + } + + @After + public void tearDown() throws Exception { + mMockingSession.finishMocking(); + } + + private void setupForRequiredProvisioning() { + // Produce some acceptable looking provision app setting if requested. + when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app)) + .thenReturn(PROVISIONING_APP_NAME); + when(mResources.getString(R.string.config_mobile_hotspot_provision_app_no_ui)) + .thenReturn(PROVISIONING_NO_UI_APP_NAME); + when(mResources.getString(R.string.config_mobile_hotspot_provision_response)).thenReturn( + PROVISIONING_APP_RESPONSE); + // Act like the CarrierConfigManager is present and ready unless told otherwise. + when(mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE)) + .thenReturn(mCarrierConfigManager); + when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(mCarrierConfig); + mCarrierConfig.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, true); + mCarrierConfig.putBoolean(CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, true); + mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + } + + @Test + public void canRequireProvisioning() { + setupForRequiredProvisioning(); + assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig)); + } + + @Test + public void toleratesCarrierConfigManagerMissing() { + setupForRequiredProvisioning(); + when(mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE)) + .thenReturn(null); + mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + // Couldn't get the CarrierConfigManager, but still had a declared provisioning app. + // Therefore provisioning still be required. + assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig)); + } + + @Test + public void toleratesCarrierConfigMissing() { + setupForRequiredProvisioning(); + when(mCarrierConfigManager.getConfig()).thenReturn(null); + mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + // We still have a provisioning app configured, so still require provisioning. + assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig)); + } + + @Test + public void toleratesCarrierConfigNotLoaded() { + setupForRequiredProvisioning(); + mCarrierConfig.putBoolean(CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, false); + // We still have a provisioning app configured, so still require provisioning. + assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig)); + } + + @Test + public void provisioningNotRequiredWhenAppNotFound() { + setupForRequiredProvisioning(); + when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app)) + .thenReturn(null); + mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + assertFalse(mEnMgr.isTetherProvisioningRequired(mConfig)); + when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app)) + .thenReturn(new String[] {"malformedApp"}); + mConfig = new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + assertFalse(mEnMgr.isTetherProvisioningRequired(mConfig)); + } + + @Test + public void testRequestLastEntitlementCacheValue() throws Exception { + // 1. Entitlement check is not required. + mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR; + ResultReceiver receiver = new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + assertEquals(TETHER_ERROR_NO_ERROR, resultCode); + } + }; + mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true); + mLooper.dispatchAll(); + assertEquals(0, mEnMgr.uiProvisionCount); + mEnMgr.reset(); + + setupForRequiredProvisioning(); + // 2. No cache value and don't need to run entitlement check. + receiver = new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode); + } + }; + mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false); + mLooper.dispatchAll(); + assertEquals(0, mEnMgr.uiProvisionCount); + mEnMgr.reset(); + // 3. No cache value and ui entitlement check is needed. + mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED; + receiver = new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + assertEquals(TETHER_ERROR_PROVISIONING_FAILED, resultCode); + } + }; + mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true); + mLooper.dispatchAll(); + assertEquals(1, mEnMgr.uiProvisionCount); + mEnMgr.reset(); + // 4. Cache value is TETHER_ERROR_PROVISIONING_FAILED and don't need to run entitlement + // check. + mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR; + receiver = new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + assertEquals(TETHER_ERROR_PROVISIONING_FAILED, resultCode); + } + }; + mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, false); + mLooper.dispatchAll(); + assertEquals(0, mEnMgr.uiProvisionCount); + mEnMgr.reset(); + // 5. Cache value is TETHER_ERROR_PROVISIONING_FAILED and ui entitlement check is needed. + mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR; + receiver = new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + assertEquals(TETHER_ERROR_NO_ERROR, resultCode); + } + }; + mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true); + mLooper.dispatchAll(); + assertEquals(1, mEnMgr.uiProvisionCount); + mEnMgr.reset(); + // 6. Cache value is TETHER_ERROR_NO_ERROR. + mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR; + receiver = new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + assertEquals(TETHER_ERROR_NO_ERROR, resultCode); + } + }; + mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI, receiver, true); + mLooper.dispatchAll(); + assertEquals(0, mEnMgr.uiProvisionCount); + mEnMgr.reset(); + // 7. Test get value for other downstream type. + receiver = new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode); + } + }; + mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_USB, receiver, false); + mLooper.dispatchAll(); + assertEquals(0, mEnMgr.uiProvisionCount); + mEnMgr.reset(); + // 8. Test get value for invalid downstream type. + mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR; + receiver = new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + assertEquals(TETHER_ERROR_ENTITLEMENT_UNKNOWN, resultCode); + } + }; + mEnMgr.requestLatestTetheringEntitlementResult(TETHERING_WIFI_P2P, receiver, true); + mLooper.dispatchAll(); + assertEquals(0, mEnMgr.uiProvisionCount); + mEnMgr.reset(); + } + + private void assertPermissionChangeCallback(InOrder inOrder) { + inOrder.verify(mPermissionChangeCallback, times(1)).run(); + } + + private void assertNoPermissionChange(InOrder inOrder) { + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void verifyPermissionResult() { + final InOrder inOrder = inOrder(mPermissionChangeCallback); + setupForRequiredProvisioning(); + mEnMgr.notifyUpstream(true); + mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED; + mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true); + mLooper.dispatchAll(); + // Permitted: true -> false + assertPermissionChangeCallback(inOrder); + assertFalse(mEnMgr.isCellularUpstreamPermitted()); + + mEnMgr.stopProvisioningIfNeeded(TETHERING_WIFI); + mLooper.dispatchAll(); + // Permitted: false -> false + assertNoPermissionChange(inOrder); + + mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR; + mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true); + mLooper.dispatchAll(); + // Permitted: false -> true + assertPermissionChangeCallback(inOrder); + assertTrue(mEnMgr.isCellularUpstreamPermitted()); + } + + @Test + public void verifyPermissionIfAllNotApproved() { + final InOrder inOrder = inOrder(mPermissionChangeCallback); + setupForRequiredProvisioning(); + mEnMgr.notifyUpstream(true); + mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED; + mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true); + mLooper.dispatchAll(); + // Permitted: true -> false + assertPermissionChangeCallback(inOrder); + assertFalse(mEnMgr.isCellularUpstreamPermitted()); + + mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED; + mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true); + mLooper.dispatchAll(); + // Permitted: false -> false + assertNoPermissionChange(inOrder); + assertFalse(mEnMgr.isCellularUpstreamPermitted()); + + mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED; + mEnMgr.startProvisioningIfNeeded(TETHERING_BLUETOOTH, true); + mLooper.dispatchAll(); + // Permitted: false -> false + assertNoPermissionChange(inOrder); + assertFalse(mEnMgr.isCellularUpstreamPermitted()); + } + + @Test + public void verifyPermissionIfAnyApproved() { + final InOrder inOrder = inOrder(mPermissionChangeCallback); + setupForRequiredProvisioning(); + mEnMgr.notifyUpstream(true); + mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR; + mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true); + mLooper.dispatchAll(); + // Permitted: true -> true + assertNoPermissionChange(inOrder); + assertTrue(mEnMgr.isCellularUpstreamPermitted()); + + mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED; + mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true); + mLooper.dispatchAll(); + // Permitted: true -> true + assertNoPermissionChange(inOrder); + assertTrue(mEnMgr.isCellularUpstreamPermitted()); + + mEnMgr.stopProvisioningIfNeeded(TETHERING_WIFI); + mLooper.dispatchAll(); + // Permitted: true -> false + assertPermissionChangeCallback(inOrder); + assertFalse(mEnMgr.isCellularUpstreamPermitted()); + } + + @Test + public void verifyPermissionWhenProvisioningNotStarted() { + final InOrder inOrder = inOrder(mPermissionChangeCallback); + assertTrue(mEnMgr.isCellularUpstreamPermitted()); + assertNoPermissionChange(inOrder); + setupForRequiredProvisioning(); + assertFalse(mEnMgr.isCellularUpstreamPermitted()); + assertNoPermissionChange(inOrder); + } + + @Test + public void testRunTetherProvisioning() { + final InOrder inOrder = inOrder(mPermissionChangeCallback); + setupForRequiredProvisioning(); + // 1. start ui provisioning, upstream is mobile + mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR; + mEnMgr.notifyUpstream(true); + mLooper.dispatchAll(); + mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true); + mLooper.dispatchAll(); + assertEquals(1, mEnMgr.uiProvisionCount); + assertEquals(0, mEnMgr.silentProvisionCount); + // Permitted: true -> true + assertNoPermissionChange(inOrder); + assertTrue(mEnMgr.isCellularUpstreamPermitted()); + mEnMgr.reset(); + + // 2. start no-ui provisioning + mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR; + mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, false); + mLooper.dispatchAll(); + assertEquals(0, mEnMgr.uiProvisionCount); + assertEquals(1, mEnMgr.silentProvisionCount); + // Permitted: true -> true + assertNoPermissionChange(inOrder); + assertTrue(mEnMgr.isCellularUpstreamPermitted()); + mEnMgr.reset(); + + // 3. tear down mobile, then start ui provisioning + mEnMgr.notifyUpstream(false); + mLooper.dispatchAll(); + mEnMgr.startProvisioningIfNeeded(TETHERING_BLUETOOTH, true); + mLooper.dispatchAll(); + assertEquals(0, mEnMgr.uiProvisionCount); + assertEquals(0, mEnMgr.silentProvisionCount); + assertNoPermissionChange(inOrder); + mEnMgr.reset(); + + // 4. switch upstream back to mobile + mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR; + mEnMgr.notifyUpstream(true); + mLooper.dispatchAll(); + assertEquals(1, mEnMgr.uiProvisionCount); + assertEquals(0, mEnMgr.silentProvisionCount); + // Permitted: true -> true + assertNoPermissionChange(inOrder); + assertTrue(mEnMgr.isCellularUpstreamPermitted()); + mEnMgr.reset(); + + // 5. tear down mobile, then switch SIM + mEnMgr.notifyUpstream(false); + mLooper.dispatchAll(); + mEnMgr.reevaluateSimCardProvisioning(mConfig); + assertEquals(0, mEnMgr.uiProvisionCount); + assertEquals(0, mEnMgr.silentProvisionCount); + assertNoPermissionChange(inOrder); + mEnMgr.reset(); + + // 6. switch upstream back to mobile again + mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED; + mEnMgr.notifyUpstream(true); + mLooper.dispatchAll(); + assertEquals(0, mEnMgr.uiProvisionCount); + assertEquals(3, mEnMgr.silentProvisionCount); + // Permitted: true -> false + assertPermissionChangeCallback(inOrder); + assertFalse(mEnMgr.isCellularUpstreamPermitted()); + mEnMgr.reset(); + + // 7. start ui provisioning, upstream is mobile, downstream is ethernet + mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR; + mEnMgr.startProvisioningIfNeeded(TETHERING_ETHERNET, true); + mLooper.dispatchAll(); + assertEquals(1, mEnMgr.uiProvisionCount); + assertEquals(0, mEnMgr.silentProvisionCount); + // Permitted: false -> true + assertPermissionChangeCallback(inOrder); + assertTrue(mEnMgr.isCellularUpstreamPermitted()); + mEnMgr.reset(); + + // 8. downstream is invalid + mEnMgr.fakeEntitlementResult = TETHER_ERROR_NO_ERROR; + mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI_P2P, true); + mLooper.dispatchAll(); + assertEquals(0, mEnMgr.uiProvisionCount); + assertEquals(0, mEnMgr.silentProvisionCount); + assertNoPermissionChange(inOrder); + mEnMgr.reset(); + } + + @Test + public void testCallStopTetheringWhenUiProvisioningFail() { + setupForRequiredProvisioning(); + verify(mEntitlementFailedListener, times(0)).onUiEntitlementFailed(TETHERING_WIFI); + mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED; + mEnMgr.notifyUpstream(true); + mLooper.dispatchAll(); + mEnMgr.startProvisioningIfNeeded(TETHERING_WIFI, true); + mLooper.dispatchAll(); + assertEquals(1, mEnMgr.uiProvisionCount); + verify(mEntitlementFailedListener, times(1)).onUiEntitlementFailed(TETHERING_WIFI); + } + + @Test + public void testsetExemptedDownstreamType() throws Exception { + setupForRequiredProvisioning(); + // Cellular upstream is not permitted when no entitlement result. + assertFalse(mEnMgr.isCellularUpstreamPermitted()); + + // If there is exempted downstream and no other non-exempted downstreams, cellular is + // permitted. + mEnMgr.setExemptedDownstreamType(TETHERING_WIFI); + assertTrue(mEnMgr.isCellularUpstreamPermitted()); + + // If second downstream run entitlement check fail, cellular upstream is not permitted. + mEnMgr.fakeEntitlementResult = TETHER_ERROR_PROVISIONING_FAILED; + mEnMgr.notifyUpstream(true); + mLooper.dispatchAll(); + mEnMgr.startProvisioningIfNeeded(TETHERING_USB, true); + mLooper.dispatchAll(); + assertFalse(mEnMgr.isCellularUpstreamPermitted()); + + // When second downstream is down, exempted downstream can use cellular upstream. + assertEquals(1, mEnMgr.uiProvisionCount); + verify(mEntitlementFailedListener).onUiEntitlementFailed(TETHERING_USB); + mEnMgr.stopProvisioningIfNeeded(TETHERING_USB); + assertTrue(mEnMgr.isCellularUpstreamPermitted()); + + mEnMgr.stopProvisioningIfNeeded(TETHERING_WIFI); + assertFalse(mEnMgr.isCellularUpstreamPermitted()); + } +} diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java new file mode 100644 index 0000000000..f2b5314e5a --- /dev/null +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/IPv6TetheringCoordinatorTest.java @@ -0,0 +1,156 @@ +/* + * 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.networkstack.tethering; + +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.RouteInfo.RTN_UNICAST; +import static android.net.ip.IpServer.STATE_LOCAL_ONLY; +import static android.net.ip.IpServer.STATE_TETHERED; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.net.InetAddresses; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.RouteInfo; +import android.net.ip.IpServer; +import android.net.util.SharedLog; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class IPv6TetheringCoordinatorTest { + private static final String TEST_DNS_SERVER = "2001:4860:4860::8888"; + private static final String TEST_INTERFACE = "test_rmnet0"; + private static final String TEST_IPV6_ADDRESS = "2001:db8::1/64"; + private static final String TEST_IPV4_ADDRESS = "192.168.100.1/24"; + + private IPv6TetheringCoordinator mIPv6TetheringCoordinator; + private ArrayList mNotifyList; + + @Mock private SharedLog mSharedLog; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + when(mSharedLog.forSubComponent(anyString())).thenReturn(mSharedLog); + mNotifyList = new ArrayList(); + mIPv6TetheringCoordinator = new IPv6TetheringCoordinator(mNotifyList, mSharedLog); + } + + private UpstreamNetworkState createDualStackUpstream(final int transportType) { + final Network network = mock(Network.class); + final NetworkCapabilities netCap = + new NetworkCapabilities.Builder().addTransportType(transportType).build(); + final InetAddress dns = InetAddresses.parseNumericAddress(TEST_DNS_SERVER); + final LinkProperties linkProp = new LinkProperties(); + linkProp.setInterfaceName(TEST_INTERFACE); + linkProp.addLinkAddress(new LinkAddress(TEST_IPV6_ADDRESS)); + linkProp.addLinkAddress(new LinkAddress(TEST_IPV4_ADDRESS)); + linkProp.addRoute(new RouteInfo(new IpPrefix("::/0"), null, TEST_INTERFACE, RTN_UNICAST)); + linkProp.addRoute(new RouteInfo(new IpPrefix("0.0.0.0/0"), null, TEST_INTERFACE, + RTN_UNICAST)); + linkProp.addDnsServer(dns); + return new UpstreamNetworkState(linkProp, netCap, network); + } + + private void assertOnlyOneV6AddressAndNoV4(LinkProperties lp) { + assertEquals(lp.getInterfaceName(), TEST_INTERFACE); + assertFalse(lp.hasIpv4Address()); + final List addresses = lp.getLinkAddresses(); + assertEquals(addresses.size(), 1); + final LinkAddress v6Address = addresses.get(0); + assertEquals(v6Address, new LinkAddress(TEST_IPV6_ADDRESS)); + } + + @Test + public void testUpdateIpv6Upstream() throws Exception { + // 1. Add first IpServer. + final IpServer firstServer = mock(IpServer.class); + mNotifyList.add(firstServer); + mIPv6TetheringCoordinator.addActiveDownstream(firstServer, STATE_TETHERED); + verify(firstServer).sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, null); + verifyNoMoreInteractions(firstServer); + + // 2. Add second IpServer and it would not have ipv6 tethering. + final IpServer secondServer = mock(IpServer.class); + mNotifyList.add(secondServer); + mIPv6TetheringCoordinator.addActiveDownstream(secondServer, STATE_LOCAL_ONLY); + verifyNoMoreInteractions(secondServer); + reset(firstServer, secondServer); + + // 3. No upstream. + mIPv6TetheringCoordinator.updateUpstreamNetworkState(null); + verify(secondServer).sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, null); + reset(firstServer, secondServer); + + // 4. Update ipv6 mobile upstream. + final UpstreamNetworkState mobileUpstream = createDualStackUpstream(TRANSPORT_CELLULAR); + final ArgumentCaptor lp = ArgumentCaptor.forClass(LinkProperties.class); + mIPv6TetheringCoordinator.updateUpstreamNetworkState(mobileUpstream); + verify(firstServer).sendMessage(eq(IpServer.CMD_IPV6_TETHER_UPDATE), eq(-1), eq(0), + lp.capture()); + final LinkProperties v6OnlyLink = lp.getValue(); + assertOnlyOneV6AddressAndNoV4(v6OnlyLink); + verifyNoMoreInteractions(firstServer); + verifyNoMoreInteractions(secondServer); + reset(firstServer, secondServer); + + // 5. Remove first IpServer. + mNotifyList.remove(firstServer); + mIPv6TetheringCoordinator.removeActiveDownstream(firstServer); + verify(firstServer).sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, null); + verify(secondServer).sendMessage(eq(IpServer.CMD_IPV6_TETHER_UPDATE), eq(-1), eq(0), + lp.capture()); + final LinkProperties localOnlyLink = lp.getValue(); + assertNotNull(localOnlyLink); + assertNotEquals(localOnlyLink, v6OnlyLink); + reset(firstServer, secondServer); + + // 6. Remove second IpServer. + mNotifyList.remove(secondServer); + mIPv6TetheringCoordinator.removeActiveDownstream(secondServer); + verifyNoMoreInteractions(firstServer); + verify(secondServer).sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, null); + } +} diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java new file mode 100644 index 0000000000..071a290e65 --- /dev/null +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/MockTetheringService.java @@ -0,0 +1,72 @@ +/* + * 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.networkstack.tethering; + +import static android.Manifest.permission.WRITE_SETTINGS; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + +import static org.mockito.Mockito.mock; + +import android.content.Context; +import android.content.Intent; +import android.net.ITetheringConnector; +import android.os.Binder; +import android.os.IBinder; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class MockTetheringService extends TetheringService { + private final Tethering mTethering = mock(Tethering.class); + + @Override + public IBinder onBind(Intent intent) { + return new MockTetheringConnector(super.onBind(intent)); + } + + @Override + public Tethering makeTethering(TetheringDependencies deps) { + return mTethering; + } + + @Override + boolean checkAndNoteWriteSettingsOperation(@NonNull Context context, int uid, + @NonNull String callingPackage, @Nullable String callingAttributionTag, + boolean throwException) { + // Test this does not verify the calling package / UID, as calling package could be shell + // and not match the UID. + return context.checkCallingOrSelfPermission(WRITE_SETTINGS) == PERMISSION_GRANTED; + } + + public Tethering getTethering() { + return mTethering; + } + + public class MockTetheringConnector extends Binder { + final IBinder mBase; + MockTetheringConnector(IBinder base) { + mBase = base; + } + + public ITetheringConnector getTetheringConnector() { + return ITetheringConnector.Stub.asInterface(mBase); + } + + public MockTetheringService getService() { + return MockTetheringService.this; + } + } +} diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java new file mode 100644 index 0000000000..d800816055 --- /dev/null +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java @@ -0,0 +1,916 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.ROAMING_NO; +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStats.UID_ALL; +import static android.net.NetworkStats.UID_TETHERING; +import static android.net.RouteInfo.RTN_UNICAST; +import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED; + +import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_IFACE; +import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_UID; +import static com.android.networkstack.tethering.OffloadHardwareInterface.ForwardedStats; +import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0; +import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_1; +import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS; +import static com.android.testutils.MiscAsserts.assertContainsAll; +import static com.android.testutils.MiscAsserts.assertThrows; +import static com.android.testutils.NetworkStatsUtilsKt.assertNetworkStatsEquals; + +import static junit.framework.Assert.assertNotNull; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.app.usage.NetworkStatsManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.NetworkStats; +import android.net.NetworkStats.Entry; +import android.net.RouteInfo; +import android.net.netstats.provider.NetworkStatsProvider; +import android.net.util.SharedLog; +import android.os.Handler; +import android.os.test.TestLooper; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.test.mock.MockContentResolver; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.test.FakeSettingsProvider; +import com.android.testutils.TestableNetworkStatsProviderCbBinder; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class OffloadControllerTest { + private static final String RNDIS0 = "test_rndis0"; + private static final String RMNET0 = "test_rmnet_data0"; + private static final String WLAN0 = "test_wlan0"; + + private static final String IPV6_LINKLOCAL = "fe80::/64"; + private static final String IPV6_DOC_PREFIX = "2001:db8::/64"; + private static final String IPV6_DISCARD_PREFIX = "100::/64"; + private static final String USB_PREFIX = "192.168.42.0/24"; + private static final String WIFI_PREFIX = "192.168.43.0/24"; + private static final long WAIT_FOR_IDLE_TIMEOUT = 2 * 1000; + + @Mock private OffloadHardwareInterface mHardware; + @Mock private ApplicationInfo mApplicationInfo; + @Mock private Context mContext; + @Mock private NetworkStatsManager mStatsManager; + @Mock private TetheringConfiguration mTetherConfig; + // Late init since methods must be called by the thread that created this object. + private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb; + private OffloadController.OffloadTetheringStatsProvider mTetherStatsProvider; + private final ArgumentCaptor mStringArrayCaptor = + ArgumentCaptor.forClass(ArrayList.class); + private final ArgumentCaptor mControlCallbackCaptor = + ArgumentCaptor.forClass(OffloadHardwareInterface.ControlCallback.class); + private MockContentResolver mContentResolver; + private final TestLooper mTestLooper = new TestLooper(); + private OffloadController.Dependencies mDeps = new OffloadController.Dependencies() { + @Override + public TetheringConfiguration getTetherConfig() { + return mTetherConfig; + } + }; + + @Before public void setUp() { + MockitoAnnotations.initMocks(this); + when(mContext.getApplicationInfo()).thenReturn(mApplicationInfo); + when(mContext.getPackageName()).thenReturn("OffloadControllerTest"); + mContentResolver = new MockContentResolver(mContext); + mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); + when(mContext.getContentResolver()).thenReturn(mContentResolver); + FakeSettingsProvider.clearSettingsProvider(); + when(mTetherConfig.getOffloadPollInterval()).thenReturn(-1); // Disabled. + } + + @After public void tearDown() throws Exception { + FakeSettingsProvider.clearSettingsProvider(); + } + + private void setupFunctioningHardwareInterface(int controlVersion) { + when(mHardware.initOffloadConfig()).thenReturn(true); + when(mHardware.initOffloadControl(mControlCallbackCaptor.capture())) + .thenReturn(controlVersion); + when(mHardware.setUpstreamParameters(anyString(), any(), any(), any())).thenReturn(true); + when(mHardware.getForwardedStats(any())).thenReturn(new ForwardedStats()); + when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(true); + when(mHardware.setDataWarningAndLimit(anyString(), anyLong(), anyLong())).thenReturn(true); + } + + private void enableOffload() { + Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0); + } + + private void setOffloadPollInterval(int interval) { + when(mTetherConfig.getOffloadPollInterval()).thenReturn(interval); + } + + private void waitForIdle() { + mTestLooper.dispatchAll(); + } + + private OffloadController makeOffloadController() throws Exception { + OffloadController offload = new OffloadController(new Handler(mTestLooper.getLooper()), + mHardware, mContentResolver, mStatsManager, new SharedLog("test"), mDeps); + final ArgumentCaptor + tetherStatsProviderCaptor = + ArgumentCaptor.forClass(OffloadController.OffloadTetheringStatsProvider.class); + verify(mStatsManager).registerNetworkStatsProvider(anyString(), + tetherStatsProviderCaptor.capture()); + reset(mStatsManager); + mTetherStatsProvider = tetherStatsProviderCaptor.getValue(); + assertNotNull(mTetherStatsProvider); + mTetherStatsProviderCb = new TestableNetworkStatsProviderCbBinder(); + mTetherStatsProvider.setProviderCallbackBinder(mTetherStatsProviderCb); + return offload; + } + + @Test + public void testStartStop() throws Exception { + stopOffloadController( + startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/)); + stopOffloadController( + startOffloadController(OFFLOAD_HAL_VERSION_1_1, true /*expectStart*/)); + } + + @NonNull + private OffloadController startOffloadController(int controlVersion, boolean expectStart) + throws Exception { + setupFunctioningHardwareInterface(controlVersion); + final OffloadController offload = makeOffloadController(); + offload.start(); + + final InOrder inOrder = inOrder(mHardware); + inOrder.verify(mHardware, times(1)).getDefaultTetherOffloadDisabled(); + inOrder.verify(mHardware, times(expectStart ? 1 : 0)).initOffloadConfig(); + inOrder.verify(mHardware, times(expectStart ? 1 : 0)).initOffloadControl( + any(OffloadHardwareInterface.ControlCallback.class)); + inOrder.verifyNoMoreInteractions(); + // Clear counters only instead of whole mock to preserve the mocking setup. + clearInvocations(mHardware); + return offload; + } + + private void stopOffloadController(final OffloadController offload) throws Exception { + final InOrder inOrder = inOrder(mHardware); + offload.stop(); + inOrder.verify(mHardware, times(1)).stopOffloadControl(); + inOrder.verifyNoMoreInteractions(); + reset(mHardware); + } + + @Test + public void testNoSettingsValueDefaultDisabledDoesNotStart() throws Exception { + when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(1); + assertThrows(SettingNotFoundException.class, () -> + Settings.Global.getInt(mContentResolver, TETHER_OFFLOAD_DISABLED)); + startOffloadController(OFFLOAD_HAL_VERSION_1_0, false /*expectStart*/); + } + + @Test + public void testNoSettingsValueDefaultEnabledDoesStart() throws Exception { + when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(0); + assertThrows(SettingNotFoundException.class, () -> + Settings.Global.getInt(mContentResolver, TETHER_OFFLOAD_DISABLED)); + startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/); + } + + @Test + public void testSettingsAllowsStart() throws Exception { + Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0); + startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/); + } + + @Test + public void testSettingsDisablesStart() throws Exception { + Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 1); + startOffloadController(OFFLOAD_HAL_VERSION_1_0, false /*expectStart*/); + } + + @Test + public void testSetUpstreamLinkPropertiesWorking() throws Exception { + enableOffload(); + final OffloadController offload = + startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/); + + // In reality, the UpstreamNetworkMonitor would have passed down to us + // a covering set of local prefixes representing a minimum essential + // set plus all the prefixes on networks with network agents. + // + // We simulate that there, and then add upstream elements one by one + // and watch what happens. + final Set minimumLocalPrefixes = new HashSet<>(); + for (String s : new String[]{ + "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64"}) { + minimumLocalPrefixes.add(new IpPrefix(s)); + } + offload.setLocalPrefixes(minimumLocalPrefixes); + final InOrder inOrder = inOrder(mHardware); + inOrder.verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture()); + ArrayList localPrefixes = mStringArrayCaptor.getValue(); + assertEquals(4, localPrefixes.size()); + assertContainsAll(localPrefixes, + "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64"); + inOrder.verifyNoMoreInteractions(); + + offload.setUpstreamLinkProperties(null); + // No change in local addresses means no call to setLocalPrefixes(). + inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); + // This LinkProperties value does not differ from the default upstream. + // There should be no extraneous call to setUpstreamParameters(). + inOrder.verify(mHardware, never()).setUpstreamParameters( + anyObject(), anyObject(), anyObject(), anyObject()); + inOrder.verifyNoMoreInteractions(); + + final LinkProperties lp = new LinkProperties(); + + final String testIfName = "rmnet_data17"; + lp.setInterfaceName(testIfName); + offload.setUpstreamLinkProperties(lp); + // No change in local addresses means no call to setLocalPrefixes(). + inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).setUpstreamParameters( + eq(testIfName), eq(null), eq(null), eq(null)); + inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE)); + inOrder.verifyNoMoreInteractions(); + + final String ipv4Addr = "192.0.2.5"; + final String linkAddr = ipv4Addr + "/24"; + lp.addLinkAddress(new LinkAddress(linkAddr)); + lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, null, RTN_UNICAST)); + offload.setUpstreamLinkProperties(lp); + // IPv4 prefixes and addresses on the upstream are simply left as whole + // prefixes (already passed in from UpstreamNetworkMonitor code). If a + // tethering client sends traffic to the IPv4 default router or other + // clients on the upstream this will not be hardware-forwarded, and that + // should be fine for now. Ergo: no change in local addresses, no call + // to setLocalPrefixes(). + inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).setUpstreamParameters( + eq(testIfName), eq(ipv4Addr), eq(null), eq(null)); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); + inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE)); + inOrder.verifyNoMoreInteractions(); + + final String ipv4Gateway = "192.0.2.1"; + lp.addRoute(new RouteInfo(null, InetAddress.getByName(ipv4Gateway), null, RTN_UNICAST)); + offload.setUpstreamLinkProperties(lp); + // No change in local addresses means no call to setLocalPrefixes(). + inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).setUpstreamParameters( + eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), eq(null)); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); + inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE)); + inOrder.verifyNoMoreInteractions(); + + final String ipv6Gw1 = "fe80::cafe"; + lp.addRoute(new RouteInfo(null, InetAddress.getByName(ipv6Gw1), null, RTN_UNICAST)); + offload.setUpstreamLinkProperties(lp); + // No change in local addresses means no call to setLocalPrefixes(). + inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).setUpstreamParameters( + eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); + ArrayList v6gws = mStringArrayCaptor.getValue(); + assertEquals(1, v6gws.size()); + assertTrue(v6gws.contains(ipv6Gw1)); + inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE)); + inOrder.verifyNoMoreInteractions(); + + final String ipv6Gw2 = "fe80::d00d"; + lp.addRoute(new RouteInfo(null, InetAddress.getByName(ipv6Gw2), null, RTN_UNICAST)); + offload.setUpstreamLinkProperties(lp); + // No change in local addresses means no call to setLocalPrefixes(). + inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).setUpstreamParameters( + eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); + v6gws = mStringArrayCaptor.getValue(); + assertEquals(2, v6gws.size()); + assertTrue(v6gws.contains(ipv6Gw1)); + assertTrue(v6gws.contains(ipv6Gw2)); + inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE)); + inOrder.verifyNoMoreInteractions(); + + final LinkProperties stacked = new LinkProperties(); + stacked.setInterfaceName("stacked"); + stacked.addLinkAddress(new LinkAddress("192.0.2.129/25")); + stacked.addRoute(new RouteInfo(null, InetAddress.getByName("192.0.2.254"), null, + RTN_UNICAST)); + stacked.addRoute(new RouteInfo(null, InetAddress.getByName("fe80::bad:f00"), null, + RTN_UNICAST)); + assertTrue(lp.addStackedLink(stacked)); + offload.setUpstreamLinkProperties(lp); + // No change in local addresses means no call to setLocalPrefixes(). + inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).setUpstreamParameters( + eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); + v6gws = mStringArrayCaptor.getValue(); + assertEquals(2, v6gws.size()); + assertTrue(v6gws.contains(ipv6Gw1)); + assertTrue(v6gws.contains(ipv6Gw2)); + inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE)); + inOrder.verifyNoMoreInteractions(); + + // Add in some IPv6 upstream info. When there is a tethered downstream + // making use of the IPv6 prefix we would expect to see the /64 route + // removed from "local prefixes" and /128s added for the upstream IPv6 + // addresses. This is not yet implemented, and for now we simply + // expect to see these /128s. + lp.addRoute(new RouteInfo(new IpPrefix("2001:db8::/64"), null, null, RTN_UNICAST)); + // "2001:db8::/64" plus "assigned" ASCII in hex + lp.addLinkAddress(new LinkAddress("2001:db8::6173:7369:676e:6564/64")); + // "2001:db8::/64" plus "random" ASCII in hex + lp.addLinkAddress(new LinkAddress("2001:db8::7261:6e64:6f6d/64")); + offload.setUpstreamLinkProperties(lp); + inOrder.verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture()); + localPrefixes = mStringArrayCaptor.getValue(); + assertEquals(6, localPrefixes.size()); + assertContainsAll(localPrefixes, + "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64", + "2001:db8::6173:7369:676e:6564/128", "2001:db8::7261:6e64:6f6d/128"); + // The relevant parts of the LinkProperties have not changed, but at the + // moment we do not de-dup upstream LinkProperties this carefully. + inOrder.verify(mHardware, times(1)).setUpstreamParameters( + eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture()); + v6gws = mStringArrayCaptor.getValue(); + assertEquals(2, v6gws.size()); + assertTrue(v6gws.contains(ipv6Gw1)); + assertTrue(v6gws.contains(ipv6Gw2)); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); + inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE)); + inOrder.verifyNoMoreInteractions(); + + // Completely identical LinkProperties updates are de-duped. + offload.setUpstreamLinkProperties(lp); + // This LinkProperties value does not differ from the default upstream. + // There should be no extraneous call to setUpstreamParameters(). + inOrder.verify(mHardware, never()).setUpstreamParameters( + anyObject(), anyObject(), anyObject(), anyObject()); + inOrder.verifyNoMoreInteractions(); + } + + private static @NonNull Entry buildTestEntry(@NonNull OffloadController.StatsType how, + @NonNull String iface, long rxBytes, long txBytes) { + return new Entry(iface, how == STATS_PER_IFACE ? UID_ALL : UID_TETHERING, SET_DEFAULT, + TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes, 0L, + txBytes, 0L, 0L); + } + + @Test + public void testGetForwardedStats() throws Exception { + enableOffload(); + final OffloadController offload = + startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/); + + final String ethernetIface = "eth1"; + final String mobileIface = "rmnet_data0"; + + when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn( + new ForwardedStats(12345, 54321)); + when(mHardware.getForwardedStats(eq(mobileIface))).thenReturn( + new ForwardedStats(999, 99999)); + + final InOrder inOrder = inOrder(mHardware); + + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(ethernetIface); + offload.setUpstreamLinkProperties(lp); + // Previous upstream was null, so no stats are fetched. + inOrder.verify(mHardware, never()).getForwardedStats(any()); + + lp.setInterfaceName(mobileIface); + offload.setUpstreamLinkProperties(lp); + // Expect that we fetch stats from the previous upstream. + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(ethernetIface)); + + lp.setInterfaceName(ethernetIface); + offload.setUpstreamLinkProperties(lp); + // Expect that we fetch stats from the previous upstream. + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(mobileIface)); + + // Verify that the fetched stats are stored. + final NetworkStats ifaceStats = mTetherStatsProvider.getTetherStats(STATS_PER_IFACE); + final NetworkStats uidStats = mTetherStatsProvider.getTetherStats(STATS_PER_UID); + final NetworkStats expectedIfaceStats = new NetworkStats(0L, 2) + .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 999, 99999)) + .addEntry(buildTestEntry(STATS_PER_IFACE, ethernetIface, 12345, 54321)); + + final NetworkStats expectedUidStats = new NetworkStats(0L, 2) + .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 999, 99999)) + .addEntry(buildTestEntry(STATS_PER_UID, ethernetIface, 12345, 54321)); + + assertNetworkStatsEquals(expectedIfaceStats, ifaceStats); + assertNetworkStatsEquals(expectedUidStats, uidStats); + + // Force pushing stats update to verify the stats reported. + mTetherStatsProvider.pushTetherStats(); + mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStats, expectedUidStats); + + when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn( + new ForwardedStats(100000, 100000)); + offload.setUpstreamLinkProperties(null); + // Expect that we first clear the HAL's upstream parameters. + inOrder.verify(mHardware, times(1)).setUpstreamParameters( + eq(""), eq("0.0.0.0"), eq("0.0.0.0"), eq(null)); + // Expect that we fetch stats from the previous upstream. + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(ethernetIface)); + + // There is no current upstream, so no stats are fetched. + inOrder.verify(mHardware, never()).getForwardedStats(any()); + inOrder.verifyNoMoreInteractions(); + + // Verify that the stored stats is accumulated. + final NetworkStats ifaceStatsAccu = mTetherStatsProvider.getTetherStats(STATS_PER_IFACE); + final NetworkStats uidStatsAccu = mTetherStatsProvider.getTetherStats(STATS_PER_UID); + final NetworkStats expectedIfaceStatsAccu = new NetworkStats(0L, 2) + .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 999, 99999)) + .addEntry(buildTestEntry(STATS_PER_IFACE, ethernetIface, 112345, 154321)); + + final NetworkStats expectedUidStatsAccu = new NetworkStats(0L, 2) + .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 999, 99999)) + .addEntry(buildTestEntry(STATS_PER_UID, ethernetIface, 112345, 154321)); + + assertNetworkStatsEquals(expectedIfaceStatsAccu, ifaceStatsAccu); + assertNetworkStatsEquals(expectedUidStatsAccu, uidStatsAccu); + + // Verify that only diff of stats is reported. + mTetherStatsProvider.pushTetherStats(); + final NetworkStats expectedIfaceStatsDiff = new NetworkStats(0L, 2) + .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 0, 0)) + .addEntry(buildTestEntry(STATS_PER_IFACE, ethernetIface, 100000, 100000)); + + final NetworkStats expectedUidStatsDiff = new NetworkStats(0L, 2) + .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 0, 0)) + .addEntry(buildTestEntry(STATS_PER_UID, ethernetIface, 100000, 100000)); + mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStatsDiff, + expectedUidStatsDiff); + } + + /** + * Test OffloadController with different combinations of HAL and framework versions can set + * data warning and/or limit correctly. + */ + @Test + public void testSetDataWarningAndLimit() throws Exception { + // Verify the OffloadController is called by R framework, where the framework doesn't send + // warning. + checkSetDataWarningAndLimit(false, OFFLOAD_HAL_VERSION_1_0); + checkSetDataWarningAndLimit(false, OFFLOAD_HAL_VERSION_1_1); + // Verify the OffloadController is called by S+ framework, where the framework sends + // warning along with limit. + checkSetDataWarningAndLimit(true, OFFLOAD_HAL_VERSION_1_0); + checkSetDataWarningAndLimit(true, OFFLOAD_HAL_VERSION_1_1); + } + + private void checkSetDataWarningAndLimit(boolean isProviderSetWarning, int controlVersion) + throws Exception { + enableOffload(); + final OffloadController offload = + startOffloadController(controlVersion, true /*expectStart*/); + + final String ethernetIface = "eth1"; + final String mobileIface = "rmnet_data0"; + final long ethernetLimit = 12345; + final long mobileWarning = 123456; + final long mobileLimit = 12345678; + + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(ethernetIface); + + final InOrder inOrder = inOrder(mHardware); + when(mHardware.setUpstreamParameters( + any(), any(), any(), any())).thenReturn(true); + when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(true); + when(mHardware.setDataWarningAndLimit(anyString(), anyLong(), anyLong())).thenReturn(true); + offload.setUpstreamLinkProperties(lp); + // Applying an interface sends the initial quota to the hardware. + if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) { + inOrder.verify(mHardware).setDataWarningAndLimit(ethernetIface, Long.MAX_VALUE, + Long.MAX_VALUE); + } else { + inOrder.verify(mHardware).setDataLimit(ethernetIface, Long.MAX_VALUE); + } + inOrder.verifyNoMoreInteractions(); + + // Verify that set to unlimited again won't cause duplicated calls to the hardware. + if (isProviderSetWarning) { + mTetherStatsProvider.onSetWarningAndLimit(ethernetIface, + NetworkStatsProvider.QUOTA_UNLIMITED, NetworkStatsProvider.QUOTA_UNLIMITED); + } else { + mTetherStatsProvider.onSetLimit(ethernetIface, NetworkStatsProvider.QUOTA_UNLIMITED); + } + waitForIdle(); + inOrder.verifyNoMoreInteractions(); + + // Applying an interface quota to the current upstream immediately sends it to the hardware. + if (isProviderSetWarning) { + mTetherStatsProvider.onSetWarningAndLimit(ethernetIface, + NetworkStatsProvider.QUOTA_UNLIMITED, ethernetLimit); + } else { + mTetherStatsProvider.onSetLimit(ethernetIface, ethernetLimit); + } + waitForIdle(); + if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) { + inOrder.verify(mHardware).setDataWarningAndLimit(ethernetIface, Long.MAX_VALUE, + ethernetLimit); + } else { + inOrder.verify(mHardware).setDataLimit(ethernetIface, ethernetLimit); + } + inOrder.verifyNoMoreInteractions(); + + // Applying an interface quota to another upstream does not take any immediate action. + if (isProviderSetWarning) { + mTetherStatsProvider.onSetWarningAndLimit(mobileIface, mobileWarning, mobileLimit); + } else { + mTetherStatsProvider.onSetLimit(mobileIface, mobileLimit); + } + waitForIdle(); + if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) { + inOrder.verify(mHardware, never()).setDataWarningAndLimit(anyString(), anyLong(), + anyLong()); + } else { + inOrder.verify(mHardware, never()).setDataLimit(anyString(), anyLong()); + } + + // Switching to that upstream causes the quota to be applied if the parameters were applied + // correctly. + lp.setInterfaceName(mobileIface); + offload.setUpstreamLinkProperties(lp); + waitForIdle(); + if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) { + inOrder.verify(mHardware).setDataWarningAndLimit(mobileIface, + isProviderSetWarning ? mobileWarning : Long.MAX_VALUE, + mobileLimit); + } else { + inOrder.verify(mHardware).setDataLimit(mobileIface, mobileLimit); + } + + // Setting a limit of NetworkStatsProvider.QUOTA_UNLIMITED causes the limit to be set + // to Long.MAX_VALUE. + if (isProviderSetWarning) { + mTetherStatsProvider.onSetWarningAndLimit(mobileIface, + NetworkStatsProvider.QUOTA_UNLIMITED, NetworkStatsProvider.QUOTA_UNLIMITED); + } else { + mTetherStatsProvider.onSetLimit(mobileIface, NetworkStatsProvider.QUOTA_UNLIMITED); + } + waitForIdle(); + if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) { + inOrder.verify(mHardware).setDataWarningAndLimit(mobileIface, Long.MAX_VALUE, + Long.MAX_VALUE); + } else { + inOrder.verify(mHardware).setDataLimit(mobileIface, Long.MAX_VALUE); + } + + // If setting upstream parameters fails, then the data warning and limit is not set. + when(mHardware.setUpstreamParameters(any(), any(), any(), any())).thenReturn(false); + lp.setInterfaceName(ethernetIface); + offload.setUpstreamLinkProperties(lp); + if (isProviderSetWarning) { + mTetherStatsProvider.onSetWarningAndLimit(mobileIface, mobileWarning, mobileLimit); + } else { + mTetherStatsProvider.onSetLimit(mobileIface, mobileLimit); + } + waitForIdle(); + inOrder.verify(mHardware, never()).setDataLimit(anyString(), anyLong()); + inOrder.verify(mHardware, never()).setDataWarningAndLimit(anyString(), anyLong(), + anyLong()); + + // If setting the data warning and/or limit fails while changing upstreams, offload is + // stopped. + when(mHardware.setUpstreamParameters(any(), any(), any(), any())).thenReturn(true); + when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(false); + when(mHardware.setDataWarningAndLimit(anyString(), anyLong(), anyLong())).thenReturn(false); + lp.setInterfaceName(mobileIface); + offload.setUpstreamLinkProperties(lp); + if (isProviderSetWarning) { + mTetherStatsProvider.onSetWarningAndLimit(mobileIface, mobileWarning, mobileLimit); + } else { + mTetherStatsProvider.onSetLimit(mobileIface, mobileLimit); + } + waitForIdle(); + inOrder.verify(mHardware).getForwardedStats(ethernetIface); + inOrder.verify(mHardware).stopOffloadControl(); + } + + @Test + public void testDataWarningAndLimitCallback() throws Exception { + enableOffload(); + startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/); + + OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue(); + callback.onStoppedLimitReached(); + mTetherStatsProviderCb.expectNotifyStatsUpdated(); + mTetherStatsProviderCb.expectNotifyWarningOrLimitReached(); + + startOffloadController(OFFLOAD_HAL_VERSION_1_1, true /*expectStart*/); + callback = mControlCallbackCaptor.getValue(); + callback.onWarningReached(); + mTetherStatsProviderCb.expectNotifyStatsUpdated(); + mTetherStatsProviderCb.expectNotifyWarningOrLimitReached(); + } + + @Test + public void testAddRemoveDownstreams() throws Exception { + enableOffload(); + final OffloadController offload = + startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/); + final InOrder inOrder = inOrder(mHardware); + + // Tethering makes several calls to setLocalPrefixes() before add/remove + // downstream calls are made. This is not tested here; only the behavior + // of notifyDownstreamLinkProperties() and removeDownstreamInterface() + // are tested. + + // [1] USB tethering is started. + final LinkProperties usbLinkProperties = new LinkProperties(); + usbLinkProperties.setInterfaceName(RNDIS0); + usbLinkProperties.addLinkAddress(new LinkAddress("192.168.42.1/24")); + usbLinkProperties.addRoute( + new RouteInfo(new IpPrefix(USB_PREFIX), null, null, RTN_UNICAST)); + offload.notifyDownstreamLinkProperties(usbLinkProperties); + inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, USB_PREFIX); + inOrder.verifyNoMoreInteractions(); + + // [2] Routes for IPv6 link-local prefixes should never be added. + usbLinkProperties.addRoute( + new RouteInfo(new IpPrefix(IPV6_LINKLOCAL), null, null, RTN_UNICAST)); + offload.notifyDownstreamLinkProperties(usbLinkProperties); + inOrder.verify(mHardware, never()).addDownstreamPrefix(eq(RNDIS0), anyString()); + inOrder.verifyNoMoreInteractions(); + + // [3] Add an IPv6 prefix for good measure. Only new offload-able + // prefixes should be passed to the HAL. + usbLinkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64")); + usbLinkProperties.addRoute( + new RouteInfo(new IpPrefix(IPV6_DOC_PREFIX), null, null, RTN_UNICAST)); + offload.notifyDownstreamLinkProperties(usbLinkProperties); + inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, IPV6_DOC_PREFIX); + inOrder.verifyNoMoreInteractions(); + + // [4] Adding addresses doesn't affect notifyDownstreamLinkProperties(). + // The address is passed in by a separate setLocalPrefixes() invocation. + usbLinkProperties.addLinkAddress(new LinkAddress("2001:db8::2/64")); + offload.notifyDownstreamLinkProperties(usbLinkProperties); + inOrder.verify(mHardware, never()).addDownstreamPrefix(eq(RNDIS0), anyString()); + + // [5] Differences in local routes are converted into addDownstream() + // and removeDownstream() invocations accordingly. + usbLinkProperties.removeRoute( + new RouteInfo(new IpPrefix(IPV6_DOC_PREFIX), null, RNDIS0, RTN_UNICAST)); + usbLinkProperties.addRoute( + new RouteInfo(new IpPrefix(IPV6_DISCARD_PREFIX), null, null, RTN_UNICAST)); + offload.notifyDownstreamLinkProperties(usbLinkProperties); + inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, IPV6_DOC_PREFIX); + inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, IPV6_DISCARD_PREFIX); + inOrder.verifyNoMoreInteractions(); + + // [6] Removing a downstream interface which was never added causes no + // interactions with the HAL. + offload.removeDownstreamInterface(WLAN0); + inOrder.verifyNoMoreInteractions(); + + // [7] Removing an active downstream removes all remaining prefixes. + offload.removeDownstreamInterface(RNDIS0); + inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, USB_PREFIX); + inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, IPV6_DISCARD_PREFIX); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testControlCallbackOnStoppedUnsupportedFetchesAllStats() throws Exception { + enableOffload(); + final OffloadController offload = + startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/); + + // Pretend to set a few different upstreams (only the interface name + // matters for this test; we're ignoring IP and route information). + final LinkProperties upstreamLp = new LinkProperties(); + for (String ifname : new String[]{RMNET0, WLAN0, RMNET0}) { + upstreamLp.setInterfaceName(ifname); + offload.setUpstreamLinkProperties(upstreamLp); + } + + // Clear invocation history, especially the getForwardedStats() calls + // that happen with setUpstreamParameters(). + clearInvocations(mHardware); + + OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue(); + callback.onStoppedUnsupported(); + + // Verify forwarded stats behaviour. + verify(mHardware, times(1)).getForwardedStats(eq(RMNET0)); + verify(mHardware, times(1)).getForwardedStats(eq(WLAN0)); + // TODO: verify the exact stats reported. + mTetherStatsProviderCb.expectNotifyStatsUpdated(); + mTetherStatsProviderCb.assertNoCallback(); + verifyNoMoreInteractions(mHardware); + } + + @Test + public void testControlCallbackOnSupportAvailableFetchesAllStatsAndPushesAllParameters() + throws Exception { + enableOffload(); + final OffloadController offload = + startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/); + + // Pretend to set a few different upstreams (only the interface name + // matters for this test; we're ignoring IP and route information). + final LinkProperties upstreamLp = new LinkProperties(); + for (String ifname : new String[]{RMNET0, WLAN0, RMNET0}) { + upstreamLp.setInterfaceName(ifname); + offload.setUpstreamLinkProperties(upstreamLp); + } + + // Pretend that some local prefixes and downstreams have been added + // (and removed, for good measure). + final Set minimumLocalPrefixes = new HashSet<>(); + for (String s : new String[]{ + "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64"}) { + minimumLocalPrefixes.add(new IpPrefix(s)); + } + offload.setLocalPrefixes(minimumLocalPrefixes); + + final LinkProperties usbLinkProperties = new LinkProperties(); + usbLinkProperties.setInterfaceName(RNDIS0); + usbLinkProperties.addLinkAddress(new LinkAddress("192.168.42.1/24")); + usbLinkProperties.addRoute( + new RouteInfo(new IpPrefix(USB_PREFIX), null, null, RTN_UNICAST)); + offload.notifyDownstreamLinkProperties(usbLinkProperties); + + final LinkProperties wifiLinkProperties = new LinkProperties(); + wifiLinkProperties.setInterfaceName(WLAN0); + wifiLinkProperties.addLinkAddress(new LinkAddress("192.168.43.1/24")); + wifiLinkProperties.addRoute( + new RouteInfo(new IpPrefix(WIFI_PREFIX), null, null, RTN_UNICAST)); + wifiLinkProperties.addRoute( + new RouteInfo(new IpPrefix(IPV6_LINKLOCAL), null, null, RTN_UNICAST)); + // Use a benchmark prefix (RFC 5180 + erratum), since the documentation + // prefix is included in the excluded prefix list. + wifiLinkProperties.addLinkAddress(new LinkAddress("2001:2::1/64")); + wifiLinkProperties.addLinkAddress(new LinkAddress("2001:2::2/64")); + wifiLinkProperties.addRoute( + new RouteInfo(new IpPrefix("2001:2::/64"), null, null, RTN_UNICAST)); + offload.notifyDownstreamLinkProperties(wifiLinkProperties); + + offload.removeDownstreamInterface(RNDIS0); + + // Clear invocation history, especially the getForwardedStats() calls + // that happen with setUpstreamParameters(). + clearInvocations(mHardware); + + OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue(); + callback.onSupportAvailable(); + + // Verify forwarded stats behaviour. + verify(mHardware, times(1)).getForwardedStats(eq(RMNET0)); + verify(mHardware, times(1)).getForwardedStats(eq(WLAN0)); + mTetherStatsProviderCb.expectNotifyStatsUpdated(); + mTetherStatsProviderCb.assertNoCallback(); + + // TODO: verify local prefixes and downstreams are also pushed to the HAL. + verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture()); + ArrayList localPrefixes = mStringArrayCaptor.getValue(); + assertEquals(4, localPrefixes.size()); + assertContainsAll(localPrefixes, + // TODO: The logic to find and exclude downstream IP prefixes + // is currently in Tethering's OffloadWrapper but must be moved + // into OffloadController proper. After this, also check for: + // "192.168.43.1/32", "2001:2::1/128", "2001:2::2/128" + "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64"); + verify(mHardware, times(1)).addDownstreamPrefix(WLAN0, "192.168.43.0/24"); + verify(mHardware, times(1)).addDownstreamPrefix(WLAN0, "2001:2::/64"); + verify(mHardware, times(1)).setUpstreamParameters(eq(RMNET0), any(), any(), any()); + verify(mHardware, times(1)).setDataLimit(eq(RMNET0), anyLong()); + verifyNoMoreInteractions(mHardware); + } + + @Test + public void testOnSetAlert() throws Exception { + enableOffload(); + setOffloadPollInterval(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); + final OffloadController offload = + startOffloadController(OFFLOAD_HAL_VERSION_1_0, true /*expectStart*/); + + // Initialize with fake eth upstream. + final String ethernetIface = "eth1"; + InOrder inOrder = inOrder(mHardware); + offload.setUpstreamLinkProperties(makeEthernetLinkProperties()); + // Previous upstream was null, so no stats are fetched. + inOrder.verify(mHardware, never()).getForwardedStats(any()); + + // Verify that set quota to 0 will immediately triggers an callback. + mTetherStatsProvider.onSetAlert(0); + waitForIdle(); + mTetherStatsProviderCb.expectNotifyAlertReached(); + + // Verify that notifyAlertReached never fired if quota is not yet reached. + when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn( + new ForwardedStats(0, 0)); + mTetherStatsProvider.onSetAlert(100); + mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); + waitForIdle(); + mTetherStatsProviderCb.assertNoCallback(); + + // Verify that notifyAlertReached fired when quota is reached. + when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn( + new ForwardedStats(50, 50)); + mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); + waitForIdle(); + mTetherStatsProviderCb.expectNotifyAlertReached(); + + // Verify that set quota with UNLIMITED won't trigger any callback, and won't fetch + // any stats since the polling is stopped. + reset(mHardware); + mTetherStatsProvider.onSetAlert(NetworkStatsProvider.QUOTA_UNLIMITED); + mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); + waitForIdle(); + mTetherStatsProviderCb.assertNoCallback(); + verify(mHardware, never()).getForwardedStats(any()); + } + + private static LinkProperties makeEthernetLinkProperties() { + final String ethernetIface = "eth1"; + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(ethernetIface); + return lp; + } + + private void checkSoftwarePollingUsed(int controlVersion) throws Exception { + enableOffload(); + setOffloadPollInterval(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); + OffloadController offload = + startOffloadController(controlVersion, true /*expectStart*/); + offload.setUpstreamLinkProperties(makeEthernetLinkProperties()); + mTetherStatsProvider.onSetAlert(0); + waitForIdle(); + if (controlVersion >= OFFLOAD_HAL_VERSION_1_1) { + mTetherStatsProviderCb.assertNoCallback(); + } else { + mTetherStatsProviderCb.expectNotifyAlertReached(); + } + verify(mHardware, never()).getForwardedStats(any()); + } + + @Test + public void testSoftwarePollingUsed() throws Exception { + checkSoftwarePollingUsed(OFFLOAD_HAL_VERSION_1_0); + checkSoftwarePollingUsed(OFFLOAD_HAL_VERSION_1_1); + } +} diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java new file mode 100644 index 0000000000..a8b3b92de9 --- /dev/null +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadHardwareInterfaceTest.java @@ -0,0 +1,320 @@ +/* + * 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.networkstack.tethering; + +import static android.net.util.TetheringUtils.uint16; +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.AF_UNIX; +import static android.system.OsConstants.SOCK_STREAM; + +import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0; +import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_1; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.tetheroffload.config.V1_0.IOffloadConfig; +import android.hardware.tetheroffload.control.V1_0.IOffloadControl; +import android.hardware.tetheroffload.control.V1_0.NatTimeoutUpdate; +import android.hardware.tetheroffload.control.V1_0.NetworkProtocol; +import android.hardware.tetheroffload.control.V1_1.ITetheringOffloadCallback; +import android.hardware.tetheroffload.control.V1_1.OffloadCallbackEvent; +import android.net.netlink.StructNfGenMsg; +import android.net.netlink.StructNlMsgHdr; +import android.net.util.SharedLog; +import android.os.Handler; +import android.os.NativeHandle; +import android.os.test.TestLooper; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; +import android.util.Pair; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.FileDescriptor; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public final class OffloadHardwareInterfaceTest { + private static final String RMNET0 = "test_rmnet_data0"; + + private final TestLooper mTestLooper = new TestLooper(); + + private OffloadHardwareInterface mOffloadHw; + private ITetheringOffloadCallback mTetheringOffloadCallback; + private OffloadHardwareInterface.ControlCallback mControlCallback; + + @Mock private IOffloadConfig mIOffloadConfig; + private IOffloadControl mIOffloadControl; + @Mock private NativeHandle mNativeHandle; + + // Random values to test Netlink message. + private static final short TEST_TYPE = 184; + private static final short TEST_FLAGS = 263; + + class MyDependencies extends OffloadHardwareInterface.Dependencies { + private final int mMockControlVersion; + MyDependencies(SharedLog log, final int mockControlVersion) { + super(log); + mMockControlVersion = mockControlVersion; + } + + @Override + public IOffloadConfig getOffloadConfig() { + return mIOffloadConfig; + } + + @Override + public Pair getOffloadControl() { + switch (mMockControlVersion) { + case OFFLOAD_HAL_VERSION_1_0: + mIOffloadControl = mock(IOffloadControl.class); + break; + case OFFLOAD_HAL_VERSION_1_1: + mIOffloadControl = + mock(android.hardware.tetheroffload.control.V1_1.IOffloadControl.class); + break; + default: + throw new IllegalArgumentException("Invalid offload control version " + + mMockControlVersion); + } + return new Pair(mIOffloadControl, mMockControlVersion); + } + + @Override + public NativeHandle createConntrackSocket(final int groups) { + return mNativeHandle; + } + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mControlCallback = spy(new OffloadHardwareInterface.ControlCallback()); + } + + private void startOffloadHardwareInterface(int controlVersion) throws Exception { + final SharedLog log = new SharedLog("test"); + mOffloadHw = new OffloadHardwareInterface(new Handler(mTestLooper.getLooper()), log, + new MyDependencies(log, controlVersion)); + mOffloadHw.initOffloadConfig(); + mOffloadHw.initOffloadControl(mControlCallback); + final ArgumentCaptor mOffloadCallbackCaptor = + ArgumentCaptor.forClass(ITetheringOffloadCallback.class); + verify(mIOffloadControl).initOffload(mOffloadCallbackCaptor.capture(), any()); + mTetheringOffloadCallback = mOffloadCallbackCaptor.getValue(); + } + + @Test + public void testGetForwardedStats() throws Exception { + startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0); + final OffloadHardwareInterface.ForwardedStats stats = mOffloadHw.getForwardedStats(RMNET0); + verify(mIOffloadControl).getForwardedStats(eq(RMNET0), any()); + assertNotNull(stats); + } + + @Test + public void testSetLocalPrefixes() throws Exception { + startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0); + final ArrayList localPrefixes = new ArrayList<>(); + localPrefixes.add("127.0.0.0/8"); + localPrefixes.add("fe80::/64"); + mOffloadHw.setLocalPrefixes(localPrefixes); + verify(mIOffloadControl).setLocalPrefixes(eq(localPrefixes), any()); + } + + @Test + public void testSetDataLimit() throws Exception { + startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0); + final long limit = 12345; + mOffloadHw.setDataLimit(RMNET0, limit); + verify(mIOffloadControl).setDataLimit(eq(RMNET0), eq(limit), any()); + } + + @Test + public void testSetDataWarningAndLimit() throws Exception { + // Verify V1.0 control HAL would reject the function call with exception. + startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0); + final long warning = 12345; + final long limit = 67890; + assertThrows(IllegalArgumentException.class, + () -> mOffloadHw.setDataWarningAndLimit(RMNET0, warning, limit)); + reset(mIOffloadControl); + + // Verify V1.1 control HAL could receive this function call. + startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_1); + mOffloadHw.setDataWarningAndLimit(RMNET0, warning, limit); + verify((android.hardware.tetheroffload.control.V1_1.IOffloadControl) mIOffloadControl) + .setDataWarningAndLimit(eq(RMNET0), eq(warning), eq(limit), any()); + } + + @Test + public void testSetUpstreamParameters() throws Exception { + startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0); + final String v4addr = "192.168.10.1"; + final String v4gateway = "192.168.10.255"; + final ArrayList v6gws = new ArrayList<>(0); + v6gws.add("2001:db8::1"); + mOffloadHw.setUpstreamParameters(RMNET0, v4addr, v4gateway, v6gws); + verify(mIOffloadControl).setUpstreamParameters(eq(RMNET0), eq(v4addr), eq(v4gateway), + eq(v6gws), any()); + + final ArgumentCaptor> mArrayListCaptor = + ArgumentCaptor.forClass(ArrayList.class); + mOffloadHw.setUpstreamParameters(null, null, null, null); + verify(mIOffloadControl).setUpstreamParameters(eq(""), eq(""), eq(""), + mArrayListCaptor.capture(), any()); + assertEquals(mArrayListCaptor.getValue().size(), 0); + } + + @Test + public void testUpdateDownstreamPrefix() throws Exception { + startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0); + final String ifName = "wlan1"; + final String prefix = "192.168.43.0/24"; + mOffloadHw.addDownstreamPrefix(ifName, prefix); + verify(mIOffloadControl).addDownstream(eq(ifName), eq(prefix), any()); + + mOffloadHw.removeDownstreamPrefix(ifName, prefix); + verify(mIOffloadControl).removeDownstream(eq(ifName), eq(prefix), any()); + } + + @Test + public void testTetheringOffloadCallback() throws Exception { + startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0); + + mTetheringOffloadCallback.onEvent(OffloadCallbackEvent.OFFLOAD_STARTED); + mTestLooper.dispatchAll(); + verify(mControlCallback).onStarted(); + + mTetheringOffloadCallback.onEvent(OffloadCallbackEvent.OFFLOAD_STOPPED_ERROR); + mTestLooper.dispatchAll(); + verify(mControlCallback).onStoppedError(); + + mTetheringOffloadCallback.onEvent(OffloadCallbackEvent.OFFLOAD_STOPPED_UNSUPPORTED); + mTestLooper.dispatchAll(); + verify(mControlCallback).onStoppedUnsupported(); + + mTetheringOffloadCallback.onEvent(OffloadCallbackEvent.OFFLOAD_SUPPORT_AVAILABLE); + mTestLooper.dispatchAll(); + verify(mControlCallback).onSupportAvailable(); + + mTetheringOffloadCallback.onEvent(OffloadCallbackEvent.OFFLOAD_STOPPED_LIMIT_REACHED); + mTestLooper.dispatchAll(); + verify(mControlCallback).onStoppedLimitReached(); + + final NatTimeoutUpdate tcpParams = buildNatTimeoutUpdate(NetworkProtocol.TCP); + mTetheringOffloadCallback.updateTimeout(tcpParams); + mTestLooper.dispatchAll(); + verify(mControlCallback).onNatTimeoutUpdate(eq(OsConstants.IPPROTO_TCP), + eq(tcpParams.src.addr), + eq(uint16(tcpParams.src.port)), + eq(tcpParams.dst.addr), + eq(uint16(tcpParams.dst.port))); + + final NatTimeoutUpdate udpParams = buildNatTimeoutUpdate(NetworkProtocol.UDP); + mTetheringOffloadCallback.updateTimeout(udpParams); + mTestLooper.dispatchAll(); + verify(mControlCallback).onNatTimeoutUpdate(eq(OsConstants.IPPROTO_UDP), + eq(udpParams.src.addr), + eq(uint16(udpParams.src.port)), + eq(udpParams.dst.addr), + eq(uint16(udpParams.dst.port))); + reset(mControlCallback); + + startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_1); + + // Verify the interface will process the events that comes from V1.1 HAL. + mTetheringOffloadCallback.onEvent_1_1(OffloadCallbackEvent.OFFLOAD_STARTED); + mTestLooper.dispatchAll(); + final InOrder inOrder = inOrder(mControlCallback); + inOrder.verify(mControlCallback).onStarted(); + inOrder.verifyNoMoreInteractions(); + + mTetheringOffloadCallback.onEvent_1_1(OffloadCallbackEvent.OFFLOAD_WARNING_REACHED); + mTestLooper.dispatchAll(); + inOrder.verify(mControlCallback).onWarningReached(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testSendIpv4NfGenMsg() throws Exception { + startOffloadHardwareInterface(OFFLOAD_HAL_VERSION_1_0); + FileDescriptor writeSocket = new FileDescriptor(); + FileDescriptor readSocket = new FileDescriptor(); + try { + Os.socketpair(AF_UNIX, SOCK_STREAM, 0, writeSocket, readSocket); + } catch (ErrnoException e) { + fail(); + return; + } + when(mNativeHandle.getFileDescriptor()).thenReturn(writeSocket); + + mOffloadHw.sendIpv4NfGenMsg(mNativeHandle, TEST_TYPE, TEST_FLAGS); + + ByteBuffer buffer = ByteBuffer.allocate(9823); // Arbitrary value > expectedLen. + buffer.order(ByteOrder.nativeOrder()); + + int read = Os.read(readSocket, buffer); + final int expectedLen = StructNlMsgHdr.STRUCT_SIZE + StructNfGenMsg.STRUCT_SIZE; + assertEquals(expectedLen, read); + + buffer.flip(); + assertEquals(expectedLen, buffer.getInt()); + assertEquals(TEST_TYPE, buffer.getShort()); + assertEquals(TEST_FLAGS, buffer.getShort()); + assertEquals(0 /* seq */, buffer.getInt()); + assertEquals(0 /* pid */, buffer.getInt()); + assertEquals(AF_INET, buffer.get()); // nfgen_family + assertEquals(0 /* error */, buffer.get()); // version + assertEquals(0 /* error */, buffer.getShort()); // res_id + assertEquals(expectedLen, buffer.position()); + } + + private NatTimeoutUpdate buildNatTimeoutUpdate(final int proto) { + final NatTimeoutUpdate params = new NatTimeoutUpdate(); + params.proto = proto; + params.src.addr = "192.168.43.200"; + params.src.port = 100; + params.dst.addr = "172.50.46.169"; + params.dst.port = 150; + return params; + } +} diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java new file mode 100644 index 0000000000..41d46e522c --- /dev/null +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java @@ -0,0 +1,551 @@ +/* + * 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.networkstack.tethering; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.TetheringManager.TETHERING_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.util.PrefixUtils.asIpPrefix; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.ip.IpServer; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Arrays; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public final class PrivateAddressCoordinatorTest { + private static final String TEST_IFNAME = "test0"; + + @Mock private IpServer mHotspotIpServer; + @Mock private IpServer mUsbIpServer; + @Mock private IpServer mEthernetIpServer; + @Mock private IpServer mWifiP2pIpServer; + @Mock private Context mContext; + @Mock private ConnectivityManager mConnectivityMgr; + @Mock private TetheringConfiguration mConfig; + + private PrivateAddressCoordinator mPrivateAddressCoordinator; + private final LinkAddress mBluetoothAddress = new LinkAddress("192.168.44.1/24"); + private final LinkAddress mLegacyWifiP2pAddress = new LinkAddress("192.168.49.1/24"); + private final Network mWifiNetwork = new Network(1); + private final Network mMobileNetwork = new Network(2); + private final Network mVpnNetwork = new Network(3); + private final Network mMobileNetwork2 = new Network(4); + private final Network mMobileNetwork3 = new Network(5); + private final Network mMobileNetwork4 = new Network(6); + private final Network mMobileNetwork5 = new Network(7); + private final Network mMobileNetwork6 = new Network(8); + private final Network[] mAllNetworks = {mMobileNetwork, mWifiNetwork, mVpnNetwork, + mMobileNetwork2, mMobileNetwork3, mMobileNetwork4, mMobileNetwork5, mMobileNetwork6}; + private final ArrayList mTetheringPrefixes = new ArrayList<>(Arrays.asList( + new IpPrefix("192.168.0.0/16"), + new IpPrefix("172.16.0.0/12"), + new IpPrefix("10.0.0.0/8"))); + + private void setUpIpServers() throws Exception { + when(mUsbIpServer.interfaceType()).thenReturn(TETHERING_USB); + when(mEthernetIpServer.interfaceType()).thenReturn(TETHERING_ETHERNET); + when(mHotspotIpServer.interfaceType()).thenReturn(TETHERING_WIFI); + when(mWifiP2pIpServer.interfaceType()).thenReturn(TETHERING_WIFI_P2P); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(mConnectivityMgr); + when(mConnectivityMgr.getAllNetworks()).thenReturn(mAllNetworks); + when(mConfig.shouldEnableWifiP2pDedicatedIp()).thenReturn(false); + when(mConfig.isSelectAllPrefixRangeEnabled()).thenReturn(true); + setUpIpServers(); + mPrivateAddressCoordinator = spy(new PrivateAddressCoordinator(mContext, mConfig)); + } + + private LinkAddress requestDownstreamAddress(final IpServer ipServer, boolean useLastAddress) { + final LinkAddress address = mPrivateAddressCoordinator.requestDownstreamAddress( + ipServer, useLastAddress); + when(ipServer.getAddress()).thenReturn(address); + return address; + } + + @Test + public void testRequestDownstreamAddressWithoutUsingLastAddress() throws Exception { + final IpPrefix bluetoothPrefix = asIpPrefix(mBluetoothAddress); + final LinkAddress address = requestDownstreamAddress(mHotspotIpServer, + false /* useLastAddress */); + final IpPrefix hotspotPrefix = asIpPrefix(address); + assertNotEquals(hotspotPrefix, bluetoothPrefix); + + final LinkAddress newAddress = requestDownstreamAddress(mHotspotIpServer, + false /* useLastAddress */); + final IpPrefix testDupRequest = asIpPrefix(newAddress); + assertNotEquals(hotspotPrefix, testDupRequest); + assertNotEquals(bluetoothPrefix, testDupRequest); + mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer); + + final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer, + false /* useLastAddress */); + final IpPrefix usbPrefix = asIpPrefix(usbAddress); + assertNotEquals(usbPrefix, bluetoothPrefix); + assertNotEquals(usbPrefix, hotspotPrefix); + mPrivateAddressCoordinator.releaseDownstream(mUsbIpServer); + } + + @Test + public void testSanitizedAddress() throws Exception { + int fakeSubAddr = 0x2b00; // 43.0. + when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr); + LinkAddress actualAddress = requestDownstreamAddress(mHotspotIpServer, + false /* useLastAddress */); + assertEquals(new LinkAddress("192.168.43.2/24"), actualAddress); + mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer); + + fakeSubAddr = 0x2d01; // 45.1. + when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr); + actualAddress = requestDownstreamAddress(mHotspotIpServer, false /* useLastAddress */); + assertEquals(new LinkAddress("192.168.45.2/24"), actualAddress); + mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer); + + fakeSubAddr = 0x2eff; // 46.255. + when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr); + actualAddress = requestDownstreamAddress(mHotspotIpServer, false /* useLastAddress */); + assertEquals(new LinkAddress("192.168.46.254/24"), actualAddress); + mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer); + + fakeSubAddr = 0x2f05; // 47.5. + when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeSubAddr); + actualAddress = requestDownstreamAddress(mHotspotIpServer, false /* useLastAddress */); + assertEquals(new LinkAddress("192.168.47.5/24"), actualAddress); + mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer); + } + + @Test + public void testReservedPrefix() throws Exception { + // - Test bluetooth prefix is reserved. + when(mPrivateAddressCoordinator.getRandomInt()).thenReturn( + getSubAddress(mBluetoothAddress.getAddress().getAddress())); + final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer, + false /* useLastAddress */); + final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress); + assertNotEquals(asIpPrefix(mBluetoothAddress), hotspotPrefix); + mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer); + + // - Test previous enabled hotspot prefix(cached prefix) is reserved. + when(mPrivateAddressCoordinator.getRandomInt()).thenReturn( + getSubAddress(hotspotAddress.getAddress().getAddress())); + final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer, + false /* useLastAddress */); + final IpPrefix usbPrefix = asIpPrefix(usbAddress); + assertNotEquals(asIpPrefix(mBluetoothAddress), usbPrefix); + assertNotEquals(hotspotPrefix, usbPrefix); + mPrivateAddressCoordinator.releaseDownstream(mUsbIpServer); + + // - Test wifi p2p prefix is reserved. + when(mPrivateAddressCoordinator.getRandomInt()).thenReturn( + getSubAddress(mLegacyWifiP2pAddress.getAddress().getAddress())); + final LinkAddress etherAddress = requestDownstreamAddress(mEthernetIpServer, + false /* useLastAddress */); + final IpPrefix etherPrefix = asIpPrefix(etherAddress); + assertNotEquals(asIpPrefix(mLegacyWifiP2pAddress), etherPrefix); + assertNotEquals(asIpPrefix(mBluetoothAddress), etherPrefix); + assertNotEquals(hotspotPrefix, etherPrefix); + mPrivateAddressCoordinator.releaseDownstream(mEthernetIpServer); + } + + @Test + public void testRequestLastDownstreamAddress() throws Exception { + final int fakeHotspotSubAddr = 0x2b05; // 43.5 + when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr); + final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + assertEquals("Wrong wifi prefix: ", new LinkAddress("192.168.43.5/24"), hotspotAddress); + + final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer, + true /* useLastAddress */); + assertEquals("Wrong wifi prefix: ", new LinkAddress("192.168.45.5/24"), usbAddress); + + mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer); + mPrivateAddressCoordinator.releaseDownstream(mUsbIpServer); + + final int newFakeSubAddr = 0x3c05; + when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr); + + final LinkAddress newHotspotAddress = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + assertEquals(hotspotAddress, newHotspotAddress); + final LinkAddress newUsbAddress = requestDownstreamAddress(mUsbIpServer, + true /* useLastAddress */); + assertEquals(usbAddress, newUsbAddress); + + final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork, + new LinkAddress("192.168.88.23/16"), null, + makeNetworkCapabilities(TRANSPORT_WIFI)); + mPrivateAddressCoordinator.updateUpstreamPrefix(wifiUpstream); + verify(mHotspotIpServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT); + verify(mUsbIpServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT); + } + + private UpstreamNetworkState buildUpstreamNetworkState(final Network network, + final LinkAddress v4Addr, final LinkAddress v6Addr, final NetworkCapabilities cap) { + final LinkProperties prop = new LinkProperties(); + prop.setInterfaceName(TEST_IFNAME); + if (v4Addr != null) prop.addLinkAddress(v4Addr); + + if (v6Addr != null) prop.addLinkAddress(v6Addr); + + return new UpstreamNetworkState(prop, cap, network); + } + + private NetworkCapabilities makeNetworkCapabilities(final int transportType) { + final NetworkCapabilities cap = new NetworkCapabilities(); + cap.addTransportType(transportType); + if (transportType == TRANSPORT_VPN) { + cap.removeCapability(NET_CAPABILITY_NOT_VPN); + } + + return cap; + } + + @Test + public void testNoConflictUpstreamPrefix() throws Exception { + final int fakeHotspotSubAddr = 0x2b05; // 43.5 + final IpPrefix predefinedPrefix = new IpPrefix("192.168.43.0/24"); + // Force always get subAddress "43.5" for conflict testing. + when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(fakeHotspotSubAddr); + // - Enable hotspot with prefix 192.168.43.0/24 + final LinkAddress hotspotAddr = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddr); + assertEquals("Wrong wifi prefix: ", predefinedPrefix, hotspotPrefix); + // - test mobile network with null NetworkCapabilities. Ideally this should not happen + // because NetworkCapabilities update should always happen before LinkProperties update + // and the UpstreamNetworkState update, just make sure no crash in this case. + final UpstreamNetworkState noCapUpstream = buildUpstreamNetworkState(mMobileNetwork, + new LinkAddress("10.0.0.8/24"), null, null); + mPrivateAddressCoordinator.updateUpstreamPrefix(noCapUpstream); + verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT); + // - test mobile upstream with no address. + final UpstreamNetworkState noAddress = buildUpstreamNetworkState(mMobileNetwork, + null, null, makeNetworkCapabilities(TRANSPORT_CELLULAR)); + mPrivateAddressCoordinator.updateUpstreamPrefix(noCapUpstream); + verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT); + // - Update v6 only mobile network, hotspot prefix should not be removed. + final UpstreamNetworkState v6OnlyMobile = buildUpstreamNetworkState(mMobileNetwork, + null, new LinkAddress("2001:db8::/64"), + makeNetworkCapabilities(TRANSPORT_CELLULAR)); + mPrivateAddressCoordinator.updateUpstreamPrefix(v6OnlyMobile); + verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT); + mPrivateAddressCoordinator.removeUpstreamPrefix(mMobileNetwork); + // - Update v4 only mobile network, hotspot prefix should not be removed. + final UpstreamNetworkState v4OnlyMobile = buildUpstreamNetworkState(mMobileNetwork, + new LinkAddress("10.0.0.8/24"), null, + makeNetworkCapabilities(TRANSPORT_CELLULAR)); + mPrivateAddressCoordinator.updateUpstreamPrefix(v4OnlyMobile); + verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT); + // - Update v4v6 mobile network, hotspot prefix should not be removed. + final UpstreamNetworkState v4v6Mobile = buildUpstreamNetworkState(mMobileNetwork, + new LinkAddress("10.0.0.8/24"), new LinkAddress("2001:db8::/64"), + makeNetworkCapabilities(TRANSPORT_CELLULAR)); + mPrivateAddressCoordinator.updateUpstreamPrefix(v4v6Mobile); + verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT); + // - Update v6 only wifi network, hotspot prefix should not be removed. + final UpstreamNetworkState v6OnlyWifi = buildUpstreamNetworkState(mWifiNetwork, + null, new LinkAddress("2001:db8::/64"), makeNetworkCapabilities(TRANSPORT_WIFI)); + mPrivateAddressCoordinator.updateUpstreamPrefix(v6OnlyWifi); + verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT); + mPrivateAddressCoordinator.removeUpstreamPrefix(mWifiNetwork); + // - Update vpn network, it conflict with hotspot prefix but VPN networks are ignored. + final UpstreamNetworkState v4OnlyVpn = buildUpstreamNetworkState(mVpnNetwork, + new LinkAddress("192.168.43.5/24"), null, makeNetworkCapabilities(TRANSPORT_VPN)); + mPrivateAddressCoordinator.updateUpstreamPrefix(v4OnlyVpn); + verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT); + // - Update v4 only wifi network, it conflict with hotspot prefix. + final UpstreamNetworkState v4OnlyWifi = buildUpstreamNetworkState(mWifiNetwork, + new LinkAddress("192.168.43.5/24"), null, makeNetworkCapabilities(TRANSPORT_WIFI)); + mPrivateAddressCoordinator.updateUpstreamPrefix(v4OnlyWifi); + verify(mHotspotIpServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT); + reset(mHotspotIpServer); + // - Restart hotspot again and its prefix is different previous. + mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer); + final LinkAddress hotspotAddr2 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + final IpPrefix hotspotPrefix2 = asIpPrefix(hotspotAddr2); + assertNotEquals(hotspotPrefix, hotspotPrefix2); + mPrivateAddressCoordinator.updateUpstreamPrefix(v4OnlyWifi); + verify(mHotspotIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT); + // - Usb tethering can be enabled and its prefix is different with conflict one. + final LinkAddress usbAddr = requestDownstreamAddress(mUsbIpServer, + true /* useLastAddress */); + final IpPrefix usbPrefix = asIpPrefix(usbAddr); + assertNotEquals(predefinedPrefix, usbPrefix); + assertNotEquals(hotspotPrefix2, usbPrefix); + // - Disable wifi upstream, then wifi's prefix can be selected again. + mPrivateAddressCoordinator.removeUpstreamPrefix(mWifiNetwork); + final LinkAddress ethAddr = requestDownstreamAddress(mEthernetIpServer, + true /* useLastAddress */); + final IpPrefix ethPrefix = asIpPrefix(ethAddr); + assertEquals(predefinedPrefix, ethPrefix); + } + + @Test + public void testChooseAvailablePrefix() throws Exception { + final int randomAddress = 0x8605; // 134.5 + when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomAddress); + final LinkAddress addr0 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + // Check whether return address is prefix 192.168.0.0/16 + subAddress 0.0.134.5. + assertEquals("Wrong prefix: ", new LinkAddress("192.168.134.5/24"), addr0); + final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork, + new LinkAddress("192.168.134.13/26"), null, + makeNetworkCapabilities(TRANSPORT_WIFI)); + mPrivateAddressCoordinator.updateUpstreamPrefix(wifiUpstream); + + // Check whether return address is next prefix of 192.168.134.0/24. + final LinkAddress addr1 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("192.168.135.5/24"), addr1); + final UpstreamNetworkState wifiUpstream2 = buildUpstreamNetworkState(mWifiNetwork, + new LinkAddress("192.168.149.16/19"), null, + makeNetworkCapabilities(TRANSPORT_WIFI)); + mPrivateAddressCoordinator.updateUpstreamPrefix(wifiUpstream2); + + + // The conflict range is 128 ~ 159, so the address is 192.168.160.5/24. + final LinkAddress addr2 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("192.168.160.5/24"), addr2); + final UpstreamNetworkState mobileUpstream = buildUpstreamNetworkState(mMobileNetwork, + new LinkAddress("192.168.129.53/18"), null, + makeNetworkCapabilities(TRANSPORT_CELLULAR)); + // Update another conflict upstream which is covered by the previous one (but not the first + // one) and verify whether this would affect the result. + final UpstreamNetworkState mobileUpstream2 = buildUpstreamNetworkState(mMobileNetwork2, + new LinkAddress("192.168.170.7/19"), null, + makeNetworkCapabilities(TRANSPORT_CELLULAR)); + mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream); + mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream2); + + // The conflict range are 128 ~ 159 and 159 ~ 191, so the address is 192.168.192.5/24. + final LinkAddress addr3 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("192.168.192.5/24"), addr3); + final UpstreamNetworkState mobileUpstream3 = buildUpstreamNetworkState(mMobileNetwork3, + new LinkAddress("192.168.188.133/17"), null, + makeNetworkCapabilities(TRANSPORT_CELLULAR)); + mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream3); + + // Conflict range: 128 ~ 255. The next available address is 192.168.0.5 because + // 192.168.134/24 ~ 192.168.255.255/24 is not available. + final LinkAddress addr4 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("192.168.0.5/24"), addr4); + final UpstreamNetworkState mobileUpstream4 = buildUpstreamNetworkState(mMobileNetwork4, + new LinkAddress("192.168.3.59/21"), null, + makeNetworkCapabilities(TRANSPORT_CELLULAR)); + mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream4); + + // Conflict ranges: 128 ~ 255 and 0 ~ 7, so the address is 192.168.8.5/24. + final LinkAddress addr5 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("192.168.8.5/24"), addr5); + final UpstreamNetworkState mobileUpstream5 = buildUpstreamNetworkState(mMobileNetwork5, + new LinkAddress("192.168.68.43/21"), null, + makeNetworkCapabilities(TRANSPORT_CELLULAR)); + mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream5); + + // Update an upstream that does *not* conflict, check whether return the same address + // 192.168.5/24. + final LinkAddress addr6 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("192.168.8.5/24"), addr6); + final UpstreamNetworkState mobileUpstream6 = buildUpstreamNetworkState(mMobileNetwork6, + new LinkAddress("192.168.10.97/21"), null, + makeNetworkCapabilities(TRANSPORT_CELLULAR)); + mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream6); + + // Conflict ranges: 0 ~ 15 and 128 ~ 255, so the address is 192.168.16.5/24. + final LinkAddress addr7 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("192.168.16.5/24"), addr7); + final UpstreamNetworkState mobileUpstream7 = buildUpstreamNetworkState(mMobileNetwork6, + new LinkAddress("192.168.0.0/17"), null, + makeNetworkCapabilities(TRANSPORT_CELLULAR)); + mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream7); + + // Choose prefix from next range(172.16.0.0/12) when no available prefix in 192.168.0.0/16. + final LinkAddress addr8 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("172.16.134.5/24"), addr8); + } + + @Test + public void testChoosePrefixFromDifferentRanges() throws Exception { + final int randomAddress = 0x1f2b2a; // 31.43.42 + when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomAddress); + final LinkAddress classC1 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + // Check whether return address is prefix 192.168.0.0/16 + subAddress 0.0.43.42. + assertEquals("Wrong prefix: ", new LinkAddress("192.168.43.42/24"), classC1); + final UpstreamNetworkState wifiUpstream = buildUpstreamNetworkState(mWifiNetwork, + new LinkAddress("192.168.88.23/17"), null, + makeNetworkCapabilities(TRANSPORT_WIFI)); + mPrivateAddressCoordinator.updateUpstreamPrefix(wifiUpstream); + verifyNotifyConflictAndRelease(mHotspotIpServer); + + // Check whether return address is next address of prefix 192.168.128.0/17. + final LinkAddress classC2 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("192.168.128.42/24"), classC2); + final UpstreamNetworkState mobileUpstream = buildUpstreamNetworkState(mMobileNetwork, + new LinkAddress("192.1.2.3/8"), null, + makeNetworkCapabilities(TRANSPORT_CELLULAR)); + mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream); + verifyNotifyConflictAndRelease(mHotspotIpServer); + + // Check whether return address is under prefix 172.16.0.0/12. + final LinkAddress classB1 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("172.31.43.42/24"), classB1); + final UpstreamNetworkState mobileUpstream2 = buildUpstreamNetworkState(mMobileNetwork2, + new LinkAddress("172.28.123.100/14"), null, + makeNetworkCapabilities(TRANSPORT_CELLULAR)); + mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream2); + verifyNotifyConflictAndRelease(mHotspotIpServer); + + // 172.28.0.0 ~ 172.31.255.255 is not available. + // Check whether return address is next address of prefix 172.16.0.0/14. + final LinkAddress classB2 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("172.16.0.42/24"), classB2); + + // Check whether new downstream is next address of address 172.16.0.42/24. + final LinkAddress classB3 = requestDownstreamAddress(mUsbIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("172.16.1.42/24"), classB3); + final UpstreamNetworkState mobileUpstream3 = buildUpstreamNetworkState(mMobileNetwork3, + new LinkAddress("172.16.0.1/24"), null, + makeNetworkCapabilities(TRANSPORT_CELLULAR)); + mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream3); + verifyNotifyConflictAndRelease(mHotspotIpServer); + verify(mUsbIpServer, never()).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT); + + // Check whether return address is next address of prefix 172.16.1.42/24. + final LinkAddress classB4 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("172.16.2.42/24"), classB4); + final UpstreamNetworkState mobileUpstream4 = buildUpstreamNetworkState(mMobileNetwork4, + new LinkAddress("172.16.0.1/13"), null, + makeNetworkCapabilities(TRANSPORT_CELLULAR)); + mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream4); + verifyNotifyConflictAndRelease(mHotspotIpServer); + verifyNotifyConflictAndRelease(mUsbIpServer); + + // Check whether return address is next address of prefix 172.16.0.1/13. + final LinkAddress classB5 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("172.24.0.42/24"), classB5); + // Check whether return address is next address of prefix 172.24.0.42/24. + final LinkAddress classB6 = requestDownstreamAddress(mUsbIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("172.24.1.42/24"), classB6); + final UpstreamNetworkState mobileUpstream5 = buildUpstreamNetworkState(mMobileNetwork5, + new LinkAddress("172.24.0.1/12"), null, + makeNetworkCapabilities(TRANSPORT_CELLULAR)); + mPrivateAddressCoordinator.updateUpstreamPrefix(mobileUpstream5); + verifyNotifyConflictAndRelease(mHotspotIpServer); + verifyNotifyConflictAndRelease(mUsbIpServer); + + // Check whether return address is prefix 10.0.0.0/8 + subAddress 0.31.43.42. + final LinkAddress classA1 = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("10.31.43.42/24"), classA1); + // Check whether new downstream is next address of address 10.31.43.42/24. + final LinkAddress classA2 = requestDownstreamAddress(mUsbIpServer, + true /* useLastAddress */); + assertEquals("Wrong prefix: ", new LinkAddress("10.31.44.42/24"), classA2); + } + + private void verifyNotifyConflictAndRelease(final IpServer ipServer) throws Exception { + verify(ipServer).sendMessage(IpServer.CMD_NOTIFY_PREFIX_CONFLICT); + mPrivateAddressCoordinator.releaseDownstream(ipServer); + reset(ipServer); + setUpIpServers(); + } + + private int getSubAddress(final byte... ipv4Address) { + assertEquals(4, ipv4Address.length); + + int subnet = Byte.toUnsignedInt(ipv4Address[2]); + return (subnet << 8) + ipv4Address[3]; + } + + private void assertReseveredWifiP2pPrefix() throws Exception { + LinkAddress address = requestDownstreamAddress(mHotspotIpServer, + true /* useLastAddress */); + final IpPrefix hotspotPrefix = asIpPrefix(address); + final IpPrefix legacyWifiP2pPrefix = asIpPrefix(mLegacyWifiP2pAddress); + assertNotEquals(legacyWifiP2pPrefix, hotspotPrefix); + mPrivateAddressCoordinator.releaseDownstream(mHotspotIpServer); + } + + @Test + public void testEnableLegacyWifiP2PAddress() throws Exception { + when(mPrivateAddressCoordinator.getRandomInt()).thenReturn( + getSubAddress(mLegacyWifiP2pAddress.getAddress().getAddress())); + // No matter #shouldEnableWifiP2pDedicatedIp() is enabled or not, legacy wifi p2p prefix + // is resevered. + assertReseveredWifiP2pPrefix(); + + when(mConfig.shouldEnableWifiP2pDedicatedIp()).thenReturn(true); + assertReseveredWifiP2pPrefix(); + + // If #shouldEnableWifiP2pDedicatedIp() is enabled, wifi P2P gets the configured address. + LinkAddress address = requestDownstreamAddress(mWifiP2pIpServer, + true /* useLastAddress */); + assertEquals(mLegacyWifiP2pAddress, address); + mPrivateAddressCoordinator.releaseDownstream(mWifiP2pIpServer); + } +} diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java new file mode 100644 index 0000000000..6090213516 --- /dev/null +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java @@ -0,0 +1,403 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +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 org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.IConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkRequest; +import android.os.Handler; +import android.os.UserHandle; +import android.util.ArrayMap; + +import androidx.annotation.Nullable; + +import java.util.Map; +import java.util.Objects; + +/** + * Simulates upstream switching and sending NetworkCallbacks and CONNECTIVITY_ACTION broadcasts. + * + * Unlike any real networking code, this class is single-threaded and entirely synchronous. + * The effects of all method calls (including sending fake broadcasts, sending callbacks, etc.) are + * performed immediately on the caller's thread before returning. + * + * TODO: this duplicates a fair amount of code from ConnectivityManager and ConnectivityService. + * Consider using a ConnectivityService object instead, as used in ConnectivityServiceTest. + * + * Things to consider: + * - ConnectivityService uses a real handler for realism, and these test use TestLooper (or even + * invoke callbacks directly inline) for determinism. Using a real ConnectivityService would + * require adding dispatchAll() calls and migrating to handlers. + * - ConnectivityService does not provide a way to order CONNECTIVITY_ACTION before or after the + * NetworkCallbacks for the same network change. That ability is useful because the upstream + * selection code in Tethering is vulnerable to race conditions, due to its reliance on multiple + * separate NetworkCallbacks and BroadcastReceivers, each of which trigger different types of + * updates. If/when the upstream selection code is refactored to a more level-triggered model + * (e.g., with an idempotent function that takes into account all state every time any part of + * that state changes), this may become less important or unnecessary. + */ +public class TestConnectivityManager extends ConnectivityManager { + public static final boolean BROADCAST_FIRST = false; + public static final boolean CALLBACKS_FIRST = true; + + final Map mAllCallbacks = new ArrayMap<>(); + // This contains the callbacks tracking the system default network, whether it's registered + // with registerSystemDefaultNetworkCallback (S+) or with a custom request (R-). + final Map mTrackingDefault = new ArrayMap<>(); + final Map mListening = new ArrayMap<>(); + final Map mRequested = new ArrayMap<>(); + final Map mLegacyTypeMap = new ArrayMap<>(); + + private final Context mContext; + + private int mNetworkId = 100; + private TestNetworkAgent mDefaultNetwork = null; + + /** + * Constructs a TestConnectivityManager. + * @param ctx the context to use. Must be a fake or a mock because otherwise the test will + * attempt to send real broadcasts and resulting in permission denials. + * @param svc an IConnectivityManager. Should be a fake or a mock. + */ + public TestConnectivityManager(Context ctx, IConnectivityManager svc) { + super(ctx, svc); + mContext = ctx; + } + + class NetworkRequestInfo { + public final NetworkRequest request; + public final Handler handler; + NetworkRequestInfo(NetworkRequest r, Handler h) { + request = r; + handler = h; + } + } + + boolean hasNoCallbacks() { + return mAllCallbacks.isEmpty() + && mTrackingDefault.isEmpty() + && mListening.isEmpty() + && mRequested.isEmpty() + && mLegacyTypeMap.isEmpty(); + } + + boolean onlyHasDefaultCallbacks() { + return (mAllCallbacks.size() == 1) + && (mTrackingDefault.size() == 1) + && mListening.isEmpty() + && mRequested.isEmpty() + && mLegacyTypeMap.isEmpty(); + } + + boolean isListeningForAll() { + final NetworkCapabilities empty = new NetworkCapabilities(); + empty.clearAll(); + + for (NetworkRequestInfo nri : mListening.values()) { + if (nri.request.networkCapabilities.equalRequestableCapabilities(empty)) { + return true; + } + } + return false; + } + + int getNetworkId() { + return ++mNetworkId; + } + + private void sendDefaultNetworkBroadcasts(TestNetworkAgent formerDefault, + TestNetworkAgent defaultNetwork) { + if (formerDefault != null) { + sendConnectivityAction(formerDefault.legacyType, false /* connected */); + } + if (defaultNetwork != null) { + sendConnectivityAction(defaultNetwork.legacyType, true /* connected */); + } + } + + private void sendDefaultNetworkCallbacks(TestNetworkAgent formerDefault, + TestNetworkAgent defaultNetwork) { + for (NetworkCallback cb : mTrackingDefault.keySet()) { + final NetworkRequestInfo nri = mTrackingDefault.get(cb); + if (defaultNetwork != null) { + nri.handler.post(() -> cb.onAvailable(defaultNetwork.networkId)); + nri.handler.post(() -> cb.onCapabilitiesChanged( + defaultNetwork.networkId, defaultNetwork.networkCapabilities)); + nri.handler.post(() -> cb.onLinkPropertiesChanged( + defaultNetwork.networkId, defaultNetwork.linkProperties)); + } else if (formerDefault != null) { + nri.handler.post(() -> cb.onLost(formerDefault.networkId)); + } + } + } + + void makeDefaultNetwork(TestNetworkAgent agent, boolean order, @Nullable Runnable inBetween) { + if (Objects.equals(mDefaultNetwork, agent)) return; + + final TestNetworkAgent formerDefault = mDefaultNetwork; + mDefaultNetwork = agent; + + if (order == CALLBACKS_FIRST) { + sendDefaultNetworkCallbacks(formerDefault, mDefaultNetwork); + if (inBetween != null) inBetween.run(); + sendDefaultNetworkBroadcasts(formerDefault, mDefaultNetwork); + } else { + sendDefaultNetworkBroadcasts(formerDefault, mDefaultNetwork); + if (inBetween != null) inBetween.run(); + sendDefaultNetworkCallbacks(formerDefault, mDefaultNetwork); + } + } + + void makeDefaultNetwork(TestNetworkAgent agent, boolean order) { + makeDefaultNetwork(agent, order, null /* inBetween */); + } + + void makeDefaultNetwork(TestNetworkAgent agent) { + makeDefaultNetwork(agent, BROADCAST_FIRST, null /* inBetween */); + } + + static boolean looksLikeDefaultRequest(NetworkRequest req) { + return req.hasCapability(NET_CAPABILITY_INTERNET) + && !req.hasCapability(NET_CAPABILITY_DUN) + && !req.hasTransport(TRANSPORT_CELLULAR); + } + + @Override + public void requestNetwork(NetworkRequest req, NetworkCallback cb, Handler h) { + assertFalse(mAllCallbacks.containsKey(cb)); + mAllCallbacks.put(cb, new NetworkRequestInfo(req, h)); + // For R- devices, Tethering will invoke this function in 2 cases, one is to request mobile + // network, the other is to track system default network. + if (looksLikeDefaultRequest(req)) { + assertFalse(mTrackingDefault.containsKey(cb)); + mTrackingDefault.put(cb, new NetworkRequestInfo(req, h)); + } else { + assertFalse(mRequested.containsKey(cb)); + mRequested.put(cb, new NetworkRequestInfo(req, h)); + } + } + + @Override + public void requestNetwork(NetworkRequest req, NetworkCallback cb) { + fail("Should never be called."); + } + + @Override + public void requestNetwork(NetworkRequest req, + int timeoutMs, int legacyType, Handler h, NetworkCallback cb) { + assertFalse(mAllCallbacks.containsKey(cb)); + NetworkRequest newReq = new NetworkRequest(req.networkCapabilities, legacyType, + -1 /** testId */, req.type); + mAllCallbacks.put(cb, new NetworkRequestInfo(newReq, h)); + assertFalse(mRequested.containsKey(cb)); + mRequested.put(cb, new NetworkRequestInfo(newReq, h)); + assertFalse(mLegacyTypeMap.containsKey(cb)); + if (legacyType != ConnectivityManager.TYPE_NONE) { + mLegacyTypeMap.put(cb, legacyType); + } + } + + @Override + public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb, Handler h) { + assertFalse(mAllCallbacks.containsKey(cb)); + mAllCallbacks.put(cb, new NetworkRequestInfo(req, h)); + assertFalse(mListening.containsKey(cb)); + mListening.put(cb, new NetworkRequestInfo(req, h)); + } + + @Override + public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb) { + fail("Should never be called."); + } + + @Override + public void registerDefaultNetworkCallback(NetworkCallback cb, Handler h) { + fail("Should never be called."); + } + + @Override + public void registerDefaultNetworkCallback(NetworkCallback cb) { + fail("Should never be called."); + } + + @Override + public void unregisterNetworkCallback(NetworkCallback cb) { + if (mTrackingDefault.containsKey(cb)) { + mTrackingDefault.remove(cb); + } else if (mListening.containsKey(cb)) { + mListening.remove(cb); + } else if (mRequested.containsKey(cb)) { + mRequested.remove(cb); + mLegacyTypeMap.remove(cb); + } else { + fail("Unexpected callback removed"); + } + mAllCallbacks.remove(cb); + + assertFalse(mAllCallbacks.containsKey(cb)); + assertFalse(mTrackingDefault.containsKey(cb)); + assertFalse(mListening.containsKey(cb)); + assertFalse(mRequested.containsKey(cb)); + } + + private void sendConnectivityAction(int type, boolean connected) { + NetworkInfo ni = new NetworkInfo(type, 0 /* subtype */, getNetworkTypeName(type), + "" /* subtypeName */); + NetworkInfo.DetailedState state = connected + ? NetworkInfo.DetailedState.CONNECTED + : NetworkInfo.DetailedState.DISCONNECTED; + ni.setDetailedState(state, "" /* reason */, "" /* extraInfo */); + Intent intent = new Intent(CONNECTIVITY_ACTION); + intent.putExtra(EXTRA_NETWORK_INFO, ni); + mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + } + + public static class TestNetworkAgent { + public final TestConnectivityManager cm; + public final Network networkId; + public final NetworkCapabilities networkCapabilities; + public final LinkProperties linkProperties; + // TODO: delete when tethering no longer uses CONNECTIVITY_ACTION. + public final int legacyType; + + public TestNetworkAgent(TestConnectivityManager cm, NetworkCapabilities nc) { + this.cm = cm; + this.networkId = new Network(cm.getNetworkId()); + networkCapabilities = copy(nc); + linkProperties = new LinkProperties(); + legacyType = toLegacyType(nc); + } + + public TestNetworkAgent(TestConnectivityManager cm, UpstreamNetworkState state) { + this.cm = cm; + networkId = state.network; + networkCapabilities = state.networkCapabilities; + linkProperties = state.linkProperties; + this.legacyType = toLegacyType(networkCapabilities); + } + + private static int toLegacyType(NetworkCapabilities nc) { + for (int type = 0; type < ConnectivityManager.TYPE_TEST; type++) { + if (matchesLegacyType(nc, type)) return type; + } + throw new IllegalArgumentException(("Can't determine legacy type for: ") + nc); + } + + private static boolean matchesLegacyType(NetworkCapabilities nc, int legacyType) { + final NetworkCapabilities typeNc; + try { + typeNc = ConnectivityManager.networkCapabilitiesForType(legacyType); + } catch (IllegalArgumentException e) { + // networkCapabilitiesForType does not support all legacy types. + return false; + } + return typeNc.satisfiedByNetworkCapabilities(nc); + } + + private boolean matchesLegacyType(int legacyType) { + return matchesLegacyType(networkCapabilities, legacyType); + } + + private void maybeSendConnectivityBroadcast(boolean connected) { + for (Integer requestedLegacyType : cm.mLegacyTypeMap.values()) { + if (requestedLegacyType.intValue() == legacyType) { + cm.sendConnectivityAction(legacyType, connected /* connected */); + // In practice, a given network can match only one legacy type. + break; + } + } + } + + public void fakeConnect() { + fakeConnect(BROADCAST_FIRST, null); + } + + public void fakeConnect(boolean order, @Nullable Runnable inBetween) { + if (order == BROADCAST_FIRST) { + maybeSendConnectivityBroadcast(true /* connected */); + if (inBetween != null) inBetween.run(); + } + + for (NetworkCallback cb : cm.mListening.keySet()) { + final NetworkRequestInfo nri = cm.mListening.get(cb); + nri.handler.post(() -> cb.onAvailable(networkId)); + nri.handler.post(() -> cb.onCapabilitiesChanged( + networkId, copy(networkCapabilities))); + nri.handler.post(() -> cb.onLinkPropertiesChanged(networkId, copy(linkProperties))); + } + + if (order == CALLBACKS_FIRST) { + if (inBetween != null) inBetween.run(); + maybeSendConnectivityBroadcast(true /* connected */); + } + // mTrackingDefault will be updated if/when the caller calls makeDefaultNetwork + } + + public void fakeDisconnect() { + fakeDisconnect(BROADCAST_FIRST, null); + } + + public void fakeDisconnect(boolean order, @Nullable Runnable inBetween) { + if (order == BROADCAST_FIRST) { + maybeSendConnectivityBroadcast(false /* connected */); + if (inBetween != null) inBetween.run(); + } + + for (NetworkCallback cb : cm.mListening.keySet()) { + cb.onLost(networkId); + } + + if (order == CALLBACKS_FIRST) { + if (inBetween != null) inBetween.run(); + maybeSendConnectivityBroadcast(false /* connected */); + } + // mTrackingDefault will be updated if/when the caller calls makeDefaultNetwork + } + + public void sendLinkProperties() { + for (NetworkCallback cb : cm.mListening.keySet()) { + cb.onLinkPropertiesChanged(networkId, copy(linkProperties)); + } + } + + @Override + public String toString() { + return String.format("TestNetworkAgent: %s %s", networkId, networkCapabilities); + } + } + + static NetworkCapabilities copy(NetworkCapabilities nc) { + return new NetworkCapabilities(nc); + } + + static LinkProperties copy(LinkProperties lp) { + return new LinkProperties(lp); + } +} diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java new file mode 100644 index 0000000000..a6433a6330 --- /dev/null +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java @@ -0,0 +1,542 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import static android.net.ConnectivityManager.TYPE_ETHERNET; +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_MOBILE_DUN; +import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI; +import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; +import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.ModuleInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.net.util.SharedLog; +import android.os.Build; +import android.provider.DeviceConfig; +import android.telephony.TelephonyManager; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.test.BroadcastInterceptingContext; +import com.android.net.module.util.DeviceConfigUtils; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +import java.util.Arrays; +import java.util.Iterator; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class TetheringConfigurationTest { + private final SharedLog mLog = new SharedLog("TetheringConfigurationTest"); + + @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(); + + private static final String[] PROVISIONING_APP_NAME = {"some", "app"}; + private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app"; + private static final String PROVISIONING_APP_RESPONSE = "app_response"; + private static final String TEST_PACKAGE_NAME = "com.android.tethering.test"; + private static final String APEX_NAME = "com.android.tethering"; + private static final long TEST_PACKAGE_VERSION = 1234L; + @Mock private Context mContext; + @Mock private TelephonyManager mTelephonyManager; + @Mock private Resources mResources; + @Mock private Resources mResourcesForSubId; + @Mock private PackageManager mPackageManager; + @Mock private ModuleInfo mMi; + private Context mMockContext; + private boolean mHasTelephonyManager; + private boolean mEnableLegacyDhcpServer; + private MockitoSession mMockingSession; + + private class MockTetheringConfiguration extends TetheringConfiguration { + MockTetheringConfiguration(Context ctx, SharedLog log, int id) { + super(ctx, log, id); + } + + @Override + protected Resources getResourcesForSubIdWrapper(Context ctx, int subId) { + return mResourcesForSubId; + } + } + + private class MockContext extends BroadcastInterceptingContext { + MockContext(Context base) { + super(base); + } + + @Override + public Resources getResources() { + return mResources; + } + + @Override + public Object getSystemService(String name) { + if (Context.TELEPHONY_SERVICE.equals(name)) { + return mHasTelephonyManager ? mTelephonyManager : null; + } + return super.getSystemService(name); + } + + @Override + public PackageManager getPackageManager() { + return mPackageManager; + } + + @Override + public String getPackageName() { + return TEST_PACKAGE_NAME; + } + } + + @Before + public void setUp() throws Exception { + // TODO: use a dependencies class instead of mock statics. + mMockingSession = mockitoSession() + .initMocks(this) + .mockStatic(DeviceConfig.class) + .strictness(Strictness.WARN) + .startMocking(); + DeviceConfigUtils.resetPackageVersionCacheForTest(); + doReturn(null).when( + () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY), + eq(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER))); + setTetherForceUpstreamAutomaticFlagVersion(null); + + final PackageInfo pi = new PackageInfo(); + pi.setLongVersionCode(TEST_PACKAGE_VERSION); + doReturn(pi).when(mPackageManager).getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt()); + doReturn(mMi).when(mPackageManager).getModuleInfo(eq(APEX_NAME), anyInt()); + doReturn(TEST_PACKAGE_NAME).when(mMi).getPackageName(); + + when(mResources.getStringArray(R.array.config_tether_dhcp_range)).thenReturn( + new String[0]); + when(mResources.getInteger(R.integer.config_tether_offload_poll_interval)).thenReturn( + TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); + when(mResources.getStringArray(R.array.config_tether_usb_regexs)).thenReturn(new String[0]); + when(mResources.getStringArray(R.array.config_tether_wifi_regexs)) + .thenReturn(new String[]{ "test_wlan\\d" }); + when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs)).thenReturn( + new String[0]); + when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn(new int[0]); + when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app)) + .thenReturn(new String[0]); + when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn( + false); + when(mResources.getBoolean(R.bool.config_tether_enable_legacy_wifi_p2p_dedicated_ip)) + .thenReturn(false); + initializeBpfOffloadConfiguration(true, null /* unset */); + initEnableSelectAllPrefixRangeFlag(null /* unset */); + + mHasTelephonyManager = true; + mMockContext = new MockContext(mContext); + mEnableLegacyDhcpServer = false; + } + + @After + public void tearDown() throws Exception { + mMockingSession.finishMocking(); + DeviceConfigUtils.resetPackageVersionCacheForTest(); + } + + private TetheringConfiguration getTetheringConfiguration(int... legacyTetherUpstreamTypes) { + when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn( + legacyTetherUpstreamTypes); + return new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + } + + @Test + public void testNoTelephonyManagerMeansNoDun() { + mHasTelephonyManager = false; + final TetheringConfiguration cfg = getTetheringConfiguration( + new int[]{TYPE_MOBILE_DUN, TYPE_WIFI}); + assertFalse(cfg.isDunRequired); + assertFalse(cfg.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE_DUN)); + // Just to prove we haven't clobbered Wi-Fi: + assertTrue(cfg.preferredUpstreamIfaceTypes.contains(TYPE_WIFI)); + } + + @Test + public void testDunFromTelephonyManagerMeansDun() { + when(mTelephonyManager.isTetheringApnRequired()).thenReturn(true); + + final TetheringConfiguration cfgWifi = getTetheringConfiguration(TYPE_WIFI); + final TetheringConfiguration cfgMobileWifiHipri = getTetheringConfiguration( + TYPE_MOBILE, TYPE_WIFI, TYPE_MOBILE_HIPRI); + final TetheringConfiguration cfgWifiDun = getTetheringConfiguration( + TYPE_WIFI, TYPE_MOBILE_DUN); + final TetheringConfiguration cfgMobileWifiHipriDun = getTetheringConfiguration( + TYPE_MOBILE, TYPE_WIFI, TYPE_MOBILE_HIPRI, TYPE_MOBILE_DUN); + + for (TetheringConfiguration cfg : Arrays.asList(cfgWifi, cfgMobileWifiHipri, + cfgWifiDun, cfgMobileWifiHipriDun)) { + String msg = "config=" + cfg.toString(); + assertTrue(msg, cfg.isDunRequired); + assertTrue(msg, cfg.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE_DUN)); + assertFalse(msg, cfg.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE)); + assertFalse(msg, cfg.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE_HIPRI)); + // Just to prove we haven't clobbered Wi-Fi: + assertTrue(msg, cfg.preferredUpstreamIfaceTypes.contains(TYPE_WIFI)); + } + } + + @Test + public void testDunNotRequiredFromTelephonyManagerMeansNoDun() { + when(mTelephonyManager.isTetheringApnRequired()).thenReturn(false); + + final TetheringConfiguration cfgWifi = getTetheringConfiguration(TYPE_WIFI); + final TetheringConfiguration cfgMobileWifiHipri = getTetheringConfiguration( + TYPE_MOBILE, TYPE_WIFI, TYPE_MOBILE_HIPRI); + final TetheringConfiguration cfgWifiDun = getTetheringConfiguration( + TYPE_WIFI, TYPE_MOBILE_DUN); + final TetheringConfiguration cfgWifiMobile = getTetheringConfiguration( + TYPE_WIFI, TYPE_MOBILE); + final TetheringConfiguration cfgWifiHipri = getTetheringConfiguration( + TYPE_WIFI, TYPE_MOBILE_HIPRI); + final TetheringConfiguration cfgMobileWifiHipriDun = getTetheringConfiguration( + TYPE_MOBILE, TYPE_WIFI, TYPE_MOBILE_HIPRI, TYPE_MOBILE_DUN); + + String msg; + // TYPE_MOBILE_DUN should be present in none of the combinations. + // TYPE_WIFI should not be affected. + for (TetheringConfiguration cfg : Arrays.asList(cfgWifi, cfgMobileWifiHipri, cfgWifiDun, + cfgWifiMobile, cfgWifiHipri, cfgMobileWifiHipriDun)) { + msg = "config=" + cfg.toString(); + assertFalse(msg, cfg.isDunRequired); + assertFalse(msg, cfg.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE_DUN)); + assertTrue(msg, cfg.preferredUpstreamIfaceTypes.contains(TYPE_WIFI)); + } + + for (TetheringConfiguration cfg : Arrays.asList(cfgWifi, cfgMobileWifiHipri, cfgWifiDun, + cfgMobileWifiHipriDun)) { + msg = "config=" + cfg.toString(); + assertTrue(msg, cfg.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE)); + assertTrue(msg, cfg.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE_HIPRI)); + } + msg = "config=" + cfgWifiMobile.toString(); + assertTrue(msg, cfgWifiMobile.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE)); + assertFalse(msg, cfgWifiMobile.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE_HIPRI)); + msg = "config=" + cfgWifiHipri.toString(); + assertFalse(msg, cfgWifiHipri.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE)); + assertTrue(msg, cfgWifiHipri.preferredUpstreamIfaceTypes.contains(TYPE_MOBILE_HIPRI)); + + } + + @Test + public void testNoDefinedUpstreamTypesAddsEthernet() { + when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn(new int[]{}); + when(mTelephonyManager.isTetheringApnRequired()).thenReturn(false); + + final TetheringConfiguration cfg = new TetheringConfiguration( + mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + final Iterator upstreamIterator = cfg.preferredUpstreamIfaceTypes.iterator(); + assertTrue(upstreamIterator.hasNext()); + assertEquals(TYPE_ETHERNET, upstreamIterator.next().intValue()); + // The following is because the code always adds some kind of mobile + // upstream, be it DUN or, in this case where DUN is NOT required, + // make sure there is at least one of MOBILE or HIPRI. With the empty + // list of the configuration in this test, it will always add both + // MOBILE and HIPRI, in that order. + assertTrue(upstreamIterator.hasNext()); + assertEquals(TYPE_MOBILE, upstreamIterator.next().intValue()); + assertTrue(upstreamIterator.hasNext()); + assertEquals(TYPE_MOBILE_HIPRI, upstreamIterator.next().intValue()); + assertFalse(upstreamIterator.hasNext()); + } + + @Test + public void testDefinedUpstreamTypesSansEthernetAddsEthernet() { + when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn( + new int[]{TYPE_WIFI, TYPE_MOBILE_HIPRI}); + when(mTelephonyManager.isTetheringApnRequired()).thenReturn(false); + + final TetheringConfiguration cfg = new TetheringConfiguration( + mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + final Iterator upstreamIterator = cfg.preferredUpstreamIfaceTypes.iterator(); + assertTrue(upstreamIterator.hasNext()); + assertEquals(TYPE_ETHERNET, upstreamIterator.next().intValue()); + assertTrue(upstreamIterator.hasNext()); + assertEquals(TYPE_WIFI, upstreamIterator.next().intValue()); + assertTrue(upstreamIterator.hasNext()); + assertEquals(TYPE_MOBILE_HIPRI, upstreamIterator.next().intValue()); + assertFalse(upstreamIterator.hasNext()); + } + + @Test + public void testDefinedUpstreamTypesWithEthernetDoesNotAddEthernet() { + when(mResources.getIntArray(R.array.config_tether_upstream_types)) + .thenReturn(new int[]{TYPE_WIFI, TYPE_ETHERNET, TYPE_MOBILE_HIPRI}); + when(mTelephonyManager.isTetheringApnRequired()).thenReturn(false); + + final TetheringConfiguration cfg = new TetheringConfiguration( + mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + final Iterator upstreamIterator = cfg.preferredUpstreamIfaceTypes.iterator(); + assertTrue(upstreamIterator.hasNext()); + assertEquals(TYPE_WIFI, upstreamIterator.next().intValue()); + assertTrue(upstreamIterator.hasNext()); + assertEquals(TYPE_ETHERNET, upstreamIterator.next().intValue()); + assertTrue(upstreamIterator.hasNext()); + assertEquals(TYPE_MOBILE_HIPRI, upstreamIterator.next().intValue()); + assertFalse(upstreamIterator.hasNext()); + } + + private void initializeBpfOffloadConfiguration( + final boolean fromRes, final String fromDevConfig) { + when(mResources.getBoolean(R.bool.config_tether_enable_bpf_offload)).thenReturn(fromRes); + doReturn(fromDevConfig).when( + () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY), + eq(TetheringConfiguration.OVERRIDE_TETHER_ENABLE_BPF_OFFLOAD))); + } + + @Test + public void testBpfOffloadEnabledByResource() { + initializeBpfOffloadConfiguration(true, null /* unset */); + final TetheringConfiguration enableByRes = + new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + assertTrue(enableByRes.isBpfOffloadEnabled()); + } + + @Test + public void testBpfOffloadEnabledByDeviceConfigOverride() { + for (boolean res : new boolean[]{true, false}) { + initializeBpfOffloadConfiguration(res, "true"); + final TetheringConfiguration enableByDevConOverride = + new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + assertTrue(enableByDevConOverride.isBpfOffloadEnabled()); + } + } + + @Test + public void testBpfOffloadDisabledByResource() { + initializeBpfOffloadConfiguration(false, null /* unset */); + final TetheringConfiguration disableByRes = + new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + assertFalse(disableByRes.isBpfOffloadEnabled()); + } + + @Test + public void testBpfOffloadDisabledByDeviceConfigOverride() { + for (boolean res : new boolean[]{true, false}) { + initializeBpfOffloadConfiguration(res, "false"); + final TetheringConfiguration disableByDevConOverride = + new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + assertFalse(disableByDevConOverride.isBpfOffloadEnabled()); + } + } + + @Test + public void testNewDhcpServerDisabled() { + when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn( + true); + doReturn("false").when( + () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY), + eq(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER))); + + final TetheringConfiguration enableByRes = + new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + assertTrue(enableByRes.enableLegacyDhcpServer); + + when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn( + false); + doReturn("true").when( + () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY), + eq(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER))); + + final TetheringConfiguration enableByDevConfig = + new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + assertTrue(enableByDevConfig.enableLegacyDhcpServer); + } + + @Test + public void testNewDhcpServerEnabled() { + when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn( + false); + doReturn("false").when( + () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY), + eq(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER))); + + final TetheringConfiguration cfg = + new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + + assertFalse(cfg.enableLegacyDhcpServer); + } + + @Test + public void testOffloadIntervalByResource() { + final TetheringConfiguration intervalByDefault = + new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + assertEquals(TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, + intervalByDefault.getOffloadPollInterval()); + + final int[] testOverrides = {0, 3000, -1}; + for (final int override : testOverrides) { + when(mResources.getInteger(R.integer.config_tether_offload_poll_interval)).thenReturn( + override); + final TetheringConfiguration overrideByRes = + new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + assertEquals(override, overrideByRes.getOffloadPollInterval()); + } + } + + @Test + public void testGetResourcesBySubId() { + setUpResourceForSubId(); + final TetheringConfiguration cfg = new TetheringConfiguration( + mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + assertTrue(cfg.provisioningApp.length == 0); + final int anyValidSubId = 1; + final MockTetheringConfiguration mockCfg = + new MockTetheringConfiguration(mMockContext, mLog, anyValidSubId); + assertEquals(mockCfg.provisioningApp[0], PROVISIONING_APP_NAME[0]); + assertEquals(mockCfg.provisioningApp[1], PROVISIONING_APP_NAME[1]); + assertEquals(mockCfg.provisioningAppNoUi, PROVISIONING_NO_UI_APP_NAME); + assertEquals(mockCfg.provisioningResponse, PROVISIONING_APP_RESPONSE); + } + + private void setUpResourceForSubId() { + when(mResourcesForSubId.getStringArray( + R.array.config_tether_dhcp_range)).thenReturn(new String[0]); + when(mResourcesForSubId.getStringArray( + R.array.config_tether_usb_regexs)).thenReturn(new String[0]); + when(mResourcesForSubId.getStringArray( + R.array.config_tether_wifi_regexs)).thenReturn(new String[]{ "test_wlan\\d" }); + when(mResourcesForSubId.getStringArray( + R.array.config_tether_bluetooth_regexs)).thenReturn(new String[0]); + when(mResourcesForSubId.getIntArray(R.array.config_tether_upstream_types)).thenReturn( + new int[0]); + when(mResourcesForSubId.getStringArray( + R.array.config_mobile_hotspot_provision_app)).thenReturn(PROVISIONING_APP_NAME); + when(mResourcesForSubId.getString(R.string.config_mobile_hotspot_provision_app_no_ui)) + .thenReturn(PROVISIONING_NO_UI_APP_NAME); + when(mResourcesForSubId.getString( + R.string.config_mobile_hotspot_provision_response)).thenReturn( + PROVISIONING_APP_RESPONSE); + } + + @Test + public void testEnableLegacyWifiP2PAddress() throws Exception { + final TetheringConfiguration defaultCfg = new TetheringConfiguration( + mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + assertFalse(defaultCfg.shouldEnableWifiP2pDedicatedIp()); + + when(mResources.getBoolean(R.bool.config_tether_enable_legacy_wifi_p2p_dedicated_ip)) + .thenReturn(true); + final TetheringConfiguration testCfg = new TetheringConfiguration( + mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + assertTrue(testCfg.shouldEnableWifiP2pDedicatedIp()); + } + + private void initEnableSelectAllPrefixRangeFlag(final String value) { + doReturn(value).when( + () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY), + eq(TetheringConfiguration.TETHER_ENABLE_SELECT_ALL_PREFIX_RANGES))); + } + + @Test + public void testSelectAllPrefixRangeFlag() throws Exception { + // Test default value. + final TetheringConfiguration defaultCfg = new TetheringConfiguration( + mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + assertTrue(defaultCfg.isSelectAllPrefixRangeEnabled()); + + // Test disable flag. + initEnableSelectAllPrefixRangeFlag("false"); + final TetheringConfiguration testDisable = new TetheringConfiguration( + mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + assertFalse(testDisable.isSelectAllPrefixRangeEnabled()); + + // Test enable flag. + initEnableSelectAllPrefixRangeFlag("true"); + final TetheringConfiguration testEnable = new TetheringConfiguration( + mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + assertTrue(testEnable.isSelectAllPrefixRangeEnabled()); + } + + @Test + public void testChooseUpstreamAutomatically() throws Exception { + when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)) + .thenReturn(true); + assertChooseUpstreamAutomaticallyIs(true); + + when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)) + .thenReturn(false); + assertChooseUpstreamAutomaticallyIs(false); + } + + // The flag override only works on R- + @Test @IgnoreAfter(Build.VERSION_CODES.R) + public void testChooseUpstreamAutomatically_FlagOverride() throws Exception { + when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)) + .thenReturn(false); + setTetherForceUpstreamAutomaticFlagVersion(TEST_PACKAGE_VERSION - 1); + assertTrue(DeviceConfigUtils.isFeatureEnabled(mMockContext, NAMESPACE_CONNECTIVITY, + TetheringConfiguration.TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION, APEX_NAME, false)); + + assertChooseUpstreamAutomaticallyIs(true); + + setTetherForceUpstreamAutomaticFlagVersion(0L); + assertChooseUpstreamAutomaticallyIs(false); + + setTetherForceUpstreamAutomaticFlagVersion(Long.MAX_VALUE); + assertChooseUpstreamAutomaticallyIs(false); + } + + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testChooseUpstreamAutomatically_FlagOverrideAfterR() throws Exception { + when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)) + .thenReturn(false); + setTetherForceUpstreamAutomaticFlagVersion(TEST_PACKAGE_VERSION - 1); + assertChooseUpstreamAutomaticallyIs(false); + } + + private void setTetherForceUpstreamAutomaticFlagVersion(Long version) { + doReturn(version == null ? null : Long.toString(version)).when( + () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY), + eq(TetheringConfiguration.TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION))); + } + + private void assertChooseUpstreamAutomaticallyIs(boolean value) { + assertEquals(value, new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID) + .chooseUpstreamAutomatically); + } +} diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt new file mode 100644 index 0000000000..75c819bb0c --- /dev/null +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringNotificationUpdaterTest.kt @@ -0,0 +1,444 @@ +/* + * 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.networkstack.tethering + +import android.app.Notification +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.PendingIntent.FLAG_IMMUTABLE +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.content.res.Resources +import android.net.ConnectivityManager.TETHERING_WIFI +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper +import android.os.UserHandle +import android.provider.Settings +import android.telephony.TelephonyManager +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.AndroidJUnit4 +import com.android.internal.util.test.BroadcastInterceptingContext +import com.android.networkstack.tethering.TetheringNotificationUpdater.ACTION_DISABLE_TETHERING +import com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE +import com.android.networkstack.tethering.TetheringNotificationUpdater.EVENT_SHOW_NO_UPSTREAM +import com.android.networkstack.tethering.TetheringNotificationUpdater.NO_UPSTREAM_NOTIFICATION_ID +import com.android.networkstack.tethering.TetheringNotificationUpdater.RESTRICTED_NOTIFICATION_ID +import com.android.networkstack.tethering.TetheringNotificationUpdater.ROAMING_NOTIFICATION_ID +import com.android.networkstack.tethering.TetheringNotificationUpdater.VERIZON_CARRIER_ID +import com.android.networkstack.tethering.TetheringNotificationUpdater.getSettingsPackageName +import com.android.testutils.waitForIdle +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.MockitoAnnotations + +const val TEST_SUBID = 1 +const val WIFI_MASK = 1 shl TETHERING_WIFI +const val TEST_DISALLOW_TITLE = "Tether function is disallowed" +const val TEST_DISALLOW_MESSAGE = "Please contact your admin" +const val TEST_NO_UPSTREAM_TITLE = "Hotspot has no internet access" +const val TEST_NO_UPSTREAM_MESSAGE = "Device cannot connect to internet." +const val TEST_NO_UPSTREAM_BUTTON = "Turn off hotspot" +const val TEST_ROAMING_TITLE = "Hotspot is on" +const val TEST_ROAMING_MESSAGE = "Additional charges may apply while roaming." + +@RunWith(AndroidJUnit4::class) +@SmallTest +class TetheringNotificationUpdaterTest { + // lateinit used here for mocks as they need to be reinitialized between each test and the test + // should crash if they are used before being initialized. + @Mock private lateinit var mockContext: Context + @Mock private lateinit var notificationManager: NotificationManager + @Mock private lateinit var telephonyManager: TelephonyManager + @Mock private lateinit var testResources: Resources + + // lateinit for these classes under test, as they should be reset to a different instance for + // every test but should always be initialized before use (or the test should crash). + private lateinit var context: TestContext + private lateinit var notificationUpdater: TetheringNotificationUpdater + + // Initializing the following members depends on initializing some of the mocks and + // is more logically done in setup(). + private lateinit var fakeTetheringThread: HandlerThread + + private val ROAMING_CAPABILITIES = NetworkCapabilities() + private val HOME_CAPABILITIES = NetworkCapabilities().addCapability(NET_CAPABILITY_NOT_ROAMING) + private val NOTIFICATION_ICON_ID = R.drawable.stat_sys_tether_general + private val TIMEOUT_MS = 500L + private val ACTIVITY_PENDING_INTENT = 0 + private val BROADCAST_PENDING_INTENT = 1 + + private inner class TestContext(c: Context) : BroadcastInterceptingContext(c) { + override fun createContextAsUser(user: UserHandle, flags: Int) = + if (user == UserHandle.ALL) mockContext else this + override fun getSystemService(name: String) = + if (name == Context.TELEPHONY_SERVICE) telephonyManager + else super.getSystemService(name) + } + + private inner class WrappedNotificationUpdater(c: Context, looper: Looper) + : TetheringNotificationUpdater(c, looper) { + override fun getResourcesForSubId(c: Context, subId: Int) = + if (subId == TEST_SUBID) testResources else super.getResourcesForSubId(c, subId) + } + + private fun setupResources() { + doReturn(5).`when`(testResources) + .getInteger(R.integer.delay_to_show_no_upstream_after_no_backhaul) + doReturn(true).`when`(testResources) + .getBoolean(R.bool.config_upstream_roaming_notification) + doReturn(TEST_DISALLOW_TITLE).`when`(testResources) + .getString(R.string.disable_tether_notification_title) + doReturn(TEST_DISALLOW_MESSAGE).`when`(testResources) + .getString(R.string.disable_tether_notification_message) + doReturn(TEST_NO_UPSTREAM_TITLE).`when`(testResources) + .getString(R.string.no_upstream_notification_title) + doReturn(TEST_NO_UPSTREAM_MESSAGE).`when`(testResources) + .getString(R.string.no_upstream_notification_message) + doReturn(TEST_NO_UPSTREAM_BUTTON).`when`(testResources) + .getString(R.string.no_upstream_notification_disable_button) + doReturn(TEST_ROAMING_TITLE).`when`(testResources) + .getString(R.string.upstream_roaming_notification_title) + doReturn(TEST_ROAMING_MESSAGE).`when`(testResources) + .getString(R.string.upstream_roaming_notification_message) + } + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + context = TestContext(InstrumentationRegistry.getInstrumentation().context) + doReturn(notificationManager).`when`(mockContext) + .getSystemService(Context.NOTIFICATION_SERVICE) + fakeTetheringThread = HandlerThread(this::class.java.simpleName) + fakeTetheringThread.start() + notificationUpdater = WrappedNotificationUpdater(context, fakeTetheringThread.looper) + setupResources() + } + + @After + fun tearDown() { + fakeTetheringThread.quitSafely() + } + + private fun verifyActivityPendingIntent(intent: Intent, flags: Int) { + // Use FLAG_NO_CREATE to verify whether PendingIntent has FLAG_IMMUTABLE flag(forcefully add + // the flag in creating arguments). If the described PendingIntent does not already exist, + // getActivity() will return null instead of PendingIntent object. + val pi = PendingIntent.getActivity( + context.createContextAsUser(UserHandle.CURRENT, 0 /* flags */), + 0 /* requestCode */, + intent, + flags or FLAG_IMMUTABLE or PendingIntent.FLAG_NO_CREATE, + null /* options */) + assertNotNull("Activity PendingIntent with FLAG_IMMUTABLE does not exist.", pi) + } + + private fun verifyBroadcastPendingIntent(intent: Intent, flags: Int) { + // Use FLAG_NO_CREATE to verify whether PendingIntent has FLAG_IMMUTABLE flag(forcefully add + // the flag in creating arguments). If the described PendingIntent does not already exist, + // getBroadcast() will return null instead of PendingIntent object. + val pi = PendingIntent.getBroadcast( + context.createContextAsUser(UserHandle.CURRENT, 0 /* flags */), + 0 /* requestCode */, + intent, + flags or FLAG_IMMUTABLE or PendingIntent.FLAG_NO_CREATE) + assertNotNull("Broadcast PendingIntent with FLAG_IMMUTABLE does not exist.", pi) + } + + private fun Notification.title() = this.extras.getString(Notification.EXTRA_TITLE) + private fun Notification.text() = this.extras.getString(Notification.EXTRA_TEXT) + + private fun verifyNotification( + iconId: Int, + title: String, + text: String, + id: Int, + intentSenderType: Int, + intent: Intent, + flags: Int + ) { + verify(notificationManager, never()).cancel(any(), eq(id)) + + val notificationCaptor = ArgumentCaptor.forClass(Notification::class.java) + verify(notificationManager, times(1)) + .notify(any(), eq(id), notificationCaptor.capture()) + + val notification = notificationCaptor.getValue() + assertEquals(iconId, notification.smallIcon.resId) + assertEquals(title, notification.title()) + assertEquals(text, notification.text()) + + when (intentSenderType) { + ACTIVITY_PENDING_INTENT -> verifyActivityPendingIntent(intent, flags) + BROADCAST_PENDING_INTENT -> verifyBroadcastPendingIntent(intent, flags) + } + + reset(notificationManager) + } + + private fun verifyNotificationCancelled( + notificationIds: List, + resetAfterVerified: Boolean = true + ) { + notificationIds.forEach { + verify(notificationManager, times(1)).cancel(any(), eq(it)) + } + if (resetAfterVerified) reset(notificationManager) + } + + @Test + fun testRestrictedNotification() { + val settingsIntent = Intent(Settings.ACTION_TETHER_SETTINGS) + .setPackage(getSettingsPackageName(context.packageManager)) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + // Set test sub id. + notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID) + verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID)) + + // User restrictions on. Show restricted notification. + notificationUpdater.notifyTetheringDisabledByRestriction() + verifyNotification(NOTIFICATION_ICON_ID, TEST_DISALLOW_TITLE, TEST_DISALLOW_MESSAGE, + RESTRICTED_NOTIFICATION_ID, ACTIVITY_PENDING_INTENT, settingsIntent, FLAG_IMMUTABLE) + + // User restrictions off. Clear notification. + notificationUpdater.tetheringRestrictionLifted() + verifyNotificationCancelled(listOf(RESTRICTED_NOTIFICATION_ID)) + + // No downstream. + notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE) + verifyZeroInteractions(notificationManager) + + // User restrictions on again. Show restricted notification. + notificationUpdater.notifyTetheringDisabledByRestriction() + verifyNotification(NOTIFICATION_ICON_ID, TEST_DISALLOW_TITLE, TEST_DISALLOW_MESSAGE, + RESTRICTED_NOTIFICATION_ID, ACTIVITY_PENDING_INTENT, settingsIntent, FLAG_IMMUTABLE) + } + + val MAX_BACKOFF_MS = 200L + /** + * Waits for all messages, including delayed ones, to be processed. + * + * This will wait until the handler has no more messages to be processed including + * delayed ones, or the timeout has expired. It uses an exponential backoff strategy + * to wait longer and longer to consume less CPU, with the max granularity being + * MAX_BACKOFF_MS. + * + * @return true if all messages have been processed including delayed ones, false if timeout + * + * TODO: Move this method to com.android.testutils.HandlerUtils.kt. + */ + private fun Handler.waitForDelayedMessage(what: Int?, timeoutMs: Long) { + fun hasMatchingMessages() = + if (what == null) hasMessagesOrCallbacks() else hasMessages(what) + val expiry = System.currentTimeMillis() + timeoutMs + var delay = 5L + while (System.currentTimeMillis() < expiry && hasMatchingMessages()) { + // None of Handler, Looper, Message and MessageQueue expose any way to retrieve + // the time when the next (let alone the last) message will be processed, so + // short of examining the internals with reflection sleep() is the only solution. + Thread.sleep(delay) + delay = (delay * 2) + .coerceAtMost(expiry - System.currentTimeMillis()) + .coerceAtMost(MAX_BACKOFF_MS) + } + + val timeout = expiry - System.currentTimeMillis() + if (timeout <= 0) fail("Delayed message did not process yet after ${timeoutMs}ms") + waitForIdle(timeout) + } + + @Test + fun testNoUpstreamNotification() { + val disableIntent = Intent(ACTION_DISABLE_TETHERING).setPackage(context.packageName) + + // Set test sub id. + notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID) + verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID)) + + // Wifi downstream. + notificationUpdater.onDownstreamChanged(WIFI_MASK) + verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID)) + + // There is no upstream. Show no upstream notification. + notificationUpdater.onUpstreamCapabilitiesChanged(null) + notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, TIMEOUT_MS) + verifyNotification(NOTIFICATION_ICON_ID, TEST_NO_UPSTREAM_TITLE, TEST_NO_UPSTREAM_MESSAGE, + NO_UPSTREAM_NOTIFICATION_ID, BROADCAST_PENDING_INTENT, disableIntent, + FLAG_IMMUTABLE) + + // Same capabilities changed. Nothing happened. + notificationUpdater.onUpstreamCapabilitiesChanged(null) + verifyZeroInteractions(notificationManager) + + // Upstream come back. Clear no upstream notification. + notificationUpdater.onUpstreamCapabilitiesChanged(HOME_CAPABILITIES) + verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID)) + + // No upstream again. Show no upstream notification. + notificationUpdater.onUpstreamCapabilitiesChanged(null) + notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, TIMEOUT_MS) + verifyNotification(NOTIFICATION_ICON_ID, TEST_NO_UPSTREAM_TITLE, TEST_NO_UPSTREAM_MESSAGE, + NO_UPSTREAM_NOTIFICATION_ID, BROADCAST_PENDING_INTENT, disableIntent, + FLAG_IMMUTABLE) + + // No downstream. + notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE) + verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID)) + + // Wifi downstream and home capabilities. + notificationUpdater.onDownstreamChanged(WIFI_MASK) + notificationUpdater.onUpstreamCapabilitiesChanged(HOME_CAPABILITIES) + verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID)) + + // Set R.integer.delay_to_show_no_upstream_after_no_backhaul to -1 and change to no upstream + // again. Don't put up no upstream notification. + doReturn(-1).`when`(testResources) + .getInteger(R.integer.delay_to_show_no_upstream_after_no_backhaul) + notificationUpdater.onUpstreamCapabilitiesChanged(null) + notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, TIMEOUT_MS) + verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID)) + } + + @Test + fun testGetResourcesForSubId() { + doReturn(telephonyManager).`when`(telephonyManager).createForSubscriptionId(anyInt()) + doReturn(1234).`when`(telephonyManager).getSimCarrierId() + doReturn("000000").`when`(telephonyManager).getSimOperator() + + val subId = -2 // Use invalid subId to avoid getting resource from cache or real subId. + val config = context.resources.configuration + var res = notificationUpdater.getResourcesForSubId(context, subId) + assertEquals(config.mcc, res.configuration.mcc) + assertEquals(config.mnc, res.configuration.mnc) + + doReturn(VERIZON_CARRIER_ID).`when`(telephonyManager).getSimCarrierId() + res = notificationUpdater.getResourcesForSubId(context, subId) + assertEquals(config.mcc, res.configuration.mcc) + assertEquals(config.mnc, res.configuration.mnc) + + doReturn("20404").`when`(telephonyManager).getSimOperator() + res = notificationUpdater.getResourcesForSubId(context, subId) + assertEquals(311, res.configuration.mcc) + assertEquals(480, res.configuration.mnc) + } + + @Test + fun testRoamingNotification() { + val disableIntent = Intent(ACTION_DISABLE_TETHERING).setPackage(context.packageName) + val settingsIntent = Intent(Settings.ACTION_TETHER_SETTINGS) + .setPackage(getSettingsPackageName(context.packageManager)) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + // Set test sub id. + notificationUpdater.onActiveDataSubscriptionIdChanged(TEST_SUBID) + verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID)) + + // Wifi downstream. + notificationUpdater.onDownstreamChanged(WIFI_MASK) + verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID)) + + // Upstream capabilities changed to roaming state. Show roaming notification. + notificationUpdater.onUpstreamCapabilitiesChanged(ROAMING_CAPABILITIES) + verifyNotification(NOTIFICATION_ICON_ID, TEST_ROAMING_TITLE, TEST_ROAMING_MESSAGE, + ROAMING_NOTIFICATION_ID, ACTIVITY_PENDING_INTENT, settingsIntent, FLAG_IMMUTABLE) + + // Same capabilities change. Nothing happened. + notificationUpdater.onUpstreamCapabilitiesChanged(ROAMING_CAPABILITIES) + verifyZeroInteractions(notificationManager) + + // Upstream capabilities changed to home state. Clear roaming notification. + notificationUpdater.onUpstreamCapabilitiesChanged(HOME_CAPABILITIES) + verifyNotificationCancelled(listOf(ROAMING_NOTIFICATION_ID)) + + // Upstream capabilities changed to roaming state again. Show roaming notification. + notificationUpdater.onUpstreamCapabilitiesChanged(ROAMING_CAPABILITIES) + verifyNotification(NOTIFICATION_ICON_ID, TEST_ROAMING_TITLE, TEST_ROAMING_MESSAGE, + ROAMING_NOTIFICATION_ID, ACTIVITY_PENDING_INTENT, settingsIntent, FLAG_IMMUTABLE) + + // No upstream. Clear roaming notification and show no upstream notification. + notificationUpdater.onUpstreamCapabilitiesChanged(null) + notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, TIMEOUT_MS) + verifyNotificationCancelled(listOf(ROAMING_NOTIFICATION_ID), false) + verifyNotification(NOTIFICATION_ICON_ID, TEST_NO_UPSTREAM_TITLE, TEST_NO_UPSTREAM_MESSAGE, + NO_UPSTREAM_NOTIFICATION_ID, BROADCAST_PENDING_INTENT, disableIntent, + FLAG_IMMUTABLE) + + // No downstream. + notificationUpdater.onDownstreamChanged(DOWNSTREAM_NONE) + verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID)) + + // Wifi downstream again. + notificationUpdater.onDownstreamChanged(WIFI_MASK) + notificationUpdater.handler.waitForDelayedMessage(EVENT_SHOW_NO_UPSTREAM, TIMEOUT_MS) + verifyNotificationCancelled(listOf(ROAMING_NOTIFICATION_ID), false) + verifyNotification(NOTIFICATION_ICON_ID, TEST_NO_UPSTREAM_TITLE, TEST_NO_UPSTREAM_MESSAGE, + NO_UPSTREAM_NOTIFICATION_ID, BROADCAST_PENDING_INTENT, disableIntent, + FLAG_IMMUTABLE) + + // Set R.bool.config_upstream_roaming_notification to false and change upstream + // network to roaming state again. No roaming notification. + doReturn(false).`when`(testResources) + .getBoolean(R.bool.config_upstream_roaming_notification) + notificationUpdater.onUpstreamCapabilitiesChanged(ROAMING_CAPABILITIES) + verifyNotificationCancelled(listOf(NO_UPSTREAM_NOTIFICATION_ID, ROAMING_NOTIFICATION_ID)) + } + + @Test + fun testGetSettingsPackageName() { + val defaultSettingsPackageName = "com.android.settings" + val testSettingsPackageName = "com.android.test.settings" + val pm = mock(PackageManager::class.java) + doReturn(null).`when`(pm).resolveActivity(any(), anyInt()) + assertEquals(defaultSettingsPackageName, getSettingsPackageName(pm)) + + val resolveInfo = ResolveInfo().apply { + activityInfo = ActivityInfo().apply { + name = "test" + applicationInfo = ApplicationInfo().apply { + packageName = testSettingsPackageName + } + } + } + doReturn(resolveInfo).`when`(pm).resolveActivity(any(), anyInt()) + assertEquals(testSettingsPackageName, getSettingsPackageName(pm)) + } +} diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java new file mode 100644 index 0000000000..941cd78a62 --- /dev/null +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringServiceTest.java @@ -0,0 +1,488 @@ +/* + * 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.networkstack.tethering; + +import static android.Manifest.permission.ACCESS_NETWORK_STATE; +import static android.Manifest.permission.TETHER_PRIVILEGED; +import static android.Manifest.permission.WRITE_SETTINGS; +import static android.net.TetheringManager.TETHERING_WIFI; +import static android.net.TetheringManager.TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION; +import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION; +import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.app.UiAutomation; +import android.content.Intent; +import android.net.IIntResultListener; +import android.net.ITetheringConnector; +import android.net.ITetheringEventCallback; +import android.net.TetheringRequestParcel; +import android.net.ip.IpServer; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.rule.ServiceTestRule; +import androidx.test.runner.AndroidJUnit4; + +import com.android.networkstack.tethering.MockTetheringService.MockTetheringConnector; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public final class TetheringServiceTest { + private static final String TEST_IFACE_NAME = "test_wlan0"; + private static final String TEST_CALLER_PKG = "com.android.shell"; + private static final String TEST_ATTRIBUTION_TAG = null; + @Mock private ITetheringEventCallback mITetheringEventCallback; + @Rule public ServiceTestRule mServiceTestRule; + private Tethering mTethering; + private Intent mMockServiceIntent; + private ITetheringConnector mTetheringConnector; + private UiAutomation mUiAutomation; + + private class TestTetheringResult extends IIntResultListener.Stub { + private int mResult = -1; // Default value that does not match any result code. + @Override + public void onResult(final int resultCode) { + mResult = resultCode; + } + + public void assertResult(final int expected) { + assertEquals(expected, mResult); + } + } + + private class MyResultReceiver extends ResultReceiver { + MyResultReceiver(Handler handler) { + super(handler); + } + private int mResult = -1; // Default value that does not match any result code. + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + mResult = resultCode; + } + + public void assertResult(int expected) { + assertEquals(expected, mResult); + } + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mUiAutomation = + InstrumentationRegistry.getInstrumentation().getUiAutomation(); + mServiceTestRule = new ServiceTestRule(); + mMockServiceIntent = new Intent( + InstrumentationRegistry.getTargetContext(), + MockTetheringService.class); + final MockTetheringConnector mockConnector = + (MockTetheringConnector) mServiceTestRule.bindService(mMockServiceIntent); + mTetheringConnector = mockConnector.getTetheringConnector(); + final MockTetheringService service = mockConnector.getService(); + mTethering = service.getTethering(); + } + + @After + public void tearDown() throws Exception { + mServiceTestRule.unbindService(); + mUiAutomation.dropShellPermissionIdentity(); + } + + private interface TestTetheringCall { + void runTetheringCall(TestTetheringResult result) throws Exception; + } + + private void runAsNoPermission(final TestTetheringCall test) throws Exception { + runTetheringCall(test, new String[0]); + } + + private void runAsTetherPrivileged(final TestTetheringCall test) throws Exception { + runTetheringCall(test, TETHER_PRIVILEGED); + } + + private void runAsAccessNetworkState(final TestTetheringCall test) throws Exception { + runTetheringCall(test, ACCESS_NETWORK_STATE); + } + + private void runAsWriteSettings(final TestTetheringCall test) throws Exception { + runTetheringCall(test, WRITE_SETTINGS); + } + + private void runTetheringCall(final TestTetheringCall test, String... permissions) + throws Exception { + if (permissions.length > 0) mUiAutomation.adoptShellPermissionIdentity(permissions); + try { + when(mTethering.isTetheringSupported()).thenReturn(true); + test.runTetheringCall(new TestTetheringResult()); + } finally { + mUiAutomation.dropShellPermissionIdentity(); + } + } + + private void verifyNoMoreInteractionsForTethering() { + verifyNoMoreInteractions(mTethering); + verifyNoMoreInteractions(mITetheringEventCallback); + reset(mTethering, mITetheringEventCallback); + } + + private void runTether(final TestTetheringResult result) throws Exception { + mTetheringConnector.tether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result); + verify(mTethering).isTetheringSupported(); + verify(mTethering).tether(TEST_IFACE_NAME, IpServer.STATE_TETHERED, result); + } + + @Test + public void testTether() throws Exception { + runAsNoPermission((result) -> { + mTetheringConnector.tether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, + result); + verify(mTethering).isTetherProvisioningRequired(); + result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION); + verifyNoMoreInteractionsForTethering(); + }); + + runAsTetherPrivileged((result) -> { + runTether(result); + verifyNoMoreInteractionsForTethering(); + }); + + runAsWriteSettings((result) -> { + runTether(result); + verify(mTethering).isTetherProvisioningRequired(); + verifyNoMoreInteractionsForTethering(); + }); + } + + private void runUnTether(final TestTetheringResult result) throws Exception { + mTetheringConnector.untether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, + result); + verify(mTethering).isTetheringSupported(); + verify(mTethering).untether(eq(TEST_IFACE_NAME), eq(result)); + } + + @Test + public void testUntether() throws Exception { + runAsNoPermission((result) -> { + mTetheringConnector.untether(TEST_IFACE_NAME, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, + result); + verify(mTethering).isTetherProvisioningRequired(); + result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION); + verifyNoMoreInteractionsForTethering(); + }); + + runAsTetherPrivileged((result) -> { + runUnTether(result); + verifyNoMoreInteractionsForTethering(); + }); + + runAsWriteSettings((result) -> { + runUnTether(result); + verify(mTethering).isTetherProvisioningRequired(); + verifyNoMoreInteractionsForTethering(); + }); + } + + private void runSetUsbTethering(final TestTetheringResult result) throws Exception { + doAnswer((invocation) -> { + final IIntResultListener listener = invocation.getArgument(1); + listener.onResult(TETHER_ERROR_NO_ERROR); + return null; + }).when(mTethering).setUsbTethering(anyBoolean(), any(IIntResultListener.class)); + mTetheringConnector.setUsbTethering(true /* enable */, TEST_CALLER_PKG, + TEST_ATTRIBUTION_TAG, result); + verify(mTethering).isTetheringSupported(); + verify(mTethering).setUsbTethering(eq(true) /* enable */, any(IIntResultListener.class)); + result.assertResult(TETHER_ERROR_NO_ERROR); + } + + @Test + public void testSetUsbTethering() throws Exception { + runAsNoPermission((result) -> { + mTetheringConnector.setUsbTethering(true /* enable */, TEST_CALLER_PKG, + TEST_ATTRIBUTION_TAG, result); + verify(mTethering).isTetherProvisioningRequired(); + result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION); + verifyNoMoreInteractionsForTethering(); + }); + + runAsTetherPrivileged((result) -> { + runSetUsbTethering(result); + verifyNoMoreInteractionsForTethering(); + }); + + runAsWriteSettings((result) -> { + runSetUsbTethering(result); + verify(mTethering).isTetherProvisioningRequired(); + verifyNoMoreInteractionsForTethering(); + }); + + } + + private void runStartTethering(final TestTetheringResult result, + final TetheringRequestParcel request) throws Exception { + mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, + result); + verify(mTethering).isTetheringSupported(); + verify(mTethering).startTethering(eq(request), eq(result)); + } + + @Test + public void testStartTethering() throws Exception { + final TetheringRequestParcel request = new TetheringRequestParcel(); + request.tetheringType = TETHERING_WIFI; + + runAsNoPermission((result) -> { + mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, + result); + verify(mTethering).isTetherProvisioningRequired(); + result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION); + verifyNoMoreInteractionsForTethering(); + }); + + runAsTetherPrivileged((result) -> { + runStartTethering(result, request); + verifyNoMoreInteractionsForTethering(); + }); + + runAsWriteSettings((result) -> { + runStartTethering(result, request); + verify(mTethering).isTetherProvisioningRequired(); + verifyNoMoreInteractionsForTethering(); + }); + } + + private void runStartTetheringAndVerifyNoPermission(final TestTetheringResult result) + throws Exception { + final TetheringRequestParcel request = new TetheringRequestParcel(); + request.tetheringType = TETHERING_WIFI; + request.exemptFromEntitlementCheck = true; + mTetheringConnector.startTethering(request, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, + result); + result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION); + verifyNoMoreInteractionsForTethering(); + } + + @Test + public void testFailToBypassEntitlementWithoutNeworkStackPermission() throws Exception { + final TetheringRequestParcel request = new TetheringRequestParcel(); + request.tetheringType = TETHERING_WIFI; + request.exemptFromEntitlementCheck = true; + + runAsNoPermission((result) -> { + runStartTetheringAndVerifyNoPermission(result); + }); + + runAsTetherPrivileged((result) -> { + runStartTetheringAndVerifyNoPermission(result); + }); + + runAsWriteSettings((result) -> { + runStartTetheringAndVerifyNoPermission(result); + }); + } + + private void runStopTethering(final TestTetheringResult result) throws Exception { + mTetheringConnector.stopTethering(TETHERING_WIFI, TEST_CALLER_PKG, + TEST_ATTRIBUTION_TAG, result); + verify(mTethering).isTetheringSupported(); + verify(mTethering).stopTethering(TETHERING_WIFI); + result.assertResult(TETHER_ERROR_NO_ERROR); + } + + @Test + public void testStopTethering() throws Exception { + runAsNoPermission((result) -> { + mTetheringConnector.stopTethering(TETHERING_WIFI, TEST_CALLER_PKG, + TEST_ATTRIBUTION_TAG, result); + verify(mTethering).isTetherProvisioningRequired(); + result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION); + verifyNoMoreInteractionsForTethering(); + }); + + runAsTetherPrivileged((result) -> { + runStopTethering(result); + verifyNoMoreInteractionsForTethering(); + }); + + runAsWriteSettings((result) -> { + runStopTethering(result); + verify(mTethering).isTetherProvisioningRequired(); + verifyNoMoreInteractionsForTethering(); + }); + } + + private void runRequestLatestTetheringEntitlementResult() throws Exception { + final MyResultReceiver result = new MyResultReceiver(null); + mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result, + true /* showEntitlementUi */, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG); + verify(mTethering).isTetheringSupported(); + verify(mTethering).requestLatestTetheringEntitlementResult(eq(TETHERING_WIFI), + eq(result), eq(true) /* showEntitlementUi */); + } + + @Test + public void testRequestLatestTetheringEntitlementResult() throws Exception { + // Run as no permission. + final MyResultReceiver result = new MyResultReceiver(null); + mTetheringConnector.requestLatestTetheringEntitlementResult(TETHERING_WIFI, result, + true /* showEntitlementUi */, TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG); + verify(mTethering).isTetherProvisioningRequired(); + result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION); + verifyNoMoreInteractions(mTethering); + + runAsTetherPrivileged((none) -> { + runRequestLatestTetheringEntitlementResult(); + verifyNoMoreInteractionsForTethering(); + }); + + runAsWriteSettings((none) -> { + runRequestLatestTetheringEntitlementResult(); + verify(mTethering).isTetherProvisioningRequired(); + verifyNoMoreInteractionsForTethering(); + }); + } + + private void runRegisterTetheringEventCallback() throws Exception { + mTetheringConnector.registerTetheringEventCallback(mITetheringEventCallback, + TEST_CALLER_PKG); + verify(mTethering).registerTetheringEventCallback(eq(mITetheringEventCallback)); + } + + @Test + public void testRegisterTetheringEventCallback() throws Exception { + runAsNoPermission((result) -> { + mTetheringConnector.registerTetheringEventCallback(mITetheringEventCallback, + TEST_CALLER_PKG); + verify(mITetheringEventCallback).onCallbackStopped( + TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION); + verifyNoMoreInteractionsForTethering(); + }); + + runAsTetherPrivileged((none) -> { + runRegisterTetheringEventCallback(); + verifyNoMoreInteractionsForTethering(); + }); + + runAsAccessNetworkState((none) -> { + runRegisterTetheringEventCallback(); + verifyNoMoreInteractionsForTethering(); + }); + } + + private void runUnregisterTetheringEventCallback() throws Exception { + mTetheringConnector.unregisterTetheringEventCallback(mITetheringEventCallback, + TEST_CALLER_PKG); + verify(mTethering).unregisterTetheringEventCallback(eq(mITetheringEventCallback)); + } + + @Test + public void testUnregisterTetheringEventCallback() throws Exception { + runAsNoPermission((result) -> { + mTetheringConnector.unregisterTetheringEventCallback(mITetheringEventCallback, + TEST_CALLER_PKG); + verify(mITetheringEventCallback).onCallbackStopped( + TETHER_ERROR_NO_ACCESS_TETHERING_PERMISSION); + verifyNoMoreInteractionsForTethering(); + }); + + runAsTetherPrivileged((none) -> { + runUnregisterTetheringEventCallback(); + verifyNoMoreInteractionsForTethering(); + }); + + runAsAccessNetworkState((none) -> { + runUnregisterTetheringEventCallback(); + verifyNoMoreInteractionsForTethering(); + }); + } + + private void runStopAllTethering(final TestTetheringResult result) throws Exception { + mTetheringConnector.stopAllTethering(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result); + verify(mTethering).isTetheringSupported(); + verify(mTethering).untetherAll(); + result.assertResult(TETHER_ERROR_NO_ERROR); + } + + @Test + public void testStopAllTethering() throws Exception { + runAsNoPermission((result) -> { + mTetheringConnector.stopAllTethering(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result); + verify(mTethering).isTetherProvisioningRequired(); + result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION); + verifyNoMoreInteractionsForTethering(); + }); + + runAsTetherPrivileged((result) -> { + runStopAllTethering(result); + verifyNoMoreInteractionsForTethering(); + }); + + runAsWriteSettings((result) -> { + runStopAllTethering(result); + verify(mTethering).isTetherProvisioningRequired(); + verifyNoMoreInteractionsForTethering(); + }); + } + + private void runIsTetheringSupported(final TestTetheringResult result) throws Exception { + mTetheringConnector.isTetheringSupported(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, result); + verify(mTethering).isTetheringSupported(); + result.assertResult(TETHER_ERROR_NO_ERROR); + } + + @Test + public void testIsTetheringSupported() throws Exception { + runAsNoPermission((result) -> { + mTetheringConnector.isTetheringSupported(TEST_CALLER_PKG, TEST_ATTRIBUTION_TAG, + result); + verify(mTethering).isTetherProvisioningRequired(); + result.assertResult(TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION); + verifyNoMoreInteractionsForTethering(); + }); + + runAsTetherPrivileged((result) -> { + runIsTetheringSupported(result); + verifyNoMoreInteractionsForTethering(); + }); + + runAsWriteSettings((result) -> { + runIsTetheringSupported(result); + verify(mTethering).isTetherProvisioningRequired(); + verifyNoMoreInteractionsForTethering(); + }); + } +} diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java new file mode 100644 index 0000000000..2b158665cc --- /dev/null +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java @@ -0,0 +1,2572 @@ +/* + * 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.networkstack.tethering; + +import static android.Manifest.permission.NETWORK_SETTINGS; +import static android.content.pm.PackageManager.GET_ACTIVITIES; +import static android.hardware.usb.UsbManager.USB_CONFIGURED; +import static android.hardware.usb.UsbManager.USB_CONNECTED; +import static android.hardware.usb.UsbManager.USB_FUNCTION_NCM; +import static android.hardware.usb.UsbManager.USB_FUNCTION_RNDIS; +import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED; +import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; +import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED; +import static android.net.ConnectivityManager.TYPE_MOBILE_DUN; +import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.RouteInfo.RTN_UNICAST; +import static android.net.TetheringManager.ACTION_TETHER_STATE_CHANGED; +import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL; +import static android.net.TetheringManager.EXTRA_ACTIVE_LOCAL_ONLY; +import static android.net.TetheringManager.EXTRA_ACTIVE_TETHER; +import static android.net.TetheringManager.EXTRA_AVAILABLE_TETHER; +import static android.net.TetheringManager.TETHERING_BLUETOOTH; +import static android.net.TetheringManager.TETHERING_ETHERNET; +import static android.net.TetheringManager.TETHERING_NCM; +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_IFACE_CFG_ERROR; +import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR; +import static android.net.TetheringManager.TETHER_ERROR_UNKNOWN_IFACE; +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 android.net.dhcp.IDhcpServer.STATUS_SUCCESS; +import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME; +import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE; +import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE; +import static android.net.wifi.WifiManager.IFACE_IP_MODE_LOCAL_ONLY; +import static android.net.wifi.WifiManager.IFACE_IP_MODE_TETHERED; +import static android.net.wifi.WifiManager.WIFI_AP_STATE_ENABLED; +import static android.system.OsConstants.RT_SCOPE_UNIVERSE; +import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; + +import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH; +import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH; +import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_1_0; +import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_NONE; +import static com.android.networkstack.tethering.TestConnectivityManager.BROADCAST_FIRST; +import static com.android.networkstack.tethering.TestConnectivityManager.CALLBACKS_FIRST; +import static com.android.networkstack.tethering.Tethering.UserRestrictionActionListener; +import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE; +import static com.android.networkstack.tethering.UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES; +import static com.android.testutils.TestPermissionUtil.runAsShell; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.app.usage.NetworkStatsManager; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothPan; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothProfile.ServiceListener; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.hardware.usb.UsbManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.EthernetManager; +import android.net.EthernetManager.TetheredInterfaceCallback; +import android.net.EthernetManager.TetheredInterfaceRequest; +import android.net.IConnectivityManager; +import android.net.IIntResultListener; +import android.net.INetd; +import android.net.ITetheringEventCallback; +import android.net.InetAddresses; +import android.net.InterfaceConfigurationParcel; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.MacAddress; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.RouteInfo; +import android.net.TetherStatesParcel; +import android.net.TetheredClient; +import android.net.TetheredClient.AddressInfo; +import android.net.TetheringCallbackStartedParcel; +import android.net.TetheringConfigurationParcel; +import android.net.TetheringInterface; +import android.net.TetheringRequestParcel; +import android.net.dhcp.DhcpLeaseParcelable; +import android.net.dhcp.DhcpServerCallbacks; +import android.net.dhcp.DhcpServingParamsParcel; +import android.net.dhcp.IDhcpEventCallbacks; +import android.net.dhcp.IDhcpServer; +import android.net.ip.DadProxy; +import android.net.ip.IpNeighborMonitor; +import android.net.ip.IpServer; +import android.net.ip.RouterAdvertisementDaemon; +import android.net.util.InterfaceParams; +import android.net.util.NetworkConstants; +import android.net.util.SharedLog; +import android.net.wifi.SoftApConfiguration; +import android.net.wifi.WifiClient; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.SoftApCallback; +import android.net.wifi.p2p.WifiP2pGroup; +import android.net.wifi.p2p.WifiP2pInfo; +import android.net.wifi.p2p.WifiP2pManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.PersistableBundle; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.os.test.TestLooper; +import android.provider.Settings; +import android.telephony.CarrierConfigManager; +import android.telephony.PhoneStateListener; +import android.telephony.TelephonyManager; +import android.test.mock.MockContentResolver; + +import androidx.annotation.NonNull; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.StateMachine; +import com.android.internal.util.test.BroadcastInterceptingContext; +import com.android.internal.util.test.FakeSettingsProvider; +import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent; +import com.android.testutils.MiscAsserts; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Vector; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class TetheringTest { + private static final int IFINDEX_OFFSET = 100; + + private static final String TEST_MOBILE_IFNAME = "test_rmnet_data0"; + private static final String TEST_DUN_IFNAME = "test_dun0"; + private static final String TEST_XLAT_MOBILE_IFNAME = "v4-test_rmnet_data0"; + private static final String TEST_USB_IFNAME = "test_rndis0"; + private static final String TEST_WIFI_IFNAME = "test_wlan0"; + private static final String TEST_WLAN_IFNAME = "test_wlan1"; + private static final String TEST_P2P_IFNAME = "test_p2p-p2p0-0"; + private static final String TEST_NCM_IFNAME = "test_ncm0"; + private static final String TEST_ETH_IFNAME = "test_eth0"; + private static final String TEST_BT_IFNAME = "test_pan0"; + private static final String TETHERING_NAME = "Tethering"; + private static final String[] PROVISIONING_APP_NAME = {"some", "app"}; + private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app"; + + private static final int CELLULAR_NETID = 100; + private static final int WIFI_NETID = 101; + private static final int DUN_NETID = 102; + + private static final int DHCPSERVER_START_TIMEOUT_MS = 1000; + + @Mock private ApplicationInfo mApplicationInfo; + @Mock private Context mContext; + @Mock private NetworkStatsManager mStatsManager; + @Mock private OffloadHardwareInterface mOffloadHardwareInterface; + @Mock private OffloadHardwareInterface.ForwardedStats mForwardedStats; + @Mock private Resources mResources; + @Mock private TelephonyManager mTelephonyManager; + @Mock private UsbManager mUsbManager; + @Mock private WifiManager mWifiManager; + @Mock private CarrierConfigManager mCarrierConfigManager; + @Mock private IPv6TetheringCoordinator mIPv6TetheringCoordinator; + @Mock private DadProxy mDadProxy; + @Mock private RouterAdvertisementDaemon mRouterAdvertisementDaemon; + @Mock private IpNeighborMonitor mIpNeighborMonitor; + @Mock private IDhcpServer mDhcpServer; + @Mock private INetd mNetd; + @Mock private UserManager mUserManager; + @Mock private EthernetManager mEm; + @Mock private TetheringNotificationUpdater mNotificationUpdater; + @Mock private BpfCoordinator mBpfCoordinator; + @Mock private PackageManager mPackageManager; + @Mock private BluetoothAdapter mBluetoothAdapter; + @Mock private BluetoothPan mBluetoothPan; + + private final MockIpServerDependencies mIpServerDependencies = + spy(new MockIpServerDependencies()); + private final MockTetheringDependencies mTetheringDependencies = + new MockTetheringDependencies(); + + // Like so many Android system APIs, these cannot be mocked because it is marked final. + // We have to use the real versions. + private final PersistableBundle mCarrierConfig = new PersistableBundle(); + private final TestLooper mLooper = new TestLooper(); + + private Vector mIntents; + private BroadcastInterceptingContext mServiceContext; + private MockContentResolver mContentResolver; + private BroadcastReceiver mBroadcastReceiver; + private Tethering mTethering; + private PhoneStateListener mPhoneStateListener; + private InterfaceConfigurationParcel mInterfaceConfiguration; + private TetheringConfiguration mConfig; + private EntitlementManager mEntitleMgr; + private OffloadController mOffloadCtrl; + private PrivateAddressCoordinator mPrivateAddressCoordinator; + private SoftApCallback mSoftApCallback; + private UpstreamNetworkMonitor mUpstreamNetworkMonitor; + + private TestConnectivityManager mCm; + + private class TestContext extends BroadcastInterceptingContext { + TestContext(Context base) { + super(base); + } + + @Override + public ApplicationInfo getApplicationInfo() { + return mApplicationInfo; + } + + @Override + public ContentResolver getContentResolver() { + return mContentResolver; + } + + @Override + public String getPackageName() { + return "TetheringTest"; + } + + @Override + public Resources getResources() { + return mResources; + } + + @Override + public Object getSystemService(String name) { + if (Context.WIFI_SERVICE.equals(name)) return mWifiManager; + if (Context.USB_SERVICE.equals(name)) return mUsbManager; + if (Context.TELEPHONY_SERVICE.equals(name)) return mTelephonyManager; + if (Context.USER_SERVICE.equals(name)) return mUserManager; + if (Context.NETWORK_STATS_SERVICE.equals(name)) return mStatsManager; + if (Context.CONNECTIVITY_SERVICE.equals(name)) return mCm; + if (Context.ETHERNET_SERVICE.equals(name)) return mEm; + return super.getSystemService(name); + } + + @Override + public PackageManager getPackageManager() { + return mPackageManager; + } + + @Override + public String getSystemServiceName(Class serviceClass) { + if (TelephonyManager.class.equals(serviceClass)) return Context.TELEPHONY_SERVICE; + return super.getSystemServiceName(serviceClass); + } + } + + public class MockIpServerDependencies extends IpServer.Dependencies { + @Override + public DadProxy getDadProxy( + Handler handler, InterfaceParams ifParams) { + return mDadProxy; + } + + @Override + public RouterAdvertisementDaemon getRouterAdvertisementDaemon( + InterfaceParams ifParams) { + return mRouterAdvertisementDaemon; + } + + @Override + public InterfaceParams getInterfaceParams(String ifName) { + assertTrue("Non-mocked interface " + ifName, + ifName.equals(TEST_USB_IFNAME) + || ifName.equals(TEST_WLAN_IFNAME) + || ifName.equals(TEST_WIFI_IFNAME) + || ifName.equals(TEST_MOBILE_IFNAME) + || ifName.equals(TEST_DUN_IFNAME) + || ifName.equals(TEST_P2P_IFNAME) + || ifName.equals(TEST_NCM_IFNAME) + || ifName.equals(TEST_ETH_IFNAME) + || ifName.equals(TEST_BT_IFNAME)); + final String[] ifaces = new String[] { + TEST_USB_IFNAME, TEST_WLAN_IFNAME, TEST_WIFI_IFNAME, TEST_MOBILE_IFNAME, + TEST_DUN_IFNAME, TEST_P2P_IFNAME, TEST_NCM_IFNAME, TEST_ETH_IFNAME}; + return new InterfaceParams(ifName, ArrayUtils.indexOf(ifaces, ifName) + IFINDEX_OFFSET, + MacAddress.ALL_ZEROS_ADDRESS); + } + + @Override + public void makeDhcpServer(String ifName, DhcpServingParamsParcel params, + DhcpServerCallbacks cb) { + new Thread(() -> { + try { + cb.onDhcpServerCreated(STATUS_SUCCESS, mDhcpServer); + } catch (RemoteException e) { + fail(e.getMessage()); + } + }).run(); + } + + public IpNeighborMonitor getIpNeighborMonitor(Handler h, SharedLog l, + IpNeighborMonitor.NeighborEventConsumer c) { + return mIpNeighborMonitor; + } + } + + // MyTetheringConfiguration is used to override static method for testing. + private class MyTetheringConfiguration extends TetheringConfiguration { + MyTetheringConfiguration(Context ctx, SharedLog log, int id) { + super(ctx, log, id); + } + + @Override + protected String getDeviceConfigProperty(final String name) { + return null; + } + + @Override + protected boolean isFeatureEnabled(Context ctx, String featureVersionFlag) { + return false; + } + + @Override + protected Resources getResourcesForSubIdWrapper(Context ctx, int subId) { + return mResources; + } + } + + public class MockTetheringDependencies extends TetheringDependencies { + StateMachine mUpstreamNetworkMonitorSM; + ArrayList mIpv6CoordinatorNotifyList; + + public void reset() { + mUpstreamNetworkMonitorSM = null; + mIpv6CoordinatorNotifyList = null; + } + + @Override + public BpfCoordinator getBpfCoordinator( + BpfCoordinator.Dependencies deps) { + return mBpfCoordinator; + } + + @Override + public OffloadHardwareInterface getOffloadHardwareInterface(Handler h, SharedLog log) { + return mOffloadHardwareInterface; + } + + @Override + public OffloadController getOffloadController(Handler h, SharedLog log, + OffloadController.Dependencies deps) { + mOffloadCtrl = spy(super.getOffloadController(h, log, deps)); + // Return real object here instead of mock because + // testReportFailCallbackIfOffloadNotSupported depend on real OffloadController object. + return mOffloadCtrl; + } + + @Override + public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx, + StateMachine target, SharedLog log, int what) { + // Use a real object instead of a mock so that some tests can use a real UNM and some + // can use a mock. + mUpstreamNetworkMonitorSM = target; + mUpstreamNetworkMonitor = spy(super.getUpstreamNetworkMonitor(ctx, target, log, what)); + return mUpstreamNetworkMonitor; + } + + @Override + public IPv6TetheringCoordinator getIPv6TetheringCoordinator( + ArrayList notifyList, SharedLog log) { + mIpv6CoordinatorNotifyList = notifyList; + return mIPv6TetheringCoordinator; + } + + @Override + public IpServer.Dependencies getIpServerDependencies() { + return mIpServerDependencies; + } + + @Override + public EntitlementManager getEntitlementManager(Context ctx, Handler h, SharedLog log, + Runnable callback) { + mEntitleMgr = spy(super.getEntitlementManager(ctx, h, log, callback)); + return mEntitleMgr; + } + + @Override + public boolean isTetheringSupported() { + return true; + } + + @Override + public TetheringConfiguration generateTetheringConfiguration(Context ctx, SharedLog log, + int subId) { + mConfig = spy(new MyTetheringConfiguration(ctx, log, subId)); + return mConfig; + } + + @Override + public INetd getINetd(Context context) { + return mNetd; + } + + @Override + public Looper getTetheringLooper() { + return mLooper.getLooper(); + } + + @Override + public Context getContext() { + return mServiceContext; + } + + @Override + public BluetoothAdapter getBluetoothAdapter() { + return mBluetoothAdapter; + } + + @Override + public TetheringNotificationUpdater getNotificationUpdater(Context ctx, Looper looper) { + return mNotificationUpdater; + } + + @Override + public boolean isTetheringDenied() { + return false; + } + + + @Override + public PrivateAddressCoordinator getPrivateAddressCoordinator(Context ctx, + TetheringConfiguration cfg) { + mPrivateAddressCoordinator = super.getPrivateAddressCoordinator(ctx, cfg); + return mPrivateAddressCoordinator; + } + } + + private static LinkProperties buildUpstreamLinkProperties(String interfaceName, + boolean withIPv4, boolean withIPv6, boolean with464xlat) { + final LinkProperties prop = new LinkProperties(); + prop.setInterfaceName(interfaceName); + + if (withIPv4) { + prop.addLinkAddress(new LinkAddress("10.1.2.3/15")); + prop.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), + InetAddresses.parseNumericAddress("10.0.0.1"), + interfaceName, RTN_UNICAST)); + } + + if (withIPv6) { + prop.addDnsServer(InetAddresses.parseNumericAddress("2001:db8::2")); + prop.addLinkAddress( + new LinkAddress(InetAddresses.parseNumericAddress("2001:db8::"), + NetworkConstants.RFC7421_PREFIX_LENGTH)); + prop.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), + InetAddresses.parseNumericAddress("2001:db8::1"), + interfaceName, RTN_UNICAST)); + } + + if (with464xlat) { + final String clatInterface = "v4-" + interfaceName; + final LinkProperties stackedLink = new LinkProperties(); + stackedLink.setInterfaceName(clatInterface); + stackedLink.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), + InetAddresses.parseNumericAddress("192.0.0.1"), + clatInterface, RTN_UNICAST)); + + prop.addStackedLink(stackedLink); + } + + return prop; + } + + private static NetworkCapabilities buildUpstreamCapabilities(int transport, int... otherCaps) { + // TODO: add NOT_VCN_MANAGED. + final NetworkCapabilities nc = new NetworkCapabilities() + .addTransportType(transport); + for (int cap : otherCaps) { + nc.addCapability(cap); + } + return nc; + } + + private static UpstreamNetworkState buildMobileUpstreamState(boolean withIPv4, + boolean withIPv6, boolean with464xlat) { + return new UpstreamNetworkState( + buildUpstreamLinkProperties(TEST_MOBILE_IFNAME, withIPv4, withIPv6, with464xlat), + buildUpstreamCapabilities(TRANSPORT_CELLULAR, NET_CAPABILITY_INTERNET), + new Network(CELLULAR_NETID)); + } + + private static UpstreamNetworkState buildMobileIPv4UpstreamState() { + return buildMobileUpstreamState(true, false, false); + } + + private static UpstreamNetworkState buildMobileIPv6UpstreamState() { + return buildMobileUpstreamState(false, true, false); + } + + private static UpstreamNetworkState buildMobileDualStackUpstreamState() { + return buildMobileUpstreamState(true, true, false); + } + + private static UpstreamNetworkState buildMobile464xlatUpstreamState() { + return buildMobileUpstreamState(false, true, true); + } + + private static UpstreamNetworkState buildWifiUpstreamState() { + return new UpstreamNetworkState( + buildUpstreamLinkProperties(TEST_WIFI_IFNAME, true /* IPv4 */, true /* IPv6 */, + false /* 464xlat */), + buildUpstreamCapabilities(TRANSPORT_WIFI, NET_CAPABILITY_INTERNET), + new Network(WIFI_NETID)); + } + + private static UpstreamNetworkState buildDunUpstreamState() { + return new UpstreamNetworkState( + buildUpstreamLinkProperties(TEST_DUN_IFNAME, true /* IPv4 */, true /* IPv6 */, + false /* 464xlat */), + buildUpstreamCapabilities(TRANSPORT_CELLULAR, NET_CAPABILITY_DUN), + new Network(DUN_NETID)); + } + + // See FakeSettingsProvider#clearSettingsProvider() that this needs to be called before and + // after use. + @BeforeClass + public static void setupOnce() { + FakeSettingsProvider.clearSettingsProvider(); + } + + @AfterClass + public static void tearDownOnce() { + FakeSettingsProvider.clearSettingsProvider(); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + when(mResources.getStringArray(R.array.config_tether_dhcp_range)) + .thenReturn(new String[0]); + when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn( + false); + when(mNetd.interfaceGetList()) + .thenReturn(new String[] { + TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_USB_IFNAME, TEST_P2P_IFNAME, + TEST_NCM_IFNAME, TEST_ETH_IFNAME, TEST_BT_IFNAME}); + when(mResources.getString(R.string.config_wifi_tether_enable)).thenReturn(""); + mInterfaceConfiguration = new InterfaceConfigurationParcel(); + mInterfaceConfiguration.flags = new String[0]; + when(mRouterAdvertisementDaemon.start()) + .thenReturn(true); + initOffloadConfiguration(true /* offloadConfig */, OFFLOAD_HAL_VERSION_1_0, + 0 /* defaultDisabled */); + when(mOffloadHardwareInterface.getForwardedStats(any())).thenReturn(mForwardedStats); + + mServiceContext = new TestContext(mContext); + mServiceContext.setUseRegisteredHandlers(true); + mContentResolver = new MockContentResolver(mServiceContext); + mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); + setTetheringSupported(true /* supported */); + mIntents = new Vector<>(); + mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + mIntents.addElement(intent); + } + }; + mServiceContext.registerReceiver(mBroadcastReceiver, + new IntentFilter(ACTION_TETHER_STATE_CHANGED)); + + mCm = spy(new TestConnectivityManager(mServiceContext, mock(IConnectivityManager.class))); + + mTethering = makeTethering(); + verify(mStatsManager, times(1)).registerNetworkStatsProvider(anyString(), any()); + verify(mNetd).registerUnsolicitedEventListener(any()); + verifyDefaultNetworkRequestFiled(); + + final ArgumentCaptor phoneListenerCaptor = + ArgumentCaptor.forClass(PhoneStateListener.class); + verify(mTelephonyManager).listen(phoneListenerCaptor.capture(), + eq(PhoneStateListener.LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE)); + mPhoneStateListener = phoneListenerCaptor.getValue(); + + final ArgumentCaptor softApCallbackCaptor = + ArgumentCaptor.forClass(SoftApCallback.class); + verify(mWifiManager).registerSoftApCallback(any(), softApCallbackCaptor.capture()); + mSoftApCallback = softApCallbackCaptor.getValue(); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true); + } + + private void setTetheringSupported(final boolean supported) { + Settings.Global.putInt(mContentResolver, Settings.Global.TETHER_SUPPORTED, + supported ? 1 : 0); + when(mUserManager.hasUserRestriction( + UserManager.DISALLOW_CONFIG_TETHERING)).thenReturn(!supported); + // Setup tetherable configuration. + when(mResources.getStringArray(R.array.config_tether_usb_regexs)) + .thenReturn(new String[] { "test_rndis\\d" }); + when(mResources.getStringArray(R.array.config_tether_wifi_regexs)) + .thenReturn(new String[] { "test_wlan\\d" }); + when(mResources.getStringArray(R.array.config_tether_wifi_p2p_regexs)) + .thenReturn(new String[] { "test_p2p-p2p\\d-.*" }); + when(mResources.getStringArray(R.array.config_tether_bluetooth_regexs)) + .thenReturn(new String[] { "test_pan\\d" }); + when(mResources.getStringArray(R.array.config_tether_ncm_regexs)) + .thenReturn(new String[] { "test_ncm\\d" }); + when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn( + new int[] { TYPE_WIFI, TYPE_MOBILE_DUN }); + when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(true); + } + + private void initTetheringUpstream(UpstreamNetworkState upstreamState) { + doReturn(upstreamState).when(mUpstreamNetworkMonitor).getCurrentPreferredUpstream(); + doReturn(upstreamState).when(mUpstreamNetworkMonitor).selectPreferredUpstreamType(any()); + } + + private Tethering makeTethering() { + mTetheringDependencies.reset(); + return new Tethering(mTetheringDependencies); + } + + private TetheringRequestParcel createTetheringRequestParcel(final int type) { + return createTetheringRequestParcel(type, null, null, false, CONNECTIVITY_SCOPE_GLOBAL); + } + + private TetheringRequestParcel createTetheringRequestParcel(final int type, + final LinkAddress serverAddr, final LinkAddress clientAddr, final boolean exempt, + final int scope) { + final TetheringRequestParcel request = new TetheringRequestParcel(); + request.tetheringType = type; + request.localIPv4Address = serverAddr; + request.staticClientAddress = clientAddr; + request.exemptFromEntitlementCheck = exempt; + request.showProvisioningUi = false; + request.connectivityScope = scope; + + return request; + } + + @After + public void tearDown() { + mServiceContext.unregisterReceiver(mBroadcastReceiver); + } + + private void sendWifiApStateChanged(int state) { + final Intent intent = new Intent(WifiManager.WIFI_AP_STATE_CHANGED_ACTION); + intent.putExtra(EXTRA_WIFI_AP_STATE, state); + mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + mLooper.dispatchAll(); + } + + private void sendWifiApStateChanged(int state, String ifname, int ipmode) { + final Intent intent = new Intent(WifiManager.WIFI_AP_STATE_CHANGED_ACTION); + intent.putExtra(EXTRA_WIFI_AP_STATE, state); + intent.putExtra(EXTRA_WIFI_AP_INTERFACE_NAME, ifname); + intent.putExtra(EXTRA_WIFI_AP_MODE, ipmode); + mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + mLooper.dispatchAll(); + } + + private static final String[] P2P_RECEIVER_PERMISSIONS_FOR_BROADCAST = { + android.Manifest.permission.ACCESS_FINE_LOCATION, + android.Manifest.permission.ACCESS_WIFI_STATE + }; + + private void sendWifiP2pConnectionChanged( + boolean isGroupFormed, boolean isGroupOwner, String ifname) { + WifiP2pGroup group = null; + WifiP2pInfo p2pInfo = new WifiP2pInfo(); + p2pInfo.groupFormed = isGroupFormed; + if (isGroupFormed) { + p2pInfo.isGroupOwner = isGroupOwner; + group = mock(WifiP2pGroup.class); + when(group.isGroupOwner()).thenReturn(isGroupOwner); + when(group.getInterface()).thenReturn(ifname); + } + + final Intent intent = mock(Intent.class); + when(intent.getAction()).thenReturn(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION); + when(intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO)).thenReturn(p2pInfo); + when(intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP)).thenReturn(group); + + mServiceContext.sendBroadcastAsUserMultiplePermissions(intent, UserHandle.ALL, + P2P_RECEIVER_PERMISSIONS_FOR_BROADCAST); + mLooper.dispatchAll(); + } + + private void sendUsbBroadcast(boolean connected, boolean configured, boolean function, + int type) { + final Intent intent = new Intent(UsbManager.ACTION_USB_STATE); + intent.putExtra(USB_CONNECTED, connected); + intent.putExtra(USB_CONFIGURED, configured); + if (type == TETHERING_USB) { + intent.putExtra(USB_FUNCTION_RNDIS, function); + } else { + intent.putExtra(USB_FUNCTION_NCM, function); + } + mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + mLooper.dispatchAll(); + } + + private void sendConfigurationChanged() { + final Intent intent = new Intent(Intent.ACTION_CONFIGURATION_CHANGED); + mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + mLooper.dispatchAll(); + } + + private void verifyDefaultNetworkRequestFiled() { + ArgumentCaptor reqCaptor = ArgumentCaptor.forClass(NetworkRequest.class); + verify(mCm, times(1)).requestNetwork(reqCaptor.capture(), + any(NetworkCallback.class), any(Handler.class)); + assertTrue(TestConnectivityManager.looksLikeDefaultRequest(reqCaptor.getValue())); + // The default network request is only ever filed once. + verifyNoMoreInteractions(mCm); + mUpstreamNetworkMonitor.startTrackDefaultNetwork(mEntitleMgr); + verifyNoMoreInteractions(mCm); + } + + private void verifyInterfaceServingModeStarted(String ifname) throws Exception { + verify(mNetd, times(1)).interfaceSetCfg(any(InterfaceConfigurationParcel.class)); + verify(mNetd, times(1)).tetherInterfaceAdd(ifname); + verify(mNetd, times(1)).networkAddInterface(INetd.LOCAL_NET_ID, ifname); + verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(ifname), + anyString(), anyString()); + } + + private void verifyTetheringBroadcast(String ifname, String whichExtra) { + // Verify that ifname is in the whichExtra array of the tether state changed broadcast. + final Intent bcast = mIntents.get(0); + assertEquals(ACTION_TETHER_STATE_CHANGED, bcast.getAction()); + final ArrayList ifnames = bcast.getStringArrayListExtra(whichExtra); + assertTrue(ifnames.contains(ifname)); + mIntents.remove(bcast); + } + + public void failingLocalOnlyHotspotLegacyApBroadcast( + boolean emulateInterfaceStatusChanged) throws Exception { + // Emulate externally-visible WifiManager effects, causing the + // per-interface state machine to start up, and telling us that + // hotspot mode is to be started. + if (emulateInterfaceStatusChanged) { + mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true); + } + sendWifiApStateChanged(WIFI_AP_STATE_ENABLED); + + // If, and only if, Tethering received an interface status changed then + // it creates a IpServer and sends out a broadcast indicating that the + // interface is "available". + if (emulateInterfaceStatusChanged) { + // There is 1 IpServer state change event: STATE_AVAILABLE + verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE); + verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER); + verify(mWifiManager).updateInterfaceIpState( + TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED); + } + verifyNoMoreInteractions(mNetd); + verifyNoMoreInteractions(mWifiManager); + } + + private void prepareNcmTethering() { + // Emulate startTethering(TETHERING_NCM) called + mTethering.startTethering(createTetheringRequestParcel(TETHERING_NCM), null); + mLooper.dispatchAll(); + verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NCM); + + mTethering.interfaceStatusChanged(TEST_NCM_IFNAME, true); + } + + private void prepareUsbTethering() { + // Emulate pressing the USB tethering button in Settings UI. + final TetheringRequestParcel request = createTetheringRequestParcel(TETHERING_USB); + mTethering.startTethering(request, null); + mLooper.dispatchAll(); + verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS); + assertEquals(1, mTethering.getActiveTetheringRequests().size()); + assertEquals(request, mTethering.getActiveTetheringRequests().get(TETHERING_USB)); + + mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true); + } + + @Test + public void testUsbConfiguredBroadcastStartsTethering() throws Exception { + UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState(); + initTetheringUpstream(upstreamState); + prepareUsbTethering(); + + // This should produce no activity of any kind. + verifyNoMoreInteractions(mNetd); + + // Pretend we then receive USB configured broadcast. + sendUsbBroadcast(true, true, true, TETHERING_USB); + // Now we should see the start of tethering mechanics (in this case: + // tetherMatchingInterfaces() which starts by fetching all interfaces). + verify(mNetd, times(1)).interfaceGetList(); + + // UpstreamNetworkMonitor should receive selected upstream + verify(mUpstreamNetworkMonitor, times(1)).getCurrentPreferredUpstream(); + verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(upstreamState.network); + } + + @Test + public void failingLocalOnlyHotspotLegacyApBroadcastWithIfaceStatusChanged() throws Exception { + failingLocalOnlyHotspotLegacyApBroadcast(true); + } + + @Test + public void failingLocalOnlyHotspotLegacyApBroadcastSansIfaceStatusChanged() throws Exception { + failingLocalOnlyHotspotLegacyApBroadcast(false); + } + + public void workingLocalOnlyHotspotEnrichedApBroadcast( + boolean emulateInterfaceStatusChanged) throws Exception { + // Emulate externally-visible WifiManager effects, causing the + // per-interface state machine to start up, and telling us that + // hotspot mode is to be started. + if (emulateInterfaceStatusChanged) { + mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true); + } + sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_LOCAL_ONLY); + + verifyInterfaceServingModeStarted(TEST_WLAN_IFNAME); + verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER); + verify(mNetd, times(1)).ipfwdEnableForwarding(TETHERING_NAME); + verify(mNetd, times(1)).tetherStartWithConfiguration(any()); + verifyNoMoreInteractions(mNetd); + verify(mWifiManager).updateInterfaceIpState( + TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED); + verify(mWifiManager).updateInterfaceIpState( + TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_LOCAL_ONLY); + verifyNoMoreInteractions(mWifiManager); + verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY); + verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks(); + // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY + verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE); + + // Emulate externally-visible WifiManager effects, when hotspot mode + // is being torn down. + sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED); + mTethering.interfaceRemoved(TEST_WLAN_IFNAME); + mLooper.dispatchAll(); + + verify(mNetd, times(1)).tetherApplyDnsInterfaces(); + verify(mNetd, times(1)).tetherInterfaceRemove(TEST_WLAN_IFNAME); + verify(mNetd, times(1)).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME); + // interfaceSetCfg() called once for enabling and twice disabling IPv4. + verify(mNetd, times(3)).interfaceSetCfg(any(InterfaceConfigurationParcel.class)); + verify(mNetd, times(1)).tetherStop(); + verify(mNetd, times(1)).ipfwdDisableForwarding(TETHERING_NAME); + verify(mWifiManager, times(3)).updateInterfaceIpState( + TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED); + verifyNoMoreInteractions(mNetd); + verifyNoMoreInteractions(mWifiManager); + // Asking for the last error after the per-interface state machine + // has been reaped yields an unknown interface error. + assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastErrorForTest(TEST_WLAN_IFNAME)); + } + + /** + * Send CMD_IPV6_TETHER_UPDATE to IpServers as would be done by IPv6TetheringCoordinator. + */ + private void sendIPv6TetherUpdates(UpstreamNetworkState upstreamState) { + // IPv6TetheringCoordinator must have been notified of downstream + verify(mIPv6TetheringCoordinator, times(1)).addActiveDownstream( + argThat(sm -> sm.linkProperties().getInterfaceName().equals(TEST_USB_IFNAME)), + eq(IpServer.STATE_TETHERED)); + + for (IpServer ipSrv : mTetheringDependencies.mIpv6CoordinatorNotifyList) { + UpstreamNetworkState ipv6OnlyState = buildMobileUpstreamState(false, true, false); + ipSrv.sendMessage(IpServer.CMD_IPV6_TETHER_UPDATE, 0, 0, + upstreamState.linkProperties.isIpv6Provisioned() + ? ipv6OnlyState.linkProperties + : null); + } + mLooper.dispatchAll(); + } + + private void runUsbTethering(UpstreamNetworkState upstreamState) { + initTetheringUpstream(upstreamState); + prepareUsbTethering(); + sendUsbBroadcast(true, true, true, TETHERING_USB); + } + + private void assertSetIfaceToDadProxy(final int numOfCalls, final String ifaceName) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R || "S".equals(Build.VERSION.CODENAME) + || "T".equals(Build.VERSION.CODENAME)) { + verify(mDadProxy, times(numOfCalls)).setUpstreamIface( + argThat(ifaceParams -> ifaceName.equals(ifaceParams.name))); + } + } + + @Test + public void workingMobileUsbTethering_IPv4() throws Exception { + UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState(); + runUsbTethering(upstreamState); + + verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); + verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); + + sendIPv6TetherUpdates(upstreamState); + assertSetIfaceToDadProxy(0 /* numOfCalls */, "" /* ifaceName */); + verify(mRouterAdvertisementDaemon, never()).buildNewRa(any(), notNull()); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); + } + + @Test + public void workingMobileUsbTethering_IPv4LegacyDhcp() { + when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn( + true); + sendConfigurationChanged(); + final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState(); + runUsbTethering(upstreamState); + sendIPv6TetherUpdates(upstreamState); + + verify(mIpServerDependencies, never()).makeDhcpServer(any(), any(), any()); + } + + @Test + public void workingMobileUsbTethering_IPv6() throws Exception { + UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState(); + runUsbTethering(upstreamState); + + verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); + verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); + + sendIPv6TetherUpdates(upstreamState); + // TODO: add interfaceParams to compare in verify. + assertSetIfaceToDadProxy(1 /* numOfCalls */, TEST_MOBILE_IFNAME /* ifaceName */); + verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull()); + verify(mNetd, times(1)).tetherApplyDnsInterfaces(); + } + + @Test + public void workingMobileUsbTethering_DualStack() throws Exception { + UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState(); + runUsbTethering(upstreamState); + + verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); + verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); + verify(mRouterAdvertisementDaemon, times(1)).start(); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); + + sendIPv6TetherUpdates(upstreamState); + assertSetIfaceToDadProxy(1 /* numOfCalls */, TEST_MOBILE_IFNAME /* ifaceName */); + verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull()); + verify(mNetd, times(1)).tetherApplyDnsInterfaces(); + } + + @Test + public void workingMobileUsbTethering_MultipleUpstreams() throws Exception { + UpstreamNetworkState upstreamState = buildMobile464xlatUpstreamState(); + runUsbTethering(upstreamState); + + verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME); + verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); + verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME); + verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); + + sendIPv6TetherUpdates(upstreamState); + assertSetIfaceToDadProxy(1 /* numOfCalls */, TEST_MOBILE_IFNAME /* ifaceName */); + verify(mRouterAdvertisementDaemon, times(1)).buildNewRa(any(), notNull()); + verify(mNetd, times(1)).tetherApplyDnsInterfaces(); + } + + @Test + public void workingMobileUsbTethering_v6Then464xlat() throws Exception { + // Setup IPv6 + UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState(); + runUsbTethering(upstreamState); + + verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); + verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); + + // Then 464xlat comes up + upstreamState = buildMobile464xlatUpstreamState(); + initTetheringUpstream(upstreamState); + + // Upstream LinkProperties changed: UpstreamNetworkMonitor sends EVENT_ON_LINKPROPERTIES. + mTetheringDependencies.mUpstreamNetworkMonitorSM.sendMessage( + Tethering.TetherMainSM.EVENT_UPSTREAM_CALLBACK, + UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES, + 0, + upstreamState); + mLooper.dispatchAll(); + + // Forwarding is added for 464xlat + verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME); + verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_XLAT_MOBILE_IFNAME); + // Forwarding was not re-added for v6 (still times(1)) + verify(mNetd, times(1)).tetherAddForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); + verify(mNetd, times(1)).ipfwdAddInterfaceForward(TEST_USB_IFNAME, TEST_MOBILE_IFNAME); + // DHCP not restarted on downstream (still times(1)) + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); + } + + @Test + public void configTetherUpstreamAutomaticIgnoresConfigTetherUpstreamTypes() throws Exception { + when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(true); + sendConfigurationChanged(); + + // Setup IPv6 + final UpstreamNetworkState upstreamState = buildMobileIPv6UpstreamState(); + runUsbTethering(upstreamState); + + // UpstreamNetworkMonitor should choose upstream automatically + // (in this specific case: choose the default network). + verify(mUpstreamNetworkMonitor, times(1)).getCurrentPreferredUpstream(); + verify(mUpstreamNetworkMonitor, never()).selectPreferredUpstreamType(any()); + + verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(upstreamState.network); + } + + private void upstreamSelectionTestCommon(final boolean automatic, InOrder inOrder, + TestNetworkAgent mobile, TestNetworkAgent wifi) throws Exception { + // Enable automatic upstream selection. + when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(automatic); + sendConfigurationChanged(); + mLooper.dispatchAll(); + + // Start USB tethering with no current upstream. + prepareUsbTethering(); + sendUsbBroadcast(true, true, true, TETHERING_USB); + inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks(); + inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true); + + // Pretend cellular connected and expect the upstream to be set. + mobile.fakeConnect(); + mCm.makeDefaultNetwork(mobile, BROADCAST_FIRST); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId); + + // Switch upstream to wifi. + wifi.fakeConnect(); + mCm.makeDefaultNetwork(wifi, BROADCAST_FIRST); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId); + } + + @Test + public void testAutomaticUpstreamSelection() throws Exception { + TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState()); + TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState()); + InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor); + // Enable automatic upstream selection. + upstreamSelectionTestCommon(true, inOrder, mobile, wifi); + + // This code has historically been racy, so test different orderings of CONNECTIVITY_ACTION + // broadcasts and callbacks, and add mLooper.dispatchAll() calls between the two. + final Runnable doDispatchAll = () -> mLooper.dispatchAll(); + + // Switch upstreams a few times. + mCm.makeDefaultNetwork(mobile, BROADCAST_FIRST, doDispatchAll); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId); + + mCm.makeDefaultNetwork(wifi, BROADCAST_FIRST, doDispatchAll); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId); + + mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId); + + mCm.makeDefaultNetwork(wifi, CALLBACKS_FIRST); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId); + + mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST, doDispatchAll); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId); + + // Wifi disconnecting should not have any affect since it's not the current upstream. + wifi.fakeDisconnect(); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor, never()).setCurrentUpstream(any()); + + // Lose and regain upstream. + assertTrue(mUpstreamNetworkMonitor.getCurrentPreferredUpstream().linkProperties + .hasIPv4Address()); + mCm.makeDefaultNetwork(null, BROADCAST_FIRST, doDispatchAll); + mLooper.dispatchAll(); + mobile.fakeDisconnect(); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null); + + mobile = new TestNetworkAgent(mCm, buildMobile464xlatUpstreamState()); + mobile.fakeConnect(); + mCm.makeDefaultNetwork(mobile, BROADCAST_FIRST, doDispatchAll); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId); + + // Check the IP addresses to ensure that the upstream is indeed not the same as the previous + // mobile upstream, even though the netId is (unrealistically) the same. + assertFalse(mUpstreamNetworkMonitor.getCurrentPreferredUpstream().linkProperties + .hasIPv4Address()); + + // Lose and regain upstream again. + mCm.makeDefaultNetwork(null, CALLBACKS_FIRST, doDispatchAll); + mobile.fakeDisconnect(); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null); + + mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState()); + mobile.fakeConnect(); + mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST, doDispatchAll); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId); + + assertTrue(mUpstreamNetworkMonitor.getCurrentPreferredUpstream().linkProperties + .hasIPv4Address()); + + // Check that the code does not crash if onLinkPropertiesChanged is received after onLost. + mobile.fakeDisconnect(); + mobile.sendLinkProperties(); + mLooper.dispatchAll(); + } + + @Test + public void testLegacyUpstreamSelection() throws Exception { + TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState()); + TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState()); + InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor); + // Enable legacy upstream selection. + upstreamSelectionTestCommon(false, inOrder, mobile, wifi); + + // Wifi disconnecting and the default network switch to mobile, the upstream should also + // switch to mobile. + wifi.fakeDisconnect(); + mLooper.dispatchAll(); + mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST, null); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId); + + wifi.fakeConnect(); + mLooper.dispatchAll(); + mCm.makeDefaultNetwork(wifi, CALLBACKS_FIRST, null); + mLooper.dispatchAll(); + } + + @Test + public void testChooseDunUpstreamByAutomaticMode() throws Exception { + // Enable automatic upstream selection. + TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState()); + TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState()); + TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState()); + InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor); + chooseDunUpstreamTestCommon(true, inOrder, mobile, wifi, dun); + + // When default network switch to mobile and wifi is connected (may have low signal), + // automatic mode would request dun again and choose it as upstream. + mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST); + mLooper.dispatchAll(); + ArgumentCaptor captor = ArgumentCaptor.forClass(NetworkCallback.class); + inOrder.verify(mCm).requestNetwork(any(), eq(0), eq(TYPE_MOBILE_DUN), any(), any()); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null); + final Runnable doDispatchAll = () -> mLooper.dispatchAll(); + dun.fakeConnect(CALLBACKS_FIRST, doDispatchAll); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(dun.networkId); + + // Lose and regain upstream again. + dun.fakeDisconnect(CALLBACKS_FIRST, doDispatchAll); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null); + inOrder.verify(mCm, never()).unregisterNetworkCallback(any(NetworkCallback.class)); + dun.fakeConnect(CALLBACKS_FIRST, doDispatchAll); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(dun.networkId); + } + + @Test + public void testChooseDunUpstreamByLegacyMode() throws Exception { + // Enable Legacy upstream selection. + TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState()); + TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState()); + TestNetworkAgent dun = new TestNetworkAgent(mCm, buildDunUpstreamState()); + InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor); + chooseDunUpstreamTestCommon(false, inOrder, mobile, wifi, dun); + + // Legacy mode would keep use wifi as upstream (because it has higher priority in the + // list). + mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor, never()).setCurrentUpstream(any()); + // BUG: when wifi disconnect, the dun request would not be filed again because wifi is + // no longer be default network which do not have CONNECTIVIY_ACTION broadcast. + wifi.fakeDisconnect(); + mLooper.dispatchAll(); + inOrder.verify(mCm, never()).requestNetwork(any(), eq(0), eq(TYPE_MOBILE_DUN), any(), + any()); + + // Change the legacy priority list that dun is higher than wifi. + when(mResources.getIntArray(R.array.config_tether_upstream_types)).thenReturn( + new int[] { TYPE_MOBILE_DUN, TYPE_WIFI }); + sendConfigurationChanged(); + mLooper.dispatchAll(); + + // Make wifi as default network. Note: mobile also connected. + wifi.fakeConnect(); + mLooper.dispatchAll(); + mCm.makeDefaultNetwork(wifi, CALLBACKS_FIRST); + mLooper.dispatchAll(); + // BUG: dun has higher priority than wifi but tethering don't file dun request because + // current upstream is wifi. + inOrder.verify(mCm, never()).requestNetwork(any(), eq(0), eq(TYPE_MOBILE_DUN), any(), + any()); + } + + private void chooseDunUpstreamTestCommon(final boolean automatic, InOrder inOrder, + TestNetworkAgent mobile, TestNetworkAgent wifi, TestNetworkAgent dun) throws Exception { + when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(automatic); + when(mTelephonyManager.isTetheringApnRequired()).thenReturn(true); + sendConfigurationChanged(); + mLooper.dispatchAll(); + + // Start USB tethering with no current upstream. + prepareUsbTethering(); + sendUsbBroadcast(true, true, true, TETHERING_USB); + inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks(); + inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true); + ArgumentCaptor captor = ArgumentCaptor.forClass(NetworkCallback.class); + inOrder.verify(mCm).requestNetwork(any(), eq(0), eq(TYPE_MOBILE_DUN), any(), + captor.capture()); + final NetworkCallback dunNetworkCallback1 = captor.getValue(); + + // Pretend cellular connected and expect the upstream to be set. + mobile.fakeConnect(); + mCm.makeDefaultNetwork(mobile, BROADCAST_FIRST); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor, never()).setCurrentUpstream(mobile.networkId); + + // Pretend dun connected and expect choose dun as upstream. + final Runnable doDispatchAll = () -> mLooper.dispatchAll(); + dun.fakeConnect(BROADCAST_FIRST, doDispatchAll); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(dun.networkId); + + // When wifi connected, unregister dun request and choose wifi as upstream. + wifi.fakeConnect(); + mCm.makeDefaultNetwork(wifi, CALLBACKS_FIRST); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setTryCell(false); + inOrder.verify(mCm).unregisterNetworkCallback(eq(dunNetworkCallback1)); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId); + dun.fakeDisconnect(BROADCAST_FIRST, doDispatchAll); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor, never()).setCurrentUpstream(any()); + } + + private void runNcmTethering() { + prepareNcmTethering(); + sendUsbBroadcast(true, true, true, TETHERING_NCM); + } + + @Test + public void workingNcmTethering() throws Exception { + runNcmTethering(); + + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); + } + + @Test + public void workingNcmTethering_LegacyDhcp() { + when(mResources.getBoolean(R.bool.config_tether_enable_legacy_dhcp_server)).thenReturn( + true); + sendConfigurationChanged(); + runNcmTethering(); + + verify(mIpServerDependencies, never()).makeDhcpServer(any(), any(), any()); + } + + @Test + public void workingLocalOnlyHotspotEnrichedApBroadcastWithIfaceChanged() throws Exception { + workingLocalOnlyHotspotEnrichedApBroadcast(true); + } + + @Test + public void workingLocalOnlyHotspotEnrichedApBroadcastSansIfaceChanged() throws Exception { + workingLocalOnlyHotspotEnrichedApBroadcast(false); + } + + // TODO: Test with and without interfaceStatusChanged(). + @Test + public void failingWifiTetheringLegacyApBroadcast() throws Exception { + when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true); + + // Emulate pressing the WiFi tethering button. + mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null); + mLooper.dispatchAll(); + verify(mWifiManager, times(1)).startTetheredHotspot(null); + verifyNoMoreInteractions(mWifiManager); + verifyNoMoreInteractions(mNetd); + + // Emulate externally-visible WifiManager effects, causing the + // per-interface state machine to start up, and telling us that + // tethering mode is to be started. + mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true); + sendWifiApStateChanged(WIFI_AP_STATE_ENABLED); + + // There is 1 IpServer state change event: STATE_AVAILABLE + verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE); + verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER); + verify(mWifiManager).updateInterfaceIpState( + TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED); + verifyNoMoreInteractions(mNetd); + verifyNoMoreInteractions(mWifiManager); + } + + // TODO: Test with and without interfaceStatusChanged(). + @Test + public void workingWifiTetheringEnrichedApBroadcast() throws Exception { + when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true); + + // Emulate pressing the WiFi tethering button. + mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null); + mLooper.dispatchAll(); + verify(mWifiManager, times(1)).startTetheredHotspot(null); + verifyNoMoreInteractions(mWifiManager); + verifyNoMoreInteractions(mNetd); + + // Emulate externally-visible WifiManager effects, causing the + // per-interface state machine to start up, and telling us that + // tethering mode is to be started. + mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true); + sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED); + + verifyInterfaceServingModeStarted(TEST_WLAN_IFNAME); + verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER); + verify(mNetd, times(1)).ipfwdEnableForwarding(TETHERING_NAME); + verify(mNetd, times(1)).tetherStartWithConfiguration(any()); + verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_WLAN_IFNAME), + anyString(), anyString()); + verifyNoMoreInteractions(mNetd); + verify(mWifiManager).updateInterfaceIpState( + TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED); + verify(mWifiManager).updateInterfaceIpState( + TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_TETHERED); + verifyNoMoreInteractions(mWifiManager); + verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_ACTIVE_TETHER); + verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks(); + // In tethering mode, in the default configuration, an explicit request + // for a mobile network is also made. + verify(mUpstreamNetworkMonitor, times(1)).setTryCell(true); + // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_TETHERED + verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE); + verify(mNotificationUpdater, times(1)).onDownstreamChanged(eq(1 << TETHERING_WIFI)); + + ///// + // We do not currently emulate any upstream being found. + // + // This is why there are no calls to verify mNetd.tetherAddForward() or + // mNetd.ipfwdAddInterfaceForward(). + ///// + + // Emulate pressing the WiFi tethering button. + mTethering.stopTethering(TETHERING_WIFI); + mLooper.dispatchAll(); + verify(mWifiManager, times(1)).stopSoftAp(); + verifyNoMoreInteractions(mWifiManager); + verifyNoMoreInteractions(mNetd); + + // Emulate externally-visible WifiManager effects, when tethering mode + // is being torn down. + sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED); + mTethering.interfaceRemoved(TEST_WLAN_IFNAME); + mLooper.dispatchAll(); + + verify(mNetd, times(1)).tetherApplyDnsInterfaces(); + verify(mNetd, times(1)).tetherInterfaceRemove(TEST_WLAN_IFNAME); + verify(mNetd, times(1)).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME); + // interfaceSetCfg() called once for enabling and twice for disabling IPv4. + verify(mNetd, times(3)).interfaceSetCfg(any(InterfaceConfigurationParcel.class)); + verify(mNetd, times(1)).tetherStop(); + verify(mNetd, times(1)).ipfwdDisableForwarding(TETHERING_NAME); + verify(mWifiManager, times(3)).updateInterfaceIpState( + TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED); + verifyNoMoreInteractions(mNetd); + verifyNoMoreInteractions(mWifiManager); + // Asking for the last error after the per-interface state machine + // has been reaped yields an unknown interface error. + assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastErrorForTest(TEST_WLAN_IFNAME)); + } + + // TODO: Test with and without interfaceStatusChanged(). + @Test + public void failureEnablingIpForwarding() throws Exception { + when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true); + doThrow(new RemoteException()).when(mNetd).ipfwdEnableForwarding(TETHERING_NAME); + + // Emulate pressing the WiFi tethering button. + mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null); + mLooper.dispatchAll(); + verify(mWifiManager, times(1)).startTetheredHotspot(null); + verifyNoMoreInteractions(mWifiManager); + verifyNoMoreInteractions(mNetd); + + // Emulate externally-visible WifiManager effects, causing the + // per-interface state machine to start up, and telling us that + // tethering mode is to be started. + mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true); + sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED); + + // We verify get/set called three times here: twice for setup and once during + // teardown because all events happen over the course of the single + // dispatchAll() above. Note that once the IpServer IPv4 address config + // code is refactored the two calls during shutdown will revert to one. + verify(mNetd, times(3)).interfaceSetCfg(argThat(p -> TEST_WLAN_IFNAME.equals(p.ifName))); + verify(mNetd, times(1)).tetherInterfaceAdd(TEST_WLAN_IFNAME); + verify(mNetd, times(1)).networkAddInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME); + verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_WLAN_IFNAME), + anyString(), anyString()); + verify(mWifiManager).updateInterfaceIpState( + TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED); + verify(mWifiManager).updateInterfaceIpState( + TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_TETHERED); + // There are 3 IpServer state change event: + // STATE_AVAILABLE -> STATE_TETHERED -> STATE_AVAILABLE. + verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE); + verify(mNotificationUpdater, times(1)).onDownstreamChanged(eq(1 << TETHERING_WIFI)); + verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER); + // This is called, but will throw. + verify(mNetd, times(1)).ipfwdEnableForwarding(TETHERING_NAME); + // This never gets called because of the exception thrown above. + verify(mNetd, times(0)).tetherStartWithConfiguration(any()); + // When the main state machine transitions to an error state it tells + // downstream interfaces, which causes us to tell Wi-Fi about the error + // so it can take down AP mode. + verify(mNetd, times(1)).tetherApplyDnsInterfaces(); + verify(mNetd, times(1)).tetherInterfaceRemove(TEST_WLAN_IFNAME); + verify(mNetd, times(1)).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME); + verify(mWifiManager).updateInterfaceIpState( + TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_CONFIGURATION_ERROR); + + verifyNoMoreInteractions(mWifiManager); + verifyNoMoreInteractions(mNetd); + } + + private UserRestrictionActionListener makeUserRestrictionActionListener( + final Tethering tethering, final boolean currentDisallow, final boolean nextDisallow) { + final Bundle newRestrictions = new Bundle(); + newRestrictions.putBoolean(UserManager.DISALLOW_CONFIG_TETHERING, nextDisallow); + when(mUserManager.getUserRestrictions()).thenReturn(newRestrictions); + + final UserRestrictionActionListener ural = + new UserRestrictionActionListener(mUserManager, tethering, mNotificationUpdater); + ural.mDisallowTethering = currentDisallow; + return ural; + } + + private void runUserRestrictionsChange( + boolean currentDisallow, boolean nextDisallow, boolean isTetheringActive, + int expectedInteractionsWithShowNotification) throws Exception { + final Tethering mockTethering = mock(Tethering.class); + when(mockTethering.isTetheringActive()).thenReturn(isTetheringActive); + + final UserRestrictionActionListener ural = + makeUserRestrictionActionListener(mockTethering, currentDisallow, nextDisallow); + ural.onUserRestrictionsChanged(); + + verify(mNotificationUpdater, times(expectedInteractionsWithShowNotification)) + .notifyTetheringDisabledByRestriction(); + verify(mockTethering, times(expectedInteractionsWithShowNotification)).untetherAll(); + } + + @Test + public void testDisallowTetheringWhenTetheringIsNotActive() throws Exception { + final boolean isTetheringActive = false; + final boolean currDisallow = false; + final boolean nextDisallow = true; + final int expectedInteractionsWithShowNotification = 0; + + runUserRestrictionsChange(currDisallow, nextDisallow, isTetheringActive, + expectedInteractionsWithShowNotification); + } + + @Test + public void testDisallowTetheringWhenTetheringIsActive() throws Exception { + final boolean isTetheringActive = true; + final boolean currDisallow = false; + final boolean nextDisallow = true; + final int expectedInteractionsWithShowNotification = 1; + + runUserRestrictionsChange(currDisallow, nextDisallow, isTetheringActive, + expectedInteractionsWithShowNotification); + } + + @Test + public void testAllowTetheringWhenTetheringIsNotActive() throws Exception { + final boolean isTetheringActive = false; + final boolean currDisallow = true; + final boolean nextDisallow = false; + final int expectedInteractionsWithShowNotification = 0; + + runUserRestrictionsChange(currDisallow, nextDisallow, isTetheringActive, + expectedInteractionsWithShowNotification); + } + + @Test + public void testAllowTetheringWhenTetheringIsActive() throws Exception { + final boolean isTetheringActive = true; + final boolean currDisallow = true; + final boolean nextDisallow = false; + final int expectedInteractionsWithShowNotification = 0; + + runUserRestrictionsChange(currDisallow, nextDisallow, isTetheringActive, + expectedInteractionsWithShowNotification); + } + + @Test + public void testDisallowTetheringUnchanged() throws Exception { + final boolean isTetheringActive = true; + final int expectedInteractionsWithShowNotification = 0; + boolean currDisallow = true; + boolean nextDisallow = true; + + runUserRestrictionsChange(currDisallow, nextDisallow, isTetheringActive, + expectedInteractionsWithShowNotification); + + currDisallow = false; + nextDisallow = false; + + runUserRestrictionsChange(currDisallow, nextDisallow, isTetheringActive, + expectedInteractionsWithShowNotification); + } + + @Test + public void testUntetherUsbWhenRestrictionIsOn() { + // Start usb tethering and check that usb interface is tethered. + final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState(); + runUsbTethering(upstreamState); + assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_USB_IFNAME); + assertTrue(mTethering.isTetheringActive()); + assertEquals(0, mTethering.getActiveTetheringRequests().size()); + + final Tethering.UserRestrictionActionListener ural = makeUserRestrictionActionListener( + mTethering, false /* currentDisallow */, true /* nextDisallow */); + + ural.onUserRestrictionsChanged(); + mLooper.dispatchAll(); + + // Verify that restriction notification has showed to user. + verify(mNotificationUpdater, times(1)).notifyTetheringDisabledByRestriction(); + // Verify that usb tethering has been disabled. + verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NONE); + } + + private class TestTetheringEventCallback extends ITetheringEventCallback.Stub { + private final ArrayList mActualUpstreams = new ArrayList<>(); + private final ArrayList mTetheringConfigs = + new ArrayList<>(); + private final ArrayList mTetherStates = new ArrayList<>(); + private final ArrayList mOffloadStatus = new ArrayList<>(); + private final ArrayList> mTetheredClients = new ArrayList<>(); + + // This function will remove the recorded callbacks, so it must be called once for + // each callback. If this is called after multiple callback, the order matters. + // onCallbackCreated counts as the first call to expectUpstreamChanged with + // @see onCallbackCreated. + public void expectUpstreamChanged(Network... networks) { + if (networks == null) { + assertNoUpstreamChangeCallback(); + return; + } + + final ArrayList expectedUpstreams = + new ArrayList(Arrays.asList(networks)); + for (Network upstream : expectedUpstreams) { + // throws OOB if no expectations + assertEquals(mActualUpstreams.remove(0), upstream); + } + assertNoUpstreamChangeCallback(); + } + + // This function will remove the recorded callbacks, so it must be called once + // for each callback. If this is called after multiple callback, the order matters. + // onCallbackCreated counts as the first call to onConfigurationChanged with + // @see onCallbackCreated. + public void expectConfigurationChanged(TetheringConfigurationParcel... tetherConfigs) { + final ArrayList expectedTetherConfig = + new ArrayList(Arrays.asList(tetherConfigs)); + for (TetheringConfigurationParcel config : expectedTetherConfig) { + // throws OOB if no expectations + final TetheringConfigurationParcel actualConfig = mTetheringConfigs.remove(0); + assertTetherConfigParcelEqual(actualConfig, config); + } + assertNoConfigChangeCallback(); + } + + public void expectOffloadStatusChanged(final int expectedStatus) { + assertOffloadStatusChangedCallback(); + assertEquals(mOffloadStatus.remove(0), new Integer(expectedStatus)); + } + + public TetherStatesParcel pollTetherStatesChanged() { + assertStateChangeCallback(); + return mTetherStates.remove(0); + } + + public void expectTetheredClientChanged(List leases) { + assertFalse(mTetheredClients.isEmpty()); + final List result = mTetheredClients.remove(0); + assertEquals(leases.size(), result.size()); + assertTrue(leases.containsAll(result)); + } + + @Override + public void onUpstreamChanged(Network network) { + mActualUpstreams.add(network); + } + + @Override + public void onConfigurationChanged(TetheringConfigurationParcel config) { + mTetheringConfigs.add(config); + } + + @Override + public void onTetherStatesChanged(TetherStatesParcel states) { + mTetherStates.add(states); + } + + @Override + public void onTetherClientsChanged(List clients) { + mTetheredClients.add(clients); + } + + @Override + public void onOffloadStatusChanged(final int status) { + mOffloadStatus.add(status); + } + + @Override + public void onCallbackStarted(TetheringCallbackStartedParcel parcel) { + mActualUpstreams.add(parcel.upstreamNetwork); + mTetheringConfigs.add(parcel.config); + mTetherStates.add(parcel.states); + mOffloadStatus.add(parcel.offloadStatus); + mTetheredClients.add(parcel.tetheredClients); + } + + @Override + public void onCallbackStopped(int errorCode) { } + + public void assertNoUpstreamChangeCallback() { + assertTrue(mActualUpstreams.isEmpty()); + } + + public void assertNoConfigChangeCallback() { + assertTrue(mTetheringConfigs.isEmpty()); + } + + public void assertNoStateChangeCallback() { + assertTrue(mTetherStates.isEmpty()); + } + + public void assertStateChangeCallback() { + assertFalse(mTetherStates.isEmpty()); + } + + public void assertOffloadStatusChangedCallback() { + assertFalse(mOffloadStatus.isEmpty()); + } + + public void assertNoCallback() { + assertNoUpstreamChangeCallback(); + assertNoConfigChangeCallback(); + assertNoStateChangeCallback(); + assertTrue(mTetheredClients.isEmpty()); + } + + private void assertTetherConfigParcelEqual(@NonNull TetheringConfigurationParcel actual, + @NonNull TetheringConfigurationParcel expect) { + assertEquals(actual.subId, expect.subId); + assertArrayEquals(actual.tetherableUsbRegexs, expect.tetherableUsbRegexs); + assertArrayEquals(actual.tetherableWifiRegexs, expect.tetherableWifiRegexs); + assertArrayEquals(actual.tetherableBluetoothRegexs, expect.tetherableBluetoothRegexs); + assertEquals(actual.isDunRequired, expect.isDunRequired); + assertEquals(actual.chooseUpstreamAutomatically, expect.chooseUpstreamAutomatically); + assertArrayEquals(actual.preferredUpstreamIfaceTypes, + expect.preferredUpstreamIfaceTypes); + assertArrayEquals(actual.legacyDhcpRanges, expect.legacyDhcpRanges); + assertArrayEquals(actual.defaultIPv4DNS, expect.defaultIPv4DNS); + assertEquals(actual.enableLegacyDhcpServer, expect.enableLegacyDhcpServer); + assertArrayEquals(actual.provisioningApp, expect.provisioningApp); + assertEquals(actual.provisioningAppNoUi, expect.provisioningAppNoUi); + assertEquals(actual.provisioningCheckPeriod, expect.provisioningCheckPeriod); + } + } + + private void assertTetherStatesNotNullButEmpty(final TetherStatesParcel parcel) { + assertFalse(parcel == null); + assertEquals(0, parcel.availableList.length); + assertEquals(0, parcel.tetheredList.length); + assertEquals(0, parcel.localOnlyList.length); + assertEquals(0, parcel.erroredIfaceList.length); + assertEquals(0, parcel.lastErrorList.length); + MiscAsserts.assertFieldCountEquals(5, TetherStatesParcel.class); + } + + @Test + public void testRegisterTetheringEventCallback() throws Exception { + TestTetheringEventCallback callback = new TestTetheringEventCallback(); + TestTetheringEventCallback callback2 = new TestTetheringEventCallback(); + final TetheringInterface wifiIface = new TetheringInterface( + TETHERING_WIFI, TEST_WLAN_IFNAME); + + // 1. Register one callback before running any tethering. + mTethering.registerTetheringEventCallback(callback); + mLooper.dispatchAll(); + callback.expectTetheredClientChanged(Collections.emptyList()); + callback.expectUpstreamChanged(new Network[] {null}); + callback.expectConfigurationChanged( + mTethering.getTetheringConfiguration().toStableParcelable()); + TetherStatesParcel tetherState = callback.pollTetherStatesChanged(); + assertTetherStatesNotNullButEmpty(tetherState); + callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED); + // 2. Enable wifi tethering. + UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState(); + initTetheringUpstream(upstreamState); + when(mWifiManager.startTetheredHotspot(any(SoftApConfiguration.class))).thenReturn(true); + mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true); + mLooper.dispatchAll(); + tetherState = callback.pollTetherStatesChanged(); + assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface}); + + mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null); + sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED); + tetherState = callback.pollTetherStatesChanged(); + assertArrayEquals(tetherState.tetheredList, new TetheringInterface[] {wifiIface}); + callback.expectUpstreamChanged(upstreamState.network); + callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STARTED); + + // 3. Register second callback. + mTethering.registerTetheringEventCallback(callback2); + mLooper.dispatchAll(); + callback2.expectTetheredClientChanged(Collections.emptyList()); + callback2.expectUpstreamChanged(upstreamState.network); + callback2.expectConfigurationChanged( + mTethering.getTetheringConfiguration().toStableParcelable()); + tetherState = callback2.pollTetherStatesChanged(); + assertEquals(tetherState.tetheredList, new TetheringInterface[] {wifiIface}); + callback2.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STARTED); + + // 4. Unregister first callback and disable wifi tethering + mTethering.unregisterTetheringEventCallback(callback); + mLooper.dispatchAll(); + mTethering.stopTethering(TETHERING_WIFI); + sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED); + tetherState = callback2.pollTetherStatesChanged(); + assertArrayEquals(tetherState.availableList, new TetheringInterface[] {wifiIface}); + mLooper.dispatchAll(); + callback2.expectUpstreamChanged(new Network[] {null}); + callback2.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED); + callback.assertNoCallback(); + } + + @Test + public void testReportFailCallbackIfOffloadNotSupported() throws Exception { + final UpstreamNetworkState upstreamState = buildMobileDualStackUpstreamState(); + TestTetheringEventCallback callback = new TestTetheringEventCallback(); + mTethering.registerTetheringEventCallback(callback); + mLooper.dispatchAll(); + callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED); + + // 1. Offload fail if no OffloadConfig. + initOffloadConfiguration(false /* offloadConfig */, OFFLOAD_HAL_VERSION_1_0, + 0 /* defaultDisabled */); + runUsbTethering(upstreamState); + callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_FAILED); + runStopUSBTethering(); + callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED); + reset(mUsbManager); + // 2. Offload fail if no OffloadControl. + initOffloadConfiguration(true /* offloadConfig */, OFFLOAD_HAL_VERSION_NONE, + 0 /* defaultDisabled */); + runUsbTethering(upstreamState); + callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_FAILED); + runStopUSBTethering(); + callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED); + reset(mUsbManager); + // 3. Offload fail if disabled by settings. + initOffloadConfiguration(true /* offloadConfig */, OFFLOAD_HAL_VERSION_1_0, + 1 /* defaultDisabled */); + runUsbTethering(upstreamState); + callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_FAILED); + runStopUSBTethering(); + callback.expectOffloadStatusChanged(TETHER_HARDWARE_OFFLOAD_STOPPED); + } + + private void runStopUSBTethering() { + mTethering.stopTethering(TETHERING_USB); + mLooper.dispatchAll(); + mTethering.interfaceRemoved(TEST_USB_IFNAME); + mLooper.dispatchAll(); + } + + private void initOffloadConfiguration(final boolean offloadConfig, + @OffloadHardwareInterface.OffloadHalVersion final int offloadControlVersion, + final int defaultDisabled) { + when(mOffloadHardwareInterface.initOffloadConfig()).thenReturn(offloadConfig); + when(mOffloadHardwareInterface.initOffloadControl(any())).thenReturn(offloadControlVersion); + when(mOffloadHardwareInterface.getDefaultTetherOffloadDisabled()).thenReturn( + defaultDisabled); + } + + @Test + public void testMultiSimAware() throws Exception { + final TetheringConfiguration initailConfig = mTethering.getTetheringConfiguration(); + assertEquals(INVALID_SUBSCRIPTION_ID, initailConfig.activeDataSubId); + + final int fakeSubId = 1234; + mPhoneStateListener.onActiveDataSubscriptionIdChanged(fakeSubId); + final TetheringConfiguration newConfig = mTethering.getTetheringConfiguration(); + assertEquals(fakeSubId, newConfig.activeDataSubId); + verify(mNotificationUpdater, times(1)).onActiveDataSubscriptionIdChanged(eq(fakeSubId)); + } + + @Test + public void testNoDuplicatedEthernetRequest() throws Exception { + final TetheredInterfaceRequest mockRequest = mock(TetheredInterfaceRequest.class); + when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest); + mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null); + mLooper.dispatchAll(); + verify(mEm, times(1)).requestTetheredInterface(any(), any()); + mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null); + mLooper.dispatchAll(); + verifyNoMoreInteractions(mEm); + mTethering.stopTethering(TETHERING_ETHERNET); + mLooper.dispatchAll(); + verify(mockRequest, times(1)).release(); + mTethering.stopTethering(TETHERING_ETHERNET); + mLooper.dispatchAll(); + verifyNoMoreInteractions(mEm); + } + + private void workingWifiP2pGroupOwner( + boolean emulateInterfaceStatusChanged) throws Exception { + if (emulateInterfaceStatusChanged) { + mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true); + } + sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME); + + verifyInterfaceServingModeStarted(TEST_P2P_IFNAME); + verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_AVAILABLE_TETHER); + verify(mNetd, times(1)).ipfwdEnableForwarding(TETHERING_NAME); + verify(mNetd, times(1)).tetherStartWithConfiguration(any()); + verifyNoMoreInteractions(mNetd); + verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_ACTIVE_LOCAL_ONLY); + verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks(); + // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_LOCAL_ONLY + verify(mNotificationUpdater, times(2)).onDownstreamChanged(DOWNSTREAM_NONE); + + assertEquals(TETHER_ERROR_NO_ERROR, mTethering.getLastErrorForTest(TEST_P2P_IFNAME)); + + // Emulate externally-visible WifiP2pManager effects, when wifi p2p group + // is being removed. + sendWifiP2pConnectionChanged(false, true, TEST_P2P_IFNAME); + mTethering.interfaceRemoved(TEST_P2P_IFNAME); + + verify(mNetd, times(1)).tetherApplyDnsInterfaces(); + verify(mNetd, times(1)).tetherInterfaceRemove(TEST_P2P_IFNAME); + verify(mNetd, times(1)).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_P2P_IFNAME); + // interfaceSetCfg() called once for enabling and twice for disabling IPv4. + verify(mNetd, times(3)).interfaceSetCfg(any(InterfaceConfigurationParcel.class)); + verify(mNetd, times(1)).tetherStop(); + verify(mNetd, times(1)).ipfwdDisableForwarding(TETHERING_NAME); + verify(mUpstreamNetworkMonitor, never()).getCurrentPreferredUpstream(); + verify(mUpstreamNetworkMonitor, never()).selectPreferredUpstreamType(any()); + verifyNoMoreInteractions(mNetd); + // Asking for the last error after the per-interface state machine + // has been reaped yields an unknown interface error. + assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastErrorForTest(TEST_P2P_IFNAME)); + } + + private void workingWifiP2pGroupClient( + boolean emulateInterfaceStatusChanged) throws Exception { + if (emulateInterfaceStatusChanged) { + mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true); + } + sendWifiP2pConnectionChanged(true, false, TEST_P2P_IFNAME); + + verify(mNetd, never()).interfaceSetCfg(any(InterfaceConfigurationParcel.class)); + verify(mNetd, never()).tetherInterfaceAdd(TEST_P2P_IFNAME); + verify(mNetd, never()).networkAddInterface(INetd.LOCAL_NET_ID, TEST_P2P_IFNAME); + verify(mNetd, never()).ipfwdEnableForwarding(TETHERING_NAME); + verify(mNetd, never()).tetherStartWithConfiguration(any()); + + // Emulate externally-visible WifiP2pManager effects, when wifi p2p group + // is being removed. + sendWifiP2pConnectionChanged(false, false, TEST_P2P_IFNAME); + mTethering.interfaceRemoved(TEST_P2P_IFNAME); + + verify(mNetd, never()).tetherApplyDnsInterfaces(); + verify(mNetd, never()).tetherInterfaceRemove(TEST_P2P_IFNAME); + verify(mNetd, never()).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_P2P_IFNAME); + verify(mNetd, never()).interfaceSetCfg(any(InterfaceConfigurationParcel.class)); + verify(mNetd, never()).tetherStop(); + verify(mNetd, never()).ipfwdDisableForwarding(TETHERING_NAME); + verifyNoMoreInteractions(mNetd); + // Asking for the last error after the per-interface state machine + // has been reaped yields an unknown interface error. + assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastErrorForTest(TEST_P2P_IFNAME)); + } + + @Test + public void workingWifiP2pGroupOwnerWithIfaceChanged() throws Exception { + workingWifiP2pGroupOwner(true); + } + + @Test + public void workingWifiP2pGroupOwnerSansIfaceChanged() throws Exception { + workingWifiP2pGroupOwner(false); + } + + private void workingWifiP2pGroupOwnerLegacyMode( + boolean emulateInterfaceStatusChanged) throws Exception { + // change to legacy mode and update tethering information by chaning SIM + when(mResources.getStringArray(R.array.config_tether_wifi_p2p_regexs)) + .thenReturn(new String[]{}); + final int fakeSubId = 1234; + mPhoneStateListener.onActiveDataSubscriptionIdChanged(fakeSubId); + + if (emulateInterfaceStatusChanged) { + mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true); + } + sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME); + + verify(mNetd, never()).interfaceSetCfg(any(InterfaceConfigurationParcel.class)); + verify(mNetd, never()).tetherInterfaceAdd(TEST_P2P_IFNAME); + verify(mNetd, never()).networkAddInterface(INetd.LOCAL_NET_ID, TEST_P2P_IFNAME); + verify(mNetd, never()).ipfwdEnableForwarding(TETHERING_NAME); + verify(mNetd, never()).tetherStartWithConfiguration(any()); + assertEquals(TETHER_ERROR_UNKNOWN_IFACE, mTethering.getLastErrorForTest(TEST_P2P_IFNAME)); + } + @Test + public void workingWifiP2pGroupOwnerLegacyModeWithIfaceChanged() throws Exception { + workingWifiP2pGroupOwnerLegacyMode(true); + } + + @Test + public void workingWifiP2pGroupOwnerLegacyModeSansIfaceChanged() throws Exception { + workingWifiP2pGroupOwnerLegacyMode(false); + } + + @Test + public void workingWifiP2pGroupClientWithIfaceChanged() throws Exception { + workingWifiP2pGroupClient(true); + } + + @Test + public void workingWifiP2pGroupClientSansIfaceChanged() throws Exception { + workingWifiP2pGroupClient(false); + } + + private void setDataSaverEnabled(boolean enabled) { + final int status = enabled ? RESTRICT_BACKGROUND_STATUS_ENABLED + : RESTRICT_BACKGROUND_STATUS_DISABLED; + doReturn(status).when(mCm).getRestrictBackgroundStatus(); + + final Intent intent = new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED); + mServiceContext.sendBroadcastAsUser(intent, UserHandle.ALL); + mLooper.dispatchAll(); + } + + @Test + public void testDataSaverChanged() { + // Start Tethering. + final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState(); + runUsbTethering(upstreamState); + assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_USB_IFNAME); + // Data saver is ON. + setDataSaverEnabled(true); + // Verify that tethering should be disabled. + verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NONE); + mTethering.interfaceRemoved(TEST_USB_IFNAME); + mLooper.dispatchAll(); + assertEquals(mTethering.getTetheredIfaces(), new String[0]); + reset(mUsbManager); + + runUsbTethering(upstreamState); + // Verify that user can start tethering again without turning OFF data saver. + assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_USB_IFNAME); + + // If data saver is keep ON with change event, tethering should not be OFF this time. + setDataSaverEnabled(true); + verify(mUsbManager, times(0)).setCurrentFunctions(UsbManager.FUNCTION_NONE); + assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_USB_IFNAME); + + // If data saver is turned OFF, it should not change tethering. + setDataSaverEnabled(false); + verify(mUsbManager, times(0)).setCurrentFunctions(UsbManager.FUNCTION_NONE); + assertContains(Arrays.asList(mTethering.getTetheredIfaces()), TEST_USB_IFNAME); + } + + private static void assertContains(Collection collection, T element) { + assertTrue(element + " not found in " + collection, collection.contains(element)); + } + + private class ResultListener extends IIntResultListener.Stub { + private final int mExpectedResult; + private boolean mHasResult = false; + ResultListener(final int expectedResult) { + mExpectedResult = expectedResult; + } + + @Override + public void onResult(final int resultCode) { + mHasResult = true; + if (resultCode != mExpectedResult) { + fail("expected result: " + mExpectedResult + " but actual result: " + resultCode); + } + } + + public void assertHasResult() { + if (!mHasResult) fail("No callback result"); + } + } + + @Test + public void testMultipleStartTethering() throws Exception { + final LinkAddress serverLinkAddr = new LinkAddress("192.168.20.1/24"); + final LinkAddress clientLinkAddr = new LinkAddress("192.168.20.42/24"); + final String serverAddr = "192.168.20.1"; + final ResultListener firstResult = new ResultListener(TETHER_ERROR_NO_ERROR); + final ResultListener secondResult = new ResultListener(TETHER_ERROR_NO_ERROR); + final ResultListener thirdResult = new ResultListener(TETHER_ERROR_NO_ERROR); + + // Enable USB tethering and check that Tethering starts USB. + mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), firstResult); + mLooper.dispatchAll(); + firstResult.assertHasResult(); + verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS); + verifyNoMoreInteractions(mUsbManager); + + // Enable USB tethering again with the same request and expect no change to USB. + mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB), secondResult); + mLooper.dispatchAll(); + secondResult.assertHasResult(); + verify(mUsbManager, never()).setCurrentFunctions(UsbManager.FUNCTION_NONE); + reset(mUsbManager); + + // Enable USB tethering with a different request and expect that USB is stopped and + // started. + mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB, + serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL), thirdResult); + mLooper.dispatchAll(); + thirdResult.assertHasResult(); + verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_NONE); + verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS); + + // Expect that when USB comes up, the DHCP server is configured with the requested address. + mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true); + sendUsbBroadcast(true, true, true, TETHERING_USB); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); + verify(mNetd).interfaceSetCfg(argThat(cfg -> serverAddr.equals(cfg.ipv4Addr))); + } + + @Test + public void testRequestStaticIp() throws Exception { + final LinkAddress serverLinkAddr = new LinkAddress("192.168.0.123/24"); + final LinkAddress clientLinkAddr = new LinkAddress("192.168.0.42/24"); + final String serverAddr = "192.168.0.123"; + final int clientAddrParceled = 0xc0a8002a; + final ArgumentCaptor dhcpParamsCaptor = + ArgumentCaptor.forClass(DhcpServingParamsParcel.class); + mTethering.startTethering(createTetheringRequestParcel(TETHERING_USB, + serverLinkAddr, clientLinkAddr, false, CONNECTIVITY_SCOPE_GLOBAL), null); + mLooper.dispatchAll(); + verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS); + mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true); + sendUsbBroadcast(true, true, true, TETHERING_USB); + verify(mNetd).interfaceSetCfg(argThat(cfg -> serverAddr.equals(cfg.ipv4Addr))); + verify(mIpServerDependencies, times(1)).makeDhcpServer(any(), dhcpParamsCaptor.capture(), + any()); + final DhcpServingParamsParcel params = dhcpParamsCaptor.getValue(); + assertEquals(serverAddr, intToInet4AddressHTH(params.serverAddr).getHostAddress()); + assertEquals(24, params.serverAddrPrefixLength); + assertEquals(clientAddrParceled, params.singleClientAddr); + } + + @Test + public void testUpstreamNetworkChanged() { + final Tethering.TetherMainSM stateMachine = (Tethering.TetherMainSM) + mTetheringDependencies.mUpstreamNetworkMonitorSM; + final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState(); + initTetheringUpstream(upstreamState); + stateMachine.chooseUpstreamType(true); + + verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(eq(upstreamState.network)); + verify(mNotificationUpdater, times(1)).onUpstreamCapabilitiesChanged(any()); + } + + @Test + public void testUpstreamCapabilitiesChanged() { + final Tethering.TetherMainSM stateMachine = (Tethering.TetherMainSM) + mTetheringDependencies.mUpstreamNetworkMonitorSM; + final UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState(); + initTetheringUpstream(upstreamState); + stateMachine.chooseUpstreamType(true); + + stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState); + // Should have two onUpstreamCapabilitiesChanged(). + // One is called by reportUpstreamChanged(). One is called by EVENT_ON_CAPABILITIES. + verify(mNotificationUpdater, times(2)).onUpstreamCapabilitiesChanged(any()); + reset(mNotificationUpdater); + + // Verify that onUpstreamCapabilitiesChanged won't be called if not current upstream network + // capabilities changed. + final UpstreamNetworkState upstreamState2 = new UpstreamNetworkState( + upstreamState.linkProperties, upstreamState.networkCapabilities, + new Network(WIFI_NETID)); + stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState2); + verify(mNotificationUpdater, never()).onUpstreamCapabilitiesChanged(any()); + } + + @Test + public void testDumpTetheringLog() throws Exception { + final FileDescriptor mockFd = mock(FileDescriptor.class); + final PrintWriter mockPw = mock(PrintWriter.class); + runUsbTethering(null); + mLooper.startAutoDispatch(); + mTethering.dump(mockFd, mockPw, new String[0]); + verify(mConfig).dump(any()); + verify(mEntitleMgr).dump(any()); + verify(mOffloadCtrl).dump(any()); + mLooper.stopAutoDispatch(); + } + + @Test + public void testExemptFromEntitlementCheck() throws Exception { + setupForRequiredProvisioning(); + final TetheringRequestParcel wifiNotExemptRequest = + createTetheringRequestParcel(TETHERING_WIFI, null, null, false, + CONNECTIVITY_SCOPE_GLOBAL); + mTethering.startTethering(wifiNotExemptRequest, null); + mLooper.dispatchAll(); + verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false); + verify(mEntitleMgr, never()).setExemptedDownstreamType(TETHERING_WIFI); + assertFalse(mEntitleMgr.isCellularUpstreamPermitted()); + mTethering.stopTethering(TETHERING_WIFI); + mLooper.dispatchAll(); + verify(mEntitleMgr).stopProvisioningIfNeeded(TETHERING_WIFI); + reset(mEntitleMgr); + + setupForRequiredProvisioning(); + final TetheringRequestParcel wifiExemptRequest = + createTetheringRequestParcel(TETHERING_WIFI, null, null, true, + CONNECTIVITY_SCOPE_GLOBAL); + mTethering.startTethering(wifiExemptRequest, null); + mLooper.dispatchAll(); + verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false); + verify(mEntitleMgr).setExemptedDownstreamType(TETHERING_WIFI); + assertTrue(mEntitleMgr.isCellularUpstreamPermitted()); + mTethering.stopTethering(TETHERING_WIFI); + mLooper.dispatchAll(); + verify(mEntitleMgr).stopProvisioningIfNeeded(TETHERING_WIFI); + reset(mEntitleMgr); + + // If one app enables tethering without provisioning check first, then another app enables + // tethering of the same type but does not disable the provisioning check. + setupForRequiredProvisioning(); + mTethering.startTethering(wifiExemptRequest, null); + mLooper.dispatchAll(); + verify(mEntitleMgr, never()).startProvisioningIfNeeded(TETHERING_WIFI, false); + verify(mEntitleMgr).setExemptedDownstreamType(TETHERING_WIFI); + assertTrue(mEntitleMgr.isCellularUpstreamPermitted()); + reset(mEntitleMgr); + setupForRequiredProvisioning(); + mTethering.startTethering(wifiNotExemptRequest, null); + mLooper.dispatchAll(); + verify(mEntitleMgr).startProvisioningIfNeeded(TETHERING_WIFI, false); + verify(mEntitleMgr, never()).setExemptedDownstreamType(TETHERING_WIFI); + assertFalse(mEntitleMgr.isCellularUpstreamPermitted()); + mTethering.stopTethering(TETHERING_WIFI); + mLooper.dispatchAll(); + verify(mEntitleMgr).stopProvisioningIfNeeded(TETHERING_WIFI); + reset(mEntitleMgr); + } + + private void setupForRequiredProvisioning() { + // Produce some acceptable looking provision app setting if requested. + when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app)) + .thenReturn(PROVISIONING_APP_NAME); + when(mResources.getString(R.string.config_mobile_hotspot_provision_app_no_ui)) + .thenReturn(PROVISIONING_NO_UI_APP_NAME); + // Act like the CarrierConfigManager is present and ready unless told otherwise. + when(mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE)) + .thenReturn(mCarrierConfigManager); + when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(mCarrierConfig); + mCarrierConfig.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, true); + mCarrierConfig.putBoolean(CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, true); + sendConfigurationChanged(); + } + + private static UpstreamNetworkState buildV4UpstreamState(final LinkAddress address, + final Network network, final String iface, final int transportType) { + final LinkProperties prop = new LinkProperties(); + prop.setInterfaceName(iface); + + prop.addLinkAddress(address); + + final NetworkCapabilities capabilities = new NetworkCapabilities() + .addTransportType(transportType); + return new UpstreamNetworkState(prop, capabilities, network); + } + + private void updateV4Upstream(final LinkAddress ipv4Address, final Network network, + final String iface, final int transportType) { + final UpstreamNetworkState upstream = buildV4UpstreamState(ipv4Address, network, iface, + transportType); + mTetheringDependencies.mUpstreamNetworkMonitorSM.sendMessage( + Tethering.TetherMainSM.EVENT_UPSTREAM_CALLBACK, + UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES, + 0, + upstream); + mLooper.dispatchAll(); + } + + @Test + public void testHandleIpConflict() throws Exception { + final Network wifiNetwork = new Network(200); + final Network[] allNetworks = { wifiNetwork }; + doReturn(allNetworks).when(mCm).getAllNetworks(); + runUsbTethering(null); + final ArgumentCaptor ifaceConfigCaptor = + ArgumentCaptor.forClass(InterfaceConfigurationParcel.class); + verify(mNetd).interfaceSetCfg(ifaceConfigCaptor.capture()); + final String ipv4Address = ifaceConfigCaptor.getValue().ipv4Addr; + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); + reset(mNetd, mUsbManager); + + // Cause a prefix conflict by assigning a /30 out of the downstream's /24 to the upstream. + updateV4Upstream(new LinkAddress(InetAddresses.parseNumericAddress(ipv4Address), 30), + wifiNetwork, TEST_WIFI_IFNAME, TRANSPORT_WIFI); + // verify turn off usb tethering + verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NONE); + mTethering.interfaceRemoved(TEST_USB_IFNAME); + mLooper.dispatchAll(); + // verify restart usb tethering + verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_RNDIS); + } + + @Test + public void testNoAddressAvailable() throws Exception { + final Network wifiNetwork = new Network(200); + final Network btNetwork = new Network(201); + final Network mobileNetwork = new Network(202); + final Network[] allNetworks = { wifiNetwork, btNetwork, mobileNetwork }; + doReturn(allNetworks).when(mCm).getAllNetworks(); + runUsbTethering(null); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks( + any(), any()); + reset(mUsbManager); + final TetheredInterfaceRequest mockRequest = mock(TetheredInterfaceRequest.class); + when(mEm.requestTetheredInterface(any(), any())).thenReturn(mockRequest); + final ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(TetheredInterfaceCallback.class); + mTethering.startTethering(createTetheringRequestParcel(TETHERING_ETHERNET), null); + mLooper.dispatchAll(); + verify(mEm).requestTetheredInterface(any(), callbackCaptor.capture()); + TetheredInterfaceCallback ethCallback = callbackCaptor.getValue(); + ethCallback.onAvailable(TEST_ETH_IFNAME); + mLooper.dispatchAll(); + reset(mUsbManager, mEm); + + updateV4Upstream(new LinkAddress("192.168.0.100/16"), wifiNetwork, TEST_WIFI_IFNAME, + TRANSPORT_WIFI); + updateV4Upstream(new LinkAddress("172.16.0.0/12"), btNetwork, TEST_BT_IFNAME, + TRANSPORT_BLUETOOTH); + updateV4Upstream(new LinkAddress("10.0.0.0/8"), mobileNetwork, TEST_MOBILE_IFNAME, + TRANSPORT_CELLULAR); + + mLooper.dispatchAll(); + // verify turn off usb tethering + verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_NONE); + // verify turn off ethernet tethering + verify(mockRequest).release(); + mTethering.interfaceRemoved(TEST_USB_IFNAME); + ethCallback.onUnavailable(); + mLooper.dispatchAll(); + // verify restart usb tethering + verify(mUsbManager).setCurrentFunctions(UsbManager.FUNCTION_RNDIS); + // verify restart ethernet tethering + verify(mEm).requestTetheredInterface(any(), callbackCaptor.capture()); + ethCallback = callbackCaptor.getValue(); + ethCallback.onAvailable(TEST_ETH_IFNAME); + + reset(mUsbManager, mEm); + when(mNetd.interfaceGetList()) + .thenReturn(new String[] { + TEST_MOBILE_IFNAME, TEST_WLAN_IFNAME, TEST_USB_IFNAME, TEST_P2P_IFNAME, + TEST_NCM_IFNAME, TEST_ETH_IFNAME}); + + mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true); + sendUsbBroadcast(true, true, true, TETHERING_USB); + assertContains(Arrays.asList(mTethering.getTetherableIfacesForTest()), TEST_USB_IFNAME); + assertContains(Arrays.asList(mTethering.getTetherableIfacesForTest()), TEST_ETH_IFNAME); + assertEquals(TETHER_ERROR_IFACE_CFG_ERROR, mTethering.getLastErrorForTest(TEST_USB_IFNAME)); + assertEquals(TETHER_ERROR_IFACE_CFG_ERROR, mTethering.getLastErrorForTest(TEST_ETH_IFNAME)); + } + + @Test + public void testProvisioningNeededButUnavailable() throws Exception { + assertTrue(mTethering.isTetheringSupported()); + verify(mPackageManager, never()).getPackageInfo(PROVISIONING_APP_NAME[0], GET_ACTIVITIES); + + setupForRequiredProvisioning(); + assertTrue(mTethering.isTetheringSupported()); + verify(mPackageManager).getPackageInfo(PROVISIONING_APP_NAME[0], GET_ACTIVITIES); + reset(mPackageManager); + + doThrow(PackageManager.NameNotFoundException.class).when(mPackageManager).getPackageInfo( + PROVISIONING_APP_NAME[0], GET_ACTIVITIES); + setupForRequiredProvisioning(); + assertFalse(mTethering.isTetheringSupported()); + verify(mPackageManager).getPackageInfo(PROVISIONING_APP_NAME[0], GET_ACTIVITIES); + } + + @Test + public void testUpdateConnectedClients() throws Exception { + TestTetheringEventCallback callback = new TestTetheringEventCallback(); + runAsShell(NETWORK_SETTINGS, () -> { + mTethering.registerTetheringEventCallback(callback); + mLooper.dispatchAll(); + }); + callback.expectTetheredClientChanged(Collections.emptyList()); + + IDhcpEventCallbacks eventCallbacks; + final ArgumentCaptor dhcpEventCbsCaptor = + ArgumentCaptor.forClass(IDhcpEventCallbacks.class); + // Run local only tethering. + mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true); + sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks( + any(), dhcpEventCbsCaptor.capture()); + eventCallbacks = dhcpEventCbsCaptor.getValue(); + // Update lease for local only tethering. + final MacAddress testMac1 = MacAddress.fromString("11:11:11:11:11:11"); + final ArrayList p2pLeases = new ArrayList<>(); + p2pLeases.add(createDhcpLeaseParcelable("clientId1", testMac1, "192.168.50.24", 24, + Long.MAX_VALUE, "test1")); + notifyDhcpLeasesChanged(p2pLeases, eventCallbacks); + final List clients = toTetheredClients(p2pLeases, TETHERING_WIFI_P2P); + callback.expectTetheredClientChanged(clients); + reset(mDhcpServer); + + // Run wifi tethering. + mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true); + sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED); + verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks( + any(), dhcpEventCbsCaptor.capture()); + eventCallbacks = dhcpEventCbsCaptor.getValue(); + // Update mac address from softAp callback before getting dhcp lease. + final ArrayList wifiClients = new ArrayList<>(); + final MacAddress testMac2 = MacAddress.fromString("22:22:22:22:22:22"); + final WifiClient testClient = mock(WifiClient.class); + when(testClient.getMacAddress()).thenReturn(testMac2); + wifiClients.add(testClient); + mSoftApCallback.onConnectedClientsChanged(wifiClients); + final TetheredClient noAddrClient = new TetheredClient(testMac2, + Collections.emptyList() /* addresses */, TETHERING_WIFI); + clients.add(noAddrClient); + callback.expectTetheredClientChanged(clients); + + // Update dhcp lease for wifi tethering. + clients.remove(noAddrClient); + final ArrayList wifiLeases = new ArrayList<>(); + wifiLeases.add(createDhcpLeaseParcelable("clientId2", testMac2, "192.168.43.24", 24, + Long.MAX_VALUE, "test2")); + notifyDhcpLeasesChanged(wifiLeases, eventCallbacks); + clients.addAll(toTetheredClients(wifiLeases, TETHERING_WIFI)); + callback.expectTetheredClientChanged(clients); + + // Test onStarted callback that register second callback when tethering is running. + TestTetheringEventCallback callback2 = new TestTetheringEventCallback(); + runAsShell(NETWORK_SETTINGS, () -> { + mTethering.registerTetheringEventCallback(callback2); + mLooper.dispatchAll(); + }); + callback2.expectTetheredClientChanged(clients); + } + + private void notifyDhcpLeasesChanged(List leaseParcelables, + IDhcpEventCallbacks callback) throws Exception { + callback.onLeasesChanged(leaseParcelables); + mLooper.dispatchAll(); + } + + private List toTetheredClients(List leaseParcelables, + int type) throws Exception { + final ArrayList leases = new ArrayList<>(); + for (DhcpLeaseParcelable lease : leaseParcelables) { + final LinkAddress address = new LinkAddress( + intToInet4AddressHTH(lease.netAddr), lease.prefixLength, + 0 /* flags */, RT_SCOPE_UNIVERSE /* as per RFC6724#3.2 */, + lease.expTime /* deprecationTime */, lease.expTime /* expirationTime */); + + final MacAddress macAddress = MacAddress.fromBytes(lease.hwAddr); + + final AddressInfo addressInfo = new TetheredClient.AddressInfo(address, lease.hostname); + leases.add(new TetheredClient( + macAddress, + Collections.singletonList(addressInfo), + type)); + } + + return leases; + } + + private DhcpLeaseParcelable createDhcpLeaseParcelable(final String clientId, + final MacAddress hwAddr, final String netAddr, final int prefixLength, + final long expTime, final String hostname) throws Exception { + final DhcpLeaseParcelable lease = new DhcpLeaseParcelable(); + lease.clientId = clientId.getBytes(); + lease.hwAddr = hwAddr.toByteArray(); + lease.netAddr = inet4AddressToIntHTH( + (Inet4Address) InetAddresses.parseNumericAddress(netAddr)); + lease.prefixLength = prefixLength; + lease.expTime = expTime; + lease.hostname = hostname; + + return lease; + } + + @Test + public void testBluetoothTethering() throws Exception { + final ResultListener result = new ResultListener(TETHER_ERROR_NO_ERROR); + when(mBluetoothAdapter.isEnabled()).thenReturn(true); + mTethering.startTethering(createTetheringRequestParcel(TETHERING_BLUETOOTH), result); + mLooper.dispatchAll(); + verifySetBluetoothTethering(true); + result.assertHasResult(); + + mTethering.interfaceAdded(TEST_BT_IFNAME); + mLooper.dispatchAll(); + + mTethering.interfaceStatusChanged(TEST_BT_IFNAME, false); + mTethering.interfaceStatusChanged(TEST_BT_IFNAME, true); + final ResultListener tetherResult = new ResultListener(TETHER_ERROR_NO_ERROR); + mTethering.tether(TEST_BT_IFNAME, IpServer.STATE_TETHERED, tetherResult); + mLooper.dispatchAll(); + tetherResult.assertHasResult(); + + verify(mNetd).tetherInterfaceAdd(TEST_BT_IFNAME); + verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME); + verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME), + anyString(), anyString()); + verify(mNetd).ipfwdEnableForwarding(TETHERING_NAME); + verify(mNetd).tetherStartWithConfiguration(any()); + verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME), + anyString(), anyString()); + verifyNoMoreInteractions(mNetd); + reset(mNetd); + + when(mBluetoothAdapter.isEnabled()).thenReturn(true); + mTethering.stopTethering(TETHERING_BLUETOOTH); + mLooper.dispatchAll(); + final ResultListener untetherResult = new ResultListener(TETHER_ERROR_NO_ERROR); + mTethering.untether(TEST_BT_IFNAME, untetherResult); + mLooper.dispatchAll(); + untetherResult.assertHasResult(); + verifySetBluetoothTethering(false); + + verify(mNetd).tetherApplyDnsInterfaces(); + verify(mNetd).tetherInterfaceRemove(TEST_BT_IFNAME); + verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME); + verify(mNetd).interfaceSetCfg(any(InterfaceConfigurationParcel.class)); + verify(mNetd).tetherStop(); + verify(mNetd).ipfwdDisableForwarding(TETHERING_NAME); + verifyNoMoreInteractions(mNetd); + } + + private void verifySetBluetoothTethering(final boolean enable) { + final ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(ServiceListener.class); + verify(mBluetoothAdapter).isEnabled(); + verify(mBluetoothAdapter).getProfileProxy(eq(mServiceContext), listenerCaptor.capture(), + eq(BluetoothProfile.PAN)); + final ServiceListener listener = listenerCaptor.getValue(); + when(mBluetoothPan.isTetheringOn()).thenReturn(enable); + listener.onServiceConnected(BluetoothProfile.PAN, mBluetoothPan); + verify(mBluetoothPan).setBluetoothTethering(enable); + verify(mBluetoothPan).isTetheringOn(); + verify(mBluetoothAdapter).closeProfileProxy(eq(BluetoothProfile.PAN), eq(mBluetoothPan)); + verifyNoMoreInteractions(mBluetoothAdapter, mBluetoothPan); + reset(mBluetoothAdapter, mBluetoothPan); + } + + // TODO: Test that a request for hotspot mode doesn't interfere with an + // already operating tethering mode interface. +} diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java new file mode 100644 index 0000000000..ce4ba85565 --- /dev/null +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java @@ -0,0 +1,642 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import static android.net.ConnectivityManager.TYPE_MOBILE_DUN; +import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI; +import static android.net.ConnectivityManager.TYPE_WIFI; +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_WIFI; + +import static com.android.networkstack.tethering.UpstreamNetworkMonitor.TYPE_NONE; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.IConnectivityManager; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.util.SharedLog; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.test.TestLooper; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.State; +import com.android.internal.util.StateMachine; +import com.android.networkstack.tethering.TestConnectivityManager.NetworkRequestInfo; +import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class UpstreamNetworkMonitorTest { + private static final int EVENT_UNM_UPDATE = 1; + + private static final boolean INCLUDES = true; + private static final boolean EXCLUDES = false; + + private static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_CELLULAR).addCapability(NET_CAPABILITY_INTERNET).build(); + private static final NetworkCapabilities DUN_CAPABILITIES = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_CELLULAR).addCapability(NET_CAPABILITY_DUN).build(); + private static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_WIFI).addCapability(NET_CAPABILITY_INTERNET).build(); + + @Mock private Context mContext; + @Mock private EntitlementManager mEntitleMgr; + @Mock private IConnectivityManager mCS; + @Mock private SharedLog mLog; + + private TestStateMachine mSM; + private TestConnectivityManager mCM; + private UpstreamNetworkMonitor mUNM; + + private final TestLooper mLooper = new TestLooper(); + + @Before public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + reset(mContext); + reset(mCS); + reset(mLog); + when(mLog.forSubComponent(anyString())).thenReturn(mLog); + when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true); + + mCM = spy(new TestConnectivityManager(mContext, mCS)); + when(mContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE))).thenReturn(mCM); + mSM = new TestStateMachine(mLooper.getLooper()); + mUNM = new UpstreamNetworkMonitor(mContext, mSM, mLog, EVENT_UNM_UPDATE); + } + + @After public void tearDown() throws Exception { + if (mSM != null) { + mSM.quit(); + mSM = null; + } + } + + @Test + public void testStopWithoutStartIsNonFatal() { + mUNM.stop(); + mUNM.stop(); + mUNM.stop(); + } + + @Test + public void testDoesNothingBeforeTrackDefaultAndStarted() throws Exception { + assertTrue(mCM.hasNoCallbacks()); + assertFalse(mUNM.mobileNetworkRequested()); + + mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */); + assertTrue(mCM.hasNoCallbacks()); + mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */); + assertTrue(mCM.hasNoCallbacks()); + } + + @Test + public void testDefaultNetworkIsTracked() throws Exception { + assertTrue(mCM.hasNoCallbacks()); + mUNM.startTrackDefaultNetwork(mEntitleMgr); + + mUNM.startObserveAllNetworks(); + assertEquals(1, mCM.mTrackingDefault.size()); + + mUNM.stop(); + assertTrue(mCM.onlyHasDefaultCallbacks()); + } + + @Test + public void testListensForAllNetworks() throws Exception { + assertTrue(mCM.mListening.isEmpty()); + + mUNM.startTrackDefaultNetwork(mEntitleMgr); + mUNM.startObserveAllNetworks(); + assertFalse(mCM.mListening.isEmpty()); + assertTrue(mCM.isListeningForAll()); + + mUNM.stop(); + assertTrue(mCM.onlyHasDefaultCallbacks()); + } + + @Test + public void testCallbacksRegistered() { + mUNM.startTrackDefaultNetwork(mEntitleMgr); + // Verify the fired default request matches expectation. + final ArgumentCaptor requestCaptor = + ArgumentCaptor.forClass(NetworkRequest.class); + verify(mCM, times(1)).requestNetwork( + requestCaptor.capture(), any(NetworkCallback.class), any(Handler.class)); + // For R- devices, Tethering will invoke this function in 2 cases, one is to + // request mobile network, the other is to track system default network. Verify + // the request is the one tracks default network. + assertTrue(TestConnectivityManager.looksLikeDefaultRequest(requestCaptor.getValue())); + + mUNM.startObserveAllNetworks(); + verify(mCM, times(1)).registerNetworkCallback( + any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class)); + + mUNM.stop(); + verify(mCM, times(1)).unregisterNetworkCallback(any(NetworkCallback.class)); + } + + @Test + public void testRequestsMobileNetwork() throws Exception { + assertFalse(mUNM.mobileNetworkRequested()); + assertEquals(0, mCM.mRequested.size()); + + mUNM.startObserveAllNetworks(); + assertFalse(mUNM.mobileNetworkRequested()); + assertEquals(0, mCM.mRequested.size()); + + mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */); + assertFalse(mUNM.mobileNetworkRequested()); + assertEquals(0, mCM.mRequested.size()); + + mUNM.setTryCell(true); + assertTrue(mUNM.mobileNetworkRequested()); + assertUpstreamTypeRequested(TYPE_MOBILE_HIPRI); + assertFalse(isDunRequested()); + + mUNM.stop(); + assertFalse(mUNM.mobileNetworkRequested()); + assertTrue(mCM.hasNoCallbacks()); + } + + @Test + public void testDuplicateMobileRequestsIgnored() throws Exception { + assertFalse(mUNM.mobileNetworkRequested()); + assertEquals(0, mCM.mRequested.size()); + + mUNM.startObserveAllNetworks(); + verify(mCM, times(1)).registerNetworkCallback( + any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class)); + assertFalse(mUNM.mobileNetworkRequested()); + assertEquals(0, mCM.mRequested.size()); + + mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */); + mUNM.setTryCell(true); + verify(mCM, times(1)).requestNetwork( + any(NetworkRequest.class), anyInt(), anyInt(), any(Handler.class), + any(NetworkCallback.class)); + + assertTrue(mUNM.mobileNetworkRequested()); + assertUpstreamTypeRequested(TYPE_MOBILE_DUN); + assertTrue(isDunRequested()); + + // Try a few things that must not result in any state change. + mUNM.setTryCell(true); + mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */); + mUNM.setTryCell(true); + + assertTrue(mUNM.mobileNetworkRequested()); + assertUpstreamTypeRequested(TYPE_MOBILE_DUN); + assertTrue(isDunRequested()); + + mUNM.stop(); + verify(mCM, times(2)).unregisterNetworkCallback(any(NetworkCallback.class)); + + verifyNoMoreInteractions(mCM); + } + + @Test + public void testRequestsDunNetwork() throws Exception { + assertFalse(mUNM.mobileNetworkRequested()); + assertEquals(0, mCM.mRequested.size()); + + mUNM.startObserveAllNetworks(); + assertFalse(mUNM.mobileNetworkRequested()); + assertEquals(0, mCM.mRequested.size()); + + mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */); + assertFalse(mUNM.mobileNetworkRequested()); + assertEquals(0, mCM.mRequested.size()); + + mUNM.setTryCell(true); + assertTrue(mUNM.mobileNetworkRequested()); + assertUpstreamTypeRequested(TYPE_MOBILE_DUN); + assertTrue(isDunRequested()); + + mUNM.stop(); + assertFalse(mUNM.mobileNetworkRequested()); + assertTrue(mCM.hasNoCallbacks()); + } + + @Test + public void testUpdateMobileRequiresDun() throws Exception { + mUNM.startObserveAllNetworks(); + + // Test going from no-DUN to DUN correctly re-registers callbacks. + mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */); + mUNM.setTryCell(true); + assertTrue(mUNM.mobileNetworkRequested()); + assertUpstreamTypeRequested(TYPE_MOBILE_HIPRI); + assertFalse(isDunRequested()); + mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */); + assertTrue(mUNM.mobileNetworkRequested()); + assertUpstreamTypeRequested(TYPE_MOBILE_DUN); + assertTrue(isDunRequested()); + + // Test going from DUN to no-DUN correctly re-registers callbacks. + mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */); + assertTrue(mUNM.mobileNetworkRequested()); + assertUpstreamTypeRequested(TYPE_MOBILE_HIPRI); + assertFalse(isDunRequested()); + + mUNM.stop(); + assertFalse(mUNM.mobileNetworkRequested()); + } + + @Test + public void testSelectPreferredUpstreamType() throws Exception { + final Collection preferredTypes = new ArrayList<>(); + preferredTypes.add(TYPE_WIFI); + + mUNM.startTrackDefaultNetwork(mEntitleMgr); + mUNM.startObserveAllNetworks(); + // There are no networks, so there is nothing to select. + assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes)); + + final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES); + wifiAgent.fakeConnect(); + mLooper.dispatchAll(); + // WiFi is up, we should prefer it. + assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes)); + wifiAgent.fakeDisconnect(); + mLooper.dispatchAll(); + // There are no networks, so there is nothing to select. + assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes)); + + final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES); + cellAgent.fakeConnect(); + mLooper.dispatchAll(); + assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes)); + + preferredTypes.add(TYPE_MOBILE_DUN); + // This is coupled with preferred types in TetheringConfiguration. + mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */); + // DUN is available, but only use regular cell: no upstream selected. + assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes)); + preferredTypes.remove(TYPE_MOBILE_DUN); + // No WiFi, but our preferred flavour of cell is up. + preferredTypes.add(TYPE_MOBILE_HIPRI); + // This is coupled with preferred types in TetheringConfiguration. + mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */); + assertSatisfiesLegacyType(TYPE_MOBILE_HIPRI, + mUNM.selectPreferredUpstreamType(preferredTypes)); + // mobile is not permitted, we should not use HIPRI. + when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(false); + assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes)); + when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true); + assertSatisfiesLegacyType(TYPE_MOBILE_HIPRI, + mUNM.selectPreferredUpstreamType(preferredTypes)); + + wifiAgent.fakeConnect(); + mLooper.dispatchAll(); + // WiFi is up, and we should prefer it over cell. + assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes)); + + preferredTypes.remove(TYPE_MOBILE_HIPRI); + preferredTypes.add(TYPE_MOBILE_DUN); + // This is coupled with preferred types in TetheringConfiguration. + mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */); + assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes)); + + final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, DUN_CAPABILITIES); + dunAgent.fakeConnect(); + mLooper.dispatchAll(); + + // WiFi is still preferred. + assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes)); + + // WiFi goes down, cell and DUN are still up but only DUN is preferred. + wifiAgent.fakeDisconnect(); + mLooper.dispatchAll(); + assertSatisfiesLegacyType(TYPE_MOBILE_DUN, + mUNM.selectPreferredUpstreamType(preferredTypes)); + // mobile is not permitted, we should not use DUN. + when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(false); + assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes)); + // mobile change back to permitted, DUN should come back + when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true); + assertSatisfiesLegacyType(TYPE_MOBILE_DUN, + mUNM.selectPreferredUpstreamType(preferredTypes)); + } + + @Test + public void testGetCurrentPreferredUpstream() throws Exception { + mUNM.startTrackDefaultNetwork(mEntitleMgr); + mUNM.startObserveAllNetworks(); + mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */); + mUNM.setTryCell(true); + + // [0] Mobile connects, DUN not required -> mobile selected. + final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES); + cellAgent.fakeConnect(); + mCM.makeDefaultNetwork(cellAgent); + mLooper.dispatchAll(); + assertEquals(cellAgent.networkId, mUNM.getCurrentPreferredUpstream().network); + assertEquals(0, mCM.mRequested.size()); + + // [1] Mobile connects but not permitted -> null selected + when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(false); + assertEquals(null, mUNM.getCurrentPreferredUpstream()); + when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true); + assertEquals(0, mCM.mRequested.size()); + + // [2] WiFi connects but not validated/promoted to default -> mobile selected. + final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES); + wifiAgent.fakeConnect(); + mLooper.dispatchAll(); + assertEquals(cellAgent.networkId, mUNM.getCurrentPreferredUpstream().network); + assertEquals(0, mCM.mRequested.size()); + + // [3] WiFi validates and is promoted to the default network -> WiFi selected. + mCM.makeDefaultNetwork(wifiAgent); + mLooper.dispatchAll(); + assertEquals(wifiAgent.networkId, mUNM.getCurrentPreferredUpstream().network); + assertEquals(0, mCM.mRequested.size()); + + // [4] DUN required, no other changes -> WiFi still selected + mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */); + assertEquals(wifiAgent.networkId, mUNM.getCurrentPreferredUpstream().network); + assertEquals(1, mCM.mRequested.size()); + assertTrue(isDunRequested()); + + // [5] WiFi no longer validated, mobile becomes default, DUN required -> null selected. + mCM.makeDefaultNetwork(cellAgent); + mLooper.dispatchAll(); + assertEquals(null, mUNM.getCurrentPreferredUpstream()); + assertEquals(1, mCM.mRequested.size()); + assertTrue(isDunRequested()); + + // [6] DUN network arrives -> DUN selected + final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES); + dunAgent.networkCapabilities.addCapability(NET_CAPABILITY_DUN); + dunAgent.networkCapabilities.removeCapability(NET_CAPABILITY_INTERNET); + dunAgent.fakeConnect(); + mLooper.dispatchAll(); + assertEquals(dunAgent.networkId, mUNM.getCurrentPreferredUpstream().network); + assertEquals(1, mCM.mRequested.size()); + + // [7] Mobile is not permitted -> null selected + when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(false); + assertEquals(null, mUNM.getCurrentPreferredUpstream()); + assertEquals(1, mCM.mRequested.size()); + + // [7] Mobile is permitted again -> DUN selected + when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true); + assertEquals(dunAgent.networkId, mUNM.getCurrentPreferredUpstream().network); + assertEquals(1, mCM.mRequested.size()); + + // [8] DUN no longer required -> request is withdrawn + mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */); + assertEquals(0, mCM.mRequested.size()); + assertFalse(isDunRequested()); + } + + @Test + public void testLocalPrefixes() throws Exception { + mUNM.startTrackDefaultNetwork(mEntitleMgr); + mUNM.startObserveAllNetworks(); + + // [0] Test minimum set of local prefixes. + Set local = mUNM.getLocalPrefixes(); + assertTrue(local.isEmpty()); + + final Set alreadySeen = new HashSet<>(); + + // [1] Pretend Wi-Fi connects. + final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES); + final LinkProperties wifiLp = wifiAgent.linkProperties; + wifiLp.setInterfaceName("wlan0"); + final String[] wifi_addrs = { + "fe80::827a:bfff:fe6f:374d", "100.112.103.18", + "2001:db8:4:fd00:827a:bfff:fe6f:374d", + "2001:db8:4:fd00:6dea:325a:fdae:4ef4", + "fd6a:a640:60bf:e985::123", // ULA address for good measure. + }; + for (String addrStr : wifi_addrs) { + final String cidr = addrStr.contains(":") ? "/64" : "/20"; + wifiLp.addLinkAddress(new LinkAddress(addrStr + cidr)); + } + wifiAgent.fakeConnect(); + wifiAgent.sendLinkProperties(); + mLooper.dispatchAll(); + + local = mUNM.getLocalPrefixes(); + assertPrefixSet(local, INCLUDES, alreadySeen); + final String[] wifiLinkPrefixes = { + // Link-local prefixes are excluded and dealt with elsewhere. + "100.112.96.0/20", "2001:db8:4:fd00::/64", "fd6a:a640:60bf:e985::/64", + }; + assertPrefixSet(local, INCLUDES, wifiLinkPrefixes); + Collections.addAll(alreadySeen, wifiLinkPrefixes); + assertEquals(alreadySeen.size(), local.size()); + + // [2] Pretend mobile connects. + final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES); + final LinkProperties cellLp = cellAgent.linkProperties; + cellLp.setInterfaceName("rmnet_data0"); + final String[] cell_addrs = { + "10.102.211.48", "2001:db8:0:1:b50e:70d9:10c9:433d", + }; + for (String addrStr : cell_addrs) { + final String cidr = addrStr.contains(":") ? "/64" : "/27"; + cellLp.addLinkAddress(new LinkAddress(addrStr + cidr)); + } + cellAgent.fakeConnect(); + cellAgent.sendLinkProperties(); + mLooper.dispatchAll(); + + local = mUNM.getLocalPrefixes(); + assertPrefixSet(local, INCLUDES, alreadySeen); + final String[] cellLinkPrefixes = { "10.102.211.32/27", "2001:db8:0:1::/64" }; + assertPrefixSet(local, INCLUDES, cellLinkPrefixes); + Collections.addAll(alreadySeen, cellLinkPrefixes); + assertEquals(alreadySeen.size(), local.size()); + + // [3] Pretend DUN connects. + final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, DUN_CAPABILITIES); + final LinkProperties dunLp = dunAgent.linkProperties; + dunLp.setInterfaceName("rmnet_data1"); + final String[] dun_addrs = { + "192.0.2.48", "2001:db8:1:2:b50e:70d9:10c9:433d", + }; + for (String addrStr : dun_addrs) { + final String cidr = addrStr.contains(":") ? "/64" : "/27"; + dunLp.addLinkAddress(new LinkAddress(addrStr + cidr)); + } + dunAgent.fakeConnect(); + dunAgent.sendLinkProperties(); + mLooper.dispatchAll(); + + local = mUNM.getLocalPrefixes(); + assertPrefixSet(local, INCLUDES, alreadySeen); + final String[] dunLinkPrefixes = { "192.0.2.32/27", "2001:db8:1:2::/64" }; + assertPrefixSet(local, INCLUDES, dunLinkPrefixes); + Collections.addAll(alreadySeen, dunLinkPrefixes); + assertEquals(alreadySeen.size(), local.size()); + + // [4] Pretend Wi-Fi disconnected. It's addresses/prefixes should no + // longer be included (should be properly removed). + wifiAgent.fakeDisconnect(); + mLooper.dispatchAll(); + local = mUNM.getLocalPrefixes(); + assertPrefixSet(local, EXCLUDES, wifiLinkPrefixes); + assertPrefixSet(local, INCLUDES, cellLinkPrefixes); + assertPrefixSet(local, INCLUDES, dunLinkPrefixes); + + // [5] Pretend mobile disconnected. + cellAgent.fakeDisconnect(); + mLooper.dispatchAll(); + local = mUNM.getLocalPrefixes(); + assertPrefixSet(local, EXCLUDES, wifiLinkPrefixes); + assertPrefixSet(local, EXCLUDES, cellLinkPrefixes); + assertPrefixSet(local, INCLUDES, dunLinkPrefixes); + + // [6] Pretend DUN disconnected. + dunAgent.fakeDisconnect(); + mLooper.dispatchAll(); + local = mUNM.getLocalPrefixes(); + assertTrue(local.isEmpty()); + } + + @Test + public void testSelectMobileWhenMobileIsNotDefault() { + final Collection preferredTypes = new ArrayList<>(); + // Mobile has higher pirority than wifi. + preferredTypes.add(TYPE_MOBILE_HIPRI); + preferredTypes.add(TYPE_WIFI); + mUNM.startTrackDefaultNetwork(mEntitleMgr); + mUNM.startObserveAllNetworks(); + // Setup wifi and make wifi as default network. + final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES); + wifiAgent.fakeConnect(); + mCM.makeDefaultNetwork(wifiAgent); + // Setup mobile network. + final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES); + cellAgent.fakeConnect(); + mLooper.dispatchAll(); + + assertSatisfiesLegacyType(TYPE_MOBILE_HIPRI, + mUNM.selectPreferredUpstreamType(preferredTypes)); + verify(mEntitleMgr, times(1)).maybeRunProvisioning(); + } + + private void assertSatisfiesLegacyType(int legacyType, UpstreamNetworkState ns) { + if (legacyType == TYPE_NONE) { + assertTrue(ns == null); + return; + } + + final NetworkCapabilities nc = + UpstreamNetworkMonitor.networkCapabilitiesForType(legacyType); + assertTrue(nc.satisfiedByNetworkCapabilities(ns.networkCapabilities)); + } + + private void assertUpstreamTypeRequested(int upstreamType) throws Exception { + assertEquals(1, mCM.mRequested.size()); + assertEquals(1, mCM.mLegacyTypeMap.size()); + assertEquals(Integer.valueOf(upstreamType), + mCM.mLegacyTypeMap.values().iterator().next()); + } + + private boolean isDunRequested() { + for (NetworkRequestInfo nri : mCM.mRequested.values()) { + if (nri.request.networkCapabilities.hasCapability(NET_CAPABILITY_DUN)) { + return true; + } + } + return false; + } + + public static class TestStateMachine extends StateMachine { + public final ArrayList messages = new ArrayList<>(); + private final State mLoggingState = new LoggingState(); + + class LoggingState extends State { + @Override public void enter() { + messages.clear(); + } + + @Override public void exit() { + messages.clear(); + } + + @Override public boolean processMessage(Message msg) { + messages.add(msg); + return true; + } + } + + public TestStateMachine(Looper looper) { + super("UpstreamNetworkMonitor.TestStateMachine", looper); + addState(mLoggingState); + setInitialState(mLoggingState); + super.start(); + } + } + + static void assertPrefixSet(Set prefixes, boolean expectation, String... expected) { + final Set expectedSet = new HashSet<>(); + Collections.addAll(expectedSet, expected); + assertPrefixSet(prefixes, expectation, expectedSet); + } + + static void assertPrefixSet(Set prefixes, boolean expectation, Set expected) { + for (String expectedPrefix : expected) { + final String errStr = expectation ? "did not find" : "found"; + assertEquals( + String.format("Failed expectation: %s prefix: %s", errStr, expectedPrefix), + expectation, prefixes.contains(new IpPrefix(expectedPrefix))); + } + } +} diff --git a/framework/Android.bp b/framework/Android.bp index 6eb83484cc..ee71e154dc 100644 --- a/framework/Android.bp +++ b/framework/Android.bp @@ -16,11 +16,7 @@ package { // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], + default_applicable_licenses: ["Android-Apache-2.0"], } filegroup { diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt index 6c454bcd4c..7fc038245d 100644 --- a/framework/api/module-lib-current.txt +++ b/framework/api/module-lib-current.txt @@ -48,7 +48,6 @@ package android.net { public class ConnectivitySettingsManager { method public static void clearGlobalProxy(@NonNull android.content.Context); - method @NonNull public static java.util.Set getAppsAllowedOnRestrictedNetworks(@NonNull android.content.Context); method @Nullable public static String getCaptivePortalHttpUrl(@NonNull android.content.Context); method public static int getCaptivePortalMode(@NonNull android.content.Context, int); method @NonNull public static java.time.Duration getConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration); @@ -66,9 +65,9 @@ package android.net { method @NonNull public static String getPrivateDnsDefaultMode(@NonNull android.content.Context); method @Nullable public static String getPrivateDnsHostname(@NonNull android.content.Context); method public static int getPrivateDnsMode(@NonNull android.content.Context); + method @NonNull public static java.util.Set getUidsAllowedOnRestrictedNetworks(@NonNull android.content.Context); method public static boolean getWifiAlwaysRequested(@NonNull android.content.Context, boolean); method @NonNull public static java.time.Duration getWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration); - method public static void setAppsAllowedOnRestrictedNetworks(@NonNull android.content.Context, @NonNull java.util.Set); method public static void setCaptivePortalHttpUrl(@NonNull android.content.Context, @Nullable String); method public static void setCaptivePortalMode(@NonNull android.content.Context, int); method public static void setConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration); @@ -86,6 +85,7 @@ package android.net { method public static void setPrivateDnsDefaultMode(@NonNull android.content.Context, @NonNull int); method public static void setPrivateDnsHostname(@NonNull android.content.Context, @Nullable String); method public static void setPrivateDnsMode(@NonNull android.content.Context, int); + method public static void setUidsAllowedOnRestrictedNetworks(@NonNull android.content.Context, @NonNull java.util.Set); method public static void setWifiAlwaysRequested(@NonNull android.content.Context, boolean); method public static void setWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration); field public static final int CAPTIVE_PORTAL_MODE_AVOID = 2; // 0x2 diff --git a/framework/lint-baseline.xml b/framework/lint-baseline.xml index df37ae8514..099202f97c 100644 --- a/framework/lint-baseline.xml +++ b/framework/lint-baseline.xml @@ -7,7 +7,7 @@ errorLine1=" ParseException pe = new ParseException(e.reason, e.getCause());" errorLine2=" ~~~~~~~~~~~~~~~~~~"> @@ -18,7 +18,7 @@ errorLine1=" protected class ActiveDataSubscriptionIdListener extends TelephonyCallback" errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -29,7 +29,7 @@ errorLine1=" implements TelephonyCallback.ActiveDataSubscriptionIdListener {" errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -40,7 +40,7 @@ errorLine1=" ctx.getSystemService(TelephonyManager.class).registerTelephonyCallback(" errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java index 93455bc800..3af90db6cd 100644 --- a/framework/src/android/net/ConnectivityManager.java +++ b/framework/src/android/net/ConnectivityManager.java @@ -4918,7 +4918,7 @@ public class ConnectivityManager { InetAddressCompat.clearDnsCache(); // Must flush socket pool as idle sockets will be bound to previous network and may // cause subsequent fetches to be performed on old network. - NetworkEventDispatcher.getInstance().onNetworkConfigurationChanged(); + NetworkEventDispatcher.getInstance().dispatchNetworkConfigurationChange(); } return true; diff --git a/framework/src/android/net/ConnectivitySettingsManager.java b/framework/src/android/net/ConnectivitySettingsManager.java index 1a690991ca..ae1a8a0f71 100644 --- a/framework/src/android/net/ConnectivitySettingsManager.java +++ b/framework/src/android/net/ConnectivitySettingsManager.java @@ -374,12 +374,12 @@ public class ConnectivitySettingsManager { private static final String PRIVATE_DNS_MODE_PROVIDER_HOSTNAME_STRING = "hostname"; /** - * A list of apps that is allowed on restricted networks. + * A list of uids that is allowed to use restricted networks. * * @hide */ - public static final String APPS_ALLOWED_ON_RESTRICTED_NETWORKS = - "apps_allowed_on_restricted_networks"; + public static final String UIDS_ALLOWED_ON_RESTRICTED_NETWORKS = + "uids_allowed_on_restricted_networks"; /** * Get mobile data activity timeout from {@link Settings}. @@ -1003,6 +1003,28 @@ public class ConnectivitySettingsManager { context.getContentResolver(), NETWORK_METERED_MULTIPATH_PREFERENCE, preference); } + private static Set getUidSetFromString(@Nullable String uidList) { + final Set uids = new ArraySet<>(); + if (TextUtils.isEmpty(uidList)) { + return uids; + } + for (String uid : uidList.split(";")) { + uids.add(Integer.valueOf(uid)); + } + return uids; + } + + private static String getUidStringFromSet(@NonNull Set uidList) { + final StringJoiner joiner = new StringJoiner(";"); + for (Integer uid : uidList) { + if (uid < 0 || UserHandle.getAppId(uid) > Process.LAST_APPLICATION_UID) { + throw new IllegalArgumentException("Invalid uid"); + } + joiner.add(uid.toString()); + } + return joiner.toString(); + } + /** * Get the list of uids(from {@link Settings}) that should go on cellular networks in preference * even when higher-priority networks are connected. @@ -1015,14 +1037,7 @@ public class ConnectivitySettingsManager { public static Set getMobileDataPreferredUids(@NonNull Context context) { final String uidList = Settings.Secure.getString( context.getContentResolver(), MOBILE_DATA_PREFERRED_UIDS); - final Set uids = new ArraySet<>(); - if (TextUtils.isEmpty(uidList)) { - return uids; - } - for (String uid : uidList.split(";")) { - uids.add(Integer.valueOf(uid)); - } - return uids; + return getUidSetFromString(uidList); } /** @@ -1035,53 +1050,41 @@ public class ConnectivitySettingsManager { */ public static void setMobileDataPreferredUids(@NonNull Context context, @NonNull Set uidList) { - final StringJoiner joiner = new StringJoiner(";"); - for (Integer uid : uidList) { - if (uid < 0 || UserHandle.getAppId(uid) > Process.LAST_APPLICATION_UID) { - throw new IllegalArgumentException("Invalid uid"); - } - joiner.add(uid.toString()); - } - Settings.Secure.putString( - context.getContentResolver(), MOBILE_DATA_PREFERRED_UIDS, joiner.toString()); + final String uids = getUidStringFromSet(uidList); + Settings.Secure.putString(context.getContentResolver(), MOBILE_DATA_PREFERRED_UIDS, uids); } /** - * Get the list of apps(from {@link Settings}) that is allowed on restricted networks. + * Get the list of uids (from {@link Settings}) allowed to use restricted networks. + * + * Access to restricted networks is controlled by the (preinstalled-only) + * CONNECTIVITY_USE_RESTRICTED_NETWORKS permission, but highly privileged + * callers can also set a list of uids that can access restricted networks. + * + * This is useful for example in some jurisdictions where government apps, + * that can't be preinstalled, must still have access to emergency services. * * @param context The {@link Context} to query the setting. - * @return A list of apps that is allowed on restricted networks or null if no setting + * @return A list of uids that is allowed to use restricted networks or null if no setting * value. */ @NonNull - public static Set getAppsAllowedOnRestrictedNetworks(@NonNull Context context) { - final String appList = Settings.Secure.getString( - context.getContentResolver(), APPS_ALLOWED_ON_RESTRICTED_NETWORKS); - if (TextUtils.isEmpty(appList)) { - return new ArraySet<>(); - } - return new ArraySet<>(appList.split(";")); + public static Set getUidsAllowedOnRestrictedNetworks(@NonNull Context context) { + final String uidList = Settings.Secure.getString( + context.getContentResolver(), UIDS_ALLOWED_ON_RESTRICTED_NETWORKS); + return getUidSetFromString(uidList); } /** - * Set the list of apps(from {@link Settings}) that is allowed on restricted networks. - * - * Note: Please refer to android developer guidelines for valid app(package name). - * https://developer.android.com/guide/topics/manifest/manifest-element.html#package + * Set the list of uids(from {@link Settings}) that is allowed to use restricted networks. * * @param context The {@link Context} to set the setting. - * @param list A list of apps that is allowed on restricted networks. + * @param uidList A list of uids that is allowed to use restricted networks. */ - public static void setAppsAllowedOnRestrictedNetworks(@NonNull Context context, - @NonNull Set list) { - final StringJoiner joiner = new StringJoiner(";"); - for (String app : list) { - if (app == null || app.contains(";")) { - throw new IllegalArgumentException("Invalid app(package name)"); - } - joiner.add(app); - } - Settings.Secure.putString(context.getContentResolver(), APPS_ALLOWED_ON_RESTRICTED_NETWORKS, - joiner.toString()); + public static void setUidsAllowedOnRestrictedNetworks(@NonNull Context context, + @NonNull Set uidList) { + final String uids = getUidStringFromSet(uidList); + Settings.Secure.putString(context.getContentResolver(), UIDS_ALLOWED_ON_RESTRICTED_NETWORKS, + uids); } } diff --git a/service/Android.bp b/service/Android.bp index 72654260ae..28bcdcba89 100644 --- a/service/Android.bp +++ b/service/Android.bp @@ -16,11 +16,7 @@ package { // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], + default_applicable_licenses: ["Android-Apache-2.0"], } cc_library_shared { diff --git a/service/ServiceConnectivityResources/res/values-af/strings.xml b/service/ServiceConnectivityResources/res/values-af/strings.xml index 550ab8a65d..086c6e332c 100644 --- a/service/ServiceConnectivityResources/res/values-af/strings.xml +++ b/service/ServiceConnectivityResources/res/values-af/strings.xml @@ -17,27 +17,27 @@ - "Stelselkonnektiwiteithulpbronne" - "Meld aan by Wi-Fi-netwerk" - "Meld by netwerk aan" - + "Stelselkonnektiwiteithulpbronne" + "Meld aan by Wi-Fi-netwerk" + "Meld by netwerk aan" + - "%1$s het geen internettoegang nie" - "Tik vir opsies" - "Selnetwerk het nie internettoegang nie" - "Netwerk het nie internettoegang nie" - "Daar kan nie by private DNS-bediener ingegaan word nie" - "%1$s het beperkte konnektiwiteit" - "Tik om in elk geval te koppel" - "Het oorgeskakel na %1$s" - "Toestel gebruik %1$s wanneer %2$s geen internettoegang het nie. Heffings kan geld." - "Het oorgeskakel van %1$s na %2$s" + "%1$s het geen internettoegang nie" + "Tik vir opsies" + "Selnetwerk het nie internettoegang nie" + "Netwerk het nie internettoegang nie" + "Daar kan nie by private DNS-bediener ingegaan word nie" + "%1$s het beperkte konnektiwiteit" + "Tik om in elk geval te koppel" + "Het oorgeskakel na %1$s" + "Toestel gebruik %1$s wanneer %2$s geen internettoegang het nie. Heffings kan geld." + "Het oorgeskakel van %1$s na %2$s" - "mobiele data" - "Wi-fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobiele data" + "Wi-fi" + "Bluetooth" + "Ethernet" + "VPN" - "\'n onbekende netwerktipe" + "\'n onbekende netwerktipe" diff --git a/service/ServiceConnectivityResources/res/values-am/strings.xml b/service/ServiceConnectivityResources/res/values-am/strings.xml index 7f1a9dbe51..886b3533e8 100644 --- a/service/ServiceConnectivityResources/res/values-am/strings.xml +++ b/service/ServiceConnectivityResources/res/values-am/strings.xml @@ -17,27 +17,27 @@ - "የስርዓት ግንኙነት መርጃዎች" - "ወደ Wi-Fi አውታረ መረብ በመለያ ግባ" - "ወደ አውታረ መረብ በመለያ ይግቡ" - + "የስርዓት ግንኙነት መርጃዎች" + "ወደ Wi-Fi አውታረ መረብ በመለያ ግባ" + "ወደ አውታረ መረብ በመለያ ይግቡ" + - "%1$s ምንም የበይነ መረብ መዳረሻ የለም" - "ለአማራጮች መታ ያድርጉ" - "የተንቀሳቃሽ ስልክ አውታረ መረብ የበይነመረብ መዳረሻ የለውም" - "አውታረ መረብ የበይነመረብ መዳረሻ የለውም" - "የግል ዲኤንኤስ አገልጋይ ሊደረስበት አይችልም" - "%1$s የተገደበ ግንኙነት አለው" - "ለማንኛውም ለማገናኘት መታ ያድርጉ" - "ወደ %1$s ተቀይሯል" - "%2$s ምንም ዓይነት የበይነመረብ ግንኙነት በማይኖረው ጊዜ መሣሪያዎች %1$sን ይጠቀማሉ። ክፍያዎች ተፈጻሚ ሊሆኑ ይችላሉ።" - "ከ%1$s ወደ %2$s ተቀይሯል" + "%1$s ምንም የበይነ መረብ መዳረሻ የለም" + "ለአማራጮች መታ ያድርጉ" + "የተንቀሳቃሽ ስልክ አውታረ መረብ የበይነመረብ መዳረሻ የለውም" + "አውታረ መረብ የበይነመረብ መዳረሻ የለውም" + "የግል ዲኤንኤስ አገልጋይ ሊደረስበት አይችልም" + "%1$s የተገደበ ግንኙነት አለው" + "ለማንኛውም ለማገናኘት መታ ያድርጉ" + "ወደ %1$s ተቀይሯል" + "%2$s ምንም ዓይነት የበይነመረብ ግንኙነት በማይኖረው ጊዜ መሣሪያዎች %1$sን ይጠቀማሉ። ክፍያዎች ተፈጻሚ ሊሆኑ ይችላሉ።" + "ከ%1$s ወደ %2$s ተቀይሯል" - "የተንቀሳቃሽ ስልክ ውሂብ" - "Wi-Fi" - "ብሉቱዝ" - "ኢተርኔት" - "VPN" + "የተንቀሳቃሽ ስልክ ውሂብ" + "Wi-Fi" + "ብሉቱዝ" + "ኢተርኔት" + "VPN" - "አንድ ያልታወቀ አውታረ መረብ ዓይነት" + "አንድ ያልታወቀ አውታረ መረብ ዓይነት" diff --git a/service/ServiceConnectivityResources/res/values-ar/strings.xml b/service/ServiceConnectivityResources/res/values-ar/strings.xml index b7a62c5e1d..07d9c2eef9 100644 --- a/service/ServiceConnectivityResources/res/values-ar/strings.xml +++ b/service/ServiceConnectivityResources/res/values-ar/strings.xml @@ -17,27 +17,27 @@ - "مصادر إمكانية اتصال الخادم" - "‏تسجيل الدخول إلى شبكة Wi-Fi" - "تسجيل الدخول إلى الشبكة" - + "مصادر إمكانية اتصال الخادم" + "‏تسجيل الدخول إلى شبكة Wi-Fi" + "تسجيل الدخول إلى الشبكة" + - "لا يتوفّر في %1$s إمكانية الاتصال بالإنترنت." - "انقر للحصول على الخيارات." - "شبكة الجوّال هذه غير متصلة بالإنترنت" - "الشبكة غير متصلة بالإنترنت" - "لا يمكن الوصول إلى خادم أسماء نظام نطاقات خاص" - "إمكانية اتصال %1$s محدودة." - "يمكنك النقر للاتصال على أي حال." - "تم التبديل إلى %1$s" - "يستخدم الجهاز %1$s عندما لا يتوفر اتصال بالإنترنت في شبكة %2$s، ويمكن أن يتم فرض رسوم مقابل ذلك." - "تم التبديل من %1$s إلى %2$s" + "لا يتوفّر في %1$s إمكانية الاتصال بالإنترنت." + "انقر للحصول على الخيارات." + "شبكة الجوّال هذه غير متصلة بالإنترنت" + "الشبكة غير متصلة بالإنترنت" + "لا يمكن الوصول إلى خادم أسماء نظام نطاقات خاص" + "إمكانية اتصال %1$s محدودة." + "يمكنك النقر للاتصال على أي حال." + "تم التبديل إلى %1$s" + "يستخدم الجهاز %1$s عندما لا يتوفر اتصال بالإنترنت في شبكة %2$s، ويمكن أن يتم فرض رسوم مقابل ذلك." + "تم التبديل من %1$s إلى %2$s" - "بيانات الجوّال" - "Wi-Fi" - "بلوتوث" - "إيثرنت" - "‏شبكة افتراضية خاصة (VPN)" + "بيانات الجوّال" + "Wi-Fi" + "بلوتوث" + "إيثرنت" + "‏شبكة افتراضية خاصة (VPN)" - "نوع شبكة غير معروف" + "نوع شبكة غير معروف" diff --git a/service/ServiceConnectivityResources/res/values-as/strings.xml b/service/ServiceConnectivityResources/res/values-as/strings.xml index cf8e6ac9d4..e753cb3bf2 100644 --- a/service/ServiceConnectivityResources/res/values-as/strings.xml +++ b/service/ServiceConnectivityResources/res/values-as/strings.xml @@ -17,27 +17,27 @@ - "ছিষ্টেম সংযোগৰ উৎস" - "ৱাই-ফাই নেটৱৰ্কত ছাইন ইন কৰক" - "নেটৱৰ্কত ছাইন ইন কৰক" - + "ছিষ্টেম সংযোগৰ উৎস" + "ৱাই-ফাই নেটৱৰ্কত ছাইন ইন কৰক" + "নেটৱৰ্কত ছাইন ইন কৰক" + - "%1$sৰ ইণ্টাৰনেটৰ এক্সেছ নাই" - "অধিক বিকল্পৰ বাবে টিপক" - "ম’বাইল নেটৱৰ্কৰ কোনো ইণ্টাৰনেটৰ এক্সেছ নাই" - "নেটৱৰ্কৰ কোনো ইণ্টাৰনেটৰ এক্সেছ নাই" - "ব্যক্তিগত DNS ছাৰ্ভাৰ এক্সেছ কৰিব নোৱাৰি" - "%1$sৰ সকলো সেৱাৰ এক্সেছ নাই" - "যিকোনো প্ৰকাৰে সংযোগ কৰিবলৈ টিপক" - "%1$sলৈ সলনি কৰা হ’ল" - "যেতিয়া %2$sত ইণ্টাৰনেট নাথাকে, তেতিয়া ডিভাইচে %1$sক ব্যৱহাৰ কৰে। মাচুল প্ৰযোজ্য হ\'ব পাৰে।" - "%1$sৰ পৰা %2$s লৈ সলনি কৰা হ’ল" + "%1$sৰ ইণ্টাৰনেটৰ এক্সেছ নাই" + "অধিক বিকল্পৰ বাবে টিপক" + "ম’বাইল নেটৱৰ্কৰ কোনো ইণ্টাৰনেটৰ এক্সেছ নাই" + "নেটৱৰ্কৰ কোনো ইণ্টাৰনেটৰ এক্সেছ নাই" + "ব্যক্তিগত DNS ছাৰ্ভাৰ এক্সেছ কৰিব নোৱাৰি" + "%1$sৰ সকলো সেৱাৰ এক্সেছ নাই" + "যিকোনো প্ৰকাৰে সংযোগ কৰিবলৈ টিপক" + "%1$sলৈ সলনি কৰা হ’ল" + "যেতিয়া %2$sত ইণ্টাৰনেট নাথাকে, তেতিয়া ডিভাইচে %1$sক ব্যৱহাৰ কৰে। মাচুল প্ৰযোজ্য হ\'ব পাৰে।" + "%1$sৰ পৰা %2$s লৈ সলনি কৰা হ’ল" - "ম’বাইল ডেটা" - "ৱাই-ফাই" - "ব্লুটুথ" - "ইথাৰনেট" - "ভিপিএন" + "ম’বাইল ডেটা" + "ৱাই-ফাই" + "ব্লুটুথ" + "ইথাৰনেট" + "ভিপিএন" - "অজ্ঞাত প্ৰকাৰৰ নেটৱৰ্ক" + "অজ্ঞাত প্ৰকাৰৰ নেটৱৰ্ক" diff --git a/service/ServiceConnectivityResources/res/values-az/strings.xml b/service/ServiceConnectivityResources/res/values-az/strings.xml index 7e927ed41b..f33a3e3569 100644 --- a/service/ServiceConnectivityResources/res/values-az/strings.xml +++ b/service/ServiceConnectivityResources/res/values-az/strings.xml @@ -17,27 +17,27 @@ - "Sistem Bağlantı Resursları" - "Wi-Fi şəbəkəsinə daxil ol" - "Şəbəkəyə daxil olun" - + "Sistem Bağlantı Resursları" + "Wi-Fi şəbəkəsinə daxil ol" + "Şəbəkəyə daxil olun" + - "%1$s üçün internet girişi əlçatan deyil" - "Seçimlər üçün tıklayın" - "Mobil şəbəkənin internetə girişi yoxdur" - "Şəbəkənin internetə girişi yoxdur" - "Özəl DNS serverinə giriş mümkün deyil" - "%1$s bağlantını məhdudlaşdırdı" - "İstənilən halda klikləyin" - "%1$s şəbəkə növünə keçirildi" - "%2$s şəbəkəsinin internetə girişi olmadıqda, cihaz %1$s şəbəkəsini istifadə edir. Xidmət haqqı tutula bilər." - "%1$s şəbəkəsindən %2$s şəbəkəsinə keçirildi" + "%1$s üçün internet girişi əlçatan deyil" + "Seçimlər üçün tıklayın" + "Mobil şəbəkənin internetə girişi yoxdur" + "Şəbəkənin internetə girişi yoxdur" + "Özəl DNS serverinə giriş mümkün deyil" + "%1$s bağlantını məhdudlaşdırdı" + "İstənilən halda klikləyin" + "%1$s şəbəkə növünə keçirildi" + "%2$s şəbəkəsinin internetə girişi olmadıqda, cihaz %1$s şəbəkəsini istifadə edir. Xidmət haqqı tutula bilər." + "%1$s şəbəkəsindən %2$s şəbəkəsinə keçirildi" - "mobil data" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobil data" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "naməlum şəbəkə növü" + "naməlum şəbəkə növü" diff --git a/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml b/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml index 3f1b976fb9..7398e7cf79 100644 --- a/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml +++ b/service/ServiceConnectivityResources/res/values-b+sr+Latn/strings.xml @@ -17,27 +17,27 @@ - "Resursi za povezivanje sa sistemom" - "Prijavljivanje na WiFi mrežu" - "Prijavite se na mrežu" - + "Resursi za povezivanje sa sistemom" + "Prijavljivanje na WiFi mrežu" + "Prijavite se na mrežu" + - "%1$s nema pristup internetu" - "Dodirnite za opcije" - "Mobilna mreža nema pristup internetu" - "Mreža nema pristup internetu" - "Pristup privatnom DNS serveru nije uspeo" - "%1$s ima ograničenu vezu" - "Dodirnite da biste se ipak povezali" - "Prešli ste na tip mreže %1$s" - "Uređaj koristi tip mreže %1$s kada tip mreže %2$s nema pristup internetu. Možda će se naplaćivati troškovi." - "Prešli ste sa tipa mreže %1$s na tip mreže %2$s" + "%1$s nema pristup internetu" + "Dodirnite za opcije" + "Mobilna mreža nema pristup internetu" + "Mreža nema pristup internetu" + "Pristup privatnom DNS serveru nije uspeo" + "%1$s ima ograničenu vezu" + "Dodirnite da biste se ipak povezali" + "Prešli ste na tip mreže %1$s" + "Uređaj koristi tip mreže %1$s kada tip mreže %2$s nema pristup internetu. Možda će se naplaćivati troškovi." + "Prešli ste sa tipa mreže %1$s na tip mreže %2$s" - "mobilni podaci" - "WiFi" - "Bluetooth" - "Eternet" - "VPN" + "mobilni podaci" + "WiFi" + "Bluetooth" + "Eternet" + "VPN" - "nepoznat tip mreže" + "nepoznat tip mreže" diff --git a/service/ServiceConnectivityResources/res/values-be/strings.xml b/service/ServiceConnectivityResources/res/values-be/strings.xml index 21edf24054..3459cc7c7c 100644 --- a/service/ServiceConnectivityResources/res/values-be/strings.xml +++ b/service/ServiceConnectivityResources/res/values-be/strings.xml @@ -17,27 +17,27 @@ - "Рэсурсы для падключэння да сістэмы" - "Уваход у сетку Wi-Fi" - "Увайдзіце ў сетку" - + "Рэсурсы для падключэння да сістэмы" + "Уваход у сетку Wi-Fi" + "Увайдзіце ў сетку" + - "%1$s не мае доступу ў інтэрнэт" - "Дакраніцеся, каб убачыць параметры" - "Мабільная сетка не мае доступу ў інтэрнэт" - "Сетка не мае доступу ў інтэрнэт" - "Не ўдалося атрымаць доступ да прыватнага DNS-сервера" - "%1$s мае абмежаваную магчымасць падключэння" - "Націсніце, каб падключыцца" - "Выкананы пераход да %1$s" - "Прылада выкарыстоўвае сетку %1$s, калі ў сетцы %2$s няма доступу да інтэрнэту. Можа спаганяцца плата." - "Выкананы пераход з %1$s да %2$s" + "%1$s не мае доступу ў інтэрнэт" + "Дакраніцеся, каб убачыць параметры" + "Мабільная сетка не мае доступу ў інтэрнэт" + "Сетка не мае доступу ў інтэрнэт" + "Не ўдалося атрымаць доступ да прыватнага DNS-сервера" + "%1$s мае абмежаваную магчымасць падключэння" + "Націсніце, каб падключыцца" + "Выкананы пераход да %1$s" + "Прылада выкарыстоўвае сетку %1$s, калі ў сетцы %2$s няма доступу да інтэрнэту. Можа спаганяцца плата." + "Выкананы пераход з %1$s да %2$s" - "мабільная перадача даных" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "мабільная перадача даных" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "невядомы тып сеткі" + "невядомы тып сеткі" diff --git a/service/ServiceConnectivityResources/res/values-bg/strings.xml b/service/ServiceConnectivityResources/res/values-bg/strings.xml index c3c2d722f4..b4ae618a40 100644 --- a/service/ServiceConnectivityResources/res/values-bg/strings.xml +++ b/service/ServiceConnectivityResources/res/values-bg/strings.xml @@ -17,27 +17,27 @@ - "Ресурси за свързаността на системата" - "Влизане в Wi-Fi мрежа" - "Вход в мрежата" - + "Ресурси за свързаността на системата" + "Влизане в Wi-Fi мрежа" + "Вход в мрежата" + - "%1$s няма достъп до интернет" - "Докоснете за опции" - "Мобилната мрежа няма достъп до интернет" - "Мрежата няма достъп до интернет" - "Не може да се осъществи достъп до частния DNS сървър" - "%1$s има ограничена свързаност" - "Докоснете, за да се свържете въпреки това" - "Превключи се към %1$s" - "Устройството използва %1$s, когато %2$s няма достъп до интернет. Възможно е да бъдете таксувани." - "Превключи се от %1$s към %2$s" + "%1$s няма достъп до интернет" + "Докоснете за опции" + "Мобилната мрежа няма достъп до интернет" + "Мрежата няма достъп до интернет" + "Не може да се осъществи достъп до частния DNS сървър" + "%1$s има ограничена свързаност" + "Докоснете, за да се свържете въпреки това" + "Превключи се към %1$s" + "Устройството използва %1$s, когато %2$s няма достъп до интернет. Възможно е да бъдете таксувани." + "Превключи се от %1$s към %2$s" - "мобилни данни" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "мобилни данни" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "неизвестен тип мрежа" + "неизвестен тип мрежа" diff --git a/service/ServiceConnectivityResources/res/values-bn/strings.xml b/service/ServiceConnectivityResources/res/values-bn/strings.xml index 0f693bd60c..3b32973a40 100644 --- a/service/ServiceConnectivityResources/res/values-bn/strings.xml +++ b/service/ServiceConnectivityResources/res/values-bn/strings.xml @@ -17,27 +17,27 @@ - "সিস্টেম কানেক্টিভিটি রিসোর্সেস" - "ওয়াই-ফাই নেটওয়ার্কে সাইন-ইন করুন" - "নেটওয়ার্কে সাইন-ইন করুন" - + "সিস্টেম কানেক্টিভিটি রিসোর্সেস" + "ওয়াই-ফাই নেটওয়ার্কে সাইন-ইন করুন" + "নেটওয়ার্কে সাইন-ইন করুন" + - "%1$s-এর ইন্টারনেটে অ্যাক্সেস নেই" - "বিকল্পগুলির জন্য আলতো চাপুন" - "মোবাইল নেটওয়ার্কে কোনও ইন্টারনেট অ্যাক্সেস নেই" - "নেটওয়ার্কে কোনও ইন্টারনেট অ্যাক্সেস নেই" - "ব্যক্তিগত ডিএনএস সার্ভার অ্যাক্সেস করা যাবে না" - "%1$s-এর সীমিত কানেক্টিভিটি আছে" - "তবুও কানেক্ট করতে ট্যাপ করুন" - "%1$s এ পাল্টানো হয়েছে" - "%2$s এ ইন্টারনেট অ্যাক্সেস না থাকলে %1$s ব্যবহার করা হয়৷ ডেটা চার্জ প্রযোজ্য৷" - "%1$s থেকে %2$s এ পাল্টানো হয়েছে" + "%1$s-এর ইন্টারনেটে অ্যাক্সেস নেই" + "বিকল্পগুলির জন্য আলতো চাপুন" + "মোবাইল নেটওয়ার্কে কোনও ইন্টারনেট অ্যাক্সেস নেই" + "নেটওয়ার্কে কোনও ইন্টারনেট অ্যাক্সেস নেই" + "ব্যক্তিগত ডিএনএস সার্ভার অ্যাক্সেস করা যাবে না" + "%1$s-এর সীমিত কানেক্টিভিটি আছে" + "তবুও কানেক্ট করতে ট্যাপ করুন" + "%1$s এ পাল্টানো হয়েছে" + "%2$s এ ইন্টারনেট অ্যাক্সেস না থাকলে %1$s ব্যবহার করা হয়৷ ডেটা চার্জ প্রযোজ্য৷" + "%1$s থেকে %2$s এ পাল্টানো হয়েছে" - "মোবাইল ডেটা" - "ওয়াই-ফাই" - "ব্লুটুথ" - "ইথারনেট" - "VPN" + "মোবাইল ডেটা" + "ওয়াই-ফাই" + "ব্লুটুথ" + "ইথারনেট" + "VPN" - "এই নেটওয়ার্কের ধরন অজানা" + "এই নেটওয়ার্কের ধরন অজানা" diff --git a/service/ServiceConnectivityResources/res/values-bs/strings.xml b/service/ServiceConnectivityResources/res/values-bs/strings.xml index 33d6ed9147..0bc0a7c5ef 100644 --- a/service/ServiceConnectivityResources/res/values-bs/strings.xml +++ b/service/ServiceConnectivityResources/res/values-bs/strings.xml @@ -17,27 +17,27 @@ - "Izvori povezivosti sistema" - "Prijavljivanje na WiFi mrežu" - "Prijava na mrežu" - + "Izvori povezivosti sistema" + "Prijavljivanje na WiFi mrežu" + "Prijava na mrežu" + - "Mreža %1$s nema pristup internetu" - "Dodirnite za opcije" - "Mobilna mreža nema pristup internetu" - "Mreža nema pristup internetu" - "Nije moguće pristupiti privatnom DNS serveru" - "Mreža %1$s ima ograničenu povezivost" - "Dodirnite da se ipak povežete" - "Prebačeno na: %1$s" - "Kada %2$s nema pristup internetu, uređaj koristi mrežu %1$s. Moguća je naplata usluge." - "Prebačeno iz mreže %1$s u %2$s mrežu" + "Mreža %1$s nema pristup internetu" + "Dodirnite za opcije" + "Mobilna mreža nema pristup internetu" + "Mreža nema pristup internetu" + "Nije moguće pristupiti privatnom DNS serveru" + "Mreža %1$s ima ograničenu povezivost" + "Dodirnite da se ipak povežete" + "Prebačeno na: %1$s" + "Kada %2$s nema pristup internetu, uređaj koristi mrežu %1$s. Moguća je naplata usluge." + "Prebačeno iz mreže %1$s u %2$s mrežu" - "prijenos podataka na mobilnoj mreži" - "WiFi" - "Bluetooth" - "Ethernet" - "VPN" + "prijenos podataka na mobilnoj mreži" + "WiFi" + "Bluetooth" + "Ethernet" + "VPN" - "nepoznata vrsta mreže" + "nepoznata vrsta mreže" diff --git a/service/ServiceConnectivityResources/res/values-ca/strings.xml b/service/ServiceConnectivityResources/res/values-ca/strings.xml index 04f6bd211c..22b9dbd9e8 100644 --- a/service/ServiceConnectivityResources/res/values-ca/strings.xml +++ b/service/ServiceConnectivityResources/res/values-ca/strings.xml @@ -17,27 +17,27 @@ - "Recursos de connectivitat del sistema" - "Inicia la sessió a la xarxa Wi-Fi" - "Inicia la sessió a la xarxa" - + "Recursos de connectivitat del sistema" + "Inicia la sessió a la xarxa Wi-Fi" + "Inicia la sessió a la xarxa" + - "%1$s no té accés a Internet" - "Toca per veure les opcions" - "La xarxa mòbil no té accés a Internet" - "La xarxa no té accés a Internet" - "No es pot accedir al servidor DNS privat" - "%1$s té una connectivitat limitada" - "Toca per connectar igualment" - "Actualment en ús: %1$s" - "El dispositiu utilitza %1$s en cas que %2$s no tingui accés a Internet. És possible que s\'hi apliquin càrrecs." - "Abans es feia servir la xarxa %1$s; ara s\'utilitza %2$s" + "%1$s no té accés a Internet" + "Toca per veure les opcions" + "La xarxa mòbil no té accés a Internet" + "La xarxa no té accés a Internet" + "No es pot accedir al servidor DNS privat" + "%1$s té una connectivitat limitada" + "Toca per connectar igualment" + "Actualment en ús: %1$s" + "El dispositiu utilitza %1$s en cas que %2$s no tingui accés a Internet. És possible que s\'hi apliquin càrrecs." + "Abans es feia servir la xarxa %1$s; ara s\'utilitza %2$s" - "dades mòbils" - "Wi‑Fi" - "Bluetooth" - "Ethernet" - "VPN" + "dades mòbils" + "Wi‑Fi" + "Bluetooth" + "Ethernet" + "VPN" - "un tipus de xarxa desconegut" + "un tipus de xarxa desconegut" diff --git a/service/ServiceConnectivityResources/res/values-cs/strings.xml b/service/ServiceConnectivityResources/res/values-cs/strings.xml index 6309e788e6..ccf21ee179 100644 --- a/service/ServiceConnectivityResources/res/values-cs/strings.xml +++ b/service/ServiceConnectivityResources/res/values-cs/strings.xml @@ -17,27 +17,27 @@ - "Zdroje pro připojení systému" - "Přihlásit se k síti Wi-Fi" - "Přihlásit se k síti" - + "Zdroje pro připojení systému" + "Přihlásit se k síti Wi-Fi" + "Přihlásit se k síti" + - "Síť %1$s nemá přístup k internetu" - "Klepnutím zobrazíte možnosti" - "Mobilní síť nemá přístup k internetu" - "Síť nemá přístup k internetu" - "Nelze získat přístup k soukromému serveru DNS" - "Síť %1$s umožňuje jen omezené připojení" - "Klepnutím se i přesto připojíte" - "Přechod na síť %1$s" - "Když síť %2$s nebude mít přístup k internetu, zařízení použije síť %1$s. Mohou být účtovány poplatky." - "Přechod ze sítě %1$s na síť %2$s" + "Síť %1$s nemá přístup k internetu" + "Klepnutím zobrazíte možnosti" + "Mobilní síť nemá přístup k internetu" + "Síť nemá přístup k internetu" + "Nelze získat přístup k soukromému serveru DNS" + "Síť %1$s umožňuje jen omezené připojení" + "Klepnutím se i přesto připojíte" + "Přechod na síť %1$s" + "Když síť %2$s nebude mít přístup k internetu, zařízení použije síť %1$s. Mohou být účtovány poplatky." + "Přechod ze sítě %1$s na síť %2$s" - "mobilní data" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobilní data" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "neznámý typ sítě" + "neznámý typ sítě" diff --git a/service/ServiceConnectivityResources/res/values-da/strings.xml b/service/ServiceConnectivityResources/res/values-da/strings.xml index 57c58afc41..a33143e082 100644 --- a/service/ServiceConnectivityResources/res/values-da/strings.xml +++ b/service/ServiceConnectivityResources/res/values-da/strings.xml @@ -17,27 +17,27 @@ - "Systemets forbindelsesressourcer" - "Log ind på Wi-Fi-netværk" - "Log ind på netværk" - + "Systemets forbindelsesressourcer" + "Log ind på Wi-Fi-netværk" + "Log ind på netværk" + - "%1$s har ingen internetforbindelse" - "Tryk for at se valgmuligheder" - "Mobilnetværket har ingen internetadgang" - "Netværket har ingen internetadgang" - "Der er ikke adgang til den private DNS-server" - "%1$s har begrænset forbindelse" - "Tryk for at oprette forbindelse alligevel" - "Der blev skiftet til %1$s" - "Enheden benytter %1$s, når der ikke er internetadgang via %2$s. Der opkræves muligvis betaling." - "Der blev skiftet fra %1$s til %2$s" + "%1$s har ingen internetforbindelse" + "Tryk for at se valgmuligheder" + "Mobilnetværket har ingen internetadgang" + "Netværket har ingen internetadgang" + "Der er ikke adgang til den private DNS-server" + "%1$s har begrænset forbindelse" + "Tryk for at oprette forbindelse alligevel" + "Der blev skiftet til %1$s" + "Enheden benytter %1$s, når der ikke er internetadgang via %2$s. Der opkræves muligvis betaling." + "Der blev skiftet fra %1$s til %2$s" - "mobildata" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobildata" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "en ukendt netværkstype" + "en ukendt netværkstype" diff --git a/service/ServiceConnectivityResources/res/values-de/strings.xml b/service/ServiceConnectivityResources/res/values-de/strings.xml index d0c255197a..96cc7d2ef0 100644 --- a/service/ServiceConnectivityResources/res/values-de/strings.xml +++ b/service/ServiceConnectivityResources/res/values-de/strings.xml @@ -17,27 +17,27 @@ - "Systemverbindungsressourcen" - "In WLAN anmelden" - "Im Netzwerk anmelden" - + "Systemverbindungsressourcen" + "In WLAN anmelden" + "Im Netzwerk anmelden" + - "%1$s hat keinen Internetzugriff" - "Für Optionen tippen" - "Mobiles Netzwerk hat keinen Internetzugriff" - "Netzwerk hat keinen Internetzugriff" - "Auf den privaten DNS-Server kann nicht zugegriffen werden" - "Schlechte Verbindung mit %1$s" - "Tippen, um die Verbindung trotzdem herzustellen" - "Zu %1$s gewechselt" - "Auf dem Gerät werden %1$s genutzt, wenn über %2$s kein Internet verfügbar ist. Eventuell fallen Gebühren an." - "Von \"%1$s\" zu \"%2$s\" gewechselt" + "%1$s hat keinen Internetzugriff" + "Für Optionen tippen" + "Mobiles Netzwerk hat keinen Internetzugriff" + "Netzwerk hat keinen Internetzugriff" + "Auf den privaten DNS-Server kann nicht zugegriffen werden" + "Schlechte Verbindung mit %1$s" + "Tippen, um die Verbindung trotzdem herzustellen" + "Zu %1$s gewechselt" + "Auf dem Gerät werden %1$s genutzt, wenn über %2$s kein Internet verfügbar ist. Eventuell fallen Gebühren an." + "Von \"%1$s\" zu \"%2$s\" gewechselt" - "Mobile Daten" - "WLAN" - "Bluetooth" - "Ethernet" - "VPN" + "Mobile Daten" + "WLAN" + "Bluetooth" + "Ethernet" + "VPN" - "ein unbekannter Netzwerktyp" + "ein unbekannter Netzwerktyp" diff --git a/service/ServiceConnectivityResources/res/values-el/strings.xml b/service/ServiceConnectivityResources/res/values-el/strings.xml index 1c2838dfd4..b5f319d5ab 100644 --- a/service/ServiceConnectivityResources/res/values-el/strings.xml +++ b/service/ServiceConnectivityResources/res/values-el/strings.xml @@ -17,27 +17,27 @@ - "Πόροι συνδεσιμότητας συστήματος" - "Συνδεθείτε στο δίκτυο Wi-Fi" - "Σύνδεση στο δίκτυο" - + "Πόροι συνδεσιμότητας συστήματος" + "Συνδεθείτε στο δίκτυο Wi-Fi" + "Σύνδεση στο δίκτυο" + - "Η εφαρμογή %1$s δεν έχει πρόσβαση στο διαδίκτυο" - "Πατήστε για να δείτε τις επιλογές" - "Το δίκτυο κινητής τηλεφωνίας δεν έχει πρόσβαση στο διαδίκτυο." - "Το δίκτυο δεν έχει πρόσβαση στο διαδίκτυο." - "Δεν είναι δυνατή η πρόσβαση στον ιδιωτικό διακομιστή DNS." - "Το δίκτυο %1$s έχει περιορισμένη συνδεσιμότητα" - "Πατήστε για σύνδεση ούτως ή άλλως" - "Μετάβαση σε δίκτυο %1$s" - "Η συσκευή χρησιμοποιεί το δίκτυο %1$s όταν το δίκτυο %2$s δεν έχει πρόσβαση στο διαδίκτυο. Μπορεί να ισχύουν χρεώσεις." - "Μετάβαση από το δίκτυο %1$s στο δίκτυο %2$s" + "Η εφαρμογή %1$s δεν έχει πρόσβαση στο διαδίκτυο" + "Πατήστε για να δείτε τις επιλογές" + "Το δίκτυο κινητής τηλεφωνίας δεν έχει πρόσβαση στο διαδίκτυο." + "Το δίκτυο δεν έχει πρόσβαση στο διαδίκτυο." + "Δεν είναι δυνατή η πρόσβαση στον ιδιωτικό διακομιστή DNS." + "Το δίκτυο %1$s έχει περιορισμένη συνδεσιμότητα" + "Πατήστε για σύνδεση ούτως ή άλλως" + "Μετάβαση σε δίκτυο %1$s" + "Η συσκευή χρησιμοποιεί το δίκτυο %1$s όταν το δίκτυο %2$s δεν έχει πρόσβαση στο διαδίκτυο. Μπορεί να ισχύουν χρεώσεις." + "Μετάβαση από το δίκτυο %1$s στο δίκτυο %2$s" - "δεδομένα κινητής τηλεφωνίας" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "δεδομένα κινητής τηλεφωνίας" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "άγνωστος τύπος δικτύου" + "άγνωστος τύπος δικτύου" diff --git a/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml b/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml index db5ad7029b..c490cf8da4 100644 --- a/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml +++ b/service/ServiceConnectivityResources/res/values-en-rAU/strings.xml @@ -17,27 +17,27 @@ - "System connectivity resources" - "Sign in to a Wi-Fi network" - "Sign in to network" - + "System connectivity resources" + "Sign in to a Wi-Fi network" + "Sign in to network" + - "%1$s has no Internet access" - "Tap for options" - "Mobile network has no Internet access" - "Network has no Internet access" - "Private DNS server cannot be accessed" - "%1$s has limited connectivity" - "Tap to connect anyway" - "Switched to %1$s" - "Device uses %1$s when %2$s has no Internet access. Charges may apply." - "Switched from %1$s to %2$s" + "%1$s has no Internet access" + "Tap for options" + "Mobile network has no Internet access" + "Network has no Internet access" + "Private DNS server cannot be accessed" + "%1$s has limited connectivity" + "Tap to connect anyway" + "Switched to %1$s" + "Device uses %1$s when %2$s has no Internet access. Charges may apply." + "Switched from %1$s to %2$s" - "mobile data" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobile data" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "an unknown network type" + "an unknown network type" diff --git a/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml b/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml index db5ad7029b..c490cf8da4 100644 --- a/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml +++ b/service/ServiceConnectivityResources/res/values-en-rCA/strings.xml @@ -17,27 +17,27 @@ - "System connectivity resources" - "Sign in to a Wi-Fi network" - "Sign in to network" - + "System connectivity resources" + "Sign in to a Wi-Fi network" + "Sign in to network" + - "%1$s has no Internet access" - "Tap for options" - "Mobile network has no Internet access" - "Network has no Internet access" - "Private DNS server cannot be accessed" - "%1$s has limited connectivity" - "Tap to connect anyway" - "Switched to %1$s" - "Device uses %1$s when %2$s has no Internet access. Charges may apply." - "Switched from %1$s to %2$s" + "%1$s has no Internet access" + "Tap for options" + "Mobile network has no Internet access" + "Network has no Internet access" + "Private DNS server cannot be accessed" + "%1$s has limited connectivity" + "Tap to connect anyway" + "Switched to %1$s" + "Device uses %1$s when %2$s has no Internet access. Charges may apply." + "Switched from %1$s to %2$s" - "mobile data" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobile data" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "an unknown network type" + "an unknown network type" diff --git a/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml b/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml index db5ad7029b..c490cf8da4 100644 --- a/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml +++ b/service/ServiceConnectivityResources/res/values-en-rGB/strings.xml @@ -17,27 +17,27 @@ - "System connectivity resources" - "Sign in to a Wi-Fi network" - "Sign in to network" - + "System connectivity resources" + "Sign in to a Wi-Fi network" + "Sign in to network" + - "%1$s has no Internet access" - "Tap for options" - "Mobile network has no Internet access" - "Network has no Internet access" - "Private DNS server cannot be accessed" - "%1$s has limited connectivity" - "Tap to connect anyway" - "Switched to %1$s" - "Device uses %1$s when %2$s has no Internet access. Charges may apply." - "Switched from %1$s to %2$s" + "%1$s has no Internet access" + "Tap for options" + "Mobile network has no Internet access" + "Network has no Internet access" + "Private DNS server cannot be accessed" + "%1$s has limited connectivity" + "Tap to connect anyway" + "Switched to %1$s" + "Device uses %1$s when %2$s has no Internet access. Charges may apply." + "Switched from %1$s to %2$s" - "mobile data" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobile data" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "an unknown network type" + "an unknown network type" diff --git a/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml b/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml index db5ad7029b..c490cf8da4 100644 --- a/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml +++ b/service/ServiceConnectivityResources/res/values-en-rIN/strings.xml @@ -17,27 +17,27 @@ - "System connectivity resources" - "Sign in to a Wi-Fi network" - "Sign in to network" - + "System connectivity resources" + "Sign in to a Wi-Fi network" + "Sign in to network" + - "%1$s has no Internet access" - "Tap for options" - "Mobile network has no Internet access" - "Network has no Internet access" - "Private DNS server cannot be accessed" - "%1$s has limited connectivity" - "Tap to connect anyway" - "Switched to %1$s" - "Device uses %1$s when %2$s has no Internet access. Charges may apply." - "Switched from %1$s to %2$s" + "%1$s has no Internet access" + "Tap for options" + "Mobile network has no Internet access" + "Network has no Internet access" + "Private DNS server cannot be accessed" + "%1$s has limited connectivity" + "Tap to connect anyway" + "Switched to %1$s" + "Device uses %1$s when %2$s has no Internet access. Charges may apply." + "Switched from %1$s to %2$s" - "mobile data" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobile data" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "an unknown network type" + "an unknown network type" diff --git a/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml b/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml index 2602bfa065..67c3659349 100644 --- a/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml +++ b/service/ServiceConnectivityResources/res/values-en-rXC/strings.xml @@ -17,27 +17,27 @@ - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‏‏‎‎‎‏‏‏‎‏‏‎‎‎‏‎‎‎‎‎‎‎‎‏‏‎‏‏‏‏‎‎‎‎‏‏‏‎‎‏‏‎‏‏‏‎‏‎‏‏‏‎‎‎‏‎‏‏‎System Connectivity Resources‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‎‏‏‏‎‏‎‏‎‏‎‎‎‎‎‏‏‎‏‏‏‏‎‏‎‏‎‏‎‎‎‎‏‏‏‏‎‎‏‎‎‎‏‏‎‎‏‎‏‎‏‎‏‏‎‎‏‎Sign in to Wi-Fi network‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‎‎‎‎‏‎‏‎‏‏‎‎‏‎‏‎‎‏‏‎‎‏‏‎‏‏‏‏‏‏‎‎‎‏‎‏‎‎‎‏‏‏‎‎‎‎‎‏‏‎‏‎‎‏‏‎‎‎‎Sign in to network‎‏‎‎‏‎" - + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‏‎‎‏‎‏‏‏‎‏‎‏‏‏‎‎‏‏‎‎‎‎‏‏‏‎‏‏‏‎‎‎‏‏‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‎‎‎‎‏‏‏‏‎‎System Connectivity Resources‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‏‏‏‎‎‏‏‎‎‎‎‎‎‎‎‎‎‏‎‎‎‏‏‎‎‏‏‎‎‎‎‏‎‏‎‎‏‎‎‏‏‏‏‎‏‏‏‏‏‏‏‏‏‏‎‎‎‏‎Sign in to Wi-Fi network‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‎‎‎‏‏‎‎‏‎‏‎‎‎‏‎‎‎‎‎‎‏‎‏‎‏‎‏‏‏‏‏‏‏‏‏‎‎‏‏‏‎‏‎‏‎‏‏‎‏‏‏‏‏‎‏‎‎‏‎Sign in to network‎‏‎‎‏‎" + - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‏‎‏‏‏‏‏‎‏‎‏‏‎‎‎‏‏‎‏‎‏‏‏‎‎‏‎‎‏‏‎‏‏‎‎‏‏‎‏‏‏‎‏‏‏‏‎‎‎‏‏‏‏‏‎‎‏‎‎‎‏‎‎‏‏‎%1$s‎‏‎‎‏‏‏‎ has no internet access‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‎‏‎‎‎‎‏‏‏‎‏‎‎‎‎‏‎‏‏‏‏‎‏‏‎‏‎‎‏‏‏‏‎‏‏‎‎‏‏‏‏‎‏‏‏‏‏‎‎‏‎‎‏‎‏‎‎‎‎Tap for options‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‏‏‎‏‏‎‎‏‏‎‎‎‎‏‏‎‎‏‏‎‎‏‏‎‎‎‎‏‎‏‏‏‏‎‏‎‏‎‏‎‎‏‎‎‏‎‎‏‎‎‏‏‎‏‎‏‏‏‎Mobile network has no internet access‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‏‎‎‎‏‎‏‎‎‎‏‎‏‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‎‎‏‏‎‏‎‏‎‎‎‏‎‎‎‏‏‎‎‏‏‏‏‏‏‎‏‎‏‏‎Network has no internet access‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‏‏‎‏‏‎‎‏‎‎‎‎‏‏‏‎‏‏‎‏‎‏‎‎‎‎‏‎‏‎‏‏‎‎‏‏‏‎‎‎‎‎‏‎Private DNS server cannot be accessed‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‎‏‎‏‎‏‎‏‏‏‎‏‏‏‏‏‏‎‏‎‏‎‎‎‎‏‏‏‎‎‎‏‏‏‎‏‎‏‏‏‎‏‎‎‏‎‏‎‏‎‎‎‎‏‎‎‏‎‎‏‎‎‏‏‎%1$s‎‏‎‎‏‏‏‎ has limited connectivity‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‎‎‏‏‏‎‏‏‎‏‏‎‎‏‏‏‏‏‏‎‎‏‏‎‏‏‏‎‏‎‏‎‎‎‎‎‎‎‎‏‎‏‎‎‏‎‏‎‏‎‏‎‏‎‎Tap to connect anyway‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‏‏‎‎‎‏‎‎‎‎‎‎‏‎‏‏‎‎‏‎‏‎‎‏‎‎‏‎‏‎‏‏‎‎‎‏‎‎‎‏‏‎‎‏‏‏‎‏‏‏‎‎‏‏‎‎‎‎‎Switched to ‎‏‎‎‏‏‎%1$s‎‏‎‎‏‏‏‎‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‎‎‎‏‏‎‎‏‏‏‎‏‎‏‎‏‎‏‏‎‏‏‏‎‎‏‏‎‏‏‏‎‎‏‏‎‏‎‎‎‏‎‎‎‏‏‏‎‎‏‎‏‎‎‎‏‎‏‎Device uses ‎‏‎‎‏‏‎%1$s‎‏‎‎‏‏‏‎ when ‎‏‎‎‏‏‎%2$s‎‏‎‎‏‏‏‎ has no internet access. Charges may apply.‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‏‎‎‎‏‏‏‏‎‎‏‎‏‎‏‏‏‏‎‎‎‎‏‏‏‏‏‏‏‎‏‏‏‏‎‏‏‏‎‎‏‎‎‏‎‏‏‎‎‎‎‏‎‎‎‏‎Switched from ‎‏‎‎‏‏‎%1$s‎‏‎‎‏‏‏‎ to ‎‏‎‎‏‏‎%2$s‎‏‎‎‏‏‏‎‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‎‎‏‏‎‏‎‎‎‎‎‏‎‎‎‏‏‎‎‎‎‎‏‏‏‎‎‎‎‏‎‏‎‎‏‎‎‎‎‏‏‏‎‎‏‎‏‎‎‏‏‎‏‎‎‏‏‎‎‏‎‎‏‏‎%1$s‎‏‎‎‏‏‏‎ has no internet access‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‎‎‎‎‎‏‏‏‏‏‎‎‏‎‎‏‏‏‏‎‏‏‏‏‎‏‏‎‏‎‏‎‎‏‏‎‏‏‎‏‎‎‏‎‎‏‎‏‎‏‏‎‎‎‏‏‎‏‎‎Tap for options‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‎‎‏‎‏‏‏‎‏‎‎‏‏‏‏‏‏‎‎‏‎‎‎‏‏‏‎‏‎‏‏‏‎‎‏‎‎‏‎‏‎‏‎‎‏‎‎‎‎‏‎‎‏‎‏‎‏‎‎Mobile network has no internet access‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‏‎‎‎‎‎‏‎‎‏‏‏‎‏‎‎‏‏‏‎‏‏‏‎‏‎‎‎‏‏‎‏‎‏‏‎‎‏‎‏‎‏‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‎Network has no internet access‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‏‎‏‎‎‏‎‎‏‏‏‎‎‎‎‏‏‎‏‏‏‏‏‎‏‎‏‎‎‏‏‏‏‏‎‏‎‎‏‎‏‎‏‏‏‏‎‎‎‎‏‏‎‎‎‏‏‏‏‎Private DNS server cannot be accessed‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‎‏‎‎‎‎‎‎‏‏‏‏‎‎‏‏‎‎‎‏‎‏‏‎‎‎‎‏‏‎‎‎‏‎‏‎‏‎‏‎‎‎‏‎‏‎‏‏‏‎‎‏‏‎‏‎‏‎‎‎‏‎‎‏‏‎%1$s‎‏‎‎‏‏‏‎ has limited connectivity‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‎‏‏‎‏‎‏‏‎‎‏‏‏‏‏‏‎‏‎‏‏‎‏‎‏‏‎‏‏‎‏‏‎‏‏‎‎‎‎‏‎‎‎‎‎‏‎‏‏‎‏‏‏‏‎‏‎‏‎Tap to connect anyway‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‎‏‏‎‎‏‏‏‏‏‏‎‏‏‏‏‏‎‎‎‎‏‏‎‎‎‏‏‎‎‏‎‎‎‏‏‎‎‏‎‎‏‏‎‎‎‏‏‎‎‎‏‏‎‏‏‏‏‎Switched to ‎‏‎‎‏‏‎%1$s‎‏‎‎‏‏‏‎‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‎‏‎‏‏‏‎‎‏‎‏‏‎‏‎‎‏‏‏‏‎‏‎‎‏‏‏‏‎‏‎‏‎‎‎‎‎‏‎‏‎‏‏‎‏‏‎‏‎‎‎‏‎‏‎‎‎‎‎Device uses ‎‏‎‎‏‏‎%1$s‎‏‎‎‏‏‏‎ when ‎‏‎‎‏‏‎%2$s‎‏‎‎‏‏‏‎ has no internet access. Charges may apply.‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‎‎‏‏‏‏‏‎‏‏‎‎‏‎‎‏‎‏‎‎‏‏‏‎‎‏‎‎‎‎‏‎‎‏‎‏‎‏‎‎‏‎‎‏‎‏‏‎‏‎‏‎‎‏‏‏‏‏‎Switched from ‎‏‎‎‏‏‎%1$s‎‏‎‎‏‏‏‎ to ‎‏‎‎‏‏‎%2$s‎‏‎‎‏‏‏‎‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‎‏‏‎‎‎‎‏‎‎‎‏‏‎‎‏‎‎‎‎‎‏‏‎‏‏‏‏‎‎‎‎‏‎‎‏‏‏‎‏‎‏‏‏‎‏‏‎‎‏‎‏‎‏‏‎mobile data‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‎‎‎‎‎‎‎‏‎‎‏‎‏‎‎‎‎‎‎‏‏‏‎‎‏‎‎‎‎‎‎‎‎‎‎‎‎‎‏‎‏‎‎‏‎‎‏‎‎‎‎‏‎‏‎‎‏‎Wi-Fi‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‏‎‏‎‎‎‎‏‎‎‏‏‏‎‎‏‏‏‏‎‎‎‏‏‎‎‎‎‏‎‏‏‎‎‎‎‎‎‎‏‏‏‎‏‎‏‎‎‏‏‏‏‎‎‏‎‎‎‎Bluetooth‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‎‎‎‎‎‏‏‎‏‏‏‏‎‎‎‎‏‏‎‏‏‎‎‏‎‎‏‏‎‏‏‏‏‎‏‎‏‎‎‏‎‎‏‎‎‎‎‎‎‎‏‏‏‎‎‏‏‏‎Ethernet‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‏‏‎‏‏‏‏‎‏‏‎‏‎‏‏‎‏‏‎‎‏‎‎‎‏‎‎‏‏‎‏‏‏‎‎‏‏‎‏‎VPN‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‎‏‏‎‏‏‎‎‏‏‏‎‏‎‏‎‏‏‏‎‎‎‎‏‎‏‎‎‎‏‎‎‎‎‎‎‎‎‎‏‏‏‎‎‏‏‎‏‏‏‎‏‎‎‎‏‏‏‎mobile data‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‎‎‎‎‎‏‏‎‏‏‎‎‏‏‏‎‏‎‎‏‎‏‎‏‏‏‏‎‏‎‎‎‎‏‎‏‎‏‏‏‏‏‏‎‎‏‎‏‎‎‏‎‎‏‎‎‎‎Wi-Fi‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‎‏‎‏‎‏‎‎‎‎‏‏‏‎‎‏‎‏‎‏‎‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‏‏‎‎‎‎‏‏‏‎‏‎‏‎‏‏‎‎‎‏‏‎Bluetooth‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‎‎‏‏‎‎‏‏‏‏‎‏‎‎‎‏‏‏‏‏‏‎‎‏‎‏‏‎‎‎‎‏‏‏‎‎‏‎‏‎‏‏‏‏‏‎‏‏‏‎‏‏‎‏‏‎‎‎‎Ethernet‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‏‏‎‎‏‎‏‎‏‏‏‎‏‏‎‎‏‎‎‎‏‎‎‏‏‎‏‏‏‎‎‏‏‏‏‎‏‏‎‏‎‎‎‏‎‎‎‎‏‏‎‏‎‎‎‏‏‎VPN‎‏‎‎‏‎" - "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‎‎‏‎‎‏‏‏‎‎‏‎‏‏‎‏‎‏‏‏‏‎‏‎‏‏‎‎‏‏‏‎‏‎‏‎‏‎‎‏‎‏‏‎‎‏‎‏‎‏‏‎‏‏‏‏‎‎‎an unknown network type‎‏‎‎‏‎" + "‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‏‏‎‎‎‎‎‎‎‏‎‏‎‎‎‏‏‎‏‎‏‏‎‏‏‎‏‎‏‏‎‏‎‎‎‏‎‎‎‏‎‎‎‏‏‏‎‏‏‎‏‏‏‏‎‎‏‎‎an unknown network type‎‏‎‎‏‎" diff --git a/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml b/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml index e5f1833fdd..fdca46809c 100644 --- a/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml +++ b/service/ServiceConnectivityResources/res/values-es-rUS/strings.xml @@ -17,27 +17,27 @@ - "Recursos de conectividad del sistema" - "Accede a una red Wi-Fi." - "Acceder a la red" - + "Recursos de conectividad del sistema" + "Accede a una red Wi-Fi." + "Acceder a la red" + - "%1$sno tiene acceso a Internet" - "Presiona para ver opciones" - "La red móvil no tiene acceso a Internet" - "La red no tiene acceso a Internet" - "No se puede acceder al servidor DNS privado" - "%1$s tiene conectividad limitada" - "Presiona para conectarte de todas formas" - "Se cambió a %1$s" - "El dispositivo usa %1$s cuando %2$s no tiene acceso a Internet. Es posible que se apliquen cargos." - "Se cambió de %1$s a %2$s" + "%1$sno tiene acceso a Internet" + "Presiona para ver opciones" + "La red móvil no tiene acceso a Internet" + "La red no tiene acceso a Internet" + "No se puede acceder al servidor DNS privado" + "%1$s tiene conectividad limitada" + "Presiona para conectarte de todas formas" + "Se cambió a %1$s" + "El dispositivo usa %1$s cuando %2$s no tiene acceso a Internet. Es posible que se apliquen cargos." + "Se cambió de %1$s a %2$s" - "Datos móviles" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "Datos móviles" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "un tipo de red desconocido" + "un tipo de red desconocido" diff --git a/service/ServiceConnectivityResources/res/values-es/strings.xml b/service/ServiceConnectivityResources/res/values-es/strings.xml index e4f4307f33..f4a7e3d0e8 100644 --- a/service/ServiceConnectivityResources/res/values-es/strings.xml +++ b/service/ServiceConnectivityResources/res/values-es/strings.xml @@ -17,27 +17,27 @@ - "Recursos de conectividad del sistema" - "Iniciar sesión en red Wi-Fi" - "Iniciar sesión en la red" - + "Recursos de conectividad del sistema" + "Iniciar sesión en red Wi-Fi" + "Iniciar sesión en la red" + - "%1$s no tiene acceso a Internet" - "Toca para ver opciones" - "La red móvil no tiene acceso a Internet" - "La red no tiene acceso a Internet" - "No se ha podido acceder al servidor DNS privado" - "%1$s tiene una conectividad limitada" - "Toca para conectarte de todas formas" - "Se ha cambiado a %1$s" - "El dispositivo utiliza %1$s cuando %2$s no tiene acceso a Internet. Es posible que se apliquen cargos." - "Se ha cambiado de %1$s a %2$s" + "%1$s no tiene acceso a Internet" + "Toca para ver opciones" + "La red móvil no tiene acceso a Internet" + "La red no tiene acceso a Internet" + "No se ha podido acceder al servidor DNS privado" + "%1$s tiene una conectividad limitada" + "Toca para conectarte de todas formas" + "Se ha cambiado a %1$s" + "El dispositivo utiliza %1$s cuando %2$s no tiene acceso a Internet. Es posible que se apliquen cargos." + "Se ha cambiado de %1$s a %2$s" - "datos móviles" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "datos móviles" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "un tipo de red desconocido" + "un tipo de red desconocido" diff --git a/service/ServiceConnectivityResources/res/values-et/strings.xml b/service/ServiceConnectivityResources/res/values-et/strings.xml index cec408fd94..cf997b3336 100644 --- a/service/ServiceConnectivityResources/res/values-et/strings.xml +++ b/service/ServiceConnectivityResources/res/values-et/strings.xml @@ -17,27 +17,27 @@ - "Süsteemi ühenduvuse allikad" - "Logi sisse WiFi-võrku" - "Võrku sisselogimine" - + "Süsteemi ühenduvuse allikad" + "Logi sisse WiFi-võrku" + "Võrku sisselogimine" + - "Võrgul %1$s puudub Interneti-ühendus" - "Puudutage valikute nägemiseks" - "Mobiilsidevõrgul puudub Interneti-ühendus" - "Võrgul puudub Interneti-ühendus" - "Privaatsele DNS-serverile ei pääse juurde" - "Võrgu %1$s ühendus on piiratud" - "Puudutage, kui soovite siiski ühenduse luua" - "Lülitati võrgule %1$s" - "Seade kasutab võrku %1$s, kui võrgul %2$s puudub juurdepääs Internetile. Rakenduda võivad tasud." - "Lülitati võrgult %1$s võrgule %2$s" + "Võrgul %1$s puudub Interneti-ühendus" + "Puudutage valikute nägemiseks" + "Mobiilsidevõrgul puudub Interneti-ühendus" + "Võrgul puudub Interneti-ühendus" + "Privaatsele DNS-serverile ei pääse juurde" + "Võrgu %1$s ühendus on piiratud" + "Puudutage, kui soovite siiski ühenduse luua" + "Lülitati võrgule %1$s" + "Seade kasutab võrku %1$s, kui võrgul %2$s puudub juurdepääs Internetile. Rakenduda võivad tasud." + "Lülitati võrgult %1$s võrgule %2$s" - "mobiilne andmeside" - "WiFi" - "Bluetooth" - "Ethernet" - "VPN" + "mobiilne andmeside" + "WiFi" + "Bluetooth" + "Ethernet" + "VPN" - "tundmatu võrgutüüp" + "tundmatu võrgutüüp" diff --git a/service/ServiceConnectivityResources/res/values-eu/strings.xml b/service/ServiceConnectivityResources/res/values-eu/strings.xml index f3ee9b1287..6b0d8ef7e2 100644 --- a/service/ServiceConnectivityResources/res/values-eu/strings.xml +++ b/service/ServiceConnectivityResources/res/values-eu/strings.xml @@ -17,27 +17,27 @@ - "Sistemaren konexio-baliabideak" - "Hasi saioa Wi-Fi sarean" - "Hasi saioa sarean" - + "Sistemaren konexio-baliabideak" + "Hasi saioa Wi-Fi sarean" + "Hasi saioa sarean" + - "Ezin da konektatu Internetera %1$s sarearen bidez" - "Sakatu aukerak ikusteko" - "Sare mugikorra ezin da konektatu Internetera" - "Sarea ezin da konektatu Internetera" - "Ezin da atzitu DNS zerbitzari pribatua" - "%1$s sareak konektagarritasun murriztua du" - "Sakatu hala ere konektatzeko" - "%1$s erabiltzen ari zara orain" - "%2$s Internetera konektatzeko gauza ez denean, %1$s erabiltzen du gailuak. Agian kostuak ordaindu beharko dituzu." - "%1$s erabiltzen ari zinen, baina %2$s erabiltzen ari zara orain" + "Ezin da konektatu Internetera %1$s sarearen bidez" + "Sakatu aukerak ikusteko" + "Sare mugikorra ezin da konektatu Internetera" + "Sarea ezin da konektatu Internetera" + "Ezin da atzitu DNS zerbitzari pribatua" + "%1$s sareak konektagarritasun murriztua du" + "Sakatu hala ere konektatzeko" + "%1$s erabiltzen ari zara orain" + "%2$s Internetera konektatzeko gauza ez denean, %1$s erabiltzen du gailuak. Agian kostuak ordaindu beharko dituzu." + "%1$s erabiltzen ari zinen, baina %2$s erabiltzen ari zara orain" - "datu-konexioa" - "Wifia" - "Bluetooth-a" - "Ethernet-a" - "VPNa" + "datu-konexioa" + "Wifia" + "Bluetooth-a" + "Ethernet-a" + "VPNa" - "sare mota ezezaguna" + "sare mota ezezaguna" diff --git a/service/ServiceConnectivityResources/res/values-fa/strings.xml b/service/ServiceConnectivityResources/res/values-fa/strings.xml index 0c5b147e5a..296ce8ecab 100644 --- a/service/ServiceConnectivityResources/res/values-fa/strings.xml +++ b/service/ServiceConnectivityResources/res/values-fa/strings.xml @@ -17,27 +17,27 @@ - "منابع اتصال سیستم" - "‏ورود به شبکه Wi-Fi" - "ورود به سیستم شبکه" - + "منابع اتصال سیستم" + "‏ورود به شبکه Wi-Fi" + "ورود به سیستم شبکه" + - "%1$s به اینترنت دسترسی ندارد" - "برای گزینه‌ها ضربه بزنید" - "شبکه تلفن همراه به اینترنت دسترسی ندارد" - "شبکه به اینترنت دسترسی ندارد" - "‏سرور DNS خصوصی قابل دسترسی نیست" - "%1$s اتصال محدودی دارد" - "به‌هرصورت، برای اتصال ضربه بزنید" - "به %1$s تغییر کرد" - "وقتی %2$s به اینترنت دسترسی نداشته باشد، دستگاه از %1$s استفاده می‌کند. ممکن است هزینه‌هایی اعمال شود." - "از %1$s به %2$s تغییر کرد" + "%1$s به اینترنت دسترسی ندارد" + "برای گزینه‌ها ضربه بزنید" + "شبکه تلفن همراه به اینترنت دسترسی ندارد" + "شبکه به اینترنت دسترسی ندارد" + "‏سرور DNS خصوصی قابل دسترسی نیست" + "%1$s اتصال محدودی دارد" + "به‌هرصورت، برای اتصال ضربه بزنید" + "به %1$s تغییر کرد" + "وقتی %2$s به اینترنت دسترسی نداشته باشد، دستگاه از %1$s استفاده می‌کند. ممکن است هزینه‌هایی اعمال شود." + "از %1$s به %2$s تغییر کرد" - "داده تلفن همراه" - "Wi-Fi" - "بلوتوث" - "اترنت" - "VPN" + "داده تلفن همراه" + "Wi-Fi" + "بلوتوث" + "اترنت" + "VPN" - "نوع شبکه نامشخص" + "نوع شبکه نامشخص" diff --git a/service/ServiceConnectivityResources/res/values-fi/strings.xml b/service/ServiceConnectivityResources/res/values-fi/strings.xml index 84c0034f8b..07d2907410 100644 --- a/service/ServiceConnectivityResources/res/values-fi/strings.xml +++ b/service/ServiceConnectivityResources/res/values-fi/strings.xml @@ -17,27 +17,27 @@ - "Järjestelmän yhteysresurssit" - "Kirjaudu Wi-Fi-verkkoon" - "Kirjaudu verkkoon" - + "Järjestelmän yhteysresurssit" + "Kirjaudu Wi-Fi-verkkoon" + "Kirjaudu verkkoon" + - "%1$s ei ole yhteydessä internetiin" - "Näytä vaihtoehdot napauttamalla." - "Mobiiliverkko ei ole yhteydessä internetiin" - "Verkko ei ole yhteydessä internetiin" - "Ei pääsyä yksityiselle DNS-palvelimelle" - "%1$s toimii rajoitetulla yhteydellä" - "Yhdistä napauttamalla" - "%1$s otettiin käyttöön" - "%1$s otetaan käyttöön, kun %2$s ei voi muodostaa yhteyttä internetiin. Veloitukset ovat mahdollisia." - "%1$s poistettiin käytöstä ja %2$s otettiin käyttöön." + "%1$s ei ole yhteydessä internetiin" + "Näytä vaihtoehdot napauttamalla." + "Mobiiliverkko ei ole yhteydessä internetiin" + "Verkko ei ole yhteydessä internetiin" + "Ei pääsyä yksityiselle DNS-palvelimelle" + "%1$s toimii rajoitetulla yhteydellä" + "Yhdistä napauttamalla" + "%1$s otettiin käyttöön" + "%1$s otetaan käyttöön, kun %2$s ei voi muodostaa yhteyttä internetiin. Veloitukset ovat mahdollisia." + "%1$s poistettiin käytöstä ja %2$s otettiin käyttöön." - "mobiilidata" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobiilidata" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "tuntematon verkon tyyppi" + "tuntematon verkon tyyppi" diff --git a/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml b/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml index 0badf1be0f..7d5b366cf7 100644 --- a/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml +++ b/service/ServiceConnectivityResources/res/values-fr-rCA/strings.xml @@ -17,27 +17,27 @@ - "Ressources de connectivité système" - "Connectez-vous au réseau Wi-Fi" - "Connectez-vous au réseau" - + "Ressources de connectivité système" + "Connectez-vous au réseau Wi-Fi" + "Connectez-vous au réseau" + - "Le réseau %1$s n\'offre aucun accès à Internet" - "Touchez pour afficher les options" - "Le réseau cellulaire n\'offre aucun accès à Internet" - "Le réseau n\'offre aucun accès à Internet" - "Impossible d\'accéder au serveur DNS privé" - "Le réseau %1$s offre une connectivité limitée" - "Touchez pour vous connecter quand même" - "Passé au réseau %1$s" - "L\'appareil utilise %1$s quand %2$s n\'a pas d\'accès à Internet. Des frais peuvent s\'appliquer." - "Passé du réseau %1$s au %2$s" + "Le réseau %1$s n\'offre aucun accès à Internet" + "Touchez pour afficher les options" + "Le réseau cellulaire n\'offre aucun accès à Internet" + "Le réseau n\'offre aucun accès à Internet" + "Impossible d\'accéder au serveur DNS privé" + "Le réseau %1$s offre une connectivité limitée" + "Touchez pour vous connecter quand même" + "Passé au réseau %1$s" + "L\'appareil utilise %1$s quand %2$s n\'a pas d\'accès à Internet. Des frais peuvent s\'appliquer." + "Passé du réseau %1$s au %2$s" - "données cellulaires" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "RPV" + "données cellulaires" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "RPV" - "un type de réseau inconnu" + "un type de réseau inconnu" diff --git a/service/ServiceConnectivityResources/res/values-fr/strings.xml b/service/ServiceConnectivityResources/res/values-fr/strings.xml index b4835258d9..2331d9ba59 100644 --- a/service/ServiceConnectivityResources/res/values-fr/strings.xml +++ b/service/ServiceConnectivityResources/res/values-fr/strings.xml @@ -17,27 +17,27 @@ - "Ressources de connectivité système" - "Connectez-vous au réseau Wi-Fi" - "Se connecter au réseau" - + "Ressources de connectivité système" + "Connectez-vous au réseau Wi-Fi" + "Se connecter au réseau" + - "Aucune connexion à Internet pour %1$s" - "Appuyez ici pour afficher des options." - "Le réseau mobile ne dispose d\'aucun accès à Internet" - "Le réseau ne dispose d\'aucun accès à Internet" - "Impossible d\'accéder au serveur DNS privé" - "La connectivité de %1$s est limitée" - "Appuyer pour se connecter quand même" - "Nouveau réseau : %1$s" - "L\'appareil utilise %1$s lorsque %2$s n\'a pas de connexion Internet. Des frais peuvent s\'appliquer." - "Ancien réseau : %1$s. Nouveau réseau : %2$s" + "Aucune connexion à Internet pour %1$s" + "Appuyez ici pour afficher des options." + "Le réseau mobile ne dispose d\'aucun accès à Internet" + "Le réseau ne dispose d\'aucun accès à Internet" + "Impossible d\'accéder au serveur DNS privé" + "La connectivité de %1$s est limitée" + "Appuyer pour se connecter quand même" + "Nouveau réseau : %1$s" + "L\'appareil utilise %1$s lorsque %2$s n\'a pas de connexion Internet. Des frais peuvent s\'appliquer." + "Ancien réseau : %1$s. Nouveau réseau : %2$s" - "données mobiles" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "données mobiles" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "type de réseau inconnu" + "type de réseau inconnu" diff --git a/service/ServiceConnectivityResources/res/values-gl/strings.xml b/service/ServiceConnectivityResources/res/values-gl/strings.xml index dfe813746e..f46f84b0d9 100644 --- a/service/ServiceConnectivityResources/res/values-gl/strings.xml +++ b/service/ServiceConnectivityResources/res/values-gl/strings.xml @@ -17,27 +17,27 @@ - "Recursos de conectividade do sistema" - "Inicia sesión na rede wifi" - "Inicia sesión na rede" - + "Recursos de conectividade do sistema" + "Inicia sesión na rede wifi" + "Inicia sesión na rede" + - "%1$s non ten acceso a Internet" - "Toca para ver opcións." - "A rede de telefonía móbil non ten acceso a Internet" - "A rede non ten acceso a Internet" - "Non se puido acceder ao servidor DNS privado" - "A conectividade de %1$s é limitada" - "Toca para conectarte de todas formas" - "Cambiouse a: %1$s" - "O dispositivo utiliza %1$s cando %2$s non ten acceso a Internet. Pódense aplicar cargos." - "Cambiouse de %1$s a %2$s" + "%1$s non ten acceso a Internet" + "Toca para ver opcións." + "A rede de telefonía móbil non ten acceso a Internet" + "A rede non ten acceso a Internet" + "Non se puido acceder ao servidor DNS privado" + "A conectividade de %1$s é limitada" + "Toca para conectarte de todas formas" + "Cambiouse a: %1$s" + "O dispositivo utiliza %1$s cando %2$s non ten acceso a Internet. Pódense aplicar cargos." + "Cambiouse de %1$s a %2$s" - "datos móbiles" - "wifi" - "Bluetooth" - "Ethernet" - "VPN" + "datos móbiles" + "wifi" + "Bluetooth" + "Ethernet" + "VPN" - "un tipo de rede descoñecido" + "un tipo de rede descoñecido" diff --git a/service/ServiceConnectivityResources/res/values-gu/strings.xml b/service/ServiceConnectivityResources/res/values-gu/strings.xml index e49b11d21e..ec9ecd30ec 100644 --- a/service/ServiceConnectivityResources/res/values-gu/strings.xml +++ b/service/ServiceConnectivityResources/res/values-gu/strings.xml @@ -17,27 +17,27 @@ - "સિસ્ટમની કનેક્ટિવિટીનાં સાધનો" - "વાઇ-ફાઇ નેટવર્ક પર સાઇન ઇન કરો" - "નેટવર્ક પર સાઇન ઇન કરો" - + "સિસ્ટમની કનેક્ટિવિટીનાં સાધનો" + "વાઇ-ફાઇ નેટવર્ક પર સાઇન ઇન કરો" + "નેટવર્ક પર સાઇન ઇન કરો" + - "%1$s ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી" - "વિકલ્પો માટે ટૅપ કરો" - "મોબાઇલ નેટવર્ક કોઈ ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી" - "નેટવર્ક કોઈ ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી" - "ખાનગી DNS સર્વર ઍક્સેસ કરી શકાતા નથી" - "%1$s મર્યાદિત કનેક્ટિવિટી ધરાવે છે" - "છતાં કનેક્ટ કરવા માટે ટૅપ કરો" - "%1$s પર સ્વિચ કર્યું" - "જ્યારે %2$s પાસે કોઈ ઇન્ટરનેટ ઍક્સેસ ન હોય ત્યારે ઉપકરણ %1$sનો ઉપયોગ કરે છે. શુલ્ક લાગુ થઈ શકે છે." - "%1$s પરથી %2$s પર સ્વિચ કર્યું" + "%1$s ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી" + "વિકલ્પો માટે ટૅપ કરો" + "મોબાઇલ નેટવર્ક કોઈ ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી" + "નેટવર્ક કોઈ ઇન્ટરનેટ ઍક્સેસ ધરાવતું નથી" + "ખાનગી DNS સર્વર ઍક્સેસ કરી શકાતા નથી" + "%1$s મર્યાદિત કનેક્ટિવિટી ધરાવે છે" + "છતાં કનેક્ટ કરવા માટે ટૅપ કરો" + "%1$s પર સ્વિચ કર્યું" + "જ્યારે %2$s પાસે કોઈ ઇન્ટરનેટ ઍક્સેસ ન હોય ત્યારે ઉપકરણ %1$sનો ઉપયોગ કરે છે. શુલ્ક લાગુ થઈ શકે છે." + "%1$s પરથી %2$s પર સ્વિચ કર્યું" - "મોબાઇલ ડેટા" - "વાઇ-ફાઇ" - "બ્લૂટૂથ" - "ઇથરનેટ" - "VPN" + "મોબાઇલ ડેટા" + "વાઇ-ફાઇ" + "બ્લૂટૂથ" + "ઇથરનેટ" + "VPN" - "કોઈ અજાણ્યો નેટવર્કનો પ્રકાર" + "કોઈ અજાણ્યો નેટવર્કનો પ્રકાર" diff --git a/service/ServiceConnectivityResources/res/values-hi/strings.xml b/service/ServiceConnectivityResources/res/values-hi/strings.xml index 80ed699768..6e3bc6b919 100644 --- a/service/ServiceConnectivityResources/res/values-hi/strings.xml +++ b/service/ServiceConnectivityResources/res/values-hi/strings.xml @@ -17,27 +17,27 @@ - "सिस्टम कनेक्टिविटी के संसाधन" - "वाई-फ़ाई नेटवर्क में साइन इन करें" - "नेटवर्क में साइन इन करें" - + "सिस्टम कनेक्टिविटी के संसाधन" + "वाई-फ़ाई नेटवर्क में साइन इन करें" + "नेटवर्क में साइन इन करें" + - "%1$s का इंटरनेट नहीं चल रहा है" - "विकल्पों के लिए टैप करें" - "मोबाइल नेटवर्क पर इंटरनेट ऐक्सेस नहीं है" - "इस नेटवर्क पर इंटरनेट ऐक्सेस नहीं है" - "निजी डीएनएस सर्वर को ऐक्सेस नहीं किया जा सकता" - "%1$s की कनेक्टिविटी सीमित है" - "फिर भी कनेक्ट करने के लिए टैप करें" - "%1$s पर ले जाया गया" - "%2$s में इंटरनेट की सुविधा नहीं होने पर डिवाइस %1$s का इस्तेमाल करता है. इसके लिए शुल्क लिया जा सकता है." - "%1$s से %2$s पर ले जाया गया" + "%1$s का इंटरनेट नहीं चल रहा है" + "विकल्पों के लिए टैप करें" + "मोबाइल नेटवर्क पर इंटरनेट ऐक्सेस नहीं है" + "इस नेटवर्क पर इंटरनेट ऐक्सेस नहीं है" + "निजी डीएनएस सर्वर को ऐक्सेस नहीं किया जा सकता" + "%1$s की कनेक्टिविटी सीमित है" + "फिर भी कनेक्ट करने के लिए टैप करें" + "%1$s पर ले जाया गया" + "%2$s में इंटरनेट की सुविधा नहीं होने पर डिवाइस %1$s का इस्तेमाल करता है. इसके लिए शुल्क लिया जा सकता है." + "%1$s से %2$s पर ले जाया गया" - "मोबाइल डेटा" - "वाई-फ़ाई" - "ब्लूटूथ" - "ईथरनेट" - "वीपीएन" + "मोबाइल डेटा" + "वाई-फ़ाई" + "ब्लूटूथ" + "ईथरनेट" + "वीपीएन" - "अज्ञात नेटवर्क टाइप" + "अज्ञात नेटवर्क टाइप" diff --git a/service/ServiceConnectivityResources/res/values-hr/strings.xml b/service/ServiceConnectivityResources/res/values-hr/strings.xml index 24bb22fd69..6a6de4cdaf 100644 --- a/service/ServiceConnectivityResources/res/values-hr/strings.xml +++ b/service/ServiceConnectivityResources/res/values-hr/strings.xml @@ -17,27 +17,27 @@ - "Resursi za povezivost sustava" - "Prijava na Wi-Fi mrežu" - "Prijava na mrežu" - + "Resursi za povezivost sustava" + "Prijava na Wi-Fi mrežu" + "Prijava na mrežu" + - "%1$s nema pristup internetu" - "Dodirnite za opcije" - "Mobilna mreža nema pristup internetu" - "Mreža nema pristup internetu" - "Nije moguće pristupiti privatnom DNS poslužitelju" - "%1$s ima ograničenu povezivost" - "Dodirnite da biste se ipak povezali" - "Prelazak na drugu mrežu: %1$s" - "Kada %2$s nema pristup internetu, na uređaju se upotrebljava %1$s. Moguća je naplata naknade." - "Mreža je promijenjena: %1$s > %2$s" + "%1$s nema pristup internetu" + "Dodirnite za opcije" + "Mobilna mreža nema pristup internetu" + "Mreža nema pristup internetu" + "Nije moguće pristupiti privatnom DNS poslužitelju" + "%1$s ima ograničenu povezivost" + "Dodirnite da biste se ipak povezali" + "Prelazak na drugu mrežu: %1$s" + "Kada %2$s nema pristup internetu, na uređaju se upotrebljava %1$s. Moguća je naplata naknade." + "Mreža je promijenjena: %1$s > %2$s" - "mobilni podaci" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobilni podaci" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "nepoznata vrsta mreže" + "nepoznata vrsta mreže" diff --git a/service/ServiceConnectivityResources/res/values-hu/strings.xml b/service/ServiceConnectivityResources/res/values-hu/strings.xml index 47a1142ac3..1d39d308f7 100644 --- a/service/ServiceConnectivityResources/res/values-hu/strings.xml +++ b/service/ServiceConnectivityResources/res/values-hu/strings.xml @@ -17,27 +17,27 @@ - "Rendszerkapcsolat erőforrásai" - "Bejelentkezés Wi-Fi hálózatba" - "Bejelentkezés a hálózatba" - + "Rendszerkapcsolat erőforrásai" + "Bejelentkezés Wi-Fi hálózatba" + "Bejelentkezés a hálózatba" + - "A(z) %1$s hálózaton nincs internet-hozzáférés" - "Koppintson a beállítások megjelenítéséhez" - "A mobilhálózaton nincs internet-hozzáférés" - "A hálózaton nincs internet-hozzáférés" - "A privát DNS-kiszolgálóhoz nem lehet hozzáférni" - "A(z) %1$s hálózat korlátozott kapcsolatot biztosít" - "Koppintson, ha mindenképpen csatlakozni szeretne" - "Átváltva erre: %1$s" - "%1$s használata, ha nincs internet-hozzáférés %2$s-kapcsolaton keresztül. A szolgáltató díjat számíthat fel." - "Átváltva %1$s-hálózatról erre: %2$s" + "A(z) %1$s hálózaton nincs internet-hozzáférés" + "Koppintson a beállítások megjelenítéséhez" + "A mobilhálózaton nincs internet-hozzáférés" + "A hálózaton nincs internet-hozzáférés" + "A privát DNS-kiszolgálóhoz nem lehet hozzáférni" + "A(z) %1$s hálózat korlátozott kapcsolatot biztosít" + "Koppintson, ha mindenképpen csatlakozni szeretne" + "Átváltva erre: %1$s" + "%1$s használata, ha nincs internet-hozzáférés %2$s-kapcsolaton keresztül. A szolgáltató díjat számíthat fel." + "Átváltva %1$s-hálózatról erre: %2$s" - "mobiladatok" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobiladatok" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "ismeretlen hálózati típus" + "ismeretlen hálózati típus" diff --git a/service/ServiceConnectivityResources/res/values-hy/strings.xml b/service/ServiceConnectivityResources/res/values-hy/strings.xml index dd951e8182..99386d445a 100644 --- a/service/ServiceConnectivityResources/res/values-hy/strings.xml +++ b/service/ServiceConnectivityResources/res/values-hy/strings.xml @@ -17,27 +17,27 @@ - "System Connectivity Resources" - "Մուտք գործեք Wi-Fi ցանց" - "Մուտք գործեք ցանց" - + "System Connectivity Resources" + "Մուտք գործեք Wi-Fi ցանց" + "Մուտք գործեք ցանց" + - "%1$s ցանցը չունի մուտք ինտերնետին" - "Հպեք՝ ընտրանքները տեսնելու համար" - "Բջջային ցանցը չի ապահովում ինտերնետ կապ" - "Ցանցը միացված չէ ինտերնետին" - "Մասնավոր DNS սերվերն անհասանելի է" - "%1$s ցանցի կապը սահմանափակ է" - "Հպեք՝ միանալու համար" - "Անցել է %1$s ցանցի" - "Երբ %2$s ցանցում ինտերնետ կապ չի լինում, սարքն անցնում է %1$s ցանցի: Նման դեպքում կարող են վճարներ գանձվել:" - "%1$s ցանցից անցել է %2$s ցանցի" + "%1$s ցանցը չունի մուտք ինտերնետին" + "Հպեք՝ ընտրանքները տեսնելու համար" + "Բջջային ցանցը չի ապահովում ինտերնետ կապ" + "Ցանցը միացված չէ ինտերնետին" + "Մասնավոր DNS սերվերն անհասանելի է" + "%1$s ցանցի կապը սահմանափակ է" + "Հպեք՝ միանալու համար" + "Անցել է %1$s ցանցի" + "Երբ %2$s ցանցում ինտերնետ կապ չի լինում, սարքն անցնում է %1$s ցանցի: Նման դեպքում կարող են վճարներ գանձվել:" + "%1$s ցանցից անցել է %2$s ցանցի" - "բջջային ինտերնետ" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "բջջային ինտերնետ" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "ցանցի անհայտ տեսակ" + "ցանցի անհայտ տեսակ" diff --git a/service/ServiceConnectivityResources/res/values-in/strings.xml b/service/ServiceConnectivityResources/res/values-in/strings.xml index d559f6b11e..f47d25752e 100644 --- a/service/ServiceConnectivityResources/res/values-in/strings.xml +++ b/service/ServiceConnectivityResources/res/values-in/strings.xml @@ -17,27 +17,27 @@ - "Resource Konektivitas Sistem" - "Login ke jaringan Wi-Fi" - "Login ke jaringan" - + "Resource Konektivitas Sistem" + "Login ke jaringan Wi-Fi" + "Login ke jaringan" + - "%1$s tidak memiliki akses internet" - "Ketuk untuk melihat opsi" - "Jaringan seluler tidak memiliki akses internet" - "Jaringan tidak memiliki akses internet" - "Server DNS pribadi tidak dapat diakses" - "%1$s memiliki konektivitas terbatas" - "Ketuk untuk tetap menyambungkan" - "Dialihkan ke %1$s" - "Perangkat menggunakan %1$s jika %2$s tidak memiliki akses internet. Tarif mungkin berlaku." - "Dialihkan dari %1$s ke %2$s" + "%1$s tidak memiliki akses internet" + "Ketuk untuk melihat opsi" + "Jaringan seluler tidak memiliki akses internet" + "Jaringan tidak memiliki akses internet" + "Server DNS pribadi tidak dapat diakses" + "%1$s memiliki konektivitas terbatas" + "Ketuk untuk tetap menyambungkan" + "Dialihkan ke %1$s" + "Perangkat menggunakan %1$s jika %2$s tidak memiliki akses internet. Tarif mungkin berlaku." + "Dialihkan dari %1$s ke %2$s" - "data seluler" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "data seluler" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "jenis jaringan yang tidak dikenal" + "jenis jaringan yang tidak dikenal" diff --git a/service/ServiceConnectivityResources/res/values-is/strings.xml b/service/ServiceConnectivityResources/res/values-is/strings.xml index 877c85f147..eeba2311fa 100644 --- a/service/ServiceConnectivityResources/res/values-is/strings.xml +++ b/service/ServiceConnectivityResources/res/values-is/strings.xml @@ -17,27 +17,27 @@ - "Tengigögn kerfis" - "Skrá inn á Wi-Fi net" - "Skrá inn á net" - + "Tengigögn kerfis" + "Skrá inn á Wi-Fi net" + "Skrá inn á net" + - "%1$s er ekki með internetaðgang" - "Ýttu til að sjá valkosti" - "Farsímakerfið er ekki tengt við internetið" - "Netkerfið er ekki tengt við internetið" - "Ekki næst í DNS-einkaþjón" - "Tengigeta %1$s er takmörkuð" - "Ýttu til að tengjast samt" - "Skipt yfir á %1$s" - "Tækið notar %1$s þegar %2$s er ekki með internetaðgang. Gjöld kunna að eiga við." - "Skipt úr %1$s yfir í %2$s" + "%1$s er ekki með internetaðgang" + "Ýttu til að sjá valkosti" + "Farsímakerfið er ekki tengt við internetið" + "Netkerfið er ekki tengt við internetið" + "Ekki næst í DNS-einkaþjón" + "Tengigeta %1$s er takmörkuð" + "Ýttu til að tengjast samt" + "Skipt yfir á %1$s" + "Tækið notar %1$s þegar %2$s er ekki með internetaðgang. Gjöld kunna að eiga við." + "Skipt úr %1$s yfir í %2$s" - "farsímagögn" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "farsímagögn" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "óþekkt tegund netkerfis" + "óþekkt tegund netkerfis" diff --git a/service/ServiceConnectivityResources/res/values-it/strings.xml b/service/ServiceConnectivityResources/res/values-it/strings.xml index bcac393c9c..ec3ff8c711 100644 --- a/service/ServiceConnectivityResources/res/values-it/strings.xml +++ b/service/ServiceConnectivityResources/res/values-it/strings.xml @@ -17,27 +17,27 @@ - "Risorse per connettività di sistema" - "Accedi a rete Wi-Fi" - "Accedi alla rete" - + "Risorse per connettività di sistema" + "Accedi a rete Wi-Fi" + "Accedi alla rete" + - "%1$s non ha accesso a Internet" - "Tocca per le opzioni" - "La rete mobile non ha accesso a Internet" - "La rete non ha accesso a Internet" - "Non è possibile accedere al server DNS privato" - "%1$s ha una connettività limitata" - "Tocca per connettere comunque" - "Passato a %1$s" - "Il dispositivo utilizza %1$s quando %2$s non ha accesso a Internet. Potrebbero essere applicati costi." - "Passato da %1$s a %2$s" + "%1$s non ha accesso a Internet" + "Tocca per le opzioni" + "La rete mobile non ha accesso a Internet" + "La rete non ha accesso a Internet" + "Non è possibile accedere al server DNS privato" + "%1$s ha una connettività limitata" + "Tocca per connettere comunque" + "Passato a %1$s" + "Il dispositivo utilizza %1$s quando %2$s non ha accesso a Internet. Potrebbero essere applicati costi." + "Passato da %1$s a %2$s" - "dati mobili" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "dati mobili" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "tipo di rete sconosciuto" + "tipo di rete sconosciuto" diff --git a/service/ServiceConnectivityResources/res/values-iw/strings.xml b/service/ServiceConnectivityResources/res/values-iw/strings.xml index d6684ce106..d123ebba03 100644 --- a/service/ServiceConnectivityResources/res/values-iw/strings.xml +++ b/service/ServiceConnectivityResources/res/values-iw/strings.xml @@ -17,27 +17,27 @@ - "משאבי קישוריות מערכת" - "‏היכנס לרשת Wi-Fi" - "היכנס לרשת" - + "משאבי קישוריות מערכת" + "‏היכנס לרשת Wi-Fi" + "היכנס לרשת" + - "ל-%1$s אין גישה לאינטרנט" - "הקש לקבלת אפשרויות" - "לרשת הסלולרית אין גישה לאינטרנט" - "לרשת אין גישה לאינטרנט" - "‏לא ניתן לגשת לשרת DNS הפרטי" - "הקישוריות של %1$s מוגבלת" - "כדי להתחבר למרות זאת יש להקיש" - "מעבר אל %1$s" - "המכשיר משתמש ברשת %1$s כשלרשת %2$s אין גישה לאינטרנט. עשויים לחול חיובים." - "עבר מרשת %1$s לרשת %2$s" + "ל-%1$s אין גישה לאינטרנט" + "הקש לקבלת אפשרויות" + "לרשת הסלולרית אין גישה לאינטרנט" + "לרשת אין גישה לאינטרנט" + "‏לא ניתן לגשת לשרת DNS הפרטי" + "הקישוריות של %1$s מוגבלת" + "כדי להתחבר למרות זאת יש להקיש" + "מעבר אל %1$s" + "המכשיר משתמש ברשת %1$s כשלרשת %2$s אין גישה לאינטרנט. עשויים לחול חיובים." + "עבר מרשת %1$s לרשת %2$s" - "חבילת גלישה" - "Wi-Fi" - "Bluetooth" - "אתרנט" - "VPN" + "חבילת גלישה" + "Wi-Fi" + "Bluetooth" + "אתרנט" + "VPN" - "סוג רשת לא מזוהה" + "סוג רשת לא מזוהה" diff --git a/service/ServiceConnectivityResources/res/values-ja/strings.xml b/service/ServiceConnectivityResources/res/values-ja/strings.xml index fa4a30aedb..7bb6f85620 100644 --- a/service/ServiceConnectivityResources/res/values-ja/strings.xml +++ b/service/ServiceConnectivityResources/res/values-ja/strings.xml @@ -17,27 +17,27 @@ - "システム接続リソース" - "Wi-Fiネットワークにログイン" - "ネットワークにログインしてください" - + "システム接続リソース" + "Wi-Fiネットワークにログイン" + "ネットワークにログインしてください" + - "%1$s はインターネットにアクセスできません" - "タップしてその他のオプションを表示" - "モバイル ネットワークがインターネットに接続されていません" - "ネットワークがインターネットに接続されていません" - "プライベート DNS サーバーにアクセスできません" - "%1$s の接続が制限されています" - "接続するにはタップしてください" - "「%1$s」に切り替えました" - "デバイスで「%2$s」によるインターネット接続ができない場合に「%1$s」を使用します。通信料が発生することがあります。" - "「%1$s」から「%2$s」に切り替えました" + "%1$s はインターネットにアクセスできません" + "タップしてその他のオプションを表示" + "モバイル ネットワークがインターネットに接続されていません" + "ネットワークがインターネットに接続されていません" + "プライベート DNS サーバーにアクセスできません" + "%1$s の接続が制限されています" + "接続するにはタップしてください" + "「%1$s」に切り替えました" + "デバイスで「%2$s」によるインターネット接続ができない場合に「%1$s」を使用します。通信料が発生することがあります。" + "「%1$s」から「%2$s」に切り替えました" - "モバイルデータ" - "Wi-Fi" - "Bluetooth" - "イーサネット" - "VPN" + "モバイルデータ" + "Wi-Fi" + "Bluetooth" + "イーサネット" + "VPN" - "不明なネットワーク タイプ" + "不明なネットワーク タイプ" diff --git a/service/ServiceConnectivityResources/res/values-ka/strings.xml b/service/ServiceConnectivityResources/res/values-ka/strings.xml index 418331036e..f42c567147 100644 --- a/service/ServiceConnectivityResources/res/values-ka/strings.xml +++ b/service/ServiceConnectivityResources/res/values-ka/strings.xml @@ -17,27 +17,27 @@ - "სისტემის კავშირის რესურსები" - "Wi-Fi ქსელთან დაკავშირება" - "ქსელში შესვლა" - + "სისტემის კავშირის რესურსები" + "Wi-Fi ქსელთან დაკავშირება" + "ქსელში შესვლა" + - "%1$s-ს არ აქვს ინტერნეტზე წვდომა" - "შეეხეთ ვარიანტების სანახავად" - "მობილურ ქსელს არ აქვს ინტერნეტზე წვდომა" - "ქსელს არ აქვს ინტერნეტზე წვდომა" - "პირად DNS სერვერზე წვდომა შეუძლებელია" - "%1$s-ის კავშირები შეზღუდულია" - "შეეხეთ, თუ მაინც გსურთ დაკავშირება" - "ახლა გამოიყენება %1$s" - "თუ %2$s ინტერნეტთან კავშირს დაკარგავს, მოწყობილობის მიერ %1$s იქნება გამოყენებული, რამაც შეიძლება დამატებითი ხარჯები გამოიწვიოს." - "ახლა გამოიყენება %1$s (გამოიყენებოდა %2$s)" + "%1$s-ს არ აქვს ინტერნეტზე წვდომა" + "შეეხეთ ვარიანტების სანახავად" + "მობილურ ქსელს არ აქვს ინტერნეტზე წვდომა" + "ქსელს არ აქვს ინტერნეტზე წვდომა" + "პირად DNS სერვერზე წვდომა შეუძლებელია" + "%1$s-ის კავშირები შეზღუდულია" + "შეეხეთ, თუ მაინც გსურთ დაკავშირება" + "ახლა გამოიყენება %1$s" + "თუ %2$s ინტერნეტთან კავშირს დაკარგავს, მოწყობილობის მიერ %1$s იქნება გამოყენებული, რამაც შეიძლება დამატებითი ხარჯები გამოიწვიოს." + "ახლა გამოიყენება %1$s (გამოიყენებოდა %2$s)" - "მობილური ინტერნეტი" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "მობილური ინტერნეტი" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "უცნობი ტიპის ქსელი" + "უცნობი ტიპის ქსელი" diff --git a/service/ServiceConnectivityResources/res/values-kk/strings.xml b/service/ServiceConnectivityResources/res/values-kk/strings.xml index 54d5eb30cd..00c0f3926e 100644 --- a/service/ServiceConnectivityResources/res/values-kk/strings.xml +++ b/service/ServiceConnectivityResources/res/values-kk/strings.xml @@ -17,27 +17,27 @@ - "Жүйе байланысы ресурстары" - "Wi-Fi желісіне кіру" - "Желіге кіру" - + "Жүйе байланысы ресурстары" + "Wi-Fi желісіне кіру" + "Желіге кіру" + - "%1$s желісінің интернетті пайдалану мүмкіндігі шектеулі." - "Опциялар үшін түртіңіз" - "Мобильдік желі интернетке қосылмаған." - "Желі интернетке қосылмаған." - "Жеке DNS серверіне кіру мүмкін емес." - "%1$s желісінің қосылу мүмкіндігі шектеулі." - "Бәрібір жалғау үшін түртіңіз." - "%1$s желісіне ауысты" - "Құрылғы %2$s желісінде интернетпен байланыс жоғалған жағдайда %1$s желісін пайдаланады. Деректер ақысы алынуы мүмкін." - "%1$s желісінен %2$s желісіне ауысты" + "%1$s желісінің интернетті пайдалану мүмкіндігі шектеулі." + "Опциялар үшін түртіңіз" + "Мобильдік желі интернетке қосылмаған." + "Желі интернетке қосылмаған." + "Жеке DNS серверіне кіру мүмкін емес." + "%1$s желісінің қосылу мүмкіндігі шектеулі." + "Бәрібір жалғау үшін түртіңіз." + "%1$s желісіне ауысты" + "Құрылғы %2$s желісінде интернетпен байланыс жоғалған жағдайда %1$s желісін пайдаланады. Деректер ақысы алынуы мүмкін." + "%1$s желісінен %2$s желісіне ауысты" - "мобильдік деректер" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "мобильдік деректер" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "желі түрі белгісіз" + "желі түрі белгісіз" diff --git a/service/ServiceConnectivityResources/res/values-km/strings.xml b/service/ServiceConnectivityResources/res/values-km/strings.xml index bd778a1623..fa06c5b421 100644 --- a/service/ServiceConnectivityResources/res/values-km/strings.xml +++ b/service/ServiceConnectivityResources/res/values-km/strings.xml @@ -17,27 +17,27 @@ - "ធនធាន​តភ្ជាប់​ប្រព័ន្ធ" - "ចូល​បណ្ដាញ​វ៉ាយហ្វាយ" - "ចូលទៅបណ្តាញ" - + "ធនធាន​តភ្ជាប់​ប្រព័ន្ធ" + "ចូល​បណ្ដាញ​វ៉ាយហ្វាយ" + "ចូលទៅបណ្តាញ" + - "%1$s មិនមាន​ការតភ្ជាប់អ៊ីនធឺណិត​ទេ" - "ប៉ះសម្រាប់ជម្រើស" - "បណ្ដាញ​ទូរសព្ទ​ចល័ត​មិនមានការតភ្ជាប់​អ៊ីនធឺណិតទេ" - "បណ្ដាញ​មិនមាន​ការតភ្ជាប់​អ៊ីនធឺណិតទេ" - "មិនអាច​ចូលប្រើ​ម៉ាស៊ីនមេ DNS ឯកជន​បានទេ" - "%1$s មានការតភ្ជាប់​មានកម្រិត" - "មិន​អី​ទេ ចុច​​ភ្ជាប់​ចុះ" - "បានប្តូរទៅ %1$s" - "ឧបករណ៍​ប្រើ %1$s នៅ​ពេល​ដែល %2$s មិនមាន​ការ​តភ្ជាប់​អ៊ីនធឺណិត។ អាច​គិតថ្លៃ​លើការ​ប្រើប្រាស់​ទិន្នន័យ។" - "បានប្តូរពី %1$s ទៅ %2$s" + "%1$s មិនមាន​ការតភ្ជាប់អ៊ីនធឺណិត​ទេ" + "ប៉ះសម្រាប់ជម្រើស" + "បណ្ដាញ​ទូរសព្ទ​ចល័ត​មិនមានការតភ្ជាប់​អ៊ីនធឺណិតទេ" + "បណ្ដាញ​មិនមាន​ការតភ្ជាប់​អ៊ីនធឺណិតទេ" + "មិនអាច​ចូលប្រើ​ម៉ាស៊ីនមេ DNS ឯកជន​បានទេ" + "%1$s មានការតភ្ជាប់​មានកម្រិត" + "មិន​អី​ទេ ចុច​​ភ្ជាប់​ចុះ" + "បានប្តូរទៅ %1$s" + "ឧបករណ៍​ប្រើ %1$s នៅ​ពេល​ដែល %2$s មិនមាន​ការ​តភ្ជាប់​អ៊ីនធឺណិត។ អាច​គិតថ្លៃ​លើការ​ប្រើប្រាស់​ទិន្នន័យ។" + "បានប្តូរពី %1$s ទៅ %2$s" - "ទិន្នន័យ​ទូរសព្ទចល័ត" - "Wi-Fi" - "ប៊្លូធូស" - "អ៊ីសឺរណិត" - "VPN" + "ទិន្នន័យ​ទូរសព្ទចល័ត" + "Wi-Fi" + "ប៊្លូធូស" + "អ៊ីសឺរណិត" + "VPN" - "ប្រភេទបណ្តាញដែលមិនស្គាល់" + "ប្រភេទបណ្តាញដែលមិនស្គាល់" diff --git a/service/ServiceConnectivityResources/res/values-kn/strings.xml b/service/ServiceConnectivityResources/res/values-kn/strings.xml index 7f3a420dac..cde8facd83 100644 --- a/service/ServiceConnectivityResources/res/values-kn/strings.xml +++ b/service/ServiceConnectivityResources/res/values-kn/strings.xml @@ -17,27 +17,27 @@ - "ಸಿಸ್ಟಂ ಸಂಪರ್ಕ ಕಲ್ಪಿಸುವಿಕೆ ಮಾಹಿತಿಯ ಮೂಲಗಳು" - "ವೈ-ಫೈ ನೆಟ್‍ವರ್ಕ್‌ಗೆ ಸೈನ್ ಇನ್ ಮಾಡಿ" - "ನೆಟ್‌ವರ್ಕ್‌ಗೆ ಸೈನ್ ಇನ್ ಮಾಡಿ" - + "ಸಿಸ್ಟಂ ಸಂಪರ್ಕ ಕಲ್ಪಿಸುವಿಕೆ ಮಾಹಿತಿಯ ಮೂಲಗಳು" + "ವೈ-ಫೈ ನೆಟ್‍ವರ್ಕ್‌ಗೆ ಸೈನ್ ಇನ್ ಮಾಡಿ" + "ನೆಟ್‌ವರ್ಕ್‌ಗೆ ಸೈನ್ ಇನ್ ಮಾಡಿ" + - "%1$s ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಸಂಪರ್ಕವನ್ನು ಹೊಂದಿಲ್ಲ" - "ಆಯ್ಕೆಗಳಿಗೆ ಟ್ಯಾಪ್ ಮಾಡಿ" - "ಮೊಬೈಲ್ ನೆಟ್‌ವರ್ಕ್‌ ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಪ್ರವೇಶವನ್ನು ಹೊಂದಿಲ್ಲ" - "ನೆಟ್‌ವರ್ಕ್‌ ಇಂಟರ್ನೆಟ್‌ ಪ್ರವೇಶವನ್ನು ಹೊಂದಿಲ್ಲ" - "ಖಾಸಗಿ DNS ಸರ್ವರ್ ಅನ್ನು ಪ್ರವೇಶಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ" - "%1$s ಸೀಮಿತ ಸಂಪರ್ಕ ಕಲ್ಪಿಸುವಿಕೆಯನ್ನು ಹೊಂದಿದೆ" - "ಹೇಗಾದರೂ ಸಂಪರ್ಕಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ" - "%1$s ಗೆ ಬದಲಾಯಿಸಲಾಗಿದೆ" - "%2$s ಇಂಟರ್ನೆಟ್ ಪ್ರವೇಶ ಹೊಂದಿಲ್ಲದಿರುವಾಗ, ಸಾಧನವು %1$s ಬಳಸುತ್ತದೆ. ಶುಲ್ಕಗಳು ಅನ್ವಯವಾಗಬಹುದು." - "%1$s ರಿಂದ %2$s ಗೆ ಬದಲಾಯಿಸಲಾಗಿದೆ" + "%1$s ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಸಂಪರ್ಕವನ್ನು ಹೊಂದಿಲ್ಲ" + "ಆಯ್ಕೆಗಳಿಗೆ ಟ್ಯಾಪ್ ಮಾಡಿ" + "ಮೊಬೈಲ್ ನೆಟ್‌ವರ್ಕ್‌ ಯಾವುದೇ ಇಂಟರ್ನೆಟ್ ಪ್ರವೇಶವನ್ನು ಹೊಂದಿಲ್ಲ" + "ನೆಟ್‌ವರ್ಕ್‌ ಇಂಟರ್ನೆಟ್‌ ಪ್ರವೇಶವನ್ನು ಹೊಂದಿಲ್ಲ" + "ಖಾಸಗಿ DNS ಸರ್ವರ್ ಅನ್ನು ಪ್ರವೇಶಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ" + "%1$s ಸೀಮಿತ ಸಂಪರ್ಕ ಕಲ್ಪಿಸುವಿಕೆಯನ್ನು ಹೊಂದಿದೆ" + "ಹೇಗಾದರೂ ಸಂಪರ್ಕಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ" + "%1$s ಗೆ ಬದಲಾಯಿಸಲಾಗಿದೆ" + "%2$s ಇಂಟರ್ನೆಟ್ ಪ್ರವೇಶ ಹೊಂದಿಲ್ಲದಿರುವಾಗ, ಸಾಧನವು %1$s ಬಳಸುತ್ತದೆ. ಶುಲ್ಕಗಳು ಅನ್ವಯವಾಗಬಹುದು." + "%1$s ರಿಂದ %2$s ಗೆ ಬದಲಾಯಿಸಲಾಗಿದೆ" - "ಮೊಬೈಲ್ ಡೇಟಾ" - "ವೈ-ಫೈ" - "ಬ್ಲೂಟೂತ್" - "ಇಥರ್ನೆಟ್" - "VPN" + "ಮೊಬೈಲ್ ಡೇಟಾ" + "ವೈ-ಫೈ" + "ಬ್ಲೂಟೂತ್" + "ಇಥರ್ನೆಟ್" + "VPN" - "ಅಪರಿಚಿತ ನೆಟ್‌ವರ್ಕ್ ಪ್ರಕಾರ" + "ಅಪರಿಚಿತ ನೆಟ್‌ವರ್ಕ್ ಪ್ರಕಾರ" diff --git a/service/ServiceConnectivityResources/res/values-ko/strings.xml b/service/ServiceConnectivityResources/res/values-ko/strings.xml index a763cc5e9c..eef59a9289 100644 --- a/service/ServiceConnectivityResources/res/values-ko/strings.xml +++ b/service/ServiceConnectivityResources/res/values-ko/strings.xml @@ -17,27 +17,27 @@ - "시스템 연결 리소스" - "Wi-Fi 네트워크에 로그인" - "네트워크에 로그인" - + "시스템 연결 리소스" + "Wi-Fi 네트워크에 로그인" + "네트워크에 로그인" + - "%1$s이(가) 인터넷에 액세스할 수 없습니다." - "탭하여 옵션 보기" - "모바일 네트워크에 인터넷이 연결되어 있지 않습니다." - "네트워크에 인터넷이 연결되어 있지 않습니다." - "비공개 DNS 서버에 액세스할 수 없습니다." - "%1$s에서 연결을 제한했습니다." - "계속 연결하려면 탭하세요." - "%1$s(으)로 전환" - "%2$s(으)로 인터넷에 연결할 수 없는 경우 기기에서 %1$s이(가) 사용됩니다. 요금이 부과될 수 있습니다." - "%1$s에서 %2$s(으)로 전환" + "%1$s이(가) 인터넷에 액세스할 수 없습니다." + "탭하여 옵션 보기" + "모바일 네트워크에 인터넷이 연결되어 있지 않습니다." + "네트워크에 인터넷이 연결되어 있지 않습니다." + "비공개 DNS 서버에 액세스할 수 없습니다." + "%1$s에서 연결을 제한했습니다." + "계속 연결하려면 탭하세요." + "%1$s(으)로 전환" + "%2$s(으)로 인터넷에 연결할 수 없는 경우 기기에서 %1$s이(가) 사용됩니다. 요금이 부과될 수 있습니다." + "%1$s에서 %2$s(으)로 전환" - "모바일 데이터" - "Wi-Fi" - "블루투스" - "이더넷" - "VPN" + "모바일 데이터" + "Wi-Fi" + "블루투스" + "이더넷" + "VPN" - "알 수 없는 네트워크 유형" + "알 수 없는 네트워크 유형" diff --git a/service/ServiceConnectivityResources/res/values-ky/strings.xml b/service/ServiceConnectivityResources/res/values-ky/strings.xml index 3550af80cd..9ff53cfc3d 100644 --- a/service/ServiceConnectivityResources/res/values-ky/strings.xml +++ b/service/ServiceConnectivityResources/res/values-ky/strings.xml @@ -17,27 +17,27 @@ - "Тутумдун байланыш булагы" - "Wi-Fi түйүнүнө кирүү" - "Тармакка кирүү" - + "Тутумдун байланыш булагы" + "Wi-Fi түйүнүнө кирүү" + "Тармакка кирүү" + - "%1$s Интернетке туташуусу жок" - "Параметрлерди ачуу үчүн таптап коюңуз" - "Мобилдик Интернет жок" - "Тармактын Интернет жок" - "Жеке DNS сервери жеткиликсиз" - "%1$s байланышы чектелген" - "Баары бир туташуу үчүн таптаңыз" - "%1$s тармагына которуштурулду" - "%2$s тармагы Интернетке туташпай турганда, түзмөгүңүз %1$s тармагын колдонот. Акы алынышы мүмкүн." - "%1$s дегенден %2$s тармагына которуштурулду" + "%1$s Интернетке туташуусу жок" + "Параметрлерди ачуу үчүн таптап коюңуз" + "Мобилдик Интернет жок" + "Тармактын Интернет жок" + "Жеке DNS сервери жеткиликсиз" + "%1$s байланышы чектелген" + "Баары бир туташуу үчүн таптаңыз" + "%1$s тармагына которуштурулду" + "%2$s тармагы Интернетке туташпай турганда, түзмөгүңүз %1$s тармагын колдонот. Акы алынышы мүмкүн." + "%1$s дегенден %2$s тармагына которуштурулду" - "мобилдик трафик" - "Wi‑Fi" - "Bluetooth" - "Ethernet" - "VPN" + "мобилдик трафик" + "Wi‑Fi" + "Bluetooth" + "Ethernet" + "VPN" - "белгисиз тармак түрү" + "белгисиз тармак түрү" diff --git a/service/ServiceConnectivityResources/res/values-lo/strings.xml b/service/ServiceConnectivityResources/res/values-lo/strings.xml index 4b3056f540..64419f9e96 100644 --- a/service/ServiceConnectivityResources/res/values-lo/strings.xml +++ b/service/ServiceConnectivityResources/res/values-lo/strings.xml @@ -17,27 +17,27 @@ - "ແຫຼ່ງຂໍ້ມູນການເຊື່ອມຕໍ່ລະບົບ" - "ເຂົ້າສູ່ລະບົບເຄືອຂ່າຍ Wi-Fi" - "ລົງຊື່ເຂົ້າເຄືອຂ່າຍ" - + "ແຫຼ່ງຂໍ້ມູນການເຊື່ອມຕໍ່ລະບົບ" + "ເຂົ້າສູ່ລະບົບເຄືອຂ່າຍ Wi-Fi" + "ລົງຊື່ເຂົ້າເຄືອຂ່າຍ" + - "%1$s ບໍ່ມີການເຊື່ອມຕໍ່ອິນເຕີເນັດ" - "ແຕະເພື່ອເບິ່ງຕົວເລືອກ" - "ເຄືອຂ່າຍມືຖືບໍ່ສາມາດເຂົ້າເຖິງອິນເຕີເນັດໄດ້" - "ເຄືອຂ່າຍບໍ່ສາມາດເຂົ້າເຖິງອິນເຕີເນັດໄດ້" - "ບໍ່ສາມາດເຂົ້າເຖິງເຊີບເວີ DNS ສ່ວນຕົວໄດ້" - "%1$s ມີການເຊື່ອມຕໍ່ທີ່ຈຳກັດ" - "ແຕະເພື່ອຢືນຢັນການເຊື່ອມຕໍ່" - "ສະຫຼັບໄປໃຊ້ %1$s ແລ້ວ" - "ອຸປະກອນຈະໃຊ້ %1$s ເມື່ອ %2$s ບໍ່ມີການເຊື່ອມຕໍ່ອິນເຕີເນັດ. ອາດມີການຮຽກເກັບຄ່າບໍລິການ." - "ສະຫຼັບຈາກ %1$s ໄປໃຊ້ %2$s ແລ້ວ" + "%1$s ບໍ່ມີການເຊື່ອມຕໍ່ອິນເຕີເນັດ" + "ແຕະເພື່ອເບິ່ງຕົວເລືອກ" + "ເຄືອຂ່າຍມືຖືບໍ່ສາມາດເຂົ້າເຖິງອິນເຕີເນັດໄດ້" + "ເຄືອຂ່າຍບໍ່ສາມາດເຂົ້າເຖິງອິນເຕີເນັດໄດ້" + "ບໍ່ສາມາດເຂົ້າເຖິງເຊີບເວີ DNS ສ່ວນຕົວໄດ້" + "%1$s ມີການເຊື່ອມຕໍ່ທີ່ຈຳກັດ" + "ແຕະເພື່ອຢືນຢັນການເຊື່ອມຕໍ່" + "ສະຫຼັບໄປໃຊ້ %1$s ແລ້ວ" + "ອຸປະກອນຈະໃຊ້ %1$s ເມື່ອ %2$s ບໍ່ມີການເຊື່ອມຕໍ່ອິນເຕີເນັດ. ອາດມີການຮຽກເກັບຄ່າບໍລິການ." + "ສະຫຼັບຈາກ %1$s ໄປໃຊ້ %2$s ແລ້ວ" - "ອິນເຕີເນັດມືຖື" - "Wi-Fi" - "Bluetooth" - "ອີເທີເນັດ" - "VPN" + "ອິນເຕີເນັດມືຖື" + "Wi-Fi" + "Bluetooth" + "ອີເທີເນັດ" + "VPN" - "ບໍ່ຮູ້ຈັກປະເພດເຄືອຂ່າຍ" + "ບໍ່ຮູ້ຈັກປະເພດເຄືອຂ່າຍ" diff --git a/service/ServiceConnectivityResources/res/values-lt/strings.xml b/service/ServiceConnectivityResources/res/values-lt/strings.xml index 8eb41f1efa..f73f142766 100644 --- a/service/ServiceConnectivityResources/res/values-lt/strings.xml +++ b/service/ServiceConnectivityResources/res/values-lt/strings.xml @@ -17,27 +17,27 @@ - "System Connectivity Resources" - "Prisijungti prie „Wi-Fi“ tinklo" - "Prisijungti prie tinklo" - + "System Connectivity Resources" + "Prisijungti prie „Wi-Fi“ tinklo" + "Prisijungti prie tinklo" + - "„%1$s“ negali pasiekti interneto" - "Palieskite, kad būtų rodomos parinktys." - "Mobiliojo ryšio tinkle nėra prieigos prie interneto" - "Tinkle nėra prieigos prie interneto" - "Privataus DNS serverio negalima pasiekti" - "„%1$s“ ryšys apribotas" - "Palieskite, jei vis tiek norite prisijungti" - "Perjungta į tinklą %1$s" - "Įrenginyje naudojamas kitas tinklas (%1$s), kai dabartiniame tinkle (%2$s) nėra interneto ryšio. Gali būti taikomi mokesčiai." - "Perjungta iš tinklo %1$s į tinklą %2$s" + "„%1$s“ negali pasiekti interneto" + "Palieskite, kad būtų rodomos parinktys." + "Mobiliojo ryšio tinkle nėra prieigos prie interneto" + "Tinkle nėra prieigos prie interneto" + "Privataus DNS serverio negalima pasiekti" + "„%1$s“ ryšys apribotas" + "Palieskite, jei vis tiek norite prisijungti" + "Perjungta į tinklą %1$s" + "Įrenginyje naudojamas kitas tinklas (%1$s), kai dabartiniame tinkle (%2$s) nėra interneto ryšio. Gali būti taikomi mokesčiai." + "Perjungta iš tinklo %1$s į tinklą %2$s" - "mobiliojo ryšio duomenys" - "Wi-Fi" - "Bluetooth" - "Eternetas" - "VPN" + "mobiliojo ryšio duomenys" + "Wi-Fi" + "Bluetooth" + "Eternetas" + "VPN" - "nežinomas tinklo tipas" + "nežinomas tinklo tipas" diff --git a/service/ServiceConnectivityResources/res/values-lv/strings.xml b/service/ServiceConnectivityResources/res/values-lv/strings.xml index 0647a4fb07..9d26c40aa6 100644 --- a/service/ServiceConnectivityResources/res/values-lv/strings.xml +++ b/service/ServiceConnectivityResources/res/values-lv/strings.xml @@ -17,27 +17,27 @@ - "Sistēmas savienojamības resursi" - "Pierakstieties Wi-Fi tīklā" - "Pierakstīšanās tīklā" - + "Sistēmas savienojamības resursi" + "Pierakstieties Wi-Fi tīklā" + "Pierakstīšanās tīklā" + - "Tīklā %1$s nav piekļuves internetam" - "Pieskarieties, lai skatītu iespējas." - "Mobilajā tīklā nav piekļuves internetam." - "Tīklā nav piekļuves internetam." - "Nevar piekļūt privātam DNS serverim." - "Tīklā %1$s ir ierobežota savienojamība" - "Lai tik un tā izveidotu savienojumu, pieskarieties" - "Pārslēdzās uz tīklu %1$s" - "Kad vienā tīklā (%2$s) nav piekļuves internetam, ierīcē tiek izmantots cits tīkls (%1$s). Var tikt piemērota maksa." - "Pārslēdzās no tīkla %1$s uz tīklu %2$s" + "Tīklā %1$s nav piekļuves internetam" + "Pieskarieties, lai skatītu iespējas." + "Mobilajā tīklā nav piekļuves internetam." + "Tīklā nav piekļuves internetam." + "Nevar piekļūt privātam DNS serverim." + "Tīklā %1$s ir ierobežota savienojamība" + "Lai tik un tā izveidotu savienojumu, pieskarieties" + "Pārslēdzās uz tīklu %1$s" + "Kad vienā tīklā (%2$s) nav piekļuves internetam, ierīcē tiek izmantots cits tīkls (%1$s). Var tikt piemērota maksa." + "Pārslēdzās no tīkla %1$s uz tīklu %2$s" - "mobilie dati" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobilie dati" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "nezināms tīkla veids" + "nezināms tīkla veids" diff --git a/service/ServiceConnectivityResources/res/values-mk/strings.xml b/service/ServiceConnectivityResources/res/values-mk/strings.xml index b0024e296c..fb105e0f79 100644 --- a/service/ServiceConnectivityResources/res/values-mk/strings.xml +++ b/service/ServiceConnectivityResources/res/values-mk/strings.xml @@ -17,27 +17,27 @@ - "System Connectivity Resources" - "Најавете се на мрежа на Wi-Fi" - "Најавете се на мрежа" - + "System Connectivity Resources" + "Најавете се на мрежа на Wi-Fi" + "Најавете се на мрежа" + - "%1$s нема интернет-пристап" - "Допрете за опции" - "Мобилната мрежа нема интернет-пристап" - "Мрежата нема интернет-пристап" - "Не може да се пристапи до приватниот DNS-сервер" - "%1$s има ограничена поврзливост" - "Допрете за да се поврзете и покрај тоа" - "Префрлено на %1$s" - "Уредот користи %1$s кога %2$s нема пристап до интернет. Може да се наплатат трошоци." - "Префрлено од %1$s на %2$s" + "%1$s нема интернет-пристап" + "Допрете за опции" + "Мобилната мрежа нема интернет-пристап" + "Мрежата нема интернет-пристап" + "Не може да се пристапи до приватниот DNS-сервер" + "%1$s има ограничена поврзливост" + "Допрете за да се поврзете и покрај тоа" + "Префрлено на %1$s" + "Уредот користи %1$s кога %2$s нема пристап до интернет. Може да се наплатат трошоци." + "Префрлено од %1$s на %2$s" - "мобилен интернет" - "Wi-Fi" - "Bluetooth" - "Етернет" - "VPN" + "мобилен интернет" + "Wi-Fi" + "Bluetooth" + "Етернет" + "VPN" - "непознат тип мрежа" + "непознат тип мрежа" diff --git a/service/ServiceConnectivityResources/res/values-ml/strings.xml b/service/ServiceConnectivityResources/res/values-ml/strings.xml index 8ce7667fbf..9a512389c1 100644 --- a/service/ServiceConnectivityResources/res/values-ml/strings.xml +++ b/service/ServiceConnectivityResources/res/values-ml/strings.xml @@ -17,27 +17,27 @@ - "സിസ്‌റ്റം കണക്‌റ്റിവിറ്റി ഉറവിടങ്ങൾ" - "വൈഫൈ നെറ്റ്‌വർക്കിലേക്ക് സൈൻ ഇൻ ചെയ്യുക" - "നെറ്റ്‌വർക്കിലേക്ക് സൈൻ ഇൻ ചെയ്യുക" - + "സിസ്‌റ്റം കണക്‌റ്റിവിറ്റി ഉറവിടങ്ങൾ" + "വൈഫൈ നെറ്റ്‌വർക്കിലേക്ക് സൈൻ ഇൻ ചെയ്യുക" + "നെറ്റ്‌വർക്കിലേക്ക് സൈൻ ഇൻ ചെയ്യുക" + - "%1$s എന്നതിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല" - "ഓപ്ഷനുകൾക്ക് ടാപ്പുചെയ്യുക" - "മൊബെെൽ നെറ്റ്‌വർക്കിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല" - "നെറ്റ്‌വർക്കിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല" - "സ്വകാര്യ DNS സെർവർ ആക്‌സസ് ചെയ്യാനാവില്ല" - "%1$s എന്നതിന് പരിമിതമായ കണക്റ്റിവിറ്റി ഉണ്ട്" - "ഏതുവിധേനയും കണക്‌റ്റ് ചെയ്യാൻ ടാപ്പ് ചെയ്യുക" - "%1$s എന്നതിലേക്ക് മാറി" - "%2$s-ന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ലാത്തപ്പോൾ ഉപകരണം %1$s ഉപയോഗിക്കുന്നു. നിരക്കുകൾ ബാധകമായേക്കാം." - "%1$s നെറ്റ്‌വർക്കിൽ നിന്ന് %2$s നെറ്റ്‌വർക്കിലേക്ക് മാറി" + "%1$s എന്നതിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല" + "ഓപ്ഷനുകൾക്ക് ടാപ്പുചെയ്യുക" + "മൊബെെൽ നെറ്റ്‌വർക്കിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല" + "നെറ്റ്‌വർക്കിന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ല" + "സ്വകാര്യ DNS സെർവർ ആക്‌സസ് ചെയ്യാനാവില്ല" + "%1$s എന്നതിന് പരിമിതമായ കണക്റ്റിവിറ്റി ഉണ്ട്" + "ഏതുവിധേനയും കണക്‌റ്റ് ചെയ്യാൻ ടാപ്പ് ചെയ്യുക" + "%1$s എന്നതിലേക്ക് മാറി" + "%2$s-ന് ഇന്റർനെറ്റ് ആക്‌സസ് ഇല്ലാത്തപ്പോൾ ഉപകരണം %1$s ഉപയോഗിക്കുന്നു. നിരക്കുകൾ ബാധകമായേക്കാം." + "%1$s നെറ്റ്‌വർക്കിൽ നിന്ന് %2$s നെറ്റ്‌വർക്കിലേക്ക് മാറി" - "മൊബൈൽ ഡാറ്റ" - "വൈഫൈ" - "Bluetooth" - "ഇതർനെറ്റ്" - "VPN" + "മൊബൈൽ ഡാറ്റ" + "വൈഫൈ" + "Bluetooth" + "ഇതർനെറ്റ്" + "VPN" - "അജ്ഞാതമായ നെറ്റ്‌വർക്ക് തരം" + "അജ്ഞാതമായ നെറ്റ്‌വർക്ക് തരം" diff --git a/service/ServiceConnectivityResources/res/values-mn/strings.xml b/service/ServiceConnectivityResources/res/values-mn/strings.xml index be8b59202b..83725330a8 100644 --- a/service/ServiceConnectivityResources/res/values-mn/strings.xml +++ b/service/ServiceConnectivityResources/res/values-mn/strings.xml @@ -17,27 +17,27 @@ - "Системийн холболтын нөөцүүд" - "Wi-Fi сүлжээнд нэвтэрнэ үү" - "Сүлжээнд нэвтэрнэ үү" - + "Системийн холболтын нөөцүүд" + "Wi-Fi сүлжээнд нэвтэрнэ үү" + "Сүлжээнд нэвтэрнэ үү" + - "%1$s-д интернэтийн хандалт алга" - "Сонголт хийхийн тулд товшино уу" - "Мобайл сүлжээнд интернэт хандалт байхгүй байна" - "Сүлжээнд интернэт хандалт байхгүй байна" - "Хувийн DNS серверт хандах боломжгүй байна" - "%1$s зарим үйлчилгээнд хандах боломжгүй байна" - "Ямар ч тохиолдолд холбогдохын тулд товших" - "%1$s руу шилжүүлсэн" - "%2$s интернет холболтгүй үед төхөөрөмж %1$s-г ашигладаг. Төлбөр гарч болзошгүй." - "%1$s%2$s руу шилжүүлсэн" + "%1$s-д интернэтийн хандалт алга" + "Сонголт хийхийн тулд товшино уу" + "Мобайл сүлжээнд интернэт хандалт байхгүй байна" + "Сүлжээнд интернэт хандалт байхгүй байна" + "Хувийн DNS серверт хандах боломжгүй байна" + "%1$s зарим үйлчилгээнд хандах боломжгүй байна" + "Ямар ч тохиолдолд холбогдохын тулд товших" + "%1$s руу шилжүүлсэн" + "%2$s интернет холболтгүй үед төхөөрөмж %1$s-г ашигладаг. Төлбөр гарч болзошгүй." + "%1$s%2$s руу шилжүүлсэн" - "мобайл дата" - "Wi-Fi" - "Bluetooth" - "Этернэт" - "VPN" + "мобайл дата" + "Wi-Fi" + "Bluetooth" + "Этернэт" + "VPN" - "үл мэдэгдэх сүлжээний төрөл" + "үл мэдэгдэх сүлжээний төрөл" diff --git a/service/ServiceConnectivityResources/res/values-mr/strings.xml b/service/ServiceConnectivityResources/res/values-mr/strings.xml index fe7df841b1..658b19b8e9 100644 --- a/service/ServiceConnectivityResources/res/values-mr/strings.xml +++ b/service/ServiceConnectivityResources/res/values-mr/strings.xml @@ -17,27 +17,27 @@ - "सिस्टम कनेक्टिव्हिटी चे स्रोत" - "वाय-फाय नेटवर्कमध्‍ये साइन इन करा" - "नेटवर्कवर साइन इन करा" - + "सिस्टम कनेक्टिव्हिटी चे स्रोत" + "वाय-फाय नेटवर्कमध्‍ये साइन इन करा" + "नेटवर्कवर साइन इन करा" + - "%1$s ला इंटरनेट अ‍ॅक्सेस नाही" - "पर्यायांसाठी टॅप करा" - "मोबाइल नेटवर्कला इंटरनेट ॲक्सेस नाही" - "नेटवर्कला इंटरनेट ॲक्सेस नाही" - "खाजगी DNS सर्व्हर ॲक्सेस करू शकत नाही" - "%1$s ला मर्यादित कनेक्टिव्हिटी आहे" - "तरीही कनेक्ट करण्यासाठी टॅप करा" - "%1$s वर स्विच केले" - "%2$s कडे इंटरनेटचा अ‍ॅक्सेस नसताना डिव्हाइस %1$s वापरते. शुल्क लागू शकते." - "%1$s वरून %2$s वर स्विच केले" + "%1$s ला इंटरनेट अ‍ॅक्सेस नाही" + "पर्यायांसाठी टॅप करा" + "मोबाइल नेटवर्कला इंटरनेट ॲक्सेस नाही" + "नेटवर्कला इंटरनेट ॲक्सेस नाही" + "खाजगी DNS सर्व्हर ॲक्सेस करू शकत नाही" + "%1$s ला मर्यादित कनेक्टिव्हिटी आहे" + "तरीही कनेक्ट करण्यासाठी टॅप करा" + "%1$s वर स्विच केले" + "%2$s कडे इंटरनेटचा अ‍ॅक्सेस नसताना डिव्हाइस %1$s वापरते. शुल्क लागू शकते." + "%1$s वरून %2$s वर स्विच केले" - "मोबाइल डेटा" - "वाय-फाय" - "ब्लूटूथ" - "इथरनेट" - "VPN" + "मोबाइल डेटा" + "वाय-फाय" + "ब्लूटूथ" + "इथरनेट" + "VPN" - "अज्ञात नेटवर्क प्रकार" + "अज्ञात नेटवर्क प्रकार" diff --git a/service/ServiceConnectivityResources/res/values-ms/strings.xml b/service/ServiceConnectivityResources/res/values-ms/strings.xml index 54b49a20f7..84b242c674 100644 --- a/service/ServiceConnectivityResources/res/values-ms/strings.xml +++ b/service/ServiceConnectivityResources/res/values-ms/strings.xml @@ -17,27 +17,27 @@ - "Sumber Kesambungan Sistem" - "Log masuk ke rangkaian Wi-Fi" - "Log masuk ke rangkaian" - + "Sumber Kesambungan Sistem" + "Log masuk ke rangkaian Wi-Fi" + "Log masuk ke rangkaian" + - "%1$s tiada akses Internet" - "Ketik untuk mendapatkan pilihan" - "Rangkaian mudah alih tiada akses Internet" - "Rangkaian tiada akses Internet" - "Pelayan DNS peribadi tidak boleh diakses" - "%1$s mempunyai kesambungan terhad" - "Ketik untuk menyambung juga" - "Beralih kepada %1$s" - "Peranti menggunakan %1$s apabila %2$s tiada akses Internet. Bayaran mungkin dikenakan." - "Beralih daripada %1$s kepada %2$s" + "%1$s tiada akses Internet" + "Ketik untuk mendapatkan pilihan" + "Rangkaian mudah alih tiada akses Internet" + "Rangkaian tiada akses Internet" + "Pelayan DNS peribadi tidak boleh diakses" + "%1$s mempunyai kesambungan terhad" + "Ketik untuk menyambung juga" + "Beralih kepada %1$s" + "Peranti menggunakan %1$s apabila %2$s tiada akses Internet. Bayaran mungkin dikenakan." + "Beralih daripada %1$s kepada %2$s" - "data mudah alih" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "data mudah alih" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "jenis rangkaian tidak diketahui" + "jenis rangkaian tidak diketahui" diff --git a/service/ServiceConnectivityResources/res/values-my/strings.xml b/service/ServiceConnectivityResources/res/values-my/strings.xml index 15b75f0fc3..683226365c 100644 --- a/service/ServiceConnectivityResources/res/values-my/strings.xml +++ b/service/ServiceConnectivityResources/res/values-my/strings.xml @@ -17,27 +17,27 @@ - "စနစ်ချိတ်ဆက်နိုင်မှု ရင်းမြစ်များ" - "ဝိုင်ဖိုင်ကွန်ရက်သို့ လက်မှတ်ထိုးဝင်ပါ" - "ကွန်ယက်သို့ လက်မှတ်ထိုးဝင်ရန်" - + "စနစ်ချိတ်ဆက်နိုင်မှု ရင်းမြစ်များ" + "ဝိုင်ဖိုင်ကွန်ရက်သို့ လက်မှတ်ထိုးဝင်ပါ" + "ကွန်ယက်သို့ လက်မှတ်ထိုးဝင်ရန်" + - "%1$s တွင် အင်တာနက်အသုံးပြုခွင့် မရှိပါ" - "အခြားရွေးချယ်စရာများကိုကြည့်ရန် တို့ပါ" - "မိုဘိုင်းကွန်ရက်တွင် အင်တာနက်ချိတ်ဆက်မှု မရှိပါ" - "ကွန်ရက်တွင် အင်တာနက်အသုံးပြုခွင့် မရှိပါ" - "သီးသန့် ဒီအန်အက်စ် (DNS) ဆာဗာကို သုံး၍မရပါ။" - "%1$s တွင် ချိတ်ဆက်မှုကို ကန့်သတ်ထားသည်" - "မည်သို့ပင်ဖြစ်စေ ချိတ်ဆက်ရန် တို့ပါ" - "%1$s သို့ ပြောင်းလိုက်ပြီ" - "%2$s ဖြင့် အင်တာနက် အသုံးမပြုနိုင်သည့်အချိန်တွင် စက်ပစ္စည်းသည် %1$s ကို သုံးပါသည်။ ဒေတာသုံးစွဲခ ကျသင့်နိုင်ပါသည်။" - "%1$s မှ %2$s သို့ ပြောင်းလိုက်ပြီ" + "%1$s တွင် အင်တာနက်အသုံးပြုခွင့် မရှိပါ" + "အခြားရွေးချယ်စရာများကိုကြည့်ရန် တို့ပါ" + "မိုဘိုင်းကွန်ရက်တွင် အင်တာနက်ချိတ်ဆက်မှု မရှိပါ" + "ကွန်ရက်တွင် အင်တာနက်အသုံးပြုခွင့် မရှိပါ" + "သီးသန့် ဒီအန်အက်စ် (DNS) ဆာဗာကို သုံး၍မရပါ။" + "%1$s တွင် ချိတ်ဆက်မှုကို ကန့်သတ်ထားသည်" + "မည်သို့ပင်ဖြစ်စေ ချိတ်ဆက်ရန် တို့ပါ" + "%1$s သို့ ပြောင်းလိုက်ပြီ" + "%2$s ဖြင့် အင်တာနက် အသုံးမပြုနိုင်သည့်အချိန်တွင် စက်ပစ္စည်းသည် %1$s ကို သုံးပါသည်။ ဒေတာသုံးစွဲခ ကျသင့်နိုင်ပါသည်။" + "%1$s မှ %2$s သို့ ပြောင်းလိုက်ပြီ" - "မိုဘိုင်းဒေတာ" - "Wi-Fi" - "ဘလူးတုသ်" - "အီသာနက်" - "VPN" + "မိုဘိုင်းဒေတာ" + "Wi-Fi" + "ဘလူးတုသ်" + "အီသာနက်" + "VPN" - "အမည်မသိကွန်ရက်အမျိုးအစား" + "အမည်မသိကွန်ရက်အမျိုးအစား" diff --git a/service/ServiceConnectivityResources/res/values-nb/strings.xml b/service/ServiceConnectivityResources/res/values-nb/strings.xml index a561def65c..00a0728d2f 100644 --- a/service/ServiceConnectivityResources/res/values-nb/strings.xml +++ b/service/ServiceConnectivityResources/res/values-nb/strings.xml @@ -17,27 +17,27 @@ - "Ressurser for systemtilkobling" - "Logg på Wi-Fi-nettverket" - "Logg på nettverk" - + "Ressurser for systemtilkobling" + "Logg på Wi-Fi-nettverket" + "Logg på nettverk" + - "%1$s har ingen internettilkobling" - "Trykk for å få alternativer" - "Mobilnettverket har ingen internettilgang" - "Nettverket har ingen internettilgang" - "Den private DNS-tjeneren kan ikke nås" - "%1$s har begrenset tilkobling" - "Trykk for å koble til likevel" - "Byttet til %1$s" - "Enheten bruker %1$s når %2$s ikke har Internett-tilgang. Avgifter kan påløpe." - "Byttet fra %1$s til %2$s" + "%1$s har ingen internettilkobling" + "Trykk for å få alternativer" + "Mobilnettverket har ingen internettilgang" + "Nettverket har ingen internettilgang" + "Den private DNS-tjeneren kan ikke nås" + "%1$s har begrenset tilkobling" + "Trykk for å koble til likevel" + "Byttet til %1$s" + "Enheten bruker %1$s når %2$s ikke har Internett-tilgang. Avgifter kan påløpe." + "Byttet fra %1$s til %2$s" - "mobildata" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobildata" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "en ukjent nettverkstype" + "en ukjent nettverkstype" diff --git a/service/ServiceConnectivityResources/res/values-ne/strings.xml b/service/ServiceConnectivityResources/res/values-ne/strings.xml index f74542d942..2eaf16276e 100644 --- a/service/ServiceConnectivityResources/res/values-ne/strings.xml +++ b/service/ServiceConnectivityResources/res/values-ne/strings.xml @@ -17,27 +17,27 @@ - "सिस्टम कनेक्टिभिटीका स्रोतहरू" - "Wi-Fi नेटवर्कमा साइन इन गर्नुहोस्" - "सञ्जालमा साइन इन गर्नुहोस्" - + "सिस्टम कनेक्टिभिटीका स्रोतहरू" + "Wi-Fi नेटवर्कमा साइन इन गर्नुहोस्" + "सञ्जालमा साइन इन गर्नुहोस्" + - "%1$s को इन्टरनेटमाथि पहुँच छैन" - "विकल्पहरूका लागि ट्याप गर्नुहोस्" - "मोबाइल नेटवर्कको इन्टरनेटमाथि पहुँच छैन" - "नेटवर्कको इन्टरनेटमाथि पहुँच छैन" - "निजी DNS सर्भरमाथि पहुँच प्राप्त गर्न सकिँदैन" - "%1$s को जडान सीमित छ" - "जसरी भए पनि जडान गर्न ट्याप गर्नुहोस्" - "%1$s मा बदल्नुहोस्" - "%2$s मार्फत इन्टरनेटमाथि पहुँच राख्न नसकेको अवस्थामा यन्त्रले %1$s प्रयोग गर्दछ। शुल्क लाग्न सक्छ।" - "%1$s बाट %2$s मा परिवर्तन गरियो" + "%1$s को इन्टरनेटमाथि पहुँच छैन" + "विकल्पहरूका लागि ट्याप गर्नुहोस्" + "मोबाइल नेटवर्कको इन्टरनेटमाथि पहुँच छैन" + "नेटवर्कको इन्टरनेटमाथि पहुँच छैन" + "निजी DNS सर्भरमाथि पहुँच प्राप्त गर्न सकिँदैन" + "%1$s को जडान सीमित छ" + "जसरी भए पनि जडान गर्न ट्याप गर्नुहोस्" + "%1$s मा बदल्नुहोस्" + "%2$s मार्फत इन्टरनेटमाथि पहुँच राख्न नसकेको अवस्थामा यन्त्रले %1$s प्रयोग गर्दछ। शुल्क लाग्न सक्छ।" + "%1$s बाट %2$s मा परिवर्तन गरियो" - "मोबाइल डेटा" - "Wi-Fi" - "ब्लुटुथ" - "इथरनेट" - "VPN" + "मोबाइल डेटा" + "Wi-Fi" + "ब्लुटुथ" + "इथरनेट" + "VPN" - "नेटवर्कको कुनै अज्ञात प्रकार" + "नेटवर्कको कुनै अज्ञात प्रकार" diff --git a/service/ServiceConnectivityResources/res/values-nl/strings.xml b/service/ServiceConnectivityResources/res/values-nl/strings.xml index 0f3203beb5..394c552ae3 100644 --- a/service/ServiceConnectivityResources/res/values-nl/strings.xml +++ b/service/ServiceConnectivityResources/res/values-nl/strings.xml @@ -17,27 +17,27 @@ - "Resources voor systeemconnectiviteit" - "Inloggen bij wifi-netwerk" - "Inloggen bij netwerk" - + "Resources voor systeemconnectiviteit" + "Inloggen bij wifi-netwerk" + "Inloggen bij netwerk" + - "%1$s heeft geen internettoegang" - "Tik voor opties" - "Mobiel netwerk heeft geen internettoegang" - "Netwerk heeft geen internettoegang" - "Geen toegang tot privé-DNS-server" - "%1$s heeft beperkte connectiviteit" - "Tik om toch verbinding te maken" - "Overgeschakeld naar %1$s" - "Apparaat gebruikt %1$s wanneer %2$s geen internetverbinding heeft. Er kunnen kosten in rekening worden gebracht." - "Overgeschakeld van %1$s naar %2$s" + "%1$s heeft geen internettoegang" + "Tik voor opties" + "Mobiel netwerk heeft geen internettoegang" + "Netwerk heeft geen internettoegang" + "Geen toegang tot privé-DNS-server" + "%1$s heeft beperkte connectiviteit" + "Tik om toch verbinding te maken" + "Overgeschakeld naar %1$s" + "Apparaat gebruikt %1$s wanneer %2$s geen internetverbinding heeft. Er kunnen kosten in rekening worden gebracht." + "Overgeschakeld van %1$s naar %2$s" - "mobiele data" - "Wifi" - "Bluetooth" - "Ethernet" - "VPN" + "mobiele data" + "Wifi" + "Bluetooth" + "Ethernet" + "VPN" - "een onbekend netwerktype" + "een onbekend netwerktype" diff --git a/service/ServiceConnectivityResources/res/values-or/strings.xml b/service/ServiceConnectivityResources/res/values-or/strings.xml index ecf4d694d0..8b85884fe9 100644 --- a/service/ServiceConnectivityResources/res/values-or/strings.xml +++ b/service/ServiceConnectivityResources/res/values-or/strings.xml @@ -17,27 +17,27 @@ - "ସିଷ୍ଟମର ସଂଯୋଗ ସମ୍ବନ୍ଧିତ ରିସୋର୍ସଗୁଡ଼ିକ" - "ୱାଇ-ଫାଇ ନେଟୱର୍କରେ ସାଇନ୍‍-ଇନ୍‍ କରନ୍ତୁ" - "ନେଟ୍‌ୱର୍କରେ ସାଇନ୍‍ ଇନ୍‍ କରନ୍ତୁ" - + "ସିଷ୍ଟମର ସଂଯୋଗ ସମ୍ବନ୍ଧିତ ରିସୋର୍ସଗୁଡ଼ିକ" + "ୱାଇ-ଫାଇ ନେଟୱର୍କରେ ସାଇନ୍‍-ଇନ୍‍ କରନ୍ତୁ" + "ନେଟ୍‌ୱର୍କରେ ସାଇନ୍‍ ଇନ୍‍ କରନ୍ତୁ" + - "%1$sର ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ" - "ବିକଳ୍ପ ପାଇଁ ଟାପ୍‍ କରନ୍ତୁ" - "ମୋବାଇଲ୍ ନେଟ୍‌ୱାର୍କରେ ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ" - "ନେଟ୍‌ୱାର୍କରେ ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ" - "ବ୍ୟକ୍ତିଗତ DNS ସର୍ଭର୍ ଆକ୍ସେସ୍ କରିହେବ ନାହିଁ" - "%1$sର ସୀମିତ ସଂଯୋଗ ଅଛି" - "ତଥାପି ଯୋଗାଯୋଗ କରିବାକୁ ଟାପ୍ କରନ୍ତୁ" - "%1$sକୁ ବଦଳାଗଲା" - "%2$sର ଇଣ୍ଟରନେଟ୍‍ ଆକ୍ସେସ୍ ନଥିବାବେଳେ ଡିଭାଇସ୍‍ %1$s ବ୍ୟବହାର କରିଥାଏ। ଶୁଳ୍କ ଲାଗୁ ହୋଇପାରେ।" - "%1$s ରୁ %2$sକୁ ବଦଳାଗଲା" + "%1$sର ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ" + "ବିକଳ୍ପ ପାଇଁ ଟାପ୍‍ କରନ୍ତୁ" + "ମୋବାଇଲ୍ ନେଟ୍‌ୱାର୍କରେ ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ" + "ନେଟ୍‌ୱାର୍କରେ ଇଣ୍ଟର୍ନେଟ୍ ଆକ୍ସେସ୍ ନାହିଁ" + "ବ୍ୟକ୍ତିଗତ DNS ସର୍ଭର୍ ଆକ୍ସେସ୍ କରିହେବ ନାହିଁ" + "%1$sର ସୀମିତ ସଂଯୋଗ ଅଛି" + "ତଥାପି ଯୋଗାଯୋଗ କରିବାକୁ ଟାପ୍ କରନ୍ତୁ" + "%1$sକୁ ବଦଳାଗଲା" + "%2$sର ଇଣ୍ଟରନେଟ୍‍ ଆକ୍ସେସ୍ ନଥିବାବେଳେ ଡିଭାଇସ୍‍ %1$s ବ୍ୟବହାର କରିଥାଏ। ଶୁଳ୍କ ଲାଗୁ ହୋଇପାରେ।" + "%1$s ରୁ %2$sକୁ ବଦଳାଗଲା" - "ମୋବାଇଲ ଡାଟା" - "ୱାଇ-ଫାଇ" - "ବ୍ଲୁଟୁଥ୍" - "ଇଥରନେଟ୍" - "VPN" + "ମୋବାଇଲ ଡାଟା" + "ୱାଇ-ଫାଇ" + "ବ୍ଲୁଟୁଥ୍" + "ଇଥରନେଟ୍" + "VPN" - "ଏକ ଅଜଣା ନେଟୱାର୍କ ପ୍ରକାର" + "ଏକ ଅଜଣା ନେଟୱାର୍କ ପ୍ରକାର" diff --git a/service/ServiceConnectivityResources/res/values-pa/strings.xml b/service/ServiceConnectivityResources/res/values-pa/strings.xml index 43280549e7..9f71cac55a 100644 --- a/service/ServiceConnectivityResources/res/values-pa/strings.xml +++ b/service/ServiceConnectivityResources/res/values-pa/strings.xml @@ -17,27 +17,27 @@ - "ਸਿਸਟਮ ਕਨੈਕਟੀਵਿਟੀ ਸਰੋਤ" - "ਵਾਈ-ਫਾਈ ਨੈੱਟਵਰਕ \'ਤੇ ਸਾਈਨ-ਇਨ ਕਰੋ" - "ਨੈੱਟਵਰਕ \'ਤੇ ਸਾਈਨ-ਇਨ ਕਰੋ" - + "ਸਿਸਟਮ ਕਨੈਕਟੀਵਿਟੀ ਸਰੋਤ" + "ਵਾਈ-ਫਾਈ ਨੈੱਟਵਰਕ \'ਤੇ ਸਾਈਨ-ਇਨ ਕਰੋ" + "ਨੈੱਟਵਰਕ \'ਤੇ ਸਾਈਨ-ਇਨ ਕਰੋ" + - "%1$s ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹੁੰਚ ਨਹੀਂ ਹੈ" - "ਵਿਕਲਪਾਂ ਲਈ ਟੈਪ ਕਰੋ" - "ਮੋਬਾਈਲ ਨੈੱਟਵਰਕ ਕੋਲ ਇੰਟਰਨੈੱਟ ਤੱਕ ਪਹੁੰਚ ਨਹੀਂ ਹੈ" - "ਨੈੱਟਵਰਕ ਕੋਲ ਇੰਟਰਨੈੱਟ ਤੱਕ ਪਹੁੰਚ ਨਹੀਂ ਹੈ" - "ਨਿੱਜੀ ਡੋਮੇਨ ਨਾਮ ਪ੍ਰਣਾਲੀ (DNS) ਸਰਵਰ \'ਤੇ ਪਹੁੰਚ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕੀ" - "%1$s ਕੋਲ ਸੀਮਤ ਕਨੈਕਟੀਵਿਟੀ ਹੈ" - "ਫਿਰ ਵੀ ਕਨੈਕਟ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ" - "ਬਦਲਕੇ %1$s ਲਿਆਂਦਾ ਗਿਆ" - "%2$s ਦੀ ਇੰਟਰਨੈੱਟ \'ਤੇ ਪਹੁੰਚ ਨਾ ਹੋਣ \'ਤੇ ਡੀਵਾਈਸ %1$s ਦੀ ਵਰਤੋਂ ਕਰਦਾ ਹੈ। ਖਰਚੇ ਲਾਗੂ ਹੋ ਸਕਦੇ ਹਨ।" - "%1$s ਤੋਂ ਬਦਲਕੇ %2$s \'ਤੇ ਕੀਤਾ ਗਿਆ" + "%1$s ਕੋਲ ਇੰਟਰਨੈੱਟ ਪਹੁੰਚ ਨਹੀਂ ਹੈ" + "ਵਿਕਲਪਾਂ ਲਈ ਟੈਪ ਕਰੋ" + "ਮੋਬਾਈਲ ਨੈੱਟਵਰਕ ਕੋਲ ਇੰਟਰਨੈੱਟ ਤੱਕ ਪਹੁੰਚ ਨਹੀਂ ਹੈ" + "ਨੈੱਟਵਰਕ ਕੋਲ ਇੰਟਰਨੈੱਟ ਤੱਕ ਪਹੁੰਚ ਨਹੀਂ ਹੈ" + "ਨਿੱਜੀ ਡੋਮੇਨ ਨਾਮ ਪ੍ਰਣਾਲੀ (DNS) ਸਰਵਰ \'ਤੇ ਪਹੁੰਚ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕੀ" + "%1$s ਕੋਲ ਸੀਮਤ ਕਨੈਕਟੀਵਿਟੀ ਹੈ" + "ਫਿਰ ਵੀ ਕਨੈਕਟ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ" + "ਬਦਲਕੇ %1$s ਲਿਆਂਦਾ ਗਿਆ" + "%2$s ਦੀ ਇੰਟਰਨੈੱਟ \'ਤੇ ਪਹੁੰਚ ਨਾ ਹੋਣ \'ਤੇ ਡੀਵਾਈਸ %1$s ਦੀ ਵਰਤੋਂ ਕਰਦਾ ਹੈ। ਖਰਚੇ ਲਾਗੂ ਹੋ ਸਕਦੇ ਹਨ।" + "%1$s ਤੋਂ ਬਦਲਕੇ %2$s \'ਤੇ ਕੀਤਾ ਗਿਆ" - "ਮੋਬਾਈਲ ਡਾਟਾ" - "ਵਾਈ-ਫਾਈ" - "ਬਲੂਟੁੱਥ" - "ਈਥਰਨੈੱਟ" - "VPN" + "ਮੋਬਾਈਲ ਡਾਟਾ" + "ਵਾਈ-ਫਾਈ" + "ਬਲੂਟੁੱਥ" + "ਈਥਰਨੈੱਟ" + "VPN" - "ਕੋਈ ਅਗਿਆਤ ਨੈੱਟਵਰਕ ਦੀ ਕਿਸਮ" + "ਕੋਈ ਅਗਿਆਤ ਨੈੱਟਵਰਕ ਦੀ ਕਿਸਮ" diff --git a/service/ServiceConnectivityResources/res/values-pl/strings.xml b/service/ServiceConnectivityResources/res/values-pl/strings.xml index e6b3a0cff6..cc84e29a44 100644 --- a/service/ServiceConnectivityResources/res/values-pl/strings.xml +++ b/service/ServiceConnectivityResources/res/values-pl/strings.xml @@ -17,27 +17,27 @@ - "Zasoby systemowe dotyczące łączności" - "Zaloguj się w sieci Wi-Fi" - "Zaloguj się do sieci" - + "Zasoby systemowe dotyczące łączności" + "Zaloguj się w sieci Wi-Fi" + "Zaloguj się do sieci" + - "%1$s nie ma dostępu do internetu" - "Kliknij, by wyświetlić opcje" - "Sieć komórkowa nie ma dostępu do internetu" - "Sieć nie ma dostępu do internetu" - "Brak dostępu do prywatnego serwera DNS" - "%1$s ma ograniczoną łączność" - "Kliknij, by mimo to nawiązać połączenie" - "Zmieniono na połączenie typu %1$s" - "Urządzenie korzysta z połączenia typu %1$s, gdy %2$s nie dostępu do internetu. Mogą zostać naliczone opłaty." - "Przełączono z połączenia typu %1$s na %2$s." + "%1$s nie ma dostępu do internetu" + "Kliknij, by wyświetlić opcje" + "Sieć komórkowa nie ma dostępu do internetu" + "Sieć nie ma dostępu do internetu" + "Brak dostępu do prywatnego serwera DNS" + "%1$s ma ograniczoną łączność" + "Kliknij, by mimo to nawiązać połączenie" + "Zmieniono na połączenie typu %1$s" + "Urządzenie korzysta z połączenia typu %1$s, gdy %2$s nie dostępu do internetu. Mogą zostać naliczone opłaty." + "Przełączono z połączenia typu %1$s na %2$s." - "mobilna transmisja danych" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobilna transmisja danych" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "nieznany typ sieci" + "nieznany typ sieci" diff --git a/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml b/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml index f1d0bc0b0c..3c15a7682d 100644 --- a/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml +++ b/service/ServiceConnectivityResources/res/values-pt-rBR/strings.xml @@ -17,27 +17,27 @@ - "Recursos de conectividade do sistema" - "Fazer login na rede Wi-Fi" - "Fazer login na rede" - + "Recursos de conectividade do sistema" + "Fazer login na rede Wi-Fi" + "Fazer login na rede" + - "%1$s não tem acesso à Internet" - "Toque para ver opções" - "A rede móvel não tem acesso à Internet" - "A rede não tem acesso à Internet" - "Não é possível acessar o servidor DNS privado" - "%1$s tem conectividade limitada" - "Toque para conectar mesmo assim" - "Alternado para %1$s" - "O dispositivo usa %1$s quando %2$s não tem acesso à Internet. Esse serviço pode ser cobrado." - "Alternado de %1$s para %2$s" + "%1$s não tem acesso à Internet" + "Toque para ver opções" + "A rede móvel não tem acesso à Internet" + "A rede não tem acesso à Internet" + "Não é possível acessar o servidor DNS privado" + "%1$s tem conectividade limitada" + "Toque para conectar mesmo assim" + "Alternado para %1$s" + "O dispositivo usa %1$s quando %2$s não tem acesso à Internet. Esse serviço pode ser cobrado." + "Alternado de %1$s para %2$s" - "dados móveis" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "dados móveis" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "um tipo de rede desconhecido" + "um tipo de rede desconhecido" diff --git a/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml b/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml index 163d70b081..48dde754b7 100644 --- a/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml +++ b/service/ServiceConnectivityResources/res/values-pt-rPT/strings.xml @@ -17,27 +17,27 @@ - "Recursos de conetividade do sistema" - "Iniciar sessão na rede Wi-Fi" - "Início de sessão na rede" - + "Recursos de conetividade do sistema" + "Iniciar sessão na rede Wi-Fi" + "Início de sessão na rede" + - "%1$s não tem acesso à Internet" - "Toque para obter mais opções" - "A rede móvel não tem acesso à Internet" - "A rede não tem acesso à Internet" - "Não é possível aceder ao servidor DNS." - "%1$s tem conetividade limitada." - "Toque para ligar mesmo assim." - "Mudou para %1$s" - "O dispositivo utiliza %1$s quando %2$s não tem acesso à Internet. Podem aplicar-se custos." - "Mudou de %1$s para %2$s" + "%1$s não tem acesso à Internet" + "Toque para obter mais opções" + "A rede móvel não tem acesso à Internet" + "A rede não tem acesso à Internet" + "Não é possível aceder ao servidor DNS." + "%1$s tem conetividade limitada." + "Toque para ligar mesmo assim." + "Mudou para %1$s" + "O dispositivo utiliza %1$s quando %2$s não tem acesso à Internet. Podem aplicar-se custos." + "Mudou de %1$s para %2$s" - "dados móveis" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "dados móveis" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "um tipo de rede desconhecido" + "um tipo de rede desconhecido" diff --git a/service/ServiceConnectivityResources/res/values-pt/strings.xml b/service/ServiceConnectivityResources/res/values-pt/strings.xml index f1d0bc0b0c..3c15a7682d 100644 --- a/service/ServiceConnectivityResources/res/values-pt/strings.xml +++ b/service/ServiceConnectivityResources/res/values-pt/strings.xml @@ -17,27 +17,27 @@ - "Recursos de conectividade do sistema" - "Fazer login na rede Wi-Fi" - "Fazer login na rede" - + "Recursos de conectividade do sistema" + "Fazer login na rede Wi-Fi" + "Fazer login na rede" + - "%1$s não tem acesso à Internet" - "Toque para ver opções" - "A rede móvel não tem acesso à Internet" - "A rede não tem acesso à Internet" - "Não é possível acessar o servidor DNS privado" - "%1$s tem conectividade limitada" - "Toque para conectar mesmo assim" - "Alternado para %1$s" - "O dispositivo usa %1$s quando %2$s não tem acesso à Internet. Esse serviço pode ser cobrado." - "Alternado de %1$s para %2$s" + "%1$s não tem acesso à Internet" + "Toque para ver opções" + "A rede móvel não tem acesso à Internet" + "A rede não tem acesso à Internet" + "Não é possível acessar o servidor DNS privado" + "%1$s tem conectividade limitada" + "Toque para conectar mesmo assim" + "Alternado para %1$s" + "O dispositivo usa %1$s quando %2$s não tem acesso à Internet. Esse serviço pode ser cobrado." + "Alternado de %1$s para %2$s" - "dados móveis" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "dados móveis" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "um tipo de rede desconhecido" + "um tipo de rede desconhecido" diff --git a/service/ServiceConnectivityResources/res/values-ro/strings.xml b/service/ServiceConnectivityResources/res/values-ro/strings.xml index 221261c66c..fa5848fe56 100644 --- a/service/ServiceConnectivityResources/res/values-ro/strings.xml +++ b/service/ServiceConnectivityResources/res/values-ro/strings.xml @@ -17,27 +17,27 @@ - "Resurse pentru conectivitatea sistemului" - "Conectați-vă la rețeaua Wi-Fi" - "Conectați-vă la rețea" - + "Resurse pentru conectivitatea sistemului" + "Conectați-vă la rețeaua Wi-Fi" + "Conectați-vă la rețea" + - "%1$s nu are acces la internet" - "Atingeți pentru opțiuni" - "Rețeaua mobilă nu are acces la internet" - "Rețeaua nu are acces la internet" - "Serverul DNS privat nu poate fi accesat" - "%1$s are conectivitate limitată" - "Atingeți pentru a vă conecta oricum" - "S-a comutat la %1$s" - "Dispozitivul folosește %1$s când %2$s nu are acces la internet. Se pot aplica taxe." - "S-a comutat de la %1$s la %2$s" + "%1$s nu are acces la internet" + "Atingeți pentru opțiuni" + "Rețeaua mobilă nu are acces la internet" + "Rețeaua nu are acces la internet" + "Serverul DNS privat nu poate fi accesat" + "%1$s are conectivitate limitată" + "Atingeți pentru a vă conecta oricum" + "S-a comutat la %1$s" + "Dispozitivul folosește %1$s când %2$s nu are acces la internet. Se pot aplica taxe." + "S-a comutat de la %1$s la %2$s" - "date mobile" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "date mobile" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "un tip de rețea necunoscut" + "un tip de rețea necunoscut" diff --git a/service/ServiceConnectivityResources/res/values-ru/strings.xml b/service/ServiceConnectivityResources/res/values-ru/strings.xml index ba179b7394..2e074ed5d4 100644 --- a/service/ServiceConnectivityResources/res/values-ru/strings.xml +++ b/service/ServiceConnectivityResources/res/values-ru/strings.xml @@ -17,27 +17,27 @@ - "System Connectivity Resources" - "Подключение к Wi-Fi" - "Регистрация в сети" - + "System Connectivity Resources" + "Подключение к Wi-Fi" + "Регистрация в сети" + - "Сеть \"%1$s\" не подключена к Интернету" - "Нажмите, чтобы показать варианты." - "Мобильная сеть не подключена к Интернету" - "Сеть не подключена к Интернету" - "Доступа к частному DNS-серверу нет." - "Подключение к сети \"%1$s\" ограничено" - "Нажмите, чтобы подключиться" - "Новое подключение: %1$s" - "Устройство использует %1$s, если подключение к сети %2$s недоступно. Может взиматься плата за передачу данных." - "Устройство отключено от сети %2$s и теперь использует %1$s" + "Сеть \"%1$s\" не подключена к Интернету" + "Нажмите, чтобы показать варианты." + "Мобильная сеть не подключена к Интернету" + "Сеть не подключена к Интернету" + "Доступа к частному DNS-серверу нет." + "Подключение к сети \"%1$s\" ограничено" + "Нажмите, чтобы подключиться" + "Новое подключение: %1$s" + "Устройство использует %1$s, если подключение к сети %2$s недоступно. Может взиматься плата за передачу данных." + "Устройство отключено от сети %2$s и теперь использует %1$s" - "мобильный интернет" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "мобильный интернет" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "неизвестный тип сети" + "неизвестный тип сети" diff --git a/service/ServiceConnectivityResources/res/values-si/strings.xml b/service/ServiceConnectivityResources/res/values-si/strings.xml index 1c493a78dc..a4f720a205 100644 --- a/service/ServiceConnectivityResources/res/values-si/strings.xml +++ b/service/ServiceConnectivityResources/res/values-si/strings.xml @@ -17,27 +17,27 @@ - "පද්ධති සබැඳුම් හැකියා සම්පත්" - "Wi-Fi ජාලයට පුරනය වන්න" - "ජාලයට පුරනය වන්න" - + "පද්ධති සබැඳුම් හැකියා සම්පත්" + "Wi-Fi ජාලයට පුරනය වන්න" + "ජාලයට පුරනය වන්න" + - "%1$s හට අන්තර්ජාල ප්‍රවේශය නැත" - "විකල්ප සඳහා තට්ටු කරන්න" - "ජංගම ජාලවලට අන්තර්ජාල ප්‍රවේශය නැත" - "ජාලයට අන්තර්ජාල ප්‍රවේශය නැත" - "පුද්ගලික DNS සේවාදායකයට ප්‍රවේශ වීමට නොහැකිය" - "%1$s හට සීමිත සබැඳුම් හැකියාවක් ඇත" - "කෙසේ වෙතත් ඉදිරියට යාමට තට්ටු කරන්න" - "%1$s වෙත මාරු විය" - "උපාංගය %1$s %2$s සඳහා අන්තර්ජාල ප්‍රවේශය නැති විට භාවිත කරයි. ගාස්තු අදාළ විය හැකිය." - "%1$s සිට %2$s වෙත මාරු විය" + "%1$s හට අන්තර්ජාල ප්‍රවේශය නැත" + "විකල්ප සඳහා තට්ටු කරන්න" + "ජංගම ජාලවලට අන්තර්ජාල ප්‍රවේශය නැත" + "ජාලයට අන්තර්ජාල ප්‍රවේශය නැත" + "පුද්ගලික DNS සේවාදායකයට ප්‍රවේශ වීමට නොහැකිය" + "%1$s හට සීමිත සබැඳුම් හැකියාවක් ඇත" + "කෙසේ වෙතත් ඉදිරියට යාමට තට්ටු කරන්න" + "%1$s වෙත මාරු විය" + "උපාංගය %1$s %2$s සඳහා අන්තර්ජාල ප්‍රවේශය නැති විට භාවිත කරයි. ගාස්තු අදාළ විය හැකිය." + "%1$s සිට %2$s වෙත මාරු විය" - "ජංගම දත්ත" - "Wi-Fi" - "බ්ලූටූත්" - "ඊතර්නෙට්" - "VPN" + "ජංගම දත්ත" + "Wi-Fi" + "බ්ලූටූත්" + "ඊතර්නෙට්" + "VPN" - "නොදන්නා ජාල වර්ගයකි" + "නොදන්නා ජාල වර්ගයකි" diff --git a/service/ServiceConnectivityResources/res/values-sk/strings.xml b/service/ServiceConnectivityResources/res/values-sk/strings.xml index 1b9313af17..432b67040b 100644 --- a/service/ServiceConnectivityResources/res/values-sk/strings.xml +++ b/service/ServiceConnectivityResources/res/values-sk/strings.xml @@ -17,27 +17,27 @@ - "Zdroje možností pripojenia systému" - "Prihlásiť sa do siete Wi‑Fi" - "Prihlásenie do siete" - + "Zdroje možností pripojenia systému" + "Prihlásiť sa do siete Wi‑Fi" + "Prihlásenie do siete" + - "%1$s nemá prístup k internetu" - "Klepnutím získate možnosti" - "Mobilná sieť nemá prístup k internetu" - "Sieť nemá prístup k internetu" - "K súkromnému serveru DNS sa nepodarilo získať prístup" - "%1$s má obmedzené pripojenie" - "Ak sa chcete aj napriek tomu pripojiť, klepnite" - "Prepnuté na sieť: %1$s" - "Keď %2$s nemá prístup k internetu, zariadenie používa %1$s. Môžu sa účtovať poplatky." - "Prepnuté zo siete %1$s na sieť %2$s" + "%1$s nemá prístup k internetu" + "Klepnutím získate možnosti" + "Mobilná sieť nemá prístup k internetu" + "Sieť nemá prístup k internetu" + "K súkromnému serveru DNS sa nepodarilo získať prístup" + "%1$s má obmedzené pripojenie" + "Ak sa chcete aj napriek tomu pripojiť, klepnite" + "Prepnuté na sieť: %1$s" + "Keď %2$s nemá prístup k internetu, zariadenie používa %1$s. Môžu sa účtovať poplatky." + "Prepnuté zo siete %1$s na sieť %2$s" - "mobilné dáta" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobilné dáta" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "neznámy typ siete" + "neznámy typ siete" diff --git a/service/ServiceConnectivityResources/res/values-sl/strings.xml b/service/ServiceConnectivityResources/res/values-sl/strings.xml index 739fb8e491..b727614095 100644 --- a/service/ServiceConnectivityResources/res/values-sl/strings.xml +++ b/service/ServiceConnectivityResources/res/values-sl/strings.xml @@ -17,27 +17,27 @@ - "Viri povezljivosti sistema" - "Prijavite se v omrežje Wi-Fi" - "Prijava v omrežje" - + "Viri povezljivosti sistema" + "Prijavite se v omrežje Wi-Fi" + "Prijava v omrežje" + - "Omrežje %1$s nima dostopa do interneta" - "Dotaknite se za možnosti" - "Mobilno omrežje nima dostopa do interneta" - "Omrežje nima dostopa do interneta" - "Do zasebnega strežnika DNS ni mogoče dostopati" - "Povezljivost omrežja %1$s je omejena" - "Dotaknite se, da kljub temu vzpostavite povezavo" - "Preklopljeno na omrežje vrste %1$s" - "Naprava uporabi omrežje vrste %1$s, ko omrežje vrste %2$s nima dostopa do interneta. Prenos podatkov se lahko zaračuna." - "Preklopljeno z omrežja vrste %1$s na omrežje vrste %2$s" + "Omrežje %1$s nima dostopa do interneta" + "Dotaknite se za možnosti" + "Mobilno omrežje nima dostopa do interneta" + "Omrežje nima dostopa do interneta" + "Do zasebnega strežnika DNS ni mogoče dostopati" + "Povezljivost omrežja %1$s je omejena" + "Dotaknite se, da kljub temu vzpostavite povezavo" + "Preklopljeno na omrežje vrste %1$s" + "Naprava uporabi omrežje vrste %1$s, ko omrežje vrste %2$s nima dostopa do interneta. Prenos podatkov se lahko zaračuna." + "Preklopljeno z omrežja vrste %1$s na omrežje vrste %2$s" - "prenos podatkov v mobilnem omrežju" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "prenos podatkov v mobilnem omrežju" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "neznana vrsta omrežja" + "neznana vrsta omrežja" diff --git a/service/ServiceConnectivityResources/res/values-sq/strings.xml b/service/ServiceConnectivityResources/res/values-sq/strings.xml index cf8cf3b40e..385c75c65e 100644 --- a/service/ServiceConnectivityResources/res/values-sq/strings.xml +++ b/service/ServiceConnectivityResources/res/values-sq/strings.xml @@ -17,27 +17,27 @@ - "Burimet e lidhshmërisë së sistemit" - "Identifikohu në rrjetin Wi-Fi" - "Identifikohu në rrjet" - + "Burimet e lidhshmërisë së sistemit" + "Identifikohu në rrjetin Wi-Fi" + "Identifikohu në rrjet" + - "%1$s nuk ka qasje në internet" - "Trokit për opsionet" - "Rrjeti celular nuk ka qasje në internet" - "Rrjeti nuk ka qasje në internet" - "Serveri privat DNS nuk mund të qaset" - "%1$s ka lidhshmëri të kufizuar" - "Trokit për t\'u lidhur gjithsesi" - "Kaloi te %1$s" - "Pajisja përdor %1$s kur %2$s nuk ka qasje në internet. Mund të zbatohen tarifa." - "Kaloi nga %1$s te %2$s" + "%1$s nuk ka qasje në internet" + "Trokit për opsionet" + "Rrjeti celular nuk ka qasje në internet" + "Rrjeti nuk ka qasje në internet" + "Serveri privat DNS nuk mund të qaset" + "%1$s ka lidhshmëri të kufizuar" + "Trokit për t\'u lidhur gjithsesi" + "Kaloi te %1$s" + "Pajisja përdor %1$s kur %2$s nuk ka qasje në internet. Mund të zbatohen tarifa." + "Kaloi nga %1$s te %2$s" - "të dhënat celulare" - "Wi-Fi" - "Bluetooth" - "Eternet" - "VPN" + "të dhënat celulare" + "Wi-Fi" + "Bluetooth" + "Eternet" + "VPN" - "një lloj rrjeti i panjohur" + "një lloj rrjeti i panjohur" diff --git a/service/ServiceConnectivityResources/res/values-sr/strings.xml b/service/ServiceConnectivityResources/res/values-sr/strings.xml index 1f7c95c94d..928dc798d3 100644 --- a/service/ServiceConnectivityResources/res/values-sr/strings.xml +++ b/service/ServiceConnectivityResources/res/values-sr/strings.xml @@ -17,27 +17,27 @@ - "Ресурси за повезивање са системом" - "Пријављивање на WiFi мрежу" - "Пријавите се на мрежу" - + "Ресурси за повезивање са системом" + "Пријављивање на WiFi мрежу" + "Пријавите се на мрежу" + - "%1$s нема приступ интернету" - "Додирните за опције" - "Мобилна мрежа нема приступ интернету" - "Мрежа нема приступ интернету" - "Приступ приватном DNS серверу није успео" - "%1$s има ограничену везу" - "Додирните да бисте се ипак повезали" - "Прешли сте на тип мреже %1$s" - "Уређај користи тип мреже %1$s када тип мреже %2$s нема приступ интернету. Можда ће се наплаћивати трошкови." - "Прешли сте са типа мреже %1$s на тип мреже %2$s" + "%1$s нема приступ интернету" + "Додирните за опције" + "Мобилна мрежа нема приступ интернету" + "Мрежа нема приступ интернету" + "Приступ приватном DNS серверу није успео" + "%1$s има ограничену везу" + "Додирните да бисте се ипак повезали" + "Прешли сте на тип мреже %1$s" + "Уређај користи тип мреже %1$s када тип мреже %2$s нема приступ интернету. Можда ће се наплаћивати трошкови." + "Прешли сте са типа мреже %1$s на тип мреже %2$s" - "мобилни подаци" - "WiFi" - "Bluetooth" - "Етернет" - "VPN" + "мобилни подаци" + "WiFi" + "Bluetooth" + "Етернет" + "VPN" - "непознат тип мреже" + "непознат тип мреже" diff --git a/service/ServiceConnectivityResources/res/values-sv/strings.xml b/service/ServiceConnectivityResources/res/values-sv/strings.xml index 57e74e95e9..d71412429a 100644 --- a/service/ServiceConnectivityResources/res/values-sv/strings.xml +++ b/service/ServiceConnectivityResources/res/values-sv/strings.xml @@ -17,27 +17,27 @@ - "Resurser för systemanslutning" - "Logga in på ett wifi-nätverk" - "Logga in på nätverket" - + "Resurser för systemanslutning" + "Logga in på ett wifi-nätverk" + "Logga in på nätverket" + - "%1$s har ingen internetanslutning" - "Tryck för alternativ" - "Mobilnätverket har ingen internetanslutning" - "Nätverket har ingen internetanslutning" - "Det går inte att komma åt den privata DNS-servern." - "%1$s har begränsad anslutning" - "Tryck för att ansluta ändå" - "Byte av nätverk till %1$s" - "%1$s används på enheten när det inte finns internetåtkomst via %2$s. Avgifter kan tillkomma." - "Byte av nätverk från %1$s till %2$s" + "%1$s har ingen internetanslutning" + "Tryck för alternativ" + "Mobilnätverket har ingen internetanslutning" + "Nätverket har ingen internetanslutning" + "Det går inte att komma åt den privata DNS-servern." + "%1$s har begränsad anslutning" + "Tryck för att ansluta ändå" + "Byte av nätverk till %1$s" + "%1$s används på enheten när det inte finns internetåtkomst via %2$s. Avgifter kan tillkomma." + "Byte av nätverk från %1$s till %2$s" - "mobildata" - "Wifi" - "Bluetooth" - "Ethernet" - "VPN" + "mobildata" + "Wifi" + "Bluetooth" + "Ethernet" + "VPN" - "en okänd nätverkstyp" + "en okänd nätverkstyp" diff --git a/service/ServiceConnectivityResources/res/values-sw/strings.xml b/service/ServiceConnectivityResources/res/values-sw/strings.xml index 5c4d59410d..15d6cab3d6 100644 --- a/service/ServiceConnectivityResources/res/values-sw/strings.xml +++ b/service/ServiceConnectivityResources/res/values-sw/strings.xml @@ -17,27 +17,27 @@ - "Nyenzo za Muunganisho wa Mfumo" - "Ingia kwa mtandao wa Wi-Fi" - "Ingia katika mtandao" - + "Nyenzo za Muunganisho wa Mfumo" + "Ingia kwa mtandao wa Wi-Fi" + "Ingia katika mtandao" + - "%1$s haina uwezo wa kufikia intaneti" - "Gusa ili upate chaguo" - "Mtandao wa simu hauna uwezo wa kufikia intaneti" - "Mtandao hauna uwezo wa kufikia intaneti" - "Seva ya faragha ya DNS haiwezi kufikiwa" - "%1$s ina muunganisho unaofikia huduma chache." - "Gusa ili uunganishe tu" - "Sasa inatumia %1$s" - "Kifaa hutumia %1$s wakati %2$s haina intaneti. Huenda ukalipishwa." - "Imebadilisha mtandao kutoka %1$s na sasa inatumia %2$s" + "%1$s haina uwezo wa kufikia intaneti" + "Gusa ili upate chaguo" + "Mtandao wa simu hauna uwezo wa kufikia intaneti" + "Mtandao hauna uwezo wa kufikia intaneti" + "Seva ya faragha ya DNS haiwezi kufikiwa" + "%1$s ina muunganisho unaofikia huduma chache." + "Gusa ili uunganishe tu" + "Sasa inatumia %1$s" + "Kifaa hutumia %1$s wakati %2$s haina intaneti. Huenda ukalipishwa." + "Imebadilisha mtandao kutoka %1$s na sasa inatumia %2$s" - "data ya mtandao wa simu" - "Wi-Fi" - "Bluetooth" - "Ethaneti" - "VPN" + "data ya mtandao wa simu" + "Wi-Fi" + "Bluetooth" + "Ethaneti" + "VPN" - "aina ya mtandao isiyojulikana" + "aina ya mtandao isiyojulikana" diff --git a/service/ServiceConnectivityResources/res/values-ta/strings.xml b/service/ServiceConnectivityResources/res/values-ta/strings.xml index 90f89c988e..43a3f41d82 100644 --- a/service/ServiceConnectivityResources/res/values-ta/strings.xml +++ b/service/ServiceConnectivityResources/res/values-ta/strings.xml @@ -17,27 +17,27 @@ - "சிஸ்டம் இணைப்பு மூலங்கள்" - "வைஃபை நெட்வொர்க்கில் உள்நுழையவும்" - "நெட்வொர்க்கில் உள்நுழையவும்" - + "சிஸ்டம் இணைப்பு மூலங்கள்" + "வைஃபை நெட்வொர்க்கில் உள்நுழையவும்" + "நெட்வொர்க்கில் உள்நுழையவும்" + - "%1$s நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை" - "விருப்பங்களுக்கு, தட்டவும்" - "மொபைல் நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை" - "நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை" - "தனிப்பட்ட DNS சேவையகத்தை அணுக இயலாது" - "%1$s வரம்பிற்கு உட்பட்ட இணைப்புநிலையைக் கொண்டுள்ளது" - "எப்படியேனும் இணைப்பதற்குத் தட்டவும்" - "%1$sக்கு மாற்றப்பட்டது" - "%2$s நெட்வொர்க்கில் இண்டர்நெட் அணுகல் இல்லாததால், சாதனமானது %1$s நெட்வொர்க்கைப் பயன்படுத்துகிறது. கட்டணங்கள் விதிக்கப்படலாம்." - "%1$s இலிருந்து %2$sக்கு மாற்றப்பட்டது" + "%1$s நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை" + "விருப்பங்களுக்கு, தட்டவும்" + "மொபைல் நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை" + "நெட்வொர்க்கிற்கு இணைய அணுகல் இல்லை" + "தனிப்பட்ட DNS சேவையகத்தை அணுக இயலாது" + "%1$s வரம்பிற்கு உட்பட்ட இணைப்புநிலையைக் கொண்டுள்ளது" + "எப்படியேனும் இணைப்பதற்குத் தட்டவும்" + "%1$sக்கு மாற்றப்பட்டது" + "%2$s நெட்வொர்க்கில் இண்டர்நெட் அணுகல் இல்லாததால், சாதனமானது %1$s நெட்வொர்க்கைப் பயன்படுத்துகிறது. கட்டணங்கள் விதிக்கப்படலாம்." + "%1$s இலிருந்து %2$sக்கு மாற்றப்பட்டது" - "மொபைல் டேட்டா" - "வைஃபை" - "புளூடூத்" - "ஈதர்நெட்" - "VPN" + "மொபைல் டேட்டா" + "வைஃபை" + "புளூடூத்" + "ஈதர்நெட்" + "VPN" - "தெரியாத நெட்வொர்க் வகை" + "தெரியாத நெட்வொர்க் வகை" diff --git a/service/ServiceConnectivityResources/res/values-te/strings.xml b/service/ServiceConnectivityResources/res/values-te/strings.xml index c69b599e19..f7182a8de6 100644 --- a/service/ServiceConnectivityResources/res/values-te/strings.xml +++ b/service/ServiceConnectivityResources/res/values-te/strings.xml @@ -17,27 +17,27 @@ - "సిస్టమ్ కనెక్టివిటీ రిసోర్స్‌లు" - "Wi-Fi నెట్‌వర్క్‌కి సైన్ ఇన్ చేయండి" - "నెట్‌వర్క్‌కి సైన్ ఇన్ చేయండి" - + "సిస్టమ్ కనెక్టివిటీ రిసోర్స్‌లు" + "Wi-Fi నెట్‌వర్క్‌కి సైన్ ఇన్ చేయండి" + "నెట్‌వర్క్‌కి సైన్ ఇన్ చేయండి" + - "%1$sకి ఇంటర్నెట్ యాక్సెస్ లేదు" - "ఎంపికల కోసం నొక్కండి" - "మొబైల్ నెట్‌వర్క్‌కు ఇంటర్నెట్ యాక్సెస్ లేదు" - "నెట్‌వర్క్‌కు ఇంటర్నెట్ యాక్సెస్ లేదు" - "ప్రైవేట్ DNS సర్వర్‌ను యాక్సెస్ చేయడం సాధ్యపడదు" - "%1$s పరిమిత కనెక్టివిటీని కలిగి ఉంది" - "ఏదేమైనా కనెక్ట్ చేయడానికి నొక్కండి" - "%1$sకి మార్చబడింది" - "పరికరం %2$sకి ఇంటర్నెట్ యాక్సెస్ లేనప్పుడు %1$sని ఉపయోగిస్తుంది. ఛార్జీలు వర్తించవచ్చు." - "%1$s నుండి %2$sకి మార్చబడింది" + "%1$sకి ఇంటర్నెట్ యాక్సెస్ లేదు" + "ఎంపికల కోసం నొక్కండి" + "మొబైల్ నెట్‌వర్క్‌కు ఇంటర్నెట్ యాక్సెస్ లేదు" + "నెట్‌వర్క్‌కు ఇంటర్నెట్ యాక్సెస్ లేదు" + "ప్రైవేట్ DNS సర్వర్‌ను యాక్సెస్ చేయడం సాధ్యపడదు" + "%1$s పరిమిత కనెక్టివిటీని కలిగి ఉంది" + "ఏదేమైనా కనెక్ట్ చేయడానికి నొక్కండి" + "%1$sకి మార్చబడింది" + "పరికరం %2$sకి ఇంటర్నెట్ యాక్సెస్ లేనప్పుడు %1$sని ఉపయోగిస్తుంది. ఛార్జీలు వర్తించవచ్చు." + "%1$s నుండి %2$sకి మార్చబడింది" - "మొబైల్ డేటా" - "Wi-Fi" - "బ్లూటూత్" - "ఈథర్‌నెట్" - "VPN" + "మొబైల్ డేటా" + "Wi-Fi" + "బ్లూటూత్" + "ఈథర్‌నెట్" + "VPN" - "తెలియని నెట్‌వర్క్ రకం" + "తెలియని నెట్‌వర్క్ రకం" diff --git a/service/ServiceConnectivityResources/res/values-th/strings.xml b/service/ServiceConnectivityResources/res/values-th/strings.xml index eee5a3520d..7049309d6f 100644 --- a/service/ServiceConnectivityResources/res/values-th/strings.xml +++ b/service/ServiceConnectivityResources/res/values-th/strings.xml @@ -17,27 +17,27 @@ - "ทรัพยากรการเชื่อมต่อของระบบ" - "ลงชื่อเข้าใช้เครือข่าย WiFi" - "ลงชื่อเข้าใช้เครือข่าย" - + "ทรัพยากรการเชื่อมต่อของระบบ" + "ลงชื่อเข้าใช้เครือข่าย WiFi" + "ลงชื่อเข้าใช้เครือข่าย" + - "%1$s เข้าถึงอินเทอร์เน็ตไม่ได้" - "แตะเพื่อดูตัวเลือก" - "เครือข่ายมือถือไม่มีการเข้าถึงอินเทอร์เน็ต" - "เครือข่ายไม่มีการเข้าถึงอินเทอร์เน็ต" - "เข้าถึงเซิร์ฟเวอร์ DNS ไม่ได้" - "%1$s มีการเชื่อมต่อจำกัด" - "แตะเพื่อเชื่อมต่อ" - "เปลี่ยนเป็น %1$s" - "อุปกรณ์จะใช้ %1$s เมื่อ %2$s เข้าถึงอินเทอร์เน็ตไม่ได้ โดยอาจมีค่าบริการ" - "เปลี่ยนจาก %1$s เป็น %2$s" + "%1$s เข้าถึงอินเทอร์เน็ตไม่ได้" + "แตะเพื่อดูตัวเลือก" + "เครือข่ายมือถือไม่มีการเข้าถึงอินเทอร์เน็ต" + "เครือข่ายไม่มีการเข้าถึงอินเทอร์เน็ต" + "เข้าถึงเซิร์ฟเวอร์ DNS ไม่ได้" + "%1$s มีการเชื่อมต่อจำกัด" + "แตะเพื่อเชื่อมต่อ" + "เปลี่ยนเป็น %1$s" + "อุปกรณ์จะใช้ %1$s เมื่อ %2$s เข้าถึงอินเทอร์เน็ตไม่ได้ โดยอาจมีค่าบริการ" + "เปลี่ยนจาก %1$s เป็น %2$s" - "อินเทอร์เน็ตมือถือ" - "Wi-Fi" - "บลูทูธ" - "อีเทอร์เน็ต" - "VPN" + "อินเทอร์เน็ตมือถือ" + "Wi-Fi" + "บลูทูธ" + "อีเทอร์เน็ต" + "VPN" - "ประเภทเครือข่ายที่ไม่รู้จัก" + "ประเภทเครือข่ายที่ไม่รู้จัก" diff --git a/service/ServiceConnectivityResources/res/values-tl/strings.xml b/service/ServiceConnectivityResources/res/values-tl/strings.xml index 8d665fe5f9..c866fd413b 100644 --- a/service/ServiceConnectivityResources/res/values-tl/strings.xml +++ b/service/ServiceConnectivityResources/res/values-tl/strings.xml @@ -17,27 +17,27 @@ - "Mga Resource ng Pagkakonekta ng System" - "Mag-sign in sa Wi-Fi network" - "Mag-sign in sa network" - + "Mga Resource ng Pagkakonekta ng System" + "Mag-sign in sa Wi-Fi network" + "Mag-sign in sa network" + - "Walang access sa internet ang %1$s" - "I-tap para sa mga opsyon" - "Walang access sa internet ang mobile network" - "Walang access sa internet ang network" - "Hindi ma-access ang pribadong DNS server" - "Limitado ang koneksyon ng %1$s" - "I-tap para kumonekta pa rin" - "Lumipat sa %1$s" - "Ginagamit ng device ang %1$s kapag walang access sa internet ang %2$s. Maaaring may mga malapat na singilin." - "Lumipat sa %2$s mula sa %1$s" + "Walang access sa internet ang %1$s" + "I-tap para sa mga opsyon" + "Walang access sa internet ang mobile network" + "Walang access sa internet ang network" + "Hindi ma-access ang pribadong DNS server" + "Limitado ang koneksyon ng %1$s" + "I-tap para kumonekta pa rin" + "Lumipat sa %1$s" + "Ginagamit ng device ang %1$s kapag walang access sa internet ang %2$s. Maaaring may mga malapat na singilin." + "Lumipat sa %2$s mula sa %1$s" - "mobile data" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobile data" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "isang hindi kilalang uri ng network" + "isang hindi kilalang uri ng network" diff --git a/service/ServiceConnectivityResources/res/values-tr/strings.xml b/service/ServiceConnectivityResources/res/values-tr/strings.xml index cfb76323ed..c4930a851a 100644 --- a/service/ServiceConnectivityResources/res/values-tr/strings.xml +++ b/service/ServiceConnectivityResources/res/values-tr/strings.xml @@ -17,27 +17,27 @@ - "Sistem Bağlantı Kaynakları" - "Kablosuz ağda oturum açın" - "Ağda oturum açın" - + "Sistem Bağlantı Kaynakları" + "Kablosuz ağda oturum açın" + "Ağda oturum açın" + - "%1$s ağının internet bağlantısı yok" - "Seçenekler için dokunun" - "Mobil ağın internet bağlantısı yok" - "Ağın internet bağlantısı yok" - "Gizli DNS sunucusuna erişilemiyor" - "%1$s sınırlı bağlantıya sahip" - "Yine de bağlanmak için dokunun" - "%1$s ağına geçildi" - "%2$s ağının internet erişimi olmadığında cihaz %1$s ağını kullanır. Bunun için ödeme alınabilir." - "%1$s ağından %2$s ağına geçildi" + "%1$s ağının internet bağlantısı yok" + "Seçenekler için dokunun" + "Mobil ağın internet bağlantısı yok" + "Ağın internet bağlantısı yok" + "Gizli DNS sunucusuna erişilemiyor" + "%1$s sınırlı bağlantıya sahip" + "Yine de bağlanmak için dokunun" + "%1$s ağına geçildi" + "%2$s ağının internet erişimi olmadığında cihaz %1$s ağını kullanır. Bunun için ödeme alınabilir." + "%1$s ağından %2$s ağına geçildi" - "mobil veri" - "Kablosuz" - "Bluetooth" - "Ethernet" - "VPN" + "mobil veri" + "Kablosuz" + "Bluetooth" + "Ethernet" + "VPN" - "bilinmeyen ağ türü" + "bilinmeyen ağ türü" diff --git a/service/ServiceConnectivityResources/res/values-uk/strings.xml b/service/ServiceConnectivityResources/res/values-uk/strings.xml index c5da7460b3..88112638bd 100644 --- a/service/ServiceConnectivityResources/res/values-uk/strings.xml +++ b/service/ServiceConnectivityResources/res/values-uk/strings.xml @@ -17,27 +17,27 @@ - "Ресурси для підключення системи" - "Вхід у мережу Wi-Fi" - "Вхід у мережу" - + "Ресурси для підключення системи" + "Вхід у мережу Wi-Fi" + "Вхід у мережу" + - "Мережа %1$s не має доступу до Інтернету" - "Торкніться, щоб відкрити опції" - "Мобільна мережа не має доступу до Інтернету" - "Мережа не має доступу до Інтернету" - "Немає доступу до приватного DNS-сервера" - "Підключення до мережі %1$s обмежено" - "Натисніть, щоб усе одно підключитися" - "Пристрій перейшов на мережу %1$s" - "Коли мережа %2$s не має доступу до Інтернету, використовується %1$s. Може стягуватися плата." - "Пристрій перейшов з мережі %1$s на мережу %2$s" + "Мережа %1$s не має доступу до Інтернету" + "Торкніться, щоб відкрити опції" + "Мобільна мережа не має доступу до Інтернету" + "Мережа не має доступу до Інтернету" + "Немає доступу до приватного DNS-сервера" + "Підключення до мережі %1$s обмежено" + "Натисніть, щоб усе одно підключитися" + "Пристрій перейшов на мережу %1$s" + "Коли мережа %2$s не має доступу до Інтернету, використовується %1$s. Може стягуватися плата." + "Пристрій перейшов з мережі %1$s на мережу %2$s" - "мобільний Інтернет" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "мобільний Інтернет" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "невідомий тип мережі" + "невідомий тип мережі" diff --git a/service/ServiceConnectivityResources/res/values-ur/strings.xml b/service/ServiceConnectivityResources/res/values-ur/strings.xml index bd2a228ab4..8f9656c725 100644 --- a/service/ServiceConnectivityResources/res/values-ur/strings.xml +++ b/service/ServiceConnectivityResources/res/values-ur/strings.xml @@ -17,27 +17,27 @@ - "سسٹم کنیکٹوٹی کے وسائل" - "‏Wi-Fi نیٹ ورک میں سائن ان کریں" - "نیٹ ورک میں سائن ان کریں" - + "سسٹم کنیکٹوٹی کے وسائل" + "‏Wi-Fi نیٹ ورک میں سائن ان کریں" + "نیٹ ورک میں سائن ان کریں" + - "%1$s کو انٹرنیٹ تک رسائی حاصل نہیں ہے" - "اختیارات کیلئے تھپتھپائیں" - "موبائل نیٹ ورک کو انٹرنیٹ تک رسائی حاصل نہیں ہے" - "نیٹ ورک کو انٹرنیٹ تک رسائی حاصل نہیں ہے" - "‏نجی DNS سرور تک رسائی حاصل نہیں کی جا سکی" - "%1$s کی کنیکٹوٹی محدود ہے" - "بہر حال منسلک کرنے کے لیے تھپتھپائیں" - "%1$s پر سوئچ ہو گیا" - "جب %2$s کو انٹرنیٹ تک رسائی نہیں ہوتی ہے تو آلہ %1$s کا استعمال کرتا ہے۔ چارجز لاگو ہو سکتے ہیں۔" - "%1$s سے %2$s پر سوئچ ہو گیا" + "%1$s کو انٹرنیٹ تک رسائی حاصل نہیں ہے" + "اختیارات کیلئے تھپتھپائیں" + "موبائل نیٹ ورک کو انٹرنیٹ تک رسائی حاصل نہیں ہے" + "نیٹ ورک کو انٹرنیٹ تک رسائی حاصل نہیں ہے" + "‏نجی DNS سرور تک رسائی حاصل نہیں کی جا سکی" + "%1$s کی کنیکٹوٹی محدود ہے" + "بہر حال منسلک کرنے کے لیے تھپتھپائیں" + "%1$s پر سوئچ ہو گیا" + "جب %2$s کو انٹرنیٹ تک رسائی نہیں ہوتی ہے تو آلہ %1$s کا استعمال کرتا ہے۔ چارجز لاگو ہو سکتے ہیں۔" + "%1$s سے %2$s پر سوئچ ہو گیا" - "موبائل ڈیٹا" - "Wi-Fi" - "بلوٹوتھ" - "ایتھرنیٹ" - "VPN" + "موبائل ڈیٹا" + "Wi-Fi" + "بلوٹوتھ" + "ایتھرنیٹ" + "VPN" - "نامعلوم نیٹ ورک کی قسم" + "نامعلوم نیٹ ورک کی قسم" diff --git a/service/ServiceConnectivityResources/res/values-uz/strings.xml b/service/ServiceConnectivityResources/res/values-uz/strings.xml index 567aa886b4..d7285ad7d8 100644 --- a/service/ServiceConnectivityResources/res/values-uz/strings.xml +++ b/service/ServiceConnectivityResources/res/values-uz/strings.xml @@ -17,27 +17,27 @@ - "Tizim aloqa resurslari" - "Wi-Fi tarmoqqa kirish" - "Tarmoqqa kirish" - + "Tizim aloqa resurslari" + "Wi-Fi tarmoqqa kirish" + "Tarmoqqa kirish" + - "%1$s nomli tarmoqda internetga ruxsati yoʻq" - "Variantlarni ko‘rsatish uchun bosing" - "Mobil tarmoq internetga ulanmagan" - "Tarmoq internetga ulanmagan" - "Xususiy DNS server ishlamayapti" - "%1$s nomli tarmoqda aloqa cheklangan" - "Baribir ulash uchun bosing" - "Yangi ulanish: %1$s" - "Agar %2$s tarmoqda internet uzilsa, qurilma %1$sga ulanadi. Sarflangan trafik uchun haq olinishi mumkin." - "%1$s tarmog‘idan %2$s tarmog‘iga o‘tildi" + "%1$s nomli tarmoqda internetga ruxsati yoʻq" + "Variantlarni ko‘rsatish uchun bosing" + "Mobil tarmoq internetga ulanmagan" + "Tarmoq internetga ulanmagan" + "Xususiy DNS server ishlamayapti" + "%1$s nomli tarmoqda aloqa cheklangan" + "Baribir ulash uchun bosing" + "Yangi ulanish: %1$s" + "Agar %2$s tarmoqda internet uzilsa, qurilma %1$sga ulanadi. Sarflangan trafik uchun haq olinishi mumkin." + "%1$s tarmog‘idan %2$s tarmog‘iga o‘tildi" - "mobil internet" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "mobil internet" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "nomaʼlum tarmoq turi" + "nomaʼlum tarmoq turi" diff --git a/service/ServiceConnectivityResources/res/values-vi/strings.xml b/service/ServiceConnectivityResources/res/values-vi/strings.xml index 590b388c96..239fb81b8d 100644 --- a/service/ServiceConnectivityResources/res/values-vi/strings.xml +++ b/service/ServiceConnectivityResources/res/values-vi/strings.xml @@ -17,27 +17,27 @@ - "Tài nguyên kết nối hệ thống" - "Đăng nhập vào mạng Wi-Fi" - "Đăng nhập vào mạng" - + "Tài nguyên kết nối hệ thống" + "Đăng nhập vào mạng Wi-Fi" + "Đăng nhập vào mạng" + - "%1$s không có quyền truy cập Internet" - "Nhấn để biết tùy chọn" - "Mạng di động không có quyền truy cập Internet" - "Mạng không có quyền truy cập Internet" - "Không thể truy cập máy chủ DNS riêng tư" - "%1$s có khả năng kết nối giới hạn" - "Nhấn để tiếp tục kết nối" - "Đã chuyển sang %1$s" - "Thiết bị sử dụng %1$s khi %2$s không có quyền truy cập Internet. Bạn có thể phải trả phí." - "Đã chuyển từ %1$s sang %2$s" + "%1$s không có quyền truy cập Internet" + "Nhấn để biết tùy chọn" + "Mạng di động không có quyền truy cập Internet" + "Mạng không có quyền truy cập Internet" + "Không thể truy cập máy chủ DNS riêng tư" + "%1$s có khả năng kết nối giới hạn" + "Nhấn để tiếp tục kết nối" + "Đã chuyển sang %1$s" + "Thiết bị sử dụng %1$s khi %2$s không có quyền truy cập Internet. Bạn có thể phải trả phí." + "Đã chuyển từ %1$s sang %2$s" - "dữ liệu di động" - "Wi-Fi" - "Bluetooth" - "Ethernet" - "VPN" + "dữ liệu di động" + "Wi-Fi" + "Bluetooth" + "Ethernet" + "VPN" - "loại mạng không xác định" + "loại mạng không xác định" diff --git a/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml b/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml index 9d6cff90e2..e318c0ba44 100644 --- a/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml +++ b/service/ServiceConnectivityResources/res/values-zh-rCN/strings.xml @@ -17,27 +17,27 @@ - "系统网络连接资源" - "登录到WLAN网络" - "登录到网络" - + "系统网络连接资源" + "登录到WLAN网络" + "登录到网络" + - "%1$s 无法访问互联网" - "点按即可查看相关选项" - "此移动网络无法访问互联网" - "此网络无法访问互联网" - "无法访问私人 DNS 服务器" - "%1$s 的连接受限" - "点按即可继续连接" - "已切换至%1$s" - "设备会在%2$s无法访问互联网时使用%1$s(可能需要支付相应的费用)。" - "已从%1$s切换至%2$s" + "%1$s 无法访问互联网" + "点按即可查看相关选项" + "此移动网络无法访问互联网" + "此网络无法访问互联网" + "无法访问私人 DNS 服务器" + "%1$s 的连接受限" + "点按即可继续连接" + "已切换至%1$s" + "设备会在%2$s无法访问互联网时使用%1$s(可能需要支付相应的费用)。" + "已从%1$s切换至%2$s" - "移动数据" - "WLAN" - "蓝牙" - "以太网" - "VPN" + "移动数据" + "WLAN" + "蓝牙" + "以太网" + "VPN" - "未知网络类型" + "未知网络类型" diff --git a/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml b/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml index c84241c896..af3dccd1f4 100644 --- a/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml +++ b/service/ServiceConnectivityResources/res/values-zh-rHK/strings.xml @@ -17,27 +17,27 @@ - "系統連線資源" - "登入 Wi-Fi 網絡" - "登入網絡" - + "系統連線資源" + "登入 Wi-Fi 網絡" + "登入網絡" + - "%1$s未有連接至互聯網" - "輕按即可查看選項" - "流動網絡並未連接互聯網" - "網絡並未連接互聯網" - "無法存取私人 DNS 伺服器" - "%1$s連線受限" - "仍要輕按以連結至此網絡" - "已切換至%1$s" - "裝置會在 %2$s 無法連線至互聯網時使用%1$s (可能需要支付相關費用)。" - "已從%1$s切換至%2$s" + "%1$s未有連接至互聯網" + "輕按即可查看選項" + "流動網絡並未連接互聯網" + "網絡並未連接互聯網" + "無法存取私人 DNS 伺服器" + "%1$s連線受限" + "仍要輕按以連結至此網絡" + "已切換至%1$s" + "裝置會在 %2$s 無法連線至互聯網時使用%1$s (可能需要支付相關費用)。" + "已從%1$s切換至%2$s" - "流動數據" - "Wi-Fi" - "藍牙" - "以太網絡" - "VPN" + "流動數據" + "Wi-Fi" + "藍牙" + "以太網絡" + "VPN" - "不明網絡類型" + "不明網絡類型" diff --git a/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml b/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml index 07540d18c0..644170760e 100644 --- a/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml +++ b/service/ServiceConnectivityResources/res/values-zh-rTW/strings.xml @@ -17,27 +17,27 @@ - "系統連線資源" - "登入 Wi-Fi 網路" - "登入網路" - + "系統連線資源" + "登入 Wi-Fi 網路" + "登入網路" + - "%1$s 沒有網際網路連線" - "輕觸即可查看選項" - "這個行動網路沒有網際網路連線" - "這個網路沒有網際網路連線" - "無法存取私人 DNS 伺服器" - "%1$s 的連線能力受限" - "輕觸即可繼續連線" - "已切換至%1$s" - "裝置會在無法連上「%2$s」時切換至「%1$s」(可能需要支付相關費用)。" - "已從 %1$s 切換至%2$s" + "%1$s 沒有網際網路連線" + "輕觸即可查看選項" + "這個行動網路沒有網際網路連線" + "這個網路沒有網際網路連線" + "無法存取私人 DNS 伺服器" + "%1$s 的連線能力受限" + "輕觸即可繼續連線" + "已切換至%1$s" + "裝置會在無法連上「%2$s」時切換至「%1$s」(可能需要支付相關費用)。" + "已從 %1$s 切換至%2$s" - "行動數據" - "Wi-Fi" - "藍牙" - "乙太網路" - "VPN" + "行動數據" + "Wi-Fi" + "藍牙" + "乙太網路" + "VPN" - "不明的網路類型" + "不明的網路類型" diff --git a/service/ServiceConnectivityResources/res/values-zu/strings.xml b/service/ServiceConnectivityResources/res/values-zu/strings.xml index 19f390ba61..b59f0d1b74 100644 --- a/service/ServiceConnectivityResources/res/values-zu/strings.xml +++ b/service/ServiceConnectivityResources/res/values-zu/strings.xml @@ -17,27 +17,27 @@ - "Izinsiza Zokuxhumeka Zesistimu" - "Ngena ngemvume kunethiwekhi ye-Wi-Fi" - "Ngena ngemvume kunethiwekhi" - + "Izinsiza Zokuxhumeka Zesistimu" + "Ngena ngemvume kunethiwekhi ye-Wi-Fi" + "Ngena ngemvume kunethiwekhi" + - "I-%1$s ayinakho ukufinyelela kwe-inthanethi" - "Thepha ukuze uthole izinketho" - "Inethiwekhi yeselula ayinakho ukufinyelela kwe-inthanethi" - "Inethiwekhi ayinakho ukufinyelela kwenethiwekhi" - "Iseva eyimfihlo ye-DNS ayikwazi ukufinyelelwa" - "I-%1$s inokuxhumeka okukhawulelwe" - "Thepha ukuze uxhume noma kunjalo" - "Kushintshelwe ku-%1$s" - "Idivayisi isebenzisa i-%1$s uma i-%2$s inganakho ukufinyelela kwe-inthanethi. Kungasebenza izindleko." - "Kushintshelewe kusuka ku-%1$s kuya ku-%2$s" + "I-%1$s ayinakho ukufinyelela kwe-inthanethi" + "Thepha ukuze uthole izinketho" + "Inethiwekhi yeselula ayinakho ukufinyelela kwe-inthanethi" + "Inethiwekhi ayinakho ukufinyelela kwenethiwekhi" + "Iseva eyimfihlo ye-DNS ayikwazi ukufinyelelwa" + "I-%1$s inokuxhumeka okukhawulelwe" + "Thepha ukuze uxhume noma kunjalo" + "Kushintshelwe ku-%1$s" + "Idivayisi isebenzisa i-%1$s uma i-%2$s inganakho ukufinyelela kwe-inthanethi. Kungasebenza izindleko." + "Kushintshelewe kusuka ku-%1$s kuya ku-%2$s" - "idatha yeselula" - "I-Wi-Fi" - "I-Bluetooth" - "I-Ethernet" - "I-VPN" + "idatha yeselula" + "I-Wi-Fi" + "I-Bluetooth" + "I-Ethernet" + "I-VPN" - "uhlobo olungaziwa lwenethiwekhi" + "uhlobo olungaziwa lwenethiwekhi" diff --git a/service/lint-baseline.xml b/service/lint-baseline.xml index 95c169ce64..119b64ff95 100644 --- a/service/lint-baseline.xml +++ b/service/lint-baseline.xml @@ -7,7 +7,7 @@ errorLine1=" if (tm.isDataCapable()) {" errorLine2=" ~~~~~~~~~~~~~"> @@ -18,7 +18,7 @@ errorLine1=" mUserAllContext.sendStickyBroadcast(intent, options);" errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -29,7 +29,7 @@ errorLine1=" final int callingVersion = pm.getTargetSdkVersion(callingPackageName);" errorLine2=" ~~~~~~~~~~~~~~~~~~~"> diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java index 9bda59cc86..fb126ca6ee 100644 --- a/service/src/com/android/server/connectivity/PermissionMonitor.java +++ b/service/src/com/android/server/connectivity/PermissionMonitor.java @@ -24,7 +24,7 @@ import static android.Manifest.permission.UPDATE_DEVICE_STATS; import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED; import static android.content.pm.PackageManager.GET_PERMISSIONS; import static android.content.pm.PackageManager.MATCH_ANY_USER; -import static android.net.ConnectivitySettingsManager.APPS_ALLOWED_ON_RESTRICTED_NETWORKS; +import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS; import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; import static android.os.Process.INVALID_UID; import static android.os.Process.SYSTEM_UID; @@ -109,13 +109,13 @@ public class PermissionMonitor { @GuardedBy("this") private final Set mAllApps = new HashSet<>(); - // A set of apps which are allowed to use restricted networks. These apps can't hold the - // CONNECTIVITY_USE_RESTRICTED_NETWORKS permission because they can't be signature|privileged - // apps. However, these apps should still be able to use restricted networks under certain - // conditions (e.g. government app using emergency services). So grant netd system permission - // to uids whose package name is listed in APPS_ALLOWED_ON_RESTRICTED_NETWORKS setting. + // A set of uids which are allowed to use restricted networks. The packages of these uids can't + // hold the CONNECTIVITY_USE_RESTRICTED_NETWORKS permission because they can't be + // signature|privileged apps. However, these apps should still be able to use restricted + // networks under certain conditions (e.g. government app using emergency services). So grant + // netd system permission to these uids which is listed in UIDS_ALLOWED_ON_RESTRICTED_NETWORKS. @GuardedBy("this") - private final Set mAppsAllowedOnRestrictedNetworks = new ArraySet<>(); + private final Set mUidsAllowedOnRestrictedNetworks = new ArraySet<>(); private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { @Override @@ -149,10 +149,10 @@ public class PermissionMonitor { } /** - * Get apps allowed to use restricted networks via ConnectivitySettingsManager. + * Get uids allowed to use restricted networks via ConnectivitySettingsManager. */ - public Set getAppsAllowedOnRestrictedNetworks(@NonNull Context context) { - return ConnectivitySettingsManager.getAppsAllowedOnRestrictedNetworks(context); + public Set getUidsAllowedOnRestrictedNetworks(@NonNull Context context) { + return ConnectivitySettingsManager.getUidsAllowedOnRestrictedNetworks(context); } /** @@ -194,10 +194,10 @@ public class PermissionMonitor { mIntentReceiver, intentFilter, null /* broadcastPermission */, null /* scheduler */); - // Register APPS_ALLOWED_ON_RESTRICTED_NETWORKS setting observer + // Register UIDS_ALLOWED_ON_RESTRICTED_NETWORKS setting observer mDeps.registerContentObserver( userAllContext, - Settings.Secure.getUriFor(APPS_ALLOWED_ON_RESTRICTED_NETWORKS), + Settings.Secure.getUriFor(UIDS_ALLOWED_ON_RESTRICTED_NETWORKS), false /* notifyForDescendants */, new ContentObserver(null) { @Override @@ -206,9 +206,9 @@ public class PermissionMonitor { } }); - // Read APPS_ALLOWED_ON_RESTRICTED_NETWORKS setting and update - // mAppsAllowedOnRestrictedNetworks. - updateAppsAllowedOnRestrictedNetworks(mDeps.getAppsAllowedOnRestrictedNetworks(mContext)); + // Read UIDS_ALLOWED_ON_RESTRICTED_NETWORKS setting and update + // mUidsAllowedOnRestrictedNetworks. + updateUidsAllowedOnRestrictedNetworks(mDeps.getUidsAllowedOnRestrictedNetworks(mContext)); List apps = mPackageManager.getInstalledPackages(GET_PERMISSIONS | MATCH_ANY_USER); @@ -265,9 +265,9 @@ public class PermissionMonitor { } @VisibleForTesting - void updateAppsAllowedOnRestrictedNetworks(final Set apps) { - mAppsAllowedOnRestrictedNetworks.clear(); - mAppsAllowedOnRestrictedNetworks.addAll(apps); + void updateUidsAllowedOnRestrictedNetworks(final Set uids) { + mUidsAllowedOnRestrictedNetworks.clear(); + mUidsAllowedOnRestrictedNetworks.addAll(uids); } @VisibleForTesting @@ -285,10 +285,11 @@ public class PermissionMonitor { } @VisibleForTesting - boolean isAppAllowedOnRestrictedNetworks(@NonNull final PackageInfo app) { - // Check whether package name is in allowed on restricted networks app list. If so, this app - // can have netd system permission. - return mAppsAllowedOnRestrictedNetworks.contains(app.packageName); + boolean isUidAllowedOnRestrictedNetworks(final ApplicationInfo appInfo) { + if (appInfo == null) return false; + // Check whether package's uid is in allowed on restricted networks uid list. If so, this + // uid can have netd system permission. + return mUidsAllowedOnRestrictedNetworks.contains(appInfo.uid); } @VisibleForTesting @@ -310,7 +311,8 @@ public class PermissionMonitor { boolean hasRestrictedNetworkPermission(@NonNull final PackageInfo app) { // TODO : remove carryover package check in the future(b/31479477). All apps should just // request the appropriate permission for their use case since android Q. - return isCarryoverPackage(app.applicationInfo) || isAppAllowedOnRestrictedNetworks(app) + return isCarryoverPackage(app.applicationInfo) + || isUidAllowedOnRestrictedNetworks(app.applicationInfo) || hasPermission(app, PERMISSION_MAINLINE_NETWORK_STACK) || hasPermission(app, NETWORK_STACK) || hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS); @@ -770,35 +772,31 @@ public class PermissionMonitor { } private synchronized void onSettingChanged() { - // Step1. Update apps allowed to use restricted networks and compute the set of packages to + // Step1. Update uids allowed to use restricted networks and compute the set of uids to // update. - final Set packagesToUpdate = new ArraySet<>(mAppsAllowedOnRestrictedNetworks); - updateAppsAllowedOnRestrictedNetworks(mDeps.getAppsAllowedOnRestrictedNetworks(mContext)); - packagesToUpdate.addAll(mAppsAllowedOnRestrictedNetworks); + final Set uidsToUpdate = new ArraySet<>(mUidsAllowedOnRestrictedNetworks); + updateUidsAllowedOnRestrictedNetworks(mDeps.getUidsAllowedOnRestrictedNetworks(mContext)); + uidsToUpdate.addAll(mUidsAllowedOnRestrictedNetworks); - final Map updatedApps = new HashMap<>(); - final Map removedApps = new HashMap<>(); + final Map updatedUids = new HashMap<>(); + final Map removedUids = new HashMap<>(); - // Step2. For each package to update, find out its new permission. - for (String app : packagesToUpdate) { - final PackageInfo info = getPackageInfo(app); - if (info == null || info.applicationInfo == null) continue; - - final int uid = info.applicationInfo.uid; + // Step2. For each uid to update, find out its new permission. + for (Integer uid : uidsToUpdate) { final Boolean permission = highestUidNetworkPermission(uid); if (null == permission) { - removedApps.put(uid, NETWORK); // Doesn't matter which permission is set here. + removedUids.put(uid, NETWORK); // Doesn't matter which permission is set here. mApps.remove(uid); } else { - updatedApps.put(uid, permission); + updatedUids.put(uid, permission); mApps.put(uid, permission); } } // Step3. Update or revoke permission for uids with netd. - update(mUsers, updatedApps, true /* add */); - update(mUsers, removedApps, false /* add */); + update(mUsers, updatedUids, true /* add */); + update(mUsers, removedUids, false /* add */); } /** Dump info to dumpsys */ diff --git a/tests/common/Android.bp b/tests/common/Android.bp index 73314532a5..e8963b9011 100644 --- a/tests/common/Android.bp +++ b/tests/common/Android.bp @@ -18,11 +18,7 @@ // They must be fast and stable, and exercise public or test APIs. package { // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], + default_applicable_licenses: ["Android-Apache-2.0"], } java_library { diff --git a/tests/cts/OWNERS b/tests/cts/OWNERS new file mode 100644 index 0000000000..426434508a --- /dev/null +++ b/tests/cts/OWNERS @@ -0,0 +1,4 @@ +# Bug component: 31808 +set noparent +lorenzo@google.com +satk@google.com \ No newline at end of file diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp new file mode 100644 index 0000000000..3185f7edcd --- /dev/null +++ b/tests/cts/hostside/Android.bp @@ -0,0 +1,33 @@ +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +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..7a7331375d --- /dev/null +++ b/tests/cts/hostside/AndroidTest.xml @@ -0,0 +1,44 @@ + + + + diff --git a/tests/cts/hostside/OWNERS b/tests/cts/hostside/OWNERS new file mode 100644 index 0000000000..20bc55e8bd --- /dev/null +++ b/tests/cts/hostside/OWNERS @@ -0,0 +1,4 @@ +# Bug component: 61373 +# Inherits parent owners +sudheersai@google.com +jchalard@google.com diff --git a/tests/cts/hostside/TEST_MAPPING b/tests/cts/hostside/TEST_MAPPING new file mode 100644 index 0000000000..fcec4830da --- /dev/null +++ b/tests/cts/hostside/TEST_MAPPING @@ -0,0 +1,18 @@ +{ + "presubmit-large": [ + { + "name": "CtsHostsideNetworkTests", + "options": [ + { + "include-filter": "com.android.cts.net.HostsideRestrictBackgroundNetworkTests" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + }, + { + "exclude-annotation": "android.platform.test.annotations.FlakyTest" + } + ] + } + ] +} diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp new file mode 100644 index 0000000000..2751f6ff82 --- /dev/null +++ b/tests/cts/hostside/aidl/Android.bp @@ -0,0 +1,28 @@ +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +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..28437c28a6 --- /dev/null +++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl @@ -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. + */ + +package com.android.cts.net.hostside; + +import android.app.job.JobInfo; + +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 NetworkRequest request, in INetworkCallback cb); + void unregisterNetworkCallback(); + void scheduleJob(in JobInfo jobInfo); +} 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..19198c5f99 --- /dev/null +++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/INetworkStateObserver.aidl @@ -0,0 +1,26 @@ +/* + * 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 { + void onNetworkStateChecked(int resultCode, String resultData); + + const int RESULT_SUCCESS_NETWORK_STATE_CHECKED = 0; + const int RESULT_ERROR_UNEXPECTED_PROC_STATE = 1; + const int RESULT_ERROR_UNEXPECTED_CAPABILITIES = 2; + const int RESULT_ERROR_OTHER = 3; +} \ 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..5b2369cddd --- /dev/null +++ b/tests/cts/hostside/app/Android.bp @@ -0,0 +1,48 @@ +// +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test_helper_app { + name: "CtsHostsideNetworkTestsApp", + defaults: [ + "cts_support_defaults", + "framework-connectivity-test-defaults", + ], + platform_apis: true, + static_libs: [ + "androidx.test.rules", + "androidx.test.ext.junit", + "compatibility-device-util-axt", + "cts-net-utils", + "ctstestrunner-axt", + "ub-uiautomator", + "CtsHostsideNetworkTestsAidl", + "modules-utils-build", + ], + 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..e5bae5fdc0 --- /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..d9ff53955c --- /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(); + + resetBatteryState(); + 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..e0ce4ead39 --- /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 { + setPendingIntentAllowlistDuration(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/AbstractExpeditedJobTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractExpeditedJobTest.java new file mode 100644 index 0000000000..a850e3b983 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractExpeditedJobTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.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 org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class AbstractExpeditedJobTest extends AbstractRestrictBackgroundNetworkTestCase { + @Before + public final void setUp() throws Exception { + super.setUp(); + resetDeviceState(); + } + + @After + public final void tearDown() throws Exception { + super.tearDown(); + resetDeviceState(); + } + + private void resetDeviceState() throws Exception { + resetBatteryState(); + setBatterySaverMode(false); + setRestrictBackground(false); + setAppIdle(false); + setDozeMode(false); + } + + @Test + @RequiredProperties({BATTERY_SAVER_MODE}) + public void testNetworkAccess_batterySaverMode() throws Exception { + assertBackgroundNetworkAccess(true); + assertExpeditedJobHasNetworkAccess(); + + setBatterySaverMode(true); + assertBackgroundNetworkAccess(false); + assertExpeditedJobHasNetworkAccess(); + } + + @Test + @RequiredProperties({DATA_SAVER_MODE, METERED_NETWORK}) + public void testNetworkAccess_dataSaverMode() throws Exception { + assertBackgroundNetworkAccess(true); + assertExpeditedJobHasNetworkAccess(); + + setRestrictBackground(true); + assertBackgroundNetworkAccess(false); + assertExpeditedJobHasNoNetworkAccess(); + } + + @Test + @RequiredProperties({APP_STANDBY_MODE}) + public void testNetworkAccess_appIdleState() throws Exception { + turnBatteryOn(); + assertBackgroundNetworkAccess(true); + assertExpeditedJobHasNetworkAccess(); + + setAppIdle(true); + assertBackgroundNetworkAccess(false); + assertExpeditedJobHasNetworkAccess(); + } + + @Test + @RequiredProperties({DOZE_MODE}) + public void testNetworkAccess_dozeMode() throws Exception { + assertBackgroundNetworkAccess(true); + assertExpeditedJobHasNetworkAccess(); + + setDozeMode(true); + assertBackgroundNetworkAccess(false); + assertExpeditedJobHasNetworkAccess(); + } + + @Test + @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, METERED_NETWORK}) + public void testNetworkAccess_dataAndBatterySaverMode() throws Exception { + assertBackgroundNetworkAccess(true); + assertExpeditedJobHasNetworkAccess(); + + setRestrictBackground(true); + setBatterySaverMode(true); + assertBackgroundNetworkAccess(false); + assertExpeditedJobHasNoNetworkAccess(); + } + + @Test + @RequiredProperties({DOZE_MODE, DATA_SAVER_MODE, METERED_NETWORK}) + public void testNetworkAccess_dozeAndDataSaverMode() throws Exception { + assertBackgroundNetworkAccess(true); + assertExpeditedJobHasNetworkAccess(); + + setRestrictBackground(true); + setDozeMode(true); + assertBackgroundNetworkAccess(false); + assertExpeditedJobHasNoNetworkAccess(); + } + + @Test + @RequiredProperties({DATA_SAVER_MODE, BATTERY_SAVER_MODE, METERED_NETWORK, DOZE_MODE, + APP_STANDBY_MODE}) + public void testNetworkAccess_allRestrictionsEnabled() throws Exception { + assertBackgroundNetworkAccess(true); + assertExpeditedJobHasNetworkAccess(); + + setRestrictBackground(true); + setBatterySaverMode(true); + setAppIdle(true); + setDozeMode(true); + assertBackgroundNetworkAccess(false); + assertExpeditedJobHasNoNetworkAccess(); + } +} 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..f9454ad052 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java @@ -0,0 +1,974 @@ +/* + * 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.forceRunJob; +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.isAppStandbySupported; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isBatterySaverSupported; +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.app.job.JobInfo; +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.NetworkRequest; +import android.os.BatteryManager; +import android.os.Binder; +import android.os.Bundle; +import android.os.SystemClock; +import android.provider.DeviceConfig; +import android.service.notification.NotificationListenerService; +import android.util.Log; +import android.util.Pair; + +import com.android.compatibility.common.util.BatteryUtils; +import com.android.compatibility.common.util.DeviceConfigStateHelper; + +import org.junit.Rule; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +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 String TEST_APP2_JOB_SERVICE_CLASS = TEST_APP2_PKG + ".MyJobService"; + + private static final ComponentName TEST_JOB_COMPONENT = new ComponentName( + TEST_APP2_PKG, TEST_APP2_JOB_SERVICE_CLASS); + + private static final int TEST_JOB_ID = 7357437; + + 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_FINISH_ACTIVITY = + "com.android.cts.net.hostside.app2.action.FINISH_ACTIVITY"; + private static final String ACTION_FINISH_JOB = + "com.android.cts.net.hostside.app2.action.FINISH_JOB"; + + 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"; + private static final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks"; + + protected static final int TYPE_COMPONENT_ACTIVTIY = 0; + protected static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1; + protected static final int TYPE_EXPEDITED_JOB = 2; + + private static final int BATTERY_STATE_TIMEOUT_MS = 5000; + private static final int BATTERY_STATE_CHECK_INTERVAL_MS = 500; + + private static final int ACTIVITY_NETWORK_STATE_TIMEOUT_MS = 6_000; + private static final int JOB_NETWORK_STATE_TIMEOUT_MS = 10_000; + + // 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 DeviceConfigStateHelper mDeviceIdleDeviceConfigStateHelper; + + @Rule + public final RuleChain mRuleChain = RuleChain.outerRule(new RequiredPropertiesRule()) + .around(new MeterednessConfigurationRule()); + + protected void setUp() throws Exception { + // TODO: Annotate these constants with @TestApi instead of obtaining them using reflection + PROCESS_STATE_FOREGROUND_SERVICE = (Integer) ActivityManager.class + .getDeclaredField("PROCESS_STATE_FOREGROUND_SERVICE").get(null); + mInstrumentation = getInstrumentation(); + mContext = getContext(); + mCm = getConnectivityManager(); + mDeviceIdleDeviceConfigStateHelper = + new DeviceConfigStateHelper(DeviceConfig.NAMESPACE_DEVICE_IDLE); + mUid = getUid(TEST_APP2_PKG); + mMyUid = getUid(mContext.getPackageName()); + mServiceClient = new MyServiceClient(mContext); + mServiceClient.bind(); + 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 { + assertForegroundNetworkAccess(true); + } + + protected void assertForegroundNetworkAccess(boolean expectAllowed) 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(expectAllowed /* 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, 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 void assertExpeditedJobHasNetworkAccess() throws Exception { + launchComponentAndAssertNetworkAccess(TYPE_EXPEDITED_JOB); + finishExpeditedJob(); + } + + protected void assertExpeditedJobHasNoNetworkAccess() throws Exception { + launchComponentAndAssertNetworkAccess(TYPE_EXPEDITED_JOB, false); + finishExpeditedJob(); + } + + 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 == null) 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.length() == 0 ? null : 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); + } + + protected void resetBatteryState() { + BatteryUtils.runDumpsysBatteryReset(); + } + + 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 { + if (!isBatterySaverSupported()) { + return; + } + 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 { + if (!isDozeModeSupported()) { + return; + } + + 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 { + if (!isAppStandbySupported()) { + return; + } + 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 { + if (!isAppStandbySupported()) { + return; + } + 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(final NetworkRequest request, INetworkCallback cb) + throws Exception { + Log.i(TAG, "Registering network callback for request: " + request); + mServiceClient.registerNetworkCallback(request, 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 setPendingIntentAllowlistDuration(long durationMs) { + mDeviceIdleDeviceConfigStateHelper.set("notification_allowlist_duration_ms", + String.valueOf(durationMs)); + } + + protected void resetDeviceIdleSettings() { + mDeviceIdleDeviceConfigStateHelper.restoreOriginalValues(); + } + + protected void launchComponentAndAssertNetworkAccess(int type) throws Exception { + launchComponentAndAssertNetworkAccess(type, true); + } + + protected void launchComponentAndAssertNetworkAccess(int type, boolean expectAvailable) + 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 ArrayList> result = new ArrayList<>(1); + extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result)); + extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable); + launchIntent.putExtras(extras); + mContext.startActivity(launchIntent); + if (latch.await(ACTIVITY_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + final int resultCode = result.get(0).first; + final String resultData = result.get(0).second; + if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) { + final String error = checkForAvailabilityInResultData( + resultData, expectAvailable); + if (error != null) { + fail("Network is not available for activity in app2 (" + mUid + "): " + + error); + } + } else if (resultCode == INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE) { + Log.d(TAG, resultData); + // App didn't come to foreground when the activity is started, so try again. + assertForegroundNetworkAccess(); + } else { + fail("Unexpected resultCode=" + resultCode + "; received=[" + resultData + "]"); + } + } else { + fail("Timed out waiting for network availability status from app2's activity (" + + mUid + ")"); + } + } else if (type == TYPE_EXPEDITED_JOB) { + final Bundle extras = new Bundle(); + final ArrayList> result = new ArrayList<>(1); + final CountDownLatch latch = new CountDownLatch(1); + extras.putBinder(KEY_NETWORK_STATE_OBSERVER, getNewNetworkStateObserver(latch, result)); + extras.putBoolean(KEY_SKIP_VALIDATION_CHECKS, !expectAvailable); + final JobInfo jobInfo = new JobInfo.Builder(TEST_JOB_ID, TEST_JOB_COMPONENT) + .setExpedited(true) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + .setTransientExtras(extras) + .build(); + mServiceClient.scheduleJob(jobInfo); + forceRunJob(TEST_APP2_PKG, TEST_JOB_ID); + if (latch.await(JOB_NETWORK_STATE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + final int resultCode = result.get(0).first; + final String resultData = result.get(0).second; + if (resultCode == INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED) { + final String error = checkForAvailabilityInResultData( + resultData, expectAvailable); + if (error != null) { + fail("Network is not available for expedited job in app2 (" + mUid + "): " + + error); + } + } else { + fail("Unexpected resultCode=" + resultCode + "; received=[" + resultData + "]"); + } + } else { + fail("Timed out waiting for network availability status from app2's expedited job (" + + 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 ArrayList> result) { + return new INetworkStateObserver.Stub() { + @Override + public void onNetworkStateChecked(int resultCode, String resultData) { + result.add(Pair.create(resultCode, resultData)); + latch.countDown(); + } + }; + } + + /** + * Finishes an activity on app2 so its process is demoted from foreground status. + */ + protected void finishActivity() throws Exception { + final Intent intent = new Intent(ACTION_FINISH_ACTIVITY) + .setPackage(TEST_APP2_PKG) + .setFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY); + sendOrderedBroadcast(intent); + } + + /** + * Finishes the expedited job on app2 so its process is demoted from foreground status. + */ + private void finishExpeditedJob() throws Exception { + final Intent intent = new Intent(ACTION_FINISH_JOB) + .setPackage(TEST_APP2_PKG) + .setFlags(Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY); + sendOrderedBroadcast(intent); + } + + 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..66cb935a1c --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java @@ -0,0 +1,102 @@ +/* + * 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 androidx.test.platform.app.InstrumentationRegistry; + +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; + +public class DumpOnFailureRule extends OnFailureRule { + private File mDumpDir = new File(Environment.getExternalStorageDirectory(), + "CtsHostsideNetworkTests"); + + @Override + public void onTestFailure(Statement base, Description description, Throwable throwable) { + if (throwable instanceof AssumptionViolatedException) { + final String testName = description.getClassName() + "_" + description.getMethodName(); + Log.d(TAG, "Skipping test " + testName + ": " + throwable); + return; + } + + prepareDumpRootDir(); + final File dumpFile = new File(mDumpDir, "dump-" + getShortenedTestName(description)); + 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); + } + } + + private String getShortenedTestName(Description description) { + final String qualifiedClassName = description.getClassName(); + final String className = qualifiedClassName.substring( + qualifiedClassName.lastIndexOf(".") + 1); + final String shortenedClassName = className.chars() + .filter(Character::isUpperCase) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + return shortenedClassName + "_" + description.getMethodName(); + } + + 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/ExpeditedJobMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobMeteredTest.java new file mode 100644 index 0000000000..3809534e21 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobMeteredTest.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.cts.net.hostside; + +import static com.android.cts.net.hostside.Property.METERED_NETWORK; + +@RequiredProperties({METERED_NETWORK}) +public class ExpeditedJobMeteredTest extends AbstractExpeditedJobTest { +} diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobNonMeteredTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobNonMeteredTest.java new file mode 100644 index 0000000000..6596269ceb --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/ExpeditedJobNonMeteredTest.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.cts.net.hostside; + +import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK; + +@RequiredProperties({NON_METERED_NETWORK}) +public class ExpeditedJobNonMeteredTest extends AbstractExpeditedJobTest { +} 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..5c99c679c8 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java @@ -0,0 +1,59 @@ +/* + * 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.setupActiveNetworkMeteredness; +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 com.android.compatibility.common.util.BeforeAfterRule; +import com.android.compatibility.common.util.ThrowingRunnable; + +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +public class MeterednessConfigurationRule extends BeforeAfterRule { + private ThrowingRunnable mMeterednessResetter; + + @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 { + mMeterednessResetter = setupActiveNetworkMeteredness(metered); + } + + public void resetNetworkMeteredness() throws Exception { + if (mMeterednessResetter != null) { + mMeterednessResetter.run(); + mMeterednessResetter = null; + } + } +} 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..55eec1190d --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java @@ -0,0 +1,55 @@ +/* + * 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.app.KeyguardManager; +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); + + // Dismiss the keyguard so that the tests can click on the VPN confirmation dialog. + // FLAG_DISMISS_KEYGUARD is not sufficient to do this because as soon as the dialog appears, + // this activity goes into the background and the keyguard reappears. + getSystemService(KeyguardManager.class).requestDismissKeyguard(this, null /* callback */); + } + + @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..8b70f9b549 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.cts.net.hostside; + +import android.app.job.JobInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.net.NetworkRequest; +import android.os.ConditionVariable; +import android.os.IBinder; +import android.os.RemoteException; + +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(final NetworkRequest request, INetworkCallback cb) + throws RemoteException { + mService.registerNetworkCallback(request, cb); + } + + public void unregisterNetworkCallback() throws RemoteException { + mService.unregisterNetworkCallback(); + } + + public void scheduleJob(JobInfo jobInfo) throws RemoteException { + mService.scheduleJob(jobInfo); + } +} 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..0715e32bd1 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java @@ -0,0 +1,308 @@ +/* + * 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 android.net.NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED; + +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getActiveNetworkCapabilities; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground; +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.fail; +import static org.junit.Assume.assumeTrue; + +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +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(canChangeActiveNetworkMeteredness()); + + registerBroadcastReceiver(); + + removeRestrictBackgroundWhitelist(mUid); + removeRestrictBackgroundBlacklist(mUid); + assertRestrictBackgroundChangedReceived(0); + + // Initial state + setBatterySaverMode(false); + setRestrictBackground(false); + + // Get transports of the active network, this has to be done before changing meteredness, + // since wifi will be disconnected when changing from non-metered to metered. + final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities(); + + // Mark network as metered. + mMeterednessConfiguration.configureNetworkMeteredness(true); + + // Register callback, copy the capabilities from the active network to expect the "original" + // network before disconnecting, but null out some fields to prevent over-specified. + registerNetworkCallback(new NetworkRequest.Builder() + .setCapabilities(networkCapabilities.setTransportInfo(null)) + .removeCapability(NET_CAPABILITY_NOT_METERED) + .setSignalStrength(SIGNAL_STRENGTH_UNSPECIFIED).build(), mTestNetworkCallback); + // Wait for onAvailable() callback to ensure network is available 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..7da1a212ad --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java @@ -0,0 +1,418 @@ +/* + * 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_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_METERED; +import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_NONE; + +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.assertNotEquals; +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.UiAutomation; +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.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.ActionListener; +import android.os.PersistableBundle; +import android.os.Process; +import android.os.UserHandle; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; +import android.telephony.data.ApnSetting; +import android.util.Log; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.compatibility.common.util.AppStandbyUtils; +import com.android.compatibility.common.util.BatteryUtils; +import com.android.compatibility.common.util.ShellIdentityUtils; +import com.android.compatibility.common.util.ThrowingRunnable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +public class NetworkPolicyTestUtils { + + // android.telephony.CarrierConfigManager.KEY_CARRIER_METERED_APN_TYPES_STRINGS + // TODO: Expose it as a @TestApi instead of copying the constant + private static final String KEY_CARRIER_METERED_APN_TYPES_STRINGS = + "carrier_metered_apn_types_strings"; + + private static final int TIMEOUT_CHANGE_METEREDNESS_MS = 10_000; + + private static ConnectivityManager mCm; + private static WifiManager mWm; + private static CarrierConfigManager mCarrierConfigManager; + + 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 { + setRestrictBackgroundInternal(true); + mDataSaverSupported = !isMyRestrictBackgroundStatus( + RESTRICT_BACKGROUND_STATUS_DISABLED); + } finally { + setRestrictBackgroundInternal(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(); + } + + /** Forces JobScheduler to run the job if constraints are met. */ + public static void forceRunJob(String pkg, int jobId) { + executeShellCommand("cmd jobscheduler run -f -u " + UserHandle.myUserId() + + " " + pkg + " " + jobId); + } + + 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 NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities(); + return networkCapabilities.hasTransport(TRANSPORT_WIFI) + || networkCapabilities.hasTransport(TRANSPORT_CELLULAR); + } + + /** + * Updates the meteredness of the active network. Right now we can only change meteredness + * of either Wifi or cellular network, so if the active network is not either of these, this + * will throw an exception. + * + * @return a {@link ThrowingRunnable} object that can used to reset the meteredness change + * made by this method. + */ + public static ThrowingRunnable setupActiveNetworkMeteredness(boolean metered) throws Exception { + if (isActiveNetworkMetered(metered)) { + return null; + } + final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities(); + if (networkCapabilities.hasTransport(TRANSPORT_WIFI)) { + final String ssid = getWifiSsid(); + setWifiMeteredStatus(ssid, metered); + return () -> setWifiMeteredStatus(ssid, !metered); + } else if (networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) { + final int subId = SubscriptionManager.getActiveDataSubscriptionId(); + setCellularMeteredStatus(subId, metered); + return () -> setCellularMeteredStatus(subId, !metered); + } else { + // Right now, we don't have a way to change meteredness of networks other + // than Wi-Fi or Cellular, so just throw an exception. + throw new IllegalStateException("Can't change meteredness of current active network"); + } + } + + private static String getWifiSsid() { + final UiAutomation uiAutomation = getInstrumentation().getUiAutomation(); + try { + uiAutomation.adoptShellPermissionIdentity(); + final String ssid = getWifiManager().getConnectionInfo().getSSID(); + assertNotEquals(WifiManager.UNKNOWN_SSID, ssid); + return ssid; + } finally { + uiAutomation.dropShellPermissionIdentity(); + } + } + + static NetworkCapabilities getActiveNetworkCapabilities() { + final Network activeNetwork = getConnectivityManager().getActiveNetwork(); + assertNotNull("No active network available", activeNetwork); + return getConnectivityManager().getNetworkCapabilities(activeNetwork); + } + + private static void setWifiMeteredStatus(String ssid, boolean metered) throws Exception { + final UiAutomation uiAutomation = getInstrumentation().getUiAutomation(); + try { + uiAutomation.adoptShellPermissionIdentity(); + final WifiConfiguration currentConfig = getWifiConfiguration(ssid); + currentConfig.meteredOverride = metered + ? METERED_OVERRIDE_METERED : METERED_OVERRIDE_NONE; + BlockingQueue blockingQueue = new LinkedBlockingQueue<>(); + getWifiManager().save(currentConfig, createActionListener( + blockingQueue, Integer.MAX_VALUE)); + Integer resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS, + TimeUnit.MILLISECONDS); + if (resultCode == null) { + fail("Timed out waiting for meteredness to change; ssid=" + ssid + + ", metered=" + metered); + } else if (resultCode != Integer.MAX_VALUE) { + fail("Error overriding the meteredness; ssid=" + ssid + + ", metered=" + metered + ", error=" + resultCode); + } + final boolean success = assertActiveNetworkMetered(metered, false /* throwOnFailure */); + if (!success) { + Log.i(TAG, "Retry connecting to wifi; ssid=" + ssid); + blockingQueue = new LinkedBlockingQueue<>(); + getWifiManager().connect(currentConfig, createActionListener( + blockingQueue, Integer.MAX_VALUE)); + resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS, + TimeUnit.MILLISECONDS); + if (resultCode == null) { + fail("Timed out waiting for wifi to connect; ssid=" + ssid); + } else if (resultCode != Integer.MAX_VALUE) { + fail("Error connecting to wifi; ssid=" + ssid + + ", error=" + resultCode); + } + assertActiveNetworkMetered(metered, true /* throwOnFailure */); + } + } finally { + uiAutomation.dropShellPermissionIdentity(); + } + } + + private static WifiConfiguration getWifiConfiguration(String ssid) { + final List ssids = new ArrayList<>(); + for (WifiConfiguration config : getWifiManager().getConfiguredNetworks()) { + if (config.SSID.equals(ssid)) { + return config; + } + ssids.add(config.SSID); + } + fail("Couldn't find the wifi config; ssid=" + ssid + + ", all=" + Arrays.toString(ssids.toArray())); + return null; + } + + private static ActionListener createActionListener(BlockingQueue blockingQueue, + int successCode) { + return new ActionListener() { + @Override + public void onSuccess() { + blockingQueue.offer(successCode); + } + + @Override + public void onFailure(int reason) { + blockingQueue.offer(reason); + } + }; + } + + private static void setCellularMeteredStatus(int subId, boolean metered) throws Exception { + final PersistableBundle bundle = new PersistableBundle(); + bundle.putStringArray(KEY_CARRIER_METERED_APN_TYPES_STRINGS, + new String[] {ApnSetting.TYPE_MMS_STRING}); + ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(getCarrierConfigManager(), + (cm) -> cm.overrideConfig(subId, metered ? null : bundle)); + assertActiveNetworkMetered(metered, true /* throwOnFailure */); + } + + private static boolean assertActiveNetworkMetered(boolean expectedMeteredStatus, + boolean throwOnFailure) 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); + try { + if (!latch.await(TIMEOUT_CHANGE_METEREDNESS_MS, TimeUnit.MILLISECONDS)) { + final String errorMsg = "Timed out waiting for active network metered status " + + "to change to " + expectedMeteredStatus + "; network = " + + getConnectivityManager().getActiveNetwork(); + if (throwOnFailure) { + fail(errorMsg); + } + Log.w(TAG, errorMsg); + return false; + } + return true; + } finally { + getConnectivityManager().unregisterNetworkCallback(networkCallback); + } + } + + public static void setRestrictBackground(boolean enabled) { + if (!isDataSaverSupported()) { + return; + } + setRestrictBackgroundInternal(enabled); + } + + private static void setRestrictBackgroundInternal(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 CarrierConfigManager getCarrierConfigManager() { + if (mCarrierConfigManager == null) { + mCarrierConfigManager = (CarrierConfigManager) getContext().getSystemService( + Context.CARRIER_CONFIG_SERVICE); + } + return mCarrierConfigManager; + } + + 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/RestrictedModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java new file mode 100644 index 0000000000..29d3c6e1ba --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.cts.net.hostside; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public final class RestrictedModeTest extends AbstractRestrictBackgroundNetworkTestCase { + @Before + public void setUp() throws Exception { + super.setUp(); + } + + @After + public void tearDown() throws Exception { + setRestrictedMode(false); + super.tearDown(); + } + + private void setRestrictedMode(boolean enabled) throws Exception { + executeSilentShellCommand( + "settings put global restricted_networking_mode " + (enabled ? 1 : 0)); + assertRestrictedModeState(enabled); + } + + private void assertRestrictedModeState(boolean enabled) throws Exception { + assertDelayedShellCommand("cmd netpolicy get restricted-mode", + "Restricted mode status: " + (enabled ? "enabled" : "disabled")); + } + + @Test + public void testNetworkAccess() throws Exception { + setRestrictedMode(false); + + // go to foreground state and enable restricted mode + launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY); + setRestrictedMode(true); + assertForegroundNetworkAccess(false); + + // go to background state + finishActivity(); + assertBackgroundNetworkAccess(false); + + // disable restricted mode and assert network access in foreground and background states + setRestrictedMode(false); + launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY); + assertForegroundNetworkAccess(true); + + // go to background state + finishActivity(); + assertBackgroundNetworkAccess(true); + } +} 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..532fd86345 --- /dev/null +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java @@ -0,0 +1,1248 @@ +/* + * 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.Manifest.permission.NETWORK_SETTINGS; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; +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 static android.test.MoreAsserts.assertNotEqual; + +import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; + +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.TransportInfo; +import android.net.Uri; +import android.net.VpnManager; +import android.net.VpnService; +import android.net.VpnTransportInfo; +import android.net.wifi.WifiManager; +import android.os.Handler; +import android.os.Looper; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.os.SystemProperties; +import android.os.UserHandle; +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 com.android.modules.utils.build.SdkLevel; +import com.android.testutils.TestableNetworkCallback; + +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.List; +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(); + + // Test the behaviour of a variety of types of network callbacks. + final Network defaultNetwork = mCM.getActiveNetwork(); + final TestableNetworkCallback systemDefaultCallback = new TestableNetworkCallback(); + final TestableNetworkCallback otherUidCallback = new TestableNetworkCallback(); + final TestableNetworkCallback myUidCallback = new TestableNetworkCallback(); + if (SdkLevel.isAtLeastS()) { + final int otherUid = + UserHandle.of(5 /* userId */).getUid(Process.FIRST_APPLICATION_UID); + final Handler h = new Handler(Looper.getMainLooper()); + runWithShellPermissionIdentity(() -> { + mCM.registerSystemDefaultNetworkCallback(systemDefaultCallback, h); + mCM.registerDefaultNetworkCallbackForUid(otherUid, otherUidCallback, h); + mCM.registerDefaultNetworkCallbackForUid(Process.myUid(), myUidCallback, h); + }, NETWORK_SETTINGS); + for (TestableNetworkCallback callback : + List.of(systemDefaultCallback, otherUidCallback, myUidCallback)) { + callback.expectAvailableCallbacks(defaultNetwork, false /* suspended */, + true /* validated */, false /* blocked */, TIMEOUT_MS); + } + } + + 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(); + + final Network vpnNetwork = mCM.getActiveNetwork(); + myUidCallback.expectAvailableThenValidatedCallbacks(vpnNetwork, TIMEOUT_MS); + assertEquals(vpnNetwork, mCM.getActiveNetwork()); + assertNotEqual(defaultNetwork, vpnNetwork); + maybeExpectVpnTransportInfo(vpnNetwork); + + if (SdkLevel.isAtLeastS()) { + // Check that system default network callback has not seen any network changes, even + // though the app's default network changed. Also check that otherUidCallback saw no + // network changes, because otherUid is in a different user and not subject to the VPN. + // This needs to be done before testing private DNS because checkStrictModePrivateDns + // will set the private DNS server to a nonexistent name, which will cause validation to + // fail and could cause the default network to switch (e.g., from wifi to cellular). + systemDefaultCallback.assertNoCallback(); + otherUidCallback.assertNoCallback(); + mCM.unregisterNetworkCallback(systemDefaultCallback); + mCM.unregisterNetworkCallback(otherUidCallback); + mCM.unregisterNetworkCallback(myUidCallback); + } + + 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(); + + maybeExpectVpnTransportInfo(mCM.getActiveNetwork()); + + 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(); + + final Network network = mCM.getActiveNetwork(); + final NetworkCapabilities nc = mCM.getNetworkCapabilities(network); + assertFalse(nc.hasTransport(TRANSPORT_VPN)); + } + + 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); + assertEquals("Only an active VPN app should see connection information", + INVALID_UID, uid); + } catch (SecurityException acceptable) { + // R and below throw SecurityException if a non-active VPN calls this method. + // As long as we can't actually get socket information, either behaviour is fine. + 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()); + + maybeExpectVpnTransportInfo(mCM.getActiveNetwork()); + } + + 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()); + + maybeExpectVpnTransportInfo(mCM.getActiveNetwork()); + } + + 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()); + + maybeExpectVpnTransportInfo(mCM.getActiveNetwork()); + } + + 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()); + + maybeExpectVpnTransportInfo(mCM.getActiveNetwork()); + } + + 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()); + + maybeExpectVpnTransportInfo(mCM.getActiveNetwork()); + } + + 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 maybeExpectVpnTransportInfo(Network network) { + if (!SdkLevel.isAtLeastS()) return; + final NetworkCapabilities vpnNc = mCM.getNetworkCapabilities(network); + assertTrue(vpnNc.hasTransport(TRANSPORT_VPN)); + final TransportInfo ti = vpnNc.getTransportInfo(); + assertTrue(ti instanceof VpnTransportInfo); + assertEquals(VpnManager.TYPE_VPN_SERVICE, ((VpnTransportInfo) ti).getType()); + } + + 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..dd33eed366 --- /dev/null +++ b/tests/cts/hostside/app2/Android.bp @@ -0,0 +1,33 @@ +// +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test_helper_app { + name: "CtsHostsideNetworkTestsApp2", + defaults: ["cts_support_defaults"], + sdk_version: "test_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..4ac4bcb16c --- /dev/null +++ b/tests/cts/hostside/app2/AndroidManifest.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..62b508c244 --- /dev/null +++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/Common.java @@ -0,0 +1,149 @@ +/* + * 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.ActivityManager; +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.Process; +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_FINISH_JOB = + "com.android.cts.net.hostside.app2.action.FINISH_JOB"; + 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 final String KEY_SKIP_VALIDATION_CHECKS = TEST_PKG + ".skip_validation_checks"; + + static final int TYPE_COMPONENT_ACTIVTY = 0; + static final int TYPE_COMPONENT_FOREGROUND_SERVICE = 1; + static final int TYPE_COMPONENT_EXPEDITED_JOB = 2; + + 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); + } + } + + private static boolean validateComponentState(Context context, int componentType, + INetworkStateObserver observer) throws RemoteException { + final ActivityManager activityManager = context.getSystemService(ActivityManager.class); + switch (componentType) { + case TYPE_COMPONENT_ACTIVTY: { + final int procState = activityManager.getUidProcessState(Process.myUid()); + if (procState != ActivityManager.PROCESS_STATE_TOP) { + observer.onNetworkStateChecked( + INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE, + "Unexpected procstate: " + procState); + return false; + } + return true; + } + case TYPE_COMPONENT_FOREGROUND_SERVICE: { + final int procState = activityManager.getUidProcessState(Process.myUid()); + if (procState != ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) { + observer.onNetworkStateChecked( + INetworkStateObserver.RESULT_ERROR_UNEXPECTED_PROC_STATE, + "Unexpected procstate: " + procState); + return false; + } + return true; + } + case TYPE_COMPONENT_EXPEDITED_JOB: { + final int capabilities = activityManager.getUidProcessCapabilities(Process.myUid()); + if ((capabilities & ActivityManager.PROCESS_CAPABILITY_NETWORK) == 0) { + observer.onNetworkStateChecked( + INetworkStateObserver.RESULT_ERROR_UNEXPECTED_CAPABILITIES, + "Unexpected capabilities: " + capabilities); + return false; + } + return true; + } + default: { + observer.onNetworkStateChecked(INetworkStateObserver.RESULT_ERROR_OTHER, + "Unknown component type: " + componentType); + return false; + } + } + } + + static void notifyNetworkStateObserver(Context context, Intent intent, int componentType) { + if (intent == null) { + return; + } + final Bundle extras = intent.getExtras(); + notifyNetworkStateObserver(context, extras, componentType); + } + + static void notifyNetworkStateObserver(Context context, Bundle extras, int componentType) { + if (extras == null) { + return; + } + final INetworkStateObserver observer = INetworkStateObserver.Stub.asInterface( + extras.getBinder(KEY_NETWORK_STATE_OBSERVER)); + if (observer != null) { + try { + final boolean skipValidation = extras.getBoolean(KEY_SKIP_VALIDATION_CHECKS); + if (!skipValidation && !validateComponentState(context, componentType, observer)) { + return; + } + } catch (RemoteException e) { + Log.e(TAG, "Error occurred while informing the validation result: " + e); + } + AsyncTask.execute(() -> { + try { + observer.onNetworkStateChecked( + INetworkStateObserver.RESULT_SUCCESS_NETWORK_STATE_CHECKED, + 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..9fdb9c9d8c --- /dev/null +++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyActivity.java @@ -0,0 +1,76 @@ +/* + * 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 static com.android.cts.net.hostside.app2.Common.TYPE_COMPONENT_ACTIVTY; + +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(), TYPE_COMPONENT_ACTIVTY); + 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..c9ae16fe32 --- /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: + * + *

    + *
  • {@code NetinfoState}: enum value of {@link NetworkInfo.State}. + *
  • {@code NetinfoDetailedState}: enum value of {@link NetworkInfo.DetailedState}. + *
  • {@code RealConnectionCheck}: boolean value of a real connection check (i.e., an attempt + * to access an external website. + *
  • {@code RealConnectionCheckDetails}: if HTTP output core or exception string of the real + * connection attempt + *
  • {@code Netinfo}: string representation of the {@link NetworkInfo}. + *
+ * + * 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, + PendingIntent.FLAG_MUTABLE); + 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..b55761c58d --- /dev/null +++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyForegroundService.java @@ -0,0 +1,73 @@ +/* + * 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 static com.android.cts.net.hostside.app2.Common.TYPE_COMPONENT_FOREGROUND_SERVICE; + +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, TYPE_COMPONENT_FOREGROUND_SERVICE); + 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/MyJobService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyJobService.java new file mode 100644 index 0000000000..51c3157f75 --- /dev/null +++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyJobService.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.cts.net.hostside.app2; + +import static com.android.cts.net.hostside.app2.Common.ACTION_FINISH_JOB; +import static com.android.cts.net.hostside.app2.Common.TAG; +import static com.android.cts.net.hostside.app2.Common.TYPE_COMPONENT_EXPEDITED_JOB; + +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; + +public class MyJobService extends JobService { + + private BroadcastReceiver mFinishCommandReceiver = null; + + @Override + public void onCreate() { + super.onCreate(); + Log.v(TAG, "MyJobService.onCreate()"); + } + + @Override + public boolean onStartJob(JobParameters params) { + Log.v(TAG, "MyJobService.onStartJob()"); + Common.notifyNetworkStateObserver(this, params.getTransientExtras(), + TYPE_COMPONENT_EXPEDITED_JOB); + mFinishCommandReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Log.v(TAG, "Finishing MyJobService"); + try { + jobFinished(params, /*wantsReschedule=*/ false); + } finally { + if (mFinishCommandReceiver != null) { + unregisterReceiver(mFinishCommandReceiver); + mFinishCommandReceiver = null; + } + } + } + }; + registerReceiver(mFinishCommandReceiver, new IntentFilter(ACTION_FINISH_JOB)); + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + // If this job is stopped before it had a chance to send network status via + // INetworkStateObserver, the test will fail. It could happen either due to test timing out + // or this app moving to a lower proc_state and losing network access. + Log.v(TAG, "MyJobService.onStopJob()"); + if (mFinishCommandReceiver != null) { + unregisterReceiver(mFinishCommandReceiver); + mFinishCommandReceiver = null; + } + return false; + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.v(TAG, "MyJobService.onDestroy()"); + } +} 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..7dc4b9c3e9 --- /dev/null +++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java @@ -0,0 +1,195 @@ +/* + * 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.app.job.JobInfo; +import android.app.job.JobScheduler; +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(final NetworkRequest request, INetworkCallback cb) { + if (mNetworkCallback != null) { + Log.d(TAG, "unregister previous network callback: " + mNetworkCallback); + unregisterNetworkCallback(); + } + Log.d(TAG, "registering network callback for " + request); + + 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(request, 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; + } + } + + @Override + public void scheduleJob(JobInfo jobInfo) { + final JobScheduler jobScheduler = getApplicationContext() + .getSystemService(JobScheduler.class); + jobScheduler.schedule(jobInfo); + } + }; + + @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..60b54769ea --- /dev/null +++ b/tests/cts/hostside/certs/Android.bp @@ -0,0 +1,8 @@ +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +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..89c79d3677 --- /dev/null +++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java @@ -0,0 +1,188 @@ +/* + * 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 */, "-t")); + } + + 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()) { + final TestStatus testStatus = resultEntry.getValue().getStatus(); + if (!TestStatus.PASSED.equals(testStatus) + && !TestStatus.ASSUMPTION_FAILURE.equals(testStatus)) { + 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..d026fe00c7 --- /dev/null +++ b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java @@ -0,0 +1,401 @@ +/* + * 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 android.platform.test.annotations.FlakyTest; + +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"); + } + + @FlakyTest(bugId=170180675) + 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"); + } + + @FlakyTest(bugId=170180675) + 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"); + } + + /************************** + * Restricted mode tests. * + **************************/ + public void testNetworkAccess_restrictedMode() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".RestrictedModeTest", + "testNetworkAccess"); + } + + /************************ + * Expedited job tests. * + ************************/ + + public void testMeteredNetworkAccess_expeditedJob() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".ExpeditedJobMeteredTest"); + } + + public void testNonMeteredNetworkAccess_expeditedJob() throws Exception { + runDeviceTests(TEST_PKG, TEST_PKG + ".ExpeditedJobNonMeteredTest"); + } + + /******************* + * 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..25596a907c --- /dev/null +++ b/tests/cts/net/Android.bp @@ -0,0 +1,103 @@ +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_defaults { + name: "CtsNetTestCasesDefaults", + defaults: [ + "cts_defaults", + "framework-connectivity-test-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: [ + "bouncycastle-unbundled", + "FrameworksNetCommonTests", + "core-tests-support", + "cts-net-utils", + "ctstestrunner-axt", + "junit", + "junit-params", + "modules-utils-build", + "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"], + // TODO: CTS should not depend on the entirety of the networkstack code. + static_libs: [ + "NetworkStackApiCurrentLib", + ], + 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"], + // TODO: CTS should not depend on the entirety of the networkstack code. + static_libs: [ + "NetworkStackApiStableLib", + ], + jni_uses_sdk_apis: true, + min_sdk_version: "29", + target_sdk_version: "30", + test_suites: [ + "general-tests", + "mts-dnsresolver", + "mts-networking", + "mts-tethering", + "mts-wifi", + ], + 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..3b4710010e --- /dev/null +++ b/tests/cts/net/AndroidManifest.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml new file mode 100644 index 0000000000..474eefe7bf --- /dev/null +++ b/tests/cts/net/AndroidTestTemplate.xml @@ -0,0 +1,36 @@ + + + diff --git a/tests/cts/net/OWNERS b/tests/cts/net/OWNERS new file mode 100644 index 0000000000..432bd9b27f --- /dev/null +++ b/tests/cts/net/OWNERS @@ -0,0 +1,3 @@ +# Bug component: 31808 +# Inherits parent owners +per-file src/android/net/cts/NetworkWatchlistTest.java=alanstokes@google.com diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp new file mode 100644 index 0000000000..5b372944c6 --- /dev/null +++ b/tests/cts/net/api23Test/Android.bp @@ -0,0 +1,55 @@ +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +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..69ee0dd87b --- /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..b39690f14f --- /dev/null +++ b/tests/cts/net/appForApi23/Android.bp @@ -0,0 +1,36 @@ +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +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..158b9c41f1 --- /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..13f38d77cb --- /dev/null +++ b/tests/cts/net/jni/Android.bp @@ -0,0 +1,55 @@ +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +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..5e9af8e55a --- /dev/null +++ b/tests/cts/net/native/dns/Android.bp @@ -0,0 +1,46 @@ +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +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-dnsresolver", + "mts-networking", + ], +} 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..68bb14da87 --- /dev/null +++ b/tests/cts/net/native/qtaguid/Android.bp @@ -0,0 +1,57 @@ +// 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. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +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..a889c41f53 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt @@ -0,0 +1,213 @@ +/* + * 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.NET_CAPABILITY_INTERNET +import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED +import android.net.NetworkCapabilities.TRANSPORT_CELLULAR +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.RecorderCallback +import com.android.testutils.TestHttpServer +import com.android.testutils.TestHttpServer.Request +import com.android.testutils.TestableNetworkCallback +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() + val cellNetwork = utils.connectToCell() + + // Verify cell network is validated + val cellReq = NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .build() + val cellCb = TestableNetworkCallback(timeoutMs = TEST_TIMEOUT_MS) + cm.registerNetworkCallback(cellReq, cellCb) + val cb = cellCb.eventuallyExpectOrNull { + it.network == cellNetwork && it.caps.hasCapability(NET_CAPABILITY_VALIDATED) + } + assertNotNull(cb, "Mobile network $cellNetwork has no access to the internet. " + + "Check the mobile data connection.") + + // 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 + assertTrue(WIFI_CONNECT_TIMEOUT_MS < TimeUnit.MINUTES.toMillis(10)) + setUrlExpirationDeviceConfig(System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS) + + // 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..ccbdbd35b5 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java @@ -0,0 +1,636 @@ +/* + * 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(); + + // Lock for accessing Shell Permissions. Use of this lock around adoptShellPermissionIdentity, + // runWithShellPermissionIdentity, and callWithShellPermissionIdentity ensures Shell Permission + // is not interrupted by another operation (which would drop all previously adopted + // permissions). + private Object mShellPermissionsIdentityLock = new Object(); + + 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()}); + + synchronized (mShellPermissionsIdentityLock) { + 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. + synchronized (mShellPermissionsIdentityLock) { + 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 { + // CountDownLatch used to wait for this BroadcastReceiver to be notified of a CarrierConfig + // change. This latch will be counted down if a broadcast indicates this package has carrier + // configs, or if an Exception occurs in #onReceive. + private final CountDownLatch mLatch = new CountDownLatch(1); + private final int mSubId; + + // #onReceive may encounter Exceptions while running on the Process' main Thread and + // #waitForCarrierConfigChanged checks the cached Exception from the test Thread. These + // Exceptions must be cached and thrown later, as throwing on the Process' main Thread will + // crash the process and cause other tests to fail. + private Exception mOnReceiveException; + + CarrierConfigReceiver(int subId) { + mSubId = subId; + } + + @Override + public void onReceive(Context context, Intent intent) { + if (!CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(intent.getAction())) { + // Received an incorrect broadcast - ignore + return; + } + + final int subId = + intent.getIntExtra( + CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + if (mSubId != subId) { + // Received a broadcast for the wrong subId - ignore + return; + } + + final PersistableBundle carrierConfigs; + try { + synchronized (mShellPermissionsIdentityLock) { + carrierConfigs = callWithShellPermissionIdentity( + () -> mCarrierConfigManager.getConfigForSubId(subId), + android.Manifest.permission.READ_PHONE_STATE); + } + } catch (Exception exception) { + // callWithShellPermissionIdentity() threw an Exception - cache it and allow + // waitForCarrierConfigChanged() to throw it + mOnReceiveException = exception; + mLatch.countDown(); + return; + } + + if (!CarrierConfigManager.isConfigForIdentifiedCarrier(carrierConfigs)) { + // Configs are not for an identified carrier (meaning they are defaults) - ignore + return; + } + + final String[] certs = carrierConfigs.getStringArray( + CarrierConfigManager.KEY_CARRIER_CERTIFICATE_STRING_ARRAY); + try { + if (ArrayUtils.contains(certs, getCertHashForThisPackage())) { + // Received an update for this package's cert hash - countdown and exit + mLatch.countDown(); + } + // Broadcast is for the right subId, but does not show this package as Carrier + // Privileged. Keep waiting for a broadcast that indicates Carrier Privileges. + } catch (Exception exception) { + // getCertHashForThisPackage() threw an Exception - cache it and allow + // waitForCarrierConfigChanged() to throw it + mOnReceiveException = exception; + mLatch.countDown(); + } + } + + /** + * Waits for the CarrierConfig changed broadcast to reach this CarrierConfigReceiver. + * + *

Must be called from the Test Thread. + * + * @throws Exception if an Exception occurred during any #onReceive invocation + */ + boolean waitForCarrierConfigChanged() throws Exception { + final boolean result = mLatch.await(CARRIER_CONFIG_CHANGED_BROADCAST_TIMEOUT, + TimeUnit.MILLISECONDS); + + if (mOnReceiveException != null) { + throw mOnReceiveException; + } + + return result; + } + } +} 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..90efba00ff --- /dev/null +++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java @@ -0,0 +1,1998 @@ +/* + * 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_INTERNAL; +import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS; +import static android.Manifest.permission.NETWORK_SETTINGS; +import static android.content.pm.PackageManager.FEATURE_BLUETOOTH; +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_WATCH; +import static android.content.pm.PackageManager.FEATURE_WIFI; +import static android.content.pm.PackageManager.FEATURE_WIFI_DIRECT; +import static android.content.pm.PackageManager.GET_PERMISSIONS; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.net.ConnectivityManager.TYPE_BLUETOOTH; +import static android.net.ConnectivityManager.TYPE_ETHERNET; +import static android.net.ConnectivityManager.TYPE_MOBILE_CBS; +import static android.net.ConnectivityManager.TYPE_MOBILE_DUN; +import static android.net.ConnectivityManager.TYPE_MOBILE_EMERGENCY; +import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA; +import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI; +import static android.net.ConnectivityManager.TYPE_MOBILE_IA; +import static android.net.ConnectivityManager.TYPE_MOBILE_IMS; +import static android.net.ConnectivityManager.TYPE_MOBILE_MMS; +import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL; +import static android.net.ConnectivityManager.TYPE_PROXY; +import static android.net.ConnectivityManager.TYPE_VPN; +import static android.net.ConnectivityManager.TYPE_WIFI_P2P; +import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND; +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_TEST; +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.networkstack.apishim.ConstantsShim.BLOCKED_REASON_LOCKDOWN_VPN; +import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_NONE; +import static com.android.testutils.MiscAsserts.assertThrows; +import static com.android.testutils.TestPermissionUtil.runAsShell; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +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.InetAddresses; +import android.net.IpSecManager; +import android.net.IpSecManager.UdpEncapsulationSocket; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkInfo.DetailedState; +import android.net.NetworkInfo.State; +import android.net.NetworkRequest; +import android.net.NetworkSpecifier; +import android.net.NetworkStateSnapshot; +import android.net.NetworkUtils; +import android.net.ProxyInfo; +import android.net.SocketKeepalive; +import android.net.TelephonyNetworkSpecifier; +import android.net.TestNetworkInterface; +import android.net.TestNetworkManager; +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.Handler; +import android.os.Looper; +import android.os.MessageQueue; +import android.os.Process; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.os.VintfRuntimeInfo; +import android.platform.test.annotations.AppModeFull; +import android.provider.Settings; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Log; +import android.util.Pair; +import android.util.Range; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.ArrayUtils; +import com.android.modules.utils.build.SdkLevel; +import com.android.networkstack.apishim.ConnectivityManagerShimImpl; +import com.android.networkstack.apishim.ConstantsShim; +import com.android.networkstack.apishim.NetworkInformationShimImpl; +import com.android.networkstack.apishim.common.ConnectivityManagerShim; +import com.android.testutils.CompatUtil; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRuleKt; +import com.android.testutils.RecorderCallback.CallbackEntry; +import com.android.testutils.SkipPresubmit; +import com.android.testutils.TestableNetworkCallback; + +import junit.framework.AssertionFailedError; + +import libcore.io.Streams; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +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.List; +import java.util.Objects; +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 { + @Rule + public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); + + 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; + + private static final int NETWORK_CALLBACK_TIMEOUT_MS = 30_000; + private static final int NO_CALLBACK_TIMEOUT_MS = 100; + 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 static final LinkAddress TEST_LINKADDR = new LinkAddress( + InetAddresses.parseNumericAddress("2001:db8::8"), 64); + + private Context mContext; + private Instrumentation mInstrumentation; + private ConnectivityManager mCm; + private ConnectivityManagerShim mCmShim; + private WifiManager mWifiManager; + private PackageManager mPackageManager; + private final ArraySet mNetworkTypes = new ArraySet<>(); + private UiAutomation mUiAutomation; + private CtsNetUtils mCtsNetUtils; + + // Used for cleanup purposes. + private final List> mVpnRequiredUidRanges = new ArrayList<>(); + + @Before + public void setUp() throws Exception { + mInstrumentation = InstrumentationRegistry.getInstrumentation(); + mContext = mInstrumentation.getContext(); + mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + mCmShim = ConnectivityManagerShimImpl.newInstance(mContext); + mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + mPackageManager = mContext.getPackageManager(); + mCtsNetUtils = new CtsNetUtils(mContext); + + if (DevSdkIgnoreRuleKt.isDevSdkInRange(null /* minExclusive */, + Build.VERSION_CODES.R /* maxInclusive */)) { + addLegacySupportedNetworkTypes(); + } else { + addSupportedNetworkTypes(); + } + + mUiAutomation = mInstrumentation.getUiAutomation(); + + assertNotNull("CTS requires a working Internet connection", mCm.getActiveNetwork()); + } + + private void addLegacySupportedNetworkTypes() { + // Network type support as expected for android R- + // Get com.android.internal.R.array.networkAttributes + int resId = mContext.getResources().getIdentifier("networkAttributes", "array", "android"); + String[] naStrings = mContext.getResources().getStringArray(resId); + boolean wifiOnly = SystemProperties.getBoolean("ro.radio.noril", false); + for (String naString : naStrings) { + try { + final String[] splitConfig = naString.split(","); + // Format was name,type,radio,priority,restoreTime,dependencyMet + final int type = Integer.parseInt(splitConfig[1]); + if (wifiOnly && ConnectivityManager.isNetworkTypeMobile(type)) { + continue; + } + mNetworkTypes.add(type); + } catch (Exception e) {} + } + } + + private void addSupportedNetworkTypes() { + final PackageManager pm = mContext.getPackageManager(); + if (pm.hasSystemFeature(FEATURE_WIFI)) { + mNetworkTypes.add(TYPE_WIFI); + } + if (pm.hasSystemFeature(FEATURE_WIFI_DIRECT)) { + mNetworkTypes.add(TYPE_WIFI_P2P); + } + if (mContext.getSystemService(TelephonyManager.class).isDataCapable()) { + mNetworkTypes.add(TYPE_MOBILE); + mNetworkTypes.add(TYPE_MOBILE_MMS); + mNetworkTypes.add(TYPE_MOBILE_SUPL); + mNetworkTypes.add(TYPE_MOBILE_DUN); + mNetworkTypes.add(TYPE_MOBILE_HIPRI); + mNetworkTypes.add(TYPE_MOBILE_FOTA); + mNetworkTypes.add(TYPE_MOBILE_IMS); + mNetworkTypes.add(TYPE_MOBILE_CBS); + mNetworkTypes.add(TYPE_MOBILE_IA); + mNetworkTypes.add(TYPE_MOBILE_EMERGENCY); + } + if (pm.hasSystemFeature(FEATURE_BLUETOOTH)) { + mNetworkTypes.add(TYPE_BLUETOOTH); + } + if (pm.hasSystemFeature(FEATURE_WATCH)) { + mNetworkTypes.add(TYPE_PROXY); + } + if (mContext.getSystemService(Context.ETHERNET_SERVICE) != null) { + mNetworkTypes.add(TYPE_ETHERNET); + } + mNetworkTypes.add(TYPE_VPN); + } + + @After + public void tearDown() throws Exception { + // Release any NetworkRequests filed to connect mobile data. + if (mCtsNetUtils.cellConnectAttempted()) { + mCtsNetUtils.disconnectFromCell(); + } + + if (TestUtils.shouldTestSApis()) { + runWithShellPermissionIdentity( + () -> mCmShim.setRequireVpnForUids(false, mVpnRequiredUidRanges), + NETWORK_SETTINGS); + } + + // 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); + } + } + + @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); + } + } + + private String getSubscriberIdForCellNetwork(Network cellNetwork) { + final NetworkCapabilities cellCaps = mCm.getNetworkCapabilities(cellNetwork); + final NetworkSpecifier specifier = cellCaps.getNetworkSpecifier(); + assertTrue(specifier instanceof TelephonyNetworkSpecifier); + // Get subscription from Telephony network specifier. + final int subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId(); + assertNotEquals(SubscriptionManager.INVALID_SUBSCRIPTION_ID, subId); + + // Get subscriber Id from telephony manager. + final TelephonyManager tm = mContext.getSystemService(TelephonyManager.class); + return runWithShellPermissionIdentity(() -> tm.getSubscriberId(subId), + android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE); + } + + @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) + @Test + public void testGetAllNetworkStateSnapshots() + throws InterruptedException { + // Make sure cell is active to retrieve IMSI for verification in later step. + final Network cellNetwork = mCtsNetUtils.connectToCell(); + final String subscriberId = getSubscriberIdForCellNetwork(cellNetwork); + assertFalse(TextUtils.isEmpty(subscriberId)); + + // Verify the API cannot be called without proper permission. + assertThrows(SecurityException.class, () -> mCm.getAllNetworkStateSnapshots()); + + // Get all networks, verify the result of getAllNetworkStateSnapshots matches the result + // got from other APIs. + final Network[] networks = mCm.getAllNetworks(); + assertGreaterOrEqual(networks.length, 1); + final List snapshots = runWithShellPermissionIdentity( + () -> mCm.getAllNetworkStateSnapshots(), NETWORK_SETTINGS); + assertEquals(networks.length, snapshots.size()); + for (final Network network : networks) { + // Can't use a lambda because it will cause the test to crash on R with + // NoClassDefFoundError. + NetworkStateSnapshot snapshot = null; + for (NetworkStateSnapshot item : snapshots) { + if (item.getNetwork().equals(network)) { + snapshot = item; + break; + } + } + assertNotNull(snapshot); + final NetworkCapabilities caps = + Objects.requireNonNull(mCm.getNetworkCapabilities(network)); + // Redact specifier of the capabilities of the snapshot before comparing since + // the result returned from getNetworkCapabilities always get redacted. + final NetworkSpecifier redactedSnapshotCapSpecifier = + snapshot.getNetworkCapabilities().getNetworkSpecifier().redact(); + assertEquals("", caps.describeImmutableDifferences( + snapshot.getNetworkCapabilities() + .setNetworkSpecifier(redactedSnapshotCapSpecifier))); + assertEquals(mCm.getLinkProperties(network), snapshot.getLinkProperties()); + assertEquals(mCm.getNetworkInfo(network).getType(), snapshot.getLegacyType()); + + if (network.equals(cellNetwork)) { + assertEquals(subscriberId, snapshot.getSubscriberId()); + } + } + } + + /** + * 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 mNetworkTypes.contains(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); + + final TestNetworkCallback systemDefaultCallback = new TestNetworkCallback(); + final TestNetworkCallback perUidCallback = new TestNetworkCallback(); + final Handler h = new Handler(Looper.getMainLooper()); + if (TestUtils.shouldTestSApis()) { + runWithShellPermissionIdentity(() -> { + mCmShim.registerSystemDefaultNetworkCallback(systemDefaultCallback, h); + mCmShim.registerDefaultNetworkCallbackForUid(Process.myUid(), perUidCallback, h); + }, NETWORK_SETTINGS); + } + + Network wifiNetwork = null; + + try { + mCtsNetUtils.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 onAvailable for TRANSPORT_WIFI request", + wifiNetwork); + + final Network defaultNetwork = defaultTrackingCallback.waitForAvailable(); + assertNotNull("Did not receive onAvailable on default network callback", + defaultNetwork); + + if (TestUtils.shouldTestSApis()) { + assertNotNull("Did not receive onAvailable on system default network callback", + systemDefaultCallback.waitForAvailable()); + final Network perUidNetwork = perUidCallback.waitForAvailable(); + assertNotNull("Did not receive onAvailable on per-UID default network callback", + perUidNetwork); + assertEquals(defaultNetwork, perUidNetwork); + } + + } catch (InterruptedException e) { + fail("Broadcast receiver or NetworkCallback wait was interrupted."); + } finally { + mCm.unregisterNetworkCallback(callback); + mCm.unregisterNetworkCallback(defaultTrackingCallback); + if (TestUtils.shouldTestSApis()) { + mCm.unregisterNetworkCallback(systemDefaultCallback); + mCm.unregisterNetworkCallback(perUidCallback); + } + } + } + + /** + * 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. + final IntentFilter filter = new IntentFilter(); + filter.addAction(NETWORK_CALLBACK_ACTION); + + final ConnectivityActionReceiver receiver = new ConnectivityActionReceiver( + mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.CONNECTED); + mContext.registerReceiver(receiver, filter); + + // Create a broadcast PendingIntent for NETWORK_CALLBACK_ACTION. + final Intent intent = new Intent(NETWORK_CALLBACK_ACTION) + .setPackage(mContext.getPackageName()); + // While ConnectivityService would put extra info such as network or request id before + // broadcasting the inner intent. The MUTABLE flag needs to be added accordingly. + // TODO: replace with PendingIntent.FLAG_MUTABLE when this code compiles against S+ or + // shims. + final int pendingIntentFlagMutable = 1 << 25; + final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0 /*requestCode*/, + intent, PendingIntent.FLAG_CANCEL_CURRENT | pendingIntentFlagMutable); + + // We will register for a WIFI network being available or lost. + mCm.registerNetworkCallback(makeWifiNetworkRequest(), pendingIntent); + + try { + mCtsNetUtils.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; + } + + /** + * Checks that enabling/disabling wifi causes CONNECTIVITY_ACTION broadcasts. + */ + @AppModeFull(reason = "Cannot get WifiManager in instant app mode") + @Test + public void testToggleWifiConnectivityAction() { + // toggleWifi calls connectToWifi and disconnectFromWifi, which both wait for + // CONNECTIVITY_ACTION broadcasts. + mCtsNetUtils.toggleWifi(); + } + + /** 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); + // Changing meteredness on wifi involves reconnecting, which can take several seconds + // (involves re-associating, DHCP...). + if (!latch.await(NETWORK_CALLBACK_TIMEOUT_MS, 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(); + mCtsNetUtils.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 = mCtsNetUtils.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 = mCtsNetUtils.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 = mCtsNetUtils.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 = mCtsNetUtils.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 = SdkLevel.isAtLeastS() + ? Build.VERSION.DEVICE_INITIAL_SDK_INT + // FIRST_SDK_INT was a @TestApi field renamed to DEVICE_INITIAL_SDK_INT in S + : Build.VERSION.class.getField("FIRST_SDK_INT").getInt(null); + 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 = mCtsNetUtils.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 = mCtsNetUtils.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, NETWORK_CALLBACK_TIMEOUT_MS, + c -> c instanceof CallbackEntry.Available); + } + + private void waitForLost(@NonNull final TestableNetworkCallback cb) { + cb.eventuallyExpect(CallbackEntry.LOST, NETWORK_CALLBACK_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 permission = Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q + ? CONNECTIVITY_INTERNAL + : NETWORK_SETTINGS; + final String url = runAsShell(permission, 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); + } + } + + /** + * Verifies that apps are forbidden from getting ssid information from + * {@Code NetworkCapabilities} if they do not hold NETWORK_SETTINGS permission. + * See b/161370134. + */ + @AppModeFull(reason = "Cannot get WifiManager in instant app mode") + @Test + public void testSsidInNetworkCapabilities() throws Exception { + assumeTrue("testSsidInNetworkCapabilities cannot execute unless device supports WiFi", + mPackageManager.hasSystemFeature(FEATURE_WIFI)); + + final Network network = mCtsNetUtils.ensureWifiConnected(); + final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID()); + assertNotNull("Ssid getting from WifiManager is null", ssid); + // This package should have no NETWORK_SETTINGS permission. Verify that no ssid is contained + // in the NetworkCapabilities. + verifySsidFromQueriedNetworkCapabilities(network, ssid, false /* hasSsid */); + verifySsidFromCallbackNetworkCapabilities(ssid, false /* hasSsid */); + // Adopt shell permission to allow to get ssid information. + runWithShellPermissionIdentity(() -> { + verifySsidFromQueriedNetworkCapabilities(network, ssid, true /* hasSsid */); + verifySsidFromCallbackNetworkCapabilities(ssid, true /* hasSsid */); + }); + } + + private void verifySsidFromQueriedNetworkCapabilities(@NonNull Network network, + @NonNull String ssid, boolean hasSsid) throws Exception { + // Verify if ssid is contained in NetworkCapabilities queried from ConnectivityManager. + final NetworkCapabilities nc = mCm.getNetworkCapabilities(network); + assertNotNull("NetworkCapabilities of the network is null", nc); + assertEquals(hasSsid, Pattern.compile(ssid).matcher(nc.toString()).find()); + } + + private void verifySsidFromCallbackNetworkCapabilities(@NonNull String ssid, boolean hasSsid) + throws Exception { + final CompletableFuture foundNc = new CompletableFuture(); + final NetworkCallback callback = new NetworkCallback() { + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) { + foundNc.complete(nc); + } + }; + try { + mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback); + // Registering a callback here guarantees onCapabilitiesChanged is called immediately + // because WiFi network should be connected. + final NetworkCapabilities nc = + foundNc.get(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS); + // Verify if ssid is contained in the NetworkCapabilities received from callback. + assertNotNull("NetworkCapabilities of the network is null", nc); + assertEquals(hasSsid, Pattern.compile(ssid).matcher(nc.toString()).find()); + } finally { + mCm.unregisterNetworkCallback(callback); + } + } + + /** + * Verify background request can only be requested when acquiring + * {@link android.Manifest.permission.NETWORK_SETTINGS}. + */ + @AppModeFull(reason = "Instant apps cannot create test networks") + @Test + public void testRequestBackgroundNetwork() { + // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31 + // shims, and @IgnoreUpTo does not check that. + assumeTrue(TestUtils.shouldTestSApis()); + + // Create a tun interface. Use the returned interface name as the specifier to create + // a test network request. + final TestNetworkManager tnm = runWithShellPermissionIdentity(() -> + mContext.getSystemService(TestNetworkManager.class), + android.Manifest.permission.MANAGE_TEST_NETWORKS); + final TestNetworkInterface testNetworkInterface = runWithShellPermissionIdentity(() -> + tnm.createTunInterface(new LinkAddress[]{TEST_LINKADDR}), + android.Manifest.permission.MANAGE_TEST_NETWORKS, + android.Manifest.permission.NETWORK_SETTINGS); + assertNotNull(testNetworkInterface); + + final NetworkRequest testRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_TEST) + // Test networks do not have NOT_VPN or TRUSTED capabilities by default + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) + .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier( + testNetworkInterface.getInterfaceName())) + .build(); + + // Verify background network cannot be requested without NETWORK_SETTINGS permission. + final TestableNetworkCallback callback = new TestableNetworkCallback(); + final Handler handler = new Handler(Looper.getMainLooper()); + assertThrows(SecurityException.class, + () -> mCmShim.requestBackgroundNetwork(testRequest, callback, handler)); + + Network testNetwork = null; + try { + // Request background test network via Shell identity which has NETWORK_SETTINGS + // permission granted. + runWithShellPermissionIdentity( + () -> mCmShim.requestBackgroundNetwork(testRequest, callback, handler), + new String[] { android.Manifest.permission.NETWORK_SETTINGS }); + + // Register the test network agent which has no foreground request associated to it. + // And verify it can satisfy the background network request just fired. + final Binder binder = new Binder(); + runWithShellPermissionIdentity(() -> + tnm.setupTestNetwork(testNetworkInterface.getInterfaceName(), binder), + new String[] { android.Manifest.permission.MANAGE_TEST_NETWORKS, + android.Manifest.permission.NETWORK_SETTINGS }); + waitForAvailable(callback); + testNetwork = callback.getLastAvailableNetwork(); + assertNotNull(testNetwork); + + // The test network that has just connected is a foreground network, + // non-listen requests will get available callback before it can be put into + // background if no foreground request can be satisfied. Thus, wait for a short + // period is needed to let foreground capability go away. + callback.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED, + NETWORK_CALLBACK_TIMEOUT_MS, + c -> c instanceof CallbackEntry.CapabilitiesChanged + && !((CallbackEntry.CapabilitiesChanged) c).getCaps() + .hasCapability(NET_CAPABILITY_FOREGROUND)); + final NetworkCapabilities nc = mCm.getNetworkCapabilities(testNetwork); + assertFalse("expected background network, but got " + nc, + nc.hasCapability(NET_CAPABILITY_FOREGROUND)); + } finally { + final Network n = testNetwork; + runWithShellPermissionIdentity(() -> { + if (null != n) { + tnm.teardownTestNetwork(n); + callback.eventuallyExpect(CallbackEntry.LOST, + NETWORK_CALLBACK_TIMEOUT_MS, + lost -> n.equals(lost.getNetwork())); + } + testNetworkInterface.getFileDescriptor().close(); + }, new String[] { android.Manifest.permission.MANAGE_TEST_NETWORKS }); + mCm.unregisterNetworkCallback(callback); + } + } + + private class DetailedBlockedStatusCallback extends TestableNetworkCallback { + public void expectAvailableCallbacks(Network network) { + super.expectAvailableCallbacks(network, false /* suspended */, true /* validated */, + BLOCKED_REASON_NONE, NETWORK_CALLBACK_TIMEOUT_MS); + } + public void expectBlockedStatusCallback(Network network, int blockedStatus) { + super.expectBlockedStatusCallback(blockedStatus, network, NETWORK_CALLBACK_TIMEOUT_MS); + } + public void onBlockedStatusChanged(Network network, int blockedReasons) { + getHistory().add(new CallbackEntry.BlockedStatusInt(network, blockedReasons)); + } + } + + private void setRequireVpnForUids(boolean requireVpn, Collection> ranges) + throws Exception { + mCmShim.setRequireVpnForUids(requireVpn, ranges); + for (Range range : ranges) { + if (requireVpn) { + mVpnRequiredUidRanges.add(range); + } else { + assertTrue(mVpnRequiredUidRanges.remove(range)); + } + } + } + + private void doTestBlockedStatusCallback() throws Exception { + final DetailedBlockedStatusCallback myUidCallback = new DetailedBlockedStatusCallback(); + final DetailedBlockedStatusCallback otherUidCallback = new DetailedBlockedStatusCallback(); + + final int myUid = Process.myUid(); + final int otherUid = UserHandle.getUid(5, Process.FIRST_APPLICATION_UID); + final Handler handler = new Handler(Looper.getMainLooper()); + mCm.registerDefaultNetworkCallback(myUidCallback, handler); + mCmShim.registerDefaultNetworkCallbackForUid(otherUid, otherUidCallback, handler); + + final Network defaultNetwork = mCm.getActiveNetwork(); + final List allCallbacks = + List.of(myUidCallback, otherUidCallback); + for (DetailedBlockedStatusCallback callback : allCallbacks) { + callback.expectAvailableCallbacks(defaultNetwork); + } + + final Range myUidRange = new Range<>(myUid, myUid); + final Range otherUidRange = new Range<>(otherUid, otherUid); + + setRequireVpnForUids(true, List.of(myUidRange)); + myUidCallback.expectBlockedStatusCallback(defaultNetwork, BLOCKED_REASON_LOCKDOWN_VPN); + otherUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS); + + setRequireVpnForUids(true, List.of(myUidRange, otherUidRange)); + myUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS); + otherUidCallback.expectBlockedStatusCallback(defaultNetwork, BLOCKED_REASON_LOCKDOWN_VPN); + + // setRequireVpnForUids does no deduplication or refcounting. Removing myUidRange does not + // unblock myUid because it was added to the blocked ranges twice. + setRequireVpnForUids(false, List.of(myUidRange)); + myUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS); + otherUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS); + + setRequireVpnForUids(false, List.of(myUidRange, otherUidRange)); + myUidCallback.expectBlockedStatusCallback(defaultNetwork, BLOCKED_REASON_NONE); + otherUidCallback.expectBlockedStatusCallback(defaultNetwork, BLOCKED_REASON_NONE); + + myUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS); + otherUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS); + } + + @Test + public void testBlockedStatusCallback() { + // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31 + // shims, and @IgnoreUpTo does not check that. + assumeTrue(TestUtils.shouldTestSApis()); + runWithShellPermissionIdentity(() -> doTestBlockedStatusCallback(), NETWORK_SETTINGS); + } + + private void doTestLegacyLockdownEnabled() throws Exception { + NetworkInfo info = mCm.getActiveNetworkInfo(); + assertNotNull(info); + assertEquals(DetailedState.CONNECTED, info.getDetailedState()); + + try { + mCmShim.setLegacyLockdownVpnEnabled(true); + + // setLegacyLockdownVpnEnabled is asynchronous and only takes effect when the + // ConnectivityService handler thread processes it. Ensure it has taken effect by doing + // something that blocks until the handler thread is idle. + final TestableNetworkCallback callback = new TestableNetworkCallback(); + mCm.registerDefaultNetworkCallback(callback); + waitForAvailable(callback); + mCm.unregisterNetworkCallback(callback); + + // Test one of the effects of setLegacyLockdownVpnEnabled: the fact that any NetworkInfo + // in state CONNECTED is degraded to CONNECTING if the legacy VPN is not connected. + info = mCm.getActiveNetworkInfo(); + assertNotNull(info); + assertEquals(DetailedState.CONNECTING, info.getDetailedState()); + } finally { + mCmShim.setLegacyLockdownVpnEnabled(false); + } + } + + @Test + public void testLegacyLockdownEnabled() { + // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31 + // shims, and @IgnoreUpTo does not check that. + assumeTrue(TestUtils.shouldTestSApis()); + runWithShellPermissionIdentity(() -> doTestLegacyLockdownEnabled(), NETWORK_SETTINGS); + } + + @Test + public void testGetCapabilityCarrierName() { + assumeTrue(TestUtils.shouldTestSApis()); + assertEquals("ENTERPRISE", NetworkInformationShimImpl.newInstance() + .getCapabilityCarrierName(ConstantsShim.NET_CAPABILITY_ENTERPRISE)); + assertNull(NetworkInformationShimImpl.newInstance() + .getCapabilityCarrierName(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)); + } + + @Test + public void testSetGlobalProxy() { + assumeTrue(TestUtils.shouldTestSApis()); + // Behavior is verified in gts. Verify exception thrown w/o permission. + assertThrows(SecurityException.class, () -> mCm.setGlobalProxy( + ProxyInfo.buildDirectProxy("example.com" /* host */, 8080 /* port */))); + } +} 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..4d95fbe9a9 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/DnsResolverTest.java @@ -0,0 +1,763 @@ +/* + * 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.content.pm.PackageManager; +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 PackageManager mPackageManager; + 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(); + mPackageManager = mContext.getPackageManager(); + } + + @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() { + if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)) { + mCtsNetUtils.ensureWifiConnected(); + } + 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(); + mLatch.countDown(); + } + } + + 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..c6d8d65bb4 --- /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.testutils.DevSdkIgnoreRule.IgnoreUpTo; +import com.android.testutils.DevSdkIgnoreRunner; + +import org.bouncycastle.x509.X509V1CertificateGenerator; +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..d08f6e99c4 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/IpSecManagerTest.java @@ -0,0 +1,1200 @@ +/* + * 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 com.android.testutils.SkipPresubmit; + +import org.junit.Test; +import org.junit.runner.RunWith; + +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; + +@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 + @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec") + 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 + @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec") + 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 + @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec") + 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 + @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec") + 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 + @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec") + 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 + @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec") + 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 + @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec") + 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 + @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec") + 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 + @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec") + 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 + @SkipPresubmit(reason = "b/186608065 - kernel 5.10 regression in TrafficStats with ipsec") + 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..1c9aba1e11 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt @@ -0,0 +1,867 @@ +/* + * 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.NETWORK_SETTINGS +import android.app.Instrumentation +import android.content.Context +import android.net.ConnectivityManager +import android.net.INetworkAgent +import android.net.INetworkAgentRegistry +import android.net.InetAddresses +import android.net.IpPrefix +import android.net.KeepalivePacketData +import android.net.LinkAddress +import android.net.LinkProperties +import android.net.NattKeepalivePacketData +import android.net.Network +import android.net.NetworkAgent +import android.net.NetworkAgent.INVALID_NETWORK +import android.net.NetworkAgent.VALID_NETWORK +import android.net.NetworkAgentConfig +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED +import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN +import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED +import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED +import android.net.NetworkCapabilities.TRANSPORT_TEST +import android.net.NetworkCapabilities.TRANSPORT_VPN +import android.net.NetworkInfo +import android.net.NetworkProvider +import android.net.NetworkRequest +import android.net.NetworkScore +import android.net.RouteInfo +import android.net.SocketKeepalive +import android.net.Uri +import android.net.VpnManager +import android.net.VpnTransportInfo +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.OnNetworkCreated +import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnNetworkDestroyed +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.HandlerThread +import android.os.Looper +import android.os.Message +import android.os.SystemClock +import android.util.DebugUtils.valueToString +import androidx.test.InstrumentationRegistry +import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity +import com.android.compatibility.common.util.ThrowingSupplier +import com.android.modules.utils.build.SdkLevel +import com.android.net.module.util.ArrayTrackRecord +import com.android.testutils.CompatUtil +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.RecorderCallback.CallbackEntry.Available +import com.android.testutils.RecorderCallback.CallbackEntry.Losing +import com.android.testutils.RecorderCallback.CallbackEntry.Lost +import com.android.testutils.TestableNetworkCallback +import org.junit.After +import org.junit.Assert.assertArrayEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.argThat +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.timeout +import org.mockito.Mockito.verify +import java.time.Duration +import java.util.Arrays +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 +import kotlin.test.fail + +// 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 WORSE_NETWORK_SCORE = 65 +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(DevSdkIgnoreRunner::class) +// NetworkAgent is not updatable in R-, so this test does not need to be compatible with older +// versions. NetworkAgent was also based on AsyncChannel before S so cannot be tested the same way. +@IgnoreUpTo(Build.VERSION_CODES.R) +class NetworkAgentTest { + private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1") + private val REMOTE_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.2") + + private val mCM = realContext.getSystemService(ConnectivityManager::class.java) + private val mHandlerThread = HandlerThread("${javaClass.simpleName} handler thread") + private val mFakeConnectivityService = FakeConnectivityService() + + 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 { + val mockRegistry = mock(INetworkAgentRegistry::class.java) + private var agentField: INetworkAgent? = null + private val registry = object : INetworkAgentRegistry.Stub(), + INetworkAgentRegistry by mockRegistry { + // asBinder has implementations in both INetworkAgentRegistry.Stub and mockRegistry, so + // it needs to be disambiguated. Just fail the test as it should be unused here. + // asBinder is used when sending the registry in binder transactions, so not in this + // test (the test just uses in-process direct calls). If it were used across processes, + // using the Stub super.asBinder() implementation would allow sending the registry in + // binder transactions, while recording incoming calls on the other mockito-generated + // methods. + override fun asBinder() = fail("asBinder should be unused in this test") + } + + val agent: INetworkAgent + get() = agentField ?: fail("No INetworkAgent") + + fun connect(agent: INetworkAgent) { + this.agentField = agent + agent.onRegistered(registry) + } + + fun disconnect() = agent.onDisconnected() + } + + 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() + object OnNetworkCreated : CallbackEntry() + object OnNetworkDestroyed : CallbackEntry() + } + + 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)) + } + + override fun onNetworkCreated() { + history.add(OnNetworkCreated) + } + + override fun onNetworkDestroyed() { + history.add(OnNetworkDestroyed) + } + + // 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, + initialNc: NetworkCapabilities? = null, + initialLp: LinkProperties? = null, + initialConfig: NetworkAgentConfig? = null + ): TestableNetworkAgent { + val nc = initialNc ?: NetworkCapabilities().apply { + addTransportType(TRANSPORT_TEST) + removeCapability(NET_CAPABILITY_TRUSTED) + removeCapability(NET_CAPABILITY_INTERNET) + addCapability(NET_CAPABILITY_NOT_SUSPENDED) + addCapability(NET_CAPABILITY_NOT_ROAMING) + addCapability(NET_CAPABILITY_NOT_VPN) + if (SdkLevel.isAtLeastS()) { + addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + } + if (null != name) { + setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name)) + } + } + val lp = initialLp ?: LinkProperties().apply { + addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32)) + addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null)) + } + val config = initialConfig ?: 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(TRANSPORT_TEST) + .build() + val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS) + requestNetwork(request, callback) + val agent = createNetworkAgent(context, name) + agent.setTeardownDelayMillis(0) + agent.register() + agent.markConnected() + agent.expectCallback() + 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() + } + agent.expectCallback() + } + + @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(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 = NattKeepalivePacketData( + LOCAL_IPV4_ADDRESS /* srcAddress */, 1234 /* srcPort */, + REMOTE_IPV4_ADDRESS /* dstAddress */, 4567 /* dstPort */, + ByteArray(100 /* size */)) + val slot = 4 + val interval = 37 + + mFakeConnectivityService.agent.onAddNattKeepalivePacketFilter(slot, packet) + mFakeConnectivityService.agent.onStartNattSocketKeepalive(slot, interval, 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) + verify(mFakeConnectivityService.mockRegistry, timeout(DEFAULT_TIMEOUT_MS)) + .sendSocketKeepaliveEvent(slot, SocketKeepalive.ERROR_UNSUPPORTED) + + mFakeConnectivityService.agent.onStopSocketKeepalive(slot) + mFakeConnectivityService.agent.onRemoveKeepalivePacketFilter(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(NET_CAPABILITY_NOT_METERED) + agent.sendNetworkCapabilities(nc) + callback.expectCapabilitiesThat(agent.network) { + it.hasCapability(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(TRANSPORT_TEST) + .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name1)) + .build() + val request2 = NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(TRANSPORT_TEST) + .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(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(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) + // If using the int ranking, agent1 must be upgraded to a better score so that there is + // no ambiguity when agent2 connects that agent1 is still better. If using policy + // ranking, this is not necessary. + agent1.sendNetworkScore(NetworkScore.Builder().setLegacyInt(BETTER_NETWORK_SCORE) + .build()) + // Connect the second agent + createConnectedNetworkAgent(name = name2).let { (agent2, _) -> + agent2.markConnected() + // The callback should not see anything yet. With int ranking, agent1 was upgraded + // to a stronger score beforehand. With policy ranking, agent1 is preferred by + // virtue of already satisfying the request. + callback.assertNoCallback(NO_CALLBACK_TIMEOUT) + // Now downgrade the score and expect the callback now prefers agent2 + agent1.sendNetworkScore(NetworkScore.Builder() + .setLegacyInt(WORSE_NETWORK_SCORE) + .setExiting(true) + .build()) + callback.expectCallback(agent2.network) + } + } + + // tearDown() will unregister the requests and agents + } + + private fun hasAllTransports(nc: NetworkCapabilities?, transports: IntArray) = + nc != null && transports.all { nc.hasTransport(it) } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + fun testSetUnderlyingNetworksAndVpnSpecifier() { + val mySessionId = "MySession12345" + val request = NetworkRequest.Builder() + .addTransportType(TRANSPORT_TEST) + .addTransportType(TRANSPORT_VPN) + .removeCapability(NET_CAPABILITY_NOT_VPN) + .removeCapability(NET_CAPABILITY_TRUSTED) // TODO: add to VPN! + .build() + val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS) + registerNetworkCallback(request, callback) + + val nc = NetworkCapabilities().apply { + addTransportType(TRANSPORT_TEST) + addTransportType(TRANSPORT_VPN) + removeCapability(NET_CAPABILITY_NOT_VPN) + setTransportInfo(VpnTransportInfo(VpnManager.TYPE_VPN_SERVICE, mySessionId)) + if (SdkLevel.isAtLeastS()) { + addCapability(NET_CAPABILITY_NOT_VCN_MANAGED) + } + } + val defaultNetwork = mCM.activeNetwork + assertNotNull(defaultNetwork) + val defaultNetworkCapabilities = mCM.getNetworkCapabilities(defaultNetwork) + val defaultNetworkTransports = defaultNetworkCapabilities.transportTypes + + val agent = createNetworkAgent(initialNc = nc) + agent.register() + agent.markConnected() + callback.expectAvailableThenValidatedCallbacks(agent.network!!) + + // Check that the default network's transport is propagated to the VPN. + var vpnNc = mCM.getNetworkCapabilities(agent.network) + assertNotNull(vpnNc) + assertEquals(VpnManager.TYPE_VPN_SERVICE, + (vpnNc.transportInfo as VpnTransportInfo).type) + assertEquals(mySessionId, (vpnNc.transportInfo as VpnTransportInfo).sessionId) + + val testAndVpn = intArrayOf(TRANSPORT_TEST, TRANSPORT_VPN) + assertTrue(hasAllTransports(vpnNc, testAndVpn)) + assertFalse(vpnNc.hasCapability(NET_CAPABILITY_NOT_VPN)) + assertTrue(hasAllTransports(vpnNc, defaultNetworkTransports), + "VPN transports ${Arrays.toString(vpnNc.transportTypes)}" + + " lacking transports from ${Arrays.toString(defaultNetworkTransports)}") + + // Check that when no underlying networks are announced the underlying transport disappears. + agent.setUnderlyingNetworks(listOf()) + callback.expectCapabilitiesThat(agent.network!!) { + it.transportTypes.size == 2 && hasAllTransports(it, testAndVpn) + } + + // Put the underlying network back and check that the underlying transport reappears. + val expectedTransports = (defaultNetworkTransports.toSet() + TRANSPORT_TEST + TRANSPORT_VPN) + .toIntArray() + agent.setUnderlyingNetworks(null) + callback.expectCapabilitiesThat(agent.network!!) { + it.transportTypes.size == expectedTransports.size && + hasAllTransports(it, expectedTransports) + } + + // Check that some underlying capabilities are propagated. + // This is not very accurate because the test does not control the capabilities of the + // underlying networks, and because not congested, not roaming, and not suspended are the + // default anyway. It's still useful as an extra check though. + vpnNc = mCM.getNetworkCapabilities(agent.network) + for (cap in listOf(NET_CAPABILITY_NOT_CONGESTED, + NET_CAPABILITY_NOT_ROAMING, + NET_CAPABILITY_NOT_SUSPENDED)) { + val capStr = valueToString(NetworkCapabilities::class.java, "NET_CAPABILITY_", cap) + if (defaultNetworkCapabilities.hasCapability(cap) && !vpnNc.hasCapability(cap)) { + fail("$capStr not propagated from underlying: $defaultNetworkCapabilities") + } + } + + agent.unregister() + callback.expectCallback(agent.network) + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + fun testAgentStartsInConnecting() { + val mockContext = mock(Context::class.java) + val mockCm = mock(ConnectivityManager::class.java) + doReturn(mockCm).`when`(mockContext).getSystemService(Context.CONNECTIVITY_SERVICE) + val agent = createNetworkAgent(mockContext) + agent.register() + verify(mockCm).registerNetworkAgent(any(), + argThat { it.detailedState == NetworkInfo.DetailedState.CONNECTING }, + any(LinkProperties::class.java), + any(NetworkCapabilities::class.java), + any(NetworkScore::class.java), + any(NetworkAgentConfig::class.java), + eq(NetworkProvider.ID_NONE)) + } + + @Test + fun testSetAcceptUnvalidated() { + createNetworkAgentWithFakeCS().let { agent -> + mFakeConnectivityService.agent.onSaveAcceptUnvalidated(true) + agent.expectCallback().let { + assertTrue(it.accept) + } + agent.assertNoCallback() + } + } + + @Test + fun testSetAcceptUnvalidatedPreventAutomaticReconnect() { + createNetworkAgentWithFakeCS().let { agent -> + mFakeConnectivityService.agent.onSaveAcceptUnvalidated(false) + mFakeConnectivityService.agent.onPreventAutomaticReconnect() + agent.expectCallback().let { + assertFalse(it.accept) + } + agent.expectCallback() + agent.assertNoCallback() + // When automatic reconnect is turned off, the network is torn down and + // ConnectivityService disconnects. As part of the disconnect, ConnectivityService will + // also send itself a message to unregister the NetworkAgent from its internal + // structure. + mFakeConnectivityService.disconnect() + agent.expectCallback() + } + } + + @Test + fun testPreventAutomaticReconnect() { + createNetworkAgentWithFakeCS().let { agent -> + mFakeConnectivityService.agent.onPreventAutomaticReconnect() + agent.expectCallback() + agent.assertNoCallback() + mFakeConnectivityService.disconnect() + agent.expectCallback() + } + } + + @Test + fun testValidationStatus() = createNetworkAgentWithFakeCS().let { agent -> + val uri = Uri.parse("http://www.google.com") + mFakeConnectivityService.agent.onValidationStatusChanged(VALID_NETWORK, + uri.toString()) + agent.expectCallback().let { + assertEquals(it.status, VALID_NETWORK) + assertEquals(it.uri, uri) + } + + mFakeConnectivityService.agent.onValidationStatusChanged(INVALID_NETWORK, null) + 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(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(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(NET_CAPABILITY_TEMPORARILY_NOT_METERED) + agent.sendNetworkCapabilities(nc1) + callback.expectCapabilitiesThat(agent.network) { + it.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED) + } + + // Remove TEMP_NOT_METERED and check that the callback is called appropriately. + val nc2 = NetworkCapabilities(agent.nc) + .removeCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED) + agent.sendNetworkCapabilities(nc2) + callback.expectCapabilitiesThat(agent.network) { + !it.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED) + } + } + + // tearDown() will unregister the requests and agents + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + fun testSetLingerDuration() { + // This test will create two networks and check that the one with the stronger + // score wins out for a request that matches them both. And the weaker agent will + // be disconnected after customized linger duration. + + // Connect the first Network + val name1 = UUID.randomUUID().toString() + val name2 = UUID.randomUUID().toString() + val (agent1, callback) = createConnectedNetworkAgent(name = name1) + callback.expectAvailableThenValidatedCallbacks(agent1.network!!) + // Downgrade agent1 to a worse score so that there is no ambiguity when + // agent2 connects. + agent1.sendNetworkScore(NetworkScore.Builder().setLegacyInt(WORSE_NETWORK_SCORE) + .setExiting(true).build()) + + // Verify invalid linger duration cannot be set. + assertFailsWith { + agent1.setLingerDuration(Duration.ofMillis(-1)) + } + assertFailsWith { agent1.setLingerDuration(Duration.ZERO) } + assertFailsWith { + agent1.setLingerDuration(Duration.ofMillis(Integer.MIN_VALUE.toLong())) + } + assertFailsWith { + agent1.setLingerDuration(Duration.ofMillis(Integer.MAX_VALUE.toLong() + 1)) + } + assertFailsWith { + agent1.setLingerDuration(Duration.ofMillis( + NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - 1)) + } + // Verify valid linger timer can be set, but it should not take effect since the network + // is still needed. + agent1.setLingerDuration(Duration.ofMillis(Integer.MAX_VALUE.toLong())) + callback.assertNoCallback(NO_CALLBACK_TIMEOUT) + // Set to the value we want to verify the functionality. + agent1.setLingerDuration(Duration.ofMillis(NetworkAgent.MIN_LINGER_TIMER_MS.toLong())) + // Make a listener which can observe agent1 lost later. + val callbackWeaker = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS) + registerNetworkCallback(NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(TRANSPORT_TEST) + .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name1)) + .build(), callbackWeaker) + callbackWeaker.expectAvailableCallbacks(agent1.network!!) + + // Connect the second agent with a score better than agent1. Verify the callback for + // agent1 sees the linger expiry while the callback for both sees the winner. + // Record linger start timestamp prior to send score to prevent possible race, the actual + // timestamp should be slightly late than this since the service handles update + // network score asynchronously. + val lingerStart = SystemClock.elapsedRealtime() + val agent2 = createNetworkAgent(name = name2) + agent2.register() + agent2.markConnected() + callback.expectAvailableCallbacks(agent2.network!!) + callbackWeaker.expectCallback(agent1.network!!) + val expectedRemainingLingerDuration = lingerStart + + NetworkAgent.MIN_LINGER_TIMER_MS.toLong() - SystemClock.elapsedRealtime() + // If the available callback is too late. The remaining duration will be reduced. + assertTrue(expectedRemainingLingerDuration > 0, + "expected remaining linger duration is $expectedRemainingLingerDuration") + callbackWeaker.assertNoCallback(expectedRemainingLingerDuration) + callbackWeaker.expectCallback(agent1.network!!) + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + fun testSetSubscriberId() { + val name = "TEST-AGENT" + val imsi = UUID.randomUUID().toString() + val config = NetworkAgentConfig.Builder().setSubscriberId(imsi).build() + + val request: NetworkRequest = NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(TRANSPORT_TEST) + .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name)) + .build() + val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS) + requestNetwork(request, callback) + + val agent = createNetworkAgent(name = name, initialConfig = config) + agent.register() + agent.markConnected() + callback.expectAvailableThenValidatedCallbacks(agent.network!!) + val snapshots = runWithShellPermissionIdentity(ThrowingSupplier { + mCM!!.allNetworkStateSnapshots }, NETWORK_SETTINGS) + val testNetworkSnapshot = snapshots.findLast { it.network == agent.network } + assertEquals(imsi, testNetworkSnapshot!!.subscriberId) + } +} 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..bca4456fc9 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java @@ -0,0 +1,489 @@ +/* + * 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_DUN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; +import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL; +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 junit.framework.Assert.fail; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import android.annotation.NonNull; +import android.net.MacAddress; +import android.net.MatchAllNetworkSpecifier; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.NetworkSpecifier; +import android.net.wifi.WifiNetworkSpecifier; +import android.os.Build; +import android.os.PatternMatcher; +import android.os.Process; +import android.util.ArraySet; +import android.util.Range; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.modules.utils.build.SdkLevel; +import com.android.networkstack.apishim.ConstantsShim; +import com.android.networkstack.apishim.NetworkRequestShimImpl; +import com.android.networkstack.apishim.common.NetworkRequestShim; +import com.android.networkstack.apishim.common.UnsupportedApiLevelException; +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.Set; + +@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()); + } + + private void addNotVcnManagedCapability(@NonNull NetworkCapabilities nc) { + if (SdkLevel.isAtLeastS()) { + nc.addCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED); + } + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + public void testCanBeSatisfiedBy() { + final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */); + final LocalNetworkSpecifier specifier2 = new LocalNetworkSpecifier(5678 /* id */); + + // Some requests are adding NOT_VCN_MANAGED capability automatically. Add it to the + // capabilities below for bypassing the check. + final NetworkCapabilities capCellularMmsInternet = new NetworkCapabilities() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_MMS) + .addCapability(NET_CAPABILITY_INTERNET); + addNotVcnManagedCapability(capCellularMmsInternet); + final NetworkCapabilities capCellularVpnMmsInternet = + new NetworkCapabilities(capCellularMmsInternet).addTransportType(TRANSPORT_VPN); + addNotVcnManagedCapability(capCellularVpnMmsInternet); + final NetworkCapabilities capCellularMmsInternetSpecifier1 = + new NetworkCapabilities(capCellularMmsInternet).setNetworkSpecifier(specifier1); + addNotVcnManagedCapability(capCellularMmsInternetSpecifier1); + final NetworkCapabilities capVpnInternetSpecifier1 = new NetworkCapabilities() + .addCapability(NET_CAPABILITY_INTERNET) + .addTransportType(TRANSPORT_VPN) + .setNetworkSpecifier(specifier1); + addNotVcnManagedCapability(capVpnInternetSpecifier1); + final NetworkCapabilities capCellularMmsInternetMatchallspecifier = + new NetworkCapabilities(capCellularMmsInternet) + .setNetworkSpecifier(new MatchAllNetworkSpecifier()); + addNotVcnManagedCapability(capCellularMmsInternetMatchallspecifier); + final NetworkCapabilities capCellularMmsInternetSpecifier2 = + new NetworkCapabilities(capCellularMmsInternet) + .setNetworkSpecifier(specifier2); + addNotVcnManagedCapability(capCellularMmsInternetSpecifier2); + + 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)); + } + + private void setUids(NetworkRequest.Builder builder, Set> ranges) + throws UnsupportedApiLevelException { + if (SdkLevel.isAtLeastS()) { + final NetworkRequestShim networkRequestShim = NetworkRequestShimImpl.newInstance(); + networkRequestShim.setUids(builder, ranges); + } + } + + @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 NetworkRequest.Builder nrBuilder = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .setLinkUpstreamBandwidthKbps(1000) + .setNetworkSpecifier(specifier1) + .setSignalStrength(-123); + + // The uid ranges should be set into the request, but setUids() takes a set of UidRange + // that is hidden and inaccessible from shims. Before, S setUids will be a no-op. But + // because NetworkRequest.Builder sets the UID of the request to the current UID, the + // request contains the current UID both on S and before S. + final Set> ranges = new ArraySet<>(); + ranges.add(new Range(uid, uid)); + try { + setUids(nrBuilder, ranges); + } catch (UnsupportedApiLevelException e) { + // Not supported before API31. + } + final NetworkRequest requestCombination = nrBuilder.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(); + addNotVcnManagedCapability(capCellInternetBWSpecifier1Signal); + 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)); + } + + private static Set> uidRangesForUid(int uid) { + final Range range = new Range<>(uid, uid); + return Set.of(range); + } + + @Test + public void testSetIncludeOtherUidNetworks() throws Exception { + assumeTrue(TestUtils.shouldTestSApis()); + final NetworkRequestShim shim = NetworkRequestShimImpl.newInstance(); + + final NetworkRequest.Builder builder = new NetworkRequest.Builder(); + // NetworkRequests have NET_CAPABILITY_NOT_VCN_MANAGED by default. + builder.removeCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED); + shim.setIncludeOtherUidNetworks(builder, false); + final NetworkRequest request = builder.build(); + + final NetworkRequest.Builder otherUidsBuilder = new NetworkRequest.Builder(); + otherUidsBuilder.removeCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED); + shim.setIncludeOtherUidNetworks(otherUidsBuilder, true); + final NetworkRequest otherUidsRequest = otherUidsBuilder.build(); + + assertNotEquals(Process.SYSTEM_UID, Process.myUid()); + final NetworkCapabilities ncWithMyUid = new NetworkCapabilities() + .setUids(uidRangesForUid(Process.myUid())); + final NetworkCapabilities ncWithOtherUid = new NetworkCapabilities() + .setUids(uidRangesForUid(Process.SYSTEM_UID)); + + assertTrue(request + " should be satisfied by " + ncWithMyUid, + request.canBeSatisfiedBy(ncWithMyUid)); + assertTrue(otherUidsRequest + " should be satisfied by " + ncWithMyUid, + otherUidsRequest.canBeSatisfiedBy(ncWithMyUid)); + assertFalse(request + " should not be satisfied by " + ncWithOtherUid, + request.canBeSatisfiedBy(ncWithOtherUid)); + assertTrue(otherUidsRequest + " should be satisfied by " + ncWithOtherUid, + otherUidsRequest.canBeSatisfiedBy(ncWithOtherUid)); + } + + @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()); + } + + // TODO: 1. Refactor test cases with helper method. + // 2. Test capability that does not yet exist. + @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testBypassingVcn() { + // Make an empty request. Verify the NOT_VCN_MANAGED is added. + final NetworkRequest emptyRequest = new NetworkRequest.Builder().build(); + assertTrue(emptyRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)); + + // Make a request explicitly add NOT_VCN_MANAGED. Verify the NOT_VCN_MANAGED is preserved. + final NetworkRequest mmsAddNotVcnRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_MMS) + .addCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED) + .build(); + assertTrue(mmsAddNotVcnRequest.hasCapability( + ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)); + + // Similar to above, but the opposite order. + final NetworkRequest mmsAddNotVcnRequest2 = new NetworkRequest.Builder() + .addCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED) + .addCapability(NET_CAPABILITY_MMS) + .build(); + assertTrue(mmsAddNotVcnRequest2.hasCapability( + ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)); + + // Make a request explicitly remove NOT_VCN_MANAGED. Verify the NOT_VCN_MANAGED is removed. + final NetworkRequest removeNotVcnRequest = new NetworkRequest.Builder() + .removeCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED).build(); + assertFalse(removeNotVcnRequest.hasCapability( + ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)); + + // Make a request add some capability inside VCN supported capabilities. + // Verify the NOT_VCN_MANAGED is added. + final NetworkRequest notRoamRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_NOT_ROAMING).build(); + assertTrue(notRoamRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)); + + // Make an internet request. Verify the NOT_VCN_MANAGED is added. + final NetworkRequest internetRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET).build(); + assertTrue(internetRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)); + + // Make an internet request which explicitly removed NOT_VCN_MANAGED. + // Verify the NOT_VCN_MANAGED is removed. + final NetworkRequest internetRemoveNotVcnRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_INTERNET) + .removeCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED).build(); + assertFalse(internetRemoveNotVcnRequest.hasCapability( + ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)); + + // Make a normal MMS request. Verify the request could bypass VCN. + final NetworkRequest mmsRequest = + new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build(); + assertFalse(mmsRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)); + + // Make a SUPL request along with internet. Verify NOT_VCN_MANAGED is not added since + // SUPL is not in the supported list. + final NetworkRequest suplWithInternetRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_SUPL) + .addCapability(NET_CAPABILITY_INTERNET).build(); + assertFalse(suplWithInternetRequest.hasCapability( + ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)); + + // Make a FOTA request with explicitly add NOT_VCN_MANAGED capability. Verify + // NOT_VCN_MANAGED is preserved. + final NetworkRequest fotaRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_FOTA) + .addCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED).build(); + assertTrue(fotaRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)); + + // Make a DUN request, which is in {@code VCN_SUPPORTED_CAPABILITIES}. + // Verify NOT_VCN_MANAGED is preserved. + final NetworkRequest dunRequest = new NetworkRequest.Builder() + .addCapability(NET_CAPABILITY_DUN).build(); + assertTrue(dunRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)); + + // Make an internet request but with NetworkSpecifier. Verify the NOT_VCN_MANAGED is not + // added. + final NetworkRequest internetWithSpecifierRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI).addCapability(NET_CAPABILITY_INTERNET) + .setNetworkSpecifier(makeTestWifiSpecifier()).build(); + assertFalse(internetWithSpecifierRequest.hasCapability( + ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)); + } + + private void verifyEqualRequestBuilt(NetworkRequest orig) { + try { + final NetworkRequestShim shim = NetworkRequestShimImpl.newInstance(); + final NetworkRequest copy = shim.newBuilder(orig).build(); + assertEquals(orig, copy); + } catch (UnsupportedApiLevelException e) { + fail("NetworkRequestShim.newBuilder should be supported in this SDK version"); + } + } + + @Test + public void testBuildRequestFromExistingRequestWithBuilder() { + assumeTrue(TestUtils.shouldTestSApis()); + final NetworkRequest.Builder builder = new NetworkRequest.Builder(); + + final NetworkRequest baseRequest = builder.build(); + verifyEqualRequestBuilt(baseRequest); + + final NetworkRequest requestCellMms = builder + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_MMS) + .setSignalStrength(-99).build(); + verifyEqualRequestBuilt(requestCellMms); + + final NetworkRequest requestWifi = builder + .addTransportType(TRANSPORT_WIFI) + .removeTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET) + .removeCapability(NET_CAPABILITY_MMS) + .setNetworkSpecifier(makeTestWifiSpecifier()) + .setSignalStrength(-33).build(); + verifyEqualRequestBuilt(requestWifi); + } + + private WifiNetworkSpecifier makeTestWifiSpecifier() { + return new WifiNetworkSpecifier.Builder() + .setSsidPattern(new PatternMatcher(TEST_SSID, PatternMatcher.PATTERN_LITERAL)) + .setBssidPattern(ARBITRARY_ADDRESS, ARBITRARY_ADDRESS) + .build(); + } +} 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..6833c70994 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/NetworkWatchlistTest.java @@ -0,0 +1,172 @@ +/* + * 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.app.UiAutomation; +import android.content.Context; +import android.net.ConnectivityManager; +import android.os.FileUtils; +import android.os.ParcelFileDescriptor; +import android.platform.test.annotations.AppModeFull; +import android.util.Log; + +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 setWatchlistConfig(String watchlistConfigFile) throws Exception { + Log.w("NetworkWatchlistTest", "Setting watchlist config " + watchlistConfigFile + + " in " + Thread.currentThread().getName()); + 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(); + } + + private void saveResourceToFile(String res, String filePath) throws IOException { + final UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation() + .getUiAutomation(); + // App can't access /data/local/tmp directly, so we pipe resource to file through stdin. + // Not all devices have symlink for /dev/stdin, so use /proc/self/fd/0 directly. + // /dev/stdin maps to /proc/self/fd/0. + final ParcelFileDescriptor[] fileDescriptors = uiAutomation.executeShellCommandRw( + "cp /proc/self/fd/0 " + filePath); + + ParcelFileDescriptor stdin = fileDescriptors[1]; + ParcelFileDescriptor stdout = fileDescriptors[0]; + + pipeResourceToFileDescriptor(res, stdin); + + // Wait for the process to close its stdout - which should mean it has completed. + consumeFile(stdout); + } + + private void consumeFile(ParcelFileDescriptor pfd) throws IOException { + try (InputStream stream = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) { + for (;;) { + if (stream.read() == -1) { + return; + } + } + } + } + + private void pipeResourceToFileDescriptor(String res, ParcelFileDescriptor pfd) + throws IOException { + try (InputStream resStream = getClass().getClassLoader().getResourceAsStream(res); + FileOutputStream fdStream = new ParcelFileDescriptor.AutoCloseOutputStream(pfd)) { + FileUtils.copy(resStream, fdStream); + } + } + + private static String runCommand(String command) throws IOException { + return SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(), command); + } +} diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.java b/tests/cts/net/src/android/net/cts/NsdManagerTest.java new file mode 100644 index 0000000000..2bcfdc315b --- /dev/null +++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.java @@ -0,0 +1,594 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.cts; + +import android.content.Context; +import android.net.nsd.NsdManager; +import android.net.nsd.NsdServiceInfo; +import android.platform.test.annotations.AppModeFull; +import android.test.AndroidTestCase; +import android.util.Log; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.Arrays; +import java.util.Random; +import java.util.List; +import java.util.ArrayList; + +@AppModeFull(reason = "Socket cannot bind in instant app mode") +public class NsdManagerTest extends AndroidTestCase { + + private static final String TAG = "NsdManagerTest"; + private static final String SERVICE_TYPE = "_nmt._tcp"; + private static final int TIMEOUT = 2000; + + private static final boolean DBG = false; + + NsdManager mNsdManager; + + NsdManager.RegistrationListener mRegistrationListener; + NsdManager.DiscoveryListener mDiscoveryListener; + NsdManager.ResolveListener mResolveListener; + private NsdServiceInfo mResolvedService; + + public NsdManagerTest() { + initRegistrationListener(); + initDiscoveryListener(); + initResolveListener(); + } + + private void initRegistrationListener() { + mRegistrationListener = new NsdManager.RegistrationListener() { + @Override + public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) { + setEvent("onRegistrationFailed", errorCode); + } + + @Override + public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) { + setEvent("onUnregistrationFailed", errorCode); + } + + @Override + public void onServiceRegistered(NsdServiceInfo serviceInfo) { + setEvent("onServiceRegistered", serviceInfo); + } + + @Override + public void onServiceUnregistered(NsdServiceInfo serviceInfo) { + setEvent("onServiceUnregistered", serviceInfo); + } + }; + } + + private void initDiscoveryListener() { + mDiscoveryListener = new NsdManager.DiscoveryListener() { + @Override + public void onStartDiscoveryFailed(String serviceType, int errorCode) { + setEvent("onStartDiscoveryFailed", errorCode); + } + + @Override + public void onStopDiscoveryFailed(String serviceType, int errorCode) { + setEvent("onStopDiscoveryFailed", errorCode); + } + + @Override + public void onDiscoveryStarted(String serviceType) { + NsdServiceInfo info = new NsdServiceInfo(); + info.setServiceType(serviceType); + setEvent("onDiscoveryStarted", info); + } + + @Override + public void onDiscoveryStopped(String serviceType) { + NsdServiceInfo info = new NsdServiceInfo(); + info.setServiceType(serviceType); + setEvent("onDiscoveryStopped", info); + } + + @Override + public void onServiceFound(NsdServiceInfo serviceInfo) { + setEvent("onServiceFound", serviceInfo); + } + + @Override + public void onServiceLost(NsdServiceInfo serviceInfo) { + setEvent("onServiceLost", serviceInfo); + } + }; + } + + private void initResolveListener() { + mResolveListener = new NsdManager.ResolveListener() { + @Override + public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { + setEvent("onResolveFailed", errorCode); + } + + @Override + public void onServiceResolved(NsdServiceInfo serviceInfo) { + mResolvedService = serviceInfo; + setEvent("onServiceResolved", serviceInfo); + } + }; + } + + + + private final class EventData { + EventData(String callbackName, NsdServiceInfo info) { + mCallbackName = callbackName; + mSucceeded = true; + mErrorCode = 0; + mInfo = info; + } + EventData(String callbackName, int errorCode) { + mCallbackName = callbackName; + mSucceeded = false; + mErrorCode = errorCode; + mInfo = null; + } + private final String mCallbackName; + private final boolean mSucceeded; + private final int mErrorCode; + private final NsdServiceInfo mInfo; + } + + private final List mEventCache = new ArrayList(); + + private void setEvent(String callbackName, int errorCode) { + if (DBG) Log.d(TAG, callbackName + " failed with " + String.valueOf(errorCode)); + EventData eventData = new EventData(callbackName, errorCode); + synchronized (mEventCache) { + mEventCache.add(eventData); + mEventCache.notify(); + } + } + + private void setEvent(String callbackName, NsdServiceInfo info) { + if (DBG) Log.d(TAG, "Received event " + callbackName + " for " + info.getServiceName()); + EventData eventData = new EventData(callbackName, info); + synchronized (mEventCache) { + mEventCache.add(eventData); + mEventCache.notify(); + } + } + + void clearEventCache() { + synchronized(mEventCache) { + mEventCache.clear(); + } + } + + int eventCacheSize() { + synchronized(mEventCache) { + return mEventCache.size(); + } + } + + private int mWaitId = 0; + private EventData waitForCallback(String callbackName) { + + synchronized(mEventCache) { + + mWaitId ++; + if (DBG) Log.d(TAG, "Waiting for " + callbackName + ", id=" + String.valueOf(mWaitId)); + + try { + long startTime = android.os.SystemClock.uptimeMillis(); + long elapsedTime = 0; + int index = 0; + while (elapsedTime < TIMEOUT ) { + // first check if we've received that event + for (; index < mEventCache.size(); index++) { + EventData e = mEventCache.get(index); + if (e.mCallbackName.equals(callbackName)) { + if (DBG) Log.d(TAG, "exiting wait id=" + String.valueOf(mWaitId)); + return e; + } + } + + // Not yet received, just wait + mEventCache.wait(TIMEOUT - elapsedTime); + elapsedTime = android.os.SystemClock.uptimeMillis() - startTime; + } + // we exited the loop because of TIMEOUT; fail the call + if (DBG) Log.d(TAG, "timed out waiting id=" + String.valueOf(mWaitId)); + return null; + } catch (InterruptedException e) { + return null; // wait timed out! + } + } + } + + private EventData waitForNewEvents() throws InterruptedException { + if (DBG) Log.d(TAG, "Waiting for a bit, id=" + String.valueOf(mWaitId)); + + long startTime = android.os.SystemClock.uptimeMillis(); + long elapsedTime = 0; + synchronized (mEventCache) { + int index = mEventCache.size(); + while (elapsedTime < TIMEOUT ) { + // first check if we've received that event + for (; index < mEventCache.size(); index++) { + EventData e = mEventCache.get(index); + return e; + } + + // Not yet received, just wait + mEventCache.wait(TIMEOUT - elapsedTime); + elapsedTime = android.os.SystemClock.uptimeMillis() - startTime; + } + } + + return null; + } + + private String mServiceName; + + @Override + public void setUp() throws Exception { + super.setUp(); + if (DBG) Log.d(TAG, "Setup test ..."); + mNsdManager = (NsdManager) getContext().getSystemService(Context.NSD_SERVICE); + + Random rand = new Random(); + mServiceName = new String("NsdTest"); + for (int i = 0; i < 4; i++) { + mServiceName = mServiceName + String.valueOf(rand.nextInt(10)); + } + } + + @Override + public void tearDown() throws Exception { + if (DBG) Log.d(TAG, "Tear down test ..."); + super.tearDown(); + } + + public void testNDSManager() throws Exception { + EventData lastEvent = null; + + if (DBG) Log.d(TAG, "Starting test ..."); + + NsdServiceInfo si = new NsdServiceInfo(); + si.setServiceType(SERVICE_TYPE); + si.setServiceName(mServiceName); + + byte testByteArray[] = new byte[] {-128, 127, 2, 1, 0, 1, 2}; + String String256 = "1_________2_________3_________4_________5_________6_________" + + "7_________8_________9_________10________11________12________13________" + + "14________15________16________17________18________19________20________" + + "21________22________23________24________25________123456"; + + // Illegal attributes + try { + si.setAttribute(null, (String) null); + fail("Could set null key"); + } catch (IllegalArgumentException e) { + // expected + } + + try { + si.setAttribute("", (String) null); + fail("Could set empty key"); + } catch (IllegalArgumentException e) { + // expected + } + + try { + si.setAttribute(String256, (String) null); + fail("Could set key with 255 characters"); + } catch (IllegalArgumentException e) { + // expected + } + + try { + si.setAttribute("key", String256.substring(3)); + fail("Could set key+value combination with more than 255 characters"); + } catch (IllegalArgumentException e) { + // expected + } + + try { + si.setAttribute("key", String256.substring(4)); + fail("Could set key+value combination with 255 characters"); + } catch (IllegalArgumentException e) { + // expected + } + + try { + si.setAttribute(new String(new byte[]{0x19}), (String) null); + fail("Could set key with invalid character"); + } catch (IllegalArgumentException e) { + // expected + } + + try { + si.setAttribute("=", (String) null); + fail("Could set key with invalid character"); + } catch (IllegalArgumentException e) { + // expected + } + + try { + si.setAttribute(new String(new byte[]{0x7F}), (String) null); + fail("Could set key with invalid character"); + } catch (IllegalArgumentException e) { + // expected + } + + // Allowed attributes + si.setAttribute("booleanAttr", (String) null); + si.setAttribute("keyValueAttr", "value"); + si.setAttribute("keyEqualsAttr", "="); + si.setAttribute(" whiteSpaceKeyValueAttr ", " value "); + si.setAttribute("binaryDataAttr", testByteArray); + si.setAttribute("nullBinaryDataAttr", (byte[]) null); + si.setAttribute("emptyBinaryDataAttr", new byte[]{}); + si.setAttribute("longkey", String256.substring(9)); + + ServerSocket socket; + int localPort; + + try { + socket = new ServerSocket(0); + localPort = socket.getLocalPort(); + si.setPort(localPort); + } catch (IOException e) { + if (DBG) Log.d(TAG, "Could not open a local socket"); + assertTrue(false); + return; + } + + if (DBG) Log.d(TAG, "Port = " + String.valueOf(localPort)); + + clearEventCache(); + + mNsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener); + lastEvent = waitForCallback("onServiceRegistered"); // id = 1 + assertTrue(lastEvent != null); + assertTrue(lastEvent.mSucceeded); + assertTrue(eventCacheSize() == 1); + + // We may not always get the name that we tried to register; + // This events tells us the name that was registered. + String registeredName = lastEvent.mInfo.getServiceName(); + si.setServiceName(registeredName); + + clearEventCache(); + + mNsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, + mDiscoveryListener); + + // Expect discovery started + lastEvent = waitForCallback("onDiscoveryStarted"); // id = 2 + + assertTrue(lastEvent != null); + assertTrue(lastEvent.mSucceeded); + + // Remove this event, so accounting becomes easier later + synchronized (mEventCache) { + mEventCache.remove(lastEvent); + } + + // Expect a service record to be discovered (and filter the ones + // that are unrelated to this test) + boolean found = false; + for (int i = 0; i < 32; i++) { + + lastEvent = waitForCallback("onServiceFound"); // id = 3 + if (lastEvent == null) { + // no more onServiceFound events are being reported! + break; + } + + assertTrue(lastEvent.mSucceeded); + + if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " + + lastEvent.mInfo.getServiceName()); + + if (lastEvent.mInfo.getServiceName().equals(registeredName)) { + // Save it, as it will get overwritten with new serviceFound events + si = lastEvent.mInfo; + found = true; + } + + // Remove this event from the event cache, so it won't be found by subsequent + // calls to waitForCallback + synchronized (mEventCache) { + mEventCache.remove(lastEvent); + } + } + + assertTrue(found); + + // We've removed all serviceFound events, and we've removed the discoveryStarted + // event as well, so now the event cache should be empty! + assertTrue(eventCacheSize() == 0); + + // Resolve the service + clearEventCache(); + mNsdManager.resolveService(si, mResolveListener); + lastEvent = waitForCallback("onServiceResolved"); // id = 4 + + assertNotNull(mResolvedService); + + // Check Txt attributes + assertEquals(8, mResolvedService.getAttributes().size()); + assertTrue(mResolvedService.getAttributes().containsKey("booleanAttr")); + assertNull(mResolvedService.getAttributes().get("booleanAttr")); + assertEquals("value", new String(mResolvedService.getAttributes().get("keyValueAttr"))); + assertEquals("=", new String(mResolvedService.getAttributes().get("keyEqualsAttr"))); + assertEquals(" value ", new String(mResolvedService.getAttributes() + .get(" whiteSpaceKeyValueAttr "))); + assertEquals(String256.substring(9), new String(mResolvedService.getAttributes() + .get("longkey"))); + assertTrue(Arrays.equals(testByteArray, + mResolvedService.getAttributes().get("binaryDataAttr"))); + assertTrue(mResolvedService.getAttributes().containsKey("nullBinaryDataAttr")); + assertNull(mResolvedService.getAttributes().get("nullBinaryDataAttr")); + assertTrue(mResolvedService.getAttributes().containsKey("emptyBinaryDataAttr")); + assertNull(mResolvedService.getAttributes().get("emptyBinaryDataAttr")); + + assertTrue(lastEvent != null); + assertTrue(lastEvent.mSucceeded); + + if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": Port = " + + String.valueOf(lastEvent.mInfo.getPort())); + + assertTrue(lastEvent.mInfo.getPort() == localPort); + assertTrue(eventCacheSize() == 1); + + checkForAdditionalEvents(); + clearEventCache(); + + // Unregister the service + mNsdManager.unregisterService(mRegistrationListener); + lastEvent = waitForCallback("onServiceUnregistered"); // id = 5 + + assertTrue(lastEvent != null); + assertTrue(lastEvent.mSucceeded); + + // Expect a callback for service lost + lastEvent = waitForCallback("onServiceLost"); // id = 6 + + assertTrue(lastEvent != null); + assertTrue(lastEvent.mInfo.getServiceName().equals(registeredName)); + + // Register service again to see if we discover it + checkForAdditionalEvents(); + clearEventCache(); + + si = new NsdServiceInfo(); + si.setServiceType(SERVICE_TYPE); + si.setServiceName(mServiceName); + si.setPort(localPort); + + // Create a new registration listener and register same service again + initRegistrationListener(); + + mNsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener); + + lastEvent = waitForCallback("onServiceRegistered"); // id = 7 + + assertTrue(lastEvent != null); + assertTrue(lastEvent.mSucceeded); + + registeredName = lastEvent.mInfo.getServiceName(); + + // Expect a record to be discovered + // Expect a service record to be discovered (and filter the ones + // that are unrelated to this test) + found = false; + for (int i = 0; i < 32; i++) { + + lastEvent = waitForCallback("onServiceFound"); // id = 8 + if (lastEvent == null) { + // no more onServiceFound events are being reported! + break; + } + + assertTrue(lastEvent.mSucceeded); + + if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " + + lastEvent.mInfo.getServiceName()); + + if (lastEvent.mInfo.getServiceName().equals(registeredName)) { + // Save it, as it will get overwritten with new serviceFound events + si = lastEvent.mInfo; + found = true; + } + + // Remove this event from the event cache, so it won't be found by subsequent + // calls to waitForCallback + synchronized (mEventCache) { + mEventCache.remove(lastEvent); + } + } + + assertTrue(found); + + // Resolve the service + clearEventCache(); + mNsdManager.resolveService(si, mResolveListener); + lastEvent = waitForCallback("onServiceResolved"); // id = 9 + + assertTrue(lastEvent != null); + assertTrue(lastEvent.mSucceeded); + + if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " + + lastEvent.mInfo.getServiceName()); + + assertTrue(lastEvent.mInfo.getServiceName().equals(registeredName)); + + assertNotNull(mResolvedService); + + // Check that we don't have any TXT records + assertEquals(0, mResolvedService.getAttributes().size()); + + checkForAdditionalEvents(); + clearEventCache(); + + mNsdManager.stopServiceDiscovery(mDiscoveryListener); + lastEvent = waitForCallback("onDiscoveryStopped"); // id = 10 + assertTrue(lastEvent != null); + assertTrue(lastEvent.mSucceeded); + assertTrue(checkCacheSize(1)); + + checkForAdditionalEvents(); + clearEventCache(); + + mNsdManager.unregisterService(mRegistrationListener); + + lastEvent = waitForCallback("onServiceUnregistered"); // id = 11 + assertTrue(lastEvent != null); + assertTrue(lastEvent.mSucceeded); + assertTrue(checkCacheSize(1)); + } + + boolean checkCacheSize(int size) { + synchronized (mEventCache) { + int cacheSize = mEventCache.size(); + if (cacheSize != size) { + Log.d(TAG, "id = " + mWaitId + ": event cache size = " + cacheSize); + for (int i = 0; i < cacheSize; i++) { + EventData e = mEventCache.get(i); + String sname = (e.mInfo != null) ? "(" + e.mInfo.getServiceName() + ")" : ""; + Log.d(TAG, "eventName is " + e.mCallbackName + sname); + } + } + return (cacheSize == size); + } + } + + boolean checkForAdditionalEvents() { + try { + EventData e = waitForNewEvents(); + if (e != null) { + String sname = (e.mInfo != null) ? "(" + e.mInfo.getServiceName() + ")" : ""; + Log.d(TAG, "ignoring unexpected event " + e.mCallbackName + sname); + } + return (e == null); + } + catch (InterruptedException ex) { + return false; + } + } +} + 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/TestUtils.java b/tests/cts/net/src/android/net/cts/TestUtils.java new file mode 100644 index 0000000000..c1100b111f --- /dev/null +++ b/tests/cts/net/src/android/net/cts/TestUtils.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.cts; + +import android.os.Build; + +import com.android.modules.utils.build.SdkLevel; +import com.android.networkstack.apishim.ConstantsShim; + +/** + * Utils class to provide common shared test helper methods or constants that behave differently + * depending on the SDK against which they are compiled. + */ +public class TestUtils { + /** + * Whether to test S+ APIs. This requires a) that the test be running on an S+ device, and + * b) that the code be compiled against shims new enough to access these APIs. + */ + public static boolean shouldTestSApis() { + return SdkLevel.isAtLeastS() && ConstantsShim.VERSION > Build.VERSION_CODES.R; + } +} 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..1d9268ae11 --- /dev/null +++ b/tests/cts/net/src/android/net/cts/TrafficStatsTest.java @@ -0,0 +1,295 @@ +/* + * 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 testValidIfaceStats() { + assertTrue(TrafficStats.getTxPackets("lo") >= 0); + assertTrue(TrafficStats.getRxPackets("lo") >= 0); + assertTrue(TrafficStats.getTxBytes("lo") >= 0); + assertTrue(TrafficStats.getRxBytes("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"); + final long ifaceTxBytesBefore = TrafficStats.getTxBytes("lo"); + final long ifaceRxBytesBefore = TrafficStats.getRxBytes("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 ifaceTxBytesAfter = TrafficStats.getTxBytes("lo"); + final long ifaceRxBytesAfter = TrafficStats.getRxBytes("lo"); + final long ifaceTxDeltaPackets = ifaceTxPacketsAfter - ifaceTxPacketsBefore; + final long ifaceRxDeltaPackets = ifaceRxPacketsAfter - ifaceRxPacketsBefore; + final long ifaceTxDeltaBytes = ifaceTxBytesAfter - ifaceTxBytesBefore; + final long ifaceRxDeltaBytes = ifaceRxBytesAfter - ifaceRxBytesBefore; + + // 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); + assertInRange("ifrxp", ifaceRxDeltaPackets, packetCount + minExpectedExtraPackets, + packetCount + packetCount + maxExpectedExtraPackets); + assertInRange("iftxb", ifaceTxDeltaBytes, pktBytes + minExpExtraPktBytes, + pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes); + assertInRange("ifrxb", ifaceRxDeltaBytes, pktBytes + minExpExtraPktBytes, + pktBytes + pktWithNoDataBytes + maxExpExtraPktBytes); + + // 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); + assertTrue("iftxb: " + ifaceTxBytesBefore + " -> " + ifaceTxBytesAfter, + totalTxBytesAfter >= totalTxBytesBefore + ifaceTxDeltaBytes); + assertTrue("ifrxb: " + ifaceRxBytesBefore + " -> " + ifaceRxBytesAfter, + totalRxBytesAfter >= totalRxBytesBefore + ifaceRxDeltaBytes); + + // 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..7887385234 --- /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 = 2000; + + 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..88a206858d --- /dev/null +++ b/tests/cts/net/util/Android.bp @@ -0,0 +1,30 @@ +// +// 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. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_library { + name: "cts-net-utils", + srcs: ["java/**/*.java", "java/**/*.kt"], + static_libs: [ + "compatibility-device-util-axt", + "junit", + "net-tests-utils", + ], +} 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..d5a26c4f62 --- /dev/null +++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java @@ -0,0 +1,694 @@ +/* + * 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.ACCESS_WIFI_STATE; +import static android.Manifest.permission.NETWORK_SETTINGS; +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 = 10_000; + private static final int CONNECTIVITY_CHANGE_TIMEOUT_SECS = 30; + private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic"; + 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 + ": onAvailable callback not received", wifiNetwork); + connected = !expectLegacyBroadcast || receiver.waitForState(); + } catch (InterruptedException ex) { + fail("connectToWifi was interrupted"); + } finally { + mCm.unregisterNetworkCallback(callback); + mContext.unregisterReceiver(receiver); + } + + assertTrue(err + ": CONNECTIVITY_ACTION not received", 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, ACCESS_WIFI_STATE, () -> { + 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..1bdd5337b3 --- /dev/null +++ b/tests/cts/net/util/java/android/net/cts/util/CtsTetheringUtils.java @@ -0,0 +1,520 @@ +/* + * 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.assertEquals; +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.TetheringInterface; +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; +import java.util.Set; + +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)); + } + } + + private static boolean isRegexMatch(final String[] ifaceRegexs, String iface) { + if (ifaceRegexs == null) fail("ifaceRegexs should not be null"); + + for (String regex : ifaceRegexs) { + if (iface.matches(regex)) return true; + } + + return false; + } + + public static boolean isAnyIfaceMatch(final String[] ifaceRegexs, final List ifaces) { + if (ifaces == null) return false; + + for (String s : ifaces) { + if (isRegexMatch(ifaceRegexs, s)) return true; + } + + return false; + } + + private static TetheringInterface getFirstMatchingTetheringInterface(final List regexs, + final int type, final Set ifaces) { + if (ifaces == null || regexs == null) return null; + + final String[] regexArray = regexs.toArray(new String[0]); + for (TetheringInterface iface : ifaces) { + if (isRegexMatch(regexArray, iface.getInterface()) && type == iface.getType()) { + return iface; + } + } + + return null; + } + + // 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; + private String mErrorIface; + private int mErrorCode; + + @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; + } + // Call the interface default implementation, which will call + // onTetherableInterfacesChanged(List). This ensures that the default implementation + // of the new callback method calls the old callback method and avoids the need to convert + // Set to List in this code. + @Override + public void onTetherableInterfacesChanged(Set interfaces) { + TetheringEventCallback.super.onTetherableInterfacesChanged(interfaces); + assertHasAllTetheringInterfaces(interfaces, mTetherableIfaces); + mHistory.add(new CallbackValue(CallbackType.ON_TETHERABLE_IFACES, interfaces, 0)); + } + + @Override + public void onTetheredInterfacesChanged(List interfaces) { + mTetheredIfaces = interfaces; + } + + @Override + public void onTetheredInterfacesChanged(Set interfaces) { + TetheringEventCallback.super.onTetheredInterfacesChanged(interfaces); + assertHasAllTetheringInterfaces(interfaces, mTetheredIfaces); + mHistory.add(new CallbackValue(CallbackType.ON_TETHERED_IFACES, interfaces, 0)); + } + + @Override + public void onError(String ifName, int error) { + mErrorIface = ifName; + mErrorCode = error; + } + + @Override + public void onError(TetheringInterface ifName, int error) { + TetheringEventCallback.super.onError(ifName, error); + assertEquals(ifName.getInterface(), mErrorIface); + assertEquals(error, mErrorCode); + 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)); + } + + private void assertHasAllTetheringInterfaces(Set tetheringIfaces, + List ifaces) { + // This does not check that the interfaces are the same. This checks that the + // List has all the interface names contained by the Set. + assertEquals(tetheringIfaces.size(), ifaces.size()); + for (TetheringInterface tether : tetheringIfaces) { + assertTrue("iface " + tether.getInterface() + + " seen by new callback but not old callback", + ifaces.contains(tether.getInterface())); + } + } + + public void expectTetherableInterfacesChanged(@NonNull final List regexs, + final int type) { + assertNotNull("No expected tetherable ifaces callback", mCurrent.poll(TIMEOUT_MS, + (cv) -> { + if (cv.callbackType != CallbackType.ON_TETHERABLE_IFACES) return false; + final Set interfaces = + (Set) cv.callbackParam; + return getFirstMatchingTetheringInterface(regexs, type, interfaces) != null; + })); + } + + public void expectNoTetheringActive() { + assertNotNull("At least one tethering type unexpectedly active", + mCurrent.poll(TIMEOUT_MS, (cv) -> { + if (cv.callbackType != CallbackType.ON_TETHERED_IFACES) return false; + + return ((Set) cv.callbackParam).isEmpty(); + })); + } + + public TetheringInterface expectTetheredInterfacesChanged( + @NonNull final List regexs, final int type) { + while (true) { + final CallbackValue cv = mCurrent.poll(TIMEOUT_MS, c -> true); + if (cv == null) { + fail("No expected tethered ifaces callback, expected type: " + type); + } + + if (cv.callbackType != CallbackType.ON_TETHERED_IFACES) continue; + + final Set interfaces = + (Set) cv.callbackParam; + + final TetheringInterface iface = + getFirstMatchingTetheringInterface(regexs, type, interfaces); + + if (iface != null) return iface; + } + } + + public void expectCallbackStarted() { + // This method uses its own readhead because it just check whether last tethering status + // is updated after TetheringEventCallback get registered but do not check content + // of received callbacks. Using shared readhead (mCurrent) only when the callbacks the + // method polled is also not necessary for other methods which using shared readhead. + // All of methods using mCurrent is order mattered. + final ArrayTrackRecord.ReadHead history = + mHistory.newReadHead(); + 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 = history.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 TetheringInterface iface) { + assertNotNull("No expected callback", mCurrent.poll(TIMEOUT_MS, (cv) -> { + if (cv.callbackType == CallbackType.ON_ERROR + && iface.equals((TetheringInterface) cv.callbackParam)) { + return true; + } + if (cv.callbackType == CallbackType.ON_TETHERED_IFACES + && ((Set) 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; + } + } + + 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 TetheringInterface startWifiTethering(final TestTetheringEventCallback callback) + throws InterruptedException { + final List wifiRegexs = getWifiTetherableInterfaceRegexps(callback); + + 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(); + + final TetheringInterface iface = + callback.expectTetheredInterfacesChanged(wifiRegexs, TETHERING_WIFI); + + callback.expectOneOfOffloadStatusChanged( + TETHER_HARDWARE_OFFLOAD_STARTED, + TETHER_HARDWARE_OFFLOAD_FAILED); + + return iface; + } + + 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.expectNoTetheringActive(); + 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..52ce83a12d --- /dev/null +++ b/tests/cts/tethering/Android.bp @@ -0,0 +1,98 @@ +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_defaults { + name: "CtsTetheringTestDefaults", + defaults: ["cts_defaults"], + + libs: [ + "android.test.base", + ], + + srcs: [ + "src/**/*.java", + ], + + static_libs: [ + "TetheringCommonTests", + "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, +} + +// Tethering CTS tests that target the latest released SDK. These tests can be installed on release +// devices which has equal or lowner sdk version than target sdk and are useful for qualifying +// mainline modules on release devices. +android_test { + name: "CtsTetheringTestLatestSdk", + defaults: ["CtsTetheringTestDefaults"], + + min_sdk_version: "30", + target_sdk_version: "30", + + static_libs: [ + "TetheringIntegrationTestsLatestSdkLib", + ], + + test_suites: [ + "general-tests", + "mts-tethering", + ], + + test_config_template: "AndroidTestTemplate.xml", + + // Include both the 32 and 64 bit versions + compile_multilib: "both", +} + +// Tethering 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: "CtsTetheringTest", + defaults: ["CtsTetheringTestDefaults"], + + static_libs: [ + "TetheringIntegrationTestsLib", + ], + + // Tag this module as a cts test artifact + test_suites: [ + "cts", + "general-tests", + ], + + test_config_template: "AndroidTestTemplate.xml", + + // 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/AndroidTestTemplate.xml b/tests/cts/tethering/AndroidTestTemplate.xml new file mode 100644 index 0000000000..491b00436c --- /dev/null +++ b/tests/cts/tethering/AndroidTestTemplate.xml @@ -0,0 +1,35 @@ + + + + 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..0a5e506c07 --- /dev/null +++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java @@ -0,0 +1,466 @@ +/* + * 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.isAnyIfaceMatch; + +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.TetheringInterface; +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 isAnyIfaceMatch(ifaceRegexs, state.mActive); + } + + private void assertNoErroredIfaces(final TetherState state, final String[] ifaceRegexs) { + if (state == null || state.mErrored == null) return; + + if (isAnyIfaceMatch(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); + tetherEventCallback.expectNoTetheringActive(); + + final TetheringInterface tetheredIface = + mCtsTetheringUtils.startWifiTethering(tetherEventCallback); + + assertNotNull(tetheredIface); + final String wifiTetheringIface = tetheredIface.getInterface(); + + 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( + new TetheringInterface(TETHERING_WIFI, 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.expectNoTetheringActive(); + } 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); + tetherEventCallback.expectNoTetheringActive(); + + 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(); + } + } + } +} diff --git a/tests/deflake/Android.bp b/tests/deflake/Android.bp index 58ece37ef6..806f805dd3 100644 --- a/tests/deflake/Android.bp +++ b/tests/deflake/Android.bp @@ -16,11 +16,7 @@ package { // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], + default_applicable_licenses: ["Android-Apache-2.0"], } java_test_host { diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp index 39c424e31f..81f9b4879e 100644 --- a/tests/integration/Android.bp +++ b/tests/integration/Android.bp @@ -16,11 +16,7 @@ package { // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], + default_applicable_licenses: ["Android-Apache-2.0"], } android_test { diff --git a/tests/smoketest/Android.bp b/tests/smoketest/Android.bp index 1535f3ddcb..8011540293 100644 --- a/tests/smoketest/Android.bp +++ b/tests/smoketest/Android.bp @@ -11,11 +11,7 @@ // dependent libraries. package { // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], + default_applicable_licenses: ["Android-Apache-2.0"], } android_test { diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp index 6f503ace03..beae0cf7a3 100644 --- a/tests/unit/Android.bp +++ b/tests/unit/Android.bp @@ -4,10 +4,10 @@ package { // See: http://go/android-license-faq // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" + // all of the 'license_kinds' from "Android-Apache-2.0" // to get the below license kinds: // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], + default_applicable_licenses: ["Android-Apache-2.0"], } java_defaults { diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java index c75618f43c..c6e7606135 100644 --- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java +++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java @@ -30,7 +30,7 @@ import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED; import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_REQUIRED; import static android.content.pm.PackageManager.GET_PERMISSIONS; import static android.content.pm.PackageManager.MATCH_ANY_USER; -import static android.net.ConnectivitySettingsManager.APPS_ALLOWED_ON_RESTRICTED_NETWORKS; +import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS; import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; import static android.os.Process.SYSTEM_UID; @@ -142,7 +142,7 @@ public class PermissionMonitorTest { final Context asUserCtx = mock(Context.class, AdditionalAnswers.delegatesTo(mContext)); doReturn(UserHandle.ALL).when(asUserCtx).getUser(); when(mContext.createContextAsUser(eq(UserHandle.ALL), anyInt())).thenReturn(asUserCtx); - when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>()); + when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>()); mPermissionMonitor = spy(new PermissionMonitor(mContext, mNetdService, mDeps)); @@ -341,9 +341,9 @@ public class PermissionMonitorTest { } @Test - public void testHasRestrictedNetworkPermissionAppAllowedOnRestrictedNetworks() { - mPermissionMonitor.updateAppsAllowedOnRestrictedNetworks( - new ArraySet<>(new String[] { MOCK_PACKAGE1 })); + public void testHasRestrictedNetworkPermissionUidAllowedOnRestrictedNetworks() { + mPermissionMonitor.updateUidsAllowedOnRestrictedNetworks( + new ArraySet<>(new Integer[] { MOCK_UID1 })); assertTrue(hasRestrictedNetworkPermission( PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE1, MOCK_UID1)); assertTrue(hasRestrictedNetworkPermission( @@ -352,11 +352,11 @@ public class PermissionMonitorTest { PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE1, MOCK_UID1, CONNECTIVITY_INTERNAL)); assertFalse(hasRestrictedNetworkPermission( - PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE2, MOCK_UID1)); + PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE2, MOCK_UID2)); assertFalse(hasRestrictedNetworkPermission( - PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE2, MOCK_UID1, CHANGE_NETWORK_STATE)); + PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE2, MOCK_UID2, CHANGE_NETWORK_STATE)); assertFalse(hasRestrictedNetworkPermission( - PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE2, MOCK_UID1, CONNECTIVITY_INTERNAL)); + PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE2, MOCK_UID2, CONNECTIVITY_INTERNAL)); } @@ -396,32 +396,32 @@ public class PermissionMonitorTest { assertFalse(wouldBeCarryoverPackage(PARTITION_PRODUCT, VERSION_Q, MOCK_UID1)); } - private boolean wouldBeAppAllowedOnRestrictedNetworks(String packageName) { - final PackageInfo packageInfo = new PackageInfo(); - packageInfo.packageName = packageName; - return mPermissionMonitor.isAppAllowedOnRestrictedNetworks(packageInfo); + private boolean wouldBeUidAllowedOnRestrictedNetworks(int uid) { + final ApplicationInfo applicationInfo = new ApplicationInfo(); + applicationInfo.uid = uid; + return mPermissionMonitor.isUidAllowedOnRestrictedNetworks(applicationInfo); } @Test public void testIsAppAllowedOnRestrictedNetworks() { - mPermissionMonitor.updateAppsAllowedOnRestrictedNetworks(new ArraySet<>()); - assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE1)); - assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE2)); + mPermissionMonitor.updateUidsAllowedOnRestrictedNetworks(new ArraySet<>()); + assertFalse(wouldBeUidAllowedOnRestrictedNetworks(MOCK_UID1)); + assertFalse(wouldBeUidAllowedOnRestrictedNetworks(MOCK_UID2)); - mPermissionMonitor.updateAppsAllowedOnRestrictedNetworks( - new ArraySet<>(new String[] { MOCK_PACKAGE1 })); - assertTrue(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE1)); - assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE2)); + mPermissionMonitor.updateUidsAllowedOnRestrictedNetworks( + new ArraySet<>(new Integer[] { MOCK_UID1 })); + assertTrue(wouldBeUidAllowedOnRestrictedNetworks(MOCK_UID1)); + assertFalse(wouldBeUidAllowedOnRestrictedNetworks(MOCK_UID2)); - mPermissionMonitor.updateAppsAllowedOnRestrictedNetworks( - new ArraySet<>(new String[] { MOCK_PACKAGE2 })); - assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE1)); - assertTrue(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE2)); + mPermissionMonitor.updateUidsAllowedOnRestrictedNetworks( + new ArraySet<>(new Integer[] { MOCK_UID2 })); + assertFalse(wouldBeUidAllowedOnRestrictedNetworks(MOCK_UID1)); + assertTrue(wouldBeUidAllowedOnRestrictedNetworks(MOCK_UID2)); - mPermissionMonitor.updateAppsAllowedOnRestrictedNetworks( - new ArraySet<>(new String[] { "com.android.test" })); - assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE1)); - assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE2)); + mPermissionMonitor.updateUidsAllowedOnRestrictedNetworks( + new ArraySet<>(new Integer[] { 123 })); + assertFalse(wouldBeUidAllowedOnRestrictedNetworks(MOCK_UID1)); + assertFalse(wouldBeUidAllowedOnRestrictedNetworks(MOCK_UID2)); } private void assertBackgroundPermission(boolean hasPermission, String name, int uid, @@ -901,12 +901,12 @@ public class PermissionMonitorTest { } @Test - public void testAppsAllowedOnRestrictedNetworksChanged() throws Exception { + public void testUidsAllowedOnRestrictedNetworksChanged() throws Exception { final NetdMonitor mNetdMonitor = new NetdMonitor(mNetdService); final ArgumentCaptor captor = ArgumentCaptor.forClass(ContentObserver.class); verify(mDeps, times(1)).registerContentObserver(any(), - argThat(uri -> uri.getEncodedPath().contains(APPS_ALLOWED_ON_RESTRICTED_NETWORKS)), + argThat(uri -> uri.getEncodedPath().contains(UIDS_ALLOWED_ON_RESTRICTED_NETWORKS)), anyBoolean(), captor.capture()); final ContentObserver contentObserver = captor.getValue(); @@ -924,24 +924,24 @@ public class PermissionMonitorTest { when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE2), anyInt())).thenReturn(packageInfo2); when(mPackageManager.getPackagesForUid(MOCK_UID2)).thenReturn(new String[]{MOCK_PACKAGE2}); - // MOCK_PACKAGE1 is listed in setting that allow to use restricted networks, MOCK_UID1 + // MOCK_UID1 is listed in setting that allow to use restricted networks, MOCK_UID1 // should have SYSTEM permission. - when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn( - new ArraySet<>(new String[] { MOCK_PACKAGE1 })); + when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn( + new ArraySet<>(new Integer[] { MOCK_UID1 })); contentObserver.onChange(true /* selfChange */); mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1}); mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID2}); - // MOCK_PACKAGE2 is listed in setting that allow to use restricted networks, MOCK_UID2 + // MOCK_UID2 is listed in setting that allow to use restricted networks, MOCK_UID2 // should have SYSTEM permission but MOCK_UID1 should revoke permission. - when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn( - new ArraySet<>(new String[] { MOCK_PACKAGE2 })); + when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn( + new ArraySet<>(new Integer[] { MOCK_UID2 })); contentObserver.onChange(true /* selfChange */); mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID2}); mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1}); - // No app lists in setting, should revoke permission from all uids. - when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>()); + // No uid lists in setting, should revoke permission from all uids. + when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>()); contentObserver.onChange(true /* selfChange */); mNetdMonitor.expectNoPermission( new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1, MOCK_UID2}); @@ -953,7 +953,7 @@ public class PermissionMonitorTest { final ArgumentCaptor captor = ArgumentCaptor.forClass(ContentObserver.class); verify(mDeps, times(1)).registerContentObserver(any(), - argThat(uri -> uri.getEncodedPath().contains(APPS_ALLOWED_ON_RESTRICTED_NETWORKS)), + argThat(uri -> uri.getEncodedPath().contains(UIDS_ALLOWED_ON_RESTRICTED_NETWORKS)), anyBoolean(), captor.capture()); final ContentObserver contentObserver = captor.getValue(); @@ -974,22 +974,15 @@ public class PermissionMonitorTest { addPackageForUsers(new UserHandle[]{MOCK_USER1}, MOCK_PACKAGE1, MOCK_UID1); mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1}); - // MOCK_PACKAGE2 is listed in setting that allow to use restricted networks, MOCK_UID1 + // MOCK_UID1 is listed in setting that allow to use restricted networks, MOCK_UID1 // should upgrade to SYSTEM permission. - when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn( - new ArraySet<>(new String[] { MOCK_PACKAGE2 })); - contentObserver.onChange(true /* selfChange */); - mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1}); - - // MOCK_PACKAGE1 is listed in setting that allow to use restricted networks, MOCK_UID1 - // should still have SYSTEM permission. - when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn( - new ArraySet<>(new String[] { MOCK_PACKAGE1 })); + when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn( + new ArraySet<>(new Integer[] { MOCK_UID1 })); contentObserver.onChange(true /* selfChange */); mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1}); // No app lists in setting, MOCK_UID1 should downgrade to NETWORK permission. - when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>()); + when(mDeps.getUidsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>()); contentObserver.onChange(true /* selfChange */); mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1}); @@ -998,4 +991,4 @@ public class PermissionMonitorTest { removePackageForUsers(new UserHandle[]{MOCK_USER1}, MOCK_PACKAGE1, MOCK_UID1); mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1}); } -} \ No newline at end of file +} diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp index 22a04f5c09..1c1ba9e125 100644 --- a/tests/unit/jni/Android.bp +++ b/tests/unit/jni/Android.bp @@ -1,10 +1,6 @@ package { // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], + default_applicable_licenses: ["Android-Apache-2.0"], } cc_library_shared {