diff options
48 files changed, 5290 insertions, 145 deletions
diff --git a/TEST_MAPPING b/TEST_MAPPING index a5b97a1c47..7fa4b7f178 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -3,6 +3,10 @@ { "name": "ConnectivityCoverageTests" }, + { + // In addition to ConnectivityCoverageTests, runs non-connectivity-module tests + "name": "FrameworksNetTests" + }, // Run in addition to mainline-presubmit as mainline-presubmit is not // supported in every branch. // CtsNetTestCasesLatestSdk uses stable API shims, so does not exercise @@ -22,6 +26,9 @@ "name": "bpf_existence_test" }, { + "name": "connectivity_native_test" + }, + { "name": "netd_updatable_unit_test" }, { @@ -78,6 +85,9 @@ "name": "bpf_existence_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]" }, { + "name": "connectivity_native_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]" + }, + { "name": "netd_updatable_unit_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]" }, { diff --git a/Tethering/Android.bp b/Tethering/Android.bp index 41a0651534..29f6e126a5 100644 --- a/Tethering/Android.bp +++ b/Tethering/Android.bp @@ -47,6 +47,7 @@ java_defaults { "net-utils-device-common-bpf", "net-utils-device-common-netlink", "netd-client", + "tetheringstatsprotos", ], libs: [ "framework-connectivity", @@ -211,3 +212,14 @@ sdk { bootclasspath_fragments: ["com.android.tethering-bootclasspath-fragment"], systemserverclasspath_fragments: ["com.android.tethering-systemserverclasspath-fragment"], } + +java_library_static { + name: "tetheringstatsprotos", + proto: {type: "lite"}, + srcs: [ + "src/com/android/networkstack/tethering/metrics/stats.proto", + ], + static_libs: ["tetheringprotos"], + apex_available: ["com.android.tethering"], + min_sdk_version: "30", +} diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp index dd04d6c8a9..ac777d73ac 100644 --- a/Tethering/apex/Android.bp +++ b/Tethering/apex/Android.bp @@ -69,9 +69,10 @@ apex { ], canned_fs_config: "canned_fs_config", bpfs: [ + "block.o", "clatd.o_mainline", - "netd.o_mainline", "dscp_policy.o", + "netd.o_mainline", "offload.o", "test.o", ], @@ -131,15 +132,41 @@ bootclasspath_fragment { hidden_api: { max_target_r_low_priority: [ "hiddenapi/hiddenapi-max-target-r-loprio.txt", - ], + ], max_target_o_low_priority: [ "hiddenapi/hiddenapi-max-target-o-low-priority.txt", "hiddenapi/hiddenapi-max-target-o-low-priority-tiramisu.txt", - ], + ], unsupported: [ "hiddenapi/hiddenapi-unsupported.txt", "hiddenapi/hiddenapi-unsupported-tiramisu.txt", ], + + // The following packages contain classes from other modules on the + // bootclasspath. That means that the hidden API flags for this module + // has to explicitly list every single class this module provides in + // that package to differentiate them from the classes provided by other + // modules. That can include private classes that are not part of the + // API. + split_packages: [ + "android.app.usage", + "android.net", + "android.net.netstats", + "android.net.util", + ], + + // The following packages and all their subpackages currently only + // contain classes from this bootclasspath_fragment. Listing a package + // here won't prevent other bootclasspath modules from adding classes in + // any of those packages but it will prevent them from adding those + // classes into an API surface, e.g. public, system, etc.. Doing so will + // result in a build failure due to inconsistent flags. + package_prefixes: [ + "android.net.apf", + "android.net.connectivity", + "android.net.netstats.provider", + "android.net.nsd", + ], }, } diff --git a/Tethering/jarjar-rules.txt b/Tethering/jarjar-rules.txt index 5de4b97b76..40eed3f4fc 100644 --- a/Tethering/jarjar-rules.txt +++ b/Tethering/jarjar-rules.txt @@ -12,3 +12,5 @@ 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 + +rule com.google.protobuf.** com.android.networkstack.tethering.protobuf@1
\ No newline at end of file diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags index 6735317ec7..7b5ae0dfbe 100644 --- a/Tethering/proguard.flags +++ b/Tethering/proguard.flags @@ -19,3 +19,9 @@ -keepclassmembers class android.net.ip.IpServer { static final int CMD_*; } + +# The lite proto runtime uses reflection to access fields based on the names in +# the schema, keep all the fields. +-keepclassmembers class * extends com.android.networkstack.tethering.protobuf.MessageLite { + <fields>; +}
\ No newline at end of file diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java index 225bd5830e..f8a10941b2 100644 --- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java +++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java @@ -2033,5 +2033,13 @@ public class BpfCoordinator { return mBpfConntrackEventConsumer; } + // Return tethering client information. This is used for testing only. + @NonNull + @VisibleForTesting + final HashMap<IpServer, HashMap<Inet4Address, ClientInfo>> + getTetherClientsForTesting() { + return mTetherClients; + } + private static native String[] getBpfCounterNames(); } diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/stats.proto b/Tethering/src/com/android/networkstack/tethering/metrics/stats.proto new file mode 100644 index 0000000000..46a47af83a --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/metrics/stats.proto @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto2"; +option java_multiple_files = true; + +package com.android.networkstack.tethering.metrics; + +import "frameworks/proto_logging/stats/enums/stats/connectivity/tethering.proto"; + +/** + * Logs Tethering events + */ +message NetworkTetheringReported { + optional .android.stats.connectivity.ErrorCode error_code = 1; + optional .android.stats.connectivity.DownstreamType downstream_type = 2; + optional .android.stats.connectivity.UpstreamType upstream_type = 3; + optional .android.stats.connectivity.UserType user_type = 4; +} diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java index 179fc8af01..4967d27683 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java @@ -224,8 +224,6 @@ public class BpfCoordinatorTest { private int mSrcPort = PRIVATE_PORT; private int mDstPort = REMOTE_PORT; - Builder() {} - public Builder setProto(int proto) { if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) { fail("Not support protocol " + proto); @@ -250,8 +248,6 @@ public class BpfCoordinatorTest { private int mSrcPort = REMOTE_PORT; private int mDstPort = PUBLIC_PORT; - Builder() {} - public Builder setProto(int proto) { if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) { fail("Not support protocol " + proto); @@ -279,8 +275,6 @@ public class BpfCoordinatorTest { private int mDstPort = REMOTE_PORT; private long mLastUsed = 0; - Builder() {} - public Tether4Value build() { return new Tether4Value(mOif, mEthDstMac, mEthSrcMac, mEthProto, mPmtu, mSrc46, mDst46, mSrcPort, mDstPort, mLastUsed); @@ -301,8 +295,6 @@ public class BpfCoordinatorTest { private int mDstPort = PRIVATE_PORT; private long mLastUsed = 0; - Builder() {} - public Tether4Value build() { return new Tether4Value(mOif, mEthDstMac, mEthSrcMac, mEthProto, mPmtu, mSrc46, mDst46, mSrcPort, mDstPort, mLastUsed); @@ -321,8 +313,6 @@ public class BpfCoordinatorTest { private short mPublicPort = PUBLIC_PORT; private short mRemotePort = REMOTE_PORT; - Builder() {} - public Builder setMsgType(short msgType) { if (msgType != IPCTNL_MSG_CT_NEW && msgType != IPCTNL_MSG_CT_DELETE) { fail("Not support message type " + msgType); @@ -377,6 +367,7 @@ public class BpfCoordinatorTest { // 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 HashMap<IpServer, HashMap<Inet4Address, ClientInfo>> mTetherClients; private long mElapsedRealtimeNanos = 0; private final ArgumentCaptor<ArrayList> mStringArrayCaptor = @@ -482,6 +473,8 @@ public class BpfCoordinatorTest { final BpfCoordinator coordinator = new BpfCoordinator(mDeps); mConsumer = coordinator.getBpfConntrackEventConsumerForTesting(); + mTetherClients = coordinator.getTetherClientsForTesting(); + final ArgumentCaptor<BpfCoordinator.BpfTetherStatsProvider> tetherStatsProviderCaptor = ArgumentCaptor.forClass(BpfCoordinator.BpfTetherStatsProvider.class); @@ -1793,4 +1786,280 @@ public class BpfCoordinatorTest { clearInvocations(mBpfUpstream4Map, mBpfDownstream4Map); } } + + // Test network topology: + // + // public network UE private network + // | / \ | + // +------------+ V +-------------+ +--------------+ V +------------+ + // | Sever +------+ Upstream |+------+-----+ Downstream 1 +-----+ Client A | + // +------------+ +-------------+| | +--------------+ +------------+ + // remote ip +-------------+ | private ip + // 140.112.8.116:443 public ip | 192.168.80.12:62449 + // (upstream 1, rawip) | + // 1.0.0.1:62449 | + // 1.0.0.1:62450 | +--------------+ +------------+ + // - or - +-----+ Downstream 2 +-----+ Client B | + // (upstream 2, ether) +--------------+ +------------+ + // private ip + // 192.168.90.12:62450 + // + // Build two test rule sets which include BPF upstream and downstream rules. + // + // Rule set A: a socket connection from client A to remote server via the first upstream + // (UPSTREAM_IFINDEX). + // 192.168.80.12:62449 -> 1.0.0.1:62449 -> 140.112.8.116:443 + // Rule set B: a socket connection from client B to remote server via the first upstream + // (UPSTREAM_IFINDEX). + // 192.168.80.12:62450 -> 1.0.0.1:62450 -> 140.112.8.116:443 + // + // The second upstream (UPSTREAM_IFINDEX2) is an ethernet interface which is not supported by + // BPF. Used for testing the rule adding and removing on an unsupported upstream interface. + // + private static final Tether4Key UPSTREAM4_RULE_KEY_A = makeUpstream4Key( + DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, PRIVATE_ADDR, PRIVATE_PORT); + private static final Tether4Value UPSTREAM4_RULE_VALUE_A = makeUpstream4Value(PUBLIC_PORT); + private static final Tether4Key DOWNSTREAM4_RULE_KEY_A = makeDownstream4Key(PUBLIC_PORT); + private static final Tether4Value DOWNSTREAM4_RULE_VALUE_A = makeDownstream4Value( + DOWNSTREAM_IFINDEX, MAC_A, DOWNSTREAM_MAC, PRIVATE_ADDR, PRIVATE_PORT); + + private static final Tether4Key UPSTREAM4_RULE_KEY_B = makeUpstream4Key( + DOWNSTREAM_IFINDEX2, DOWNSTREAM_MAC2, PRIVATE_ADDR2, PRIVATE_PORT2); + private static final Tether4Value UPSTREAM4_RULE_VALUE_B = makeUpstream4Value(PUBLIC_PORT2); + private static final Tether4Key DOWNSTREAM4_RULE_KEY_B = makeDownstream4Key(PUBLIC_PORT2); + private static final Tether4Value DOWNSTREAM4_RULE_VALUE_B = makeDownstream4Value( + DOWNSTREAM_IFINDEX2, MAC_B, DOWNSTREAM_MAC2, PRIVATE_ADDR2, PRIVATE_PORT2); + + private static final ConntrackEvent CONNTRACK_EVENT_A = makeTestConntrackEvent( + PUBLIC_PORT, PRIVATE_ADDR, PRIVATE_PORT); + + private static final ConntrackEvent CONNTRACK_EVENT_B = makeTestConntrackEvent( + PUBLIC_PORT2, PRIVATE_ADDR2, PRIVATE_PORT2); + + @NonNull + private static Tether4Key makeUpstream4Key(final int downstreamIfindex, + @NonNull final MacAddress downstreamMac, @NonNull final Inet4Address privateAddr, + final short privatePort) { + return new Tether4Key(downstreamIfindex, downstreamMac, (short) IPPROTO_TCP, + privateAddr.getAddress(), REMOTE_ADDR.getAddress(), privatePort, REMOTE_PORT); + } + + @NonNull + private static Tether4Key makeDownstream4Key(final short publicPort) { + return new Tether4Key(UPSTREAM_IFINDEX, MacAddress.ALL_ZEROS_ADDRESS /* dstMac (rawip) */, + (short) IPPROTO_TCP, REMOTE_ADDR.getAddress(), PUBLIC_ADDR.getAddress(), + REMOTE_PORT, publicPort); + } + + @NonNull + private static Tether4Value makeUpstream4Value(final short publicPort) { + return new Tether4Value(UPSTREAM_IFINDEX, + MacAddress.ALL_ZEROS_ADDRESS /* ethDstMac (rawip) */, + MacAddress.ALL_ZEROS_ADDRESS /* ethSrcMac (rawip) */, ETH_P_IP, + NetworkStackConstants.ETHER_MTU, toIpv4MappedAddressBytes(PUBLIC_ADDR), + toIpv4MappedAddressBytes(REMOTE_ADDR), publicPort, REMOTE_PORT, + 0 /* lastUsed */); + } + + @NonNull + private static Tether4Value makeDownstream4Value(final int downstreamIfindex, + @NonNull final MacAddress clientMac, @NonNull final MacAddress downstreamMac, + @NonNull final Inet4Address privateAddr, final short privatePort) { + return new Tether4Value(downstreamIfindex, clientMac, downstreamMac, + ETH_P_IP, NetworkStackConstants.ETHER_MTU, toIpv4MappedAddressBytes(REMOTE_ADDR), + toIpv4MappedAddressBytes(privateAddr), REMOTE_PORT, privatePort, 0 /* lastUsed */); + } + + @NonNull + private static ConntrackEvent makeTestConntrackEvent(final short publicPort, + @NonNull final Inet4Address privateAddr, final short privatePort) { + return new ConntrackEvent( + (short) (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8 | IPCTNL_MSG_CT_NEW), + new Tuple(new TupleIpv4(privateAddr, REMOTE_ADDR), + new TupleProto((byte) IPPROTO_TCP, privatePort, REMOTE_PORT)), + new Tuple(new TupleIpv4(REMOTE_ADDR, PUBLIC_ADDR), + new TupleProto((byte) IPPROTO_TCP, REMOTE_PORT, publicPort)), + ESTABLISHED_MASK, + 100 /* nonzero, CT_NEW */); + } + + void checkRule4ExistInUpstreamDownstreamMap() throws Exception { + assertEquals(UPSTREAM4_RULE_VALUE_A, mBpfUpstream4Map.getValue(UPSTREAM4_RULE_KEY_A)); + assertEquals(DOWNSTREAM4_RULE_VALUE_A, mBpfDownstream4Map.getValue( + DOWNSTREAM4_RULE_KEY_A)); + assertEquals(UPSTREAM4_RULE_VALUE_B, mBpfUpstream4Map.getValue(UPSTREAM4_RULE_KEY_B)); + assertEquals(DOWNSTREAM4_RULE_VALUE_B, mBpfDownstream4Map.getValue( + DOWNSTREAM4_RULE_KEY_B)); + } + + void checkRule4NotExistInUpstreamDownstreamMap() throws Exception { + assertNull(mBpfUpstream4Map.getValue(UPSTREAM4_RULE_KEY_A)); + assertNull(mBpfDownstream4Map.getValue(DOWNSTREAM4_RULE_KEY_A)); + assertNull(mBpfUpstream4Map.getValue(UPSTREAM4_RULE_KEY_B)); + assertNull(mBpfDownstream4Map.getValue(DOWNSTREAM4_RULE_KEY_B)); + } + + // Both #addDownstreamAndClientInformationTo and #setUpstreamInformationTo need to be called + // before this function because upstream and downstream information are required to build + // the rules while conntrack event is received. + void addAndCheckRule4ForDownstreams() throws Exception { + // Add rule set A which is on the first downstream and rule set B which is on the second + // downstream. + mConsumer.accept(CONNTRACK_EVENT_A); + mConsumer.accept(CONNTRACK_EVENT_B); + + // Check that both rule set A and B were added. + checkRule4ExistInUpstreamDownstreamMap(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testTetherOffloadRule4Clear_RemoveDownstream() throws Exception { + final BpfCoordinator coordinator = makeBpfCoordinator(); + + // Initialize upstream and downstream information manually but calling the setup helper + // #initBpfCoordinatorForRule4 because this test needs to {update, remove} upstream and + // downstream manually for testing. + addDownstreamAndClientInformationTo(coordinator, DOWNSTREAM_IFINDEX); + addDownstreamAndClientInformationTo(coordinator, DOWNSTREAM_IFINDEX2); + + setUpstreamInformationTo(coordinator, UPSTREAM_IFINDEX); + addAndCheckRule4ForDownstreams(); + + // [1] Remove the first downstream. Remove only the rule set A which is on the first + // downstream. + coordinator.tetherOffloadClientClear(mIpServer); + assertNull(mBpfUpstream4Map.getValue(UPSTREAM4_RULE_KEY_A)); + assertNull(mBpfDownstream4Map.getValue(DOWNSTREAM4_RULE_KEY_A)); + assertEquals(UPSTREAM4_RULE_VALUE_B, mBpfUpstream4Map.getValue( + UPSTREAM4_RULE_KEY_B)); + assertEquals(DOWNSTREAM4_RULE_VALUE_B, mBpfDownstream4Map.getValue( + DOWNSTREAM4_RULE_KEY_B)); + + // Clear client information for the first downstream only. + assertNull(mTetherClients.get(mIpServer)); + assertNotNull(mTetherClients.get(mIpServer2)); + + // [2] Remove the second downstream. Remove the rule set B which is on the second + // downstream. + coordinator.tetherOffloadClientClear(mIpServer2); + assertNull(mBpfUpstream4Map.getValue(UPSTREAM4_RULE_KEY_B)); + assertNull(mBpfDownstream4Map.getValue(DOWNSTREAM4_RULE_KEY_B)); + + // Clear client information for the second downstream. + assertNull(mTetherClients.get(mIpServer2)); + } + + private void asseertClientInfoExist(@NonNull IpServer ipServer, + @NonNull ClientInfo clientInfo) { + HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer); + assertNotNull(clients); + assertEquals(clientInfo, clients.get(clientInfo.clientAddress)); + } + + // Although either ClientInfo for a given downstream (IpServer) is not found or a given + // client address is not found on a given downstream can be treated "ClientInfo not + // exist", we still want to know the real reason exactly. For example, we don't the + // exact reason in the following: + // assertNull(clients == null ? clients : clients.get(clientInfo.clientAddress)); + // This helper only verifies the case that the downstream still has at least one client. + // In other words, ClientInfo for a given IpServer has not been removed yet. + private void asseertClientInfoNotExist(@NonNull IpServer ipServer, + @NonNull ClientInfo clientInfo) { + HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer); + assertNotNull(clients); + assertNull(clients.get(clientInfo.clientAddress)); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testTetherOffloadRule4Clear_ChangeOrRemoveUpstream() throws Exception { + final BpfCoordinator coordinator = makeBpfCoordinator(); + + // Initialize upstream and downstream information manually but calling the helper + // #initBpfCoordinatorForRule4 because this test needs to {update, remove} upstream and + // downstream. + addDownstreamAndClientInformationTo(coordinator, DOWNSTREAM_IFINDEX); + addDownstreamAndClientInformationTo(coordinator, DOWNSTREAM_IFINDEX2); + + setUpstreamInformationTo(coordinator, UPSTREAM_IFINDEX); + addAndCheckRule4ForDownstreams(); + + // [1] Update the same upstream state. Nothing happens. + setUpstreamInformationTo(coordinator, UPSTREAM_IFINDEX); + checkRule4ExistInUpstreamDownstreamMap(); + + // [2] Switch upstream interface from the first upstream (rawip, bpf supported) to + // the second upstream (ethernet, bpf not supported). Clear all rules. + setUpstreamInformationTo(coordinator, UPSTREAM_IFINDEX2); + checkRule4NotExistInUpstreamDownstreamMap(); + + // Setup the upstream interface information and the rules for next test. + setUpstreamInformationTo(coordinator, UPSTREAM_IFINDEX); + addAndCheckRule4ForDownstreams(); + + // [3] Switch upstream from the first upstream (rawip, bpf supported) to no upstream. Clear + // all rules. + setUpstreamInformationTo(coordinator, INVALID_IFINDEX); + checkRule4NotExistInUpstreamDownstreamMap(); + + // Client information should be not deleted. + asseertClientInfoExist(mIpServer, CLIENT_INFO_A); + asseertClientInfoExist(mIpServer2, CLIENT_INFO_B); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testTetherOffloadClientAddRemove() throws Exception { + final BpfCoordinator coordinator = makeBpfCoordinator(); + + // [1] Add client information A and B on on the same downstream. + final ClientInfo clientA = new ClientInfo(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, + PRIVATE_ADDR, MAC_A); + final ClientInfo clientB = new ClientInfo(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, + PRIVATE_ADDR2, MAC_B); + coordinator.tetherOffloadClientAdd(mIpServer, clientA); + coordinator.tetherOffloadClientAdd(mIpServer, clientB); + asseertClientInfoExist(mIpServer, clientA); + asseertClientInfoExist(mIpServer, clientB); + + // Add the rules for client A and client B. + final Tether4Key upstream4KeyA = makeUpstream4Key( + DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, PRIVATE_ADDR, PRIVATE_PORT); + final Tether4Value upstream4ValueA = makeUpstream4Value(PUBLIC_PORT); + final Tether4Key downstream4KeyA = makeDownstream4Key(PUBLIC_PORT); + final Tether4Value downstream4ValueA = makeDownstream4Value( + DOWNSTREAM_IFINDEX, MAC_A, DOWNSTREAM_MAC, PRIVATE_ADDR, PRIVATE_PORT); + final Tether4Key upstream4KeyB = makeUpstream4Key( + DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC2, PRIVATE_ADDR2, PRIVATE_PORT2); + final Tether4Value upstream4ValueB = makeUpstream4Value(PUBLIC_PORT2); + final Tether4Key downstream4KeyB = makeDownstream4Key(PUBLIC_PORT2); + final Tether4Value downstream4ValueB = makeDownstream4Value( + DOWNSTREAM_IFINDEX, MAC_B, DOWNSTREAM_MAC2, PRIVATE_ADDR2, PRIVATE_PORT2); + + mBpfUpstream4Map.insertEntry(upstream4KeyA, upstream4ValueA); + mBpfDownstream4Map.insertEntry(downstream4KeyA, downstream4ValueA); + mBpfUpstream4Map.insertEntry(upstream4KeyB, upstream4ValueB); + mBpfDownstream4Map.insertEntry(downstream4KeyB, downstream4ValueB); + + // [2] Remove client information A. Only the rules on client A should be removed and + // the rules on client B should exist. + coordinator.tetherOffloadClientRemove(mIpServer, clientA); + asseertClientInfoNotExist(mIpServer, clientA); + asseertClientInfoExist(mIpServer, clientB); + assertNull(mBpfUpstream4Map.getValue(upstream4KeyA)); + assertNull(mBpfDownstream4Map.getValue(downstream4KeyA)); + assertEquals(upstream4ValueB, mBpfUpstream4Map.getValue(upstream4KeyB)); + assertEquals(downstream4ValueB, mBpfDownstream4Map.getValue(downstream4KeyB)); + + // [3] Remove client information B. The rules on client B should be removed. + // Exactly, ClientInfo for a given IpServer is removed because the last client B + // has been removed from the downstream. Can't use the helper #asseertClientInfoExist + // to check because the container ClientInfo for a given downstream has been removed. + // See #asseertClientInfoExist. + coordinator.tetherOffloadClientRemove(mIpServer, clientB); + assertNull(mTetherClients.get(mIpServer)); + assertNull(mBpfUpstream4Map.getValue(upstream4KeyB)); + assertNull(mBpfDownstream4Map.getValue(downstream4KeyB)); + } } diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp index 6718402652..1fe0e9acc0 100644 --- a/bpf_progs/Android.bp +++ b/bpf_progs/Android.bp @@ -47,6 +47,7 @@ cc_library_headers { "//packages/modules/Connectivity/service/native/libs/libclat", "//packages/modules/Connectivity/Tethering", "//packages/modules/Connectivity/service/native", + "//packages/modules/Connectivity/tests/native", "//packages/modules/Connectivity/service-t/native/libs/libnetworkstats", "//packages/modules/Connectivity/tests/unit/jni", "//system/netd/server", @@ -58,6 +59,16 @@ cc_library_headers { // bpf kernel programs // bpf { + name: "block.o", + srcs: ["block.c"], + cflags: [ + "-Wall", + "-Werror", + ], + sub_dir: "net_shared", +} + +bpf { name: "dscp_policy.o", srcs: ["dscp_policy.c"], cflags: [ diff --git a/bpf_progs/block.c b/bpf_progs/block.c new file mode 100644 index 0000000000..ddd9a1cbed --- /dev/null +++ b/bpf_progs/block.c @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 <linux/types.h> +#include <linux/bpf.h> +#include <netinet/in.h> +#include <stdint.h> + +#include "bpf_helpers.h" + +#define ALLOW 1 +#define DISALLOW 0 + +DEFINE_BPF_MAP_GRW(blocked_ports_map, ARRAY, int, uint64_t, + 1024 /* 64K ports -> 1024 u64s */, AID_SYSTEM) + +static inline __always_inline int block_port(struct bpf_sock_addr *ctx) { + if (!ctx->user_port) return ALLOW; + + switch (ctx->protocol) { + case IPPROTO_TCP: + case IPPROTO_MPTCP: + case IPPROTO_UDP: + case IPPROTO_UDPLITE: + case IPPROTO_DCCP: + case IPPROTO_SCTP: + break; + default: + return ALLOW; // unknown protocols are allowed + } + + int key = ctx->user_port >> 6; + int shift = ctx->user_port & 63; + + uint64_t *val = bpf_blocked_ports_map_lookup_elem(&key); + // Lookup should never fail in reality, but if it does return here to keep the + // BPF verifier happy. + if (!val) return ALLOW; + + if ((*val >> shift) & 1) return DISALLOW; + return ALLOW; +} + +DEFINE_BPF_PROG_KVER("bind4/block_port", AID_ROOT, AID_SYSTEM, + bind4_block_port, KVER(5, 4, 0)) +(struct bpf_sock_addr *ctx) { + return block_port(ctx); +} + +DEFINE_BPF_PROG_KVER("bind6/block_port", AID_ROOT, AID_SYSTEM, + bind6_block_port, KVER(5, 4, 0)) +(struct bpf_sock_addr *ctx) { + return block_port(ctx); +} + +LICENSE("Apache 2.0"); +CRITICAL("ConnectivityNative"); diff --git a/framework-t/src/android/net/EthernetManager.java b/framework-t/src/android/net/EthernetManager.java index e02ea897db..2b76dd935f 100644 --- a/framework-t/src/android/net/EthernetManager.java +++ b/framework-t/src/android/net/EthernetManager.java @@ -541,8 +541,7 @@ public class EthernetManager { * Similarly, use {@link NetworkCapabilities.Builder} to build a {@code NetworkCapabilities} * object for this network to put inside the {@code request}. * - * This function accepts an {@link OutcomeReceiver} that is called once the operation has - * finished execution. + * The provided {@link OutcomeReceiver} is called once the operation has finished execution. * * @param iface the name of the interface to act upon. * @param request the {@link EthernetNetworkUpdateRequest} used to set an ethernet network's @@ -554,7 +553,8 @@ public class EthernetManager { * information about the error. * @throws SecurityException if the process doesn't hold * {@link android.Manifest.permission.MANAGE_ETHERNET_NETWORKS}. - * @throws UnsupportedOperationException if called on a non-automotive device or on an + * @throws UnsupportedOperationException if the {@link NetworkCapabilities} are updated on a + * non-automotive device or this function is called on an * unsupported interface. * @hide */ @@ -582,9 +582,9 @@ public class EthernetManager { /** * Enable a network interface. * - * Enables a previously disabled network interface. - * This function accepts an {@link OutcomeReceiver} that is called once the operation has - * finished execution. + * Enables a previously disabled network interface. An attempt to enable an already-enabled + * interface is ignored. + * The provided {@link OutcomeReceiver} is called once the operation has finished execution. * * @param iface the name of the interface to enable. * @param executor an {@link Executor} to execute the callback on. Optional if callback is null. @@ -619,10 +619,9 @@ public class EthernetManager { /** * Disable a network interface. * - * Disables the use of a network interface to fulfill network requests. If the interface - * currently serves a request, the network will be torn down. - * This function accepts an {@link OutcomeReceiver} that is called once the operation has - * finished execution. + * Disables the specified interface. If this interface is in use in a connected + * {@link android.net.Network}, then that {@code Network} will be torn down. + * The provided {@link OutcomeReceiver} is called once the operation has finished execution. * * @param iface the name of the interface to disable. * @param executor an {@link Executor} to execute the callback on. Optional if callback is null. diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt index 7a574268cc..db1d7e9e50 100644 --- a/framework/api/system-current.txt +++ b/framework/api/system-current.txt @@ -340,7 +340,7 @@ package android.net { method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setSsid(@Nullable String); method @NonNull public android.net.NetworkCapabilities.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>); method @NonNull public android.net.NetworkCapabilities.Builder setTransportInfo(@Nullable android.net.TransportInfo); - method @NonNull public android.net.NetworkCapabilities.Builder setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>); + method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>); method @NonNull public static android.net.NetworkCapabilities.Builder withoutDefaultCapabilities(); } diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java index e25a85558c..a174fe34ce 100644 --- a/framework/src/android/net/ConnectivityManager.java +++ b/framework/src/android/net/ConnectivityManager.java @@ -995,21 +995,25 @@ public class ConnectivityManager { // LINT.ThenChange(packages/modules/Connectivity/service/native/include/Common.h) /** - * Specify default rule which may allow or drop packets depending on existing policy. + * A firewall rule which allows or drops packets depending on existing policy. + * Used by {@link #setUidFirewallRule(int, int, int)} to follow existing policy to handle + * specific uid's packets in specific firewall chain. * @hide */ @SystemApi(client = MODULE_LIBRARIES) public static final int FIREWALL_RULE_DEFAULT = 0; /** - * Specify allow rule which allows packets. + * A firewall rule which allows packets. Used by {@link #setUidFirewallRule(int, int, int)} to + * allow specific uid's packets in specific firewall chain. * @hide */ @SystemApi(client = MODULE_LIBRARIES) public static final int FIREWALL_RULE_ALLOW = 1; /** - * Specify deny rule which drops packets. + * A firewall rule which drops packets. Used by {@link #setUidFirewallRule(int, int, int)} to + * drop specific uid's packets in specific firewall chain. * @hide */ @SystemApi(client = MODULE_LIBRARIES) diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java index f7f2f57f6e..97b1f32671 100644 --- a/framework/src/android/net/NetworkCapabilities.java +++ b/framework/src/android/net/NetworkCapabilities.java @@ -863,8 +863,11 @@ public final class NetworkCapabilities implements Parcelable { } /** - * Get the underlying networks of this network. If the caller is not system privileged, this is - * always redacted to null and it will be never useful to the caller. + * Get the underlying networks of this network. If the caller doesn't have one of + * {@link android.Manifest.permission.NETWORK_FACTORY}, + * {@link android.Manifest.permission.NETWORK_SETTINGS} and + * {@link NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}, this is always redacted to null and + * it will be never useful to the caller. * * @return <li>If the list is null, this network hasn't declared underlying networks.</li> * <li>If the list is empty, this network has declared that it has no underlying @@ -2650,7 +2653,7 @@ public final class NetworkCapabilities implements Parcelable { /** * Builder class for NetworkCapabilities. * - * This class is mainly for for {@link NetworkAgent} instances to use. Many fields in + * This class is mainly for {@link NetworkAgent} instances to use. Many fields in * the built class require holding a signature permission to use - mostly * {@link android.Manifest.permission.NETWORK_FACTORY}, but refer to the specific * description of each setter. As this class lives entirely in app space it does not @@ -3058,9 +3061,20 @@ public final class NetworkCapabilities implements Parcelable { /** * Set the underlying networks of this network. * + * <p>This API is mainly for {@link NetworkAgent}s who hold + * {@link android.Manifest.permission.NETWORK_FACTORY} to set its underlying networks. + * + * <p>The underlying networks are only visible for the receiver who has one of + * {@link android.Manifest.permission.NETWORK_FACTORY}, + * {@link android.Manifest.permission.NETWORK_SETTINGS} and + * {@link NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}. + * If the receiver doesn't have required permissions, the field will be cleared before + * sending to the caller.</p> + * * @param networks The underlying networks of this network. */ @NonNull + @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public Builder setUnderlyingNetworks(@Nullable List<Network> networks) { mCaps.setUnderlyingNetworks(networks); return this; diff --git a/framework/src/android/net/QosSocketInfo.java b/framework/src/android/net/QosSocketInfo.java index a45d5075d6..39c2f334d3 100644 --- a/framework/src/android/net/QosSocketInfo.java +++ b/framework/src/android/net/QosSocketInfo.java @@ -16,6 +16,9 @@ package android.net; +import static android.system.OsConstants.SOCK_DGRAM; +import static android.system.OsConstants.SOCK_STREAM; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; @@ -24,6 +27,7 @@ import android.os.ParcelFileDescriptor; import android.os.Parcelable; import java.io.IOException; +import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; @@ -53,6 +57,8 @@ public final class QosSocketInfo implements Parcelable { @Nullable private final InetSocketAddress mRemoteSocketAddress; + private final int mSocketType; + /** * The {@link Network} the socket is on. * @@ -98,6 +104,16 @@ public final class QosSocketInfo implements Parcelable { } /** + * The socket type of the socket passed in when this QosSocketInfo object was constructed. + * + * @return the socket type of the socket. + * @hide + */ + public int getSocketType() { + return mSocketType; + } + + /** * Creates a {@link QosSocketInfo} given a {@link Network} and bound {@link Socket}. The * {@link Socket} must remain bound in order to receive {@link QosSession}s. * @@ -112,6 +128,32 @@ public final class QosSocketInfo implements Parcelable { mParcelFileDescriptor = ParcelFileDescriptor.fromSocket(socket); mLocalSocketAddress = new InetSocketAddress(socket.getLocalAddress(), socket.getLocalPort()); + mSocketType = SOCK_STREAM; + + if (socket.isConnected()) { + mRemoteSocketAddress = (InetSocketAddress) socket.getRemoteSocketAddress(); + } else { + mRemoteSocketAddress = null; + } + } + + /** + * Creates a {@link QosSocketInfo} given a {@link Network} and bound {@link DatagramSocket}. The + * {@link DatagramSocket} must remain bound in order to receive {@link QosSession}s. + * + * @param network the network + * @param socket the bound {@link DatagramSocket} + * @hide + */ + public QosSocketInfo(@NonNull final Network network, @NonNull final DatagramSocket socket) + throws IOException { + Objects.requireNonNull(socket, "socket cannot be null"); + + mNetwork = Objects.requireNonNull(network, "network cannot be null"); + mParcelFileDescriptor = ParcelFileDescriptor.fromDatagramSocket(socket); + mLocalSocketAddress = + new InetSocketAddress(socket.getLocalAddress(), socket.getLocalPort()); + mSocketType = SOCK_DGRAM; if (socket.isConnected()) { mRemoteSocketAddress = (InetSocketAddress) socket.getRemoteSocketAddress(); @@ -131,6 +173,8 @@ public final class QosSocketInfo implements Parcelable { final int remoteAddressLength = in.readInt(); mRemoteSocketAddress = remoteAddressLength == 0 ? null : readSocketAddress(in, remoteAddressLength); + + mSocketType = in.readInt(); } private @NonNull InetSocketAddress readSocketAddress(final Parcel in, final int addressLength) { @@ -170,6 +214,7 @@ public final class QosSocketInfo implements Parcelable { dest.writeByteArray(remoteAddress); dest.writeInt(mRemoteSocketAddress.getPort()); } + dest.writeInt(mSocketType); } @NonNull diff --git a/service-t/Android.bp b/service-t/Android.bp index 50dd995ef4..1b9f2ec7f2 100644 --- a/service-t/Android.bp +++ b/service-t/Android.bp @@ -22,6 +22,17 @@ package { // Include build rules from Sources.bp build = ["Sources.bp"] +filegroup { + name: "service-connectivity-tiramisu-sources", + srcs: [ + "src/**/*.java", + ], + visibility: ["//visibility:private"], +} +// The above filegroup can be used to specify different sources depending +// on the branch, while minimizing merge conflicts in the rest of the +// build rules. + // This builds T+ services depending on framework-connectivity-t // hidden symbols separately from the S+ services, to ensure that S+ // services cannot accidentally depend on T+ hidden symbols from @@ -32,9 +43,7 @@ java_library { // TODO(b/210962470): Bump this to at least S, and then T. min_sdk_version: "30", srcs: [ - "src/**/*.java", - ":ethernet-service-updatable-sources", - ":services.connectivity-tiramisu-updatable-sources", + ":service-connectivity-tiramisu-sources", ], libs: [ "framework-annotations-lib", diff --git a/service-t/Sources.bp b/service-t/Sources.bp index 04866fb9cc..4e669b655a 100644 --- a/service-t/Sources.bp +++ b/service-t/Sources.bp @@ -59,48 +59,6 @@ filegroup { ], } -// Nsd related libraries. - -filegroup { - name: "services.connectivity-nsd-sources", - srcs: [ - "src/com/android/server/INativeDaemon*.java", - "src/com/android/server/NativeDaemon*.java", - "src/com/android/server/Nsd*.java", - ], - path: "src", - visibility: [ - "//visibility:private", - ], -} - -// IpSec related libraries. - -filegroup { - name: "services.connectivity-ipsec-sources", - srcs: [ - "src/com/android/server/IpSecService.java", - ], - path: "src", - visibility: [ - "//visibility:private", - ], -} - -// Ethernet related libraries. - -filegroup { - name: "services.connectivity-ethernet-sources", - srcs: [ - "src/com/android/server/net/DelayedDiskWrite.java", - "src/com/android/server/net/IpConfigStore.java", - ], - path: "src", - visibility: [ - "//frameworks/opt/net/ethernet/tests", - ], -} - // Connectivity-T common libraries. // TODO: remove this empty filegroup. @@ -111,20 +69,6 @@ filegroup { visibility: ["//frameworks/base/services/core"], } -filegroup { - name: "services.connectivity-tiramisu-updatable-sources", - srcs: [ - ":services.connectivity-ethernet-sources", - ":services.connectivity-ipsec-sources", - ":services.connectivity-netstats-sources", - ":services.connectivity-nsd-sources", - ], - path: "src", - visibility: [ - "//packages/modules/Connectivity:__subpackages__", - ], -} - cc_library_shared { name: "libcom_android_net_module_util_jni", min_sdk_version: "30", diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java index fa86f3965a..e4efa9261b 100644 --- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java +++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java @@ -21,6 +21,7 @@ import android.util.Log; import com.android.modules.utils.build.SdkLevel; import com.android.networkstack.apishim.ConstantsShim; +import com.android.server.connectivity.ConnectivityNativeService; import com.android.server.ethernet.EthernetService; import com.android.server.ethernet.EthernetServiceImpl; import com.android.server.nearby.NearbyService; @@ -31,6 +32,7 @@ import com.android.server.nearby.NearbyService; */ public final class ConnectivityServiceInitializer extends SystemService { private static final String TAG = ConnectivityServiceInitializer.class.getSimpleName(); + private final ConnectivityNativeService mConnectivityNative; private final ConnectivityService mConnectivity; private final IpSecService mIpSecService; private final NsdService mNsdService; @@ -44,6 +46,7 @@ public final class ConnectivityServiceInitializer extends SystemService { mEthernetServiceImpl = createEthernetService(context); mConnectivity = new ConnectivityService(context); mIpSecService = createIpSecService(context); + mConnectivityNative = createConnectivityNativeService(context); mNsdService = createNsdService(context); mNearbyService = createNearbyService(context); } @@ -65,6 +68,12 @@ public final class ConnectivityServiceInitializer extends SystemService { publishBinderService(Context.IPSEC_SERVICE, mIpSecService, /* allowIsolated= */ false); } + if (mConnectivityNative != null) { + Log.i(TAG, "Registering " + ConnectivityNativeService.SERVICE_NAME); + publishBinderService(ConnectivityNativeService.SERVICE_NAME, mConnectivityNative, + /* allowIsolated= */ false); + } + if (mNsdService != null) { Log.i(TAG, "Registering " + Context.NSD_SERVICE); publishBinderService(Context.NSD_SERVICE, mNsdService, /* allowIsolated= */ false); @@ -98,6 +107,19 @@ public final class ConnectivityServiceInitializer extends SystemService { return new IpSecService(context); } + /** + * Return ConnectivityNativeService instance, or null if current SDK is lower than T. + */ + private ConnectivityNativeService createConnectivityNativeService(final Context context) { + if (!SdkLevel.isAtLeastT()) return null; + try { + return new ConnectivityNativeService(context); + } catch (UnsupportedOperationException e) { + Log.d(TAG, "Unable to get ConnectivityNative service", e); + return null; + } + } + /** Return NsdService instance or null if current SDK is lower than T */ private NsdService createNsdService(final Context context) { if (!SdkLevel.isAtLeastT()) return null; diff --git a/service-t/src/com/android/server/ethernet/EthernetConfigStore.java b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java new file mode 100644 index 0000000000..6b623f48ff --- /dev/null +++ b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java @@ -0,0 +1,88 @@ +/* + * 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.server.ethernet; + +import android.annotation.Nullable; +import android.net.IpConfiguration; +import android.os.Environment; +import android.util.ArrayMap; + +import com.android.server.net.IpConfigStore; + + +/** + * This class provides an API to store and manage Ethernet network configuration. + */ +public class EthernetConfigStore { + private static final String ipConfigFile = Environment.getDataDirectory() + + "/misc/ethernet/ipconfig.txt"; + + private IpConfigStore mStore = new IpConfigStore(); + private ArrayMap<String, IpConfiguration> mIpConfigurations; + private IpConfiguration mIpConfigurationForDefaultInterface; + private final Object mSync = new Object(); + + public EthernetConfigStore() { + mIpConfigurations = new ArrayMap<>(0); + } + + public void read() { + synchronized (mSync) { + ArrayMap<String, IpConfiguration> configs = + IpConfigStore.readIpConfigurations(ipConfigFile); + + // This configuration may exist in old file versions when there was only a single active + // Ethernet interface. + if (configs.containsKey("0")) { + mIpConfigurationForDefaultInterface = configs.remove("0"); + } + + mIpConfigurations = configs; + } + } + + public void write(String iface, IpConfiguration config) { + boolean modified; + + synchronized (mSync) { + if (config == null) { + modified = mIpConfigurations.remove(iface) != null; + } else { + IpConfiguration oldConfig = mIpConfigurations.put(iface, config); + modified = !config.equals(oldConfig); + } + + if (modified) { + mStore.writeIpConfigurations(ipConfigFile, mIpConfigurations); + } + } + } + + public ArrayMap<String, IpConfiguration> getIpConfigurations() { + synchronized (mSync) { + return new ArrayMap<>(mIpConfigurations); + } + } + + @Nullable + public IpConfiguration getIpConfigurationForDefaultInterface() { + synchronized (mSync) { + return mIpConfigurationForDefaultInterface == null + ? null : new IpConfiguration(mIpConfigurationForDefaultInterface); + } + } +} diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java b/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java new file mode 100644 index 0000000000..57fbce7e86 --- /dev/null +++ b/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.ethernet; + +import android.content.Context; +import android.net.LinkProperties; +import android.net.NetworkAgent; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkProvider; +import android.net.NetworkScore; +import android.os.Looper; +import android.annotation.NonNull; +import android.annotation.Nullable; + +public class EthernetNetworkAgent extends NetworkAgent { + + private static final String TAG = "EthernetNetworkAgent"; + + public interface Callbacks { + void onNetworkUnwanted(); + } + + private final Callbacks mCallbacks; + + EthernetNetworkAgent( + @NonNull Context context, + @NonNull Looper looper, + @NonNull NetworkCapabilities nc, + @NonNull LinkProperties lp, + @NonNull NetworkAgentConfig config, + @Nullable NetworkProvider provider, + @NonNull Callbacks cb) { + super(context, looper, TAG, nc, lp, new NetworkScore.Builder().build(), config, provider); + mCallbacks = cb; + } + + @Override + public void onNetworkUnwanted() { + mCallbacks.onNetworkUnwanted(); + } + + // sendLinkProperties is final in NetworkAgent, so it cannot be mocked. + public void sendLinkPropertiesImpl(LinkProperties lp) { + sendLinkProperties(lp); + } + + public Callbacks getCallbacks() { + return mCallbacks; + } +} diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java new file mode 100644 index 0000000000..eb22f7875b --- /dev/null +++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java @@ -0,0 +1,784 @@ +/* + * 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.server.ethernet; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.ConnectivityResources; +import android.net.EthernetManager; +import android.net.EthernetNetworkManagementException; +import android.net.EthernetNetworkSpecifier; +import android.net.INetworkInterfaceOutcomeReceiver; +import android.net.IpConfiguration; +import android.net.IpConfiguration.IpAssignment; +import android.net.IpConfiguration.ProxySettings; +import android.net.LinkProperties; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkFactory; +import android.net.NetworkProvider; +import android.net.NetworkRequest; +import android.net.NetworkSpecifier; +import android.net.ip.IIpClient; +import android.net.ip.IpClientCallbacks; +import android.net.ip.IpClientManager; +import android.net.ip.IpClientUtil; +import android.net.shared.ProvisioningConfiguration; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.AndroidRuntimeException; +import android.util.Log; +import android.util.SparseArray; + +import com.android.connectivity.resources.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.net.module.util.InterfaceParams; + +import java.io.FileDescriptor; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * {@link NetworkFactory} that represents Ethernet networks. + * + * This class reports a static network score of 70 when it is tracking an interface and that + * interface's link is up, and a score of 0 otherwise. + */ +public class EthernetNetworkFactory extends NetworkFactory { + private final static String TAG = EthernetNetworkFactory.class.getSimpleName(); + final static boolean DBG = true; + + private final static int NETWORK_SCORE = 70; + private static final String NETWORK_TYPE = "Ethernet"; + private static final String LEGACY_TCP_BUFFER_SIZES = + "524288,1048576,3145728,524288,1048576,2097152"; + + private final ConcurrentHashMap<String, NetworkInterfaceState> mTrackingInterfaces = + new ConcurrentHashMap<>(); + private final Handler mHandler; + private final Context mContext; + final Dependencies mDeps; + + public static class Dependencies { + public void makeIpClient(Context context, String iface, IpClientCallbacks callbacks) { + IpClientUtil.makeIpClient(context, iface, callbacks); + } + + public IpClientManager makeIpClientManager(@NonNull final IIpClient ipClient) { + return new IpClientManager(ipClient, TAG); + } + + public EthernetNetworkAgent makeEthernetNetworkAgent(Context context, Looper looper, + NetworkCapabilities nc, LinkProperties lp, NetworkAgentConfig config, + NetworkProvider provider, EthernetNetworkAgent.Callbacks cb) { + return new EthernetNetworkAgent(context, looper, nc, lp, config, provider, cb); + } + + public InterfaceParams getNetworkInterfaceByName(String name) { + return InterfaceParams.getByName(name); + } + + // TODO: remove legacy resource fallback after migrating its overlays. + private String getPlatformTcpBufferSizes(Context context) { + final Resources r = context.getResources(); + final int resId = r.getIdentifier("config_ethernet_tcp_buffers", "string", + context.getPackageName()); + return r.getString(resId); + } + + public String getTcpBufferSizesFromResource(Context context) { + final String tcpBufferSizes; + final String platformTcpBufferSizes = getPlatformTcpBufferSizes(context); + if (!LEGACY_TCP_BUFFER_SIZES.equals(platformTcpBufferSizes)) { + // Platform resource is not the historical default: use the overlay. + tcpBufferSizes = platformTcpBufferSizes; + } else { + final ConnectivityResources resources = new ConnectivityResources(context); + tcpBufferSizes = resources.get().getString(R.string.config_ethernet_tcp_buffers); + } + return tcpBufferSizes; + } + } + + public static class ConfigurationException extends AndroidRuntimeException { + public ConfigurationException(String msg) { + super(msg); + } + } + + public EthernetNetworkFactory(Handler handler, Context context) { + this(handler, context, new Dependencies()); + } + + @VisibleForTesting + EthernetNetworkFactory(Handler handler, Context context, Dependencies deps) { + super(handler.getLooper(), context, NETWORK_TYPE, createDefaultNetworkCapabilities()); + + mHandler = handler; + mContext = context; + mDeps = deps; + + setScoreFilter(NETWORK_SCORE); + } + + @Override + public boolean acceptRequest(NetworkRequest request) { + if (DBG) { + Log.d(TAG, "acceptRequest, request: " + request); + } + + return networkForRequest(request) != null; + } + + @Override + protected void needNetworkFor(NetworkRequest networkRequest) { + NetworkInterfaceState network = networkForRequest(networkRequest); + + if (network == null) { + Log.e(TAG, "needNetworkFor, failed to get a network for " + networkRequest); + return; + } + + if (++network.refCount == 1) { + network.start(); + } + } + + @Override + protected void releaseNetworkFor(NetworkRequest networkRequest) { + NetworkInterfaceState network = networkForRequest(networkRequest); + if (network == null) { + Log.e(TAG, "releaseNetworkFor, failed to get a network for " + networkRequest); + return; + } + + if (--network.refCount == 0) { + network.stop(); + } + } + + /** + * Returns an array of available interface names. The array is sorted: unrestricted interfaces + * goes first, then sorted by name. + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + protected String[] getAvailableInterfaces(boolean includeRestricted) { + return mTrackingInterfaces.values() + .stream() + .filter(iface -> !iface.isRestricted() || includeRestricted) + .sorted((iface1, iface2) -> { + int r = Boolean.compare(iface1.isRestricted(), iface2.isRestricted()); + return r == 0 ? iface1.name.compareTo(iface2.name) : r; + }) + .map(iface -> iface.name) + .toArray(String[]::new); + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + protected void addInterface(@NonNull final String ifaceName, @NonNull final String hwAddress, + @NonNull final IpConfiguration ipConfig, + @NonNull final NetworkCapabilities capabilities) { + if (mTrackingInterfaces.containsKey(ifaceName)) { + Log.e(TAG, "Interface with name " + ifaceName + " already exists."); + return; + } + + final NetworkCapabilities nc = new NetworkCapabilities.Builder(capabilities) + .setNetworkSpecifier(new EthernetNetworkSpecifier(ifaceName)) + .build(); + + if (DBG) { + Log.d(TAG, "addInterface, iface: " + ifaceName + ", capabilities: " + nc); + } + + final NetworkInterfaceState iface = new NetworkInterfaceState( + ifaceName, hwAddress, mHandler, mContext, ipConfig, nc, this, mDeps); + mTrackingInterfaces.put(ifaceName, iface); + updateCapabilityFilter(); + } + + @VisibleForTesting + protected int getInterfaceState(@NonNull String iface) { + final NetworkInterfaceState interfaceState = mTrackingInterfaces.get(iface); + if (interfaceState == null) { + return EthernetManager.STATE_ABSENT; + } else if (!interfaceState.mLinkUp) { + return EthernetManager.STATE_LINK_DOWN; + } else { + return EthernetManager.STATE_LINK_UP; + } + } + + /** + * Update a network's configuration and restart it if necessary. + * + * @param ifaceName the interface name of the network to be updated. + * @param ipConfig the desired {@link IpConfiguration} for the given network or null. If + * {@code null} is passed, the existing IpConfiguration is not updated. + * @param capabilities the desired {@link NetworkCapabilities} for the given network. If + * {@code null} is passed, then the network's current + * {@link NetworkCapabilities} will be used in support of existing APIs as + * the public API does not allow this. + * @param listener an optional {@link INetworkInterfaceOutcomeReceiver} to notify callers of + * completion. + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + protected void updateInterface(@NonNull final String ifaceName, + @Nullable final IpConfiguration ipConfig, + @Nullable final NetworkCapabilities capabilities, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + if (!hasInterface(ifaceName)) { + maybeSendNetworkManagementCallbackForUntracked(ifaceName, listener); + return; + } + + final NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName); + iface.updateInterface(ipConfig, capabilities, listener); + mTrackingInterfaces.put(ifaceName, iface); + updateCapabilityFilter(); + } + + private static NetworkCapabilities mixInCapabilities(NetworkCapabilities nc, + NetworkCapabilities addedNc) { + final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder(nc); + for (int transport : addedNc.getTransportTypes()) builder.addTransportType(transport); + for (int capability : addedNc.getCapabilities()) builder.addCapability(capability); + return builder.build(); + } + + private void updateCapabilityFilter() { + NetworkCapabilities capabilitiesFilter = createDefaultNetworkCapabilities(); + for (NetworkInterfaceState iface: mTrackingInterfaces.values()) { + capabilitiesFilter = mixInCapabilities(capabilitiesFilter, iface.mCapabilities); + } + + if (DBG) Log.d(TAG, "updateCapabilityFilter: " + capabilitiesFilter); + setCapabilityFilter(capabilitiesFilter); + } + + private static NetworkCapabilities createDefaultNetworkCapabilities() { + return NetworkCapabilities.Builder + .withoutDefaultCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET).build(); + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + protected void removeInterface(String interfaceName) { + NetworkInterfaceState iface = mTrackingInterfaces.remove(interfaceName); + if (iface != null) { + iface.maybeSendNetworkManagementCallbackForAbort(); + iface.stop(); + } + + updateCapabilityFilter(); + } + + /** Returns true if state has been modified */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + protected boolean updateInterfaceLinkState(@NonNull final String ifaceName, final boolean up, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + if (!hasInterface(ifaceName)) { + maybeSendNetworkManagementCallbackForUntracked(ifaceName, listener); + return false; + } + + if (DBG) { + Log.d(TAG, "updateInterfaceLinkState, iface: " + ifaceName + ", up: " + up); + } + + NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName); + return iface.updateLinkState(up, listener); + } + + private void maybeSendNetworkManagementCallbackForUntracked( + String ifaceName, INetworkInterfaceOutcomeReceiver listener) { + maybeSendNetworkManagementCallback(listener, null, + new EthernetNetworkManagementException( + ifaceName + " can't be updated as it is not available.")); + } + + @VisibleForTesting + protected boolean hasInterface(String ifaceName) { + return mTrackingInterfaces.containsKey(ifaceName); + } + + private NetworkInterfaceState networkForRequest(NetworkRequest request) { + String requestedIface = null; + + NetworkSpecifier specifier = request.getNetworkSpecifier(); + if (specifier instanceof EthernetNetworkSpecifier) { + requestedIface = ((EthernetNetworkSpecifier) specifier) + .getInterfaceName(); + } + + NetworkInterfaceState network = null; + if (!TextUtils.isEmpty(requestedIface)) { + NetworkInterfaceState n = mTrackingInterfaces.get(requestedIface); + if (n != null && request.canBeSatisfiedBy(n.mCapabilities)) { + network = n; + } + } else { + for (NetworkInterfaceState n : mTrackingInterfaces.values()) { + if (request.canBeSatisfiedBy(n.mCapabilities) && n.mLinkUp) { + network = n; + break; + } + } + } + + if (DBG) { + Log.i(TAG, "networkForRequest, request: " + request + ", network: " + network); + } + + return network; + } + + private static void maybeSendNetworkManagementCallback( + @Nullable final INetworkInterfaceOutcomeReceiver listener, + @Nullable final String iface, + @Nullable final EthernetNetworkManagementException e) { + if (null == listener) { + return; + } + + try { + if (iface != null) { + listener.onResult(iface); + } else { + listener.onError(e); + } + } catch (RemoteException re) { + Log.e(TAG, "Can't send onComplete for network management callback", re); + } + } + + @VisibleForTesting + static class NetworkInterfaceState { + final String name; + + private final String mHwAddress; + private final Handler mHandler; + private final Context mContext; + private final NetworkFactory mNetworkFactory; + private final Dependencies mDeps; + + private static String sTcpBufferSizes = null; // Lazy initialized. + + private boolean mLinkUp; + private int mLegacyType; + private LinkProperties mLinkProperties = new LinkProperties(); + + private volatile @Nullable IpClientManager mIpClient; + private @NonNull NetworkCapabilities mCapabilities; + private @Nullable EthernetIpClientCallback mIpClientCallback; + private @Nullable EthernetNetworkAgent mNetworkAgent; + private @Nullable IpConfiguration mIpConfig; + + /** + * A map of TRANSPORT_* types to legacy transport types available for each type an ethernet + * interface could propagate. + * + * There are no legacy type equivalents to LOWPAN or WIFI_AWARE. These types are set to + * TYPE_NONE to match the behavior of their own network factories. + */ + private static final SparseArray<Integer> sTransports = new SparseArray(); + static { + sTransports.put(NetworkCapabilities.TRANSPORT_ETHERNET, + ConnectivityManager.TYPE_ETHERNET); + sTransports.put(NetworkCapabilities.TRANSPORT_BLUETOOTH, + ConnectivityManager.TYPE_BLUETOOTH); + sTransports.put(NetworkCapabilities.TRANSPORT_WIFI, ConnectivityManager.TYPE_WIFI); + sTransports.put(NetworkCapabilities.TRANSPORT_CELLULAR, + ConnectivityManager.TYPE_MOBILE); + sTransports.put(NetworkCapabilities.TRANSPORT_LOWPAN, ConnectivityManager.TYPE_NONE); + sTransports.put(NetworkCapabilities.TRANSPORT_WIFI_AWARE, + ConnectivityManager.TYPE_NONE); + } + + long refCount = 0; + + private class EthernetIpClientCallback extends IpClientCallbacks { + private final ConditionVariable mIpClientStartCv = new ConditionVariable(false); + private final ConditionVariable mIpClientShutdownCv = new ConditionVariable(false); + @Nullable INetworkInterfaceOutcomeReceiver mNetworkManagementListener; + + EthernetIpClientCallback(@Nullable final INetworkInterfaceOutcomeReceiver listener) { + mNetworkManagementListener = listener; + } + + @Override + public void onIpClientCreated(IIpClient ipClient) { + mIpClient = mDeps.makeIpClientManager(ipClient); + mIpClientStartCv.open(); + } + + private void awaitIpClientStart() { + mIpClientStartCv.block(); + } + + private void awaitIpClientShutdown() { + mIpClientShutdownCv.block(); + } + + // At the time IpClient is stopped, an IpClient event may have already been posted on + // the back of the handler and is awaiting execution. Once that event is executed, the + // associated callback object may not be valid anymore + // (NetworkInterfaceState#mIpClientCallback points to a different object / null). + private boolean isCurrentCallback() { + return this == mIpClientCallback; + } + + private void handleIpEvent(final @NonNull Runnable r) { + mHandler.post(() -> { + if (!isCurrentCallback()) { + Log.i(TAG, "Ignoring stale IpClientCallbacks " + this); + return; + } + r.run(); + }); + } + + @Override + public void onProvisioningSuccess(LinkProperties newLp) { + handleIpEvent(() -> onIpLayerStarted(newLp, mNetworkManagementListener)); + } + + @Override + public void onProvisioningFailure(LinkProperties newLp) { + // This cannot happen due to provisioning timeout, because our timeout is 0. It can + // happen due to errors while provisioning or on provisioning loss. + handleIpEvent(() -> onIpLayerStopped(mNetworkManagementListener)); + } + + @Override + public void onLinkPropertiesChange(LinkProperties newLp) { + handleIpEvent(() -> updateLinkProperties(newLp)); + } + + @Override + public void onReachabilityLost(String logMsg) { + handleIpEvent(() -> updateNeighborLostEvent(logMsg)); + } + + @Override + public void onQuit() { + mIpClient = null; + mIpClientShutdownCv.open(); + } + } + + NetworkInterfaceState(String ifaceName, String hwAddress, Handler handler, Context context, + @NonNull IpConfiguration ipConfig, @NonNull NetworkCapabilities capabilities, + NetworkFactory networkFactory, Dependencies deps) { + name = ifaceName; + mIpConfig = Objects.requireNonNull(ipConfig); + mCapabilities = Objects.requireNonNull(capabilities); + mLegacyType = getLegacyType(mCapabilities); + mHandler = handler; + mContext = context; + mNetworkFactory = networkFactory; + mDeps = deps; + mHwAddress = hwAddress; + } + + /** + * Determines the legacy transport type from a NetworkCapabilities transport type. Defaults + * to legacy TYPE_NONE if there is no known conversion + */ + private static int getLegacyType(int transport) { + return sTransports.get(transport, ConnectivityManager.TYPE_NONE); + } + + private static int getLegacyType(@NonNull final NetworkCapabilities capabilities) { + final int[] transportTypes = capabilities.getTransportTypes(); + if (transportTypes.length > 0) { + return getLegacyType(transportTypes[0]); + } + + // Should never happen as transport is always one of ETHERNET or a valid override + throw new ConfigurationException("Network Capabilities do not have an associated " + + "transport type."); + } + + private void setCapabilities(@NonNull final NetworkCapabilities capabilities) { + mCapabilities = new NetworkCapabilities(capabilities); + mLegacyType = getLegacyType(mCapabilities); + } + + void updateInterface(@Nullable final IpConfiguration ipConfig, + @Nullable final NetworkCapabilities capabilities, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + if (DBG) { + Log.d(TAG, "updateInterface, iface: " + name + + ", ipConfig: " + ipConfig + ", old ipConfig: " + mIpConfig + + ", capabilities: " + capabilities + ", old capabilities: " + mCapabilities + + ", listener: " + listener + ); + } + + if (null != ipConfig){ + mIpConfig = ipConfig; + } + if (null != capabilities) { + setCapabilities(capabilities); + } + // Send an abort callback if a request is filed before the previous one has completed. + maybeSendNetworkManagementCallbackForAbort(); + // TODO: Update this logic to only do a restart if required. Although a restart may + // be required due to the capabilities or ipConfiguration values, not all + // capabilities changes require a restart. + restart(listener); + } + + boolean isRestricted() { + return !mCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED); + } + + private void start() { + start(null); + } + + private void start(@Nullable final INetworkInterfaceOutcomeReceiver listener) { + if (mIpClient != null) { + if (DBG) Log.d(TAG, "IpClient already started"); + return; + } + if (DBG) { + Log.d(TAG, String.format("Starting Ethernet IpClient(%s)", name)); + } + + mIpClientCallback = new EthernetIpClientCallback(listener); + mDeps.makeIpClient(mContext, name, mIpClientCallback); + mIpClientCallback.awaitIpClientStart(); + + if (sTcpBufferSizes == null) { + sTcpBufferSizes = mDeps.getTcpBufferSizesFromResource(mContext); + } + provisionIpClient(mIpClient, mIpConfig, sTcpBufferSizes); + } + + void onIpLayerStarted(@NonNull final LinkProperties linkProperties, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + if (mNetworkAgent != null) { + Log.e(TAG, "Already have a NetworkAgent - aborting new request"); + stop(); + return; + } + mLinkProperties = linkProperties; + + // Create our NetworkAgent. + final NetworkAgentConfig config = new NetworkAgentConfig.Builder() + .setLegacyType(mLegacyType) + .setLegacyTypeName(NETWORK_TYPE) + .setLegacyExtraInfo(mHwAddress) + .build(); + mNetworkAgent = mDeps.makeEthernetNetworkAgent(mContext, mHandler.getLooper(), + mCapabilities, mLinkProperties, config, mNetworkFactory.getProvider(), + new EthernetNetworkAgent.Callbacks() { + @Override + public void onNetworkUnwanted() { + // if mNetworkAgent is null, we have already called stop. + if (mNetworkAgent == null) return; + + if (this == mNetworkAgent.getCallbacks()) { + stop(); + } else { + Log.d(TAG, "Ignoring unwanted as we have a more modern " + + "instance"); + } + } + }); + mNetworkAgent.register(); + mNetworkAgent.markConnected(); + realizeNetworkManagementCallback(name, null); + } + + void onIpLayerStopped(@Nullable final INetworkInterfaceOutcomeReceiver listener) { + // There is no point in continuing if the interface is gone as stop() will be triggered + // by removeInterface() when processed on the handler thread and start() won't + // work for a non-existent interface. + if (null == mDeps.getNetworkInterfaceByName(name)) { + if (DBG) Log.d(TAG, name + " is no longer available."); + // Send a callback in case a provisioning request was in progress. + maybeSendNetworkManagementCallbackForAbort(); + return; + } + restart(listener); + } + + private void maybeSendNetworkManagementCallbackForAbort() { + realizeNetworkManagementCallback(null, + new EthernetNetworkManagementException( + "The IP provisioning request has been aborted.")); + } + + // Must be called on the handler thread + private void realizeNetworkManagementCallback(@Nullable final String iface, + @Nullable final EthernetNetworkManagementException e) { + ensureRunningOnEthernetHandlerThread(); + if (null == mIpClientCallback) { + return; + } + + EthernetNetworkFactory.maybeSendNetworkManagementCallback( + mIpClientCallback.mNetworkManagementListener, iface, e); + // Only send a single callback per listener. + mIpClientCallback.mNetworkManagementListener = null; + } + + private void ensureRunningOnEthernetHandlerThread() { + if (mHandler.getLooper().getThread() != Thread.currentThread()) { + throw new IllegalStateException( + "Not running on the Ethernet thread: " + + Thread.currentThread().getName()); + } + } + + void updateLinkProperties(LinkProperties linkProperties) { + mLinkProperties = linkProperties; + if (mNetworkAgent != null) { + mNetworkAgent.sendLinkPropertiesImpl(linkProperties); + } + } + + void updateNeighborLostEvent(String logMsg) { + Log.i(TAG, "updateNeighborLostEvent " + logMsg); + // Reachability lost will be seen only if the gateway is not reachable. + // Since ethernet FW doesn't have the mechanism to scan for new networks + // like WiFi, simply restart. + // If there is a better network, that will become default and apps + // will be able to use internet. If ethernet gets connected again, + // and has backhaul connectivity, it will become default. + restart(); + } + + /** Returns true if state has been modified */ + boolean updateLinkState(final boolean up, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + if (mLinkUp == up) { + EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, null, + new EthernetNetworkManagementException( + "No changes with requested link state " + up + " for " + name)); + return false; + } + mLinkUp = up; + + if (!up) { // was up, goes down + // Send an abort on a provisioning request callback if necessary before stopping. + maybeSendNetworkManagementCallbackForAbort(); + stop(); + // If only setting the interface down, send a callback to signal completion. + EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, name, null); + } else { // was down, goes up + stop(); + start(listener); + } + + return true; + } + + void stop() { + // Invalidate all previous start requests + if (mIpClient != null) { + mIpClient.shutdown(); + mIpClientCallback.awaitIpClientShutdown(); + mIpClient = null; + } + mIpClientCallback = null; + + if (mNetworkAgent != null) { + mNetworkAgent.unregister(); + mNetworkAgent = null; + } + mLinkProperties.clear(); + } + + private static void provisionIpClient(@NonNull final IpClientManager ipClient, + @NonNull final IpConfiguration config, @NonNull final String tcpBufferSizes) { + if (config.getProxySettings() == ProxySettings.STATIC || + config.getProxySettings() == ProxySettings.PAC) { + ipClient.setHttpProxy(config.getHttpProxy()); + } + + if (!TextUtils.isEmpty(tcpBufferSizes)) { + ipClient.setTcpBufferSizes(tcpBufferSizes); + } + + ipClient.startProvisioning(createProvisioningConfiguration(config)); + } + + private static ProvisioningConfiguration createProvisioningConfiguration( + @NonNull final IpConfiguration config) { + if (config.getIpAssignment() == IpAssignment.STATIC) { + return new ProvisioningConfiguration.Builder() + .withStaticConfiguration(config.getStaticIpConfiguration()) + .build(); + } + return new ProvisioningConfiguration.Builder() + .withProvisioningTimeoutMs(0) + .build(); + } + + void restart() { + restart(null); + } + + void restart(@Nullable final INetworkInterfaceOutcomeReceiver listener) { + if (DBG) Log.d(TAG, "reconnecting Ethernet"); + stop(); + start(listener); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{ " + + "refCount: " + refCount + ", " + + "iface: " + name + ", " + + "up: " + mLinkUp + ", " + + "hwAddress: " + mHwAddress + ", " + + "networkCapabilities: " + mCapabilities + ", " + + "networkAgent: " + mNetworkAgent + ", " + + "ipClient: " + mIpClient + "," + + "linkProperties: " + mLinkProperties + + "}"; + } + } + + void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) { + super.dump(fd, pw, args); + pw.println(getClass().getSimpleName()); + pw.println("Tracking interfaces:"); + pw.increaseIndent(); + for (String iface: mTrackingInterfaces.keySet()) { + NetworkInterfaceState ifaceState = mTrackingInterfaces.get(iface); + pw.println(iface + ":" + ifaceState); + pw.increaseIndent(); + if (null == ifaceState.mIpClient) { + pw.println("IpClient is null"); + } + pw.decreaseIndent(); + } + pw.decreaseIndent(); + } +} diff --git a/service-t/src/com/android/server/ethernet/EthernetService.java b/service-t/src/com/android/server/ethernet/EthernetService.java new file mode 100644 index 0000000000..d405fd59fb --- /dev/null +++ b/service-t/src/com/android/server/ethernet/EthernetService.java @@ -0,0 +1,47 @@ +/* + * 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.server.ethernet; + +import android.content.Context; +import android.net.INetd; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; + +import java.util.Objects; + +// TODO: consider renaming EthernetServiceImpl to EthernetService and deleting this file. +public final class EthernetService { + private static final String TAG = "EthernetService"; + private static final String THREAD_NAME = "EthernetServiceThread"; + + private static INetd getNetd(Context context) { + final INetd netd = + INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)); + Objects.requireNonNull(netd, "could not get netd instance"); + return netd; + } + + public static EthernetServiceImpl create(Context context) { + final HandlerThread handlerThread = new HandlerThread(THREAD_NAME); + handlerThread.start(); + final Handler handler = new Handler(handlerThread.getLooper()); + final EthernetNetworkFactory factory = new EthernetNetworkFactory(handler, context); + return new EthernetServiceImpl(context, handler, + new EthernetTracker(context, handler, factory, getNetd(context))); + } +} diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java new file mode 100644 index 0000000000..5e830ad83a --- /dev/null +++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java @@ -0,0 +1,299 @@ +/* + * 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.server.ethernet; + +import static android.net.NetworkCapabilities.TRANSPORT_TEST; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.IEthernetManager; +import android.net.IEthernetServiceListener; +import android.net.INetworkInterfaceOutcomeReceiver; +import android.net.ITetheredInterfaceCallback; +import android.net.EthernetNetworkUpdateRequest; +import android.net.IpConfiguration; +import android.net.NetworkCapabilities; +import android.os.Binder; +import android.os.Handler; +import android.os.RemoteException; +import android.util.Log; +import android.util.PrintWriterPrinter; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.net.module.util.PermissionUtils; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * EthernetServiceImpl handles remote Ethernet operation requests by implementing + * the IEthernetManager interface. + */ +public class EthernetServiceImpl extends IEthernetManager.Stub { + private static final String TAG = "EthernetServiceImpl"; + + @VisibleForTesting + final AtomicBoolean mStarted = new AtomicBoolean(false); + private final Context mContext; + private final Handler mHandler; + private final EthernetTracker mTracker; + + EthernetServiceImpl(@NonNull final Context context, @NonNull final Handler handler, + @NonNull final EthernetTracker tracker) { + mContext = context; + mHandler = handler; + mTracker = tracker; + } + + private void enforceAutomotiveDevice(final @NonNull String methodName) { + PermissionUtils.enforceSystemFeature(mContext, PackageManager.FEATURE_AUTOMOTIVE, + methodName + " is only available on automotive devices."); + } + + private boolean checkUseRestrictedNetworksPermission() { + return PermissionUtils.checkAnyPermissionOf(mContext, + android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS); + } + + public void start() { + Log.i(TAG, "Starting Ethernet service"); + mTracker.start(); + mStarted.set(true); + } + + private void throwIfEthernetNotStarted() { + if (!mStarted.get()) { + throw new IllegalStateException("System isn't ready to change ethernet configurations"); + } + } + + @Override + public String[] getAvailableInterfaces() throws RemoteException { + PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG); + return mTracker.getInterfaces(checkUseRestrictedNetworksPermission()); + } + + /** + * Get Ethernet configuration + * @return the Ethernet Configuration, contained in {@link IpConfiguration}. + */ + @Override + public IpConfiguration getConfiguration(String iface) { + PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG); + if (mTracker.isRestrictedInterface(iface)) { + PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG); + } + + return new IpConfiguration(mTracker.getIpConfiguration(iface)); + } + + /** + * Set Ethernet configuration + */ + @Override + public void setConfiguration(String iface, IpConfiguration config) { + throwIfEthernetNotStarted(); + + PermissionUtils.enforceNetworkStackPermission(mContext); + if (mTracker.isRestrictedInterface(iface)) { + PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG); + } + + // TODO: this does not check proxy settings, gateways, etc. + // Fix this by making IpConfiguration a complete representation of static configuration. + mTracker.updateIpConfiguration(iface, new IpConfiguration(config)); + } + + /** + * Indicates whether given interface is available. + */ + @Override + public boolean isAvailable(String iface) { + PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG); + if (mTracker.isRestrictedInterface(iface)) { + PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG); + } + + return mTracker.isTrackingInterface(iface); + } + + /** + * Adds a listener. + * @param listener A {@link IEthernetServiceListener} to add. + */ + public void addListener(IEthernetServiceListener listener) throws RemoteException { + Objects.requireNonNull(listener, "listener must not be null"); + PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG); + mTracker.addListener(listener, checkUseRestrictedNetworksPermission()); + } + + /** + * Removes a listener. + * @param listener A {@link IEthernetServiceListener} to remove. + */ + public void removeListener(IEthernetServiceListener listener) { + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG); + mTracker.removeListener(listener); + } + + @Override + public void setIncludeTestInterfaces(boolean include) { + PermissionUtils.enforceNetworkStackPermissionOr(mContext, + android.Manifest.permission.NETWORK_SETTINGS); + mTracker.setIncludeTestInterfaces(include); + } + + @Override + public void requestTetheredInterface(ITetheredInterfaceCallback callback) { + Objects.requireNonNull(callback, "callback must not be null"); + PermissionUtils.enforceNetworkStackPermissionOr(mContext, + android.Manifest.permission.NETWORK_SETTINGS); + mTracker.requestTetheredInterface(callback); + } + + @Override + public void releaseTetheredInterface(ITetheredInterfaceCallback callback) { + Objects.requireNonNull(callback, "callback must not be null"); + PermissionUtils.enforceNetworkStackPermissionOr(mContext, + android.Manifest.permission.NETWORK_SETTINGS); + mTracker.releaseTetheredInterface(callback); + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump EthernetService from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + pw.println("Current Ethernet state: "); + pw.increaseIndent(); + mTracker.dump(fd, pw, args); + pw.decreaseIndent(); + + pw.println("Handler:"); + pw.increaseIndent(); + mHandler.dump(new PrintWriterPrinter(pw), "EthernetServiceImpl"); + pw.decreaseIndent(); + } + + private void enforceNetworkManagementPermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.MANAGE_ETHERNET_NETWORKS, + "EthernetServiceImpl"); + } + + private void enforceManageTestNetworksPermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.MANAGE_TEST_NETWORKS, + "EthernetServiceImpl"); + } + + private void maybeValidateTestCapabilities(final String iface, + @Nullable final NetworkCapabilities nc) { + if (!mTracker.isValidTestInterface(iface)) { + return; + } + // For test interfaces, only null or capabilities that include TRANSPORT_TEST are + // allowed. + if (nc != null && !nc.hasTransport(TRANSPORT_TEST)) { + throw new IllegalArgumentException( + "Updates to test interfaces must have NetworkCapabilities.TRANSPORT_TEST."); + } + } + + private void enforceAdminPermission(final String iface, boolean enforceAutomotive, + final String logMessage) { + if (mTracker.isValidTestInterface(iface)) { + enforceManageTestNetworksPermission(); + } else { + enforceNetworkManagementPermission(); + if (enforceAutomotive) { + enforceAutomotiveDevice(logMessage); + } + } + } + + @Override + public void updateConfiguration(@NonNull final String iface, + @NonNull final EthernetNetworkUpdateRequest request, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + Objects.requireNonNull(iface); + Objects.requireNonNull(request); + throwIfEthernetNotStarted(); + + // TODO: validate that iface is listed in overlay config_ethernet_interfaces + // only automotive devices are allowed to set the NetworkCapabilities using this API + enforceAdminPermission(iface, request.getNetworkCapabilities() != null, + "updateConfiguration() with non-null capabilities"); + maybeValidateTestCapabilities(iface, request.getNetworkCapabilities()); + + mTracker.updateConfiguration( + iface, request.getIpConfiguration(), request.getNetworkCapabilities(), listener); + } + + @Override + public void connectNetwork(@NonNull final String iface, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + Log.i(TAG, "connectNetwork called with: iface=" + iface + ", listener=" + listener); + Objects.requireNonNull(iface); + throwIfEthernetNotStarted(); + + enforceAdminPermission(iface, true, "connectNetwork()"); + + mTracker.connectNetwork(iface, listener); + } + + @Override + public void disconnectNetwork(@NonNull final String iface, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + Log.i(TAG, "disconnectNetwork called with: iface=" + iface + ", listener=" + listener); + Objects.requireNonNull(iface); + throwIfEthernetNotStarted(); + + enforceAdminPermission(iface, true, "connectNetwork()"); + + mTracker.disconnectNetwork(iface, listener); + } + + @Override + public void setEthernetEnabled(boolean enabled) { + PermissionUtils.enforceNetworkStackPermissionOr(mContext, + android.Manifest.permission.NETWORK_SETTINGS); + + mTracker.setEthernetEnabled(enabled); + } + + @Override + public List<String> getInterfaceList() { + PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG); + return mTracker.getInterfaceList(); + } +} diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java new file mode 100644 index 0000000000..c291b3ff66 --- /dev/null +++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java @@ -0,0 +1,942 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.ethernet; + +import static android.net.EthernetManager.ETHERNET_STATE_DISABLED; +import static android.net.EthernetManager.ETHERNET_STATE_ENABLED; +import static android.net.TestNetworkManager.TEST_TAP_PREFIX; + +import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.net.ConnectivityResources; +import android.net.EthernetManager; +import android.net.IEthernetServiceListener; +import android.net.INetworkInterfaceOutcomeReceiver; +import android.net.INetd; +import android.net.ITetheredInterfaceCallback; +import android.net.InterfaceConfigurationParcel; +import android.net.IpConfiguration; +import android.net.IpConfiguration.IpAssignment; +import android.net.IpConfiguration.ProxySettings; +import android.net.LinkAddress; +import android.net.NetworkCapabilities; +import android.net.StaticIpConfiguration; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.net.module.util.BaseNetdUnsolicitedEventListener; +import com.android.net.module.util.NetdUtils; +import com.android.net.module.util.PermissionUtils; + +import java.io.FileDescriptor; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Tracks Ethernet interfaces and manages interface configurations. + * + * <p>Interfaces may have different {@link android.net.NetworkCapabilities}. This mapping is defined + * in {@code config_ethernet_interfaces}. Notably, some interfaces could be marked as restricted by + * not specifying {@link android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED} flag. + * Interfaces could have associated {@link android.net.IpConfiguration}. + * Ethernet Interfaces may be present at boot time or appear after boot (e.g., for Ethernet adapters + * connected over USB). This class supports multiple interfaces. When an interface appears on the + * system (or is present at boot time) this class will start tracking it and bring it up. Only + * interfaces whose names match the {@code config_ethernet_iface_regex} regular expression are + * tracked. + * + * <p>All public or package private methods must be thread-safe unless stated otherwise. + */ +@VisibleForTesting(visibility = PACKAGE) +public class EthernetTracker { + private static final int INTERFACE_MODE_CLIENT = 1; + private static final int INTERFACE_MODE_SERVER = 2; + + private static final String TAG = EthernetTracker.class.getSimpleName(); + private static final boolean DBG = EthernetNetworkFactory.DBG; + + private static final String TEST_IFACE_REGEXP = TEST_TAP_PREFIX + "\\d+"; + private static final String LEGACY_IFACE_REGEXP = "eth\\d"; + + /** + * Interface names we track. This is a product-dependent regular expression, plus, + * if setIncludeTestInterfaces is true, any test interfaces. + */ + private volatile String mIfaceMatch; + /** + * Track test interfaces if true, don't track otherwise. + */ + private boolean mIncludeTestInterfaces = false; + + /** Mapping between {iface name | mac address} -> {NetworkCapabilities} */ + private final ConcurrentHashMap<String, NetworkCapabilities> mNetworkCapabilities = + new ConcurrentHashMap<>(); + private final ConcurrentHashMap<String, IpConfiguration> mIpConfigurations = + new ConcurrentHashMap<>(); + + private final Context mContext; + private final INetd mNetd; + private final Handler mHandler; + private final EthernetNetworkFactory mFactory; + private final EthernetConfigStore mConfigStore; + private final Dependencies mDeps; + + private final RemoteCallbackList<IEthernetServiceListener> mListeners = + new RemoteCallbackList<>(); + private final TetheredInterfaceRequestList mTetheredInterfaceRequests = + new TetheredInterfaceRequestList(); + + // Used only on the handler thread + private String mDefaultInterface; + private int mDefaultInterfaceMode = INTERFACE_MODE_CLIENT; + // Tracks whether clients were notified that the tethered interface is available + private boolean mTetheredInterfaceWasAvailable = false; + private volatile IpConfiguration mIpConfigForDefaultInterface; + + private int mEthernetState = ETHERNET_STATE_ENABLED; + + private class TetheredInterfaceRequestList extends + RemoteCallbackList<ITetheredInterfaceCallback> { + @Override + public void onCallbackDied(ITetheredInterfaceCallback cb, Object cookie) { + mHandler.post(EthernetTracker.this::maybeUntetherDefaultInterface); + } + } + + public static class Dependencies { + // TODO: remove legacy resource fallback after migrating its overlays. + private String getPlatformRegexResource(Context context) { + final Resources r = context.getResources(); + final int resId = + r.getIdentifier("config_ethernet_iface_regex", "string", context.getPackageName()); + return r.getString(resId); + } + + // TODO: remove legacy resource fallback after migrating its overlays. + private String[] getPlatformInterfaceConfigs(Context context) { + final Resources r = context.getResources(); + final int resId = r.getIdentifier("config_ethernet_interfaces", "array", + context.getPackageName()); + return r.getStringArray(resId); + } + + public String getInterfaceRegexFromResource(Context context) { + final String platformRegex = getPlatformRegexResource(context); + final String match; + if (!LEGACY_IFACE_REGEXP.equals(platformRegex)) { + // Platform resource is not the historical default: use the overlay + match = platformRegex; + } else { + final ConnectivityResources resources = new ConnectivityResources(context); + match = resources.get().getString( + com.android.connectivity.resources.R.string.config_ethernet_iface_regex); + } + return match; + } + + public String[] getInterfaceConfigFromResource(Context context) { + final String[] platformInterfaceConfigs = getPlatformInterfaceConfigs(context); + final String[] interfaceConfigs; + if (platformInterfaceConfigs.length != 0) { + // Platform resource is not the historical default: use the overlay + interfaceConfigs = platformInterfaceConfigs; + } else { + final ConnectivityResources resources = new ConnectivityResources(context); + interfaceConfigs = resources.get().getStringArray( + com.android.connectivity.resources.R.array.config_ethernet_interfaces); + } + return interfaceConfigs; + } + } + + EthernetTracker(@NonNull final Context context, @NonNull final Handler handler, + @NonNull final EthernetNetworkFactory factory, @NonNull final INetd netd) { + this(context, handler, factory, netd, new Dependencies()); + } + + @VisibleForTesting + EthernetTracker(@NonNull final Context context, @NonNull final Handler handler, + @NonNull final EthernetNetworkFactory factory, @NonNull final INetd netd, + @NonNull final Dependencies deps) { + mContext = context; + mHandler = handler; + mFactory = factory; + mNetd = netd; + mDeps = deps; + + // Interface match regex. + updateIfaceMatchRegexp(); + + // Read default Ethernet interface configuration from resources + final String[] interfaceConfigs = mDeps.getInterfaceConfigFromResource(context); + for (String strConfig : interfaceConfigs) { + parseEthernetConfig(strConfig); + } + + mConfigStore = new EthernetConfigStore(); + } + + void start() { + mFactory.register(); + mConfigStore.read(); + + // Default interface is just the first one we want to track. + mIpConfigForDefaultInterface = mConfigStore.getIpConfigurationForDefaultInterface(); + final ArrayMap<String, IpConfiguration> configs = mConfigStore.getIpConfigurations(); + for (int i = 0; i < configs.size(); i++) { + mIpConfigurations.put(configs.keyAt(i), configs.valueAt(i)); + } + + try { + PermissionUtils.enforceNetworkStackPermission(mContext); + mNetd.registerUnsolicitedEventListener(new InterfaceObserver()); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Could not register InterfaceObserver " + e); + } + + mHandler.post(this::trackAvailableInterfaces); + } + + void updateIpConfiguration(String iface, IpConfiguration ipConfiguration) { + if (DBG) { + Log.i(TAG, "updateIpConfiguration, iface: " + iface + ", cfg: " + ipConfiguration); + } + writeIpConfiguration(iface, ipConfiguration); + mHandler.post(() -> { + mFactory.updateInterface(iface, ipConfiguration, null, null); + broadcastInterfaceStateChange(iface); + }); + } + + private void writeIpConfiguration(@NonNull final String iface, + @NonNull final IpConfiguration ipConfig) { + mConfigStore.write(iface, ipConfig); + mIpConfigurations.put(iface, ipConfig); + } + + private IpConfiguration getIpConfigurationForCallback(String iface, int state) { + return (state == EthernetManager.STATE_ABSENT) ? null : getOrCreateIpConfiguration(iface); + } + + private void ensureRunningOnEthernetServiceThread() { + if (mHandler.getLooper().getThread() != Thread.currentThread()) { + throw new IllegalStateException( + "Not running on EthernetService thread: " + + Thread.currentThread().getName()); + } + } + + /** + * Broadcast the link state or IpConfiguration change of existing Ethernet interfaces to all + * listeners. + */ + protected void broadcastInterfaceStateChange(@NonNull String iface) { + ensureRunningOnEthernetServiceThread(); + final int state = mFactory.getInterfaceState(iface); + final int role = getInterfaceRole(iface); + final IpConfiguration config = getIpConfigurationForCallback(iface, state); + final int n = mListeners.beginBroadcast(); + for (int i = 0; i < n; i++) { + try { + mListeners.getBroadcastItem(i).onInterfaceStateChanged(iface, state, role, config); + } catch (RemoteException e) { + // Do nothing here. + } + } + mListeners.finishBroadcast(); + } + + /** + * Unicast the interface state or IpConfiguration change of existing Ethernet interfaces to a + * specific listener. + */ + protected void unicastInterfaceStateChange(@NonNull IEthernetServiceListener listener, + @NonNull String iface) { + ensureRunningOnEthernetServiceThread(); + final int state = mFactory.getInterfaceState(iface); + final int role = getInterfaceRole(iface); + final IpConfiguration config = getIpConfigurationForCallback(iface, state); + try { + listener.onInterfaceStateChanged(iface, state, role, config); + } catch (RemoteException e) { + // Do nothing here. + } + } + + @VisibleForTesting(visibility = PACKAGE) + protected void updateConfiguration(@NonNull final String iface, + @Nullable final IpConfiguration ipConfig, + @Nullable final NetworkCapabilities capabilities, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + if (DBG) { + Log.i(TAG, "updateConfiguration, iface: " + iface + ", capabilities: " + capabilities + + ", ipConfig: " + ipConfig); + } + + final IpConfiguration localIpConfig = ipConfig == null + ? null : new IpConfiguration(ipConfig); + if (ipConfig != null) { + writeIpConfiguration(iface, localIpConfig); + } + + if (null != capabilities) { + mNetworkCapabilities.put(iface, capabilities); + } + mHandler.post(() -> { + mFactory.updateInterface(iface, localIpConfig, capabilities, listener); + broadcastInterfaceStateChange(iface); + }); + } + + @VisibleForTesting(visibility = PACKAGE) + protected void connectNetwork(@NonNull final String iface, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + mHandler.post(() -> updateInterfaceState(iface, true, listener)); + } + + @VisibleForTesting(visibility = PACKAGE) + protected void disconnectNetwork(@NonNull final String iface, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + mHandler.post(() -> updateInterfaceState(iface, false, listener)); + } + + IpConfiguration getIpConfiguration(String iface) { + return mIpConfigurations.get(iface); + } + + @VisibleForTesting(visibility = PACKAGE) + protected boolean isTrackingInterface(String iface) { + return mFactory.hasInterface(iface); + } + + String[] getInterfaces(boolean includeRestricted) { + return mFactory.getAvailableInterfaces(includeRestricted); + } + + List<String> getInterfaceList() { + final List<String> interfaceList = new ArrayList<String>(); + final String[] ifaces; + try { + ifaces = mNetd.interfaceGetList(); + } catch (RemoteException e) { + Log.e(TAG, "Could not get list of interfaces " + e); + return interfaceList; + } + final String ifaceMatch = mIfaceMatch; + for (String iface : ifaces) { + if (iface.matches(ifaceMatch)) interfaceList.add(iface); + } + return interfaceList; + } + + /** + * Returns true if given interface was configured as restricted (doesn't have + * NET_CAPABILITY_NOT_RESTRICTED) capability. Otherwise, returns false. + */ + boolean isRestrictedInterface(String iface) { + final NetworkCapabilities nc = mNetworkCapabilities.get(iface); + return nc != null && !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED); + } + + void addListener(IEthernetServiceListener listener, boolean canUseRestrictedNetworks) { + mHandler.post(() -> { + if (!mListeners.register(listener, new ListenerInfo(canUseRestrictedNetworks))) { + // Remote process has already died + return; + } + for (String iface : getInterfaces(canUseRestrictedNetworks)) { + unicastInterfaceStateChange(listener, iface); + } + + unicastEthernetStateChange(listener, mEthernetState); + }); + } + + void removeListener(IEthernetServiceListener listener) { + mHandler.post(() -> mListeners.unregister(listener)); + } + + public void setIncludeTestInterfaces(boolean include) { + mHandler.post(() -> { + mIncludeTestInterfaces = include; + updateIfaceMatchRegexp(); + mHandler.post(() -> trackAvailableInterfaces()); + }); + } + + public void requestTetheredInterface(ITetheredInterfaceCallback callback) { + mHandler.post(() -> { + if (!mTetheredInterfaceRequests.register(callback)) { + // Remote process has already died + return; + } + if (mDefaultInterfaceMode == INTERFACE_MODE_SERVER) { + if (mTetheredInterfaceWasAvailable) { + notifyTetheredInterfaceAvailable(callback, mDefaultInterface); + } + return; + } + + setDefaultInterfaceMode(INTERFACE_MODE_SERVER); + }); + } + + public void releaseTetheredInterface(ITetheredInterfaceCallback callback) { + mHandler.post(() -> { + mTetheredInterfaceRequests.unregister(callback); + maybeUntetherDefaultInterface(); + }); + } + + private void notifyTetheredInterfaceAvailable(ITetheredInterfaceCallback cb, String iface) { + try { + cb.onAvailable(iface); + } catch (RemoteException e) { + Log.e(TAG, "Error sending tethered interface available callback", e); + } + } + + private void notifyTetheredInterfaceUnavailable(ITetheredInterfaceCallback cb) { + try { + cb.onUnavailable(); + } catch (RemoteException e) { + Log.e(TAG, "Error sending tethered interface available callback", e); + } + } + + private void maybeUntetherDefaultInterface() { + if (mTetheredInterfaceRequests.getRegisteredCallbackCount() > 0) return; + if (mDefaultInterfaceMode == INTERFACE_MODE_CLIENT) return; + setDefaultInterfaceMode(INTERFACE_MODE_CLIENT); + } + + private void setDefaultInterfaceMode(int mode) { + Log.d(TAG, "Setting default interface mode to " + mode); + mDefaultInterfaceMode = mode; + if (mDefaultInterface != null) { + removeInterface(mDefaultInterface); + addInterface(mDefaultInterface); + } + } + + private int getInterfaceRole(final String iface) { + if (!mFactory.hasInterface(iface)) return EthernetManager.ROLE_NONE; + final int mode = getInterfaceMode(iface); + return (mode == INTERFACE_MODE_CLIENT) + ? EthernetManager.ROLE_CLIENT + : EthernetManager.ROLE_SERVER; + } + + private int getInterfaceMode(final String iface) { + if (iface.equals(mDefaultInterface)) { + return mDefaultInterfaceMode; + } + return INTERFACE_MODE_CLIENT; + } + + private void removeInterface(String iface) { + mFactory.removeInterface(iface); + maybeUpdateServerModeInterfaceState(iface, false); + } + + private void stopTrackingInterface(String iface) { + removeInterface(iface); + if (iface.equals(mDefaultInterface)) { + mDefaultInterface = null; + } + broadcastInterfaceStateChange(iface); + } + + private void addInterface(String iface) { + InterfaceConfigurationParcel config = null; + // Bring up the interface so we get link status indications. + try { + PermissionUtils.enforceNetworkStackPermission(mContext); + NetdUtils.setInterfaceUp(mNetd, iface); + config = NetdUtils.getInterfaceConfigParcel(mNetd, iface); + } catch (IllegalStateException e) { + // Either the system is crashing or the interface has disappeared. Just ignore the + // error; we haven't modified any state because we only do that if our calls succeed. + Log.e(TAG, "Error upping interface " + iface, e); + } + + if (config == null) { + Log.e(TAG, "Null interface config parcelable for " + iface + ". Bailing out."); + return; + } + + final String hwAddress = config.hwAddr; + + NetworkCapabilities nc = mNetworkCapabilities.get(iface); + if (nc == null) { + // Try to resolve using mac address + nc = mNetworkCapabilities.get(hwAddress); + if (nc == null) { + final boolean isTestIface = iface.matches(TEST_IFACE_REGEXP); + nc = createDefaultNetworkCapabilities(isTestIface); + } + } + + final int mode = getInterfaceMode(iface); + if (mode == INTERFACE_MODE_CLIENT) { + IpConfiguration ipConfiguration = getOrCreateIpConfiguration(iface); + Log.d(TAG, "Tracking interface in client mode: " + iface); + mFactory.addInterface(iface, hwAddress, ipConfiguration, nc); + } else { + maybeUpdateServerModeInterfaceState(iface, true); + } + + // Note: if the interface already has link (e.g., if we crashed and got + // restarted while it was running), we need to fake a link up notification so we + // start configuring it. + if (NetdUtils.hasFlag(config, "running")) { + updateInterfaceState(iface, true); + } + } + + private void updateInterfaceState(String iface, boolean up) { + updateInterfaceState(iface, up, null /* listener */); + } + + private void updateInterfaceState(@NonNull final String iface, final boolean up, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + final int mode = getInterfaceMode(iface); + final boolean factoryLinkStateUpdated = (mode == INTERFACE_MODE_CLIENT) + && mFactory.updateInterfaceLinkState(iface, up, listener); + + if (factoryLinkStateUpdated) { + broadcastInterfaceStateChange(iface); + } + } + + private void maybeUpdateServerModeInterfaceState(String iface, boolean available) { + if (available == mTetheredInterfaceWasAvailable || !iface.equals(mDefaultInterface)) return; + + Log.d(TAG, (available ? "Tracking" : "No longer tracking") + + " interface in server mode: " + iface); + + final int pendingCbs = mTetheredInterfaceRequests.beginBroadcast(); + for (int i = 0; i < pendingCbs; i++) { + ITetheredInterfaceCallback item = mTetheredInterfaceRequests.getBroadcastItem(i); + if (available) { + notifyTetheredInterfaceAvailable(item, iface); + } else { + notifyTetheredInterfaceUnavailable(item); + } + } + mTetheredInterfaceRequests.finishBroadcast(); + mTetheredInterfaceWasAvailable = available; + } + + private void maybeTrackInterface(String iface) { + if (!iface.matches(mIfaceMatch)) { + return; + } + + // If we don't already track this interface, and if this interface matches + // our regex, start tracking it. + if (mFactory.hasInterface(iface) || iface.equals(mDefaultInterface)) { + if (DBG) Log.w(TAG, "Ignoring already-tracked interface " + iface); + return; + } + if (DBG) Log.i(TAG, "maybeTrackInterface: " + iface); + + // TODO: avoid making an interface default if it has configured NetworkCapabilities. + if (mDefaultInterface == null) { + mDefaultInterface = iface; + } + + if (mIpConfigForDefaultInterface != null) { + updateIpConfiguration(iface, mIpConfigForDefaultInterface); + mIpConfigForDefaultInterface = null; + } + + addInterface(iface); + + broadcastInterfaceStateChange(iface); + } + + private void trackAvailableInterfaces() { + try { + final String[] ifaces = mNetd.interfaceGetList(); + for (String iface : ifaces) { + maybeTrackInterface(iface); + } + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Could not get list of interfaces " + e); + } + } + + private class InterfaceObserver extends BaseNetdUnsolicitedEventListener { + + @Override + public void onInterfaceLinkStateChanged(String iface, boolean up) { + if (DBG) { + Log.i(TAG, "interfaceLinkStateChanged, iface: " + iface + ", up: " + up); + } + mHandler.post(() -> updateInterfaceState(iface, up)); + } + + @Override + public void onInterfaceAdded(String iface) { + if (DBG) { + Log.i(TAG, "onInterfaceAdded, iface: " + iface); + } + mHandler.post(() -> maybeTrackInterface(iface)); + } + + @Override + public void onInterfaceRemoved(String iface) { + if (DBG) { + Log.i(TAG, "onInterfaceRemoved, iface: " + iface); + } + mHandler.post(() -> stopTrackingInterface(iface)); + } + } + + private static class ListenerInfo { + + boolean canUseRestrictedNetworks = false; + + ListenerInfo(boolean canUseRestrictedNetworks) { + this.canUseRestrictedNetworks = canUseRestrictedNetworks; + } + } + + /** + * Parses an Ethernet interface configuration + * + * @param configString represents an Ethernet configuration in the following format: {@code + * <interface name|mac address>;[Network Capabilities];[IP config];[Override Transport]} + */ + private void parseEthernetConfig(String configString) { + final EthernetTrackerConfig config = createEthernetTrackerConfig(configString); + NetworkCapabilities nc = createNetworkCapabilities( + !TextUtils.isEmpty(config.mCapabilities) /* clear default capabilities */, + config.mCapabilities, config.mTransport).build(); + mNetworkCapabilities.put(config.mIface, nc); + + if (null != config.mIpConfig) { + IpConfiguration ipConfig = parseStaticIpConfiguration(config.mIpConfig); + mIpConfigurations.put(config.mIface, ipConfig); + } + } + + @VisibleForTesting + static EthernetTrackerConfig createEthernetTrackerConfig(@NonNull final String configString) { + Objects.requireNonNull(configString, "EthernetTrackerConfig requires non-null config"); + return new EthernetTrackerConfig(configString.split(";", /* limit of tokens */ 4)); + } + + private static NetworkCapabilities createDefaultNetworkCapabilities(boolean isTestIface) { + NetworkCapabilities.Builder builder = createNetworkCapabilities( + false /* clear default capabilities */, null, null) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED); + + if (isTestIface) { + builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST); + } else { + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + } + + return builder.build(); + } + + /** + * Parses a static list of network capabilities + * + * @param clearDefaultCapabilities Indicates whether or not to clear any default capabilities + * @param commaSeparatedCapabilities A comma separated string list of integer encoded + * NetworkCapability.NET_CAPABILITY_* values + * @param overrideTransport A string representing a single integer encoded override transport + * type. Must be one of the NetworkCapability.TRANSPORT_* + * values. TRANSPORT_VPN is not supported. Errors with input + * will cause the override to be ignored. + */ + @VisibleForTesting + static NetworkCapabilities.Builder createNetworkCapabilities( + boolean clearDefaultCapabilities, @Nullable String commaSeparatedCapabilities, + @Nullable String overrideTransport) { + + final NetworkCapabilities.Builder builder = clearDefaultCapabilities + ? NetworkCapabilities.Builder.withoutDefaultCapabilities() + : new NetworkCapabilities.Builder(); + + // Determine the transport type. If someone has tried to define an override transport then + // attempt to add it. Since we can only have one override, all errors with it will + // gracefully default back to TRANSPORT_ETHERNET and warn the user. VPN is not allowed as an + // override type. Wifi Aware and LoWPAN are currently unsupported as well. + int transport = NetworkCapabilities.TRANSPORT_ETHERNET; + if (!TextUtils.isEmpty(overrideTransport)) { + try { + int parsedTransport = Integer.valueOf(overrideTransport); + if (parsedTransport == NetworkCapabilities.TRANSPORT_VPN + || parsedTransport == NetworkCapabilities.TRANSPORT_WIFI_AWARE + || parsedTransport == NetworkCapabilities.TRANSPORT_LOWPAN) { + Log.e(TAG, "Override transport '" + parsedTransport + "' is not supported. " + + "Defaulting to TRANSPORT_ETHERNET"); + } else { + transport = parsedTransport; + } + } catch (NumberFormatException nfe) { + Log.e(TAG, "Override transport type '" + overrideTransport + "' " + + "could not be parsed. Defaulting to TRANSPORT_ETHERNET"); + } + } + + // Apply the transport. If the user supplied a valid number that is not a valid transport + // then adding will throw an exception. Default back to TRANSPORT_ETHERNET if that happens + try { + builder.addTransportType(transport); + } catch (IllegalArgumentException iae) { + Log.e(TAG, transport + " is not a valid NetworkCapability.TRANSPORT_* value. " + + "Defaulting to TRANSPORT_ETHERNET"); + builder.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET); + } + + builder.setLinkUpstreamBandwidthKbps(100 * 1000); + builder.setLinkDownstreamBandwidthKbps(100 * 1000); + + if (!TextUtils.isEmpty(commaSeparatedCapabilities)) { + for (String strNetworkCapability : commaSeparatedCapabilities.split(",")) { + if (!TextUtils.isEmpty(strNetworkCapability)) { + try { + builder.addCapability(Integer.valueOf(strNetworkCapability)); + } catch (NumberFormatException nfe) { + Log.e(TAG, "Capability '" + strNetworkCapability + "' could not be parsed"); + } catch (IllegalArgumentException iae) { + Log.e(TAG, strNetworkCapability + " is not a valid " + + "NetworkCapability.NET_CAPABILITY_* value"); + } + } + } + } + // Ethernet networks have no way to update the following capabilities, so they always + // have them. + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING); + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED); + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED); + + return builder; + } + + /** + * Parses static IP configuration. + * + * @param staticIpConfig represents static IP configuration in the following format: {@code + * ip=<ip-address/mask> gateway=<ip-address> dns=<comma-sep-ip-addresses> + * domains=<comma-sep-domains>} + */ + @VisibleForTesting + static IpConfiguration parseStaticIpConfiguration(String staticIpConfig) { + final StaticIpConfiguration.Builder staticIpConfigBuilder = + new StaticIpConfiguration.Builder(); + + for (String keyValueAsString : staticIpConfig.trim().split(" ")) { + if (TextUtils.isEmpty(keyValueAsString)) continue; + + String[] pair = keyValueAsString.split("="); + if (pair.length != 2) { + throw new IllegalArgumentException("Unexpected token: " + keyValueAsString + + " in " + staticIpConfig); + } + + String key = pair[0]; + String value = pair[1]; + + switch (key) { + case "ip": + staticIpConfigBuilder.setIpAddress(new LinkAddress(value)); + break; + case "domains": + staticIpConfigBuilder.setDomains(value); + break; + case "gateway": + staticIpConfigBuilder.setGateway(InetAddress.parseNumericAddress(value)); + break; + case "dns": { + ArrayList<InetAddress> dnsAddresses = new ArrayList<>(); + for (String address: value.split(",")) { + dnsAddresses.add(InetAddress.parseNumericAddress(address)); + } + staticIpConfigBuilder.setDnsServers(dnsAddresses); + break; + } + default : { + throw new IllegalArgumentException("Unexpected key: " + key + + " in " + staticIpConfig); + } + } + } + return createIpConfiguration(staticIpConfigBuilder.build()); + } + + private static IpConfiguration createIpConfiguration( + @NonNull final StaticIpConfiguration staticIpConfig) { + return new IpConfiguration.Builder().setStaticIpConfiguration(staticIpConfig).build(); + } + + private IpConfiguration getOrCreateIpConfiguration(String iface) { + IpConfiguration ret = mIpConfigurations.get(iface); + if (ret != null) return ret; + ret = new IpConfiguration(); + ret.setIpAssignment(IpAssignment.DHCP); + ret.setProxySettings(ProxySettings.NONE); + return ret; + } + + private void updateIfaceMatchRegexp() { + final String match = mDeps.getInterfaceRegexFromResource(mContext); + mIfaceMatch = mIncludeTestInterfaces + ? "(" + match + "|" + TEST_IFACE_REGEXP + ")" + : match; + Log.d(TAG, "Interface match regexp set to '" + mIfaceMatch + "'"); + } + + /** + * Validate if a given interface is valid for testing. + * + * @param iface the name of the interface to validate. + * @return {@code true} if test interfaces are enabled and the given {@code iface} has a test + * interface prefix, {@code false} otherwise. + */ + public boolean isValidTestInterface(@NonNull final String iface) { + return mIncludeTestInterfaces && iface.matches(TEST_IFACE_REGEXP); + } + + private void postAndWaitForRunnable(Runnable r) { + final ConditionVariable cv = new ConditionVariable(); + if (mHandler.post(() -> { + r.run(); + cv.open(); + })) { + cv.block(2000L); + } + } + + @VisibleForTesting(visibility = PACKAGE) + protected void setEthernetEnabled(boolean enabled) { + mHandler.post(() -> { + int newState = enabled ? ETHERNET_STATE_ENABLED : ETHERNET_STATE_DISABLED; + if (mEthernetState == newState) return; + + mEthernetState = newState; + + if (enabled) { + trackAvailableInterfaces(); + } else { + // TODO: maybe also disable server mode interface as well. + untrackFactoryInterfaces(); + } + broadcastEthernetStateChange(mEthernetState); + }); + } + + private void untrackFactoryInterfaces() { + for (String iface : mFactory.getAvailableInterfaces(true /* includeRestricted */)) { + stopTrackingInterface(iface); + } + } + + private void unicastEthernetStateChange(@NonNull IEthernetServiceListener listener, + int state) { + ensureRunningOnEthernetServiceThread(); + try { + listener.onEthernetStateChanged(state); + } catch (RemoteException e) { + // Do nothing here. + } + } + + private void broadcastEthernetStateChange(int state) { + ensureRunningOnEthernetServiceThread(); + final int n = mListeners.beginBroadcast(); + for (int i = 0; i < n; i++) { + try { + mListeners.getBroadcastItem(i).onEthernetStateChanged(state); + } catch (RemoteException e) { + // Do nothing here. + } + } + mListeners.finishBroadcast(); + } + + void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) { + postAndWaitForRunnable(() -> { + pw.println(getClass().getSimpleName()); + pw.println("Ethernet interface name filter: " + mIfaceMatch); + pw.println("Default interface: " + mDefaultInterface); + pw.println("Default interface mode: " + mDefaultInterfaceMode); + pw.println("Tethered interface requests: " + + mTetheredInterfaceRequests.getRegisteredCallbackCount()); + pw.println("Listeners: " + mListeners.getRegisteredCallbackCount()); + pw.println("IP Configurations:"); + pw.increaseIndent(); + for (String iface : mIpConfigurations.keySet()) { + pw.println(iface + ": " + mIpConfigurations.get(iface)); + } + pw.decreaseIndent(); + pw.println(); + + pw.println("Network Capabilities:"); + pw.increaseIndent(); + for (String iface : mNetworkCapabilities.keySet()) { + pw.println(iface + ": " + mNetworkCapabilities.get(iface)); + } + pw.decreaseIndent(); + pw.println(); + + mFactory.dump(fd, pw, args); + }); + } + + @VisibleForTesting + static class EthernetTrackerConfig { + final String mIface; + final String mCapabilities; + final String mIpConfig; + final String mTransport; + + EthernetTrackerConfig(@NonNull final String[] tokens) { + Objects.requireNonNull(tokens, "EthernetTrackerConfig requires non-null tokens"); + mIface = tokens[0]; + mCapabilities = tokens.length > 1 ? tokens[1] : null; + mIpConfig = tokens.length > 2 && !TextUtils.isEmpty(tokens[2]) ? tokens[2] : null; + mTransport = tokens.length > 3 ? tokens[3] : null; + } + } +} diff --git a/service/Android.bp b/service/Android.bp index 0e6fe92b3b..25b970a4d5 100644 --- a/service/Android.bp +++ b/service/Android.bp @@ -19,6 +19,54 @@ package { default_applicable_licenses: ["Android-Apache-2.0"], } +aidl_interface { + name: "connectivity_native_aidl_interface", + local_include_dir: "binder", + vendor_available: true, + srcs: [ + "binder/android/net/connectivity/aidl/*.aidl", + ], + backend: { + java: { + apex_available: [ + "com.android.tethering", + ], + min_sdk_version: "30", + }, + ndk: { + apex_available: [ + "com.android.tethering", + ], + min_sdk_version: "30", + }, + }, + versions: ["1"], + +} + +cc_library_static { + name: "connectivity_native_aidl_interface-lateststable-ndk", + min_sdk_version: "30", + whole_static_libs: [ + "connectivity_native_aidl_interface-V1-ndk", + ], + apex_available: [ + "com.android.tethering", + ], +} + +java_library { + name: "connectivity_native_aidl_interface-lateststable-java", + sdk_version: "system_current", + min_sdk_version: "30", + static_libs: [ + "connectivity_native_aidl_interface-V1-java", + ], + apex_available: [ + "com.android.tethering", + ], +} + // The library name match the service-connectivity jarjar rules that put the JNI utils in the // android.net.connectivity.com.android.net.module.util package. cc_library_shared { @@ -35,6 +83,7 @@ cc_library_shared { ], static_libs: [ "libnet_utils_device_common_bpfjni", + "libnet_utils_device_common_bpfutils", ], shared_libs: [ "liblog", @@ -109,6 +158,7 @@ java_library { static_libs: [ // Do not add libs here if they are already included // in framework-connectivity + "connectivity_native_aidl_interface-lateststable-java", "dnsresolver_aidl_interface-V9-java", "modules-utils-shell-command-handler", "net-utils-device-common", diff --git a/service/aidl_api/connectivity_native_aidl_interface/1/.hash b/service/aidl_api/connectivity_native_aidl_interface/1/.hash new file mode 100644 index 0000000000..4625b4b942 --- /dev/null +++ b/service/aidl_api/connectivity_native_aidl_interface/1/.hash @@ -0,0 +1 @@ +037b467eb02b172a3161e11bbc3dd691aebb5fce diff --git a/service/aidl_api/connectivity_native_aidl_interface/1/android/net/connectivity/aidl/ConnectivityNative.aidl b/service/aidl_api/connectivity_native_aidl_interface/1/android/net/connectivity/aidl/ConnectivityNative.aidl new file mode 100644 index 0000000000..b3985a470f --- /dev/null +++ b/service/aidl_api/connectivity_native_aidl_interface/1/android/net/connectivity/aidl/ConnectivityNative.aidl @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2022, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/////////////////////////////////////////////////////////////////////////////// +// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. // +/////////////////////////////////////////////////////////////////////////////// + +// This file is a snapshot of an AIDL file. Do not edit it manually. There are +// two cases: +// 1). this is a frozen version file - do not edit this in any case. +// 2). this is a 'current' file. If you make a backwards compatible change to +// the interface (from the latest frozen version), the build system will +// prompt you to update this file with `m <name>-update-api`. +// +// You must not make a backward incompatible change to any AIDL file built +// with the aidl_interface module type with versions property set. The module +// type is used to build AIDL files in a way that they can be used across +// independently updatable components of the system. If a device is shipped +// with such a backward incompatible change, it has a high risk of breaking +// later when a module using the interface is updated, e.g., Mainline modules. + +package android.net.connectivity.aidl; +interface ConnectivityNative { + void blockPortForBind(in int port); + void unblockPortForBind(in int port); + void unblockAllPortsForBind(); + int[] getPortsBlockedForBind(); +} diff --git a/service/aidl_api/connectivity_native_aidl_interface/current/android/net/connectivity/aidl/ConnectivityNative.aidl b/service/aidl_api/connectivity_native_aidl_interface/current/android/net/connectivity/aidl/ConnectivityNative.aidl new file mode 100644 index 0000000000..b3985a470f --- /dev/null +++ b/service/aidl_api/connectivity_native_aidl_interface/current/android/net/connectivity/aidl/ConnectivityNative.aidl @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2022, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/////////////////////////////////////////////////////////////////////////////// +// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. // +/////////////////////////////////////////////////////////////////////////////// + +// This file is a snapshot of an AIDL file. Do not edit it manually. There are +// two cases: +// 1). this is a frozen version file - do not edit this in any case. +// 2). this is a 'current' file. If you make a backwards compatible change to +// the interface (from the latest frozen version), the build system will +// prompt you to update this file with `m <name>-update-api`. +// +// You must not make a backward incompatible change to any AIDL file built +// with the aidl_interface module type with versions property set. The module +// type is used to build AIDL files in a way that they can be used across +// independently updatable components of the system. If a device is shipped +// with such a backward incompatible change, it has a high risk of breaking +// later when a module using the interface is updated, e.g., Mainline modules. + +package android.net.connectivity.aidl; +interface ConnectivityNative { + void blockPortForBind(in int port); + void unblockPortForBind(in int port); + void unblockAllPortsForBind(); + int[] getPortsBlockedForBind(); +} diff --git a/service/binder/android/net/connectivity/aidl/ConnectivityNative.aidl b/service/binder/android/net/connectivity/aidl/ConnectivityNative.aidl new file mode 100644 index 0000000000..31e24b4004 --- /dev/null +++ b/service/binder/android/net/connectivity/aidl/ConnectivityNative.aidl @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2022, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.connectivity.aidl; + +interface ConnectivityNative { + /** + * Blocks a port from being assigned during bind(). The caller is responsible for updating + * /proc/sys/net/ipv4/ip_local_port_range with the port being blocked so that calls to connect() + * will not automatically assign one of the blocked ports. + * Will return success even if port was already blocked. + * + * @param port Int corresponding to port number. + * + * @throws IllegalArgumentException if the port is invalid. + * @throws SecurityException if the UID of the client doesn't have network stack permission. + * @throws ServiceSpecificException in case of failure, with an error code corresponding to the + * unix errno. + */ + void blockPortForBind(in int port); + + /** + * Unblocks a port that has previously been blocked. + * Will return success even if port was already unblocked. + * + * @param port Int corresponding to port number. + * + * @throws IllegalArgumentException if the port is invalid. + * @throws SecurityException if the UID of the client doesn't have network stack permission. + * @throws ServiceSpecificException in case of failure, with an error code corresponding to the + * unix errno. + */ + void unblockPortForBind(in int port); + + /** + * Unblocks all ports that have previously been blocked. + */ + void unblockAllPortsForBind(); + + /** + * Gets the list of ports that have been blocked. + * + * @return List of blocked ports. + */ + int[] getPortsBlockedForBind(); +}
\ No newline at end of file diff --git a/service/jni/com_android_net_module_util/onload.cpp b/service/jni/com_android_net_module_util/onload.cpp index 2f09e55864..d91eb0305c 100644 --- a/service/jni/com_android_net_module_util/onload.cpp +++ b/service/jni/com_android_net_module_util/onload.cpp @@ -21,6 +21,7 @@ namespace android { int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name); int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name); +int register_com_android_net_module_util_BpfUtils(JNIEnv* env, char const* class_name); extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { JNIEnv *env; @@ -35,6 +36,9 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { if (register_com_android_net_module_util_TcUtils(env, "android/net/connectivity/com/android/net/module/util/TcUtils") < 0) return JNI_ERR; + if (register_com_android_net_module_util_BpfUtils(env, + "android/net/connectivity/com/android/net/module/util/BpfUtils") < 0) return JNI_ERR; + return JNI_VERSION_1_6; } diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index b4cc41a94d..d79bdb8320 100644 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -2238,6 +2238,13 @@ public class ConnectivityService extends IConnectivityManager.Stub callingAttributionTag); } + private void redactUnderlyingNetworksForCapabilities(NetworkCapabilities nc, int pid, int uid) { + if (nc.getUnderlyingNetworks() != null + && !checkNetworkFactoryOrSettingsPermission(pid, uid)) { + nc.setUnderlyingNetworks(null); + } + } + @VisibleForTesting NetworkCapabilities networkCapabilitiesRestrictedForCallerPermissions( NetworkCapabilities nc, int callerPid, int callerUid) { @@ -2250,8 +2257,6 @@ public class ConnectivityService extends IConnectivityManager.Stub if (!checkSettingsPermission(callerPid, callerUid)) { newNc.setUids(null); newNc.setSSID(null); - // TODO: Processes holding NETWORK_FACTORY should be able to see the underlying networks - newNc.setUnderlyingNetworks(null); } if (newNc.getNetworkSpecifier() != null) { newNc.setNetworkSpecifier(newNc.getNetworkSpecifier().redact()); @@ -2265,6 +2270,7 @@ public class ConnectivityService extends IConnectivityManager.Stub newNc.setAllowedUids(new ArraySet<>()); newNc.setSubscriptionIds(Collections.emptySet()); } + redactUnderlyingNetworksForCapabilities(newNc, callerPid, callerUid); return newNc; } @@ -2858,12 +2864,16 @@ public class ConnectivityService extends IConnectivityManager.Stub } private void enforceNetworkFactoryPermission() { + // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface. + if (getCallingUid() == Process.BLUETOOTH_UID) return; enforceAnyPermissionOf( android.Manifest.permission.NETWORK_FACTORY, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); } private void enforceNetworkFactoryOrSettingsPermission() { + // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface. + if (getCallingUid() == Process.BLUETOOTH_UID) return; enforceAnyPermissionOf( android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_FACTORY, @@ -2871,12 +2881,24 @@ public class ConnectivityService extends IConnectivityManager.Stub } private void enforceNetworkFactoryOrTestNetworksPermission() { + // TODO: Check for the BLUETOOTH_STACK permission once that is in the API surface. + if (getCallingUid() == Process.BLUETOOTH_UID) return; enforceAnyPermissionOf( android.Manifest.permission.MANAGE_TEST_NETWORKS, android.Manifest.permission.NETWORK_FACTORY, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); } + private boolean checkNetworkFactoryOrSettingsPermission(int pid, int uid) { + return PERMISSION_GRANTED == mContext.checkPermission( + android.Manifest.permission.NETWORK_FACTORY, pid, uid) + || PERMISSION_GRANTED == mContext.checkPermission( + android.Manifest.permission.NETWORK_SETTINGS, pid, uid) + || PERMISSION_GRANTED == mContext.checkPermission( + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, pid, uid) + || uid == Process.BLUETOOTH_UID; + } + private boolean checkSettingsPermission() { return checkAnyPermissionOf( android.Manifest.permission.NETWORK_SETTINGS, diff --git a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java index b7617626d3..ce955fd6a4 100644 --- a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java +++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java @@ -19,8 +19,6 @@ package com.android.server.connectivity; import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; -import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_NOT_EXPORTED; - import android.annotation.NonNull; import android.content.BroadcastReceiver; import android.content.Context; @@ -160,7 +158,7 @@ public class CarrierPrivilegeAuthenticator extends BroadcastReceiver { private void registerForCarrierChanges() { final IntentFilter filter = new IntentFilter(); filter.addAction(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED); - mContext.registerReceiver(this, filter, null, mHandler, RECEIVER_NOT_EXPORTED /* flags */); + mContext.registerReceiver(this, filter, null, mHandler); registerCarrierPrivilegesListeners(); } diff --git a/service/src/com/android/server/connectivity/ConnectivityNativeService.java b/service/src/com/android/server/connectivity/ConnectivityNativeService.java new file mode 100644 index 0000000000..cde6ea75e2 --- /dev/null +++ b/service/src/com/android/server/connectivity/ConnectivityNativeService.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET4_BIND; +import static com.android.net.module.util.BpfUtils.BPF_CGROUP_INET6_BIND; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.net.connectivity.aidl.ConnectivityNative; +import android.os.Binder; +import android.os.Process; +import android.os.ServiceSpecificException; +import android.system.ErrnoException; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.BpfBitmap; +import com.android.net.module.util.BpfUtils; +import com.android.net.module.util.CollectionUtils; +import com.android.net.module.util.PermissionUtils; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * @hide + */ +public class ConnectivityNativeService extends ConnectivityNative.Stub { + public static final String SERVICE_NAME = "connectivity_native"; + + private static final String TAG = ConnectivityNativeService.class.getSimpleName(); + private static final String CGROUP_PATH = "/sys/fs/cgroup"; + private static final String V4_PROG_PATH = + "/sys/fs/bpf/prog_block_bind4_block_port"; + private static final String V6_PROG_PATH = + "/sys/fs/bpf/prog_block_bind6_block_port"; + private static final String BLOCKED_PORTS_MAP_PATH = "/sys/fs/bpf/map_block_blocked_ports_map"; + + private final Context mContext; + + // BPF map for port blocking. Exactly 65536 entries long, with one entry per port number + @Nullable + private final BpfBitmap mBpfBlockedPortsMap; + + /** + * Dependencies of ConnectivityNativeService, for injection in tests. + */ + @VisibleForTesting + public static class Dependencies { + /** Get BPF maps. */ + @Nullable public BpfBitmap getBlockPortsMap() { + try { + return new BpfBitmap(BLOCKED_PORTS_MAP_PATH); + } catch (ErrnoException e) { + throw new UnsupportedOperationException("Failed to create blocked ports map: " + + e); + } + } + } + + private void enforceBlockPortPermission() { + final int uid = Binder.getCallingUid(); + if (uid == Process.ROOT_UID || uid == Process.PHONE_UID) return; + PermissionUtils.enforceNetworkStackPermission(mContext); + } + + private void ensureValidPortNumber(int port) { + if (port < 0 || port > 65535) { + throw new IllegalArgumentException("Invalid port number " + port); + } + } + + public ConnectivityNativeService(final Context context) { + this(context, new Dependencies()); + } + + @VisibleForTesting + protected ConnectivityNativeService(final Context context, @NonNull Dependencies deps) { + mContext = context; + mBpfBlockedPortsMap = deps.getBlockPortsMap(); + attachProgram(); + } + + @Override + public void blockPortForBind(int port) { + enforceBlockPortPermission(); + ensureValidPortNumber(port); + try { + mBpfBlockedPortsMap.set(port); + } catch (ErrnoException e) { + throw new ServiceSpecificException(e.errno, e.getMessage()); + } + } + + @Override + public void unblockPortForBind(int port) { + enforceBlockPortPermission(); + ensureValidPortNumber(port); + try { + mBpfBlockedPortsMap.unset(port); + } catch (ErrnoException e) { + throw new ServiceSpecificException(e.errno, + "Could not unset bitmap value for (port: " + port + "): " + e); + } + } + + @Override + public void unblockAllPortsForBind() { + enforceBlockPortPermission(); + try { + mBpfBlockedPortsMap.clear(); + } catch (ErrnoException e) { + throw new ServiceSpecificException(e.errno, "Could not clear map: " + e); + } + } + + @Override + public int[] getPortsBlockedForBind() { + enforceBlockPortPermission(); + + ArrayList<Integer> portMap = new ArrayList<Integer>(); + for (int i = 0; i <= 65535; i++) { + try { + if (mBpfBlockedPortsMap.get(i)) portMap.add(i); + } catch (ErrnoException e) { + Log.e(TAG, "Failed to get index " + i, e); + } + } + return CollectionUtils.toIntArray(portMap); + } + + @Override + public int getInterfaceVersion() { + return this.VERSION; + } + + @Override + public String getInterfaceHash() { + return this.HASH; + } + + /** + * Attach BPF program + */ + private void attachProgram() { + try { + BpfUtils.attachProgram(BPF_CGROUP_INET4_BIND, V4_PROG_PATH, CGROUP_PATH, 0); + } catch (IOException e) { + throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET4_BIND: " + + e); + } + try { + BpfUtils.attachProgram(BPF_CGROUP_INET6_BIND, V6_PROG_PATH, CGROUP_PATH, 0); + } catch (IOException e) { + throw new UnsupportedOperationException("Unable to attach to BPF_CGROUP_INET6_BIND: " + + e); + } + Log.d(TAG, "Attached BPF_CGROUP_INET4_BIND and BPF_CGROUP_INET6_BIND programs"); + } +} diff --git a/service/src/com/android/server/connectivity/FullScore.java b/service/src/com/android/server/connectivity/FullScore.java index 799f46b524..b13ba9328a 100644 --- a/service/src/com/android/server/connectivity/FullScore.java +++ b/service/src/com/android/server/connectivity/FullScore.java @@ -29,6 +29,7 @@ import android.net.NetworkAgentConfig; import android.net.NetworkCapabilities; import android.net.NetworkScore; import android.net.NetworkScore.KeepConnectedReason; +import android.util.Log; import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; @@ -46,6 +47,8 @@ import java.util.StringJoiner; * they are handling a score that had the CS-managed bits set. */ public class FullScore { + private static final String TAG = FullScore.class.getSimpleName(); + // This will be removed soon. Do *NOT* depend on it for any new code that is not part of // a migration. private final int mLegacyInt; @@ -126,7 +129,15 @@ public class FullScore { @VisibleForTesting static @NonNull String policyNameOf(final int policy) { final String name = sMessageNames.get(policy); - if (name == null) throw new IllegalArgumentException("Unknown policy: " + policy); + if (name == null) { + // Don't throw here because name might be null due to proguard stripping out the + // POLICY_* constants, potentially causing a crash only on user builds because proguard + // does not run on userdebug builds. + // TODO: make MessageUtils safer by not returning the array and instead storing it + // internally and providing a getter (that does not throw) for individual values. + Log.wtf(TAG, "Unknown policy: " + policy); + return Integer.toString(policy); + } return name.substring("POLICY_".length()); } diff --git a/service-t/src/com/android/server/net/DelayedDiskWrite.java b/service/src/com/android/server/net/DelayedDiskWrite.java index 35dc455725..35dc455725 100644 --- a/service-t/src/com/android/server/net/DelayedDiskWrite.java +++ b/service/src/com/android/server/net/DelayedDiskWrite.java 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 index a8402421ee..f4601808b0 100644 --- 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 @@ -149,7 +149,7 @@ public abstract class AbstractRestrictBackgroundNetworkTestCase { 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 static final long TEMP_POWERSAVE_WHITELIST_DURATION_MS = 20_000; // 20 sec private static final long BROADCAST_TIMEOUT_MS = 15_000; 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 index 89a9bd6664..b6218d2acc 100644 --- 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 @@ -38,6 +38,7 @@ import android.app.ActivityManager; import android.app.Instrumentation; import android.app.UiAutomation; import android.content.Context; +import android.content.pm.PackageManager; import android.location.LocationManager; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; @@ -99,6 +100,10 @@ public class NetworkPolicyTestUtils { return mBatterySaverSupported; } + private static boolean isWear() { + return getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH); + } + /** * As per CDD requirements, if the device doesn't support data saver mode then * ConnectivityManager.getRestrictBackgroundStatus() will always return @@ -107,6 +112,9 @@ public class NetworkPolicyTestUtils { * RESTRICT_BACKGROUND_STATUS_DISABLED or not. */ public static boolean isDataSaverSupported() { + if (isWear()) { + return false; + } if (mDataSaverSupported == null) { assertMyRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED); try { diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java index 9fa146ffca..0c4c370bbb 100644 --- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java +++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java @@ -20,14 +20,18 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; import static android.net.NetworkCapabilities.TRANSPORT_VPN; import static android.net.cts.util.CtsNetUtils.TestNetworkCallback; +import static android.net.cts.util.IkeSessionTestUtils.CHILD_PARAMS; +import static android.net.cts.util.IkeSessionTestUtils.IKE_PARAMS; import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; +import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2; import static com.android.testutils.TestableNetworkCallbackKt.anyNetwork; 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.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; @@ -46,6 +50,7 @@ import android.net.ProxyInfo; import android.net.TestNetworkInterface; import android.net.VpnManager; import android.net.cts.util.CtsNetUtils; +import android.net.ipsec.ike.IkeTunnelConnectionParams; import android.os.Build; import android.os.Process; import android.platform.test.annotations.AppModeFull; @@ -55,8 +60,10 @@ import androidx.test.InstrumentationRegistry; import com.android.internal.util.HexDump; import com.android.networkstack.apishim.Ikev2VpnProfileBuilderShimImpl; import com.android.networkstack.apishim.Ikev2VpnProfileShimImpl; +import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim; import com.android.networkstack.apishim.common.Ikev2VpnProfileShim; import com.android.networkstack.apishim.common.UnsupportedApiLevelException; +import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import com.android.testutils.DevSdkIgnoreRunner; import com.android.testutils.RecorderCallback.CallbackEntry; @@ -64,6 +71,7 @@ import com.android.testutils.TestableNetworkCallback; import org.bouncycastle.x509.X509V1CertificateGenerator; import org.junit.After; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -85,7 +93,8 @@ import javax.security.auth.x500.X500Principal; @AppModeFull(reason = "Appops state changes disallowed for instant apps (OP_ACTIVATE_PLATFORM_VPN)") public class Ikev2VpnTest { private static final String TAG = Ikev2VpnTest.class.getSimpleName(); - + @Rule + public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule(); // Test vectors for IKE negotiation in test mode. private static final String SUCCESSFUL_IKE_INIT_RESP_V4 = "46b8eca1e0d72a18b2b5d9006d47a0022120222000000000000002d0220000300000002c01010004030000" @@ -204,51 +213,55 @@ public class Ikev2VpnTest { }, Manifest.permission.MANAGE_TEST_NETWORKS); } - private Ikev2VpnProfile buildIkev2VpnProfileCommon(@NonNull Ikev2VpnProfile.Builder builder, - boolean isRestrictedToTestNetworks, + private Ikev2VpnProfile buildIkev2VpnProfileCommon( + @NonNull Ikev2VpnProfileBuilderShim builderShim, boolean isRestrictedToTestNetworks, boolean requiresValidation) throws Exception { - if (isRestrictedToTestNetworks) { - builder.restrictToTestNetworks(); - } - builder.setBypassable(true) + builderShim.setBypassable(true) .setAllowedAlgorithms(TEST_ALLOWED_ALGORITHMS) .setProxy(TEST_PROXY_INFO) .setMaxMtu(TEST_MTU) .setMetered(false); if (TestUtils.shouldTestTApis()) { - Ikev2VpnProfileBuilderShimImpl.newInstance().setRequiresInternetValidation( - builder, requiresValidation); + builderShim.setRequiresInternetValidation(requiresValidation); } + + // Convert shim back to Ikev2VpnProfile.Builder since restrictToTestNetworks is a hidden + // method and does not defined in shims. + // TODO: replace it in alternative way to remove the hidden method usage + final Ikev2VpnProfile.Builder builder = (Ikev2VpnProfile.Builder) builderShim.getBuilder(); + if (isRestrictedToTestNetworks) { + builder.restrictToTestNetworks(); + } + return builder.build(); } private Ikev2VpnProfile buildIkev2VpnProfilePsk(@NonNull String remote, boolean isRestrictedToTestNetworks, boolean requiresValidation) throws Exception { - final Ikev2VpnProfile.Builder builder = - new Ikev2VpnProfile.Builder(remote, TEST_IDENTITY).setAuthPsk(TEST_PSK); - + final Ikev2VpnProfileBuilderShim builder = + Ikev2VpnProfileBuilderShimImpl.newInstance(remote, TEST_IDENTITY, null) + .setAuthPsk(TEST_PSK); return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks, requiresValidation); } 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); + final Ikev2VpnProfileBuilderShim builder = + Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY, null) + .setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa); return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks, false /* requiresValidation */); } private Ikev2VpnProfile buildIkev2VpnProfileDigitalSignature(boolean isRestrictedToTestNetworks) throws Exception { - final Ikev2VpnProfile.Builder builder = - new Ikev2VpnProfile.Builder(TEST_SERVER_ADDR_V6, TEST_IDENTITY) + final Ikev2VpnProfileBuilderShim builder = + Ikev2VpnProfileBuilderShimImpl.newInstance(TEST_SERVER_ADDR_V6, TEST_IDENTITY, null) .setAuthDigitalSignature( mUserCertKey.cert, mUserCertKey.key, mServerRootCa); - return buildIkev2VpnProfileCommon(builder, isRestrictedToTestNetworks, false /* requiresValidation */); } @@ -279,18 +292,42 @@ public class Ikev2VpnTest { assertNull(profile.getServerRootCaCert()); assertNull(profile.getRsaPrivateKey()); assertNull(profile.getUserCert()); - final Ikev2VpnProfileShim<Ikev2VpnProfile> shim = Ikev2VpnProfileShimImpl.newInstance(); + final Ikev2VpnProfileShim<Ikev2VpnProfile> shim = new Ikev2VpnProfileShimImpl(profile); if (TestUtils.shouldTestTApis()) { - assertEquals(requiresValidation, shim.isInternetValidationRequired(profile)); + assertEquals(requiresValidation, shim.isInternetValidationRequired()); } else { try { - shim.isInternetValidationRequired(profile); + shim.isInternetValidationRequired(); fail("Only supported from API level 33"); } catch (UnsupportedApiLevelException expected) { } } } + @IgnoreUpTo(SC_V2) + @Test + public void testBuildIkev2VpnProfileWithIkeTunnelConnectionParams() throws Exception { + assumeTrue(mCtsNetUtils.hasIpsecTunnelsFeature()); + assumeTrue(TestUtils.shouldTestTApis()); + + final IkeTunnelConnectionParams expectedParams = + new IkeTunnelConnectionParams(IKE_PARAMS, CHILD_PARAMS); + final Ikev2VpnProfileBuilderShim ikeProfileBuilder = + Ikev2VpnProfileBuilderShimImpl.newInstance(null, null, expectedParams); + // Verify the other Ike options could not be set with IkeTunnelConnectionParams. + final Class<IllegalArgumentException> expected = IllegalArgumentException.class; + assertThrows(expected, () -> ikeProfileBuilder.setAuthPsk(TEST_PSK)); + assertThrows(expected, () -> + ikeProfileBuilder.setAuthUsernamePassword(TEST_USER, TEST_PASSWORD, mServerRootCa)); + assertThrows(expected, () -> ikeProfileBuilder.setAuthDigitalSignature( + mUserCertKey.cert, mUserCertKey.key, mServerRootCa)); + + final Ikev2VpnProfile profile = (Ikev2VpnProfile) ikeProfileBuilder.build().getProfile(); + + assertEquals(expectedParams, + new Ikev2VpnProfileShimImpl(profile).getIkeTunnelConnectionParams()); + } + @Test public void testBuildIkev2VpnProfilePsk() throws Exception { doTestBuildIkev2VpnProfilePsk(true /* requiresValidation */); diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt index f007b83b9e..050497392e 100644 --- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt +++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt @@ -72,6 +72,7 @@ import android.os.Handler import android.os.HandlerThread import android.os.Message import android.os.SystemClock +import android.platform.test.annotations.AppModeFull import android.telephony.TelephonyManager import android.telephony.data.EpsBearerQosSessionAttributes import android.util.DebugUtils.valueToString @@ -106,7 +107,6 @@ import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnValidationStat import com.android.testutils.TestableNetworkCallback import com.android.testutils.assertThrows import org.junit.After -import org.junit.Assume.assumeFalse import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -946,11 +946,9 @@ class NetworkAgentTest { return Pair(agent, qosTestSocket!!) } + @AppModeFull(reason = "Instant apps don't have permission to bind sockets.") @Test fun testQosCallbackRegisterWithUnregister() { - // Instant apps can't bind sockets to localhost - // TODO: use @AppModeFull when supported by DevSdkIgnoreRunner - assumeFalse(realContext.packageManager.isInstantApp()) val (agent, socket) = setupForQosCallbackTesting() val qosCallback = TestableQosCallback() @@ -975,11 +973,9 @@ class NetworkAgentTest { } } + @AppModeFull(reason = "Instant apps don't have permission to bind sockets.") @Test fun testQosCallbackOnQosSession() { - // Instant apps can't bind sockets to localhost - // TODO: use @AppModeFull when supported by DevSdkIgnoreRunner - assumeFalse(realContext.packageManager.isInstantApp()) val (agent, socket) = setupForQosCallbackTesting() val qosCallback = TestableQosCallback() Executors.newSingleThreadExecutor().let { executor -> @@ -1023,11 +1019,9 @@ class NetworkAgentTest { } } + @AppModeFull(reason = "Instant apps don't have permission to bind sockets.") @Test fun testQosCallbackOnError() { - // Instant apps can't bind sockets to localhost - // TODO: use @AppModeFull when supported by DevSdkIgnoreRunner - assumeFalse(realContext.packageManager.isInstantApp()) val (agent, socket) = setupForQosCallbackTesting() val qosCallback = TestableQosCallback() Executors.newSingleThreadExecutor().let { executor -> @@ -1064,11 +1058,9 @@ class NetworkAgentTest { } } + @AppModeFull(reason = "Instant apps don't have permission to bind sockets.") @Test fun testQosCallbackIdsAreMappedCorrectly() { - // Instant apps can't bind sockets to localhost - // TODO: use @AppModeFull when supported by DevSdkIgnoreRunner - assumeFalse(realContext.packageManager.isInstantApp()) val (agent, socket) = setupForQosCallbackTesting() val qosCallback1 = TestableQosCallback() val qosCallback2 = TestableQosCallback() @@ -1107,11 +1099,9 @@ class NetworkAgentTest { } } + @AppModeFull(reason = "Instant apps don't have permission to bind sockets.") @Test fun testQosCallbackWhenNetworkReleased() { - // Instant apps can't bind sockets to localhost - // TODO: use @AppModeFull when supported by DevSdkIgnoreRunner - assumeFalse(realContext.packageManager.isInstantApp()) val (agent, socket) = setupForQosCallbackTesting() Executors.newSingleThreadExecutor().let { executor -> try { @@ -1151,6 +1141,7 @@ class NetworkAgentTest { ) } + @AppModeFull(reason = "Instant apps don't have permission to bind sockets.") @Test fun testUnregisterAfterReplacement() { // Keeps an eye on all test networks. diff --git a/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java b/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java new file mode 100644 index 0000000000..b4ebcdb611 --- /dev/null +++ b/tests/cts/net/util/java/android/net/cts/util/IkeSessionTestUtils.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.ipsec.ike.SaProposal.KEY_LEN_AES_128; +import static android.net.ipsec.ike.SaProposal.KEY_LEN_UNUSED; + +import android.net.ipsec.ike.ChildSaProposal; +import android.net.ipsec.ike.IkeFqdnIdentification; +import android.net.ipsec.ike.IkeSaProposal; +import android.net.ipsec.ike.IkeSessionParams; +import android.net.ipsec.ike.SaProposal; +import android.net.ipsec.ike.TunnelModeChildSessionParams; + +/** Shared testing parameters and util methods for testing IKE */ +public class IkeSessionTestUtils { + private static final String TEST_CLIENT_ADDR = "test.client.com"; + private static final String TEST_SERVER_ADDR = "test.server.com"; + private static final String TEST_SERVER = "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; + + public static final IkeSaProposal SA_PROPOSAL = new IkeSaProposal.Builder() + .addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES, KEY_LEN_UNUSED) + .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96) + .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC) + .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP) + .build(); + public static final ChildSaProposal CHILD_PROPOSAL = new ChildSaProposal.Builder() + .addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_128) + .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_NONE) + .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP) + .build(); + + public static final IkeSessionParams IKE_PARAMS = + new IkeSessionParams.Builder() + .setServerHostname(TEST_SERVER) + .addSaProposal(SA_PROPOSAL) + .setLocalIdentification(new IkeFqdnIdentification(TEST_CLIENT_ADDR)) + .setRemoteIdentification(new IkeFqdnIdentification(TEST_SERVER_ADDR)) + .setAuthPsk("psk".getBytes()) + .build(); + public static final TunnelModeChildSessionParams CHILD_PARAMS = + new TunnelModeChildSessionParams.Builder() + .addSaProposal(CHILD_PROPOSAL) + .build(); +} diff --git a/tests/native/Android.bp b/tests/native/Android.bp new file mode 100644 index 0000000000..cd438f66c2 --- /dev/null +++ b/tests/native/Android.bp @@ -0,0 +1,30 @@ +cc_test { + name: "connectivity_native_test", + test_suites: [ + "general-tests", + "mts-tethering", + "vts", + ], + min_sdk_version: "31", + require_root: true, + tidy: false, + srcs: [ + "connectivity_native_test.cpp", + ], + header_libs: ["bpf_connectivity_headers"], + shared_libs: [ + "libbase", + "libbinder_ndk", + "liblog", + "libnetutils", + "libprocessgroup", + ], + static_libs: [ + "connectivity_native_aidl_interface-lateststable-ndk", + "libcutils", + "libmodules-utils-build", + "libutils", + ], + compile_multilib: "first", + defaults: ["connectivity-mainline-presubmit-cc-defaults"], +} diff --git a/tests/native/connectivity_native_test.cpp b/tests/native/connectivity_native_test.cpp new file mode 100644 index 0000000000..8b089ab581 --- /dev/null +++ b/tests/native/connectivity_native_test.cpp @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 <android-modules-utils/sdk_level.h> +#include <cutils/misc.h> // FIRST_APPLICATION_UID +#include <gtest/gtest.h> +#include <netinet/in.h> +#include <android/binder_manager.h> +#include <android/binder_process.h> + +#include <aidl/android/net/connectivity/aidl/ConnectivityNative.h> + +using aidl::android::net::connectivity::aidl::IConnectivityNative; + +class ConnectivityNativeBinderTest : public ::testing::Test { + public: + std::vector<int32_t> mActualBlockedPorts; + + ConnectivityNativeBinderTest() { + AIBinder* binder = AServiceManager_getService("connectivity_native"); + ndk::SpAIBinder sBinder = ndk::SpAIBinder(binder); + mService = aidl::android::net::connectivity::aidl::IConnectivityNative::fromBinder(sBinder); + } + + void SetUp() override { + // Skip test case if not on T. + if (!android::modules::sdklevel::IsAtLeastT()) GTEST_SKIP() << + "Should be at least T device."; + + ASSERT_NE(nullptr, mService.get()); + + // If there are already ports being blocked on device unblockAllPortsForBind() store + // the currently blocked ports and add them back at the end of the test. Do this for + // every test case so additional test cases do not forget to add ports back. + ndk::ScopedAStatus status = mService->getPortsBlockedForBind(&mActualBlockedPorts); + EXPECT_TRUE(status.isOk()) << status.getDescription (); + + } + + void TearDown() override { + ndk::ScopedAStatus status; + if (mActualBlockedPorts.size() > 0) { + for (int i : mActualBlockedPorts) { + mService->blockPortForBind(i); + EXPECT_TRUE(status.isOk()) << status.getDescription (); + } + } + } + + protected: + std::shared_ptr<IConnectivityNative> mService; + + void runSocketTest (sa_family_t family, const int type, bool blockPort) { + ndk::ScopedAStatus status; + in_port_t port = 0; + int sock, sock2; + // Open two sockets with SO_REUSEADDR and expect they can both bind to port. + sock = openSocket(&port, family, type, false /* expectBindFail */); + sock2 = openSocket(&port, family, type, false /* expectBindFail */); + + int blockedPort = 0; + if (blockPort) { + blockedPort = ntohs(port); + status = mService->blockPortForBind(blockedPort); + EXPECT_TRUE(status.isOk()) << status.getDescription (); + } + + int sock3 = openSocket(&port, family, type, blockPort /* expectBindFail */); + + if (blockPort) { + EXPECT_EQ(-1, sock3); + status = mService->unblockPortForBind(blockedPort); + EXPECT_TRUE(status.isOk()) << status.getDescription (); + } else { + EXPECT_NE(-1, sock3); + } + + close(sock); + close(sock2); + close(sock3); + } + + /* + * Open the socket and update the port. + */ + int openSocket(in_port_t* port, sa_family_t family, const int type, bool expectBindFail) { + int ret = 0; + int enable = 1; + const int sock = socket(family, type, 0); + ret = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)); + EXPECT_EQ(0, ret); + + if (family == AF_INET) { + struct sockaddr_in addr4 = { .sin_family = family, .sin_port = htons(*port) }; + ret = bind(sock, (struct sockaddr*) &addr4, sizeof(addr4)); + } else { + struct sockaddr_in6 addr6 = { .sin6_family = family, .sin6_port = htons(*port) }; + ret = bind(sock, (struct sockaddr*) &addr6, sizeof(addr6)); + } + + if (expectBindFail) { + EXPECT_NE(0, ret); + // If port is blocked, return here since the port is not needed + // for subsequent sockets. + close(sock); + return -1; + } + EXPECT_EQ(0, ret) << "bind unexpectedly failed, errno: " << errno; + + if (family == AF_INET) { + struct sockaddr_in sin; + socklen_t len = sizeof(sin); + EXPECT_NE(-1, getsockname(sock, (struct sockaddr *)&sin, &len)); + EXPECT_NE(0, ntohs(sin.sin_port)); + if (*port != 0) EXPECT_EQ(*port, ntohs(sin.sin_port)); + *port = ntohs(sin.sin_port); + } else { + struct sockaddr_in6 sin; + socklen_t len = sizeof(sin); + EXPECT_NE(-1, getsockname(sock, (struct sockaddr *)&sin, &len)); + EXPECT_NE(0, ntohs(sin.sin6_port)); + if (*port != 0) EXPECT_EQ(*port, ntohs(sin.sin6_port)); + *port = ntohs(sin.sin6_port); + } + return sock; + } +}; + +TEST_F(ConnectivityNativeBinderTest, PortUnblockedV4Udp) { + runSocketTest(AF_INET, SOCK_DGRAM, false); +} + +TEST_F(ConnectivityNativeBinderTest, PortUnblockedV4Tcp) { + runSocketTest(AF_INET, SOCK_STREAM, false); +} + +TEST_F(ConnectivityNativeBinderTest, PortUnblockedV6Udp) { + runSocketTest(AF_INET6, SOCK_DGRAM, false); +} + +TEST_F(ConnectivityNativeBinderTest, PortUnblockedV6Tcp) { + runSocketTest(AF_INET6, SOCK_STREAM, false); +} + +TEST_F(ConnectivityNativeBinderTest, BlockPort4Udp) { + runSocketTest(AF_INET, SOCK_DGRAM, true); +} + +TEST_F(ConnectivityNativeBinderTest, BlockPort4Tcp) { + runSocketTest(AF_INET, SOCK_STREAM, true); +} + +TEST_F(ConnectivityNativeBinderTest, BlockPort6Udp) { + runSocketTest(AF_INET6, SOCK_DGRAM, true); +} + +TEST_F(ConnectivityNativeBinderTest, BlockPort6Tcp) { + runSocketTest(AF_INET6, SOCK_STREAM, true); +} + +TEST_F(ConnectivityNativeBinderTest, BlockPortTwice) { + ndk::ScopedAStatus status = mService->blockPortForBind(5555); + EXPECT_TRUE(status.isOk()) << status.getDescription (); + status = mService->blockPortForBind(5555); + EXPECT_TRUE(status.isOk()) << status.getDescription (); + status = mService->unblockPortForBind(5555); + EXPECT_TRUE(status.isOk()) << status.getDescription (); +} + +TEST_F(ConnectivityNativeBinderTest, GetBlockedPorts) { + ndk::ScopedAStatus status; + std::vector<int> blockedPorts{1, 100, 1220, 1333, 2700, 5555, 5600, 65000}; + for (int i : blockedPorts) { + status = mService->blockPortForBind(i); + EXPECT_TRUE(status.isOk()) << status.getDescription (); + } + std::vector<int32_t> actualBlockedPorts; + status = mService->getPortsBlockedForBind(&actualBlockedPorts); + EXPECT_TRUE(status.isOk()) << status.getDescription (); + EXPECT_FALSE(actualBlockedPorts.empty()); + EXPECT_EQ(blockedPorts, actualBlockedPorts); + + // Remove the ports we added. + status = mService->unblockAllPortsForBind(); + EXPECT_TRUE(status.isOk()) << status.getDescription (); + status = mService->getPortsBlockedForBind(&actualBlockedPorts); + EXPECT_TRUE(status.isOk()) << status.getDescription (); + EXPECT_TRUE(actualBlockedPorts.empty()); +} + +TEST_F(ConnectivityNativeBinderTest, UnblockAllPorts) { + ndk::ScopedAStatus status; + std::vector<int> blockedPorts{1, 100, 1220, 1333, 2700, 5555, 5600, 65000}; + + if (mActualBlockedPorts.size() > 0) { + status = mService->unblockAllPortsForBind(); + } + + for (int i : blockedPorts) { + status = mService->blockPortForBind(i); + EXPECT_TRUE(status.isOk()) << status.getDescription (); + } + + std::vector<int32_t> actualBlockedPorts; + status = mService->getPortsBlockedForBind(&actualBlockedPorts); + EXPECT_TRUE(status.isOk()) << status.getDescription (); + EXPECT_FALSE(actualBlockedPorts.empty()); + + status = mService->unblockAllPortsForBind(); + EXPECT_TRUE(status.isOk()) << status.getDescription (); + status = mService->getPortsBlockedForBind(&actualBlockedPorts); + EXPECT_TRUE(status.isOk()) << status.getDescription (); + EXPECT_TRUE(actualBlockedPorts.empty()); + // If mActualBlockedPorts is not empty, ports will be added back in teardown. +} + +TEST_F(ConnectivityNativeBinderTest, BlockNegativePort) { + int retry = 0; + ndk::ScopedAStatus status; + do { + status = mService->blockPortForBind(-1); + // TODO: find out why transaction failed is being thrown on the first attempt. + } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5); + EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()); +} + +TEST_F(ConnectivityNativeBinderTest, UnblockNegativePort) { + int retry = 0; + ndk::ScopedAStatus status; + do { + status = mService->unblockPortForBind(-1); + // TODO: find out why transaction failed is being thrown on the first attempt. + } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5); + EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()); +} + +TEST_F(ConnectivityNativeBinderTest, BlockMaxPort) { + int retry = 0; + ndk::ScopedAStatus status; + do { + status = mService->blockPortForBind(65536); + // TODO: find out why transaction failed is being thrown on the first attempt. + } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5); + EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()); +} + +TEST_F(ConnectivityNativeBinderTest, UnblockMaxPort) { + int retry = 0; + ndk::ScopedAStatus status; + do { + status = mService->unblockPortForBind(65536); + // TODO: find out why transaction failed is being thrown on the first attempt. + } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5); + EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()); +} + +TEST_F(ConnectivityNativeBinderTest, CheckPermission) { + int retry = 0; + int curUid = getuid(); + EXPECT_EQ(0, seteuid(FIRST_APPLICATION_UID + 2000)) << "seteuid failed: " << strerror(errno); + ndk::ScopedAStatus status; + do { + status = mService->blockPortForBind(5555); + // TODO: find out why transaction failed is being thrown on the first attempt. + } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5); + EXPECT_EQ(EX_SECURITY, status.getExceptionCode()); + EXPECT_EQ(0, seteuid(curUid)) << "seteuid failed: " << strerror(errno); +} diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp index 07dcae3ae7..c27c973009 100644 --- a/tests/unit/Android.bp +++ b/tests/unit/Android.bp @@ -76,6 +76,7 @@ filegroup { "java/com/android/server/IpSecServiceParameterizedTest.java", "java/com/android/server/IpSecServiceRefcountedResourceTest.java", "java/com/android/server/IpSecServiceTest.java", + "java/com/android/server/NativeDaemonConnectorTest.java", "java/com/android/server/NetworkManagementServiceTest.java", "java/com/android/server/NsdServiceTest.java", "java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java", @@ -83,8 +84,10 @@ filegroup { "java/com/android/server/connectivity/MultipathPolicyTrackerTest.java", "java/com/android/server/connectivity/NetdEventListenerServiceTest.java", "java/com/android/server/connectivity/VpnTest.java", + "java/com/android/server/ethernet/*.java", "java/com/android/server/net/ipmemorystore/*.java", "java/com/android/server/net/BpfInterfaceMapUpdaterTest.java", + "java/com/android/server/net/IpConfigStoreTest.java", "java/com/android/server/net/NetworkStats*.java", "java/com/android/server/net/TestableUsageCallback.kt", ] diff --git a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java index 553cb83548..157507bf28 100644 --- a/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java +++ b/tests/unit/java/com/android/server/connectivity/CarrierPrivilegeAuthenticatorTest.java @@ -69,8 +69,6 @@ import java.util.List; @RunWith(DevSdkIgnoreRunner.class) @IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available public class CarrierPrivilegeAuthenticatorTest { - // TODO : use ConstantsShim.RECEIVER_NOT_EXPORTED when it's available in tests. - private static final int RECEIVER_NOT_EXPORTED = 4; private static final int SUBSCRIPTION_COUNT = 2; private static final int TEST_SUBSCRIPTION_ID = 1; @@ -117,7 +115,7 @@ public class CarrierPrivilegeAuthenticatorTest { private IntentFilter getIntentFilter() { final ArgumentCaptor<IntentFilter> captor = ArgumentCaptor.forClass(IntentFilter.class); - verify(mContext).registerReceiver(any(), captor.capture(), any(), any(), anyInt()); + verify(mContext).registerReceiver(any(), captor.capture(), any(), any()); return captor.getValue(); } @@ -140,11 +138,10 @@ public class CarrierPrivilegeAuthenticatorTest { @Test public void testConstructor() throws Exception { verify(mContext).registerReceiver( - eq(mCarrierPrivilegeAuthenticator), - any(IntentFilter.class), - any(), - any(), - eq(RECEIVER_NOT_EXPORTED)); + eq(mCarrierPrivilegeAuthenticator), + any(IntentFilter.class), + any(), + any()); final IntentFilter filter = getIntentFilter(); assertEquals(1, filter.countActions()); assertTrue(filter.hasAction(ACTION_MULTI_SIM_CONFIG_CHANGED)); diff --git a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt index e7f6245547..c03a9cde70 100644 --- a/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt +++ b/tests/unit/java/com/android/server/connectivity/FullScoreTest.kt @@ -22,6 +22,7 @@ import android.net.NetworkScore.KEEP_CONNECTED_NONE import android.os.Build import android.text.TextUtils import android.util.ArraySet +import android.util.Log import androidx.test.filters.SmallTest import com.android.server.connectivity.FullScore.MAX_CS_MANAGED_POLICY import com.android.server.connectivity.FullScore.POLICY_ACCEPT_UNVALIDATED @@ -32,11 +33,12 @@ import com.android.server.connectivity.FullScore.POLICY_IS_VALIDATED import com.android.server.connectivity.FullScore.POLICY_IS_VPN import com.android.testutils.DevSdkIgnoreRule import com.android.testutils.DevSdkIgnoreRunner +import org.junit.After +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import kotlin.reflect.full.staticProperties import kotlin.test.assertEquals -import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -63,6 +65,23 @@ class FullScoreTest { return mixInScore(nc, nac, validated, false /* yieldToBadWifi */, destroyed) } + private val TAG = this::class.simpleName + + private var wtfHandler: Log.TerribleFailureHandler? = null + + @Before + fun setUp() { + // policyNameOf will call Log.wtf if passed an invalid policy. + wtfHandler = Log.setWtfHandler() { tagString, what, system -> + Log.d(TAG, "WTF captured, ignoring: $tagString $what") + } + } + + @After + fun tearDown() { + Log.setWtfHandler(wtfHandler) + } + @Test fun testGetLegacyInt() { val ns = FullScore(50, 0L /* policy */, KEEP_CONNECTED_NONE) @@ -101,10 +120,9 @@ class FullScoreTest { assertFalse(foundNames.contains(name)) foundNames.add(name) } - assertFailsWith<IllegalArgumentException> { - FullScore.policyNameOf(MAX_CS_MANAGED_POLICY + 1) - } assertEquals("IS_UNMETERED", FullScore.policyNameOf(POLICY_IS_UNMETERED)) + val invalidPolicy = MAX_CS_MANAGED_POLICY + 1 + assertEquals(Integer.toString(invalidPolicy), FullScore.policyNameOf(invalidPolicy)) } fun getAllPolicies() = Regex("POLICY_.*").let { nameRegex -> diff --git a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java new file mode 100644 index 0000000000..4d3e4d36d2 --- /dev/null +++ b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java @@ -0,0 +1,783 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.ethernet; + +import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.app.test.MockAnswerUtil.AnswerWithArguments; +import android.content.Context; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.EthernetNetworkSpecifier; +import android.net.EthernetNetworkManagementException; +import android.net.INetworkInterfaceOutcomeReceiver; +import android.net.IpConfiguration; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkProvider; +import android.net.NetworkRequest; +import android.net.StaticIpConfiguration; +import android.net.ip.IpClientCallbacks; +import android.net.ip.IpClientManager; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.test.TestLooper; +import android.util.Pair; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.connectivity.resources.R; +import com.android.net.module.util.InterfaceParams; + +import com.android.testutils.DevSdkIgnoreRule; + +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.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class EthernetNetworkFactoryTest { + private static final int TIMEOUT_MS = 2_000; + private static final String TEST_IFACE = "test123"; + private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null; + private static final String IP_ADDR = "192.0.2.2/25"; + private static final LinkAddress LINK_ADDR = new LinkAddress(IP_ADDR); + private static final String HW_ADDR = "01:02:03:04:05:06"; + private TestLooper mLooper; + private Handler mHandler; + private EthernetNetworkFactory mNetFactory = null; + private IpClientCallbacks mIpClientCallbacks; + @Mock private Context mContext; + @Mock private Resources mResources; + @Mock private EthernetNetworkFactory.Dependencies mDeps; + @Mock private IpClientManager mIpClient; + @Mock private EthernetNetworkAgent mNetworkAgent; + @Mock private InterfaceParams mInterfaceParams; + @Mock private Network mMockNetwork; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + setupNetworkAgentMock(); + setupIpClientMock(); + setupContext(); + } + + //TODO: Move away from usage of TestLooper in order to move this logic back into @Before. + private void initEthernetNetworkFactory() { + mLooper = new TestLooper(); + mHandler = new Handler(mLooper.getLooper()); + mNetFactory = new EthernetNetworkFactory(mHandler, mContext, mDeps); + } + + private void setupNetworkAgentMock() { + when(mDeps.makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any())) + .thenAnswer(new AnswerWithArguments() { + public EthernetNetworkAgent answer( + Context context, + Looper looper, + NetworkCapabilities nc, + LinkProperties lp, + NetworkAgentConfig config, + NetworkProvider provider, + EthernetNetworkAgent.Callbacks cb) { + when(mNetworkAgent.getCallbacks()).thenReturn(cb); + when(mNetworkAgent.getNetwork()) + .thenReturn(mMockNetwork); + return mNetworkAgent; + } + } + ); + } + + private void setupIpClientMock() throws Exception { + doAnswer(inv -> { + // these tests only support one concurrent IpClient, so make sure we do not accidentally + // create a mess. + assertNull("An IpClient has already been created.", mIpClientCallbacks); + + mIpClientCallbacks = inv.getArgument(2); + mIpClientCallbacks.onIpClientCreated(null); + mLooper.dispatchAll(); + return null; + }).when(mDeps).makeIpClient(any(Context.class), anyString(), any()); + + doAnswer(inv -> { + mIpClientCallbacks.onQuit(); + mLooper.dispatchAll(); + mIpClientCallbacks = null; + return null; + }).when(mIpClient).shutdown(); + + when(mDeps.makeIpClientManager(any())).thenReturn(mIpClient); + } + + private void triggerOnProvisioningSuccess() { + mIpClientCallbacks.onProvisioningSuccess(new LinkProperties()); + mLooper.dispatchAll(); + } + + private void triggerOnProvisioningFailure() { + mIpClientCallbacks.onProvisioningFailure(new LinkProperties()); + mLooper.dispatchAll(); + } + + private void triggerOnReachabilityLost() { + mIpClientCallbacks.onReachabilityLost("ReachabilityLost"); + mLooper.dispatchAll(); + } + + private void setupContext() { + when(mDeps.getTcpBufferSizesFromResource(eq(mContext))).thenReturn(""); + } + + @After + public void tearDown() { + // looper is shared with the network agents, so there may still be messages to dispatch on + // tear down. + mLooper.dispatchAll(); + } + + private NetworkCapabilities createDefaultFilterCaps() { + return NetworkCapabilities.Builder.withoutDefaultCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .build(); + } + + private NetworkCapabilities.Builder createInterfaceCapsBuilder(final int transportType) { + return new NetworkCapabilities.Builder() + .addTransportType(transportType) + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED); + } + + private NetworkRequest.Builder createDefaultRequestBuilder() { + return new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + } + + private NetworkRequest createDefaultRequest() { + return createDefaultRequestBuilder().build(); + } + + private IpConfiguration createDefaultIpConfig() { + IpConfiguration ipConfig = new IpConfiguration(); + ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP); + ipConfig.setProxySettings(IpConfiguration.ProxySettings.NONE); + return ipConfig; + } + + /** + * Create an {@link IpConfiguration} with an associated {@link StaticIpConfiguration}. + * + * @return {@link IpConfiguration} with its {@link StaticIpConfiguration} set. + */ + private IpConfiguration createStaticIpConfig() { + final IpConfiguration ipConfig = new IpConfiguration(); + ipConfig.setIpAssignment(IpConfiguration.IpAssignment.STATIC); + ipConfig.setStaticIpConfiguration( + new StaticIpConfiguration.Builder().setIpAddress(LINK_ADDR).build()); + return ipConfig; + } + + // creates an interface with provisioning in progress (since updating the interface link state + // automatically starts the provisioning process) + private void createInterfaceUndergoingProvisioning(String iface) { + // Default to the ethernet transport type. + createInterfaceUndergoingProvisioning(iface, NetworkCapabilities.TRANSPORT_ETHERNET); + } + + private void createInterfaceUndergoingProvisioning( + @NonNull final String iface, final int transportType) { + final IpConfiguration ipConfig = createDefaultIpConfig(); + mNetFactory.addInterface(iface, HW_ADDR, ipConfig, + createInterfaceCapsBuilder(transportType).build()); + assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER)); + verifyStart(ipConfig); + clearInvocations(mDeps); + clearInvocations(mIpClient); + } + + // creates a provisioned interface + private void createAndVerifyProvisionedInterface(String iface) throws Exception { + // Default to the ethernet transport type. + createAndVerifyProvisionedInterface(iface, NetworkCapabilities.TRANSPORT_ETHERNET, + ConnectivityManager.TYPE_ETHERNET); + } + + private void createVerifyAndRemoveProvisionedInterface(final int transportType, + final int expectedLegacyType) throws Exception { + createAndVerifyProvisionedInterface(TEST_IFACE, transportType, + expectedLegacyType); + mNetFactory.removeInterface(TEST_IFACE); + } + + private void createAndVerifyProvisionedInterface( + @NonNull final String iface, final int transportType, final int expectedLegacyType) + throws Exception { + createInterfaceUndergoingProvisioning(iface, transportType); + triggerOnProvisioningSuccess(); + // provisioning succeeded, verify that the network agent is created, registered, marked + // as connected and legacy type are correctly set. + final ArgumentCaptor<NetworkCapabilities> ncCaptor = ArgumentCaptor.forClass( + NetworkCapabilities.class); + verify(mDeps).makeEthernetNetworkAgent(any(), any(), ncCaptor.capture(), any(), + argThat(x -> x.getLegacyType() == expectedLegacyType), any(), any()); + assertEquals( + new EthernetNetworkSpecifier(iface), ncCaptor.getValue().getNetworkSpecifier()); + verifyNetworkAgentRegistersAndConnects(); + clearInvocations(mDeps); + clearInvocations(mNetworkAgent); + } + + // creates an unprovisioned interface + private void createUnprovisionedInterface(String iface) throws Exception { + // To create an unprovisioned interface, provision and then "stop" it, i.e. stop its + // NetworkAgent and IpClient. One way this can be done is by provisioning an interface and + // then calling onNetworkUnwanted. + createAndVerifyProvisionedInterface(iface); + + mNetworkAgent.getCallbacks().onNetworkUnwanted(); + mLooper.dispatchAll(); + verifyStop(); + + clearInvocations(mIpClient); + clearInvocations(mNetworkAgent); + } + + @Test + public void testAcceptRequest() throws Exception { + initEthernetNetworkFactory(); + createInterfaceUndergoingProvisioning(TEST_IFACE); + assertTrue(mNetFactory.acceptRequest(createDefaultRequest())); + + NetworkRequest wifiRequest = createDefaultRequestBuilder() + .removeTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build(); + assertFalse(mNetFactory.acceptRequest(wifiRequest)); + } + + @Test + public void testUpdateInterfaceLinkStateForActiveProvisioningInterface() throws Exception { + initEthernetNetworkFactory(); + createInterfaceUndergoingProvisioning(TEST_IFACE); + final TestNetworkManagementListener listener = new TestNetworkManagementListener(); + + // verify that the IpClient gets shut down when interface state changes to down. + final boolean ret = + mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener); + + assertTrue(ret); + verify(mIpClient).shutdown(); + assertEquals(listener.expectOnResult(), TEST_IFACE); + } + + @Test + public void testUpdateInterfaceLinkStateForProvisionedInterface() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + final TestNetworkManagementListener listener = new TestNetworkManagementListener(); + + final boolean ret = + mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener); + + assertTrue(ret); + verifyStop(); + assertEquals(listener.expectOnResult(), TEST_IFACE); + } + + @Test + public void testUpdateInterfaceLinkStateForUnprovisionedInterface() throws Exception { + initEthernetNetworkFactory(); + createUnprovisionedInterface(TEST_IFACE); + final TestNetworkManagementListener listener = new TestNetworkManagementListener(); + + final boolean ret = + mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener); + + assertTrue(ret); + // There should not be an active IPClient or NetworkAgent. + verify(mDeps, never()).makeIpClient(any(), any(), any()); + verify(mDeps, never()) + .makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any()); + assertEquals(listener.expectOnResult(), TEST_IFACE); + } + + @Test + public void testUpdateInterfaceLinkStateForNonExistingInterface() throws Exception { + initEthernetNetworkFactory(); + final TestNetworkManagementListener listener = new TestNetworkManagementListener(); + + // if interface was never added, link state cannot be updated. + final boolean ret = + mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listener); + + assertFalse(ret); + verifyNoStopOrStart(); + listener.expectOnErrorWithMessage("can't be updated as it is not available"); + } + + @Test + public void testUpdateInterfaceLinkStateWithNoChanges() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + final TestNetworkManagementListener listener = new TestNetworkManagementListener(); + + final boolean ret = + mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listener); + + assertFalse(ret); + verifyNoStopOrStart(); + listener.expectOnErrorWithMessage("No changes"); + } + + @Test + public void testNeedNetworkForOnProvisionedInterface() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + mNetFactory.needNetworkFor(createDefaultRequest()); + verify(mIpClient, never()).startProvisioning(any()); + } + + @Test + public void testNeedNetworkForOnUnprovisionedInterface() throws Exception { + initEthernetNetworkFactory(); + createUnprovisionedInterface(TEST_IFACE); + mNetFactory.needNetworkFor(createDefaultRequest()); + verify(mIpClient).startProvisioning(any()); + + triggerOnProvisioningSuccess(); + verifyNetworkAgentRegistersAndConnects(); + } + + @Test + public void testNeedNetworkForOnInterfaceUndergoingProvisioning() throws Exception { + initEthernetNetworkFactory(); + createInterfaceUndergoingProvisioning(TEST_IFACE); + mNetFactory.needNetworkFor(createDefaultRequest()); + verify(mIpClient, never()).startProvisioning(any()); + + triggerOnProvisioningSuccess(); + verifyNetworkAgentRegistersAndConnects(); + } + + @Test + public void testProvisioningLoss() throws Exception { + initEthernetNetworkFactory(); + when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams); + createAndVerifyProvisionedInterface(TEST_IFACE); + + triggerOnProvisioningFailure(); + verifyStop(); + // provisioning loss should trigger a retry, since the interface is still there + verify(mIpClient).startProvisioning(any()); + } + + @Test + public void testProvisioningLossForDisappearedInterface() throws Exception { + initEthernetNetworkFactory(); + // mocked method returns null by default, but just to be explicit in the test: + when(mDeps.getNetworkInterfaceByName(eq(TEST_IFACE))).thenReturn(null); + + createAndVerifyProvisionedInterface(TEST_IFACE); + triggerOnProvisioningFailure(); + + // the interface disappeared and getNetworkInterfaceByName returns null, we should not retry + verify(mIpClient, never()).startProvisioning(any()); + verifyNoStopOrStart(); + } + + private void verifyNoStopOrStart() { + verify(mNetworkAgent, never()).register(); + verify(mIpClient, never()).shutdown(); + verify(mNetworkAgent, never()).unregister(); + verify(mIpClient, never()).startProvisioning(any()); + } + + @Test + public void testIpClientIsNotStartedWhenLinkIsDown() throws Exception { + initEthernetNetworkFactory(); + createUnprovisionedInterface(TEST_IFACE); + mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER); + + mNetFactory.needNetworkFor(createDefaultRequest()); + + verify(mDeps, never()).makeIpClient(any(), any(), any()); + + // BUG(b/191854824): requesting a network with a specifier (Android Auto use case) should + // not start an IpClient when the link is down, but fixing this may make matters worse by + // tiggering b/197548738. + NetworkRequest specificNetRequest = new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .setNetworkSpecifier(new EthernetNetworkSpecifier(TEST_IFACE)) + .build(); + mNetFactory.needNetworkFor(specificNetRequest); + mNetFactory.releaseNetworkFor(specificNetRequest); + + mNetFactory.updateInterfaceLinkState(TEST_IFACE, true, NULL_LISTENER); + // TODO: change to once when b/191854824 is fixed. + verify(mDeps, times(2)).makeIpClient(any(), eq(TEST_IFACE), any()); + } + + @Test + public void testLinkPropertiesChanged() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + + LinkProperties lp = new LinkProperties(); + mIpClientCallbacks.onLinkPropertiesChange(lp); + mLooper.dispatchAll(); + verify(mNetworkAgent).sendLinkPropertiesImpl(same(lp)); + } + + @Test + public void testNetworkUnwanted() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + + mNetworkAgent.getCallbacks().onNetworkUnwanted(); + mLooper.dispatchAll(); + verifyStop(); + } + + @Test + public void testNetworkUnwantedWithStaleNetworkAgent() throws Exception { + initEthernetNetworkFactory(); + // ensures provisioning is restarted after provisioning loss + when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams); + createAndVerifyProvisionedInterface(TEST_IFACE); + + EthernetNetworkAgent.Callbacks oldCbs = mNetworkAgent.getCallbacks(); + // replace network agent in EthernetNetworkFactory + // Loss of provisioning will restart the ip client and network agent. + triggerOnProvisioningFailure(); + verify(mDeps).makeIpClient(any(), any(), any()); + + triggerOnProvisioningSuccess(); + verify(mDeps).makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any()); + + // verify that unwanted is ignored + clearInvocations(mIpClient); + clearInvocations(mNetworkAgent); + oldCbs.onNetworkUnwanted(); + verify(mIpClient, never()).shutdown(); + verify(mNetworkAgent, never()).unregister(); + } + + @Test + public void testTransportOverrideIsCorrectlySet() throws Exception { + initEthernetNetworkFactory(); + // createProvisionedInterface() has verifications in place for transport override + // functionality which for EthernetNetworkFactory is network score and legacy type mappings. + createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_ETHERNET, + ConnectivityManager.TYPE_ETHERNET); + createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_BLUETOOTH, + ConnectivityManager.TYPE_BLUETOOTH); + createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_WIFI, + ConnectivityManager.TYPE_WIFI); + createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_CELLULAR, + ConnectivityManager.TYPE_MOBILE); + createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_LOWPAN, + ConnectivityManager.TYPE_NONE); + createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_WIFI_AWARE, + ConnectivityManager.TYPE_NONE); + createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_TEST, + ConnectivityManager.TYPE_NONE); + } + + @Test + public void testReachabilityLoss() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + + triggerOnReachabilityLost(); + + // Reachability loss should trigger a stop and start, since the interface is still there + verifyRestart(createDefaultIpConfig()); + } + + private IpClientCallbacks getStaleIpClientCallbacks() throws Exception { + createAndVerifyProvisionedInterface(TEST_IFACE); + final IpClientCallbacks staleIpClientCallbacks = mIpClientCallbacks; + mNetFactory.removeInterface(TEST_IFACE); + verifyStop(); + assertNotSame(mIpClientCallbacks, staleIpClientCallbacks); + return staleIpClientCallbacks; + } + + @Test + public void testIgnoreOnIpLayerStartedCallbackForStaleCallback() throws Exception { + initEthernetNetworkFactory(); + final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks(); + + staleIpClientCallbacks.onProvisioningSuccess(new LinkProperties()); + mLooper.dispatchAll(); + + verify(mIpClient, never()).startProvisioning(any()); + verify(mNetworkAgent, never()).register(); + } + + @Test + public void testIgnoreOnIpLayerStoppedCallbackForStaleCallback() throws Exception { + initEthernetNetworkFactory(); + when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams); + final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks(); + + staleIpClientCallbacks.onProvisioningFailure(new LinkProperties()); + mLooper.dispatchAll(); + + verify(mIpClient, never()).startProvisioning(any()); + } + + @Test + public void testIgnoreLinkPropertiesCallbackForStaleCallback() throws Exception { + initEthernetNetworkFactory(); + final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks(); + final LinkProperties lp = new LinkProperties(); + + staleIpClientCallbacks.onLinkPropertiesChange(lp); + mLooper.dispatchAll(); + + verify(mNetworkAgent, never()).sendLinkPropertiesImpl(eq(lp)); + } + + @Test + public void testIgnoreNeighborLossCallbackForStaleCallback() throws Exception { + initEthernetNetworkFactory(); + final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks(); + + staleIpClientCallbacks.onReachabilityLost("Neighbor Lost"); + mLooper.dispatchAll(); + + verify(mIpClient, never()).startProvisioning(any()); + verify(mNetworkAgent, never()).register(); + } + + private void verifyRestart(@NonNull final IpConfiguration ipConfig) { + verifyStop(); + verifyStart(ipConfig); + } + + private void verifyStart(@NonNull final IpConfiguration ipConfig) { + verify(mDeps).makeIpClient(any(Context.class), anyString(), any()); + verify(mIpClient).startProvisioning( + argThat(x -> Objects.equals(x.mStaticIpConfig, ipConfig.getStaticIpConfiguration())) + ); + } + + private void verifyStop() { + verify(mIpClient).shutdown(); + verify(mNetworkAgent).unregister(); + } + + private void verifyNetworkAgentRegistersAndConnects() { + verify(mNetworkAgent).register(); + verify(mNetworkAgent).markConnected(); + } + + private static final class TestNetworkManagementListener + implements INetworkInterfaceOutcomeReceiver { + private final CompletableFuture<String> mResult = new CompletableFuture<>(); + private final CompletableFuture<EthernetNetworkManagementException> mError = + new CompletableFuture<>(); + + @Override + public void onResult(@NonNull String iface) { + mResult.complete(iface); + } + + @Override + public void onError(@NonNull EthernetNetworkManagementException exception) { + mError.complete(exception); + } + + String expectOnResult() throws Exception { + return mResult.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + + EthernetNetworkManagementException expectOnError() throws Exception { + return mError.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + + void expectOnErrorWithMessage(String msg) throws Exception { + assertTrue(expectOnError().getMessage().contains(msg)); + } + + @Override + public IBinder asBinder() { + return null; + } + } + + @Test + public void testUpdateInterfaceCallsListenerCorrectlyOnSuccess() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + final NetworkCapabilities capabilities = createDefaultFilterCaps(); + final IpConfiguration ipConfiguration = createStaticIpConfig(); + final TestNetworkManagementListener listener = new TestNetworkManagementListener(); + + mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener); + triggerOnProvisioningSuccess(); + + assertEquals(listener.expectOnResult(), TEST_IFACE); + } + + @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available + @Test + public void testUpdateInterfaceAbortsOnConcurrentRemoveInterface() throws Exception { + initEthernetNetworkFactory(); + verifyNetworkManagementCallIsAbortedWhenInterrupted( + TEST_IFACE, + () -> mNetFactory.removeInterface(TEST_IFACE)); + } + + @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available + @Test + public void testUpdateInterfaceAbortsOnConcurrentUpdateInterfaceLinkState() throws Exception { + initEthernetNetworkFactory(); + verifyNetworkManagementCallIsAbortedWhenInterrupted( + TEST_IFACE, + () -> mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER)); + } + + @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available + @Test + public void testUpdateInterfaceCallsListenerCorrectlyOnConcurrentRequests() throws Exception { + initEthernetNetworkFactory(); + final NetworkCapabilities capabilities = createDefaultFilterCaps(); + final IpConfiguration ipConfiguration = createStaticIpConfig(); + final TestNetworkManagementListener successfulListener = + new TestNetworkManagementListener(); + + // If two calls come in before the first one completes, the first listener will be aborted + // and the second one will be successful. + verifyNetworkManagementCallIsAbortedWhenInterrupted( + TEST_IFACE, + () -> { + mNetFactory.updateInterface( + TEST_IFACE, ipConfiguration, capabilities, successfulListener); + triggerOnProvisioningSuccess(); + }); + + assertEquals(successfulListener.expectOnResult(), TEST_IFACE); + } + + private void verifyNetworkManagementCallIsAbortedWhenInterrupted( + @NonNull final String iface, + @NonNull final Runnable interruptingRunnable) throws Exception { + createAndVerifyProvisionedInterface(iface); + final NetworkCapabilities capabilities = createDefaultFilterCaps(); + final IpConfiguration ipConfiguration = createStaticIpConfig(); + final TestNetworkManagementListener failedListener = new TestNetworkManagementListener(); + + // An active update request will be aborted on interrupt prior to provisioning completion. + mNetFactory.updateInterface(iface, ipConfiguration, capabilities, failedListener); + interruptingRunnable.run(); + + failedListener.expectOnErrorWithMessage("aborted"); + } + + @Test + public void testUpdateInterfaceRestartsAgentCorrectly() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + final NetworkCapabilities capabilities = createDefaultFilterCaps(); + final IpConfiguration ipConfiguration = createStaticIpConfig(); + final TestNetworkManagementListener listener = new TestNetworkManagementListener(); + + mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener); + triggerOnProvisioningSuccess(); + + assertEquals(listener.expectOnResult(), TEST_IFACE); + verify(mDeps).makeEthernetNetworkAgent(any(), any(), + eq(capabilities), any(), any(), any(), any()); + verifyRestart(ipConfiguration); + } + + @Test + public void testUpdateInterfaceForNonExistingInterface() throws Exception { + initEthernetNetworkFactory(); + // No interface exists due to not calling createAndVerifyProvisionedInterface(...). + final NetworkCapabilities capabilities = createDefaultFilterCaps(); + final IpConfiguration ipConfiguration = createStaticIpConfig(); + final TestNetworkManagementListener listener = new TestNetworkManagementListener(); + + mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener); + + verifyNoStopOrStart(); + listener.expectOnErrorWithMessage("can't be updated as it is not available"); + } + + @Test + public void testUpdateInterfaceWithNullIpConfiguration() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + + final IpConfiguration initialIpConfig = createStaticIpConfig(); + mNetFactory.updateInterface(TEST_IFACE, initialIpConfig, null /*capabilities*/, + null /*listener*/); + triggerOnProvisioningSuccess(); + verifyRestart(initialIpConfig); + + // TODO: have verifyXyz functions clear invocations. + clearInvocations(mDeps); + clearInvocations(mIpClient); + clearInvocations(mNetworkAgent); + + + // verify that sending a null ipConfig does not update the current ipConfig. + mNetFactory.updateInterface(TEST_IFACE, null /*ipConfig*/, null /*capabilities*/, + null /*listener*/); + triggerOnProvisioningSuccess(); + verifyRestart(initialIpConfig); + } +} diff --git a/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java new file mode 100644 index 0000000000..dd1f1edba7 --- /dev/null +++ b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.ethernet; + +import static android.net.NetworkCapabilities.TRANSPORT_TEST; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.Manifest; +import android.annotation.NonNull; +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.INetworkInterfaceOutcomeReceiver; +import android.net.EthernetNetworkUpdateRequest; +import android.net.IpConfiguration; +import android.net.NetworkCapabilities; +import android.os.Handler; + +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; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class EthernetServiceImplTest { + private static final String TEST_IFACE = "test123"; + private static final EthernetNetworkUpdateRequest UPDATE_REQUEST = + new EthernetNetworkUpdateRequest.Builder() + .setIpConfiguration(new IpConfiguration()) + .setNetworkCapabilities(new NetworkCapabilities.Builder().build()) + .build(); + private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_CAPABILITIES = + new EthernetNetworkUpdateRequest.Builder() + .setIpConfiguration(new IpConfiguration()) + .build(); + private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_IP_CONFIG = + new EthernetNetworkUpdateRequest.Builder() + .setNetworkCapabilities(new NetworkCapabilities.Builder().build()) + .build(); + private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null; + private EthernetServiceImpl mEthernetServiceImpl; + @Mock private Context mContext; + @Mock private Handler mHandler; + @Mock private EthernetTracker mEthernetTracker; + @Mock private PackageManager mPackageManager; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + doReturn(mPackageManager).when(mContext).getPackageManager(); + mEthernetServiceImpl = new EthernetServiceImpl(mContext, mHandler, mEthernetTracker); + mEthernetServiceImpl.mStarted.set(true); + toggleAutomotiveFeature(true); + shouldTrackIface(TEST_IFACE, true); + } + + private void toggleAutomotiveFeature(final boolean isEnabled) { + doReturn(isEnabled) + .when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); + } + + private void shouldTrackIface(@NonNull final String iface, final boolean shouldTrack) { + doReturn(shouldTrack).when(mEthernetTracker).isTrackingInterface(iface); + } + + @Test + public void testSetConfigurationRejectsWhenEthNotStarted() { + mEthernetServiceImpl.mStarted.set(false); + assertThrows(IllegalStateException.class, () -> { + mEthernetServiceImpl.setConfiguration("" /* iface */, new IpConfiguration()); + }); + } + + @Test + public void testUpdateConfigurationRejectsWhenEthNotStarted() { + mEthernetServiceImpl.mStarted.set(false); + assertThrows(IllegalStateException.class, () -> { + mEthernetServiceImpl.updateConfiguration( + "" /* iface */, UPDATE_REQUEST, null /* listener */); + }); + } + + @Test + public void testConnectNetworkRejectsWhenEthNotStarted() { + mEthernetServiceImpl.mStarted.set(false); + assertThrows(IllegalStateException.class, () -> { + mEthernetServiceImpl.connectNetwork("" /* iface */, null /* listener */); + }); + } + + @Test + public void testDisconnectNetworkRejectsWhenEthNotStarted() { + mEthernetServiceImpl.mStarted.set(false); + assertThrows(IllegalStateException.class, () -> { + mEthernetServiceImpl.disconnectNetwork("" /* iface */, null /* listener */); + }); + } + + @Test + public void testUpdateConfigurationRejectsNullIface() { + assertThrows(NullPointerException.class, () -> { + mEthernetServiceImpl.updateConfiguration(null, UPDATE_REQUEST, NULL_LISTENER); + }); + } + + @Test + public void testConnectNetworkRejectsNullIface() { + assertThrows(NullPointerException.class, () -> { + mEthernetServiceImpl.connectNetwork(null /* iface */, NULL_LISTENER); + }); + } + + @Test + public void testDisconnectNetworkRejectsNullIface() { + assertThrows(NullPointerException.class, () -> { + mEthernetServiceImpl.disconnectNetwork(null /* iface */, NULL_LISTENER); + }); + } + + @Test + public void testUpdateConfigurationWithCapabilitiesRejectsWithoutAutomotiveFeature() { + toggleAutomotiveFeature(false); + assertThrows(UnsupportedOperationException.class, () -> { + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER); + }); + } + + @Test + public void testUpdateConfigurationWithCapabilitiesWithAutomotiveFeature() { + toggleAutomotiveFeature(false); + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST_WITHOUT_CAPABILITIES, + NULL_LISTENER); + verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE), + eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getIpConfiguration()), + eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getNetworkCapabilities()), isNull()); + } + + @Test + public void testConnectNetworkRejectsWithoutAutomotiveFeature() { + toggleAutomotiveFeature(false); + assertThrows(UnsupportedOperationException.class, () -> { + mEthernetServiceImpl.connectNetwork("" /* iface */, NULL_LISTENER); + }); + } + + @Test + public void testDisconnectNetworkRejectsWithoutAutomotiveFeature() { + toggleAutomotiveFeature(false); + assertThrows(UnsupportedOperationException.class, () -> { + mEthernetServiceImpl.disconnectNetwork("" /* iface */, NULL_LISTENER); + }); + } + + private void denyManageEthPermission() { + doThrow(new SecurityException("")).when(mContext) + .enforceCallingOrSelfPermission( + eq(Manifest.permission.MANAGE_ETHERNET_NETWORKS), anyString()); + } + + private void denyManageTestNetworksPermission() { + doThrow(new SecurityException("")).when(mContext) + .enforceCallingOrSelfPermission( + eq(Manifest.permission.MANAGE_TEST_NETWORKS), anyString()); + } + + @Test + public void testUpdateConfigurationRejectsWithoutManageEthPermission() { + denyManageEthPermission(); + assertThrows(SecurityException.class, () -> { + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER); + }); + } + + @Test + public void testConnectNetworkRejectsWithoutManageEthPermission() { + denyManageEthPermission(); + assertThrows(SecurityException.class, () -> { + mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER); + }); + } + + @Test + public void testDisconnectNetworkRejectsWithoutManageEthPermission() { + denyManageEthPermission(); + assertThrows(SecurityException.class, () -> { + mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER); + }); + } + + private void enableTestInterface() { + when(mEthernetTracker.isValidTestInterface(eq(TEST_IFACE))).thenReturn(true); + } + + @Test + public void testUpdateConfigurationRejectsTestRequestWithoutTestPermission() { + enableTestInterface(); + denyManageTestNetworksPermission(); + assertThrows(SecurityException.class, () -> { + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER); + }); + } + + @Test + public void testConnectNetworkRejectsTestRequestWithoutTestPermission() { + enableTestInterface(); + denyManageTestNetworksPermission(); + assertThrows(SecurityException.class, () -> { + mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER); + }); + } + + @Test + public void testDisconnectNetworkRejectsTestRequestWithoutTestPermission() { + enableTestInterface(); + denyManageTestNetworksPermission(); + assertThrows(SecurityException.class, () -> { + mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER); + }); + } + + @Test + public void testUpdateConfiguration() { + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER); + verify(mEthernetTracker).updateConfiguration( + eq(TEST_IFACE), + eq(UPDATE_REQUEST.getIpConfiguration()), + eq(UPDATE_REQUEST.getNetworkCapabilities()), eq(NULL_LISTENER)); + } + + @Test + public void testConnectNetwork() { + mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER); + verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER)); + } + + @Test + public void testDisconnectNetwork() { + mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER); + verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER)); + } + + @Test + public void testUpdateConfigurationAcceptsTestRequestWithNullCapabilities() { + enableTestInterface(); + final EthernetNetworkUpdateRequest request = + new EthernetNetworkUpdateRequest + .Builder() + .setIpConfiguration(new IpConfiguration()).build(); + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, request, NULL_LISTENER); + verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE), + eq(request.getIpConfiguration()), + eq(request.getNetworkCapabilities()), isNull()); + } + + @Test + public void testUpdateConfigurationAcceptsRequestWithNullIpConfiguration() { + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST_WITHOUT_IP_CONFIG, + NULL_LISTENER); + verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE), + eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getIpConfiguration()), + eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getNetworkCapabilities()), isNull()); + } + + @Test + public void testUpdateConfigurationRejectsInvalidTestRequest() { + enableTestInterface(); + assertThrows(IllegalArgumentException.class, () -> { + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER); + }); + } + + private EthernetNetworkUpdateRequest createTestNetworkUpdateRequest() { + final NetworkCapabilities nc = new NetworkCapabilities + .Builder(UPDATE_REQUEST.getNetworkCapabilities()) + .addTransportType(TRANSPORT_TEST).build(); + + return new EthernetNetworkUpdateRequest + .Builder(UPDATE_REQUEST) + .setNetworkCapabilities(nc).build(); + } + + @Test + public void testUpdateConfigurationForTestRequestDoesNotRequireAutoOrEthernetPermission() { + enableTestInterface(); + toggleAutomotiveFeature(false); + denyManageEthPermission(); + final EthernetNetworkUpdateRequest request = createTestNetworkUpdateRequest(); + + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, request, NULL_LISTENER); + verify(mEthernetTracker).updateConfiguration( + eq(TEST_IFACE), + eq(request.getIpConfiguration()), + eq(request.getNetworkCapabilities()), eq(NULL_LISTENER)); + } + + @Test + public void testConnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() { + enableTestInterface(); + toggleAutomotiveFeature(false); + denyManageEthPermission(); + + mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER); + verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER)); + } + + @Test + public void testDisconnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() { + enableTestInterface(); + toggleAutomotiveFeature(false); + denyManageEthPermission(); + + mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER); + verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER)); + } + + private void denyPermissions(String... permissions) { + for (String permission: permissions) { + doReturn(PackageManager.PERMISSION_DENIED).when(mContext) + .checkCallingOrSelfPermission(eq(permission)); + } + } + + @Test + public void testSetEthernetEnabled() { + denyPermissions(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); + mEthernetServiceImpl.setEthernetEnabled(true); + verify(mEthernetTracker).setEthernetEnabled(true); + reset(mEthernetTracker); + + denyPermissions(Manifest.permission.NETWORK_STACK); + mEthernetServiceImpl.setEthernetEnabled(false); + verify(mEthernetTracker).setEthernetEnabled(false); + reset(mEthernetTracker); + + denyPermissions(Manifest.permission.NETWORK_SETTINGS); + try { + mEthernetServiceImpl.setEthernetEnabled(true); + fail("Should get SecurityException"); + } catch (SecurityException e) { } + verify(mEthernetTracker, never()).setEthernetEnabled(false); + } +} diff --git a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java new file mode 100644 index 0000000000..b1831c411b --- /dev/null +++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java @@ -0,0 +1,456 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.ethernet; + +import static android.net.TestNetworkManager.TEST_TAP_PREFIX; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +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.content.res.Resources; +import android.net.EthernetManager; +import android.net.InetAddresses; +import android.net.INetworkInterfaceOutcomeReceiver; +import android.net.IEthernetServiceListener; +import android.net.INetd; +import android.net.IpConfiguration; +import android.net.IpConfiguration.IpAssignment; +import android.net.IpConfiguration.ProxySettings; +import android.net.InterfaceConfigurationParcel; +import android.net.LinkAddress; +import android.net.NetworkCapabilities; +import android.net.StaticIpConfiguration; +import android.os.HandlerThread; +import android.os.RemoteException; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.connectivity.resources.R; +import com.android.testutils.HandlerUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.net.InetAddress; +import java.util.ArrayList; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class EthernetTrackerTest { + private static final String TEST_IFACE = "test123"; + private static final int TIMEOUT_MS = 1_000; + private static final String THREAD_NAME = "EthernetServiceThread"; + private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null; + private EthernetTracker tracker; + private HandlerThread mHandlerThread; + @Mock private Context mContext; + @Mock private EthernetNetworkFactory mFactory; + @Mock private INetd mNetd; + @Mock private EthernetTracker.Dependencies mDeps; + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + initMockResources(); + when(mFactory.updateInterfaceLinkState(anyString(), anyBoolean(), any())).thenReturn(false); + when(mNetd.interfaceGetList()).thenReturn(new String[0]); + mHandlerThread = new HandlerThread(THREAD_NAME); + mHandlerThread.start(); + tracker = new EthernetTracker(mContext, mHandlerThread.getThreadHandler(), mFactory, mNetd, + mDeps); + } + + @After + public void cleanUp() { + mHandlerThread.quitSafely(); + } + + private void initMockResources() { + when(mDeps.getInterfaceRegexFromResource(eq(mContext))).thenReturn(""); + when(mDeps.getInterfaceConfigFromResource(eq(mContext))).thenReturn(new String[0]); + } + + private void waitForIdle() { + HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS); + } + + /** + * Test: Creation of various valid static IP configurations + */ + @Test + public void createStaticIpConfiguration() { + // Empty gives default StaticIPConfiguration object + assertStaticConfiguration(new StaticIpConfiguration(), ""); + + // Setting only the IP address properly cascades and assumes defaults + assertStaticConfiguration(new StaticIpConfiguration.Builder() + .setIpAddress(new LinkAddress("192.0.2.10/24")).build(), "ip=192.0.2.10/24"); + + final ArrayList<InetAddress> dnsAddresses = new ArrayList<>(); + dnsAddresses.add(InetAddresses.parseNumericAddress("4.4.4.4")); + dnsAddresses.add(InetAddresses.parseNumericAddress("8.8.8.8")); + // Setting other fields properly cascades them + assertStaticConfiguration(new StaticIpConfiguration.Builder() + .setIpAddress(new LinkAddress("192.0.2.10/24")) + .setDnsServers(dnsAddresses) + .setGateway(InetAddresses.parseNumericAddress("192.0.2.1")) + .setDomains("android").build(), + "ip=192.0.2.10/24 dns=4.4.4.4,8.8.8.8 gateway=192.0.2.1 domains=android"); + + // Verify order doesn't matter + assertStaticConfiguration(new StaticIpConfiguration.Builder() + .setIpAddress(new LinkAddress("192.0.2.10/24")) + .setDnsServers(dnsAddresses) + .setGateway(InetAddresses.parseNumericAddress("192.0.2.1")) + .setDomains("android").build(), + "domains=android ip=192.0.2.10/24 gateway=192.0.2.1 dns=4.4.4.4,8.8.8.8 "); + } + + /** + * Test: Attempt creation of various bad static IP configurations + */ + @Test + public void createStaticIpConfiguration_Bad() { + assertStaticConfigurationFails("ip=192.0.2.1/24 gateway= blah=20.20.20.20"); // Unknown key + assertStaticConfigurationFails("ip=192.0.2.1"); // mask is missing + assertStaticConfigurationFails("ip=a.b.c"); // not a valid ip address + assertStaticConfigurationFails("dns=4.4.4.4,1.2.3.A"); // not valid ip address in dns + assertStaticConfigurationFails("="); // Key and value is empty + assertStaticConfigurationFails("ip="); // Value is empty + assertStaticConfigurationFails("ip=192.0.2.1/24 gateway="); // Gateway is empty + } + + private void assertStaticConfigurationFails(String config) { + try { + EthernetTracker.parseStaticIpConfiguration(config); + fail("Expected to fail: " + config); + } catch (IllegalArgumentException e) { + // expected + } + } + + private void assertStaticConfiguration(StaticIpConfiguration expectedStaticIpConfig, + String configAsString) { + final IpConfiguration expectedIpConfiguration = new IpConfiguration(); + expectedIpConfiguration.setIpAssignment(IpAssignment.STATIC); + expectedIpConfiguration.setProxySettings(ProxySettings.NONE); + expectedIpConfiguration.setStaticIpConfiguration(expectedStaticIpConfig); + + assertEquals(expectedIpConfiguration, + EthernetTracker.parseStaticIpConfiguration(configAsString)); + } + + private NetworkCapabilities.Builder makeEthernetCapabilitiesBuilder(boolean clearAll) { + final NetworkCapabilities.Builder builder = + clearAll ? NetworkCapabilities.Builder.withoutDefaultCapabilities() + : new NetworkCapabilities.Builder(); + return builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED); + } + + /** + * Test: Attempt to create a capabilties with various valid sets of capabilities/transports + */ + @Test + public void createNetworkCapabilities() { + + // Particularly common expected results + NetworkCapabilities defaultEthernetCleared = + makeEthernetCapabilitiesBuilder(true /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .build(); + + NetworkCapabilities ethernetClearedWithCommonCaps = + makeEthernetCapabilitiesBuilder(true /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .addCapability(12) + .addCapability(13) + .addCapability(14) + .addCapability(15) + .build(); + + // Empty capabilities and transports lists with a "please clear defaults" should + // yield an empty capabilities set with TRANPORT_ETHERNET + assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", ""); + + // Empty capabilities and transports without the clear defaults flag should return the + // default capabilities set with TRANSPORT_ETHERNET + assertParsedNetworkCapabilities( + makeEthernetCapabilitiesBuilder(false /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .build(), + false, "", ""); + + // A list of capabilities without the clear defaults flag should return the default + // capabilities, mixed with the desired capabilities, and TRANSPORT_ETHERNET + assertParsedNetworkCapabilities( + makeEthernetCapabilitiesBuilder(false /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .addCapability(11) + .addCapability(12) + .build(), + false, "11,12", ""); + + // Adding a list of capabilities with a clear defaults will leave exactly those capabilities + // with a default TRANSPORT_ETHERNET since no overrides are specified + assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15", ""); + + // Adding any invalid capabilities to the list will cause them to be ignored + assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,65,73", ""); + assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,abcdefg", ""); + + // Adding a valid override transport will remove the default TRANSPORT_ETHERNET transport + // and apply only the override to the capabiltities object + assertParsedNetworkCapabilities( + makeEthernetCapabilitiesBuilder(true /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addTransportType(0) + .build(), + true, "", "0"); + assertParsedNetworkCapabilities( + makeEthernetCapabilitiesBuilder(true /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addTransportType(1) + .build(), + true, "", "1"); + assertParsedNetworkCapabilities( + makeEthernetCapabilitiesBuilder(true /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addTransportType(2) + .build(), + true, "", "2"); + assertParsedNetworkCapabilities( + makeEthernetCapabilitiesBuilder(true /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addTransportType(3) + .build(), + true, "", "3"); + + // "4" is TRANSPORT_VPN, which is unsupported. Should default back to TRANPORT_ETHERNET + assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "4"); + + // "5" is TRANSPORT_WIFI_AWARE, which is currently supported due to no legacy TYPE_NONE + // conversion. When that becomes available, this test must be updated + assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "5"); + + // "6" is TRANSPORT_LOWPAN, which is currently supported due to no legacy TYPE_NONE + // conversion. When that becomes available, this test must be updated + assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "6"); + + // Adding an invalid override transport will leave the transport as TRANSPORT_ETHERNET + assertParsedNetworkCapabilities(defaultEthernetCleared,true, "", "100"); + assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "abcdefg"); + + // Ensure the adding of both capabilities and transports work + assertParsedNetworkCapabilities( + makeEthernetCapabilitiesBuilder(true /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addCapability(12) + .addCapability(13) + .addCapability(14) + .addCapability(15) + .addTransportType(3) + .build(), + true, "12,13,14,15", "3"); + + // Ensure order does not matter for capability list + assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "13,12,15,14", ""); + } + + private void assertParsedNetworkCapabilities(NetworkCapabilities expectedNetworkCapabilities, + boolean clearCapabilties, String configCapabiltiies,String configTransports) { + assertEquals(expectedNetworkCapabilities, + EthernetTracker.createNetworkCapabilities(clearCapabilties, configCapabiltiies, + configTransports).build()); + } + + @Test + public void testCreateEthernetTrackerConfigReturnsCorrectValue() { + final String capabilities = "2"; + final String ipConfig = "3"; + final String transport = "4"; + final String configString = String.join(";", TEST_IFACE, capabilities, ipConfig, transport); + + final EthernetTracker.EthernetTrackerConfig config = + EthernetTracker.createEthernetTrackerConfig(configString); + + assertEquals(TEST_IFACE, config.mIface); + assertEquals(capabilities, config.mCapabilities); + assertEquals(ipConfig, config.mIpConfig); + assertEquals(transport, config.mTransport); + } + + @Test + public void testCreateEthernetTrackerConfigThrowsNpeWithNullInput() { + assertThrows(NullPointerException.class, + () -> EthernetTracker.createEthernetTrackerConfig(null)); + } + + @Test + public void testUpdateConfiguration() { + final NetworkCapabilities capabilities = new NetworkCapabilities.Builder().build(); + final LinkAddress linkAddr = new LinkAddress("192.0.2.2/25"); + final StaticIpConfiguration staticIpConfig = + new StaticIpConfiguration.Builder().setIpAddress(linkAddr).build(); + final IpConfiguration ipConfig = + new IpConfiguration.Builder().setStaticIpConfiguration(staticIpConfig).build(); + final INetworkInterfaceOutcomeReceiver listener = null; + + tracker.updateConfiguration(TEST_IFACE, ipConfig, capabilities, listener); + waitForIdle(); + + verify(mFactory).updateInterface( + eq(TEST_IFACE), eq(ipConfig), eq(capabilities), eq(listener)); + } + + @Test + public void testConnectNetworkCorrectlyCallsFactory() { + tracker.connectNetwork(TEST_IFACE, NULL_LISTENER); + waitForIdle(); + + verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(true /* up */), + eq(NULL_LISTENER)); + } + + @Test + public void testDisconnectNetworkCorrectlyCallsFactory() { + tracker.disconnectNetwork(TEST_IFACE, NULL_LISTENER); + waitForIdle(); + + verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(false /* up */), + eq(NULL_LISTENER)); + } + + @Test + public void testIsValidTestInterfaceIsFalseWhenTestInterfacesAreNotIncluded() { + final String validIfaceName = TEST_TAP_PREFIX + "123"; + tracker.setIncludeTestInterfaces(false); + waitForIdle(); + + final boolean isValidTestInterface = tracker.isValidTestInterface(validIfaceName); + + assertFalse(isValidTestInterface); + } + + @Test + public void testIsValidTestInterfaceIsFalseWhenTestInterfaceNameIsInvalid() { + final String invalidIfaceName = "123" + TEST_TAP_PREFIX; + tracker.setIncludeTestInterfaces(true); + waitForIdle(); + + final boolean isValidTestInterface = tracker.isValidTestInterface(invalidIfaceName); + + assertFalse(isValidTestInterface); + } + + @Test + public void testIsValidTestInterfaceIsTrueWhenTestInterfacesIncludedAndValidName() { + final String validIfaceName = TEST_TAP_PREFIX + "123"; + tracker.setIncludeTestInterfaces(true); + waitForIdle(); + + final boolean isValidTestInterface = tracker.isValidTestInterface(validIfaceName); + + assertTrue(isValidTestInterface); + } + + public static class EthernetStateListener extends IEthernetServiceListener.Stub { + @Override + public void onEthernetStateChanged(int state) { } + + @Override + public void onInterfaceStateChanged(String iface, int state, int role, + IpConfiguration configuration) { } + } + + @Test + public void testListenEthernetStateChange() throws Exception { + final String testIface = "testtap123"; + final String testHwAddr = "11:22:33:44:55:66"; + final InterfaceConfigurationParcel ifaceParcel = new InterfaceConfigurationParcel(); + ifaceParcel.ifName = testIface; + ifaceParcel.hwAddr = testHwAddr; + ifaceParcel.flags = new String[] {INetd.IF_STATE_UP}; + + tracker.setIncludeTestInterfaces(true); + waitForIdle(); + + when(mNetd.interfaceGetList()).thenReturn(new String[] {testIface}); + when(mNetd.interfaceGetCfg(eq(testIface))).thenReturn(ifaceParcel); + doReturn(new String[] {testIface}).when(mFactory).getAvailableInterfaces(anyBoolean()); + doReturn(EthernetManager.STATE_LINK_UP).when(mFactory).getInterfaceState(eq(testIface)); + + final EthernetStateListener listener = spy(new EthernetStateListener()); + tracker.addListener(listener, true /* canUseRestrictedNetworks */); + // Check default state. + waitForIdle(); + verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP), + anyInt(), any()); + verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_ENABLED)); + reset(listener); + + doReturn(EthernetManager.STATE_ABSENT).when(mFactory).getInterfaceState(eq(testIface)); + tracker.setEthernetEnabled(false); + waitForIdle(); + verify(mFactory).removeInterface(eq(testIface)); + verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_DISABLED)); + verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_ABSENT), + anyInt(), any()); + reset(listener); + + doReturn(EthernetManager.STATE_LINK_UP).when(mFactory).getInterfaceState(eq(testIface)); + tracker.setEthernetEnabled(true); + waitForIdle(); + verify(mFactory).addInterface(eq(testIface), eq(testHwAddr), any(), any()); + verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_ENABLED)); + verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP), + anyInt(), any()); + } +} |
