Merge "Put tether/untether calls into handler queue"
diff --git a/OWNERS b/OWNERS
index 0e1e65d..22b5561 100644
--- a/OWNERS
+++ b/OWNERS
@@ -2,5 +2,6 @@
jchalard@google.com
junyulai@google.com
lorenzo@google.com
+maze@google.com
reminv@google.com
satk@google.com
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 1db4baa..ef96d88 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,9 +1,11 @@
{
- // Run in addition to mainline-presubmit as mainline-presubmit is not
- // supported in every branch.
"presubmit": [
+ // Run in addition to mainline-presubmit as mainline-presubmit is not
+ // supported in every branch.
+ // CtsNetTestCasesLatestSdk uses stable API shims, so does not exercise
+ // some latest APIs. Run CtsNetTestCases to get coverage of newer APIs.
{
- "name": "CtsNetTestCasesLatestSdk",
+ "name": "CtsNetTestCases",
"options": [
{
"exclude-annotation": "com.android.testutils.SkipPresubmit"
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index d8557ad..4eafc2a 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -14,28 +14,34 @@
// limitations under the License.
//
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
java_defaults {
name: "TetheringAndroidLibraryDefaults",
sdk_version: "module_current",
min_sdk_version: "30",
srcs: [
+ "apishim/**/*.java",
"src/**/*.java",
- ":framework-tethering-shared-srcs",
+ ":framework-connectivity-shared-srcs",
":tethering-module-utils-srcs",
":services-tethering-shared-srcs",
],
static_libs: [
"androidx.annotation_annotation",
- "netd_aidl_interface-unstable-java",
+ "modules-utils-build",
"netlink-client",
- // TODO: use networkstack-client instead of just including the AIDL interface
- "networkstack-aidl-interfaces-unstable-java",
+ "networkstack-client",
"android.hardware.tetheroffload.config-V1.0-java",
"android.hardware.tetheroffload.control-V1.0-java",
"net-utils-framework-common",
"net-utils-device-common",
+ "netd-client",
],
libs: [
+ "framework-connectivity",
"framework-statsd.stubs.module_lib",
"framework-tethering.impl",
"framework-wifi",
@@ -60,8 +66,12 @@
"com.android.tethering",
],
min_sdk_version: "30",
+ header_libs: [
+ "bpf_syscall_wrappers",
+ "bpf_tethering_headers",
+ ],
srcs: [
- "jni/android_net_util_TetheringUtils.cpp",
+ "jni/*.cpp",
],
shared_libs: [
"liblog",
@@ -139,3 +149,10 @@
apex_available: ["com.android.tethering"],
min_sdk_version: "30",
}
+
+sdk {
+ name: "tethering-module-sdk",
+ java_sdk_libs: [
+ "framework-tethering",
+ ],
+}
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index a1e7fd2..164bda4 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -14,13 +14,31 @@
// limitations under the License.
//
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
apex {
name: "com.android.tethering",
- updatable: true,
- min_sdk_version: "30",
- java_libs: ["framework-tethering"],
- bpfs: ["offload.o"],
- apps: ["Tethering"],
+ // TODO: make updatable again once this contains only updatable artifacts (in particular, this
+ // cannot build as updatable unless service-connectivity builds against stable API).
+ updatable: false,
+ // min_sdk_version: "30",
+ java_libs: [
+ "framework-tethering",
+ "service-connectivity",
+ ],
+ jni_libs: [
+ "libservice-connectivity",
+ ],
+ bpfs: [
+ "offload.o",
+ "test.o",
+ ],
+ apps: [
+ "ServiceConnectivityResources",
+ "Tethering",
+ ],
manifest: "manifest.json",
key: "com.android.tethering.key",
@@ -43,6 +61,7 @@
base: "com.android.tethering",
package_name: "com.android.tethering.inprocess",
apps: [
+ "ServiceConnectivityResources",
"InProcessTethering",
],
}
diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
new file mode 100644
index 0000000..e310fb6
--- /dev/null
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering.apishim.api30;
+
+import android.net.INetd;
+import android.net.MacAddress;
+import android.net.TetherStatsParcel;
+import android.net.util.SharedLog;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.Tether4Key;
+import com.android.networkstack.tethering.Tether4Value;
+import com.android.networkstack.tethering.TetherStatsValue;
+
+/**
+ * Bpf coordinator class for API shims.
+ */
+public class BpfCoordinatorShimImpl
+ extends com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim {
+ private static final String TAG = "api30.BpfCoordinatorShimImpl";
+
+ @NonNull
+ private final SharedLog mLog;
+ @NonNull
+ private final INetd mNetd;
+
+ public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) {
+ mLog = deps.getSharedLog().forSubComponent(TAG);
+ mNetd = deps.getNetd();
+ }
+
+ @Override
+ public boolean isInitialized() {
+ return true;
+ };
+
+ @Override
+ public boolean tetherOffloadRuleAdd(@NonNull final Ipv6ForwardingRule rule) {
+ try {
+ mNetd.tetherOffloadRuleAdd(rule.toTetherOffloadRuleParcel());
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e("Could not add IPv6 forwarding rule: ", e);
+ return false;
+ }
+
+ return true;
+ };
+
+ @Override
+ public boolean tetherOffloadRuleRemove(@NonNull final Ipv6ForwardingRule rule) {
+ try {
+ mNetd.tetherOffloadRuleRemove(rule.toTetherOffloadRuleParcel());
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e("Could not remove IPv6 forwarding rule: ", e);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean startUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
+ @NonNull MacAddress inDstMac, @NonNull MacAddress outSrcMac,
+ @NonNull MacAddress outDstMac, int mtu) {
+ return true;
+ }
+
+ @Override
+ public boolean stopUpstreamIpv6Forwarding(int downstreamIfindex,
+ int upstreamIfindex, @NonNull MacAddress inDstMac) {
+ return true;
+ }
+
+ @Override
+ @Nullable
+ public SparseArray<TetherStatsValue> tetherOffloadGetStats() {
+ final TetherStatsParcel[] tetherStatsList;
+ try {
+ // The reported tether stats are total data usage for all currently-active upstream
+ // interfaces since tethering start. There will only ever be one entry for a given
+ // interface index.
+ tetherStatsList = mNetd.tetherOffloadGetStats();
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e("Fail to fetch tethering stats from netd: " + e);
+ return null;
+ }
+
+ return toTetherStatsValueSparseArray(tetherStatsList);
+ }
+
+ @Override
+ public boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes) {
+ try {
+ mNetd.tetherOffloadSetInterfaceQuota(ifIndex, quotaBytes);
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e("Exception when updating quota " + quotaBytes + ": ", e);
+ return false;
+ }
+ return true;
+ }
+
+ @NonNull
+ private SparseArray<TetherStatsValue> toTetherStatsValueSparseArray(
+ @NonNull final TetherStatsParcel[] parcels) {
+ final SparseArray<TetherStatsValue> tetherStatsList = new SparseArray<TetherStatsValue>();
+
+ for (TetherStatsParcel p : parcels) {
+ tetherStatsList.put(p.ifIndex, new TetherStatsValue(p.rxPackets, p.rxBytes,
+ 0 /* rxErrors */, p.txPackets, p.txBytes, 0 /* txErrors */));
+ }
+
+ return tetherStatsList;
+ }
+
+ @Override
+ @Nullable
+ public TetherStatsValue tetherOffloadGetAndClearStats(int ifIndex) {
+ try {
+ final TetherStatsParcel stats =
+ mNetd.tetherOffloadGetAndClearStats(ifIndex);
+ return new TetherStatsValue(stats.rxPackets, stats.rxBytes, 0 /* rxErrors */,
+ stats.txPackets, stats.txBytes, 0 /* txErrors */);
+ } catch (RemoteException | ServiceSpecificException e) {
+ mLog.e("Exception when cleanup tether stats for upstream index "
+ + ifIndex + ": ", e);
+ return null;
+ }
+ }
+
+ @Override
+ public boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key,
+ @NonNull Tether4Value value) {
+ /* no op */
+ return true;
+ }
+
+ @Override
+ public boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key) {
+ /* no op */
+ return true;
+ }
+
+ @Override
+ public boolean attachProgram(String iface, boolean downstream) {
+ /* no op */
+ return true;
+ }
+
+ @Override
+ public boolean detachProgram(String iface) {
+ /* no op */
+ return true;
+ }
+
+ @Override
+ public boolean isAnyIpv4RuleOnUpstream(int ifIndex) {
+ /* no op */
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "Netd used";
+ }
+}
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
new file mode 100644
index 0000000..d7ce139
--- /dev/null
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -0,0 +1,482 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering.apishim.api31;
+
+import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
+
+import android.net.MacAddress;
+import android.net.util.SharedLog;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.BpfMap;
+import com.android.networkstack.tethering.BpfUtils;
+import com.android.networkstack.tethering.Tether4Key;
+import com.android.networkstack.tethering.Tether4Value;
+import com.android.networkstack.tethering.Tether6Value;
+import com.android.networkstack.tethering.TetherDownstream6Key;
+import com.android.networkstack.tethering.TetherLimitKey;
+import com.android.networkstack.tethering.TetherLimitValue;
+import com.android.networkstack.tethering.TetherStatsKey;
+import com.android.networkstack.tethering.TetherStatsValue;
+import com.android.networkstack.tethering.TetherUpstream6Key;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * Bpf coordinator class for API shims.
+ */
+public class BpfCoordinatorShimImpl
+ extends com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim {
+ private static final String TAG = "api31.BpfCoordinatorShimImpl";
+
+ // AF_KEY socket type. See include/linux/socket.h.
+ private static final int AF_KEY = 15;
+ // PFKEYv2 constants. See include/uapi/linux/pfkeyv2.h.
+ private static final int PF_KEY_V2 = 2;
+
+ @NonNull
+ private final SharedLog mLog;
+
+ // BPF map for downstream IPv4 forwarding.
+ @Nullable
+ private final BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map;
+
+ // BPF map for upstream IPv4 forwarding.
+ @Nullable
+ private final BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map;
+
+ // BPF map for downstream IPv6 forwarding.
+ @Nullable
+ private final BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
+
+ // BPF map for upstream IPv6 forwarding.
+ @Nullable
+ private final BpfMap<TetherUpstream6Key, Tether6Value> mBpfUpstream6Map;
+
+ // BPF map of tethering statistics of the upstream interface since tethering startup.
+ @Nullable
+ private final BpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap;
+
+ // BPF map of per-interface quota for tethering offload.
+ @Nullable
+ private final BpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap;
+
+ // Tracking IPv4 rule count while any rule is using the given upstream interfaces. Used for
+ // reducing the BPF map iteration query. The count is increased or decreased when the rule is
+ // added or removed successfully on mBpfDownstream4Map. Counting the rules on downstream4 map
+ // is because tetherOffloadRuleRemove can't get upstream interface index from upstream key,
+ // unless pass upstream value which is not required for deleting map entry. The upstream
+ // interface index is the same in Upstream4Value.oif and Downstream4Key.iif. For now, it is
+ // okay to count on Downstream4Key. See BpfConntrackEventConsumer#accept.
+ // Note that except the constructor, any calls to mBpfDownstream4Map.clear() need to clear
+ // this counter as well.
+ // TODO: Count the rule on upstream if multi-upstream is supported and the
+ // packet needs to be sent and responded on different upstream interfaces.
+ // TODO: Add IPv6 rule count.
+ private final SparseArray<Integer> mRule4CountOnUpstream = new SparseArray<>();
+
+ public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) {
+ mLog = deps.getSharedLog().forSubComponent(TAG);
+
+ mBpfDownstream4Map = deps.getBpfDownstream4Map();
+ mBpfUpstream4Map = deps.getBpfUpstream4Map();
+ mBpfDownstream6Map = deps.getBpfDownstream6Map();
+ mBpfUpstream6Map = deps.getBpfUpstream6Map();
+ mBpfStatsMap = deps.getBpfStatsMap();
+ mBpfLimitMap = deps.getBpfLimitMap();
+
+ // Clear the stubs of the maps for handling the system service crash if any.
+ // Doesn't throw the exception and clear the stubs as many as possible.
+ try {
+ if (mBpfDownstream4Map != null) mBpfDownstream4Map.clear();
+ } catch (ErrnoException e) {
+ mLog.e("Could not clear mBpfDownstream4Map: " + e);
+ }
+ try {
+ if (mBpfUpstream4Map != null) mBpfUpstream4Map.clear();
+ } catch (ErrnoException e) {
+ mLog.e("Could not clear mBpfUpstream4Map: " + e);
+ }
+ try {
+ if (mBpfDownstream6Map != null) mBpfDownstream6Map.clear();
+ } catch (ErrnoException e) {
+ mLog.e("Could not clear mBpfDownstream6Map: " + e);
+ }
+ try {
+ if (mBpfUpstream6Map != null) mBpfUpstream6Map.clear();
+ } catch (ErrnoException e) {
+ mLog.e("Could not clear mBpfUpstream6Map: " + e);
+ }
+ try {
+ if (mBpfStatsMap != null) mBpfStatsMap.clear();
+ } catch (ErrnoException e) {
+ mLog.e("Could not clear mBpfStatsMap: " + e);
+ }
+ try {
+ if (mBpfLimitMap != null) mBpfLimitMap.clear();
+ } catch (ErrnoException e) {
+ mLog.e("Could not clear mBpfLimitMap: " + e);
+ }
+ }
+
+ @Override
+ public boolean isInitialized() {
+ return mBpfDownstream4Map != null && mBpfUpstream4Map != null && mBpfDownstream6Map != null
+ && mBpfUpstream6Map != null && mBpfStatsMap != null && mBpfLimitMap != null;
+ }
+
+ @Override
+ public boolean tetherOffloadRuleAdd(@NonNull final Ipv6ForwardingRule rule) {
+ if (!isInitialized()) return false;
+
+ final TetherDownstream6Key key = rule.makeTetherDownstream6Key();
+ final Tether6Value value = rule.makeTether6Value();
+
+ try {
+ mBpfDownstream6Map.updateEntry(key, value);
+ } catch (ErrnoException e) {
+ mLog.e("Could not update entry: ", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean tetherOffloadRuleRemove(@NonNull final Ipv6ForwardingRule rule) {
+ if (!isInitialized()) return false;
+
+ try {
+ mBpfDownstream6Map.deleteEntry(rule.makeTetherDownstream6Key());
+ } catch (ErrnoException e) {
+ // Silent if the rule did not exist.
+ if (e.errno != OsConstants.ENOENT) {
+ mLog.e("Could not update entry: ", e);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean startUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
+ @NonNull MacAddress inDstMac, @NonNull MacAddress outSrcMac,
+ @NonNull MacAddress outDstMac, int mtu) {
+ if (!isInitialized()) return false;
+
+ final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfindex, inDstMac);
+ final Tether6Value value = new Tether6Value(upstreamIfindex, outSrcMac,
+ outDstMac, OsConstants.ETH_P_IPV6, mtu);
+ try {
+ mBpfUpstream6Map.insertEntry(key, value);
+ } catch (ErrnoException | IllegalStateException e) {
+ mLog.e("Could not insert upstream6 entry: " + e);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean stopUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
+ @NonNull MacAddress inDstMac) {
+ if (!isInitialized()) return false;
+
+ final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfindex, inDstMac);
+ try {
+ mBpfUpstream6Map.deleteEntry(key);
+ } catch (ErrnoException e) {
+ mLog.e("Could not delete upstream IPv6 entry: " + e);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ @Nullable
+ public SparseArray<TetherStatsValue> tetherOffloadGetStats() {
+ if (!isInitialized()) return null;
+
+ final SparseArray<TetherStatsValue> tetherStatsList = new SparseArray<TetherStatsValue>();
+ try {
+ // The reported tether stats are total data usage for all currently-active upstream
+ // interfaces since tethering start.
+ mBpfStatsMap.forEach((key, value) -> tetherStatsList.put((int) key.ifindex, value));
+ } catch (ErrnoException e) {
+ mLog.e("Fail to fetch tethering stats from BPF map: ", e);
+ return null;
+ }
+ return tetherStatsList;
+ }
+
+ @Override
+ public boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes) {
+ if (!isInitialized()) return false;
+
+ // The common case is an update, where the stats already exist,
+ // hence we read first, even though writing with BPF_NOEXIST
+ // first would make the code simpler.
+ long rxBytes, txBytes;
+ TetherStatsValue statsValue = null;
+
+ try {
+ statsValue = mBpfStatsMap.getValue(new TetherStatsKey(ifIndex));
+ } catch (ErrnoException e) {
+ // The BpfMap#getValue doesn't throw an errno ENOENT exception. Catch other error
+ // while trying to get stats entry.
+ mLog.e("Could not get stats entry of interface index " + ifIndex + ": ", e);
+ return false;
+ }
+
+ if (statsValue != null) {
+ // Ok, there was a stats entry.
+ rxBytes = statsValue.rxBytes;
+ txBytes = statsValue.txBytes;
+ } else {
+ // No stats entry - create one with zeroes.
+ try {
+ // This function is the *only* thing that can create entries.
+ // BpfMap#insertEntry use BPF_NOEXIST to create the entry. The entry is created
+ // if and only if it doesn't exist.
+ mBpfStatsMap.insertEntry(new TetherStatsKey(ifIndex), new TetherStatsValue(
+ 0 /* rxPackets */, 0 /* rxBytes */, 0 /* rxErrors */, 0 /* txPackets */,
+ 0 /* txBytes */, 0 /* txErrors */));
+ } catch (ErrnoException | IllegalArgumentException e) {
+ mLog.e("Could not create stats entry: ", e);
+ return false;
+ }
+ rxBytes = 0;
+ txBytes = 0;
+ }
+
+ // rxBytes + txBytes won't overflow even at 5gbps for ~936 years.
+ long newLimit = rxBytes + txBytes + quotaBytes;
+
+ // if adding limit (e.g., if limit is QUOTA_UNLIMITED) caused overflow: clamp to 'infinity'
+ if (newLimit < rxBytes + txBytes) newLimit = QUOTA_UNLIMITED;
+
+ try {
+ mBpfLimitMap.updateEntry(new TetherLimitKey(ifIndex), new TetherLimitValue(newLimit));
+ } catch (ErrnoException e) {
+ mLog.e("Fail to set quota " + quotaBytes + " for interface index " + ifIndex + ": ", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ @Nullable
+ public TetherStatsValue tetherOffloadGetAndClearStats(int ifIndex) {
+ if (!isInitialized()) return null;
+
+ // getAndClearTetherOffloadStats is called after all offload rules have already been
+ // deleted for the given upstream interface. Before starting to do cleanup stuff in this
+ // function, use synchronizeKernelRCU to make sure that all the current running eBPF
+ // programs are finished on all CPUs, especially the unfinished packet processing. After
+ // synchronizeKernelRCU returned, we can safely read or delete on the stats map or the
+ // limit map.
+ final int res = synchronizeKernelRCU();
+ if (res != 0) {
+ // Error log but don't return. Do as much cleanup as possible.
+ mLog.e("synchronize_rcu() failed: " + res);
+ }
+
+ TetherStatsValue statsValue = null;
+ try {
+ statsValue = mBpfStatsMap.getValue(new TetherStatsKey(ifIndex));
+ } catch (ErrnoException e) {
+ mLog.e("Could not get stats entry for interface index " + ifIndex + ": ", e);
+ return null;
+ }
+
+ if (statsValue == null) {
+ mLog.e("Could not get stats entry for interface index " + ifIndex);
+ return null;
+ }
+
+ try {
+ mBpfStatsMap.deleteEntry(new TetherStatsKey(ifIndex));
+ } catch (ErrnoException e) {
+ mLog.e("Could not delete stats entry for interface index " + ifIndex + ": ", e);
+ return null;
+ }
+
+ try {
+ mBpfLimitMap.deleteEntry(new TetherLimitKey(ifIndex));
+ } catch (ErrnoException e) {
+ mLog.e("Could not delete limit for interface index " + ifIndex + ": ", e);
+ return null;
+ }
+
+ return statsValue;
+ }
+
+ @Override
+ public boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key,
+ @NonNull Tether4Value value) {
+ if (!isInitialized()) return false;
+
+ try {
+ if (downstream) {
+ mBpfDownstream4Map.insertEntry(key, value);
+
+ // Increase the rule count while a adding rule is using a given upstream interface.
+ final int upstreamIfindex = (int) key.iif;
+ int count = mRule4CountOnUpstream.get(upstreamIfindex, 0 /* default */);
+ mRule4CountOnUpstream.put(upstreamIfindex, ++count);
+ } else {
+ mBpfUpstream4Map.insertEntry(key, value);
+ }
+ } catch (ErrnoException e) {
+ mLog.e("Could not insert entry (" + key + ", " + value + "): " + e);
+ return false;
+ } catch (IllegalStateException e) {
+ // Silent if the rule already exists. Note that the errno EEXIST was rethrown as
+ // IllegalStateException. See BpfMap#insertEntry.
+ }
+ return true;
+ }
+
+ @Override
+ public boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key) {
+ if (!isInitialized()) return false;
+
+ try {
+ if (downstream) {
+ if (!mBpfDownstream4Map.deleteEntry(key)) {
+ mLog.e("Could not delete entry (key: " + key + ")");
+ return false;
+ }
+
+ // Decrease the rule count while a deleting rule is not using a given upstream
+ // interface anymore.
+ final int upstreamIfindex = (int) key.iif;
+ Integer count = mRule4CountOnUpstream.get(upstreamIfindex);
+ if (count == null) {
+ Log.wtf(TAG, "Could not delete count for interface " + upstreamIfindex);
+ return false;
+ }
+
+ if (--count == 0) {
+ // Remove the entry if the count decreases to zero.
+ mRule4CountOnUpstream.remove(upstreamIfindex);
+ } else {
+ mRule4CountOnUpstream.put(upstreamIfindex, count);
+ }
+ } else {
+ mBpfUpstream4Map.deleteEntry(key);
+ }
+ } catch (ErrnoException e) {
+ // Silent if the rule did not exist.
+ if (e.errno != OsConstants.ENOENT) {
+ mLog.e("Could not delete entry: ", e);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean attachProgram(String iface, boolean downstream) {
+ if (!isInitialized()) return false;
+
+ try {
+ BpfUtils.attachProgram(iface, downstream);
+ } catch (IOException e) {
+ mLog.e("Could not attach program: " + e);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean detachProgram(String iface) {
+ if (!isInitialized()) return false;
+
+ try {
+ BpfUtils.detachProgram(iface);
+ } catch (IOException e) {
+ mLog.e("Could not detach program: " + e);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isAnyIpv4RuleOnUpstream(int ifIndex) {
+ // No entry means no rule for the given interface because 0 has never been stored.
+ return mRule4CountOnUpstream.get(ifIndex) != null;
+ }
+
+ private String mapStatus(BpfMap m, String name) {
+ return name + "{" + (m != null ? "OK" : "ERROR") + "}";
+ }
+
+ @Override
+ public String toString() {
+ return String.join(", ", new String[] {
+ mapStatus(mBpfDownstream6Map, "mBpfDownstream6Map"),
+ mapStatus(mBpfUpstream6Map, "mBpfUpstream6Map"),
+ mapStatus(mBpfDownstream4Map, "mBpfDownstream4Map"),
+ mapStatus(mBpfUpstream4Map, "mBpfUpstream4Map"),
+ mapStatus(mBpfStatsMap, "mBpfStatsMap"),
+ mapStatus(mBpfLimitMap, "mBpfLimitMap")
+ });
+ }
+
+ /**
+ * Call synchronize_rcu() to block until all existing RCU read-side critical sections have
+ * been completed.
+ * Note that BpfCoordinatorTest have no permissions to create or close pf_key socket. It is
+ * okay for now because the caller #bpfGetAndClearStats doesn't care the result of this
+ * function. The tests don't be broken.
+ * TODO: Wrap this function into Dependencies for mocking in tests.
+ */
+ private int synchronizeKernelRCU() {
+ // This is a temporary hack for network stats map swap on devices running
+ // 4.9 kernels. The kernel code of socket release on pf_key socket will
+ // explicitly call synchronize_rcu() which is exactly what we need.
+ FileDescriptor pfSocket;
+ try {
+ pfSocket = Os.socket(AF_KEY, OsConstants.SOCK_RAW | OsConstants.SOCK_CLOEXEC,
+ PF_KEY_V2);
+ } catch (ErrnoException e) {
+ mLog.e("create PF_KEY socket failed: ", e);
+ return e.errno;
+ }
+
+ // When closing socket, synchronize_rcu() gets called in sock_release().
+ try {
+ Os.close(pfSocket);
+ } catch (ErrnoException e) {
+ mLog.e("failed to close the PF_KEY socket: ", e);
+ return e.errno;
+ }
+
+ return 0;
+ }
+}
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
new file mode 100644
index 0000000..79a628b
--- /dev/null
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering.apishim.common;
+
+import android.net.MacAddress;
+import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
+import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.Tether4Key;
+import com.android.networkstack.tethering.Tether4Value;
+import com.android.networkstack.tethering.TetherStatsValue;
+
+/**
+ * Bpf coordinator class for API shims.
+ */
+public abstract class BpfCoordinatorShim {
+ /**
+ * Get BpfCoordinatorShim object by OS build version.
+ */
+ @NonNull
+ public static BpfCoordinatorShim getBpfCoordinatorShim(@NonNull final Dependencies deps) {
+ if (deps.isAtLeastS()) {
+ return new com.android.networkstack.tethering.apishim.api31.BpfCoordinatorShimImpl(
+ deps);
+ } else {
+ return new com.android.networkstack.tethering.apishim.api30.BpfCoordinatorShimImpl(
+ deps);
+ }
+ }
+
+ /**
+ * Return true if this class has been initialized, otherwise return false.
+ */
+ public abstract boolean isInitialized();
+
+ /**
+ * Adds a tethering offload rule to BPF map, or updates it if it already exists.
+ *
+ * Currently, only downstream /128 IPv6 entries are supported. An existing rule will be updated
+ * if the input interface and destination prefix match. Otherwise, a new rule will be created.
+ * Note that this can be only called on handler thread.
+ *
+ * @param rule The rule to add or update.
+ */
+ public abstract boolean tetherOffloadRuleAdd(@NonNull Ipv6ForwardingRule rule);
+
+ /**
+ * Deletes a tethering offload rule from the BPF map.
+ *
+ * Currently, only downstream /128 IPv6 entries are supported. An existing rule will be deleted
+ * if the destination IP address and the source interface match. It is not an error if there is
+ * no matching rule to delete.
+ *
+ * @param rule The rule to delete.
+ */
+ public abstract boolean tetherOffloadRuleRemove(@NonNull Ipv6ForwardingRule rule);
+
+ /**
+ * Starts IPv6 forwarding between the specified interfaces.
+
+ * @param downstreamIfindex the downstream interface index
+ * @param upstreamIfindex the upstream interface index
+ * @param inDstMac the destination MAC address to use for XDP
+ * @param outSrcMac the source MAC address to use for packets
+ * @param outDstMac the destination MAC address to use for packets
+ * @return true if operation succeeded or was a no-op, false otherwise
+ */
+ public abstract boolean startUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex,
+ @NonNull MacAddress inDstMac, @NonNull MacAddress outSrcMac,
+ @NonNull MacAddress outDstMac, int mtu);
+
+ /**
+ * Stops IPv6 forwarding between the specified interfaces.
+
+ * @param downstreamIfindex the downstream interface index
+ * @param upstreamIfindex the upstream interface index
+ * @param inDstMac the destination MAC address to use for XDP
+ * @return true if operation succeeded or was a no-op, false otherwise
+ */
+ public abstract boolean stopUpstreamIpv6Forwarding(int downstreamIfindex,
+ int upstreamIfindex, @NonNull MacAddress inDstMac);
+
+ /**
+ * Return BPF tethering offload statistics.
+ *
+ * @return an array of TetherStatsValue's, where each entry contains the upstream interface
+ * index and its tethering statistics since tethering was first started.
+ * There will only ever be one entry for a given interface index.
+ */
+ @Nullable
+ public abstract SparseArray<TetherStatsValue> tetherOffloadGetStats();
+
+ /**
+ * Set a per-interface quota for tethering offload.
+ *
+ * @param ifIndex Index of upstream interface
+ * @param quotaBytes The quota defined as the number of bytes, starting from zero and counting
+ * from *now*. A value of QUOTA_UNLIMITED (-1) indicates there is no limit.
+ */
+ @Nullable
+ public abstract boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes);
+
+ /**
+ * Return BPF tethering offload statistics and clear the stats for a given upstream.
+ *
+ * Must only be called once all offload rules have already been deleted for the given upstream
+ * interface. The existing stats will be fetched and returned. The stats and the limit for the
+ * given upstream interface will be deleted as well.
+ *
+ * The stats and limit for a given upstream interface must be initialized (using
+ * tetherOffloadSetInterfaceQuota) before any offload will occur on that interface.
+ *
+ * Note that this can be only called while the BPF maps were initialized.
+ *
+ * @param ifIndex Index of upstream interface.
+ * @return TetherStatsValue, which contains the given upstream interface's tethering statistics
+ * since tethering was first started on that upstream interface.
+ */
+ @Nullable
+ public abstract TetherStatsValue tetherOffloadGetAndClearStats(int ifIndex);
+
+ /**
+ * Adds a tethering IPv4 offload rule to appropriate BPF map.
+ */
+ public abstract boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key,
+ @NonNull Tether4Value value);
+
+ /**
+ * Deletes a tethering IPv4 offload rule from the appropriate BPF map.
+ */
+ public abstract boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key);
+
+ /**
+ * Whether there is currently any IPv4 rule on the specified upstream.
+ */
+ public abstract boolean isAnyIpv4RuleOnUpstream(int ifIndex);
+
+ /**
+ * Attach BPF program.
+ *
+ * TODO: consider using InterfaceParams to replace interface name.
+ */
+ public abstract boolean attachProgram(@NonNull String iface, boolean downstream);
+
+ /**
+ * Detach BPF program.
+ *
+ * TODO: consider using InterfaceParams to replace interface name.
+ */
+ public abstract boolean detachProgram(@NonNull String iface);
+}
+
diff --git a/Tethering/bpf_progs/Android.bp b/Tethering/bpf_progs/Android.bp
index d54f861..289d75d 100644
--- a/Tethering/bpf_progs/Android.bp
+++ b/Tethering/bpf_progs/Android.bp
@@ -15,6 +15,30 @@
//
//
+// struct definitions shared with JNI
+//
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_headers {
+ name: "bpf_tethering_headers",
+ vendor_available: false,
+ host_supported: false,
+ export_include_dirs: ["."],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ ],
+ sdk_version: "30",
+ min_sdk_version: "30",
+ apex_available: ["com.android.tethering"],
+ visibility: [
+ "//packages/modules/Connectivity/Tethering",
+ ],
+}
+
+//
// bpf kernel programs
//
bpf {
@@ -27,7 +51,18 @@
include_dirs: [
// TODO: get rid of system/netd.
"system/netd/bpf_progs", // for bpf_net_helpers.h
- "system/netd/libnetdbpf/include", // for bpf_shared.h
- "system/netd/libnetdutils/include", // for UidConstants.h
+ ],
+}
+
+bpf {
+ name: "test.o",
+ srcs: ["test.c"],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ ],
+ include_dirs: [
+ // TODO: get rid of system/netd.
+ "system/netd/bpf_progs", // for bpf_net_helpers.h
],
}
diff --git a/Tethering/bpf_progs/bpf_tethering.h b/Tethering/bpf_progs/bpf_tethering.h
new file mode 100644
index 0000000..5fdf8cd
--- /dev/null
+++ b/Tethering/bpf_progs/bpf_tethering.h
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <linux/if.h>
+#include <linux/if_ether.h>
+#include <linux/in.h>
+#include <linux/in6.h>
+
+// Common definitions for BPF code in the tethering mainline module.
+// These definitions are available to:
+// - The BPF programs in Tethering/bpf_progs/
+// - JNI code that depends on the bpf_tethering_headers library.
+
+#define BPF_TETHER_ERRORS \
+ ERR(INVALID_IP_VERSION) \
+ ERR(LOW_TTL) \
+ ERR(INVALID_TCP_HEADER) \
+ ERR(TCP_CONTROL_PACKET) \
+ ERR(NON_GLOBAL_SRC) \
+ ERR(NON_GLOBAL_DST) \
+ ERR(LOCAL_SRC_DST) \
+ ERR(NO_STATS_ENTRY) \
+ ERR(NO_LIMIT_ENTRY) \
+ ERR(BELOW_IPV4_MTU) \
+ ERR(BELOW_IPV6_MTU) \
+ ERR(LIMIT_REACHED) \
+ ERR(CHANGE_HEAD_FAILED) \
+ ERR(TOO_SHORT) \
+ ERR(HAS_IP_OPTIONS) \
+ ERR(IS_IP_FRAG) \
+ ERR(CHECKSUM) \
+ ERR(NON_TCP_UDP) \
+ ERR(NON_TCP) \
+ ERR(SHORT_L4_HEADER) \
+ ERR(SHORT_TCP_HEADER) \
+ ERR(SHORT_UDP_HEADER) \
+ ERR(UDP_CSUM_ZERO) \
+ ERR(TRUNCATED_IPV4) \
+ ERR(_MAX)
+
+#define ERR(x) BPF_TETHER_ERR_ ##x,
+enum {
+ BPF_TETHER_ERRORS
+};
+#undef ERR
+
+#define ERR(x) #x,
+static const char *bpf_tether_errors[] = {
+ BPF_TETHER_ERRORS
+};
+#undef ERR
+
+// This header file is shared by eBPF kernel programs (C) and netd (C++) and
+// some of the maps are also accessed directly from Java mainline module code.
+//
+// Hence: explicitly pad all relevant structures and assert that their size
+// is the sum of the sizes of their fields.
+#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.")
+
+
+#define BPF_PATH_TETHER BPF_PATH "tethering/"
+
+#define TETHER_STATS_MAP_PATH BPF_PATH_TETHER "map_offload_tether_stats_map"
+
+typedef uint32_t TetherStatsKey; // upstream ifindex
+
+typedef struct {
+ uint64_t rxPackets;
+ uint64_t rxBytes;
+ uint64_t rxErrors;
+ uint64_t txPackets;
+ uint64_t txBytes;
+ uint64_t txErrors;
+} TetherStatsValue;
+STRUCT_SIZE(TetherStatsValue, 6 * 8); // 48
+
+#define TETHER_LIMIT_MAP_PATH BPF_PATH_TETHER "map_offload_tether_limit_map"
+
+typedef uint32_t TetherLimitKey; // upstream ifindex
+typedef uint64_t TetherLimitValue; // in bytes
+
+#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream6_rawip"
+#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream6_ether"
+
+#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME
+#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME
+
+#define TETHER_DOWNSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream6_map"
+
+// For now tethering offload only needs to support downstreams that use 6-byte MAC addresses,
+// because all downstream types that are currently supported (WiFi, USB, Bluetooth and
+// Ethernet) have 6-byte MAC addresses.
+
+typedef struct {
+ uint32_t iif; // The input interface index
+ uint8_t dstMac[ETH_ALEN]; // destination ethernet mac address (zeroed iff rawip ingress)
+ uint8_t zero[2]; // zero pad for 8 byte alignment
+ struct in6_addr neigh6; // The destination IPv6 address
+} TetherDownstream6Key;
+STRUCT_SIZE(TetherDownstream6Key, 4 + 6 + 2 + 16); // 28
+
+typedef struct {
+ uint32_t oif; // The output interface to redirect to
+ struct ethhdr macHeader; // includes dst/src mac and ethertype (zeroed iff rawip egress)
+ uint16_t pmtu; // The maximum L3 output path/route mtu
+} Tether6Value;
+STRUCT_SIZE(Tether6Value, 4 + 14 + 2); // 20
+
+#define TETHER_DOWNSTREAM64_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream64_map"
+
+typedef struct {
+ uint32_t iif; // The input interface index
+ uint8_t dstMac[ETH_ALEN]; // destination ethernet mac address (zeroed iff rawip ingress)
+ uint16_t l4Proto; // IPPROTO_TCP/UDP/...
+ struct in6_addr src6; // source &
+ struct in6_addr dst6; // destination IPv6 addresses
+ __be16 srcPort; // source &
+ __be16 dstPort; // destination tcp/udp/... ports
+} TetherDownstream64Key;
+STRUCT_SIZE(TetherDownstream64Key, 4 + 6 + 2 + 16 + 16 + 2 + 2); // 48
+
+typedef struct {
+ uint32_t oif; // The output interface to redirect to
+ struct ethhdr macHeader; // includes dst/src mac and ethertype (zeroed iff rawip egress)
+ uint16_t pmtu; // The maximum L3 output path/route mtu
+ struct in_addr src4; // source &
+ struct in_addr dst4; // destination IPv4 addresses
+ __be16 srcPort; // source &
+ __be16 outPort; // destination tcp/udp/... ports
+ uint64_t lastUsed; // Kernel updates on each use with bpf_ktime_get_boot_ns()
+} TetherDownstream64Value;
+STRUCT_SIZE(TetherDownstream64Value, 4 + 14 + 2 + 4 + 4 + 2 + 2 + 8); // 40
+
+#define TETHER_UPSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_upstream6_rawip"
+#define TETHER_UPSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_upstream6_ether"
+
+#define TETHER_UPSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM6_TC_PROG_RAWIP_NAME
+#define TETHER_UPSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM6_TC_PROG_ETHER_NAME
+
+#define TETHER_UPSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_upstream6_map"
+
+typedef struct {
+ uint32_t iif; // The input interface index
+ uint8_t dstMac[ETH_ALEN]; // destination ethernet mac address (zeroed iff rawip ingress)
+ uint8_t zero[2]; // zero pad for 8 byte alignment
+ // TODO: extend this to include src ip /64 subnet
+} TetherUpstream6Key;
+STRUCT_SIZE(TetherUpstream6Key, 12);
+
+#define TETHER_DOWNSTREAM4_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream4_rawip"
+#define TETHER_DOWNSTREAM4_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream4_ether"
+
+#define TETHER_DOWNSTREAM4_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM4_TC_PROG_RAWIP_NAME
+#define TETHER_DOWNSTREAM4_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM4_TC_PROG_ETHER_NAME
+
+#define TETHER_DOWNSTREAM4_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream4_map"
+
+
+#define TETHER_UPSTREAM4_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_upstream4_rawip"
+#define TETHER_UPSTREAM4_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_upstream4_ether"
+
+#define TETHER_UPSTREAM4_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM4_TC_PROG_RAWIP_NAME
+#define TETHER_UPSTREAM4_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM4_TC_PROG_ETHER_NAME
+
+#define TETHER_UPSTREAM4_MAP_PATH BPF_PATH_TETHER "map_offload_tether_upstream4_map"
+
+typedef struct {
+ uint32_t iif; // The input interface index
+ uint8_t dstMac[ETH_ALEN]; // destination ethernet mac address (zeroed iff rawip ingress)
+ uint16_t l4Proto; // IPPROTO_TCP/UDP/...
+ struct in_addr src4; // source &
+ struct in_addr dst4; // destination IPv4 addresses
+ __be16 srcPort; // source &
+ __be16 dstPort; // destination TCP/UDP/... ports
+} Tether4Key;
+STRUCT_SIZE(Tether4Key, 4 + 6 + 2 + 4 + 4 + 2 + 2); // 24
+
+typedef struct {
+ uint32_t oif; // The output interface to redirect to
+ struct ethhdr macHeader; // includes dst/src mac and ethertype (zeroed iff rawip egress)
+ uint16_t pmtu; // Maximum L3 output path/route mtu
+ struct in6_addr src46; // source & (always IPv4 mapped for downstream)
+ struct in6_addr dst46; // destination IP addresses (may be IPv4 mapped or IPv6 for upstream)
+ __be16 srcPort; // source &
+ __be16 dstPort; // destination tcp/udp/... ports
+ uint64_t last_used; // Kernel updates on each use with bpf_ktime_get_boot_ns()
+} Tether4Value;
+STRUCT_SIZE(Tether4Value, 4 + 14 + 2 + 16 + 16 + 2 + 2 + 8); // 64
+
+#define TETHER_DOWNSTREAM_XDP_PROG_RAWIP_NAME "prog_offload_xdp_tether_downstream_rawip"
+#define TETHER_DOWNSTREAM_XDP_PROG_ETHER_NAME "prog_offload_xdp_tether_downstream_ether"
+
+#define TETHER_DOWNSTREAM_XDP_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM_XDP_PROG_RAWIP_NAME
+#define TETHER_DOWNSTREAM_XDP_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM_XDP_PROG_ETHER_NAME
+
+#define TETHER_UPSTREAM_XDP_PROG_RAWIP_NAME "prog_offload_xdp_tether_upstream_rawip"
+#define TETHER_UPSTREAM_XDP_PROG_ETHER_NAME "prog_offload_xdp_tether_upstream_ether"
+
+#define TETHER_UPSTREAM_XDP_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_UPSTREAM_XDP_PROG_RAWIP_NAME
+#define TETHER_UPSTREAM_XDP_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_UPSTREAM_XDP_PROG_ETHER_NAME
+
+#undef STRUCT_SIZE
diff --git a/Tethering/bpf_progs/offload.c b/Tethering/bpf_progs/offload.c
index cc5af31..36f6783 100644
--- a/Tethering/bpf_progs/offload.c
+++ b/Tethering/bpf_progs/offload.c
@@ -20,30 +20,112 @@
#include <linux/pkt_cls.h>
#include <linux/tcp.h>
+// bionic kernel uapi linux/udp.h header is munged...
+#define __kernel_udphdr udphdr
+#include <linux/udp.h>
+
#include "bpf_helpers.h"
#include "bpf_net_helpers.h"
-#include "netdbpf/bpf_shared.h"
+#include "bpf_tethering.h"
-DEFINE_BPF_MAP_GRW(tether_ingress_map, HASH, TetherIngressKey, TetherIngressValue, 64,
+// From kernel:include/net/ip.h
+#define IP_DF 0x4000 // Flag: "Don't Fragment"
+
+// ----- Helper functions for offsets to fields -----
+
+// They all assume simple IP packets:
+// - no VLAN ethernet tags
+// - no IPv4 options (see IPV4_HLEN/TCP4_OFFSET/UDP4_OFFSET)
+// - no IPv6 extension headers
+// - no TCP options (see TCP_HLEN)
+
+//#define ETH_HLEN sizeof(struct ethhdr)
+#define IP4_HLEN sizeof(struct iphdr)
+#define IP6_HLEN sizeof(struct ipv6hdr)
+#define TCP_HLEN sizeof(struct tcphdr)
+#define UDP_HLEN sizeof(struct udphdr)
+
+// Offsets from beginning of L4 (TCP/UDP) header
+#define TCP_OFFSET(field) offsetof(struct tcphdr, field)
+#define UDP_OFFSET(field) offsetof(struct udphdr, field)
+
+// Offsets from beginning of L3 (IPv4) header
+#define IP4_OFFSET(field) offsetof(struct iphdr, field)
+#define IP4_TCP_OFFSET(field) (IP4_HLEN + TCP_OFFSET(field))
+#define IP4_UDP_OFFSET(field) (IP4_HLEN + UDP_OFFSET(field))
+
+// Offsets from beginning of L3 (IPv6) header
+#define IP6_OFFSET(field) offsetof(struct ipv6hdr, field)
+#define IP6_TCP_OFFSET(field) (IP6_HLEN + TCP_OFFSET(field))
+#define IP6_UDP_OFFSET(field) (IP6_HLEN + UDP_OFFSET(field))
+
+// Offsets from beginning of L2 (ie. Ethernet) header (which must be present)
+#define ETH_IP4_OFFSET(field) (ETH_HLEN + IP4_OFFSET(field))
+#define ETH_IP4_TCP_OFFSET(field) (ETH_HLEN + IP4_TCP_OFFSET(field))
+#define ETH_IP4_UDP_OFFSET(field) (ETH_HLEN + IP4_UDP_OFFSET(field))
+#define ETH_IP6_OFFSET(field) (ETH_HLEN + IP6_OFFSET(field))
+#define ETH_IP6_TCP_OFFSET(field) (ETH_HLEN + IP6_TCP_OFFSET(field))
+#define ETH_IP6_UDP_OFFSET(field) (ETH_HLEN + IP6_UDP_OFFSET(field))
+
+// ----- Tethering Error Counters -----
+
+DEFINE_BPF_MAP_GRW(tether_error_map, ARRAY, uint32_t, uint32_t, BPF_TETHER_ERR__MAX,
AID_NETWORK_STACK)
+#define COUNT_AND_RETURN(counter, ret) do { \
+ uint32_t code = BPF_TETHER_ERR_ ## counter; \
+ uint32_t *count = bpf_tether_error_map_lookup_elem(&code); \
+ if (count) __sync_fetch_and_add(count, 1); \
+ return ret; \
+} while(0)
+
+#define TC_DROP(counter) COUNT_AND_RETURN(counter, TC_ACT_SHOT)
+#define TC_PUNT(counter) COUNT_AND_RETURN(counter, TC_ACT_OK)
+
+#define XDP_DROP(counter) COUNT_AND_RETURN(counter, XDP_DROP)
+#define XDP_PUNT(counter) COUNT_AND_RETURN(counter, XDP_PASS)
+
+// ----- Tethering Data Stats and Limits -----
+
// Tethering stats, indexed by upstream interface.
-DEFINE_BPF_MAP_GRW(tether_stats_map, HASH, uint32_t, TetherStatsValue, 16, AID_NETWORK_STACK)
+DEFINE_BPF_MAP_GRW(tether_stats_map, HASH, TetherStatsKey, TetherStatsValue, 16, AID_NETWORK_STACK)
// Tethering data limit, indexed by upstream interface.
// (tethering allowed when stats[iif].rxBytes + stats[iif].txBytes < limit[iif])
-DEFINE_BPF_MAP_GRW(tether_limit_map, HASH, uint32_t, uint64_t, 16, AID_NETWORK_STACK)
+DEFINE_BPF_MAP_GRW(tether_limit_map, HASH, TetherLimitKey, TetherLimitValue, 16, AID_NETWORK_STACK)
-static inline __always_inline int do_forward(struct __sk_buff* skb, bool is_ethernet) {
- int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
+// ----- IPv6 Support -----
+
+DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 64,
+ AID_NETWORK_STACK)
+
+DEFINE_BPF_MAP_GRW(tether_downstream64_map, HASH, TetherDownstream64Key, TetherDownstream64Value,
+ 1024, AID_NETWORK_STACK)
+
+DEFINE_BPF_MAP_GRW(tether_upstream6_map, HASH, TetherUpstream6Key, Tether6Value, 64,
+ AID_NETWORK_STACK)
+
+static inline __always_inline int do_forward6(struct __sk_buff* skb, const bool is_ethernet,
+ const bool downstream) {
+ // Must be meta-ethernet IPv6 frame
+ if (skb->protocol != htons(ETH_P_IPV6)) return TC_ACT_OK;
+
+ // Require ethernet dst mac address to be our unicast address.
+ if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_OK;
+
+ const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
+
+ // Since the program never writes via DPA (direct packet access) auto-pull/unclone logic does
+ // not trigger and thus we need to manually make sure we can read packet headers via DPA.
+ // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter.
+ // It has to be done early cause it will invalidate any skb->data/data_end derived pointers.
+ try_make_readable(skb, l2_header_size + IP6_HLEN + TCP_HLEN);
+
void* data = (void*)(long)skb->data;
const void* data_end = (void*)(long)skb->data_end;
struct ethhdr* eth = is_ethernet ? data : NULL; // used iff is_ethernet
struct ipv6hdr* ip6 = is_ethernet ? (void*)(eth + 1) : data;
- // Must be meta-ethernet IPv6 frame
- if (skb->protocol != htons(ETH_P_IPV6)) return TC_ACT_OK;
-
// Must have (ethernet and) ipv6 header
if (data + l2_header_size + sizeof(*ip6) > data_end) return TC_ACT_OK;
@@ -51,43 +133,71 @@
if (is_ethernet && (eth->h_proto != htons(ETH_P_IPV6))) return TC_ACT_OK;
// IP version must be 6
- if (ip6->version != 6) return TC_ACT_OK;
+ if (ip6->version != 6) TC_PUNT(INVALID_IP_VERSION);
// Cannot decrement during forward if already zero or would be zero,
// Let the kernel's stack handle these cases and generate appropriate ICMP errors.
- if (ip6->hop_limit <= 1) return TC_ACT_OK;
+ if (ip6->hop_limit <= 1) TC_PUNT(LOW_TTL);
+
+ // If hardware offload is running and programming flows based on conntrack entries,
+ // try not to interfere with it.
+ if (ip6->nexthdr == IPPROTO_TCP) {
+ struct tcphdr* tcph = (void*)(ip6 + 1);
+
+ // Make sure we can get at the tcp header
+ if (data + l2_header_size + sizeof(*ip6) + sizeof(*tcph) > data_end)
+ TC_PUNT(INVALID_TCP_HEADER);
+
+ // Do not offload TCP packets with any one of the SYN/FIN/RST flags
+ if (tcph->syn || tcph->fin || tcph->rst) TC_PUNT(TCP_CONTROL_PACKET);
+ }
// Protect against forwarding packets sourced from ::1 or fe80::/64 or other weirdness.
__be32 src32 = ip6->saddr.s6_addr32[0];
if (src32 != htonl(0x0064ff9b) && // 64:ff9b:/32 incl. XLAT464 WKP
(src32 & htonl(0xe0000000)) != htonl(0x20000000)) // 2000::/3 Global Unicast
- return TC_ACT_OK;
+ TC_PUNT(NON_GLOBAL_SRC);
- TetherIngressKey k = {
+ // Protect against forwarding packets destined to ::1 or fe80::/64 or other weirdness.
+ __be32 dst32 = ip6->daddr.s6_addr32[0];
+ if (dst32 != htonl(0x0064ff9b) && // 64:ff9b:/32 incl. XLAT464 WKP
+ (dst32 & htonl(0xe0000000)) != htonl(0x20000000)) // 2000::/3 Global Unicast
+ TC_PUNT(NON_GLOBAL_DST);
+
+ // In the upstream direction do not forward traffic within the same /64 subnet.
+ if (!downstream && (src32 == dst32) && (ip6->saddr.s6_addr32[1] == ip6->daddr.s6_addr32[1]))
+ TC_PUNT(LOCAL_SRC_DST);
+
+ TetherDownstream6Key kd = {
.iif = skb->ifindex,
.neigh6 = ip6->daddr,
};
- TetherIngressValue* v = bpf_tether_ingress_map_lookup_elem(&k);
+ TetherUpstream6Key ku = {
+ .iif = skb->ifindex,
+ };
+ if (is_ethernet) __builtin_memcpy(downstream ? kd.dstMac : ku.dstMac, eth->h_dest, ETH_ALEN);
+
+ Tether6Value* v = downstream ? bpf_tether_downstream6_map_lookup_elem(&kd)
+ : bpf_tether_upstream6_map_lookup_elem(&ku);
// If we don't find any offload information then simply let the core stack handle it...
if (!v) return TC_ACT_OK;
- uint32_t stat_and_limit_k = skb->ifindex;
+ uint32_t stat_and_limit_k = downstream ? skb->ifindex : v->oif;
TetherStatsValue* stat_v = bpf_tether_stats_map_lookup_elem(&stat_and_limit_k);
// If we don't have anywhere to put stats, then abort...
- if (!stat_v) return TC_ACT_OK;
+ if (!stat_v) TC_PUNT(NO_STATS_ENTRY);
uint64_t* limit_v = bpf_tether_limit_map_lookup_elem(&stat_and_limit_k);
// If we don't have a limit, then abort...
- if (!limit_v) return TC_ACT_OK;
+ if (!limit_v) TC_PUNT(NO_LIMIT_ENTRY);
// Required IPv6 minimum mtu is 1280, below that not clear what we should do, abort...
- const int pmtu = v->pmtu;
- if (pmtu < IPV6_MIN_MTU) return TC_ACT_OK;
+ if (v->pmtu < IPV6_MIN_MTU) TC_PUNT(BELOW_IPV6_MTU);
// Approximate handling of TCP/IPv6 overhead for incoming LRO/GRO packets: default
// outbound path mtu of 1500 is not necessarily correct, but worst case we simply
@@ -98,9 +208,9 @@
// (This is also blindly assuming 12 bytes of tcp timestamp option in tcp header)
uint64_t packets = 1;
uint64_t bytes = skb->len;
- if (bytes > pmtu) {
+ if (bytes > v->pmtu) {
const int tcp_overhead = sizeof(struct ipv6hdr) + sizeof(struct tcphdr) + 12;
- const int mss = pmtu - tcp_overhead;
+ const int mss = v->pmtu - tcp_overhead;
const uint64_t payload = bytes - tcp_overhead;
packets = (payload + mss - 1) / mss;
bytes = tcp_overhead * packets + payload;
@@ -112,15 +222,15 @@
// a packet we let the core stack deal with things.
// (The core stack needs to handle limits correctly anyway,
// since we don't offload all traffic in both directions)
- if (stat_v->rxBytes + stat_v->txBytes + bytes > *limit_v) return TC_ACT_OK;
+ if (stat_v->rxBytes + stat_v->txBytes + bytes > *limit_v) TC_PUNT(LIMIT_REACHED);
if (!is_ethernet) {
- is_ethernet = true;
- l2_header_size = sizeof(struct ethhdr);
- // Try to inject an ethernet header, and simply return if we fail
- if (bpf_skb_change_head(skb, l2_header_size, /*flags*/ 0)) {
- __sync_fetch_and_add(&stat_v->rxErrors, 1);
- return TC_ACT_OK;
+ // Try to inject an ethernet header, and simply return if we fail.
+ // We do this even if TX interface is RAWIP and thus does not need an ethernet header,
+ // because this is easier and the kernel will strip extraneous ethernet header.
+ if (bpf_skb_change_head(skb, sizeof(struct ethhdr), /*flags*/ 0)) {
+ __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ TC_PUNT(CHANGE_HEAD_FAILED);
}
// bpf_skb_change_head() invalidates all pointers - reload them
@@ -130,12 +240,16 @@
ip6 = (void*)(eth + 1);
// I do not believe this can ever happen, but keep the verifier happy...
- if (data + l2_header_size + sizeof(*ip6) > data_end) {
- __sync_fetch_and_add(&stat_v->rxErrors, 1);
- return TC_ACT_SHOT;
+ if (data + sizeof(struct ethhdr) + sizeof(*ip6) > data_end) {
+ __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ TC_DROP(TOO_SHORT);
}
};
+ // At this point we always have an ethernet header - which will get stripped by the
+ // kernel during transmit through a rawip interface. ie. 'eth' pointer is valid.
+ // Additionally note that 'is_ethernet' and 'l2_header_size' are no longer correct.
+
// CHECKSUM_COMPLETE is a 16-bit one's complement sum,
// thus corrections for it need to be done in 16-byte chunks at even offsets.
// IPv6 nexthdr is at offset 6, while hop limit is at offset 7
@@ -147,10 +261,11 @@
// (-ENOTSUPP) if it isn't.
bpf_csum_update(skb, 0xFFFF - ntohs(old_hl) + ntohs(new_hl));
- __sync_fetch_and_add(&stat_v->rxPackets, packets);
- __sync_fetch_and_add(&stat_v->rxBytes, bytes);
+ __sync_fetch_and_add(downstream ? &stat_v->rxPackets : &stat_v->txPackets, packets);
+ __sync_fetch_and_add(downstream ? &stat_v->rxBytes : &stat_v->txBytes, bytes);
// Overwrite any mac header with the new one
+ // For a rawip tx interface it will simply be a bunch of zeroes and later stripped.
*eth = v->macHeader;
// Redirect to forwarded interface.
@@ -162,9 +277,16 @@
return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
}
-SEC("schedcls/ingress/tether_ether")
-int sched_cls_ingress_tether_ether(struct __sk_buff* skb) {
- return do_forward(skb, true);
+DEFINE_BPF_PROG("schedcls/tether_downstream6_ether", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream6_ether)
+(struct __sk_buff* skb) {
+ return do_forward6(skb, /* is_ethernet */ true, /* downstream */ true);
+}
+
+DEFINE_BPF_PROG("schedcls/tether_upstream6_ether", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream6_ether)
+(struct __sk_buff* skb) {
+ return do_forward6(skb, /* is_ethernet */ true, /* downstream */ false);
}
// Note: section names must be unique to prevent programs from appending to each other,
@@ -179,29 +301,539 @@
// 5.4 kernel support was only added to Android Common Kernel in R,
// and thus a 5.4 kernel always supports this.
//
-// Hence, this mandatory (must load successfully) implementation for 5.4+ kernels:
-DEFINE_BPF_PROG_KVER("schedcls/ingress/tether_rawip$5_4", AID_ROOT, AID_ROOT,
- sched_cls_ingress_tether_rawip_5_4, KVER(5, 4, 0))
+// Hence, these mandatory (must load successfully) implementations for 5.4+ kernels:
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream6_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream6_rawip_5_4, KVER(5, 4, 0))
(struct __sk_buff* skb) {
- return do_forward(skb, false);
+ return do_forward6(skb, /* is_ethernet */ false, /* downstream */ true);
}
-// and this identical optional (may fail to load) implementation for [4.14..5.4) patched kernels:
-DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/ingress/tether_rawip$4_14", AID_ROOT, AID_ROOT,
- sched_cls_ingress_tether_rawip_4_14, KVER(4, 14, 0),
- KVER(5, 4, 0))
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream6_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream6_rawip_5_4, KVER(5, 4, 0))
(struct __sk_buff* skb) {
- return do_forward(skb, false);
+ return do_forward6(skb, /* is_ethernet */ false, /* downstream */ false);
}
-// and define a no-op stub for [4.9,4.14) and unpatched [4.14,5.4) kernels.
+// and these identical optional (may fail to load) implementations for [4.14..5.4) patched kernels:
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$4_14",
+ AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream6_rawip_4_14,
+ KVER(4, 14, 0), KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ return do_forward6(skb, /* is_ethernet */ false, /* downstream */ true);
+}
+
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$4_14",
+ AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream6_rawip_4_14,
+ KVER(4, 14, 0), KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ return do_forward6(skb, /* is_ethernet */ false, /* downstream */ false);
+}
+
+// and define no-op stubs for [4.9,4.14) and unpatched [4.14,5.4) kernels.
// (if the above real 4.14+ program loaded successfully, then bpfloader will have already pinned
// it at the same location this one would be pinned at and will thus skip loading this stub)
-DEFINE_BPF_PROG_KVER_RANGE("schedcls/ingress/tether_rawip$stub", AID_ROOT, AID_ROOT,
- sched_cls_ingress_tether_rawip_stub, KVER_NONE, KVER(5, 4, 0))
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream6_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream6_rawip_stub, KVER_NONE, KVER(5, 4, 0))
(struct __sk_buff* skb) {
return TC_ACT_OK;
}
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream6_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream6_rawip_stub, KVER_NONE, KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ return TC_ACT_OK;
+}
+
+// ----- IPv4 Support -----
+
+DEFINE_BPF_MAP_GRW(tether_downstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
+
+DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 1024, AID_NETWORK_STACK)
+
+static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
+ const bool downstream, const bool updatetime) {
+ // Require ethernet dst mac address to be our unicast address.
+ if (is_ethernet && (skb->pkt_type != PACKET_HOST)) return TC_ACT_OK;
+
+ // Must be meta-ethernet IPv4 frame
+ if (skb->protocol != htons(ETH_P_IP)) return TC_ACT_OK;
+
+ const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
+
+ // Since the program never writes via DPA (direct packet access) auto-pull/unclone logic does
+ // not trigger and thus we need to manually make sure we can read packet headers via DPA.
+ // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter.
+ // It has to be done early cause it will invalidate any skb->data/data_end derived pointers.
+ try_make_readable(skb, l2_header_size + IP4_HLEN + TCP_HLEN);
+
+ void* data = (void*)(long)skb->data;
+ const void* data_end = (void*)(long)skb->data_end;
+ struct ethhdr* eth = is_ethernet ? data : NULL; // used iff is_ethernet
+ struct iphdr* ip = is_ethernet ? (void*)(eth + 1) : data;
+
+ // Must have (ethernet and) ipv4 header
+ if (data + l2_header_size + sizeof(*ip) > data_end) return TC_ACT_OK;
+
+ // Ethertype - if present - must be IPv4
+ if (is_ethernet && (eth->h_proto != htons(ETH_P_IP))) return TC_ACT_OK;
+
+ // IP version must be 4
+ if (ip->version != 4) TC_PUNT(INVALID_IP_VERSION);
+
+ // We cannot handle IP options, just standard 20 byte == 5 dword minimal IPv4 header
+ if (ip->ihl != 5) TC_PUNT(HAS_IP_OPTIONS);
+
+ // Calculate the IPv4 one's complement checksum of the IPv4 header.
+ __wsum sum4 = 0;
+ for (int i = 0; i < sizeof(*ip) / sizeof(__u16); ++i) {
+ sum4 += ((__u16*)ip)[i];
+ }
+ // Note that sum4 is guaranteed to be non-zero by virtue of ip4->version == 4
+ sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse u32 into range 1 .. 0x1FFFE
+ sum4 = (sum4 & 0xFFFF) + (sum4 >> 16); // collapse any potential carry into u16
+ // for a correct checksum we should get *a* zero, but sum4 must be positive, ie 0xFFFF
+ if (sum4 != 0xFFFF) TC_PUNT(CHECKSUM);
+
+ // Minimum IPv4 total length is the size of the header
+ if (ntohs(ip->tot_len) < sizeof(*ip)) TC_PUNT(TRUNCATED_IPV4);
+
+ // We are incapable of dealing with IPv4 fragments
+ if (ip->frag_off & ~htons(IP_DF)) TC_PUNT(IS_IP_FRAG);
+
+ // Cannot decrement during forward if already zero or would be zero,
+ // Let the kernel's stack handle these cases and generate appropriate ICMP errors.
+ if (ip->ttl <= 1) TC_PUNT(LOW_TTL);
+
+ // If we cannot update the 'last_used' field due to lack of bpf_ktime_get_boot_ns() helper,
+ // then it is not safe to offload UDP due to the small conntrack timeouts, as such,
+ // in such a situation we can only support TCP. This also has the added nice benefit of
+ // using a separate error counter, and thus making it obvious which version of the program
+ // is loaded.
+ if (!updatetime && ip->protocol != IPPROTO_TCP) TC_PUNT(NON_TCP);
+
+ // We do not support offloading anything besides IPv4 TCP and UDP, due to need for NAT,
+ // but no need to check this if !updatetime due to check immediately above.
+ if (updatetime && (ip->protocol != IPPROTO_TCP) && (ip->protocol != IPPROTO_UDP))
+ TC_PUNT(NON_TCP_UDP);
+
+ // We want to make sure that the compiler will, in the !updatetime case, entirely optimize
+ // out all the non-tcp logic. Also note that at this point is_udp === !is_tcp.
+ const bool is_tcp = !updatetime || (ip->protocol == IPPROTO_TCP);
+
+ // This is a bit of a hack to make things easier on the bpf verifier.
+ // (In particular I believe the Linux 4.14 kernel's verifier can get confused later on about
+ // what offsets into the packet are valid and can spuriously reject the program, this is
+ // because it fails to realize that is_tcp && !is_tcp is impossible)
+ //
+ // For both TCP & UDP we'll need to read and modify the src/dst ports, which so happen to
+ // always be in the first 4 bytes of the L4 header. Additionally for UDP we'll need access
+ // to the checksum field which is in bytes 7 and 8. While for TCP we'll need to read the
+ // TCP flags (at offset 13) and access to the checksum field (2 bytes at offset 16).
+ // As such we *always* need access to at least 8 bytes.
+ if (data + l2_header_size + sizeof(*ip) + 8 > data_end) TC_PUNT(SHORT_L4_HEADER);
+
+ struct tcphdr* tcph = is_tcp ? (void*)(ip + 1) : NULL;
+ struct udphdr* udph = is_tcp ? NULL : (void*)(ip + 1);
+
+ if (is_tcp) {
+ // Make sure we can get at the tcp header
+ if (data + l2_header_size + sizeof(*ip) + sizeof(*tcph) > data_end)
+ TC_PUNT(SHORT_TCP_HEADER);
+
+ // If hardware offload is running and programming flows based on conntrack entries, try not
+ // to interfere with it, so do not offload TCP packets with any one of the SYN/FIN/RST flags
+ if (tcph->syn || tcph->fin || tcph->rst) TC_PUNT(TCP_CONTROL_PACKET);
+ } else { // UDP
+ // Make sure we can get at the udp header
+ if (data + l2_header_size + sizeof(*ip) + sizeof(*udph) > data_end)
+ TC_PUNT(SHORT_UDP_HEADER);
+
+ // Skip handling of CHECKSUM_COMPLETE packets with udp checksum zero due to need for
+ // additional updating of skb->csum (this could be fixed up manually with more effort).
+ //
+ // Note that the in-kernel implementation of 'int64_t bpf_csum_update(skb, u32 csum)' is:
+ // if (skb->ip_summed == CHECKSUM_COMPLETE)
+ // return (skb->csum = csum_add(skb->csum, csum));
+ // else
+ // return -ENOTSUPP;
+ //
+ // So this will punt any CHECKSUM_COMPLETE packet with a zero UDP checksum,
+ // and leave all other packets unaffected (since it just at most adds zero to skb->csum).
+ //
+ // In practice this should almost never trigger because most nics do not generate
+ // CHECKSUM_COMPLETE packets on receive - especially so for nics/drivers on a phone.
+ //
+ // Additionally since we're forwarding, in most cases the value of the skb->csum field
+ // shouldn't matter (it's not used by physical nic egress).
+ //
+ // It only matters if we're ingressing through a CHECKSUM_COMPLETE capable nic
+ // and egressing through a virtual interface looping back to the kernel itself
+ // (ie. something like veth) where the CHECKSUM_COMPLETE/skb->csum can get reused
+ // on ingress.
+ //
+ // If we were in the kernel we'd simply probably call
+ // void skb_checksum_complete_unset(struct sk_buff *skb) {
+ // if (skb->ip_summed == CHECKSUM_COMPLETE) skb->ip_summed = CHECKSUM_NONE;
+ // }
+ // here instead. Perhaps there should be a bpf helper for that?
+ if (!udph->check && (bpf_csum_update(skb, 0) >= 0)) TC_PUNT(UDP_CSUM_ZERO);
+ }
+
+ Tether4Key k = {
+ .iif = skb->ifindex,
+ .l4Proto = ip->protocol,
+ .src4.s_addr = ip->saddr,
+ .dst4.s_addr = ip->daddr,
+ .srcPort = is_tcp ? tcph->source : udph->source,
+ .dstPort = is_tcp ? tcph->dest : udph->dest,
+ };
+ if (is_ethernet) __builtin_memcpy(k.dstMac, eth->h_dest, ETH_ALEN);
+
+ Tether4Value* v = downstream ? bpf_tether_downstream4_map_lookup_elem(&k)
+ : bpf_tether_upstream4_map_lookup_elem(&k);
+
+ // If we don't find any offload information then simply let the core stack handle it...
+ if (!v) return TC_ACT_OK;
+
+ uint32_t stat_and_limit_k = downstream ? skb->ifindex : v->oif;
+
+ TetherStatsValue* stat_v = bpf_tether_stats_map_lookup_elem(&stat_and_limit_k);
+
+ // If we don't have anywhere to put stats, then abort...
+ if (!stat_v) TC_PUNT(NO_STATS_ENTRY);
+
+ uint64_t* limit_v = bpf_tether_limit_map_lookup_elem(&stat_and_limit_k);
+
+ // If we don't have a limit, then abort...
+ if (!limit_v) TC_PUNT(NO_LIMIT_ENTRY);
+
+ // Required IPv4 minimum mtu is 68, below that not clear what we should do, abort...
+ if (v->pmtu < 68) TC_PUNT(BELOW_IPV4_MTU);
+
+ // Approximate handling of TCP/IPv4 overhead for incoming LRO/GRO packets: default
+ // outbound path mtu of 1500 is not necessarily correct, but worst case we simply
+ // undercount, which is still better then not accounting for this overhead at all.
+ // Note: this really shouldn't be device/path mtu at all, but rather should be
+ // derived from this particular connection's mss (ie. from gro segment size).
+ // This would require a much newer kernel with newer ebpf accessors.
+ // (This is also blindly assuming 12 bytes of tcp timestamp option in tcp header)
+ uint64_t packets = 1;
+ uint64_t bytes = skb->len;
+ if (bytes > v->pmtu) {
+ const int tcp_overhead = sizeof(struct iphdr) + sizeof(struct tcphdr) + 12;
+ const int mss = v->pmtu - tcp_overhead;
+ const uint64_t payload = bytes - tcp_overhead;
+ packets = (payload + mss - 1) / mss;
+ bytes = tcp_overhead * packets + payload;
+ }
+
+ // Are we past the limit? If so, then abort...
+ // Note: will not overflow since u64 is 936 years even at 5Gbps.
+ // Do not drop here. Offload is just that, whenever we fail to handle
+ // a packet we let the core stack deal with things.
+ // (The core stack needs to handle limits correctly anyway,
+ // since we don't offload all traffic in both directions)
+ if (stat_v->rxBytes + stat_v->txBytes + bytes > *limit_v) TC_PUNT(LIMIT_REACHED);
+
+ if (!is_ethernet) {
+ // Try to inject an ethernet header, and simply return if we fail.
+ // We do this even if TX interface is RAWIP and thus does not need an ethernet header,
+ // because this is easier and the kernel will strip extraneous ethernet header.
+ if (bpf_skb_change_head(skb, sizeof(struct ethhdr), /*flags*/ 0)) {
+ __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ TC_PUNT(CHANGE_HEAD_FAILED);
+ }
+
+ // bpf_skb_change_head() invalidates all pointers - reload them
+ data = (void*)(long)skb->data;
+ data_end = (void*)(long)skb->data_end;
+ eth = data;
+ ip = (void*)(eth + 1);
+ tcph = is_tcp ? (void*)(ip + 1) : NULL;
+ udph = is_tcp ? NULL : (void*)(ip + 1);
+
+ // I do not believe this can ever happen, but keep the verifier happy...
+ if (data + sizeof(struct ethhdr) + sizeof(*ip) + (is_tcp ? sizeof(*tcph) : sizeof(*udph)) > data_end) {
+ __sync_fetch_and_add(downstream ? &stat_v->rxErrors : &stat_v->txErrors, 1);
+ TC_DROP(TOO_SHORT);
+ }
+ };
+
+ // At this point we always have an ethernet header - which will get stripped by the
+ // kernel during transmit through a rawip interface. ie. 'eth' pointer is valid.
+ // Additionally note that 'is_ethernet' and 'l2_header_size' are no longer correct.
+
+ // Overwrite any mac header with the new one
+ // For a rawip tx interface it will simply be a bunch of zeroes and later stripped.
+ *eth = v->macHeader;
+
+ const int l4_offs_csum = is_tcp ? ETH_IP4_TCP_OFFSET(check) : ETH_IP4_UDP_OFFSET(check);
+ const int sz4 = sizeof(__be32);
+ // UDP 0 is special and stored as FFFF (this flag also causes a csum of 0 to be unmodified)
+ const int l4_flags = is_tcp ? 0 : BPF_F_MARK_MANGLED_0;
+ const __be32 old_daddr = k.dst4.s_addr;
+ const __be32 old_saddr = k.src4.s_addr;
+ const __be32 new_daddr = v->dst46.s6_addr32[3];
+ const __be32 new_saddr = v->src46.s6_addr32[3];
+
+ bpf_l4_csum_replace(skb, l4_offs_csum, old_daddr, new_daddr, sz4 | BPF_F_PSEUDO_HDR | l4_flags);
+ bpf_l3_csum_replace(skb, ETH_IP4_OFFSET(check), old_daddr, new_daddr, sz4);
+ bpf_skb_store_bytes(skb, ETH_IP4_OFFSET(daddr), &new_daddr, sz4, 0);
+
+ bpf_l4_csum_replace(skb, l4_offs_csum, old_saddr, new_saddr, sz4 | BPF_F_PSEUDO_HDR | l4_flags);
+ bpf_l3_csum_replace(skb, ETH_IP4_OFFSET(check), old_saddr, new_saddr, sz4);
+ bpf_skb_store_bytes(skb, ETH_IP4_OFFSET(saddr), &new_saddr, sz4, 0);
+
+ const int sz2 = sizeof(__be16);
+ // The offsets for TCP and UDP ports: source (u16 @ L4 offset 0) & dest (u16 @ L4 offset 2) are
+ // actually the same, so the compiler should just optimize them both down to a constant.
+ bpf_l4_csum_replace(skb, l4_offs_csum, k.srcPort, v->srcPort, sz2 | l4_flags);
+ bpf_skb_store_bytes(skb, is_tcp ? ETH_IP4_TCP_OFFSET(source) : ETH_IP4_UDP_OFFSET(source),
+ &v->srcPort, sz2, 0);
+
+ bpf_l4_csum_replace(skb, l4_offs_csum, k.dstPort, v->dstPort, sz2 | l4_flags);
+ bpf_skb_store_bytes(skb, is_tcp ? ETH_IP4_TCP_OFFSET(dest) : ETH_IP4_UDP_OFFSET(dest),
+ &v->dstPort, sz2, 0);
+
+ // TEMP HACK: lack of TTL decrement
+
+ // This requires the bpf_ktime_get_boot_ns() helper which was added in 5.8,
+ // and backported to all Android Common Kernel 4.14+ trees.
+ if (updatetime) v->last_used = bpf_ktime_get_boot_ns();
+
+ __sync_fetch_and_add(downstream ? &stat_v->rxPackets : &stat_v->txPackets, packets);
+ __sync_fetch_and_add(downstream ? &stat_v->rxBytes : &stat_v->txBytes, bytes);
+
+ // Redirect to forwarded interface.
+ //
+ // Note that bpf_redirect() cannot fail unless you pass invalid flags.
+ // The redirect actually happens after the ebpf program has already terminated,
+ // and can fail for example for mtu reasons at that point in time, but there's nothing
+ // we can do about it here.
+ return bpf_redirect(v->oif, 0 /* this is effectively BPF_F_EGRESS */);
+}
+
+// Full featured (required) implementations for 5.8+ kernels (these are S+ by definition)
+
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream4_rawip_5_8, KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ true);
+}
+
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream4_rawip_5_8, KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ true);
+}
+
+DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream4_ether_5_8, KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ true);
+}
+
+DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream4_ether_5_8, KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ true);
+}
+
+// Full featured (optional) implementations for 4.14-S, 4.19-S & 5.4-S kernels
+// (optional, because we need to be able to fallback for 4.14/4.19/5.4 pre-S kernels)
+
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt",
+ AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream4_rawip_opt,
+ KVER(4, 14, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ true);
+}
+
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt",
+ AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream4_rawip_opt,
+ KVER(4, 14, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ true);
+}
+
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt",
+ AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream4_ether_opt,
+ KVER(4, 14, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ true);
+}
+
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt",
+ AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream4_ether_opt,
+ KVER(4, 14, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ true);
+}
+
+// Partial (TCP-only: will not update 'last_used' field) implementations for 4.14+ kernels.
+// These will be loaded only if the above optional ones failed (loading of *these* must succeed
+// for 5.4+, since that is always an R patched kernel).
+//
+// [Note: as a result TCP connections will not have their conntrack timeout refreshed, however,
+// since /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established defaults to 432000 (seconds),
+// this in practice means they'll break only after 5 days. This seems an acceptable trade-off.
+//
+// Additionally kernel/tests change "net-test: add bpf_ktime_get_ns / bpf_ktime_get_boot_ns tests"
+// which enforces and documents the required kernel cherrypicks will make it pretty unlikely that
+// many devices upgrading to S will end up relying on these fallback programs.
+
+// RAWIP: Required for 5.4-R kernels -- which always support bpf_skb_change_head().
+
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ false);
+}
+
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ false);
+}
+
+// RAWIP: Optional for 4.14/4.19 (R) kernels -- which support bpf_skb_change_head().
+// [Note: fallback for 4.14/4.19 (P/Q) kernels is below in stub section]
+
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$4_14",
+ AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream4_rawip_4_14,
+ KVER(4, 14, 0), KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ false);
+}
+
+DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$4_14",
+ AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream4_rawip_4_14,
+ KVER(4, 14, 0), KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ false);
+}
+
+// ETHER: Required for 4.14-Q/R, 4.19-Q/R & 5.4-R kernels.
+
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ false);
+}
+
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0))
+(struct __sk_buff* skb) {
+ return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ false);
+}
+
+// Placeholder (no-op) implementations for older Q kernels
+
+// RAWIP: 4.9-P/Q, 4.14-P/Q & 4.19-Q kernels -- without bpf_skb_change_head() for tc programs
+
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ return TC_ACT_OK;
+}
+
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0))
+(struct __sk_buff* skb) {
+ return TC_ACT_OK;
+}
+
+// ETHER: 4.9-P/Q kernel
+
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER(4, 14, 0))
+(struct __sk_buff* skb) {
+ return TC_ACT_OK;
+}
+
+DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK,
+ sched_cls_tether_upstream4_ether_stub, KVER_NONE, KVER(4, 14, 0))
+(struct __sk_buff* skb) {
+ return TC_ACT_OK;
+}
+
+// ----- XDP Support -----
+
+DEFINE_BPF_MAP_GRW(tether_xdp_devmap, DEVMAP_HASH, uint32_t, uint32_t, 64,
+ AID_NETWORK_STACK)
+
+static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const bool is_ethernet,
+ const bool downstream) {
+ return XDP_PASS;
+}
+
+static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const bool is_ethernet,
+ const bool downstream) {
+ return XDP_PASS;
+}
+
+static inline __always_inline int do_xdp_forward_ether(struct xdp_md *ctx, const bool downstream) {
+ const void* data = (void*)(long)ctx->data;
+ const void* data_end = (void*)(long)ctx->data_end;
+ const struct ethhdr* eth = data;
+
+ // Make sure we actually have an ethernet header
+ if ((void*)(eth + 1) > data_end) return XDP_PASS;
+
+ if (eth->h_proto == htons(ETH_P_IPV6))
+ return do_xdp_forward6(ctx, /* is_ethernet */ true, downstream);
+ if (eth->h_proto == htons(ETH_P_IP))
+ return do_xdp_forward4(ctx, /* is_ethernet */ true, downstream);
+
+ // Anything else we don't know how to handle...
+ return XDP_PASS;
+}
+
+static inline __always_inline int do_xdp_forward_rawip(struct xdp_md *ctx, const bool downstream) {
+ const void* data = (void*)(long)ctx->data;
+ const void* data_end = (void*)(long)ctx->data_end;
+
+ // The top nibble of both IPv4 and IPv6 headers is the IP version.
+ if (data_end - data < 1) return XDP_PASS;
+ const uint8_t v = (*(uint8_t*)data) >> 4;
+
+ if (v == 6) return do_xdp_forward6(ctx, /* is_ethernet */ false, downstream);
+ if (v == 4) return do_xdp_forward4(ctx, /* is_ethernet */ false, downstream);
+
+ // Anything else we don't know how to handle...
+ return XDP_PASS;
+}
+
+#define DEFINE_XDP_PROG(str, func) \
+ DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER(5, 9, 0))(struct xdp_md *ctx)
+
+DEFINE_XDP_PROG("xdp/tether_downstream_ether",
+ xdp_tether_downstream_ether) {
+ return do_xdp_forward_ether(ctx, /* downstream */ true);
+}
+
+DEFINE_XDP_PROG("xdp/tether_downstream_rawip",
+ xdp_tether_downstream_rawip) {
+ return do_xdp_forward_rawip(ctx, /* downstream */ true);
+}
+
+DEFINE_XDP_PROG("xdp/tether_upstream_ether",
+ xdp_tether_upstream_ether) {
+ return do_xdp_forward_ether(ctx, /* downstream */ false);
+}
+
+DEFINE_XDP_PROG("xdp/tether_upstream_rawip",
+ xdp_tether_upstream_rawip) {
+ return do_xdp_forward_rawip(ctx, /* downstream */ false);
+}
+
LICENSE("Apache 2.0");
-CRITICAL("netd");
+CRITICAL("tethering");
diff --git a/Tethering/bpf_progs/test.c b/Tethering/bpf_progs/test.c
new file mode 100644
index 0000000..3f0df2e
--- /dev/null
+++ b/Tethering/bpf_progs/test.c
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <linux/if_ether.h>
+#include <linux/in.h>
+#include <linux/ip.h>
+
+#include "bpf_helpers.h"
+#include "bpf_net_helpers.h"
+#include "bpf_tethering.h"
+
+// Used only by TetheringPrivilegedTests, not by production code.
+DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16,
+ AID_NETWORK_STACK)
+
+DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", AID_ROOT, AID_NETWORK_STACK,
+ xdp_test, KVER(5, 9, 0))
+(struct xdp_md *ctx) {
+ void *data = (void *)(long)ctx->data;
+ void *data_end = (void *)(long)ctx->data_end;
+
+ struct ethhdr *eth = data;
+ int hsize = sizeof(*eth);
+
+ struct iphdr *ip = data + hsize;
+ hsize += sizeof(struct iphdr);
+
+ if (data + hsize > data_end) return XDP_PASS;
+ if (eth->h_proto != htons(ETH_P_IP)) return XDP_PASS;
+ if (ip->protocol == IPPROTO_UDP) return XDP_DROP;
+ return XDP_PASS;
+}
+
+LICENSE("Apache 2.0");
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index a10729d..b141eae 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -13,6 +13,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
java_sdk_library {
name: "framework-tethering",
defaults: ["framework-module-defaults"],
@@ -22,6 +26,13 @@
],
srcs: [":framework-tethering-srcs"],
+ libs: ["framework-connectivity"],
+ stub_only_libs: ["framework-connectivity"],
+ aidl: {
+ include_dirs: [
+ "frameworks/base/packages/Connectivity/framework/aidl-export",
+ ],
+ },
jarjar_rules: "jarjar-rules.txt",
installable: true,
diff --git a/Tethering/jarjar-rules.txt b/Tethering/jarjar-rules.txt
index 591861f..5de4b97 100644
--- a/Tethering/jarjar-rules.txt
+++ b/Tethering/jarjar-rules.txt
@@ -1,5 +1,5 @@
-# These must be kept in sync with the framework-tethering-shared-srcs filegroup.
-# Classes from the framework-tethering-shared-srcs filegroup.
+# These must be kept in sync with the framework-connectivity-shared-srcs filegroup.
+# Classes from the framework-connectivity-shared-srcs filegroup.
# If there are files in that filegroup that are not covered below, the classes in the
# module will be overwritten by the ones in the framework.
rule com.android.internal.util.** com.android.networkstack.tethering.util.@1
@@ -8,4 +8,7 @@
rule android.net.shared.Inet4AddressUtils* com.android.networkstack.tethering.shared.Inet4AddressUtils@1
# Classes from net-utils-framework-common
-rule com.android.net.module.util.** com.android.networkstack.tethering.util.@1
\ No newline at end of file
+rule com.android.net.module.util.** com.android.networkstack.tethering.util.@1
+
+# Classes from net-utils-device-common
+rule com.android.net.module.util.Struct* com.android.networkstack.tethering.util.Struct@1
diff --git a/Tethering/jni/android_net_util_TetheringUtils.cpp b/Tethering/jni/android_net_util_TetheringUtils.cpp
index 7bfb6da..27c84cf 100644
--- a/Tethering/jni/android_net_util_TetheringUtils.cpp
+++ b/Tethering/jni/android_net_util_TetheringUtils.cpp
@@ -28,9 +28,6 @@
#include <sys/socket.h>
#include <stdio.h>
-#define LOG_TAG "TetheringUtils"
-#include <android/log.h>
-
namespace android {
static const uint32_t kIPv6NextHeaderOffset = offsetof(ip6_hdr, ip6_nxt);
@@ -184,18 +181,4 @@
gMethods, NELEM(gMethods));
}
-extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
- JNIEnv *env;
- if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
- __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "ERROR: GetEnv failed");
- return JNI_ERR;
- }
-
- if (register_android_net_util_TetheringUtils(env) < 0) {
- return JNI_ERR;
- }
-
- return JNI_VERSION_1_6;
-}
-
}; // namespace android
diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfCoordinator.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfCoordinator.cpp
new file mode 100644
index 0000000..27357f8
--- /dev/null
+++ b/Tethering/jni/com_android_networkstack_tethering_BpfCoordinator.cpp
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+
+#include "bpf_tethering.h"
+
+namespace android {
+
+static jobjectArray getBpfCounterNames(JNIEnv *env) {
+ size_t size = BPF_TETHER_ERR__MAX;
+ jobjectArray ret = env->NewObjectArray(size, env->FindClass("java/lang/String"), nullptr);
+ for (int i = 0; i < size; i++) {
+ env->SetObjectArrayElement(ret, i, env->NewStringUTF(bpf_tether_errors[i]));
+ }
+ return ret;
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ { "getBpfCounterNames", "()[Ljava/lang/String;", (void*) getBpfCounterNames },
+};
+
+int register_com_android_networkstack_tethering_BpfCoordinator(JNIEnv* env) {
+ return jniRegisterNativeMethods(env,
+ "com/android/networkstack/tethering/BpfCoordinator",
+ gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfMap.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfMap.cpp
new file mode 100644
index 0000000..eadc210
--- /dev/null
+++ b/Tethering/jni/com_android_networkstack_tethering_BpfMap.cpp
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <errno.h>
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+
+#include "nativehelper/scoped_primitive_array.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+#define BPF_FD_JUST_USE_INT
+#include "BpfSyscallWrappers.h"
+
+namespace android {
+
+static jclass sErrnoExceptionClass;
+static jmethodID sErrnoExceptionCtor2;
+static jmethodID sErrnoExceptionCtor3;
+
+static void throwErrnoException(JNIEnv* env, const char* functionName, int error) {
+ if (sErrnoExceptionClass == nullptr || sErrnoExceptionClass == nullptr) return;
+
+ jthrowable cause = nullptr;
+ if (env->ExceptionCheck()) {
+ cause = env->ExceptionOccurred();
+ env->ExceptionClear();
+ }
+
+ ScopedLocalRef<jstring> msg(env, env->NewStringUTF(functionName));
+
+ // Not really much we can do here if msg is null, let's try to stumble on...
+ if (msg.get() == nullptr) env->ExceptionClear();
+
+ jobject errnoException;
+ if (cause != nullptr) {
+ errnoException = env->NewObject(sErrnoExceptionClass, sErrnoExceptionCtor3, msg.get(),
+ error, cause);
+ } else {
+ errnoException = env->NewObject(sErrnoExceptionClass, sErrnoExceptionCtor2, msg.get(),
+ error);
+ }
+ env->Throw(static_cast<jthrowable>(errnoException));
+}
+
+static jint com_android_networkstack_tethering_BpfMap_closeMap(JNIEnv *env, jobject clazz,
+ jint fd) {
+ int ret = close(fd);
+
+ if (ret) throwErrnoException(env, "closeMap", errno);
+
+ return ret;
+}
+
+static jint com_android_networkstack_tethering_BpfMap_bpfFdGet(JNIEnv *env, jobject clazz,
+ jstring path, jint mode) {
+ ScopedUtfChars pathname(env, path);
+
+ jint fd = bpf::bpfFdGet(pathname.c_str(), static_cast<unsigned>(mode));
+
+ return fd;
+}
+
+static void com_android_networkstack_tethering_BpfMap_writeToMapEntry(JNIEnv *env, jobject clazz,
+ jint fd, jbyteArray key, jbyteArray value, jint flags) {
+ ScopedByteArrayRO keyRO(env, key);
+ ScopedByteArrayRO valueRO(env, value);
+
+ int ret = bpf::writeToMapEntry(static_cast<int>(fd), keyRO.get(), valueRO.get(),
+ static_cast<int>(flags));
+
+ if (ret) throwErrnoException(env, "writeToMapEntry", errno);
+}
+
+static jboolean throwIfNotEnoent(JNIEnv *env, const char* functionName, int ret, int err) {
+ if (ret == 0) return true;
+
+ if (err != ENOENT) throwErrnoException(env, functionName, err);
+ return false;
+}
+
+static jboolean com_android_networkstack_tethering_BpfMap_deleteMapEntry(JNIEnv *env, jobject clazz,
+ jint fd, jbyteArray key) {
+ ScopedByteArrayRO keyRO(env, key);
+
+ // On success, zero is returned. If the element is not found, -1 is returned and errno is set
+ // to ENOENT.
+ int ret = bpf::deleteMapEntry(static_cast<int>(fd), keyRO.get());
+
+ return throwIfNotEnoent(env, "deleteMapEntry", ret, errno);
+}
+
+static jboolean com_android_networkstack_tethering_BpfMap_getNextMapKey(JNIEnv *env, jobject clazz,
+ jint fd, jbyteArray key, jbyteArray nextKey) {
+ // If key is found, the operation returns zero and sets the next key pointer to the key of the
+ // next element. If key is not found, the operation returns zero and sets the next key pointer
+ // to the key of the first element. If key is the last element, -1 is returned and errno is
+ // set to ENOENT. Other possible errno values are ENOMEM, EFAULT, EPERM, and EINVAL.
+ ScopedByteArrayRW nextKeyRW(env, nextKey);
+ int ret;
+ if (key == nullptr) {
+ // Called by getFirstKey. Find the first key in the map.
+ ret = bpf::getNextMapKey(static_cast<int>(fd), nullptr, nextKeyRW.get());
+ } else {
+ ScopedByteArrayRO keyRO(env, key);
+ ret = bpf::getNextMapKey(static_cast<int>(fd), keyRO.get(), nextKeyRW.get());
+ }
+
+ return throwIfNotEnoent(env, "getNextMapKey", ret, errno);
+}
+
+static jboolean com_android_networkstack_tethering_BpfMap_findMapEntry(JNIEnv *env, jobject clazz,
+ jint fd, jbyteArray key, jbyteArray value) {
+ ScopedByteArrayRO keyRO(env, key);
+ ScopedByteArrayRW valueRW(env, value);
+
+ // If an element is found, the operation returns zero and stores the element's value into
+ // "value". If no element is found, the operation returns -1 and sets errno to ENOENT.
+ int ret = bpf::findMapEntry(static_cast<int>(fd), keyRO.get(), valueRW.get());
+
+ return throwIfNotEnoent(env, "findMapEntry", ret, errno);
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ { "closeMap", "(I)I",
+ (void*) com_android_networkstack_tethering_BpfMap_closeMap },
+ { "bpfFdGet", "(Ljava/lang/String;I)I",
+ (void*) com_android_networkstack_tethering_BpfMap_bpfFdGet },
+ { "writeToMapEntry", "(I[B[BI)V",
+ (void*) com_android_networkstack_tethering_BpfMap_writeToMapEntry },
+ { "deleteMapEntry", "(I[B)Z",
+ (void*) com_android_networkstack_tethering_BpfMap_deleteMapEntry },
+ { "getNextMapKey", "(I[B[B)Z",
+ (void*) com_android_networkstack_tethering_BpfMap_getNextMapKey },
+ { "findMapEntry", "(I[B[B)Z",
+ (void*) com_android_networkstack_tethering_BpfMap_findMapEntry },
+
+};
+
+int register_com_android_networkstack_tethering_BpfMap(JNIEnv* env) {
+ sErrnoExceptionClass = static_cast<jclass>(env->NewGlobalRef(
+ env->FindClass("android/system/ErrnoException")));
+ if (sErrnoExceptionClass == nullptr) return JNI_ERR;
+
+ sErrnoExceptionCtor2 = env->GetMethodID(sErrnoExceptionClass, "<init>",
+ "(Ljava/lang/String;I)V");
+ if (sErrnoExceptionCtor2 == nullptr) return JNI_ERR;
+
+ sErrnoExceptionCtor3 = env->GetMethodID(sErrnoExceptionClass, "<init>",
+ "(Ljava/lang/String;ILjava/lang/Throwable;)V");
+ if (sErrnoExceptionCtor3 == nullptr) return JNI_ERR;
+
+ return jniRegisterNativeMethods(env,
+ "com/android/networkstack/tethering/BpfMap",
+ gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp
new file mode 100644
index 0000000..308dfb9
--- /dev/null
+++ b/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <arpa/inet.h>
+#include <jni.h>
+#include <linux/if_arp.h>
+#include <linux/if_ether.h>
+#include <linux/netlink.h>
+#include <linux/pkt_cls.h>
+#include <linux/pkt_sched.h>
+#include <linux/rtnetlink.h>
+#include <nativehelper/JNIHelp.h>
+#include <net/if.h>
+#include <stdio.h>
+#include <sys/socket.h>
+
+// TODO: use unique_fd.
+#define BPF_FD_JUST_USE_INT
+#include "BpfSyscallWrappers.h"
+#include "bpf_tethering.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+// The maximum length of TCA_BPF_NAME. Sync from net/sched/cls_bpf.c.
+#define CLS_BPF_NAME_LEN 256
+
+namespace android {
+// Sync from system/netd/server/NetlinkCommands.h
+const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK;
+const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0};
+
+// TODO: move to frameworks/libs/net/common/native for sharing with
+// system/netd/server/OffloadUtils.{c, h}.
+static void sendAndProcessNetlinkResponse(JNIEnv* env, const void* req, int len) {
+ int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); // TODO: use unique_fd
+ if (fd == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException",
+ "socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE): %s",
+ strerror(errno));
+ return;
+ }
+
+ static constexpr int on = 1;
+ if (setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on))) {
+ jniThrowExceptionFmt(env, "java/io/IOException",
+ "setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, %d)", on);
+ close(fd);
+ return;
+ }
+
+ // this is needed to get valid strace netlink parsing, it allocates the pid
+ if (bind(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR))) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "bind(fd, {AF_NETLINK, 0, 0}): %s",
+ strerror(errno));
+ close(fd);
+ return;
+ }
+
+ // we do not want to receive messages from anyone besides the kernel
+ if (connect(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR))) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "connect(fd, {AF_NETLINK, 0, 0}): %s",
+ strerror(errno));
+ close(fd);
+ return;
+ }
+
+ int rv = send(fd, req, len, 0);
+
+ if (rv == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "send(fd, req, len, 0): %s",
+ strerror(errno));
+ close(fd);
+ return;
+ }
+
+ if (rv != len) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "send(fd, req, len, 0): %s",
+ strerror(EMSGSIZE));
+ close(fd);
+ return;
+ }
+
+ struct {
+ nlmsghdr h;
+ nlmsgerr e;
+ char buf[256];
+ } resp = {};
+
+ rv = recv(fd, &resp, sizeof(resp), MSG_TRUNC);
+
+ if (rv == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "recv() failed: %s", strerror(errno));
+ close(fd);
+ return;
+ }
+
+ if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "recv() returned short packet: %d", rv);
+ close(fd);
+ return;
+ }
+
+ if (resp.h.nlmsg_len != (unsigned)rv) {
+ jniThrowExceptionFmt(env, "java/io/IOException",
+ "recv() returned invalid header length: %d != %d", resp.h.nlmsg_len,
+ rv);
+ close(fd);
+ return;
+ }
+
+ if (resp.h.nlmsg_type != NLMSG_ERROR) {
+ jniThrowExceptionFmt(env, "java/io/IOException",
+ "recv() did not return NLMSG_ERROR message: %d", resp.h.nlmsg_type);
+ close(fd);
+ return;
+ }
+
+ if (resp.e.error) { // returns 0 on success
+ jniThrowExceptionFmt(env, "java/io/IOException", "NLMSG_ERROR message return error: %s",
+ strerror(-resp.e.error));
+ }
+ close(fd);
+ return;
+}
+
+static int hardwareAddressType(const char* interface) {
+ int fd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+ if (fd < 0) return -errno;
+
+ struct ifreq ifr = {};
+ // We use strncpy() instead of strlcpy() since kernel has to be able
+ // to handle non-zero terminated junk passed in by userspace anyway,
+ // and this way too long interface names (more than IFNAMSIZ-1 = 15
+ // characters plus terminating NULL) will not get truncated to 15
+ // characters and zero-terminated and thus potentially erroneously
+ // match a truncated interface if one were to exist.
+ strncpy(ifr.ifr_name, interface, sizeof(ifr.ifr_name));
+
+ int rv;
+ if (ioctl(fd, SIOCGIFHWADDR, &ifr, sizeof(ifr))) {
+ rv = -errno;
+ } else {
+ rv = ifr.ifr_hwaddr.sa_family;
+ }
+
+ close(fd);
+ return rv;
+}
+
+static jboolean com_android_networkstack_tethering_BpfUtils_isEthernet(JNIEnv* env, jobject clazz,
+ jstring iface) {
+ ScopedUtfChars interface(env, iface);
+
+ int rv = hardwareAddressType(interface.c_str());
+ if (rv < 0) {
+ jniThrowExceptionFmt(env, "java/io/IOException",
+ "Get hardware address type of interface %s failed: %s",
+ interface.c_str(), strerror(-rv));
+ return false;
+ }
+
+ switch (rv) {
+ case ARPHRD_ETHER:
+ return true;
+ case ARPHRD_NONE:
+ case ARPHRD_RAWIP: // in Linux 4.14+ rmnet support was upstreamed and this is 519
+ case 530: // this is ARPHRD_RAWIP on some Android 4.9 kernels with rmnet
+ return false;
+ default:
+ jniThrowExceptionFmt(env, "java/io/IOException",
+ "Unknown hardware address type %s on interface %s", rv,
+ interface.c_str());
+ return false;
+ }
+}
+
+// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/...
+// direct-action
+static void com_android_networkstack_tethering_BpfUtils_tcFilterAddDevBpf(
+ JNIEnv* env, jobject clazz, jint ifIndex, jboolean ingress, jshort prio, jshort proto,
+ jstring bpfProgPath) {
+ ScopedUtfChars pathname(env, bpfProgPath);
+
+ const int bpfFd = bpf::retrieveProgram(pathname.c_str());
+ if (bpfFd == -1) {
+ jniThrowExceptionFmt(env, "java/io/IOException", "retrieveProgram failed %s",
+ strerror(errno));
+ return;
+ }
+
+ struct {
+ nlmsghdr n;
+ tcmsg t;
+ struct {
+ nlattr attr;
+ // The maximum classifier name length is defined as IFNAMSIZ.
+ // See tcf_proto_ops in include/net/sch_generic.h.
+ char str[NLMSG_ALIGN(IFNAMSIZ)];
+ } kind;
+ struct {
+ nlattr attr;
+ struct {
+ nlattr attr;
+ __u32 u32;
+ } fd;
+ struct {
+ nlattr attr;
+ char str[NLMSG_ALIGN(CLS_BPF_NAME_LEN)];
+ } name;
+ struct {
+ nlattr attr;
+ __u32 u32;
+ } flags;
+ } options;
+ } req = {
+ .n =
+ {
+ .nlmsg_len = sizeof(req),
+ .nlmsg_type = RTM_NEWTFILTER,
+ .nlmsg_flags = NETLINK_REQUEST_FLAGS | NLM_F_EXCL | NLM_F_CREATE,
+ },
+ .t =
+ {
+ .tcm_family = AF_UNSPEC,
+ .tcm_ifindex = ifIndex,
+ .tcm_handle = TC_H_UNSPEC,
+ .tcm_parent = TC_H_MAKE(TC_H_CLSACT,
+ ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS),
+ .tcm_info = static_cast<__u32>((static_cast<uint16_t>(prio) << 16) |
+ htons(static_cast<uint16_t>(proto))),
+ },
+ .kind =
+ {
+ .attr =
+ {
+ .nla_len = sizeof(req.kind),
+ .nla_type = TCA_KIND,
+ },
+ // Classifier name. See cls_bpf_ops in net/sched/cls_bpf.c.
+ .str = "bpf",
+ },
+ .options =
+ {
+ .attr =
+ {
+ .nla_len = sizeof(req.options),
+ .nla_type = NLA_F_NESTED | TCA_OPTIONS,
+ },
+ .fd =
+ {
+ .attr =
+ {
+ .nla_len = sizeof(req.options.fd),
+ .nla_type = TCA_BPF_FD,
+ },
+ .u32 = static_cast<__u32>(bpfFd),
+ },
+ .name =
+ {
+ .attr =
+ {
+ .nla_len = sizeof(req.options.name),
+ .nla_type = TCA_BPF_NAME,
+ },
+ // Visible via 'tc filter show', but
+ // is overwritten by strncpy below
+ .str = "placeholder",
+ },
+ .flags =
+ {
+ .attr =
+ {
+ .nla_len = sizeof(req.options.flags),
+ .nla_type = TCA_BPF_FLAGS,
+ },
+ .u32 = TCA_BPF_FLAG_ACT_DIRECT,
+ },
+ },
+ };
+
+ snprintf(req.options.name.str, sizeof(req.options.name.str), "%s:[*fsobj]",
+ basename(pathname.c_str()));
+
+ // The exception may be thrown from sendAndProcessNetlinkResponse. Close the file descriptor of
+ // BPF program before returning the function in any case.
+ sendAndProcessNetlinkResponse(env, &req, sizeof(req));
+ close(bpfFd);
+}
+
+// tc filter del dev .. in/egress prio .. protocol ..
+static void com_android_networkstack_tethering_BpfUtils_tcFilterDelDev(JNIEnv* env, jobject clazz,
+ jint ifIndex,
+ jboolean ingress,
+ jshort prio, jshort proto) {
+ const struct {
+ nlmsghdr n;
+ tcmsg t;
+ } req = {
+ .n =
+ {
+ .nlmsg_len = sizeof(req),
+ .nlmsg_type = RTM_DELTFILTER,
+ .nlmsg_flags = NETLINK_REQUEST_FLAGS,
+ },
+ .t =
+ {
+ .tcm_family = AF_UNSPEC,
+ .tcm_ifindex = ifIndex,
+ .tcm_handle = TC_H_UNSPEC,
+ .tcm_parent = TC_H_MAKE(TC_H_CLSACT,
+ ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS),
+ .tcm_info = static_cast<__u32>((static_cast<uint16_t>(prio) << 16) |
+ htons(static_cast<uint16_t>(proto))),
+ },
+ };
+
+ sendAndProcessNetlinkResponse(env, &req, sizeof(req));
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"isEthernet", "(Ljava/lang/String;)Z",
+ (void*)com_android_networkstack_tethering_BpfUtils_isEthernet},
+ {"tcFilterAddDevBpf", "(IZSSLjava/lang/String;)V",
+ (void*)com_android_networkstack_tethering_BpfUtils_tcFilterAddDevBpf},
+ {"tcFilterDelDev", "(IZSS)V",
+ (void*)com_android_networkstack_tethering_BpfUtils_tcFilterDelDev},
+};
+
+int register_com_android_networkstack_tethering_BpfUtils(JNIEnv* env) {
+ return jniRegisterNativeMethods(env, "com/android/networkstack/tethering/BpfUtils", gMethods,
+ NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/Tethering/jni/onload.cpp b/Tethering/jni/onload.cpp
new file mode 100644
index 0000000..02e602d
--- /dev/null
+++ b/Tethering/jni/onload.cpp
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <nativehelper/JNIHelp.h>
+#include "jni.h"
+
+#define LOG_TAG "TetheringJni"
+#include <android/log.h>
+
+namespace android {
+
+int register_android_net_util_TetheringUtils(JNIEnv* env);
+int register_com_android_networkstack_tethering_BpfMap(JNIEnv* env);
+int register_com_android_networkstack_tethering_BpfCoordinator(JNIEnv* env);
+int register_com_android_networkstack_tethering_BpfUtils(JNIEnv* env);
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
+ JNIEnv *env;
+ if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, "ERROR: GetEnv failed");
+ return JNI_ERR;
+ }
+
+ if (register_android_net_util_TetheringUtils(env) < 0) return JNI_ERR;
+
+ if (register_com_android_networkstack_tethering_BpfMap(env) < 0) return JNI_ERR;
+
+ if (register_com_android_networkstack_tethering_BpfCoordinator(env) < 0) return JNI_ERR;
+
+ if (register_com_android_networkstack_tethering_BpfUtils(env) < 0) return JNI_ERR;
+
+ return JNI_VERSION_1_6;
+}
+
+}; // namespace android
diff --git a/Tethering/proguard.flags b/Tethering/proguard.flags
index 86b9033..75ecdce 100644
--- a/Tethering/proguard.flags
+++ b/Tethering/proguard.flags
@@ -4,6 +4,14 @@
static final int EVENT_*;
}
+-keep class com.android.networkstack.tethering.BpfMap {
+ native <methods>;
+}
+
+-keepclassmembers public class * extends com.android.networkstack.tethering.util.Struct {
+ *;
+}
+
-keepclassmembers class android.net.ip.IpServer {
static final int CMD_*;
}
diff --git a/Tethering/res/values/config.xml b/Tethering/res/values/config.xml
index 5f8d299..4391006 100644
--- a/Tethering/res/values/config.xml
+++ b/Tethering/res/values/config.xml
@@ -40,6 +40,8 @@
<string-array translatable="false" name="config_tether_wifi_regexs">
<item>"wlan\\d"</item>
<item>"softap\\d"</item>
+ <item>"ap_br_wlan\\d"</item>
+ <item>"ap_br_softap\\d"</item>
</string-array>
<!-- List of regexpressions describing the interface (if any) that represent tetherable
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 52d59fc..da15fa8 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -67,13 +67,12 @@
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.networkstack.tethering.BpfCoordinator;
+import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
import com.android.networkstack.tethering.PrivateAddressCoordinator;
-import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
-import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -186,16 +185,6 @@
return InterfaceParams.getByName(ifName);
}
- /** Get |ifName|'s interface index. */
- public int getIfindex(String ifName) {
- try {
- return NetworkInterface.getByName(ifName).getIndex();
- } catch (IOException | NullPointerException e) {
- Log.e(TAG, "Can't determine interface index for interface " + ifName);
- return 0;
- }
- }
-
/** Create a DhcpServer instance to be used by IpServer. */
public abstract void makeDhcpServer(String ifName, DhcpServingParamsParcel params,
DhcpServerCallbacks cb);
@@ -753,16 +742,14 @@
params.dnses.add(dnsServer);
}
}
-
- // Add upstream index to name mapping for the tether stats usage in the coordinator.
- // Although this mapping could be added by both class Tethering and IpServer, adding
- // mapping from IpServer guarantees that the mapping is added before the adding
- // forwarding rules. That is because there are different state machines in both
- // classes. It is hard to guarantee the link property update order between multiple
- // state machines.
- mBpfCoordinator.addUpstreamNameToLookupTable(upstreamIfIndex, upstreamIface);
}
+ // Add upstream index to name mapping. See the comment of the interface mapping update in
+ // CMD_TETHER_CONNECTION_CHANGED. Adding the mapping update here to the avoid potential
+ // timing issue. It prevents that the IPv6 capability is updated later than
+ // CMD_TETHER_CONNECTION_CHANGED.
+ mBpfCoordinator.addUpstreamNameToLookupTable(upstreamIfIndex, upstreamIface);
+
// If v6only is null, we pass in null to setRaParams(), which handles
// deprecation of any existing RA data.
@@ -941,11 +928,38 @@
}
}
+ // TODO: consider moving into BpfCoordinator.
+ private void updateClientInfoIpv4(NeighborEvent e) {
+ // TODO: Perhaps remove this protection check.
+ // See the related comment in #addIpv6ForwardingRule.
+ if (!mUsingBpfOffload) return;
+
+ if (e == null) return;
+ if (!(e.ip instanceof Inet4Address) || e.ip.isMulticastAddress()
+ || e.ip.isLoopbackAddress() || e.ip.isLinkLocalAddress()) {
+ return;
+ }
+
+ // When deleting clients, IpServer still need to pass a non-null MAC, even though it's
+ // ignored. Do this here instead of in the ClientInfo constructor to ensure that
+ // IpServer never add clients with a null MAC, only delete them.
+ final MacAddress clientMac = e.isValid() ? e.macAddr : NULL_MAC_ADDRESS;
+ final ClientInfo clientInfo = new ClientInfo(mInterfaceParams.index,
+ mInterfaceParams.macAddr, (Inet4Address) e.ip, clientMac);
+ if (e.isValid()) {
+ mBpfCoordinator.tetherOffloadClientAdd(this, clientInfo);
+ } else {
+ // TODO: Delete all related offload rules which are using this client.
+ mBpfCoordinator.tetherOffloadClientRemove(this, clientInfo);
+ }
+ }
+
private void handleNeighborEvent(NeighborEvent e) {
if (mInterfaceParams != null
&& mInterfaceParams.index == e.ifindex
&& mInterfaceParams.hasMacAddress) {
updateIpv6ForwardingRules(mLastIPv6UpstreamIfindex, mLastIPv6UpstreamIfindex, e);
+ updateClientInfoIpv4(e);
}
}
@@ -1111,9 +1125,19 @@
}
}
+ private void startConntrackMonitoring() {
+ mBpfCoordinator.startMonitoring(this);
+ }
+
+ private void stopConntrackMonitoring() {
+ mBpfCoordinator.stopMonitoring(this);
+ }
+
class BaseServingState extends State {
@Override
public void enter() {
+ startConntrackMonitoring();
+
if (!startIPv4()) {
mLastError = TetheringManager.TETHER_ERROR_IFACE_CFG_ERROR;
return;
@@ -1149,6 +1173,7 @@
}
stopIPv4();
+ stopConntrackMonitoring();
resetLinkProperties();
}
@@ -1264,6 +1289,7 @@
// Sometimes interfaces are gone before we get
// to remove their rules, which generates errors.
// Just do the best we can.
+ mBpfCoordinator.maybeDetachProgram(mIfaceName, upstreamIface);
try {
mNetd.ipfwdRemoveInterfaceForward(mIfaceName, upstreamIface);
} catch (RemoteException | ServiceSpecificException e) {
@@ -1307,6 +1333,27 @@
mUpstreamIfaceSet = newUpstreamIfaceSet;
for (String ifname : added) {
+ // Add upstream index to name mapping for the tether stats usage in the
+ // coordinator. Although this mapping could be added by both class
+ // Tethering and IpServer, adding mapping from IpServer guarantees that
+ // the mapping is added before adding forwarding rules. That is because
+ // there are different state machines in both classes. It is hard to
+ // guarantee the link property update order between multiple state machines.
+ // Note that both IPv4 and IPv6 interface may be added because
+ // Tethering::setUpstreamNetwork calls getTetheringInterfaces which merges
+ // IPv4 and IPv6 interface name (if any) into an InterfaceSet. The IPv6
+ // capability may be updated later. In that case, IPv6 interface mapping is
+ // updated in updateUpstreamIPv6LinkProperties.
+ if (!ifname.startsWith("v4-")) { // ignore clat interfaces
+ final InterfaceParams upstreamIfaceParams =
+ mDeps.getInterfaceParams(ifname);
+ if (upstreamIfaceParams != null) {
+ mBpfCoordinator.addUpstreamNameToLookupTable(
+ upstreamIfaceParams.index, ifname);
+ }
+ }
+
+ mBpfCoordinator.maybeAttachProgram(mIfaceName, ifname);
try {
mNetd.tetherAddForward(mIfaceName, ifname);
mNetd.ipfwdAddInterfaceForward(mIfaceName, ifname);
diff --git a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
index 7c0b7cc..543a5c7 100644
--- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -16,7 +16,6 @@
package android.net.ip;
-import static android.net.util.NetworkConstants.IPV6_MIN_MTU;
import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
import static android.net.util.TetheringUtils.getAllNodesForScopeId;
import static android.system.OsConstants.AF_INET6;
@@ -25,8 +24,18 @@
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_SNDTIMEO;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_RA_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU;
+import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS;
+import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK;
+import static com.android.net.module.util.NetworkStackConstants.TAG_SYSTEM_NEIGHBOR;
+
import android.net.IpPrefix;
import android.net.LinkAddress;
+import android.net.MacAddress;
import android.net.TrafficStats;
import android.net.util.InterfaceParams;
import android.net.util.SocketUtils;
@@ -37,7 +46,12 @@
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.TrafficStatsConstants;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.MtuOption;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RaHeader;
+import com.android.net.module.util.structs.RdnssOption;
import java.io.FileDescriptor;
import java.io.IOException;
@@ -69,9 +83,6 @@
*/
public class RouterAdvertisementDaemon {
private static final String TAG = RouterAdvertisementDaemon.class.getSimpleName();
- private static final byte ICMPV6_ND_ROUTER_SOLICIT = asByte(133);
- private static final byte ICMPV6_ND_ROUTER_ADVERT = asByte(134);
- private static final int MIN_RA_HEADER_SIZE = 16;
// Summary of various timers and lifetimes.
private static final int MIN_RTR_ADV_INTERVAL_SEC = 300;
@@ -366,54 +377,27 @@
}
private static void putHeader(ByteBuffer ra, boolean hasDefaultRoute, byte hopLimit) {
- /**
- Router Advertisement Message Format
-
- 0 1 2 3
- 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | Type | Code | Checksum |
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | Cur Hop Limit |M|O|H|Prf|P|R|R| Router Lifetime |
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | Reachable Time |
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | Retrans Timer |
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | Options ...
- +-+-+-+-+-+-+-+-+-+-+-+-
- */
- ra.put(ICMPV6_ND_ROUTER_ADVERT)
- .put(asByte(0))
- .putShort(asShort(0))
- .put(hopLimit)
- // RFC 4191 "high" preference, iff. advertising a default route.
- .put(hasDefaultRoute ? asByte(0x08) : asByte(0))
- .putShort(hasDefaultRoute ? asShort(DEFAULT_LIFETIME) : asShort(0))
- .putInt(0)
- .putInt(0);
+ // RFC 4191 "high" preference, iff. advertising a default route.
+ final byte flags = hasDefaultRoute ? asByte(0x08) : asByte(0);
+ final short lifetime = hasDefaultRoute ? asShort(DEFAULT_LIFETIME) : asShort(0);
+ final Icmpv6Header icmpv6Header =
+ new Icmpv6Header(asByte(ICMPV6_ROUTER_ADVERTISEMENT) /* type */,
+ asByte(0) /* code */, asShort(0) /* checksum */);
+ final RaHeader raHeader = new RaHeader(hopLimit, flags, lifetime, 0 /* reachableTime */,
+ 0 /* retransTimer */);
+ icmpv6Header.writeToByteBuffer(ra);
+ raHeader.writeToByteBuffer(ra);
}
private static void putSlla(ByteBuffer ra, byte[] slla) {
- /**
- Source/Target Link-layer Address
-
- 0 1 2 3
- 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | Type | Length | Link-Layer Address ...
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- */
if (slla == null || slla.length != 6) {
// Only IEEE 802.3 6-byte addresses are supported.
return;
}
- final byte nd_option_slla = 1;
- final byte slla_num_8octets = 1;
- ra.put(nd_option_slla)
- .put(slla_num_8octets)
- .put(slla);
+ final ByteBuffer sllaOption = LlaOption.build(asByte(ICMPV6_ND_OPTION_SLLA),
+ MacAddress.fromBytes(slla));
+ ra.put(sllaOption);
}
private static void putExpandedFlagsOption(ByteBuffer ra) {
@@ -439,70 +423,24 @@
}
private static void putMtu(ByteBuffer ra, int mtu) {
- /**
- MTU
-
- 0 1 2 3
- 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | Type | Length | Reserved |
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | MTU |
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- */
- final byte nd_option_mtu = 5;
- final byte mtu_num_8octs = 1;
- ra.put(nd_option_mtu)
- .put(mtu_num_8octs)
- .putShort(asShort(0))
- .putInt((mtu < IPV6_MIN_MTU) ? IPV6_MIN_MTU : mtu);
+ final ByteBuffer mtuOption = MtuOption.build((mtu < IPV6_MIN_MTU) ? IPV6_MIN_MTU : mtu);
+ ra.put(mtuOption);
}
private static void putPio(ByteBuffer ra, IpPrefix ipp,
int validTime, int preferredTime) {
- /**
- Prefix Information
-
- 0 1 2 3
- 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | Type | Length | Prefix Length |L|A| Reserved1 |
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | Valid Lifetime |
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | Preferred Lifetime |
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | Reserved2 |
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | |
- + +
- | |
- + Prefix +
- | |
- + +
- | |
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- */
final int prefixLength = ipp.getPrefixLength();
if (prefixLength != 64) {
return;
}
- final byte nd_option_pio = 3;
- final byte pio_num_8octets = 4;
if (validTime < 0) validTime = 0;
if (preferredTime < 0) preferredTime = 0;
if (preferredTime > validTime) preferredTime = validTime;
- final byte[] addr = ipp.getAddress().getAddress();
- ra.put(nd_option_pio)
- .put(pio_num_8octets)
- .put(asByte(prefixLength))
- .put(asByte(0xc0)) /* L & A set */
- .putInt(validTime)
- .putInt(preferredTime)
- .putInt(0)
- .put(addr);
+ final ByteBuffer pioOption = PrefixInformationOption.build(ipp,
+ asByte(PIO_FLAG_ON_LINK | PIO_FLAG_AUTONOMOUS), validTime, preferredTime);
+ ra.put(pioOption);
}
private static void putRio(ByteBuffer ra, IpPrefix ipp) {
@@ -543,22 +481,6 @@
}
private static void putRdnss(ByteBuffer ra, Set<Inet6Address> dnses, int lifetime) {
- /**
- Recursive DNS Server (RDNSS) Option
-
- 0 1 2 3
- 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | Type | Length | Reserved |
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | Lifetime |
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | |
- : Addresses of IPv6 Recursive DNS Servers :
- | |
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- */
-
final HashSet<Inet6Address> filteredDnses = new HashSet<>();
for (Inet6Address dns : dnses) {
if ((new LinkAddress(dns, RFC7421_PREFIX_LENGTH)).isGlobalPreferred()) {
@@ -567,29 +489,22 @@
}
if (filteredDnses.isEmpty()) return;
- final byte nd_option_rdnss = 25;
- final byte rdnss_num_8octets = asByte(dnses.size() * 2 + 1);
- ra.put(nd_option_rdnss)
- .put(rdnss_num_8octets)
- .putShort(asShort(0))
- .putInt(lifetime);
-
- for (Inet6Address dns : filteredDnses) {
- // NOTE: If the full of list DNS servers doesn't fit in the packet,
- // this code will cause a buffer overflow and the RA won't include
- // this instance of the option at all.
- //
- // TODO: Consider looking at ra.remaining() to determine how many
- // DNS servers will fit, and adding only those.
- ra.put(dns.getAddress());
- }
+ final Inet6Address[] dnsesArray =
+ filteredDnses.toArray(new Inet6Address[filteredDnses.size()]);
+ final ByteBuffer rdnssOption = RdnssOption.build(lifetime, dnsesArray);
+ // NOTE: If the full of list DNS servers doesn't fit in the packet,
+ // this code will cause a buffer overflow and the RA won't include
+ // this instance of the option at all.
+ //
+ // TODO: Consider looking at ra.remaining() to determine how many
+ // DNS servers will fit, and adding only those.
+ ra.put(rdnssOption);
}
private boolean createSocket() {
final int send_timout_ms = 300;
- final int oldTag = TrafficStats.getAndSetThreadStatsTag(
- TrafficStatsConstants.TAG_SYSTEM_NEIGHBOR);
+ final int oldTag = TrafficStats.getAndSetThreadStatsTag(TAG_SYSTEM_NEIGHBOR);
try {
mSocket = Os.socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
// Setting SNDTIMEO is purely for defensive purposes.
@@ -639,7 +554,7 @@
try {
synchronized (mLock) {
- if (mRaLength < MIN_RA_HEADER_SIZE) {
+ if (mRaLength < ICMPV6_RA_HEADER_LEN) {
// No actual RA to send.
return;
}
@@ -668,7 +583,7 @@
final int rval = Os.recvfrom(
mSocket, mSolicitation, 0, mSolicitation.length, 0, mSolicitor);
// Do the least possible amount of validation.
- if (rval < 1 || mSolicitation[0] != ICMPV6_ND_ROUTER_SOLICIT) {
+ if (rval < 1 || mSolicitation[0] != asByte(ICMPV6_ROUTER_SOLICITATION)) {
continue;
}
} catch (ErrnoException | SocketException e) {
@@ -721,7 +636,7 @@
private int getNextMulticastTransmitDelaySec() {
boolean deprecationInProgress = false;
synchronized (mLock) {
- if (mRaLength < MIN_RA_HEADER_SIZE) {
+ if (mRaLength < ICMPV6_RA_HEADER_LEN) {
// No actual RA to send; just sleep for 1 day.
return DAY_IN_SECONDS;
}
diff --git a/Tethering/src/android/net/util/BaseNetdUnsolicitedEventListener.java b/Tethering/src/android/net/util/BaseNetdUnsolicitedEventListener.java
deleted file mode 100644
index b1ffdb0..0000000
--- a/Tethering/src/android/net/util/BaseNetdUnsolicitedEventListener.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package android.net.util;
-
-import android.net.INetdUnsolicitedEventListener;
-
-import androidx.annotation.NonNull;
-
-/**
- * Base {@link INetdUnsolicitedEventListener} that provides no-op implementations which can be
- * overridden.
- */
-public class BaseNetdUnsolicitedEventListener extends INetdUnsolicitedEventListener.Stub {
-
- @Override
- public void onInterfaceClassActivityChanged(boolean isActive, int timerLabel, long timestampNs,
- int uid) { }
-
- @Override
- public void onQuotaLimitReached(@NonNull String alertName, @NonNull String ifName) { }
-
- @Override
- public void onInterfaceDnsServerInfo(@NonNull String ifName, long lifetimeS,
- @NonNull String[] servers) { }
-
- @Override
- public void onInterfaceAddressUpdated(@NonNull String addr, String ifName, int flags,
- int scope) { }
-
- @Override
- public void onInterfaceAddressRemoved(@NonNull String addr, @NonNull String ifName, int flags,
- int scope) { }
-
- @Override
- public void onInterfaceAdded(@NonNull String ifName) { }
-
- @Override
- public void onInterfaceRemoved(@NonNull String ifName) { }
-
- @Override
- public void onInterfaceChanged(@NonNull String ifName, boolean up) { }
-
- @Override
- public void onInterfaceLinkStateChanged(@NonNull String ifName, boolean up) { }
-
- @Override
- public void onRouteChanged(boolean updated, @NonNull String route, @NonNull String gateway,
- @NonNull String ifName) { }
-
- @Override
- public void onStrictCleartextDetected(int uid, @NonNull String hex) { }
-
- @Override
- public int getInterfaceVersion() {
- return INetdUnsolicitedEventListener.VERSION;
- }
-
- @Override
- public String getInterfaceHash() {
- return INetdUnsolicitedEventListener.HASH;
- }
-}
diff --git a/Tethering/src/android/net/util/TetheringUtils.java b/Tethering/src/android/net/util/TetheringUtils.java
index 53b54f7..9e7cc2f 100644
--- a/Tethering/src/android/net/util/TetheringUtils.java
+++ b/Tethering/src/android/net/util/TetheringUtils.java
@@ -21,6 +21,8 @@
import androidx.annotation.NonNull;
+import com.android.networkstack.tethering.TetherStatsValue;
+
import java.io.FileDescriptor;
import java.net.Inet6Address;
import java.net.SocketException;
@@ -34,6 +36,10 @@
* {@hide}
*/
public class TetheringUtils {
+ static {
+ System.loadLibrary("tetherutilsjni");
+ }
+
public static final byte[] ALL_NODES = new byte[] {
(byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
};
@@ -91,6 +97,13 @@
txPackets = tetherStats.txPackets;
}
+ public ForwardedStats(@NonNull TetherStatsValue tetherStats) {
+ rxBytes = tetherStats.rxBytes;
+ rxPackets = tetherStats.rxPackets;
+ txBytes = tetherStats.txBytes;
+ txPackets = tetherStats.txPackets;
+ }
+
public ForwardedStats(@NonNull ForwardedStats other) {
rxBytes = other.rxBytes;
rxPackets = other.rxPackets;
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 20f30ea..add4f37 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -23,25 +23,33 @@
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
import static android.net.NetworkStats.UID_TETHERING;
+import static android.net.ip.ConntrackMonitor.ConntrackEvent;
import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
+import static android.system.OsConstants.ETH_P_IP;
+import static android.system.OsConstants.ETH_P_IPV6;
+import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
+import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
import android.app.usage.NetworkStatsManager;
import android.net.INetd;
+import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.NetworkStats;
import android.net.NetworkStats.Entry;
import android.net.TetherOffloadRuleParcel;
-import android.net.TetherStatsParcel;
+import android.net.ip.ConntrackMonitor;
+import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
import android.net.ip.IpServer;
+import android.net.netlink.NetlinkConstants;
import android.net.netstats.provider.NetworkStatsProvider;
+import android.net.util.InterfaceParams;
import android.net.util.SharedLog;
import android.net.util.TetheringUtils.ForwardedStats;
import android.os.ConditionVariable;
import android.os.Handler;
-import android.os.RemoteException;
-import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
@@ -51,13 +59,23 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.Struct;
+import com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim;
+import java.net.Inet4Address;
import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
/**
* This coordinator is responsible for providing BPF offload relevant functionality.
@@ -69,8 +87,35 @@
* @hide
*/
public class BpfCoordinator {
+ // Ensure the JNI code is loaded. In production this will already have been loaded by
+ // TetherService, but for tests it needs to be either loaded here or loaded by every test.
+ // TODO: is there a better way?
+ static {
+ System.loadLibrary("tetherutilsjni");
+ }
+
private static final String TAG = BpfCoordinator.class.getSimpleName();
private static final int DUMP_TIMEOUT_MS = 10_000;
+ private static final MacAddress NULL_MAC_ADDRESS = MacAddress.fromString(
+ "00:00:00:00:00:00");
+ private static final String TETHER_DOWNSTREAM4_MAP_PATH = makeMapPath(DOWNSTREAM, 4);
+ private static final String TETHER_UPSTREAM4_MAP_PATH = makeMapPath(UPSTREAM, 4);
+ private static final String TETHER_DOWNSTREAM6_FS_PATH = makeMapPath(DOWNSTREAM, 6);
+ private static final String TETHER_UPSTREAM6_FS_PATH = makeMapPath(UPSTREAM, 6);
+ private static final String TETHER_STATS_MAP_PATH = makeMapPath("stats");
+ private static final String TETHER_LIMIT_MAP_PATH = makeMapPath("limit");
+ private static final String TETHER_ERROR_MAP_PATH = makeMapPath("error");
+
+ /** The names of all the BPF counters defined in bpf_tethering.h. */
+ public static final String[] sBpfCounterNames = getBpfCounterNames();
+
+ private static String makeMapPath(String which) {
+ return "/sys/fs/bpf/tethering/map_offload_tether_" + which + "_map";
+ }
+
+ private static String makeMapPath(boolean downstream, int ipVersion) {
+ return makeMapPath((downstream ? "downstream" : "upstream") + ipVersion);
+ }
@VisibleForTesting
enum StatsType {
@@ -86,8 +131,14 @@
private final SharedLog mLog;
@NonNull
private final Dependencies mDeps;
+ @NonNull
+ private final ConntrackMonitor mConntrackMonitor;
@Nullable
private final BpfTetherStatsProvider mStatsProvider;
+ @NonNull
+ private final BpfCoordinatorShim mBpfCoordinatorShim;
+ @NonNull
+ private final BpfConntrackEventConsumer mBpfConntrackEventConsumer;
// True if BPF offload is supported, false otherwise. The BPF offload could be disabled by
// a runtime resource overlay package or device configuration. This flag is only initialized
@@ -146,12 +197,37 @@
private final HashMap<IpServer, LinkedHashMap<Inet6Address, Ipv6ForwardingRule>>
mIpv6ForwardingRules = new LinkedHashMap<>();
+ // Map of downstream client maps. Each of these maps represents the IPv4 clients for a given
+ // downstream. Needed to build IPv4 forwarding rules when conntrack events are received.
+ // Each map:
+ // - Is owned by the IpServer that is responsible for that downstream.
+ // - Must only be modified by that IpServer.
+ // - Is created when the IpServer adds its first client, and deleted when the IpServer deletes
+ // its last client.
+ // Note that relying on the client address for finding downstream is okay for now because the
+ // client address is unique. See PrivateAddressCoordinator#requestDownstreamAddress.
+ // TODO: Refactor if any possible that the client address is not unique.
+ private final HashMap<IpServer, HashMap<Inet4Address, ClientInfo>>
+ mTetherClients = new HashMap<>();
+
+ // Set for which downstream is monitoring the conntrack netlink message.
+ private final Set<IpServer> mMonitoringIpServers = new HashSet<>();
+
+ // Map of upstream interface IPv4 address to interface index.
+ // TODO: consider making the key to be unique because the upstream address is not unique. It
+ // is okay for now because there have only one upstream generally.
+ private final HashMap<Inet4Address, Integer> mIpv4UpstreamIndices = new HashMap<>();
+
+ // Map for upstream and downstream pair.
+ private final HashMap<String, HashSet<String>> mForwardingPairs = new HashMap<>();
+
// Runnable that used by scheduling next polling of stats.
private final Runnable mScheduledPollingTask = () -> {
- updateForwardedStatsFromNetd();
+ updateForwardedStats();
maybeSchedulePollingStats();
};
+ // TODO: add BpfMap<TetherDownstream64Key, TetherDownstream64Value> retrieving function.
@VisibleForTesting
public abstract static class Dependencies {
/** Get handler. */
@@ -168,6 +244,99 @@
/** Get tethering configuration. */
@Nullable public abstract TetheringConfiguration getTetherConfig();
+
+ /** Get conntrack monitor. */
+ @NonNull public ConntrackMonitor getConntrackMonitor(ConntrackEventConsumer consumer) {
+ return new ConntrackMonitor(getHandler(), getSharedLog(), consumer);
+ }
+
+ /** Get interface information for a given interface. */
+ @NonNull public InterfaceParams getInterfaceParams(String ifName) {
+ return InterfaceParams.getByName(ifName);
+ }
+
+ /**
+ * Check OS Build at least S.
+ *
+ * TODO: move to BpfCoordinatorShim once the test doesn't need the mocked OS build for
+ * testing different code flows concurrently.
+ */
+ public boolean isAtLeastS() {
+ // TODO: consider using ShimUtils.isAtLeastS.
+ return SdkLevel.isAtLeastS();
+ }
+
+ /** Get downstream4 BPF map. */
+ @Nullable public BpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() {
+ if (!isAtLeastS()) return null;
+ try {
+ return new BpfMap<>(TETHER_DOWNSTREAM4_MAP_PATH,
+ BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot create downstream4 map: " + e);
+ return null;
+ }
+ }
+
+ /** Get upstream4 BPF map. */
+ @Nullable public BpfMap<Tether4Key, Tether4Value> getBpfUpstream4Map() {
+ if (!isAtLeastS()) return null;
+ try {
+ return new BpfMap<>(TETHER_UPSTREAM4_MAP_PATH,
+ BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot create upstream4 map: " + e);
+ return null;
+ }
+ }
+
+ /** Get downstream6 BPF map. */
+ @Nullable public BpfMap<TetherDownstream6Key, Tether6Value> getBpfDownstream6Map() {
+ if (!isAtLeastS()) return null;
+ try {
+ return new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH,
+ BpfMap.BPF_F_RDWR, TetherDownstream6Key.class, Tether6Value.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot create downstream6 map: " + e);
+ return null;
+ }
+ }
+
+ /** Get upstream6 BPF map. */
+ @Nullable public BpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() {
+ if (!isAtLeastS()) return null;
+ try {
+ return new BpfMap<>(TETHER_UPSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
+ TetherUpstream6Key.class, Tether6Value.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot create upstream6 map: " + e);
+ return null;
+ }
+ }
+
+ /** Get stats BPF map. */
+ @Nullable public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
+ if (!isAtLeastS()) return null;
+ try {
+ return new BpfMap<>(TETHER_STATS_MAP_PATH,
+ BpfMap.BPF_F_RDWR, TetherStatsKey.class, TetherStatsValue.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot create stats map: " + e);
+ return null;
+ }
+ }
+
+ /** Get limit BPF map. */
+ @Nullable public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
+ if (!isAtLeastS()) return null;
+ try {
+ return new BpfMap<>(TETHER_LIMIT_MAP_PATH,
+ BpfMap.BPF_F_RDWR, TetherLimitKey.class, TetherLimitValue.class);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot create limit map: " + e);
+ return null;
+ }
+ }
}
@VisibleForTesting
@@ -177,6 +346,14 @@
mNetd = mDeps.getNetd();
mLog = mDeps.getSharedLog().forSubComponent(TAG);
mIsBpfEnabled = isBpfEnabled();
+
+ // The conntrack consummer needs to be initialized in BpfCoordinator constructor because it
+ // have to access the data members of BpfCoordinator which is not a static class. The
+ // consumer object is also needed for initializing the conntrack monitor which may be
+ // mocked for testing.
+ mBpfConntrackEventConsumer = new BpfConntrackEventConsumer();
+ mConntrackMonitor = mDeps.getConntrackMonitor(mBpfConntrackEventConsumer);
+
BpfTetherStatsProvider provider = new BpfTetherStatsProvider();
try {
mDeps.getNetworkStatsManager().registerNetworkStatsProvider(
@@ -188,6 +365,11 @@
provider = null;
}
mStatsProvider = provider;
+
+ mBpfCoordinatorShim = BpfCoordinatorShim.getBpfCoordinatorShim(deps);
+ if (!mBpfCoordinatorShim.isInitialized()) {
+ mLog.e("Bpf shim not initialized");
+ }
}
/**
@@ -199,8 +381,8 @@
public void startPolling() {
if (mPollingStarted) return;
- if (!mIsBpfEnabled) {
- mLog.i("Offload disabled");
+ if (!isUsingBpf()) {
+ mLog.i("BPF is not using");
return;
}
@@ -225,12 +407,72 @@
if (mHandler.hasCallbacks(mScheduledPollingTask)) {
mHandler.removeCallbacks(mScheduledPollingTask);
}
- updateForwardedStatsFromNetd();
+ updateForwardedStats();
mPollingStarted = false;
mLog.i("Polling stopped");
}
+ private boolean isUsingBpf() {
+ return mIsBpfEnabled && mBpfCoordinatorShim.isInitialized();
+ }
+
+ /**
+ * Start conntrack message monitoring.
+ * Note that this can be only called on handler thread.
+ *
+ * TODO: figure out a better logging for non-interesting conntrack message.
+ * For example, the following logging is an IPCTNL_MSG_CT_GET message but looks scary.
+ * +---------------------------------------------------------------------------+
+ * | ERROR unparsable netlink msg: 1400000001010103000000000000000002000000 |
+ * +------------------+--------------------------------------------------------+
+ * | | struct nlmsghdr |
+ * | 14000000 | length = 20 |
+ * | 0101 | type = NFNL_SUBSYS_CTNETLINK << 8 | IPCTNL_MSG_CT_GET |
+ * | 0103 | flags |
+ * | 00000000 | seqno = 0 |
+ * | 00000000 | pid = 0 |
+ * | | struct nfgenmsg |
+ * | 02 | nfgen_family = AF_INET |
+ * | 00 | version = NFNETLINK_V0 |
+ * | 0000 | res_id |
+ * +------------------+--------------------------------------------------------+
+ * See NetlinkMonitor#handlePacket, NetlinkMessage#parseNfMessage.
+ */
+ public void startMonitoring(@NonNull final IpServer ipServer) {
+ // TODO: Wrap conntrackMonitor starting function into mBpfCoordinatorShim.
+ if (!isUsingBpf() || !mDeps.isAtLeastS()) return;
+
+ if (mMonitoringIpServers.contains(ipServer)) {
+ Log.wtf(TAG, "The same downstream " + ipServer.interfaceName()
+ + " should not start monitoring twice.");
+ return;
+ }
+
+ if (mMonitoringIpServers.isEmpty()) {
+ mConntrackMonitor.start();
+ mLog.i("Monitoring started");
+ }
+
+ mMonitoringIpServers.add(ipServer);
+ }
+
+ /**
+ * Stop conntrack event monitoring.
+ * Note that this can be only called on handler thread.
+ */
+ public void stopMonitoring(@NonNull final IpServer ipServer) {
+ // TODO: Wrap conntrackMonitor stopping function into mBpfCoordinatorShim.
+ if (!isUsingBpf() || !mDeps.isAtLeastS()) return;
+
+ mMonitoringIpServers.remove(ipServer);
+
+ if (!mMonitoringIpServers.isEmpty()) return;
+
+ mConntrackMonitor.stop();
+ mLog.i("Monitoring stopped");
+ }
+
/**
* Add forwarding rule. After adding the first rule on a given upstream, must add the data
* limit on the given upstream.
@@ -238,15 +480,10 @@
*/
public void tetherOffloadRuleAdd(
@NonNull final IpServer ipServer, @NonNull final Ipv6ForwardingRule rule) {
- if (!mIsBpfEnabled) return;
+ if (!isUsingBpf()) return;
- try {
- // TODO: Perhaps avoid to add a duplicate rule.
- mNetd.tetherOffloadRuleAdd(rule.toTetherOffloadRuleParcel());
- } catch (RemoteException | ServiceSpecificException e) {
- mLog.e("Could not add IPv6 forwarding rule: ", e);
- return;
- }
+ // TODO: Perhaps avoid to add a duplicate rule.
+ if (!mBpfCoordinatorShim.tetherOffloadRuleAdd(rule)) return;
if (!mIpv6ForwardingRules.containsKey(ipServer)) {
mIpv6ForwardingRules.put(ipServer, new LinkedHashMap<Inet6Address,
@@ -254,16 +491,18 @@
}
LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(ipServer);
- // Setup the data limit on the given upstream if the first rule is added.
- final int upstreamIfindex = rule.upstreamIfindex;
- if (!isAnyRuleOnUpstream(upstreamIfindex)) {
- // If failed to set a data limit, probably should not use this upstream, because
- // the upstream may not want to blow through the data limit that was told to apply.
- // TODO: Perhaps stop the coordinator.
- boolean success = updateDataLimit(upstreamIfindex);
- if (!success) {
- final String iface = mInterfaceNames.get(upstreamIfindex);
- mLog.e("Setting data limit for " + iface + " failed.");
+ // When the first rule is added to an upstream, setup upstream forwarding and data limit.
+ maybeSetLimit(rule.upstreamIfindex);
+
+ if (!isAnyRuleFromDownstreamToUpstream(rule.downstreamIfindex, rule.upstreamIfindex)) {
+ final int downstream = rule.downstreamIfindex;
+ final int upstream = rule.upstreamIfindex;
+ // TODO: support upstream forwarding on non-point-to-point interfaces.
+ // TODO: get the MTU from LinkProperties and update the rules when it changes.
+ if (!mBpfCoordinatorShim.startUpstreamIpv6Forwarding(downstream, upstream, rule.srcMac,
+ NULL_MAC_ADDRESS, NULL_MAC_ADDRESS, NetworkStackConstants.ETHER_MTU)) {
+ mLog.e("Failed to enable upstream IPv6 forwarding from "
+ + mInterfaceNames.get(downstream) + " to " + mInterfaceNames.get(upstream));
}
}
@@ -279,15 +518,9 @@
*/
public void tetherOffloadRuleRemove(
@NonNull final IpServer ipServer, @NonNull final Ipv6ForwardingRule rule) {
- if (!mIsBpfEnabled) return;
+ if (!isUsingBpf()) return;
- try {
- // TODO: Perhaps avoid to remove a non-existent rule.
- mNetd.tetherOffloadRuleRemove(rule.toTetherOffloadRuleParcel());
- } catch (RemoteException | ServiceSpecificException e) {
- mLog.e("Could not remove IPv6 forwarding rule: ", e);
- return;
- }
+ if (!mBpfCoordinatorShim.tetherOffloadRuleRemove(rule)) return;
LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(ipServer);
if (rules == null) return;
@@ -303,20 +536,19 @@
mIpv6ForwardingRules.remove(ipServer);
}
- // Do cleanup functionality if there is no more rule on the given upstream.
- final int upstreamIfindex = rule.upstreamIfindex;
- if (!isAnyRuleOnUpstream(upstreamIfindex)) {
- try {
- final TetherStatsParcel stats =
- mNetd.tetherOffloadGetAndClearStats(upstreamIfindex);
- // Update the last stats delta and delete the local cache for a given upstream.
- updateQuotaAndStatsFromSnapshot(new TetherStatsParcel[] {stats});
- mStats.remove(upstreamIfindex);
- } catch (RemoteException | ServiceSpecificException e) {
- Log.wtf(TAG, "Exception when cleanup tether stats for upstream index "
- + upstreamIfindex + ": ", e);
+ // If no more rules between this upstream and downstream, stop upstream forwarding.
+ if (!isAnyRuleFromDownstreamToUpstream(rule.downstreamIfindex, rule.upstreamIfindex)) {
+ final int downstream = rule.downstreamIfindex;
+ final int upstream = rule.upstreamIfindex;
+ if (!mBpfCoordinatorShim.stopUpstreamIpv6Forwarding(downstream, upstream,
+ rule.srcMac)) {
+ mLog.e("Failed to disable upstream IPv6 forwarding from "
+ + mInterfaceNames.get(downstream) + " to " + mInterfaceNames.get(upstream));
}
}
+
+ // Do cleanup functionality if there is no more rule on the given upstream.
+ maybeClearLimit(rule.upstreamIfindex);
}
/**
@@ -324,7 +556,7 @@
* Note that this can be only called on handler thread.
*/
public void tetherOffloadRuleClear(@NonNull final IpServer ipServer) {
- if (!mIsBpfEnabled) return;
+ if (!isUsingBpf()) return;
final LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(
ipServer);
@@ -341,19 +573,29 @@
* Note that this can be only called on handler thread.
*/
public void tetherOffloadRuleUpdate(@NonNull final IpServer ipServer, int newUpstreamIfindex) {
- if (!mIsBpfEnabled) return;
+ if (!isUsingBpf()) return;
final LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(
ipServer);
if (rules == null) return;
// Need to build a rule list because the rule map may be changed in the iteration.
- for (final Ipv6ForwardingRule rule : new ArrayList<Ipv6ForwardingRule>(rules.values())) {
+ // First remove all the old rules, then add all the new rules. This is because the upstream
+ // forwarding code in tetherOffloadRuleAdd cannot support rules on two upstreams at the
+ // same time. Deleting the rules first ensures that upstream forwarding is disabled on the
+ // old upstream when the last rule is removed from it, and re-enabled on the new upstream
+ // when the first rule is added to it.
+ // TODO: Once the IPv6 client processing code has moved from IpServer to BpfCoordinator, do
+ // something smarter.
+ final ArrayList<Ipv6ForwardingRule> rulesCopy = new ArrayList<>(rules.values());
+ for (final Ipv6ForwardingRule rule : rulesCopy) {
// Remove the old rule before adding the new one because the map uses the same key for
// both rules. Reversing the processing order causes that the new rule is removed as
// unexpected.
// TODO: Add new rule first to reduce the latency which has no rule.
tetherOffloadRuleRemove(ipServer, rule);
+ }
+ for (final Ipv6ForwardingRule rule : rulesCopy) {
tetherOffloadRuleAdd(ipServer, rule.onNewUpstream(newUpstreamIfindex));
}
}
@@ -365,7 +607,7 @@
* Note that this can be only called on handler thread.
*/
public void addUpstreamNameToLookupTable(int upstreamIfindex, @NonNull String upstreamIface) {
- if (!mIsBpfEnabled) return;
+ if (!isUsingBpf()) return;
if (upstreamIfindex == 0 || TextUtils.isEmpty(upstreamIface)) return;
@@ -382,6 +624,111 @@
}
/**
+ * Add downstream client.
+ */
+ public void tetherOffloadClientAdd(@NonNull final IpServer ipServer,
+ @NonNull final ClientInfo client) {
+ if (!isUsingBpf()) return;
+
+ if (!mTetherClients.containsKey(ipServer)) {
+ mTetherClients.put(ipServer, new HashMap<Inet4Address, ClientInfo>());
+ }
+
+ HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer);
+ clients.put(client.clientAddress, client);
+ }
+
+ /**
+ * Remove downstream client.
+ */
+ public void tetherOffloadClientRemove(@NonNull final IpServer ipServer,
+ @NonNull final ClientInfo client) {
+ if (!isUsingBpf()) return;
+
+ HashMap<Inet4Address, ClientInfo> clients = mTetherClients.get(ipServer);
+ if (clients == null) return;
+
+ // If no rule is removed, return early. Avoid unnecessary work on a non-existent rule
+ // which may have never been added or removed already.
+ if (clients.remove(client.clientAddress) == null) return;
+
+ // Remove the downstream entry if it has no more rule.
+ if (clients.isEmpty()) {
+ mTetherClients.remove(ipServer);
+ }
+ }
+
+ /**
+ * Call when UpstreamNetworkState may be changed.
+ * If upstream has ipv4 for tethering, update this new UpstreamNetworkState to map. The
+ * upstream interface index and its address mapping is prepared for building IPv4
+ * offload rule.
+ *
+ * TODO: Delete the unused upstream interface mapping.
+ * TODO: Support ether ip upstream interface.
+ */
+ public void addUpstreamIfindexToMap(LinkProperties lp) {
+ if (!mPollingStarted) return;
+
+ // This will not work on a network that is using 464xlat because hasIpv4Address will not be
+ // true.
+ // TODO: need to consider 464xlat.
+ if (lp == null || !lp.hasIpv4Address()) return;
+
+ // Support raw ip upstream interface only.
+ final InterfaceParams params = mDeps.getInterfaceParams(lp.getInterfaceName());
+ if (params == null || params.hasMacAddress) return;
+
+ Collection<InetAddress> addresses = lp.getAddresses();
+ for (InetAddress addr: addresses) {
+ if (addr instanceof Inet4Address) {
+ Inet4Address i4addr = (Inet4Address) addr;
+ if (!i4addr.isAnyLocalAddress() && !i4addr.isLinkLocalAddress()
+ && !i4addr.isLoopbackAddress() && !i4addr.isMulticastAddress()) {
+ mIpv4UpstreamIndices.put(i4addr, params.index);
+ }
+ }
+ }
+ }
+
+ /**
+ * Attach BPF program
+ *
+ * TODO: consider error handling if the attach program failed.
+ */
+ public void maybeAttachProgram(@NonNull String intIface, @NonNull String extIface) {
+ if (forwardingPairExists(intIface, extIface)) return;
+
+ boolean firstDownstreamForThisUpstream = !isAnyForwardingPairOnUpstream(extIface);
+ forwardingPairAdd(intIface, extIface);
+
+ mBpfCoordinatorShim.attachProgram(intIface, UPSTREAM);
+ // Attach if the upstream is the first time to be used in a forwarding pair.
+ if (firstDownstreamForThisUpstream) {
+ mBpfCoordinatorShim.attachProgram(extIface, DOWNSTREAM);
+ }
+ }
+
+ /**
+ * Detach BPF program
+ */
+ public void maybeDetachProgram(@NonNull String intIface, @NonNull String extIface) {
+ forwardingPairRemove(intIface, extIface);
+
+ // Detaching program may fail because the interface has been removed already.
+ mBpfCoordinatorShim.detachProgram(intIface);
+ // Detach if no more forwarding pair is using the upstream.
+ if (!isAnyForwardingPairOnUpstream(extIface)) {
+ mBpfCoordinatorShim.detachProgram(extIface);
+ }
+ }
+
+ // TODO: make mInterfaceNames accessible to the shim and move this code to there.
+ private String getIfName(long ifindex) {
+ return mInterfaceNames.get((int) ifindex, Long.toString(ifindex));
+ }
+
+ /**
* Dump information.
* Block the function until all the data are dumped on the handler thread or timed-out. The
* reason is that dumpsys invokes this function on the thread of caller and the data may only
@@ -396,6 +743,7 @@
? "registered" : "not registered"));
pw.println("Upstream quota: " + mInterfaceQuotas.toString());
pw.println("Polling interval: " + getPollingInterval() + " ms");
+ pw.println("Bpf shim: " + mBpfCoordinatorShim.toString());
pw.println("Forwarding stats:");
pw.increaseIndent();
@@ -408,11 +756,15 @@
pw.println("Forwarding rules:");
pw.increaseIndent();
- if (mIpv6ForwardingRules.size() == 0) {
- pw.println("<empty>");
- } else {
- dumpIpv6ForwardingRules(pw);
- }
+ dumpIpv6UpstreamRules(pw);
+ dumpIpv6ForwardingRules(pw);
+ dumpIpv4ForwardingRules(pw);
+ pw.decreaseIndent();
+
+ pw.println();
+ pw.println("Forwarding counters:");
+ pw.increaseIndent();
+ dumpCounters(pw);
pw.decreaseIndent();
dumpDone.open();
@@ -432,6 +784,11 @@
}
private void dumpIpv6ForwardingRules(@NonNull IndentingPrintWriter pw) {
+ if (mIpv6ForwardingRules.size() == 0) {
+ pw.println("No IPv6 rules");
+ return;
+ }
+
for (Map.Entry<IpServer, LinkedHashMap<Inet6Address, Ipv6ForwardingRule>> entry :
mIpv6ForwardingRules.entrySet()) {
IpServer ipServer = entry.getKey();
@@ -446,17 +803,164 @@
final int upstreamIfindex = rule.upstreamIfindex;
pw.println(String.format("%d(%s) %d(%s) %s %s %s", upstreamIfindex,
mInterfaceNames.get(upstreamIfindex), rule.downstreamIfindex,
- downstreamIface, rule.address, rule.srcMac, rule.dstMac));
+ downstreamIface, rule.address.getHostAddress(), rule.srcMac, rule.dstMac));
}
pw.decreaseIndent();
}
}
+ private String ipv6UpstreamRuletoString(TetherUpstream6Key key, Tether6Value value) {
+ return String.format("%d(%s) %s -> %d(%s) %04x %s %s",
+ key.iif, getIfName(key.iif), key.dstMac, value.oif, getIfName(value.oif),
+ value.ethProto, value.ethSrcMac, value.ethDstMac);
+ }
+
+ private void dumpIpv6UpstreamRules(IndentingPrintWriter pw) {
+ try (BpfMap<TetherUpstream6Key, Tether6Value> map = mDeps.getBpfUpstream6Map()) {
+ if (map == null) {
+ pw.println("No IPv6 upstream");
+ return;
+ }
+ if (map.isEmpty()) {
+ pw.println("No IPv6 upstream rules");
+ return;
+ }
+ map.forEach((k, v) -> pw.println(ipv6UpstreamRuletoString(k, v)));
+ } catch (ErrnoException e) {
+ pw.println("Error dumping IPv6 upstream map: " + e);
+ }
+ }
+
+ private String ipv4RuleToString(Tether4Key key, Tether4Value value) {
+ final String private4, public4, dst4;
+ try {
+ private4 = InetAddress.getByAddress(key.src4).getHostAddress();
+ dst4 = InetAddress.getByAddress(key.dst4).getHostAddress();
+ public4 = InetAddress.getByAddress(value.src46).getHostAddress();
+ } catch (UnknownHostException impossible) {
+ throw new AssertionError("4-byte array not valid IPv4 address!");
+ }
+ return String.format("[%s] %d(%s) %s:%d -> %d(%s) %s:%d -> %s:%d",
+ key.dstMac, key.iif, getIfName(key.iif), private4, key.srcPort,
+ value.oif, getIfName(value.oif),
+ public4, value.srcPort, dst4, key.dstPort);
+ }
+
+ private void dumpIpv4ForwardingRules(IndentingPrintWriter pw) {
+ try (BpfMap<Tether4Key, Tether4Value> map = mDeps.getBpfUpstream4Map()) {
+ if (map == null) {
+ pw.println("No IPv4 support");
+ return;
+ }
+ if (map.isEmpty()) {
+ pw.println("No IPv4 rules");
+ return;
+ }
+ pw.println("IPv4: [inDstMac] iif(iface) src -> nat -> dst");
+ pw.increaseIndent();
+ map.forEach((k, v) -> pw.println(ipv4RuleToString(k, v)));
+ } catch (ErrnoException e) {
+ pw.println("Error dumping IPv4 map: " + e);
+ }
+ pw.decreaseIndent();
+ }
+
+ /**
+ * Simple struct that only contains a u32. Must be public because Struct needs access to it.
+ * TODO: make this a public inner class of Struct so anyone can use it as, e.g., Struct.U32?
+ */
+ public static class U32Struct extends Struct {
+ @Struct.Field(order = 0, type = Struct.Type.U32)
+ public long val;
+ }
+
+ private void dumpCounters(@NonNull IndentingPrintWriter pw) {
+ if (!mDeps.isAtLeastS()) {
+ pw.println("No counter support");
+ return;
+ }
+ try (BpfMap<U32Struct, U32Struct> map = new BpfMap<>(TETHER_ERROR_MAP_PATH,
+ BpfMap.BPF_F_RDONLY, U32Struct.class, U32Struct.class)) {
+
+ map.forEach((k, v) -> {
+ String counterName;
+ try {
+ counterName = sBpfCounterNames[(int) k.val];
+ } catch (IndexOutOfBoundsException e) {
+ // Should never happen because this code gets the counter name from the same
+ // include file as the BPF program that increments the counter.
+ Log.wtf(TAG, "Unknown tethering counter type " + k.val);
+ counterName = Long.toString(k.val);
+ }
+ if (v.val > 0) pw.println(String.format("%s: %d", counterName, v.val));
+ });
+ } catch (ErrnoException e) {
+ pw.println("Error dumping counter map: " + e);
+ }
+ }
+
/** IPv6 forwarding rule class. */
public static class Ipv6ForwardingRule {
+ // The upstream6 and downstream6 rules are built as the following tables. Only raw ip
+ // upstream interface is supported.
+ // TODO: support ether ip upstream interface.
+ //
+ // NAT network topology:
+ //
+ // public network (rawip) private network
+ // | UE |
+ // +------------+ V +------------+------------+ V +------------+
+ // | Sever +---------+ Upstream | Downstream +---------+ Client |
+ // +------------+ +------------+------------+ +------------+
+ //
+ // upstream6 key and value:
+ //
+ // +------+-------------+
+ // | TetherUpstream6Key |
+ // +------+------+------+
+ // |field |iif |dstMac|
+ // | | | |
+ // +------+------+------+
+ // |value |downst|downst|
+ // | |ream |ream |
+ // +------+------+------+
+ //
+ // +------+----------------------------------+
+ // | |Tether6Value |
+ // +------+------+------+------+------+------+
+ // |field |oif |ethDst|ethSrc|ethPro|pmtu |
+ // | | |mac |mac |to | |
+ // +------+------+------+------+------+------+
+ // |value |upstre|-- |-- |ETH_P_|1500 |
+ // | |am | | |IP | |
+ // +------+------+------+------+------+------+
+ //
+ // downstream6 key and value:
+ //
+ // +------+--------------------+
+ // | |TetherDownstream6Key|
+ // +------+------+------+------+
+ // |field |iif |dstMac|neigh6|
+ // | | | | |
+ // +------+------+------+------+
+ // |value |upstre|-- |client|
+ // | |am | | |
+ // +------+------+------+------+
+ //
+ // +------+----------------------------------+
+ // | |Tether6Value |
+ // +------+------+------+------+------+------+
+ // |field |oif |ethDst|ethSrc|ethPro|pmtu |
+ // | | |mac |mac |to | |
+ // +------+------+------+------+------+------+
+ // |value |downst|client|downst|ETH_P_|1500 |
+ // | |ream | |ream |IP | |
+ // +------+------+------+------+------+------+
+ //
public final int upstreamIfindex;
public final int downstreamIfindex;
+ // TODO: store a ClientInfo object instead of storing address, srcMac, and dstMac directly.
@NonNull
public final Inet6Address address;
@NonNull
@@ -497,6 +1001,24 @@
return parcel;
}
+ /**
+ * Return a TetherDownstream6Key object built from the rule.
+ */
+ @NonNull
+ public TetherDownstream6Key makeTetherDownstream6Key() {
+ return new TetherDownstream6Key(upstreamIfindex, NULL_MAC_ADDRESS,
+ address.getAddress());
+ }
+
+ /**
+ * Return a Tether6Value object built from the rule.
+ */
+ @NonNull
+ public Tether6Value makeTether6Value() {
+ return new Tether6Value(downstreamIfindex, dstMac, srcMac, ETH_P_IPV6,
+ NetworkStackConstants.ETHER_MTU);
+ }
+
@Override
public boolean equals(Object o) {
if (!(o instanceof Ipv6ForwardingRule)) return false;
@@ -516,6 +1038,48 @@
}
}
+ /** Tethering client information class. */
+ public static class ClientInfo {
+ public final int downstreamIfindex;
+
+ @NonNull
+ public final MacAddress downstreamMac;
+ @NonNull
+ public final Inet4Address clientAddress;
+ @NonNull
+ public final MacAddress clientMac;
+
+ public ClientInfo(int downstreamIfindex,
+ @NonNull MacAddress downstreamMac, @NonNull Inet4Address clientAddress,
+ @NonNull MacAddress clientMac) {
+ this.downstreamIfindex = downstreamIfindex;
+ this.downstreamMac = downstreamMac;
+ this.clientAddress = clientAddress;
+ this.clientMac = clientMac;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ClientInfo)) return false;
+ ClientInfo that = (ClientInfo) o;
+ return this.downstreamIfindex == that.downstreamIfindex
+ && Objects.equals(this.downstreamMac, that.downstreamMac)
+ && Objects.equals(this.clientAddress, that.clientAddress)
+ && Objects.equals(this.clientMac, that.clientMac);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(downstreamIfindex, downstreamMac, clientAddress, clientMac);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("downstream: %d (%s), client: %s (%s)",
+ downstreamIfindex, downstreamMac, clientAddress, clientMac);
+ }
+ }
+
/**
* A BPF tethering stats provider to provide network statistics to the system.
* Note that this class' data may only be accessed on the handler thread.
@@ -582,6 +1146,104 @@
}
}
+ @Nullable
+ private ClientInfo getClientInfo(@NonNull Inet4Address clientAddress) {
+ for (HashMap<Inet4Address, ClientInfo> clients : mTetherClients.values()) {
+ for (ClientInfo client : clients.values()) {
+ if (clientAddress.equals(client.clientAddress)) {
+ return client;
+ }
+ }
+ }
+ return null;
+ }
+
+ // Support raw ip only.
+ // TODO: add ether ip support.
+ // TODO: parse CTA_PROTOINFO of conntrack event in ConntrackMonitor. For TCP, only add rules
+ // while TCP status is established.
+ @VisibleForTesting
+ class BpfConntrackEventConsumer implements ConntrackEventConsumer {
+ @NonNull
+ private Tether4Key makeTetherUpstream4Key(
+ @NonNull ConntrackEvent e, @NonNull ClientInfo c) {
+ return new Tether4Key(c.downstreamIfindex, c.downstreamMac,
+ e.tupleOrig.protoNum, e.tupleOrig.srcIp.getAddress(),
+ e.tupleOrig.dstIp.getAddress(), e.tupleOrig.srcPort, e.tupleOrig.dstPort);
+ }
+
+ @NonNull
+ private Tether4Key makeTetherDownstream4Key(
+ @NonNull ConntrackEvent e, @NonNull ClientInfo c, int upstreamIndex) {
+ return new Tether4Key(upstreamIndex, NULL_MAC_ADDRESS /* dstMac (rawip) */,
+ e.tupleReply.protoNum, e.tupleReply.srcIp.getAddress(),
+ e.tupleReply.dstIp.getAddress(), e.tupleReply.srcPort, e.tupleReply.dstPort);
+ }
+
+ @NonNull
+ private Tether4Value makeTetherUpstream4Value(@NonNull ConntrackEvent e,
+ int upstreamIndex) {
+ return new Tether4Value(upstreamIndex,
+ NULL_MAC_ADDRESS /* ethDstMac (rawip) */,
+ NULL_MAC_ADDRESS /* ethSrcMac (rawip) */, ETH_P_IP,
+ NetworkStackConstants.ETHER_MTU, toIpv4MappedAddressBytes(e.tupleReply.dstIp),
+ toIpv4MappedAddressBytes(e.tupleReply.srcIp), e.tupleReply.dstPort,
+ e.tupleReply.srcPort, 0 /* lastUsed, filled by bpf prog only */);
+ }
+
+ @NonNull
+ private Tether4Value makeTetherDownstream4Value(@NonNull ConntrackEvent e,
+ @NonNull ClientInfo c, int upstreamIndex) {
+ return new Tether4Value(c.downstreamIfindex,
+ c.clientMac, c.downstreamMac, ETH_P_IP, NetworkStackConstants.ETHER_MTU,
+ toIpv4MappedAddressBytes(e.tupleOrig.dstIp),
+ toIpv4MappedAddressBytes(e.tupleOrig.srcIp),
+ e.tupleOrig.dstPort, e.tupleOrig.srcPort,
+ 0 /* lastUsed, filled by bpf prog only */);
+ }
+
+ @NonNull
+ private byte[] toIpv4MappedAddressBytes(Inet4Address ia4) {
+ final byte[] addr4 = ia4.getAddress();
+ final byte[] addr6 = new byte[16];
+ addr6[10] = (byte) 0xff;
+ addr6[11] = (byte) 0xff;
+ addr6[12] = addr4[0];
+ addr6[13] = addr4[1];
+ addr6[14] = addr4[2];
+ addr6[15] = addr4[3];
+ return addr6;
+ }
+
+ public void accept(ConntrackEvent e) {
+ final ClientInfo tetherClient = getClientInfo(e.tupleOrig.srcIp);
+ if (tetherClient == null) return;
+
+ final Integer upstreamIndex = mIpv4UpstreamIndices.get(e.tupleReply.dstIp);
+ if (upstreamIndex == null) return;
+
+ final Tether4Key upstream4Key = makeTetherUpstream4Key(e, tetherClient);
+ final Tether4Key downstream4Key = makeTetherDownstream4Key(e, tetherClient,
+ upstreamIndex);
+
+ if (e.msgType == (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8
+ | NetlinkConstants.IPCTNL_MSG_CT_DELETE)) {
+ mBpfCoordinatorShim.tetherOffloadRuleRemove(UPSTREAM, upstream4Key);
+ mBpfCoordinatorShim.tetherOffloadRuleRemove(DOWNSTREAM, downstream4Key);
+ maybeClearLimit(upstreamIndex);
+ return;
+ }
+
+ final Tether4Value upstream4Value = makeTetherUpstream4Value(e, upstreamIndex);
+ final Tether4Value downstream4Value = makeTetherDownstream4Value(e, tetherClient,
+ upstreamIndex);
+
+ maybeSetLimit(upstreamIndex);
+ mBpfCoordinatorShim.tetherOffloadRuleAdd(UPSTREAM, upstream4Key, upstream4Value);
+ mBpfCoordinatorShim.tetherOffloadRuleAdd(DOWNSTREAM, downstream4Key, downstream4Value);
+ }
+ }
+
private boolean isBpfEnabled() {
final TetheringConfiguration config = mDeps.getTetherConfig();
return (config != null) ? config.isBpfOffloadEnabled() : true /* default value */;
@@ -607,20 +1269,13 @@
return quotaBytes;
}
- private boolean sendDataLimitToNetd(int ifIndex, long quotaBytes) {
+ private boolean sendDataLimitToBpfMap(int ifIndex, long quotaBytes) {
if (ifIndex == 0) {
Log.wtf(TAG, "Invalid interface index.");
return false;
}
- try {
- mNetd.tetherOffloadSetInterfaceQuota(ifIndex, quotaBytes);
- } catch (RemoteException | ServiceSpecificException e) {
- mLog.e("Exception when updating quota " + quotaBytes + ": ", e);
- return false;
- }
-
- return true;
+ return mBpfCoordinatorShim.tetherOffloadSetInterfaceQuota(ifIndex, quotaBytes);
}
// Handle the data limit update from the service which is the stats provider registered for.
@@ -633,7 +1288,7 @@
if (ifIndex == 0) return;
final long quotaBytes = getQuotaBytes(iface);
- sendDataLimitToNetd(ifIndex, quotaBytes);
+ sendDataLimitToBpfMap(ifIndex, quotaBytes);
}
// Handle the data limit update while adding forwarding rules.
@@ -644,9 +1299,50 @@
return false;
}
final long quotaBytes = getQuotaBytes(iface);
- return sendDataLimitToNetd(ifIndex, quotaBytes);
+ return sendDataLimitToBpfMap(ifIndex, quotaBytes);
}
+ private void maybeSetLimit(int upstreamIfindex) {
+ if (isAnyRuleOnUpstream(upstreamIfindex)
+ || mBpfCoordinatorShim.isAnyIpv4RuleOnUpstream(upstreamIfindex)) {
+ return;
+ }
+
+ // If failed to set a data limit, probably should not use this upstream, because
+ // the upstream may not want to blow through the data limit that was told to apply.
+ // TODO: Perhaps stop the coordinator.
+ boolean success = updateDataLimit(upstreamIfindex);
+ if (!success) {
+ final String iface = mInterfaceNames.get(upstreamIfindex);
+ mLog.e("Setting data limit for " + iface + " failed.");
+ }
+ }
+
+ // TODO: This should be also called while IpServer wants to clear all IPv4 rules. Relying on
+ // conntrack event can't cover this case.
+ private void maybeClearLimit(int upstreamIfindex) {
+ if (isAnyRuleOnUpstream(upstreamIfindex)
+ || mBpfCoordinatorShim.isAnyIpv4RuleOnUpstream(upstreamIfindex)) {
+ return;
+ }
+
+ final TetherStatsValue statsValue =
+ mBpfCoordinatorShim.tetherOffloadGetAndClearStats(upstreamIfindex);
+ if (statsValue == null) {
+ Log.wtf(TAG, "Fail to cleanup tether stats for upstream index " + upstreamIfindex);
+ return;
+ }
+
+ SparseArray<TetherStatsValue> tetherStatsList = new SparseArray<TetherStatsValue>();
+ tetherStatsList.put(upstreamIfindex, statsValue);
+
+ // Update the last stats delta and delete the local cache for a given upstream.
+ updateQuotaAndStatsFromSnapshot(tetherStatsList);
+ mStats.remove(upstreamIfindex);
+ }
+
+ // TODO: Rename to isAnyIpv6RuleOnUpstream and define an isAnyRuleOnUpstream method that called
+ // both isAnyIpv6RuleOnUpstream and mBpfCoordinatorShim.isAnyIpv4RuleOnUpstream.
private boolean isAnyRuleOnUpstream(int upstreamIfindex) {
for (LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules : mIpv6ForwardingRules
.values()) {
@@ -657,6 +1353,46 @@
return false;
}
+ private boolean isAnyRuleFromDownstreamToUpstream(int downstreamIfindex, int upstreamIfindex) {
+ for (LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules : mIpv6ForwardingRules
+ .values()) {
+ for (Ipv6ForwardingRule rule : rules.values()) {
+ if (downstreamIfindex == rule.downstreamIfindex
+ && upstreamIfindex == rule.upstreamIfindex) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private void forwardingPairAdd(@NonNull String intIface, @NonNull String extIface) {
+ if (!mForwardingPairs.containsKey(extIface)) {
+ mForwardingPairs.put(extIface, new HashSet<String>());
+ }
+ mForwardingPairs.get(extIface).add(intIface);
+ }
+
+ private void forwardingPairRemove(@NonNull String intIface, @NonNull String extIface) {
+ HashSet<String> downstreams = mForwardingPairs.get(extIface);
+ if (downstreams == null) return;
+ if (!downstreams.remove(intIface)) return;
+
+ if (downstreams.isEmpty()) {
+ mForwardingPairs.remove(extIface);
+ }
+ }
+
+ private boolean forwardingPairExists(@NonNull String intIface, @NonNull String extIface) {
+ if (!mForwardingPairs.containsKey(extIface)) return false;
+
+ return mForwardingPairs.get(extIface).contains(intIface);
+ }
+
+ private boolean isAnyForwardingPairOnUpstream(@NonNull String extIface) {
+ return mForwardingPairs.containsKey(extIface);
+ }
+
@NonNull
private NetworkStats buildNetworkStats(@NonNull StatsType type, int ifIndex,
@NonNull final ForwardedStats diff) {
@@ -690,10 +1426,11 @@
}
private void updateQuotaAndStatsFromSnapshot(
- @NonNull final TetherStatsParcel[] tetherStatsList) {
+ @NonNull final SparseArray<TetherStatsValue> tetherStatsList) {
long usedAlertQuota = 0;
- for (TetherStatsParcel tetherStats : tetherStatsList) {
- final Integer ifIndex = tetherStats.ifIndex;
+ for (int i = 0; i < tetherStatsList.size(); i++) {
+ final Integer ifIndex = tetherStatsList.keyAt(i);
+ final TetherStatsValue tetherStats = tetherStatsList.valueAt(i);
final ForwardedStats curr = new ForwardedStats(tetherStats);
final ForwardedStats base = mStats.get(ifIndex);
final ForwardedStats diff = (base != null) ? curr.subtract(base) : curr;
@@ -725,16 +1462,15 @@
// TODO: Count the used limit quota for notifying data limit reached.
}
- private void updateForwardedStatsFromNetd() {
- final TetherStatsParcel[] tetherStatsList;
- try {
- // The reported tether stats are total data usage for all currently-active upstream
- // interfaces since tethering start.
- tetherStatsList = mNetd.tetherOffloadGetStats();
- } catch (RemoteException | ServiceSpecificException e) {
- mLog.e("Problem fetching tethering stats: ", e);
+ private void updateForwardedStats() {
+ final SparseArray<TetherStatsValue> tetherStatsList =
+ mBpfCoordinatorShim.tetherOffloadGetStats();
+
+ if (tetherStatsList == null) {
+ mLog.e("Problem fetching tethering stats");
return;
}
+
updateQuotaAndStatsFromSnapshot(tetherStatsList);
}
@@ -775,4 +1511,14 @@
final SparseArray<String> getInterfaceNamesForTesting() {
return mInterfaceNames;
}
+
+ // Return BPF conntrack event consumer. This is used for testing only.
+ // Note that this can be only called on handler thread.
+ @NonNull
+ @VisibleForTesting
+ final BpfConntrackEventConsumer getBpfConntrackEventConsumerForTesting() {
+ return mBpfConntrackEventConsumer;
+ }
+
+ private static native String[] getBpfCounterNames();
}
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfMap.java b/Tethering/src/com/android/networkstack/tethering/BpfMap.java
new file mode 100644
index 0000000..1363dc5
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/BpfMap.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.networkstack.tethering;
+
+import static android.system.OsConstants.EEXIST;
+import static android.system.OsConstants.ENOENT;
+
+import android.system.ErrnoException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.Struct;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+
+/**
+ * BpfMap is a key -> value mapping structure that is designed to maintained the bpf map entries.
+ * This is a wrapper class of in-kernel data structure. The in-kernel data can be read/written by
+ * passing syscalls with map file descriptor.
+ *
+ * @param <K> the key of the map.
+ * @param <V> the value of the map.
+ */
+public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable {
+ static {
+ System.loadLibrary("tetherutilsjni");
+ }
+
+ // Following definitions from kernel include/uapi/linux/bpf.h
+ public static final int BPF_F_RDWR = 0;
+ public static final int BPF_F_RDONLY = 1 << 3;
+ public static final int BPF_F_WRONLY = 1 << 4;
+
+ public static final int BPF_MAP_TYPE_HASH = 1;
+
+ private static final int BPF_F_NO_PREALLOC = 1;
+
+ private static final int BPF_ANY = 0;
+ private static final int BPF_NOEXIST = 1;
+ private static final int BPF_EXIST = 2;
+
+ private final int mMapFd;
+ private final Class<K> mKeyClass;
+ private final Class<V> mValueClass;
+ private final int mKeySize;
+ private final int mValueSize;
+
+ /**
+ * Create a BpfMap map wrapper with "path" of filesystem.
+ *
+ * @param flag the access mode, one of BPF_F_RDWR, BPF_F_RDONLY, or BPF_F_WRONLY.
+ * @throws ErrnoException if the BPF map associated with {@code path} cannot be retrieved.
+ * @throws NullPointerException if {@code path} is null.
+ */
+ public BpfMap(@NonNull final String path, final int flag, final Class<K> key,
+ final Class<V> value) throws ErrnoException, NullPointerException {
+ mMapFd = bpfFdGet(path, flag);
+
+ mKeyClass = key;
+ mValueClass = value;
+ mKeySize = Struct.getSize(key);
+ mValueSize = Struct.getSize(value);
+ }
+
+ /**
+ * Constructor for testing only.
+ * The derived class implements an internal mocked map. It need to implement all functions
+ * which are related with the native BPF map because the BPF map handler is not initialized.
+ * See BpfCoordinatorTest#TestBpfMap.
+ */
+ @VisibleForTesting
+ protected BpfMap(final Class<K> key, final Class<V> value) {
+ mMapFd = -1;
+ mKeyClass = key;
+ mValueClass = value;
+ mKeySize = Struct.getSize(key);
+ mValueSize = Struct.getSize(value);
+ }
+
+ /**
+ * Update an existing or create a new key -> value entry in an eBbpf map.
+ * (use insertOrReplaceEntry() if you need to know whether insert or replace happened)
+ */
+ public void updateEntry(K key, V value) throws ErrnoException {
+ writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_ANY);
+ }
+
+ /**
+ * If the key does not exist in the map, insert key -> value entry into eBpf map.
+ * Otherwise IllegalStateException will be thrown.
+ */
+ public void insertEntry(K key, V value)
+ throws ErrnoException, IllegalStateException {
+ try {
+ writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST);
+ } catch (ErrnoException e) {
+ if (e.errno == EEXIST) throw new IllegalStateException(key + " already exists");
+
+ throw e;
+ }
+ }
+
+ /**
+ * If the key already exists in the map, replace its value. Otherwise NoSuchElementException
+ * will be thrown.
+ */
+ public void replaceEntry(K key, V value)
+ throws ErrnoException, NoSuchElementException {
+ try {
+ writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_EXIST);
+ } catch (ErrnoException e) {
+ if (e.errno == ENOENT) throw new NoSuchElementException(key + " not found");
+
+ throw e;
+ }
+ }
+
+ /**
+ * Update an existing or create a new key -> value entry in an eBbpf map.
+ * Returns true if inserted, false if replaced.
+ * (use updateEntry() if you don't care whether insert or replace happened)
+ * Note: see inline comment below if running concurrently with delete operations.
+ */
+ public boolean insertOrReplaceEntry(K key, V value)
+ throws ErrnoException {
+ try {
+ writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST);
+ return true; /* insert succeeded */
+ } catch (ErrnoException e) {
+ if (e.errno != EEXIST) throw e;
+ }
+ try {
+ writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_EXIST);
+ return false; /* replace succeeded */
+ } catch (ErrnoException e) {
+ if (e.errno != ENOENT) throw e;
+ }
+ /* If we reach here somebody deleted after our insert attempt and before our replace:
+ * this implies a race happened. The kernel bpf delete interface only takes a key,
+ * and not the value, so we can safely pretend the replace actually succeeded and
+ * was immediately followed by the other thread's delete, since the delete cannot
+ * observe the potential change to the value.
+ */
+ return false; /* pretend replace succeeded */
+ }
+
+ /** Remove existing key from eBpf map. Return false if map was not modified. */
+ public boolean deleteEntry(K key) throws ErrnoException {
+ return deleteMapEntry(mMapFd, key.writeToBytes());
+ }
+
+ /** Returns {@code true} if this map contains no elements. */
+ public boolean isEmpty() throws ErrnoException {
+ return getFirstKey() == null;
+ }
+
+ private K getNextKeyInternal(@Nullable K key) throws ErrnoException {
+ final byte[] rawKey = getNextRawKey(
+ key == null ? null : key.writeToBytes());
+ if (rawKey == null) return null;
+
+ final ByteBuffer buffer = ByteBuffer.wrap(rawKey);
+ buffer.order(ByteOrder.nativeOrder());
+ return Struct.parse(mKeyClass, buffer);
+ }
+
+ /**
+ * Get the next key of the passed-in key. If the passed-in key is not found, return the first
+ * key. If the passed-in key is the last one, return null.
+ *
+ * TODO: consider allowing null passed-in key.
+ */
+ public K getNextKey(@NonNull K key) throws ErrnoException {
+ Objects.requireNonNull(key);
+ return getNextKeyInternal(key);
+ }
+
+ private byte[] getNextRawKey(@Nullable final byte[] key) throws ErrnoException {
+ byte[] nextKey = new byte[mKeySize];
+ if (getNextMapKey(mMapFd, key, nextKey)) return nextKey;
+
+ return null;
+ }
+
+ /** Get the first key of eBpf map. */
+ public K getFirstKey() throws ErrnoException {
+ return getNextKeyInternal(null);
+ }
+
+ /** Check whether a key exists in the map. */
+ public boolean containsKey(@NonNull K key) throws ErrnoException {
+ Objects.requireNonNull(key);
+
+ final byte[] rawValue = getRawValue(key.writeToBytes());
+ return rawValue != null;
+ }
+
+ /** Retrieve a value from the map. Return null if there is no such key. */
+ public V getValue(@NonNull K key) throws ErrnoException {
+ Objects.requireNonNull(key);
+ final byte[] rawValue = getRawValue(key.writeToBytes());
+
+ if (rawValue == null) return null;
+
+ final ByteBuffer buffer = ByteBuffer.wrap(rawValue);
+ buffer.order(ByteOrder.nativeOrder());
+ return Struct.parse(mValueClass, buffer);
+ }
+
+ private byte[] getRawValue(final byte[] key) throws ErrnoException {
+ byte[] value = new byte[mValueSize];
+ if (findMapEntry(mMapFd, key, value)) return value;
+
+ return null;
+ }
+
+ /**
+ * Iterate through the map and handle each key -> value retrieved base on the given BiConsumer.
+ * The given BiConsumer may to delete the passed-in entry, but is not allowed to perform any
+ * other structural modifications to the map, such as adding entries or deleting other entries.
+ * Otherwise, iteration will result in undefined behaviour.
+ */
+ public void forEach(BiConsumer<K, V> action) throws ErrnoException {
+ @Nullable K nextKey = getFirstKey();
+
+ while (nextKey != null) {
+ @NonNull final K curKey = nextKey;
+ @NonNull final V value = getValue(curKey);
+
+ nextKey = getNextKey(curKey);
+ action.accept(curKey, value);
+ }
+ }
+
+ @Override
+ public void close() throws ErrnoException {
+ closeMap(mMapFd);
+ }
+
+ /**
+ * Clears the map. The map may already be empty.
+ *
+ * @throws ErrnoException if the map is already closed, if an error occurred during iteration,
+ * or if a non-ENOENT error occurred when deleting a key.
+ */
+ public void clear() throws ErrnoException {
+ K key = getFirstKey();
+ while (key != null) {
+ deleteEntry(key); // ignores ENOENT.
+ key = getFirstKey();
+ }
+ }
+
+ private static native int closeMap(int fd) throws ErrnoException;
+
+ private native int bpfFdGet(String path, int mode) throws ErrnoException, NullPointerException;
+
+ private native void writeToMapEntry(int fd, byte[] key, byte[] value, int flags)
+ throws ErrnoException;
+
+ private native boolean deleteMapEntry(int fd, byte[] key) throws ErrnoException;
+
+ // If key is found, the operation returns true and the nextKey would reference to the next
+ // element. If key is not found, the operation returns true and the nextKey would reference to
+ // the first element. If key is the last element, false is returned.
+ private native boolean getNextMapKey(int fd, byte[] key, byte[] nextKey) throws ErrnoException;
+
+ private native boolean findMapEntry(int fd, byte[] key, byte[] value) throws ErrnoException;
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfUtils.java b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java
new file mode 100644
index 0000000..0b44249
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.networkstack.tethering;
+
+import static android.system.OsConstants.ETH_P_IP;
+import static android.system.OsConstants.ETH_P_IPV6;
+
+import android.net.util.InterfaceParams;
+
+import androidx.annotation.NonNull;
+
+import java.io.IOException;
+
+/**
+ * The classes and the methods for BPF utilization.
+ *
+ * {@hide}
+ */
+public class BpfUtils {
+ static {
+ System.loadLibrary("tetherutilsjni");
+ }
+
+ // For better code clarity when used for 'bool ingress' parameter.
+ static final boolean EGRESS = false;
+ static final boolean INGRESS = true;
+
+ // For better code clarify when used for 'bool downstream' parameter.
+ //
+ // This is talking about the direction of travel of the offloaded packets.
+ //
+ // Upstream means packets heading towards the internet/uplink (upload),
+ // thus for tethering this is attached to ingress on the downstream interface,
+ // while for clat this is attached to egress on the v4-* clat interface.
+ //
+ // Downstream means packets coming from the internet/uplink (download), thus
+ // for both clat and tethering this is attached to ingress on the upstream interface.
+ static final boolean DOWNSTREAM = true;
+ static final boolean UPSTREAM = false;
+
+ // The priority of clat/tether hooks - smaller is higher priority.
+ // TC tether is higher priority then TC clat to match XDP winning over TC.
+ // Sync from system/netd/server/OffloadUtils.h.
+ static final short PRIO_TETHER6 = 1;
+ static final short PRIO_TETHER4 = 2;
+ // note that the above must be lower than PRIO_CLAT from netd's OffloadUtils.cpp
+
+ private static String makeProgPath(boolean downstream, int ipVersion, boolean ether) {
+ String path = "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_"
+ + (downstream ? "downstream" : "upstream")
+ + ipVersion + "_"
+ + (ether ? "ether" : "rawip");
+ return path;
+ }
+
+ /**
+ * Attach BPF program
+ *
+ * TODO: use interface index to replace interface name.
+ */
+ public static void attachProgram(@NonNull String iface, boolean downstream)
+ throws IOException {
+ final InterfaceParams params = InterfaceParams.getByName(iface);
+ if (params == null) {
+ throw new IOException("Fail to get interface params for interface " + iface);
+ }
+
+ boolean ether;
+ try {
+ ether = isEthernet(iface);
+ } catch (IOException e) {
+ throw new IOException("isEthernet(" + params.index + "[" + iface + "]) failure: " + e);
+ }
+
+ try {
+ // tc filter add dev .. ingress prio 1 protocol ipv6 bpf object-pinned /sys/fs/bpf/...
+ // direct-action
+ tcFilterAddDevBpf(params.index, INGRESS, PRIO_TETHER6, (short) ETH_P_IPV6,
+ makeProgPath(downstream, 6, ether));
+ } catch (IOException e) {
+ throw new IOException("tc filter add dev (" + params.index + "[" + iface
+ + "]) ingress prio PRIO_TETHER6 protocol ipv6 failure: " + e);
+ }
+
+ try {
+ // tc filter add dev .. ingress prio 2 protocol ip bpf object-pinned /sys/fs/bpf/...
+ // direct-action
+ tcFilterAddDevBpf(params.index, INGRESS, PRIO_TETHER4, (short) ETH_P_IP,
+ makeProgPath(downstream, 4, ether));
+ } catch (IOException e) {
+ throw new IOException("tc filter add dev (" + params.index + "[" + iface
+ + "]) ingress prio PRIO_TETHER4 protocol ip failure: " + e);
+ }
+ }
+
+ /**
+ * Detach BPF program
+ *
+ * TODO: use interface index to replace interface name.
+ */
+ public static void detachProgram(@NonNull String iface) throws IOException {
+ final InterfaceParams params = InterfaceParams.getByName(iface);
+ if (params == null) {
+ throw new IOException("Fail to get interface params for interface " + iface);
+ }
+
+ try {
+ // tc filter del dev .. ingress prio 1 protocol ipv6
+ tcFilterDelDev(params.index, INGRESS, PRIO_TETHER6, (short) ETH_P_IPV6);
+ } catch (IOException e) {
+ throw new IOException("tc filter del dev (" + params.index + "[" + iface
+ + "]) ingress prio PRIO_TETHER6 protocol ipv6 failure: " + e);
+ }
+
+ try {
+ // tc filter del dev .. ingress prio 2 protocol ip
+ tcFilterDelDev(params.index, INGRESS, PRIO_TETHER4, (short) ETH_P_IP);
+ } catch (IOException e) {
+ throw new IOException("tc filter del dev (" + params.index + "[" + iface
+ + "]) ingress prio PRIO_TETHER4 protocol ip failure: " + e);
+ }
+ }
+
+ private static native boolean isEthernet(String iface) throws IOException;
+
+ private static native void tcFilterAddDevBpf(int ifIndex, boolean ingress, short prio,
+ short proto, String bpfProgPath) throws IOException;
+
+ private static native void tcFilterDelDev(int ifIndex, boolean ingress, short prio,
+ short proto) throws IOException;
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
index bb7322f..b1e3cfe 100644
--- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
+++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java
@@ -418,7 +418,8 @@
if (period <= 0) return;
Intent intent = new Intent(ACTION_PROVISIONING_ALARM);
- mProvisioningRecheckAlarm = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+ mProvisioningRecheckAlarm = PendingIntent.getBroadcast(mContext, 0, intent,
+ PendingIntent.FLAG_IMMUTABLE);
AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(
Context.ALARM_SERVICE);
long periodMs = period * MS_PER_HOUR;
diff --git a/Tethering/src/com/android/networkstack/tethering/Tether4Key.java b/Tethering/src/com/android/networkstack/tethering/Tether4Key.java
new file mode 100644
index 0000000..a01ea34
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/Tether4Key.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet4Address;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/** Key type for downstream & upstream IPv4 forwarding maps. */
+public class Tether4Key extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long iif;
+
+ @Field(order = 1, type = Type.EUI48)
+ public final MacAddress dstMac;
+
+ @Field(order = 2, type = Type.U8, padding = 1)
+ public final short l4proto;
+
+ @Field(order = 3, type = Type.ByteArray, arraysize = 4)
+ public final byte[] src4;
+
+ @Field(order = 4, type = Type.ByteArray, arraysize = 4)
+ public final byte[] dst4;
+
+ @Field(order = 5, type = Type.UBE16)
+ public final int srcPort;
+
+ @Field(order = 6, type = Type.UBE16)
+ public final int dstPort;
+
+ public Tether4Key(final long iif, @NonNull final MacAddress dstMac, final short l4proto,
+ final byte[] src4, final byte[] dst4, final int srcPort,
+ final int dstPort) {
+ Objects.requireNonNull(dstMac);
+
+ this.iif = iif;
+ this.dstMac = dstMac;
+ this.l4proto = l4proto;
+ this.src4 = src4;
+ this.dst4 = dst4;
+ this.srcPort = srcPort;
+ this.dstPort = dstPort;
+ }
+
+ @Override
+ public String toString() {
+ try {
+ return String.format(
+ "iif: %d, dstMac: %s, l4proto: %d, src4: %s, dst4: %s, "
+ + "srcPort: %d, dstPort: %d",
+ iif, dstMac, l4proto,
+ Inet4Address.getByAddress(src4), Inet4Address.getByAddress(dst4),
+ Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort));
+ } catch (UnknownHostException | IllegalArgumentException e) {
+ return String.format("Invalid IP address", e);
+ }
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/Tether4Value.java b/Tethering/src/com/android/networkstack/tethering/Tether4Value.java
new file mode 100644
index 0000000..03a226c
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/Tether4Value.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/** Value type for downstream & upstream IPv4 forwarding maps. */
+public class Tether4Value extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long oif;
+
+ // The ethhdr struct which is defined in uapi/linux/if_ether.h
+ @Field(order = 1, type = Type.EUI48)
+ public final MacAddress ethDstMac;
+ @Field(order = 2, type = Type.EUI48)
+ public final MacAddress ethSrcMac;
+ @Field(order = 3, type = Type.UBE16)
+ public final int ethProto; // Packet type ID field.
+
+ @Field(order = 4, type = Type.U16)
+ public final int pmtu;
+
+ @Field(order = 5, type = Type.ByteArray, arraysize = 16)
+ public final byte[] src46;
+
+ @Field(order = 6, type = Type.ByteArray, arraysize = 16)
+ public final byte[] dst46;
+
+ @Field(order = 7, type = Type.UBE16)
+ public final int srcPort;
+
+ @Field(order = 8, type = Type.UBE16)
+ public final int dstPort;
+
+ // TODO: consider using U64.
+ @Field(order = 9, type = Type.U63)
+ public final long lastUsed;
+
+ public Tether4Value(final long oif, @NonNull final MacAddress ethDstMac,
+ @NonNull final MacAddress ethSrcMac, final int ethProto, final int pmtu,
+ final byte[] src46, final byte[] dst46, final int srcPort,
+ final int dstPort, final long lastUsed) {
+ Objects.requireNonNull(ethDstMac);
+ Objects.requireNonNull(ethSrcMac);
+
+ this.oif = oif;
+ this.ethDstMac = ethDstMac;
+ this.ethSrcMac = ethSrcMac;
+ this.ethProto = ethProto;
+ this.pmtu = pmtu;
+ this.src46 = src46;
+ this.dst46 = dst46;
+ this.srcPort = srcPort;
+ this.dstPort = dstPort;
+ this.lastUsed = lastUsed;
+ }
+
+ @Override
+ public String toString() {
+ try {
+ return String.format(
+ "oif: %d, ethDstMac: %s, ethSrcMac: %s, ethProto: %d, pmtu: %d, "
+ + "src46: %s, dst46: %s, srcPort: %d, dstPort: %d, "
+ + "lastUsed: %d",
+ oif, ethDstMac, ethSrcMac, ethProto, pmtu,
+ InetAddress.getByAddress(src46), InetAddress.getByAddress(dst46),
+ Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort),
+ lastUsed);
+ } catch (UnknownHostException | IllegalArgumentException e) {
+ return String.format("Invalid IP address", e);
+ }
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/Tether6Value.java b/Tethering/src/com/android/networkstack/tethering/Tether6Value.java
new file mode 100644
index 0000000..b3107fd
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/Tether6Value.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+
+import java.util.Objects;
+
+/** Value type for downstream and upstream IPv6 forwarding maps. */
+public class Tether6Value extends Struct {
+ @Field(order = 0, type = Type.S32)
+ public final int oif; // The output interface index.
+
+ // The ethhdr struct which is defined in uapi/linux/if_ether.h
+ @Field(order = 1, type = Type.EUI48)
+ public final MacAddress ethDstMac; // The destination mac address.
+ @Field(order = 2, type = Type.EUI48)
+ public final MacAddress ethSrcMac; // The source mac address.
+ @Field(order = 3, type = Type.UBE16)
+ public final int ethProto; // Packet type ID field.
+
+ @Field(order = 4, type = Type.U16)
+ public final int pmtu; // The maximum L3 output path/route mtu.
+
+ public Tether6Value(final int oif, @NonNull final MacAddress ethDstMac,
+ @NonNull final MacAddress ethSrcMac, final int ethProto, final int pmtu) {
+ Objects.requireNonNull(ethSrcMac);
+ Objects.requireNonNull(ethDstMac);
+
+ this.oif = oif;
+ this.ethDstMac = ethDstMac;
+ this.ethSrcMac = ethSrcMac;
+ this.ethProto = ethProto;
+ this.pmtu = pmtu;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("oif: %d, dstMac: %s, srcMac: %s, proto: %d, pmtu: %d", oif,
+ ethDstMac, ethSrcMac, ethProto, pmtu);
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherDownstream6Key.java b/Tethering/src/com/android/networkstack/tethering/TetherDownstream6Key.java
new file mode 100644
index 0000000..a08ad4a
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherDownstream6Key.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Objects;
+
+/** The key of BpfMap which is used for bpf offload. */
+public class TetherDownstream6Key extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long iif; // The input interface index.
+
+ @Field(order = 1, type = Type.EUI48, padding = 2)
+ public final MacAddress dstMac; // Destination ethernet mac address (zeroed iff rawip ingress).
+
+ @Field(order = 2, type = Type.ByteArray, arraysize = 16)
+ public final byte[] neigh6; // The destination IPv6 address.
+
+ public TetherDownstream6Key(final long iif, @NonNull final MacAddress dstMac,
+ final byte[] neigh6) {
+ Objects.requireNonNull(dstMac);
+
+ try {
+ final Inet6Address unused = (Inet6Address) InetAddress.getByAddress(neigh6);
+ } catch (ClassCastException | UnknownHostException e) {
+ throw new IllegalArgumentException("Invalid IPv6 address: "
+ + Arrays.toString(neigh6));
+ }
+ this.iif = iif;
+ this.dstMac = dstMac;
+ this.neigh6 = neigh6;
+ }
+
+ @Override
+ public String toString() {
+ try {
+ return String.format("iif: %d, dstMac: %s, neigh: %s", iif, dstMac,
+ Inet6Address.getByAddress(neigh6));
+ } catch (UnknownHostException e) {
+ // Should not happen because construtor already verify neigh6.
+ throw new IllegalStateException("Invalid TetherDownstream6Key");
+ }
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java b/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java
new file mode 100644
index 0000000..bc9bb47
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/** The key of BpfMap which is used for tethering per-interface limit. */
+public class TetherLimitKey extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long ifindex; // upstream interface index
+
+ public TetherLimitKey(final long ifindex) {
+ this.ifindex = ifindex;
+ }
+
+ // TODO: remove equals, hashCode and toString once aosp/1536721 is merged.
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+
+ if (!(obj instanceof TetherLimitKey)) return false;
+
+ final TetherLimitKey that = (TetherLimitKey) obj;
+
+ return ifindex == that.ifindex;
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(ifindex);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ifindex: %d", ifindex);
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java b/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java
new file mode 100644
index 0000000..ed7e7d4
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/** The value of BpfMap which is used for tethering per-interface limit. */
+public class TetherLimitValue extends Struct {
+ // Use the signed long variable to store the int64 limit on limit BPF map.
+ // S64 is enough for each interface limit even at 5Gbps for ~468 years.
+ // 2^63 / (5 * 1000 * 1000 * 1000) * 8 / 86400 / 365 = 468.
+ // Note that QUOTA_UNLIMITED (-1) indicates there is no limit.
+ @Field(order = 0, type = Type.S64)
+ public final long limit;
+
+ public TetherLimitValue(final long limit) {
+ this.limit = limit;
+ }
+
+ // TODO: remove equals, hashCode and toString once aosp/1536721 is merged.
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+
+ if (!(obj instanceof TetherLimitValue)) return false;
+
+ final TetherLimitValue that = (TetherLimitValue) obj;
+
+ return limit == that.limit;
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(limit);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("limit: %d", limit);
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java b/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java
new file mode 100644
index 0000000..5442480
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/** The key of BpfMap which is used for tethering stats. */
+public class TetherStatsKey extends Struct {
+ @Field(order = 0, type = Type.U32)
+ public final long ifindex; // upstream interface index
+
+ public TetherStatsKey(final long ifindex) {
+ this.ifindex = ifindex;
+ }
+
+ // TODO: remove equals, hashCode and toString once aosp/1536721 is merged.
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+
+ if (!(obj instanceof TetherStatsKey)) return false;
+
+ final TetherStatsKey that = (TetherStatsKey) obj;
+
+ return ifindex == that.ifindex;
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(ifindex);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("ifindex: %d", ifindex);
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java b/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java
new file mode 100644
index 0000000..844d2e8
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+/** The key of BpfMap which is used for tethering stats. */
+public class TetherStatsValue extends Struct {
+ // Use the signed long variable to store the uint64 stats from stats BPF map.
+ // U63 is enough for each data element even at 5Gbps for ~468 years.
+ // 2^63 / (5 * 1000 * 1000 * 1000) * 8 / 86400 / 365 = 468.
+ @Field(order = 0, type = Type.U63)
+ public final long rxPackets;
+ @Field(order = 1, type = Type.U63)
+ public final long rxBytes;
+ @Field(order = 2, type = Type.U63)
+ public final long rxErrors;
+ @Field(order = 3, type = Type.U63)
+ public final long txPackets;
+ @Field(order = 4, type = Type.U63)
+ public final long txBytes;
+ @Field(order = 5, type = Type.U63)
+ public final long txErrors;
+
+ public TetherStatsValue(final long rxPackets, final long rxBytes, final long rxErrors,
+ final long txPackets, final long txBytes, final long txErrors) {
+ this.rxPackets = rxPackets;
+ this.rxBytes = rxBytes;
+ this.rxErrors = rxErrors;
+ this.txPackets = txPackets;
+ this.txBytes = txBytes;
+ this.txErrors = txErrors;
+ }
+
+ // TODO: remove equals, hashCode and toString once aosp/1536721 is merged.
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+
+ if (!(obj instanceof TetherStatsValue)) return false;
+
+ final TetherStatsValue that = (TetherStatsValue) obj;
+
+ return rxPackets == that.rxPackets
+ && rxBytes == that.rxBytes
+ && rxErrors == that.rxErrors
+ && txPackets == that.txPackets
+ && txBytes == that.txBytes
+ && txErrors == that.txErrors;
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(rxPackets) ^ Long.hashCode(rxBytes) ^ Long.hashCode(rxErrors)
+ ^ Long.hashCode(txPackets) ^ Long.hashCode(txBytes) ^ Long.hashCode(txErrors);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("rxPackets: %s, rxBytes: %s, rxErrors: %s, txPackets: %s, "
+ + "txBytes: %s, txErrors: %s", rxPackets, rxBytes, rxErrors, txPackets,
+ txBytes, txErrors);
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java b/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java
new file mode 100644
index 0000000..5893885
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering;
+
+import android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+
+import java.util.Objects;
+
+/** Key type for upstream IPv6 forwarding map. */
+public class TetherUpstream6Key extends Struct {
+ @Field(order = 0, type = Type.S32)
+ public final int iif; // The input interface index.
+
+ @Field(order = 1, type = Type.EUI48, padding = 2)
+ public final MacAddress dstMac; // Destination ethernet mac address (zeroed iff rawip ingress).
+
+ public TetherUpstream6Key(int iif, @NonNull final MacAddress dstMac) {
+ Objects.requireNonNull(dstMac);
+
+ this.iif = iif;
+ this.dstMac = dstMac;
+ }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 0cf6fcd..1815ff3 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -93,7 +93,6 @@
import android.net.TetheringRequestParcel;
import android.net.ip.IpServer;
import android.net.shared.NetdUtils;
-import android.net.util.BaseNetdUnsolicitedEventListener;
import android.net.util.InterfaceSet;
import android.net.util.PrefixUtils;
import android.net.util.SharedLog;
@@ -132,6 +131,7 @@
import com.android.internal.util.MessageUtils;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -442,7 +442,8 @@
// NOTE: This is always invoked on the mLooper thread.
private void updateConfiguration() {
mConfig = mDeps.generateTetheringConfiguration(mContext, mLog, mActiveDataSubId);
- mUpstreamNetworkMonitor.updateMobileRequiresDun(mConfig.isDunRequired);
+ mUpstreamNetworkMonitor.setUpstreamConfig(mConfig.chooseUpstreamAutomatically,
+ mConfig.isDunRequired);
reportConfigurationChanged(mConfig.toStableParcelable());
}
@@ -1571,7 +1572,7 @@
config.preferredUpstreamIfaceTypes);
if (ns == null) {
if (tryCell) {
- mUpstreamNetworkMonitor.registerMobileNetworkRequest();
+ mUpstreamNetworkMonitor.setTryCell(true);
// We think mobile should be coming up; don't set a retry.
} else {
sendMessageDelayed(CMD_RETRY_UPSTREAM, UPSTREAM_SETTLE_TIME_MS);
@@ -1648,6 +1649,13 @@
protected void handleNewUpstreamNetworkState(UpstreamNetworkState ns) {
mIPv6TetheringCoordinator.updateUpstreamNetworkState(ns);
mOffload.updateUpstreamNetworkState(ns);
+
+ // TODO: Delete all related offload rules which are using this upstream.
+ if (ns != null) {
+ // Add upstream index to the map. The upstream interface index is required while
+ // the conntrack event builds the offload rules.
+ mBpfCoordinator.addUpstreamIfindexToMap(ns.linkProperties);
+ }
}
private void handleInterfaceServingStateActive(int mode, IpServer who) {
@@ -1694,12 +1702,14 @@
// If this is a Wi-Fi interface, tell WifiManager of any errors
// or the inactive serving state.
if (who.interfaceType() == TETHERING_WIFI) {
- if (who.lastError() != TETHER_ERROR_NO_ERROR) {
- getWifiManager().updateInterfaceIpState(
- who.interfaceName(), IFACE_IP_MODE_CONFIGURATION_ERROR);
+ final WifiManager mgr = getWifiManager();
+ final String iface = who.interfaceName();
+ if (mgr == null) {
+ Log.wtf(TAG, "Skipping WifiManager notification about inactive tethering");
+ } else if (who.lastError() != TETHER_ERROR_NO_ERROR) {
+ mgr.updateInterfaceIpState(iface, IFACE_IP_MODE_CONFIGURATION_ERROR);
} else {
- getWifiManager().updateInterfaceIpState(
- who.interfaceName(), IFACE_IP_MODE_UNSPECIFIED);
+ mgr.updateInterfaceIpState(iface, IFACE_IP_MODE_UNSPECIFIED);
}
}
}
@@ -1721,6 +1731,12 @@
break;
}
+ if (mConfig.chooseUpstreamAutomatically
+ && arg1 == UpstreamNetworkMonitor.EVENT_DEFAULT_SWITCHED) {
+ chooseUpstreamType(true);
+ return;
+ }
+
if (ns == null || !pertainsToCurrentUpstream(ns)) {
// TODO: In future, this is where upstream evaluation and selection
// could be handled for notifications which include sufficient data.
@@ -1855,7 +1871,7 @@
// longer desired, release any mobile requests.
final boolean previousUpstreamWanted = updateUpstreamWanted();
if (previousUpstreamWanted && !mUpstreamWanted) {
- mUpstreamNetworkMonitor.releaseMobileNetworkRequest();
+ mUpstreamNetworkMonitor.setTryCell(false);
}
break;
}
@@ -2230,6 +2246,13 @@
&& !isProvisioningNeededButUnavailable();
}
+ private void dumpBpf(IndentingPrintWriter pw) {
+ pw.println("BPF offload:");
+ pw.increaseIndent();
+ mBpfCoordinator.dump(pw);
+ pw.decreaseIndent();
+ }
+
void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer, @Nullable String[] args) {
// Binder.java closes the resource for us.
@SuppressWarnings("resource")
@@ -2240,6 +2263,11 @@
return;
}
+ if (argsContain(args, "bpf")) {
+ dumpBpf(pw);
+ return;
+ }
+
pw.println("Tethering:");
pw.increaseIndent();
@@ -2291,10 +2319,7 @@
mOffloadController.dump(pw);
pw.decreaseIndent();
- pw.println("BPF offload:");
- pw.increaseIndent();
- mBpfCoordinator.dump(pw);
- pw.decreaseIndent();
+ dumpBpf(pw);
pw.println("Private address coordinator:");
pw.increaseIndent();
@@ -2417,6 +2442,19 @@
mLog.log(iface + " is not a tetherable iface, ignoring");
return;
}
+
+ final PackageManager pm = mContext.getPackageManager();
+ if ((interfaceType == TETHERING_WIFI || interfaceType == TETHERING_WIGIG)
+ && !pm.hasSystemFeature(PackageManager.FEATURE_WIFI)) {
+ mLog.log(iface + " is not tetherable, because WiFi feature is disabled");
+ return;
+ }
+ if (interfaceType == TETHERING_WIFI_P2P
+ && !pm.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)) {
+ mLog.log(iface + " is not tetherable, because WiFi Direct feature is disabled");
+ return;
+ }
+
maybeTrackNewInterfaceLocked(iface, interfaceType);
}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index 799637c..413b0cb 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -33,6 +33,8 @@
import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.DeviceConfigUtils;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -94,6 +96,20 @@
"tether_enable_select_all_prefix_ranges";
/**
+ * Experiment flag to force choosing upstreams automatically.
+ *
+ * This setting is intended to help force-enable the feature on OEM devices that disabled it
+ * via resource overlays, and later noticed issues. To that end, it overrides
+ * config_tether_upstream_automatic when set to true.
+ *
+ * This flag is enabled if !=0 and less than the module APK version: see
+ * {@link DeviceConfigUtils#isFeatureEnabled}. It is also ignored after R, as later devices
+ * should just set config_tether_upstream_automatic to true instead.
+ */
+ public static final String TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION =
+ "tether_force_upstream_automatic_version";
+
+ /**
* Default value that used to periodic polls tether offload stats from tethering offload HAL
* to make the data warnings work.
*/
@@ -146,7 +162,9 @@
isDunRequired = checkDunRequired(ctx);
- chooseUpstreamAutomatically = getResourceBoolean(
+ final boolean forceAutomaticUpstream = !SdkLevel.isAtLeastS()
+ && isFeatureEnabled(ctx, TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION);
+ chooseUpstreamAutomatically = forceAutomaticUpstream || getResourceBoolean(
res, R.bool.config_tether_upstream_automatic, false /** defaultValue */);
preferredUpstreamIfaceTypes = getUpstreamIfaceTypes(res, isDunRequired);
@@ -453,6 +471,11 @@
return DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, name);
}
+ @VisibleForTesting
+ protected boolean isFeatureEnabled(Context ctx, String featureVersionFlag) {
+ return DeviceConfigUtils.isFeatureEnabled(ctx, NAMESPACE_CONNECTIVITY, featureVersionFlag);
+ }
+
private Resources getResources(Context ctx, int subId) {
if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
return getResourcesForSubIdWrapper(ctx, subId);
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
index 400b31e..1906ca7 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java
@@ -82,7 +82,6 @@
*/
@VisibleForTesting
public Tethering makeTethering(TetheringDependencies deps) {
- System.loadLibrary("tetherutilsjni");
return new Tethering(deps);
}
diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
index b17065c..f9af777 100644
--- a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
@@ -42,11 +42,15 @@
import android.util.Log;
import android.util.SparseIntArray;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.StateMachine;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Objects;
import java.util.Set;
@@ -60,7 +64,7 @@
* Calling #startObserveAllNetworks() to observe all networks. Listening all
* networks is necessary while the expression of preferred upstreams remains
* a list of legacy connectivity types. In future, this can be revisited.
- * Calling #registerMobileNetworkRequest() to bring up mobile DUN/HIPRI network.
+ * Calling #setTryCell() to request bringing up mobile DUN or HIPRI.
*
* The methods and data members of this class are only to be accessed and
* modified from the tethering main state machine thread. Any other
@@ -82,6 +86,7 @@
public static final int EVENT_ON_CAPABILITIES = 1;
public static final int EVENT_ON_LINKPROPERTIES = 2;
public static final int EVENT_ON_LOST = 3;
+ public static final int EVENT_DEFAULT_SWITCHED = 4;
public static final int NOTIFY_LOCAL_PREFIXES = 10;
// This value is used by deprecated preferredUpstreamIfaceTypes selection which is default
// disabled.
@@ -114,7 +119,14 @@
private NetworkCallback mListenAllCallback;
private NetworkCallback mDefaultNetworkCallback;
private NetworkCallback mMobileNetworkCallback;
+
+ /** Whether Tethering has requested a cellular upstream. */
+ private boolean mTryCell;
+ /** Whether the carrier requires DUN. */
private boolean mDunRequired;
+ /** Whether automatic upstream selection is enabled. */
+ private boolean mAutoUpstream;
+
// Whether the current default upstream is mobile or not.
private boolean mIsDefaultCellularUpstream;
// The current system default network (not really used yet).
@@ -190,23 +202,49 @@
mNetworkMap.clear();
}
- /** Setup or teardown DUN connection according to |dunRequired|. */
- public void updateMobileRequiresDun(boolean dunRequired) {
- final boolean valueChanged = (mDunRequired != dunRequired);
+ private void reevaluateUpstreamRequirements(boolean tryCell, boolean autoUpstream,
+ boolean dunRequired) {
+ final boolean mobileRequestRequired = tryCell && (dunRequired || !autoUpstream);
+ final boolean dunRequiredChanged = (mDunRequired != dunRequired);
+
+ mTryCell = tryCell;
mDunRequired = dunRequired;
- if (valueChanged && mobileNetworkRequested()) {
- releaseMobileNetworkRequest();
+ mAutoUpstream = autoUpstream;
+
+ if (mobileRequestRequired && !mobileNetworkRequested()) {
registerMobileNetworkRequest();
+ } else if (mobileNetworkRequested() && !mobileRequestRequired) {
+ releaseMobileNetworkRequest();
+ } else if (mobileNetworkRequested() && dunRequiredChanged) {
+ releaseMobileNetworkRequest();
+ if (mobileRequestRequired) {
+ registerMobileNetworkRequest();
+ }
}
}
+ /**
+ * Informs UpstreamNetworkMonitor that a cellular upstream is desired.
+ *
+ * This may result in filing a NetworkRequest for DUN if it is required, or for MOBILE_HIPRI if
+ * automatic upstream selection is disabled and MOBILE_HIPRI is the preferred upstream.
+ */
+ public void setTryCell(boolean tryCell) {
+ reevaluateUpstreamRequirements(tryCell, mAutoUpstream, mDunRequired);
+ }
+
+ /** Informs UpstreamNetworkMonitor of upstream configuration parameters. */
+ public void setUpstreamConfig(boolean autoUpstream, boolean dunRequired) {
+ reevaluateUpstreamRequirements(mTryCell, autoUpstream, dunRequired);
+ }
+
/** Whether mobile network is requested. */
public boolean mobileNetworkRequested() {
return (mMobileNetworkCallback != null);
}
/** Request mobile network if mobile upstream is permitted. */
- public void registerMobileNetworkRequest() {
+ private void registerMobileNetworkRequest() {
if (!isCellularUpstreamPermitted()) {
mLog.i("registerMobileNetworkRequest() is not permitted");
releaseMobileNetworkRequest();
@@ -241,14 +279,16 @@
// TODO: Change the timeout from 0 (no onUnavailable callback) to some
// moderate callback timeout. This might be useful for updating some UI.
// Additionally, we log a message to aid in any subsequent debugging.
- mLog.i("requesting mobile upstream network: " + mobileUpstreamRequest);
+ mLog.i("requesting mobile upstream network: " + mobileUpstreamRequest
+ + " mTryCell=" + mTryCell + " mAutoUpstream=" + mAutoUpstream
+ + " mDunRequired=" + mDunRequired);
cm().requestNetwork(mobileUpstreamRequest, 0, legacyType, mHandler,
mMobileNetworkCallback);
}
/** Release mobile network request. */
- public void releaseMobileNetworkRequest() {
+ private void releaseMobileNetworkRequest() {
if (mMobileNetworkCallback == null) return;
cm().unregisterNetworkCallback(mMobileNetworkCallback);
@@ -363,13 +403,20 @@
notifyTarget(EVENT_ON_CAPABILITIES, network);
}
- private void handleLinkProp(Network network, LinkProperties newLp) {
+ private @Nullable UpstreamNetworkState updateLinkProperties(@NonNull Network network,
+ LinkProperties newLp) {
final UpstreamNetworkState prev = mNetworkMap.get(network);
if (prev == null || newLp.equals(prev.linkProperties)) {
// Ignore notifications about networks for which we have not yet
// received onAvailable() (should never happen) and any duplicate
// notifications (e.g. matching more than one of our callbacks).
- return;
+ //
+ // Also, it can happen that onLinkPropertiesChanged is called after
+ // onLost removed the state from mNetworkMap. This appears to be due
+ // to a bug in disconnectAndDestroyNetwork, which calls
+ // nai.clatd.update() after the onLost callbacks.
+ // TODO: fix the bug and make this method void.
+ return null;
}
if (VDBG) {
@@ -377,11 +424,17 @@
network, newLp));
}
- mNetworkMap.put(network, new UpstreamNetworkState(
- newLp, prev.networkCapabilities, network));
- // TODO: If sufficient information is available to select a more
- // preferable upstream, do so now and notify the target.
- notifyTarget(EVENT_ON_LINKPROPERTIES, network);
+ final UpstreamNetworkState ns = new UpstreamNetworkState(newLp, prev.networkCapabilities,
+ network);
+ mNetworkMap.put(network, ns);
+ return ns;
+ }
+
+ private void handleLinkProp(Network network, LinkProperties newLp) {
+ final UpstreamNetworkState ns = updateLinkProperties(network, newLp);
+ if (ns != null) {
+ notifyTarget(EVENT_ON_LINKPROPERTIES, ns);
+ }
}
private void handleLost(Network network) {
@@ -410,6 +463,24 @@
notifyTarget(EVENT_ON_LOST, mNetworkMap.remove(network));
}
+ private void maybeHandleNetworkSwitch(@NonNull Network network) {
+ if (Objects.equals(mDefaultInternetNetwork, network)) return;
+
+ final UpstreamNetworkState ns = mNetworkMap.get(network);
+ if (ns == null) {
+ // Can never happen unless there is a bug in ConnectivityService. Entries are only
+ // removed from mNetworkMap when receiving onLost, and onLost for a given network can
+ // never be followed by any other callback on that network.
+ Log.wtf(TAG, "maybeHandleNetworkSwitch: no UpstreamNetworkState for " + network);
+ return;
+ }
+
+ // Default network changed. Update local data and notify tethering.
+ Log.d(TAG, "New default Internet network: " + network);
+ mDefaultInternetNetwork = network;
+ notifyTarget(EVENT_DEFAULT_SWITCHED, ns);
+ }
+
private void recomputeLocalPrefixes() {
final HashSet<IpPrefix> localPrefixes = allLocalPrefixes(mNetworkMap.values());
if (!mLocalPrefixes.equals(localPrefixes)) {
@@ -447,7 +518,22 @@
@Override
public void onCapabilitiesChanged(Network network, NetworkCapabilities newNc) {
if (mCallbackType == CALLBACK_DEFAULT_INTERNET) {
- mDefaultInternetNetwork = network;
+ // mDefaultInternetNetwork is not updated here because upstream selection must only
+ // run when the LinkProperties have been updated as well as the capabilities. If
+ // this callback is due to a default network switch, then the system will invoke
+ // onLinkPropertiesChanged right after this method and mDefaultInternetNetwork will
+ // be updated then.
+ //
+ // Technically, not updating here isn't necessary, because the notifications to
+ // Tethering sent by notifyTarget are messages sent to a state machine running on
+ // the same thread as this method, and so cannot arrive until after this method has
+ // returned. However, it is not a good idea to rely on that because fact that
+ // Tethering uses multiple state machines running on the same thread is a major
+ // source of race conditions and something that should be fixed.
+ //
+ // TODO: is it correct that this code always updates EntitlementManager?
+ // This code runs when the default network connects or changes capabilities, but the
+ // default network might not be the tethering upstream.
final boolean newIsCellular = isCellular(newNc);
if (mIsDefaultCellularUpstream != newIsCellular) {
mIsDefaultCellularUpstream = newIsCellular;
@@ -461,7 +547,15 @@
@Override
public void onLinkPropertiesChanged(Network network, LinkProperties newLp) {
- if (mCallbackType == CALLBACK_DEFAULT_INTERNET) return;
+ if (mCallbackType == CALLBACK_DEFAULT_INTERNET) {
+ updateLinkProperties(network, newLp);
+ // When the default network callback calls onLinkPropertiesChanged, it means that
+ // all the network information for the default network is known (because
+ // onLinkPropertiesChanged is called after onAvailable and onCapabilitiesChanged).
+ // Inform tethering that the default network might have changed.
+ maybeHandleNetworkSwitch(network);
+ return;
+ }
handleLinkProp(network, newLp);
// Any non-LISTEN_ALL callback will necessarily concern a network that will
@@ -478,6 +572,8 @@
mDefaultInternetNetwork = null;
mIsDefaultCellularUpstream = false;
mEntitlementMgr.notifyUpstream(false);
+ Log.d(TAG, "Lost default Internet network: " + network);
+ notifyTarget(EVENT_DEFAULT_SWITCHED, null);
return;
}
diff --git a/Tethering/tests/Android.bp b/Tethering/tests/Android.bp
index 731144c..8f31c57 100644
--- a/Tethering/tests/Android.bp
+++ b/Tethering/tests/Android.bp
@@ -14,6 +14,10 @@
// limitations under the License.
//
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
filegroup {
name: "TetheringTestsJarJarRules",
srcs: ["jarjar-rules.txt"],
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index 42f83bf..f63df2c 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -13,6 +13,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
java_defaults {
name: "TetheringIntegrationTestsDefaults",
srcs: [
@@ -70,6 +74,7 @@
test_config: "AndroidTest_Coverage.xml",
defaults: ["libnetworkstackutilsjni_deps"],
static_libs: [
+ "NetdStaticLibTestsLib",
"NetworkStaticLibTestsLib",
"NetworkStackTestsLib",
"TetheringTestsLib",
@@ -81,6 +86,7 @@
"libstaticjvmtiagent",
// For NetworkStackUtils included in NetworkStackBase
"libnetworkstackutilsjni",
+ "libtetherutilsjni",
],
jarjar_rules: ":TetheringTestsJarJarRules",
compile_multilib: "both",
diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp
index f925b0a..edb6356 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
android_test {
// This tests for functionality that is not required for devices that
// don't use Tethering mainline module.
@@ -48,7 +52,7 @@
// Tag this module as a mts test artifact
test_suites: [
"general-tests",
- "mts",
+ "mts-tethering",
],
// Include both the 32 and 64 bit versions
diff --git a/Tethering/tests/privileged/Android.bp b/Tethering/tests/privileged/Android.bp
index 9217345..75fdd6e 100644
--- a/Tethering/tests/privileged/Android.bp
+++ b/Tethering/tests/privileged/Android.bp
@@ -14,6 +14,10 @@
// limitations under the License.
//
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
java_defaults {
name: "TetheringPrivilegedTestsJniDefaults",
jni_libs: [
diff --git a/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java b/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java
index 42a91aa..a933e1b 100644
--- a/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java
+++ b/Tethering/tests/privileged/src/android/net/ip/DadProxyTest.java
@@ -21,9 +21,8 @@
import static com.android.net.module.util.IpUtils.icmpv6Checksum;
import static com.android.net.module.util.NetworkStackConstants.ETHER_SRC_ADDR_OFFSET;
-import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
import android.app.Instrumentation;
import android.content.Context;
@@ -52,13 +51,14 @@
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
+import java.io.IOException;
import java.nio.ByteBuffer;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class DadProxyTest {
private static final int DATA_BUFFER_LEN = 4096;
- private static final int PACKET_TIMEOUT_MS = 5_000;
+ private static final int PACKET_TIMEOUT_MS = 2_000; // Long enough for DAD to succeed.
// Start the readers manually on a common handler shared with DadProxy, for simplicity
@Rule
@@ -119,16 +119,18 @@
}
}
- private void setupTapInterfaces() {
+ private void setupTapInterfaces() throws Exception {
// Create upstream test iface.
mUpstreamReader.start(mHandler);
- mUpstreamParams = InterfaceParams.getByName(mUpstreamReader.iface.getInterfaceName());
+ final String upstreamIface = mUpstreamReader.iface.getInterfaceName();
+ mUpstreamParams = InterfaceParams.getByName(upstreamIface);
assertNotNull(mUpstreamParams);
mUpstreamPacketReader = mUpstreamReader.getReader();
// Create tethered test iface.
mTetheredReader.start(mHandler);
- mTetheredParams = InterfaceParams.getByName(mTetheredReader.getIface().getInterfaceName());
+ final String tetheredIface = mTetheredReader.getIface().getInterfaceName();
+ mTetheredParams = InterfaceParams.getByName(tetheredIface);
assertNotNull(mTetheredParams);
mTetheredPacketReader = mTetheredReader.getReader();
}
@@ -224,6 +226,12 @@
return false;
}
+ private ByteBuffer copy(ByteBuffer buf) {
+ // There does not seem to be a way to copy ByteBuffers. ByteBuffer does not implement
+ // clone() and duplicate() copies the metadata but shares the contents.
+ return ByteBuffer.wrap(buf.array().clone());
+ }
+
private void updateDstMac(ByteBuffer buf, MacAddress mac) {
buf.put(mac.toByteArray());
buf.rewind();
@@ -234,14 +242,50 @@
buf.rewind();
}
+ private void receivePacketAndMaybeExpectForwarded(boolean expectForwarded,
+ ByteBuffer in, TapPacketReader inReader, ByteBuffer out, TapPacketReader outReader)
+ throws IOException {
+
+ inReader.sendResponse(in);
+ if (waitForPacket(out, outReader)) return;
+
+ // When the test runs, DAD may be in progress, because the interface has just been created.
+ // If so, the DAD proxy will get EADDRNOTAVAIL when trying to send packets. It is not
+ // possible to work around this using IPV6_FREEBIND or IPV6_TRANSPARENT options because the
+ // kernel rawv6 code doesn't consider those options either when binding or when sending, and
+ // doesn't get the source address from the packet even in IPPROTO_RAW/HDRINCL mode (it only
+ // gets it from the socket or from cmsg).
+ //
+ // If DAD was in progress when the above was attempted, try again and expect the packet to
+ // be forwarded. Don't disable DAD in the test because if we did, the test would not notice
+ // if, for example, the DAD proxy code just crashed if it received EADDRNOTAVAIL.
+ final String msg = expectForwarded
+ ? "Did not receive expected packet even after waiting for DAD:"
+ : "Unexpectedly received packet:";
+
+ inReader.sendResponse(in);
+ assertEquals(msg, expectForwarded, waitForPacket(out, outReader));
+ }
+
+ private void receivePacketAndExpectForwarded(ByteBuffer in, TapPacketReader inReader,
+ ByteBuffer out, TapPacketReader outReader) throws IOException {
+ receivePacketAndMaybeExpectForwarded(true, in, inReader, out, outReader);
+ }
+
+ private void receivePacketAndExpectNotForwarded(ByteBuffer in, TapPacketReader inReader,
+ ByteBuffer out, TapPacketReader outReader) throws IOException {
+ receivePacketAndMaybeExpectForwarded(false, in, inReader, out, outReader);
+ }
+
@Test
public void testNaForwardingFromUpstreamToTether() throws Exception {
ByteBuffer na = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT);
- mUpstreamPacketReader.sendResponse(na);
- updateDstMac(na, MacAddress.fromString("33:33:00:00:00:01"));
- updateSrcMac(na, mTetheredParams);
- assertTrue(waitForPacket(na, mTetheredPacketReader));
+ ByteBuffer out = copy(na);
+ updateDstMac(out, MacAddress.fromString("33:33:00:00:00:01"));
+ updateSrcMac(out, mTetheredParams);
+
+ receivePacketAndExpectForwarded(na, mUpstreamPacketReader, out, mTetheredPacketReader);
}
@Test
@@ -249,19 +293,21 @@
public void testNaForwardingFromTetherToUpstream() throws Exception {
ByteBuffer na = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_ADVERTISEMENT);
- mTetheredPacketReader.sendResponse(na);
- updateDstMac(na, MacAddress.fromString("33:33:00:00:00:01"));
- updateSrcMac(na, mTetheredParams);
- assertFalse(waitForPacket(na, mUpstreamPacketReader));
+ ByteBuffer out = copy(na);
+ updateDstMac(out, MacAddress.fromString("33:33:00:00:00:01"));
+ updateSrcMac(out, mTetheredParams);
+
+ receivePacketAndExpectNotForwarded(na, mTetheredPacketReader, out, mUpstreamPacketReader);
}
@Test
public void testNsForwardingFromTetherToUpstream() throws Exception {
ByteBuffer ns = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION);
- mTetheredPacketReader.sendResponse(ns);
- updateSrcMac(ns, mUpstreamParams);
- assertTrue(waitForPacket(ns, mUpstreamPacketReader));
+ ByteBuffer out = copy(ns);
+ updateSrcMac(out, mUpstreamParams);
+
+ receivePacketAndExpectForwarded(ns, mTetheredPacketReader, out, mUpstreamPacketReader);
}
@Test
@@ -269,8 +315,9 @@
public void testNsForwardingFromUpstreamToTether() throws Exception {
ByteBuffer ns = createDadPacket(NeighborPacketForwarder.ICMPV6_NEIGHBOR_SOLICITATION);
- mUpstreamPacketReader.sendResponse(ns);
+ ByteBuffer out = copy(ns);
updateSrcMac(ns, mUpstreamParams);
- assertFalse(waitForPacket(ns, mTetheredPacketReader));
+
+ receivePacketAndExpectNotForwarded(ns, mUpstreamPacketReader, out, mTetheredPacketReader);
}
}
diff --git a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
new file mode 100644
index 0000000..1d94214
--- /dev/null
+++ b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.ip;
+
+import static android.net.RouteInfo.RTN_UNICAST;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_MTU;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_RDNSS;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_RA_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS;
+import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.net.INetd;
+import android.net.IpPrefix;
+import android.net.MacAddress;
+import android.net.RouteInfo;
+import android.net.ip.RouterAdvertisementDaemon.RaParams;
+import android.net.shared.RouteUtils;
+import android.net.util.InterfaceParams;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
+import com.android.net.module.util.structs.LlaOption;
+import com.android.net.module.util.structs.MtuOption;
+import com.android.net.module.util.structs.PrefixInformationOption;
+import com.android.net.module.util.structs.RaHeader;
+import com.android.net.module.util.structs.RdnssOption;
+import com.android.testutils.TapPacketReader;
+import com.android.testutils.TapPacketReaderRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.HashSet;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public final class RouterAdvertisementDaemonTest {
+ private static final String TAG = RouterAdvertisementDaemonTest.class.getSimpleName();
+ private static final int DATA_BUFFER_LEN = 4096;
+ private static final int PACKET_TIMEOUT_MS = 5_000;
+
+ @Rule
+ public final TapPacketReaderRule mTetheredReader = new TapPacketReaderRule(
+ DATA_BUFFER_LEN, false /* autoStart */);
+
+ private InterfaceParams mTetheredParams;
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+ private TapPacketReader mTetheredPacketReader;
+ private RouterAdvertisementDaemon mRaDaemon;
+
+ private static INetd sNetd;
+
+ @BeforeClass
+ public static void setupOnce() {
+ final Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+ final IBinder netdIBinder =
+ (IBinder) inst.getContext().getSystemService(Context.NETD_SERVICE);
+ sNetd = INetd.Stub.asInterface(netdIBinder);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mHandlerThread = new HandlerThread(getClass().getSimpleName());
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+
+ setupTapInterfaces();
+
+ // Looper must be prepared here since AndroidJUnitRunner runs tests on separate threads.
+ if (Looper.myLooper() == null) Looper.prepare();
+
+ mRaDaemon = new RouterAdvertisementDaemon(mTetheredParams);
+ sNetd.networkAddInterface(INetd.LOCAL_NET_ID, mTetheredParams.name);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mTetheredReader.stop();
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread.join(PACKET_TIMEOUT_MS);
+ }
+
+ if (mTetheredParams != null) {
+ sNetd.networkRemoveInterface(INetd.LOCAL_NET_ID, mTetheredParams.name);
+ }
+ }
+
+ private void setupTapInterfaces() {
+ // Create tethered test iface.
+ mTetheredReader.start(mHandler);
+ mTetheredParams = InterfaceParams.getByName(mTetheredReader.iface.getInterfaceName());
+ assertNotNull(mTetheredParams);
+ mTetheredPacketReader = mTetheredReader.getReader();
+ mHandler.post(mTetheredPacketReader::start);
+ }
+
+ private class TestRaPacket {
+ final RaParams mNewParams, mOldParams;
+
+ TestRaPacket(final RaParams oldParams, final RaParams newParams) {
+ mOldParams = oldParams;
+ mNewParams = newParams;
+ }
+
+ public boolean isPacketMatched(final byte[] pkt, boolean multicast) throws Exception {
+ if (pkt.length < (ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_RA_HEADER_LEN)) {
+ return false;
+ }
+ final ByteBuffer buf = ByteBuffer.wrap(pkt);
+
+ // Parse Ethernet header
+ final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
+ if (ethHdr.etherType != ETHER_TYPE_IPV6) return false;
+
+ // Parse IPv6 header
+ final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
+ assertEquals((ipv6Hdr.vtf >> 28), 6 /* ip version*/);
+
+ final int payLoadLength = pkt.length - ETHER_HEADER_LEN - IPV6_HEADER_LEN;
+ assertEquals(payLoadLength, ipv6Hdr.payloadLength);
+
+ // Parse ICMPv6 header
+ final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf);
+ if (icmpv6Hdr.type != (short) ICMPV6_ROUTER_ADVERTISEMENT) return false;
+
+ // Check whether IPv6 destination address is multicast or unicast
+ if (multicast) {
+ assertEquals(ipv6Hdr.dstIp, IPV6_ADDR_ALL_NODES_MULTICAST);
+ } else {
+ // The unicast IPv6 destination address in RA can be either link-local or global
+ // IPv6 address. This test only expects link-local address.
+ assertTrue(ipv6Hdr.dstIp.isLinkLocalAddress());
+ }
+
+ // Parse RA header
+ final RaHeader raHdr = Struct.parse(RaHeader.class, buf);
+ assertEquals(mNewParams.hopLimit, raHdr.hopLimit);
+
+ while (buf.position() < pkt.length) {
+ final int currentPos = buf.position();
+ final int type = Byte.toUnsignedInt(buf.get());
+ final int length = Byte.toUnsignedInt(buf.get());
+ switch (type) {
+ case ICMPV6_ND_OPTION_PIO:
+ // length is 4 because this test only expects one PIO included in the
+ // router advertisement packet.
+ assertEquals(4, length);
+
+ final ByteBuffer pioBuf = ByteBuffer.wrap(buf.array(), currentPos,
+ Struct.getSize(PrefixInformationOption.class));
+ final PrefixInformationOption pio =
+ Struct.parse(PrefixInformationOption.class, pioBuf);
+ assertEquals((byte) (PIO_FLAG_ON_LINK | PIO_FLAG_AUTONOMOUS), pio.flags);
+
+ final InetAddress address = InetAddress.getByAddress(pio.prefix);
+ final IpPrefix prefix = new IpPrefix(address, pio.prefixLen);
+ if (mNewParams.prefixes.contains(prefix)) {
+ assertTrue(pio.validLifetime > 0);
+ assertTrue(pio.preferredLifetime > 0);
+ } else if (mOldParams != null && mOldParams.prefixes.contains(prefix)) {
+ assertEquals(0, pio.validLifetime);
+ assertEquals(0, pio.preferredLifetime);
+ } else {
+ fail("Unexpected prefix: " + prefix);
+ }
+
+ // Move ByteBuffer position to the next option.
+ buf.position(currentPos + Struct.getSize(PrefixInformationOption.class));
+ break;
+ case ICMPV6_ND_OPTION_MTU:
+ assertEquals(1, length);
+
+ final ByteBuffer mtuBuf = ByteBuffer.wrap(buf.array(), currentPos,
+ Struct.getSize(MtuOption.class));
+ final MtuOption mtu = Struct.parse(MtuOption.class, mtuBuf);
+ assertEquals(mNewParams.mtu, mtu.mtu);
+
+ // Move ByteBuffer position to the next option.
+ buf.position(currentPos + Struct.getSize(MtuOption.class));
+ break;
+ case ICMPV6_ND_OPTION_RDNSS:
+ final int rdnssHeaderLen = Struct.getSize(RdnssOption.class);
+ final ByteBuffer RdnssBuf = ByteBuffer.wrap(buf.array(), currentPos,
+ rdnssHeaderLen);
+ final RdnssOption rdnss = Struct.parse(RdnssOption.class, RdnssBuf);
+ final String msg =
+ rdnss.lifetime > 0 ? "Unknown dns" : "Unknown deprecated dns";
+ final HashSet<Inet6Address> dnses =
+ rdnss.lifetime > 0 ? mNewParams.dnses : mOldParams.dnses;
+ assertNotNull(msg, dnses);
+
+ // Check DNS servers included in this option.
+ buf.position(currentPos + rdnssHeaderLen); // skip the rdnss option header
+ final int numOfDnses = (length - 1) / 2;
+ for (int i = 0; i < numOfDnses; i++) {
+ byte[] rawAddress = new byte[IPV6_ADDR_LEN];
+ buf.get(rawAddress);
+ final Inet6Address dns =
+ (Inet6Address) InetAddress.getByAddress(rawAddress);
+ if (!dnses.contains(dns)) fail("Unexpected dns: " + dns);
+ }
+ // Unnecessary to move ByteBuffer position here, since the position has been
+ // moved forward correctly after reading DNS servers from ByteBuffer.
+ break;
+ case ICMPV6_ND_OPTION_SLLA:
+ // Do nothing, just move ByteBuffer position to the next option.
+ buf.position(currentPos + Struct.getSize(LlaOption.class));
+ break;
+ default:
+ fail("Unknown RA option type " + type);
+ }
+ }
+ return true;
+ }
+ }
+
+ private RaParams createRaParams(final String ipv6Address) throws Exception {
+ final RaParams params = new RaParams();
+ final Inet6Address address = (Inet6Address) InetAddress.getByName(ipv6Address);
+ params.dnses.add(address);
+ params.prefixes.add(new IpPrefix(address, 64));
+
+ return params;
+ }
+
+ private boolean isRaPacket(final TestRaPacket testRa, boolean multicast) throws Exception {
+ byte[] packet;
+ while ((packet = mTetheredPacketReader.poll(PACKET_TIMEOUT_MS)) != null) {
+ if (testRa.isPacketMatched(packet, multicast)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void assertUnicastRaPacket(final TestRaPacket testRa) throws Exception {
+ assertTrue(isRaPacket(testRa, false /* multicast */));
+ }
+
+ private void assertMulticastRaPacket(final TestRaPacket testRa) throws Exception {
+ assertTrue(isRaPacket(testRa, true /* multicast */));
+ }
+
+ private ByteBuffer createRsPacket(final String srcIp) throws Exception {
+ final MacAddress dstMac = MacAddress.fromString("33:33:03:04:05:06");
+ final MacAddress srcMac = mTetheredParams.macAddr;
+ final ByteBuffer slla = LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, srcMac);
+
+ return Ipv6Utils.buildRsPacket(srcMac, dstMac, (Inet6Address) InetAddress.getByName(srcIp),
+ IPV6_ADDR_ALL_NODES_MULTICAST, slla);
+ }
+
+ @Test
+ public void testUnSolicitRouterAdvertisement() throws Exception {
+ assertTrue(mRaDaemon.start());
+ final RaParams params1 = createRaParams("2001:1122:3344::5566");
+ mRaDaemon.buildNewRa(null, params1);
+ assertMulticastRaPacket(new TestRaPacket(null, params1));
+
+ final RaParams params2 = createRaParams("2006:3344:5566::7788");
+ mRaDaemon.buildNewRa(params1, params2);
+ assertMulticastRaPacket(new TestRaPacket(params1, params2));
+ }
+
+ @Test
+ public void testSolicitRouterAdvertisement() throws Exception {
+ // Enable IPv6 forwarding is necessary, which makes kernel process RS correctly and
+ // create the neighbor entry for peer's link-layer address and IPv6 address. Otherwise,
+ // when device receives RS with IPv6 link-local address as source address, it has to
+ // initiate the address resolution first before responding the unicast RA.
+ sNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mTetheredParams.name, "forwarding", "1");
+
+ assertTrue(mRaDaemon.start());
+ final RaParams params1 = createRaParams("2001:1122:3344::5566");
+ mRaDaemon.buildNewRa(null, params1);
+ assertMulticastRaPacket(new TestRaPacket(null, params1));
+
+ // Add a default route "fe80::/64 -> ::" to local network, otherwise, device will fail to
+ // send the unicast RA out due to the ENETUNREACH error(No route to the peer's link-local
+ // address is present).
+ final String iface = mTetheredParams.name;
+ final RouteInfo linkLocalRoute =
+ new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST);
+ RouteUtils.addRoutesToLocalNetwork(sNetd, iface, List.of(linkLocalRoute));
+
+ final ByteBuffer rs = createRsPacket("fe80::1122:3344:5566:7788");
+ mTetheredPacketReader.sendResponse(rs);
+ assertUnicastRaPacket(new TestRaPacket(null, params1));
+ }
+}
diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
new file mode 100644
index 0000000..830729d
--- /dev/null
+++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering;
+
+import static android.system.OsConstants.ETH_P_IPV6;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.net.MacAddress;
+import android.os.Build;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.util.ArrayMap;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.InetAddress;
+import java.util.NoSuchElementException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+@RunWith(AndroidJUnit4.class)
+@IgnoreUpTo(Build.VERSION_CODES.R)
+public final class BpfMapTest {
+ // Sync from packages/modules/Connectivity/Tethering/bpf_progs/offload.c.
+ private static final int TEST_MAP_SIZE = 16;
+ private static final String TETHER_DOWNSTREAM6_FS_PATH =
+ "/sys/fs/bpf/tethering/map_test_tether_downstream6_map";
+
+ private ArrayMap<TetherDownstream6Key, Tether6Value> mTestData;
+
+ private BpfMap<TetherDownstream6Key, Tether6Value> mTestMap;
+
+ @BeforeClass
+ public static void setupOnce() {
+ System.loadLibrary("tetherutilsjni");
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mTestData = new ArrayMap<>();
+ mTestData.put(createTetherDownstream6Key(101, "00:00:00:00:00:aa", "2001:db8::1"),
+ createTether6Value(11, "00:00:00:00:00:0a", "11:11:11:00:00:0b",
+ ETH_P_IPV6, 1280));
+ mTestData.put(createTetherDownstream6Key(102, "00:00:00:00:00:bb", "2001:db8::2"),
+ createTether6Value(22, "00:00:00:00:00:0c", "22:22:22:00:00:0d",
+ ETH_P_IPV6, 1400));
+ mTestData.put(createTetherDownstream6Key(103, "00:00:00:00:00:cc", "2001:db8::3"),
+ createTether6Value(33, "00:00:00:00:00:0e", "33:33:33:00:00:0f",
+ ETH_P_IPV6, 1500));
+
+ initTestMap();
+ }
+
+ private void initTestMap() throws Exception {
+ mTestMap = new BpfMap<>(
+ TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
+ TetherDownstream6Key.class, Tether6Value.class);
+
+ mTestMap.forEach((key, value) -> {
+ try {
+ assertTrue(mTestMap.deleteEntry(key));
+ } catch (ErrnoException e) {
+ fail("Fail to delete the key " + key + ": " + e);
+ }
+ });
+ assertNull(mTestMap.getFirstKey());
+ assertTrue(mTestMap.isEmpty());
+ }
+
+ private TetherDownstream6Key createTetherDownstream6Key(long iif, String mac,
+ String address) throws Exception {
+ final MacAddress dstMac = MacAddress.fromString(mac);
+ final InetAddress ipv6Address = InetAddress.getByName(address);
+
+ return new TetherDownstream6Key(iif, dstMac, ipv6Address.getAddress());
+ }
+
+ private Tether6Value createTether6Value(int oif, String src, String dst, int proto, int pmtu) {
+ final MacAddress srcMac = MacAddress.fromString(src);
+ final MacAddress dstMac = MacAddress.fromString(dst);
+
+ return new Tether6Value(oif, dstMac, srcMac, proto, pmtu);
+ }
+
+ @Test
+ public void testGetFd() throws Exception {
+ try (BpfMap readOnlyMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDONLY,
+ TetherDownstream6Key.class, Tether6Value.class)) {
+ assertNotNull(readOnlyMap);
+ try {
+ readOnlyMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0));
+ fail("Writing RO map should throw ErrnoException");
+ } catch (ErrnoException expected) {
+ assertEquals(OsConstants.EPERM, expected.errno);
+ }
+ }
+ try (BpfMap writeOnlyMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_WRONLY,
+ TetherDownstream6Key.class, Tether6Value.class)) {
+ assertNotNull(writeOnlyMap);
+ try {
+ writeOnlyMap.getFirstKey();
+ fail("Reading WO map should throw ErrnoException");
+ } catch (ErrnoException expected) {
+ assertEquals(OsConstants.EPERM, expected.errno);
+ }
+ }
+ try (BpfMap readWriteMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
+ TetherDownstream6Key.class, Tether6Value.class)) {
+ assertNotNull(readWriteMap);
+ }
+ }
+
+ @Test
+ public void testIsEmpty() throws Exception {
+ assertNull(mTestMap.getFirstKey());
+ assertTrue(mTestMap.isEmpty());
+
+ mTestMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0));
+ assertFalse(mTestMap.isEmpty());
+
+ mTestMap.deleteEntry((mTestData.keyAt(0)));
+ assertTrue(mTestMap.isEmpty());
+ }
+
+ @Test
+ public void testGetFirstKey() throws Exception {
+ // getFirstKey on an empty map returns null.
+ assertFalse(mTestMap.containsKey(mTestData.keyAt(0)));
+ assertNull(mTestMap.getFirstKey());
+ assertNull(mTestMap.getValue(mTestData.keyAt(0)));
+
+ // getFirstKey on a non-empty map returns the first key.
+ mTestMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0));
+ assertEquals(mTestData.keyAt(0), mTestMap.getFirstKey());
+ }
+
+ @Test
+ public void testGetNextKey() throws Exception {
+ // [1] If the passed-in key is not found on empty map, return null.
+ final TetherDownstream6Key nonexistentKey =
+ createTetherDownstream6Key(1234, "00:00:00:00:00:01", "2001:db8::10");
+ assertNull(mTestMap.getNextKey(nonexistentKey));
+
+ // [2] If the passed-in key is null on empty map, throw NullPointerException.
+ try {
+ mTestMap.getNextKey(null);
+ fail("Getting next key with null key should throw NullPointerException");
+ } catch (NullPointerException expected) { }
+
+ // The BPF map has one entry now.
+ final ArrayMap<TetherDownstream6Key, Tether6Value> resultMap =
+ new ArrayMap<>();
+ mTestMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0));
+ resultMap.put(mTestData.keyAt(0), mTestData.valueAt(0));
+
+ // [3] If the passed-in key is the last key, return null.
+ // Because there is only one entry in the map, the first key equals the last key.
+ final TetherDownstream6Key lastKey = mTestMap.getFirstKey();
+ assertNull(mTestMap.getNextKey(lastKey));
+
+ // The BPF map has two entries now.
+ mTestMap.insertEntry(mTestData.keyAt(1), mTestData.valueAt(1));
+ resultMap.put(mTestData.keyAt(1), mTestData.valueAt(1));
+
+ // [4] If the passed-in key is found, return the next key.
+ TetherDownstream6Key nextKey = mTestMap.getFirstKey();
+ while (nextKey != null) {
+ if (resultMap.remove(nextKey).equals(nextKey)) {
+ fail("Unexpected result: " + nextKey);
+ }
+ nextKey = mTestMap.getNextKey(nextKey);
+ }
+ assertTrue(resultMap.isEmpty());
+
+ // [5] If the passed-in key is not found on non-empty map, return the first key.
+ assertEquals(mTestMap.getFirstKey(), mTestMap.getNextKey(nonexistentKey));
+
+ // [6] If the passed-in key is null on non-empty map, throw NullPointerException.
+ try {
+ mTestMap.getNextKey(null);
+ fail("Getting next key with null key should throw NullPointerException");
+ } catch (NullPointerException expected) { }
+ }
+
+ @Test
+ public void testUpdateEntry() throws Exception {
+ final TetherDownstream6Key key = mTestData.keyAt(0);
+ final Tether6Value value = mTestData.valueAt(0);
+ final Tether6Value value2 = mTestData.valueAt(1);
+ assertFalse(mTestMap.deleteEntry(key));
+
+ // updateEntry will create an entry if it does not exist already.
+ mTestMap.updateEntry(key, value);
+ assertTrue(mTestMap.containsKey(key));
+ final Tether6Value result = mTestMap.getValue(key);
+ assertEquals(value, result);
+
+ // updateEntry will update an entry that already exists.
+ mTestMap.updateEntry(key, value2);
+ assertTrue(mTestMap.containsKey(key));
+ final Tether6Value result2 = mTestMap.getValue(key);
+ assertEquals(value2, result2);
+
+ assertTrue(mTestMap.deleteEntry(key));
+ assertFalse(mTestMap.containsKey(key));
+ }
+
+ @Test
+ public void testInsertOrReplaceEntry() throws Exception {
+ final TetherDownstream6Key key = mTestData.keyAt(0);
+ final Tether6Value value = mTestData.valueAt(0);
+ final Tether6Value value2 = mTestData.valueAt(1);
+ assertFalse(mTestMap.deleteEntry(key));
+
+ // insertOrReplaceEntry will create an entry if it does not exist already.
+ assertTrue(mTestMap.insertOrReplaceEntry(key, value));
+ assertTrue(mTestMap.containsKey(key));
+ final Tether6Value result = mTestMap.getValue(key);
+ assertEquals(value, result);
+
+ // updateEntry will update an entry that already exists.
+ assertFalse(mTestMap.insertOrReplaceEntry(key, value2));
+ assertTrue(mTestMap.containsKey(key));
+ final Tether6Value result2 = mTestMap.getValue(key);
+ assertEquals(value2, result2);
+
+ assertTrue(mTestMap.deleteEntry(key));
+ assertFalse(mTestMap.containsKey(key));
+ }
+
+ @Test
+ public void testInsertReplaceEntry() throws Exception {
+ final TetherDownstream6Key key = mTestData.keyAt(0);
+ final Tether6Value value = mTestData.valueAt(0);
+ final Tether6Value value2 = mTestData.valueAt(1);
+
+ try {
+ mTestMap.replaceEntry(key, value);
+ fail("Replacing non-existent key " + key + " should throw NoSuchElementException");
+ } catch (NoSuchElementException expected) { }
+ assertFalse(mTestMap.containsKey(key));
+
+ mTestMap.insertEntry(key, value);
+ assertTrue(mTestMap.containsKey(key));
+ final Tether6Value result = mTestMap.getValue(key);
+ assertEquals(value, result);
+ try {
+ mTestMap.insertEntry(key, value);
+ fail("Inserting existing key " + key + " should throw IllegalStateException");
+ } catch (IllegalStateException expected) { }
+
+ mTestMap.replaceEntry(key, value2);
+ assertTrue(mTestMap.containsKey(key));
+ final Tether6Value result2 = mTestMap.getValue(key);
+ assertEquals(value2, result2);
+ }
+
+ @Test
+ public void testIterateBpfMap() throws Exception {
+ final ArrayMap<TetherDownstream6Key, Tether6Value> resultMap =
+ new ArrayMap<>(mTestData);
+
+ for (int i = 0; i < resultMap.size(); i++) {
+ mTestMap.insertEntry(resultMap.keyAt(i), resultMap.valueAt(i));
+ }
+
+ mTestMap.forEach((key, value) -> {
+ if (!value.equals(resultMap.remove(key))) {
+ fail("Unexpected result: " + key + ", value: " + value);
+ }
+ });
+ assertTrue(resultMap.isEmpty());
+ }
+
+ @Test
+ public void testIterateEmptyMap() throws Exception {
+ // Can't use an int because variables used in a lambda must be final.
+ final AtomicInteger count = new AtomicInteger();
+ mTestMap.forEach((key, value) -> count.incrementAndGet());
+ // Expect that the consumer was never called.
+ assertEquals(0, count.get());
+ }
+
+ @Test
+ public void testIterateDeletion() throws Exception {
+ final ArrayMap<TetherDownstream6Key, Tether6Value> resultMap =
+ new ArrayMap<>(mTestData);
+
+ for (int i = 0; i < resultMap.size(); i++) {
+ mTestMap.insertEntry(resultMap.keyAt(i), resultMap.valueAt(i));
+ }
+
+ // Can't use an int because variables used in a lambda must be final.
+ final AtomicInteger count = new AtomicInteger();
+ mTestMap.forEach((key, value) -> {
+ try {
+ assertTrue(mTestMap.deleteEntry(key));
+ } catch (ErrnoException e) {
+ fail("Fail to delete key " + key + ": " + e);
+ }
+ if (!value.equals(resultMap.remove(key))) {
+ fail("Unexpected result: " + key + ", value: " + value);
+ }
+ count.incrementAndGet();
+ });
+ assertEquals(3, count.get());
+ assertTrue(resultMap.isEmpty());
+ assertNull(mTestMap.getFirstKey());
+ }
+
+ @Test
+ public void testClear() throws Exception {
+ // Clear an empty map.
+ assertTrue(mTestMap.isEmpty());
+ mTestMap.clear();
+
+ // Clear a map with some data in it.
+ final ArrayMap<TetherDownstream6Key, Tether6Value> resultMap =
+ new ArrayMap<>(mTestData);
+ for (int i = 0; i < resultMap.size(); i++) {
+ mTestMap.insertEntry(resultMap.keyAt(i), resultMap.valueAt(i));
+ }
+ assertFalse(mTestMap.isEmpty());
+ mTestMap.clear();
+ assertTrue(mTestMap.isEmpty());
+
+ // Clearing an already-closed map throws.
+ mTestMap.close();
+ try {
+ mTestMap.clear();
+ fail("clearing already-closed map should throw");
+ } catch (ErrnoException expected) {
+ assertEquals(OsConstants.EBADF, expected.errno);
+ }
+ }
+
+ @Test
+ public void testInsertOverflow() throws Exception {
+ final ArrayMap<TetherDownstream6Key, Tether6Value> testData =
+ new ArrayMap<>();
+
+ // Build test data for TEST_MAP_SIZE + 1 entries.
+ for (int i = 1; i <= TEST_MAP_SIZE + 1; i++) {
+ testData.put(
+ createTetherDownstream6Key(i, "00:00:00:00:00:01", "2001:db8::1"),
+ createTether6Value(100, "de:ad:be:ef:00:01", "de:ad:be:ef:00:02",
+ ETH_P_IPV6, 1500));
+ }
+
+ // Insert #TEST_MAP_SIZE test entries to the map. The map has reached the limit.
+ for (int i = 0; i < TEST_MAP_SIZE; i++) {
+ mTestMap.insertEntry(testData.keyAt(i), testData.valueAt(i));
+ }
+
+ // The map won't allow inserting any more entries.
+ try {
+ mTestMap.insertEntry(testData.keyAt(TEST_MAP_SIZE), testData.valueAt(TEST_MAP_SIZE));
+ fail("Writing too many entries should throw ErrnoException");
+ } catch (ErrnoException expected) {
+ // Expect that can't insert the entry anymore because the number of elements in the
+ // map reached the limit. See man-pages/bpf.
+ assertEquals(OsConstants.E2BIG, expected.errno);
+ }
+ }
+}
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index 5e4fe52..d469b37 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -15,6 +15,10 @@
//
// Tests in this folder are included both in unit tests and CTS.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
java_library {
name: "TetheringCommonTests",
srcs: [
@@ -69,6 +73,7 @@
// For mockito extended
"libdexmakerjvmtiagent",
"libstaticjvmtiagent",
+ "libtetherutilsjni",
],
}
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 2eb7589..435cab5 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -36,6 +36,7 @@
import static android.net.netlink.StructNdMsg.NUD_FAILED;
import static android.net.netlink.StructNdMsg.NUD_REACHABLE;
import static android.net.netlink.StructNdMsg.NUD_STALE;
+import static android.system.OsConstants.ETH_P_IPV6;
import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
@@ -98,9 +99,20 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.NetworkStackConstants;
import com.android.networkstack.tethering.BpfCoordinator;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.BpfMap;
import com.android.networkstack.tethering.PrivateAddressCoordinator;
+import com.android.networkstack.tethering.Tether4Key;
+import com.android.networkstack.tethering.Tether4Value;
+import com.android.networkstack.tethering.Tether6Value;
+import com.android.networkstack.tethering.TetherDownstream6Key;
+import com.android.networkstack.tethering.TetherLimitKey;
+import com.android.networkstack.tethering.TetherLimitValue;
+import com.android.networkstack.tethering.TetherStatsKey;
+import com.android.networkstack.tethering.TetherStatsValue;
+import com.android.networkstack.tethering.TetherUpstream6Key;
import com.android.networkstack.tethering.TetheringConfiguration;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
@@ -163,6 +175,13 @@
@Mock private PrivateAddressCoordinator mAddressCoordinator;
@Mock private NetworkStatsManager mStatsManager;
@Mock private TetheringConfiguration mTetherConfig;
+ @Mock private ConntrackMonitor mConntrackMonitor;
+ @Mock private BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map;
+ @Mock private BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map;
+ @Mock private BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
+ @Mock private BpfMap<TetherUpstream6Key, Tether6Value> mBpfUpstream6Map;
+ @Mock private BpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap;
+ @Mock private BpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap;
@Captor private ArgumentCaptor<DhcpServingParamsParcel> mDhcpParamsCaptor;
@@ -173,6 +192,7 @@
private InterfaceConfigurationParcel mInterfaceConfiguration;
private NeighborEventConsumer mNeighborEventConsumer;
private BpfCoordinator mBpfCoordinator;
+ private BpfCoordinator.Dependencies mBpfDeps;
private void initStateMachine(int interfaceType) throws Exception {
initStateMachine(interfaceType, false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD);
@@ -186,9 +206,6 @@
when(mDependencies.getInterfaceParams(UPSTREAM_IFACE)).thenReturn(UPSTREAM_IFACE_PARAMS);
when(mDependencies.getInterfaceParams(UPSTREAM_IFACE2)).thenReturn(UPSTREAM_IFACE_PARAMS2);
- when(mDependencies.getIfindex(eq(UPSTREAM_IFACE))).thenReturn(UPSTREAM_IFINDEX);
- when(mDependencies.getIfindex(eq(UPSTREAM_IFACE2))).thenReturn(UPSTREAM_IFINDEX2);
-
mInterfaceConfiguration = new InterfaceConfigurationParcel();
mInterfaceConfiguration.flags = new String[0];
if (interfaceType == TETHERING_BLUETOOTH) {
@@ -230,7 +247,7 @@
lp.setInterfaceName(upstreamIface);
dispatchTetherConnectionChanged(upstreamIface, lp, 0);
}
- reset(mNetd, mCallback, mAddressCoordinator);
+ reset(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn(
mTestAddress);
}
@@ -256,8 +273,7 @@
mTestAddress);
when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(true /* default value */);
- mBpfCoordinator = spy(new BpfCoordinator(
- new BpfCoordinator.Dependencies() {
+ mBpfDeps = new BpfCoordinator.Dependencies() {
@NonNull
public Handler getHandler() {
return new Handler(mLooper.getLooper());
@@ -282,7 +298,44 @@
public TetheringConfiguration getTetherConfig() {
return mTetherConfig;
}
- }));
+
+ @NonNull
+ public ConntrackMonitor getConntrackMonitor(
+ ConntrackMonitor.ConntrackEventConsumer consumer) {
+ return mConntrackMonitor;
+ }
+
+ @Nullable
+ public BpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() {
+ return mBpfDownstream4Map;
+ }
+
+ @Nullable
+ public BpfMap<Tether4Key, Tether4Value> getBpfUpstream4Map() {
+ return mBpfUpstream4Map;
+ }
+
+ @Nullable
+ public BpfMap<TetherDownstream6Key, Tether6Value> getBpfDownstream6Map() {
+ return mBpfDownstream6Map;
+ }
+
+ @Nullable
+ public BpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() {
+ return mBpfUpstream6Map;
+ }
+
+ @Nullable
+ public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
+ return mBpfStatsMap;
+ }
+
+ @Nullable
+ public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
+ return mBpfLimitMap;
+ }
+ };
+ mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps));
setUpDhcpServer();
}
@@ -418,10 +471,16 @@
// Telling the state machine about its upstream interface triggers
// a little more configuration.
dispatchTetherConnectionChanged(UPSTREAM_IFACE);
- InOrder inOrder = inOrder(mNetd);
+ InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+
+ // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
+ inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX,
+ UPSTREAM_IFACE);
+ inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
- verifyNoMoreInteractions(mNetd, mCallback);
+
+ verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator);
}
@Test
@@ -429,12 +488,21 @@
initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
- InOrder inOrder = inOrder(mNetd);
+ InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+
+ // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
+ inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+
+ // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2>.
+ inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2,
+ UPSTREAM_IFACE2);
+ inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
- verifyNoMoreInteractions(mNetd, mCallback);
+
+ verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator);
}
@Test
@@ -444,10 +512,22 @@
doThrow(RemoteException.class).when(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
- InOrder inOrder = inOrder(mNetd);
+ InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+
+ // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
+ inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+
+ // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
+ // tetherAddForward.
+ inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2,
+ UPSTREAM_IFACE2);
+ inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
+
+ // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> to fallback.
+ inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2);
inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2);
}
@@ -460,11 +540,23 @@
IFACE_NAME, UPSTREAM_IFACE2);
dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
- InOrder inOrder = inOrder(mNetd);
+ InOrder inOrder = inOrder(mNetd, mBpfCoordinator);
+
+ // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>.
+ inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+
+ // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on
+ // ipfwdAddInterfaceForward.
+ inOrder.verify(mBpfCoordinator).addUpstreamNameToLookupTable(UPSTREAM_IFINDEX2,
+ UPSTREAM_IFACE2);
+ inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2);
inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2);
inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
+
+ // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> to fallback.
+ inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2);
inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2);
inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2);
}
@@ -474,19 +566,22 @@
initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
- InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator);
+ InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
+ inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE);
inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
inOrder.verify(mNetd).tetherApplyDnsInterfaces();
inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME);
inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
inOrder.verify(mAddressCoordinator).releaseDownstream(any());
+ inOrder.verify(mBpfCoordinator).stopMonitoring(mIpServer);
inOrder.verify(mCallback).updateInterfaceState(
mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
inOrder.verify(mCallback).updateLinkProperties(
eq(mIpServer), any(LinkProperties.class));
- verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator);
+ verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator);
}
@Test
@@ -742,19 +837,134 @@
}
@NonNull
+ private static TetherDownstream6Key makeDownstream6Key(int upstreamIfindex,
+ @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst) {
+ return new TetherDownstream6Key(upstreamIfindex, upstreamMac, dst.getAddress());
+ }
+
+ @NonNull
+ private static Tether6Value makeDownstream6Value(@NonNull final MacAddress dstMac) {
+ return new Tether6Value(TEST_IFACE_PARAMS.index, dstMac,
+ TEST_IFACE_PARAMS.macAddr, ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
+ }
+
+ private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+ if (inOrder != null) {
+ return inOrder.verify(t);
+ } else {
+ return verify(t);
+ }
+ }
+
+ private void verifyTetherOffloadRuleAdd(@Nullable InOrder inOrder, int upstreamIfindex,
+ @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst,
+ @NonNull final MacAddress dstMac) throws Exception {
+ if (mBpfDeps.isAtLeastS()) {
+ verifyWithOrder(inOrder, mBpfDownstream6Map).updateEntry(
+ makeDownstream6Key(upstreamIfindex, upstreamMac, dst),
+ makeDownstream6Value(dstMac));
+ } else {
+ verifyWithOrder(inOrder, mNetd).tetherOffloadRuleAdd(matches(upstreamIfindex, dst,
+ dstMac));
+ }
+ }
+
+ private void verifyNeverTetherOffloadRuleAdd(int upstreamIfindex,
+ @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst,
+ @NonNull final MacAddress dstMac) throws Exception {
+ if (mBpfDeps.isAtLeastS()) {
+ verify(mBpfDownstream6Map, never()).updateEntry(
+ makeDownstream6Key(upstreamIfindex, upstreamMac, dst),
+ makeDownstream6Value(dstMac));
+ } else {
+ verify(mNetd, never()).tetherOffloadRuleAdd(matches(upstreamIfindex, dst, dstMac));
+ }
+ }
+
+ private void verifyNeverTetherOffloadRuleAdd() throws Exception {
+ if (mBpfDeps.isAtLeastS()) {
+ verify(mBpfDownstream6Map, never()).updateEntry(any(), any());
+ } else {
+ verify(mNetd, never()).tetherOffloadRuleAdd(any());
+ }
+ }
+
+ private void verifyTetherOffloadRuleRemove(@Nullable InOrder inOrder, int upstreamIfindex,
+ @NonNull MacAddress upstreamMac, @NonNull final InetAddress dst,
+ @NonNull final MacAddress dstMac) throws Exception {
+ if (mBpfDeps.isAtLeastS()) {
+ verifyWithOrder(inOrder, mBpfDownstream6Map).deleteEntry(makeDownstream6Key(
+ upstreamIfindex, upstreamMac, dst));
+ } else {
+ // |dstMac| is not required for deleting rules. Used bacause tetherOffloadRuleRemove
+ // uses a whole rule to be a argument.
+ // See system/netd/server/TetherController.cpp/TetherController#removeOffloadRule.
+ verifyWithOrder(inOrder, mNetd).tetherOffloadRuleRemove(matches(upstreamIfindex, dst,
+ dstMac));
+ }
+ }
+
+ private void verifyNeverTetherOffloadRuleRemove() throws Exception {
+ if (mBpfDeps.isAtLeastS()) {
+ verify(mBpfDownstream6Map, never()).deleteEntry(any());
+ } else {
+ verify(mNetd, never()).tetherOffloadRuleRemove(any());
+ }
+ }
+
+ private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int upstreamIfindex)
+ throws Exception {
+ if (!mBpfDeps.isAtLeastS()) return;
+ final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index,
+ TEST_IFACE_PARAMS.macAddr);
+ final Tether6Value value = new Tether6Value(upstreamIfindex,
+ MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
+ ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
+ verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(key, value);
+ }
+
+ private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder)
+ throws Exception {
+ if (!mBpfDeps.isAtLeastS()) return;
+ final TetherUpstream6Key key = new TetherUpstream6Key(TEST_IFACE_PARAMS.index,
+ TEST_IFACE_PARAMS.macAddr);
+ verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(key);
+ }
+
+ private void verifyNoUpstreamIpv6ForwardingChange(@Nullable InOrder inOrder) throws Exception {
+ if (!mBpfDeps.isAtLeastS()) return;
+ if (inOrder != null) {
+ inOrder.verify(mBpfUpstream6Map, never()).deleteEntry(any());
+ inOrder.verify(mBpfUpstream6Map, never()).insertEntry(any(), any());
+ inOrder.verify(mBpfUpstream6Map, never()).updateEntry(any(), any());
+ } else {
+ verify(mBpfUpstream6Map, never()).deleteEntry(any());
+ verify(mBpfUpstream6Map, never()).insertEntry(any(), any());
+ verify(mBpfUpstream6Map, never()).updateEntry(any(), any());
+ }
+ }
+
+ @NonNull
private static TetherStatsParcel buildEmptyTetherStatsParcel(int ifIndex) {
TetherStatsParcel parcel = new TetherStatsParcel();
parcel.ifIndex = ifIndex;
return parcel;
}
- private void resetNetdAndBpfCoordinator() throws Exception {
- reset(mNetd, mBpfCoordinator);
+ private void resetNetdBpfMapAndCoordinator() throws Exception {
+ reset(mNetd, mBpfDownstream6Map, mBpfUpstream6Map, mBpfCoordinator);
+ // When the last rule is removed, tetherOffloadGetAndClearStats will log a WTF (and
+ // potentially crash the test) if the stats map is empty.
when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
when(mNetd.tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX))
.thenReturn(buildEmptyTetherStatsParcel(UPSTREAM_IFINDEX));
when(mNetd.tetherOffloadGetAndClearStats(UPSTREAM_IFINDEX2))
.thenReturn(buildEmptyTetherStatsParcel(UPSTREAM_IFINDEX2));
+ // When the last rule is removed, tetherOffloadGetAndClearStats will log a WTF (and
+ // potentially crash the test) if the stats map is empty.
+ final TetherStatsValue allZeros = new TetherStatsValue(0, 0, 0, 0, 0, 0);
+ when(mBpfStatsMap.getValue(new TetherStatsKey(UPSTREAM_IFINDEX))).thenReturn(allZeros);
+ when(mBpfStatsMap.getValue(new TetherStatsKey(UPSTREAM_IFINDEX2))).thenReturn(allZeros);
}
@Test
@@ -765,7 +975,6 @@
final int myIfindex = TEST_IFACE_PARAMS.index;
final int notMyIfindex = myIfindex - 1;
- final MacAddress myMac = TEST_IFACE_PARAMS.macAddr;
final InetAddress neighA = InetAddresses.parseNumericAddress("2001:db8::1");
final InetAddress neighB = InetAddresses.parseNumericAddress("2001:db8::2");
final InetAddress neighLL = InetAddresses.parseNumericAddress("fe80::1");
@@ -774,65 +983,81 @@
final MacAddress macA = MacAddress.fromString("00:00:00:00:00:0a");
final MacAddress macB = MacAddress.fromString("11:22:33:00:00:0b");
- resetNetdAndBpfCoordinator();
- verifyNoMoreInteractions(mBpfCoordinator, mNetd);
+ resetNetdBpfMapAndCoordinator();
+ verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
// TODO: Perhaps verify the interaction of tetherOffloadSetInterfaceQuota and
// tetherOffloadGetAndClearStats in netd while the rules are changed.
// Events on other interfaces are ignored.
recvNewNeigh(notMyIfindex, neighA, NUD_REACHABLE, macA);
- verifyNoMoreInteractions(mBpfCoordinator, mNetd);
+ verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
// Events on this interface are received and sent to netd.
recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
verify(mBpfCoordinator).tetherOffloadRuleAdd(
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
- verify(mNetd).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX, neighA, macA));
- resetNetdAndBpfCoordinator();
+ verifyTetherOffloadRuleAdd(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
+ resetNetdBpfMapAndCoordinator();
recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
verify(mBpfCoordinator).tetherOffloadRuleAdd(
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
- verify(mNetd).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX, neighB, macB));
- resetNetdAndBpfCoordinator();
+ verifyTetherOffloadRuleAdd(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
+ verifyNoUpstreamIpv6ForwardingChange(null);
+ resetNetdBpfMapAndCoordinator();
// Link-local and multicast neighbors are ignored.
recvNewNeigh(myIfindex, neighLL, NUD_REACHABLE, macA);
- verifyNoMoreInteractions(mBpfCoordinator, mNetd);
+ verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
recvNewNeigh(myIfindex, neighMC, NUD_REACHABLE, macA);
- verifyNoMoreInteractions(mBpfCoordinator, mNetd);
+ verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
// A neighbor that is no longer valid causes the rule to be removed.
// NUD_FAILED events do not have a MAC address.
recvNewNeigh(myIfindex, neighA, NUD_FAILED, null);
verify(mBpfCoordinator).tetherOffloadRuleRemove(
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macNull));
- verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighA, macNull));
- resetNetdAndBpfCoordinator();
+ verifyTetherOffloadRuleRemove(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macNull);
+ verifyNoUpstreamIpv6ForwardingChange(null);
+ resetNetdBpfMapAndCoordinator();
// A neighbor that is deleted causes the rule to be removed.
recvDelNeigh(myIfindex, neighB, NUD_STALE, macB);
verify(mBpfCoordinator).tetherOffloadRuleRemove(
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macNull));
- verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighB, macNull));
- resetNetdAndBpfCoordinator();
+ verifyTetherOffloadRuleRemove(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macNull);
+ verifyStopUpstreamIpv6Forwarding(null);
+ resetNetdBpfMapAndCoordinator();
// Upstream changes result in updating the rules.
recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
- resetNetdAndBpfCoordinator();
+ resetNetdBpfMapAndCoordinator();
- InOrder inOrder = inOrder(mNetd);
+ InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
LinkProperties lp = new LinkProperties();
lp.setInterfaceName(UPSTREAM_IFACE2);
dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp, -1);
verify(mBpfCoordinator).tetherOffloadRuleUpdate(mIpServer, UPSTREAM_IFINDEX2);
- inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighA, macA));
- inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX2, neighA, macA));
- inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighB, macB));
- inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX2, neighB, macB));
- resetNetdAndBpfCoordinator();
+ verifyTetherOffloadRuleRemove(inOrder,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
+ verifyTetherOffloadRuleRemove(inOrder,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
+ verifyStopUpstreamIpv6Forwarding(inOrder);
+ verifyTetherOffloadRuleAdd(inOrder,
+ UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
+ verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2);
+ verifyTetherOffloadRuleAdd(inOrder,
+ UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighB, macB);
+ verifyNoUpstreamIpv6ForwardingChange(inOrder);
+ resetNetdBpfMapAndCoordinator();
// When the upstream is lost, rules are removed.
dispatchTetherConnectionChanged(null, null, 0);
@@ -841,17 +1066,21 @@
// - processMessage CMD_IPV6_TETHER_UPDATE for the IPv6 upstream is lost.
// See dispatchTetherConnectionChanged.
verify(mBpfCoordinator, times(2)).tetherOffloadRuleClear(mIpServer);
- verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX2, neighA, macA));
- verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX2, neighB, macB));
- resetNetdAndBpfCoordinator();
+ verifyTetherOffloadRuleRemove(null,
+ UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighA, macA);
+ verifyTetherOffloadRuleRemove(null,
+ UPSTREAM_IFINDEX2, UPSTREAM_IFACE_PARAMS2.macAddr, neighB, macB);
+ verifyStopUpstreamIpv6Forwarding(inOrder);
+ resetNetdBpfMapAndCoordinator();
// If the upstream is IPv4-only, no rules are added.
dispatchTetherConnectionChanged(UPSTREAM_IFACE);
- resetNetdAndBpfCoordinator();
+ resetNetdBpfMapAndCoordinator();
recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
// Clear function is called by #updateIpv6ForwardingRules for the IPv6 upstream is lost.
verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
- verifyNoMoreInteractions(mBpfCoordinator, mNetd);
+ verifyNoUpstreamIpv6ForwardingChange(null);
+ verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
// Rules can be added again once upstream IPv6 connectivity is available.
lp.setInterfaceName(UPSTREAM_IFACE);
@@ -859,16 +1088,21 @@
recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
verify(mBpfCoordinator).tetherOffloadRuleAdd(
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
- verify(mNetd).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX, neighB, macB));
+ verifyTetherOffloadRuleAdd(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
verify(mBpfCoordinator, never()).tetherOffloadRuleAdd(
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
- verify(mNetd, never()).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX, neighA, macA));
+ verifyNeverTetherOffloadRuleAdd(
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
// If upstream IPv6 connectivity is lost, rules are removed.
- resetNetdAndBpfCoordinator();
+ resetNetdBpfMapAndCoordinator();
dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0);
verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
- verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighB, macB));
+ verifyTetherOffloadRuleRemove(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
+ verifyStopUpstreamIpv6Forwarding(null);
// When the interface goes down, rules are removed.
lp.setInterfaceName(UPSTREAM_IFACE);
@@ -877,19 +1111,25 @@
recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
verify(mBpfCoordinator).tetherOffloadRuleAdd(
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
- verify(mNetd).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX, neighA, macA));
+ verifyTetherOffloadRuleAdd(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
verify(mBpfCoordinator).tetherOffloadRuleAdd(
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
- verify(mNetd).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX, neighB, macB));
- resetNetdAndBpfCoordinator();
+ verifyTetherOffloadRuleAdd(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
+ resetNetdBpfMapAndCoordinator();
mIpServer.stop();
mLooper.dispatchAll();
verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
- verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighA, macA));
- verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighB, macB));
+ verifyTetherOffloadRuleRemove(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighA, macA);
+ verifyTetherOffloadRuleRemove(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neighB, macB);
+ verifyStopUpstreamIpv6Forwarding(null);
verify(mIpNeighborMonitor).stop();
- resetNetdAndBpfCoordinator();
+ resetNetdBpfMapAndCoordinator();
}
@Test
@@ -910,35 +1150,41 @@
// A neighbor that is added or deleted causes the rule to be added or removed.
initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
true /* usingBpfOffload */);
- resetNetdAndBpfCoordinator();
+ resetNetdBpfMapAndCoordinator();
recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
verify(mBpfCoordinator).tetherOffloadRuleAdd(
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neigh, macA));
- verify(mNetd).tetherOffloadRuleAdd(matches(UPSTREAM_IFINDEX, neigh, macA));
- resetNetdAndBpfCoordinator();
+ verifyTetherOffloadRuleAdd(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neigh, macA);
+ verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
+ resetNetdBpfMapAndCoordinator();
recvDelNeigh(myIfindex, neigh, NUD_STALE, macA);
verify(mBpfCoordinator).tetherOffloadRuleRemove(
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neigh, macNull));
- verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neigh, macNull));
- resetNetdAndBpfCoordinator();
+ verifyTetherOffloadRuleRemove(null,
+ UPSTREAM_IFINDEX, UPSTREAM_IFACE_PARAMS.macAddr, neigh, macNull);
+ verifyStopUpstreamIpv6Forwarding(null);
+ resetNetdBpfMapAndCoordinator();
// [2] Disable BPF offload.
// A neighbor that is added or deleted doesn’t cause the rule to be added or removed.
initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, false /* usingLegacyDhcp */,
false /* usingBpfOffload */);
- resetNetdAndBpfCoordinator();
+ resetNetdBpfMapAndCoordinator();
recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
verify(mBpfCoordinator, never()).tetherOffloadRuleAdd(any(), any());
- verify(mNetd, never()).tetherOffloadRuleAdd(any());
- resetNetdAndBpfCoordinator();
+ verifyNeverTetherOffloadRuleAdd();
+ verifyNoUpstreamIpv6ForwardingChange(null);
+ resetNetdBpfMapAndCoordinator();
recvDelNeigh(myIfindex, neigh, NUD_STALE, macA);
verify(mBpfCoordinator, never()).tetherOffloadRuleRemove(any(), any());
- verify(mNetd, never()).tetherOffloadRuleRemove(any());
- resetNetdAndBpfCoordinator();
+ verifyNeverTetherOffloadRuleRemove();
+ verifyNoUpstreamIpv6ForwardingChange(null);
+ resetNetdBpfMapAndCoordinator();
}
@Test
diff --git a/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java b/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java
index 91c7771..9968b5f 100644
--- a/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java
+++ b/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java
@@ -17,27 +17,49 @@
import static android.net.TetheringManager.TETHERING_USB;
import static android.net.TetheringManager.TETHERING_WIFI;
+import static android.system.OsConstants.AF_UNIX;
+import static android.system.OsConstants.EAGAIN;
+import static android.system.OsConstants.SOCK_CLOEXEC;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import android.net.LinkAddress;
+import android.net.MacAddress;
import android.net.TetheringRequestParcel;
+import android.system.ErrnoException;
+import android.system.Os;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.net.module.util.Ipv6Utils;
+import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.structs.EthernetHeader;
+import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv6Header;
import com.android.testutils.MiscAsserts;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.FileDescriptor;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
@RunWith(AndroidJUnit4.class)
@SmallTest
public class TetheringUtilsTest {
private static final LinkAddress TEST_SERVER_ADDR = new LinkAddress("192.168.43.1/24");
private static final LinkAddress TEST_CLIENT_ADDR = new LinkAddress("192.168.43.5/24");
+ private static final int PACKET_SIZE = 1500;
+
private TetheringRequestParcel mTetheringRequest;
@Before
@@ -84,4 +106,82 @@
MiscAsserts.assertFieldCountEquals(5, TetheringRequestParcel.class);
}
+
+ // Writes the specified packet to a filedescriptor, skipping the Ethernet header.
+ // Needed because the Ipv6Utils methods for building packets always include the Ethernet header,
+ // but socket filters applied by TetheringUtils expect the packet to start from the IP header.
+ private int writePacket(FileDescriptor fd, ByteBuffer pkt) throws Exception {
+ pkt.flip();
+ int offset = Struct.getSize(EthernetHeader.class);
+ int len = pkt.capacity() - offset;
+ return Os.write(fd, pkt.array(), offset, len);
+ }
+
+ // Reads a packet from the filedescriptor.
+ private ByteBuffer readIpPacket(FileDescriptor fd) throws Exception {
+ ByteBuffer buf = ByteBuffer.allocate(PACKET_SIZE);
+ Os.read(fd, buf);
+ return buf;
+ }
+
+ private interface SocketFilter {
+ void apply(FileDescriptor fd) throws Exception;
+ }
+
+ private ByteBuffer checkIcmpSocketFilter(ByteBuffer passed, ByteBuffer dropped,
+ SocketFilter filter) throws Exception {
+ FileDescriptor in = new FileDescriptor();
+ FileDescriptor out = new FileDescriptor();
+ Os.socketpair(AF_UNIX, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, in, out);
+
+ // Before the filter is applied, it doesn't drop anything.
+ int len = writePacket(out, dropped);
+ ByteBuffer received = readIpPacket(in);
+ assertEquals(len, received.position());
+
+ // Install the socket filter. Then write two packets, the first expected to be dropped and
+ // the second expected to be passed. Check that only the second makes it through.
+ filter.apply(in);
+ writePacket(out, dropped);
+ len = writePacket(out, passed);
+ received = readIpPacket(in);
+ assertEquals(len, received.position());
+ received.flip();
+
+ // Check there are no more packets to read.
+ try {
+ readIpPacket(in);
+ } catch (ErrnoException expected) {
+ assertEquals(EAGAIN, expected.errno);
+ }
+
+ return received;
+ }
+
+ @Test
+ public void testIcmpSocketFilters() throws Exception {
+ MacAddress mac1 = MacAddress.fromString("11:22:33:44:55:66");
+ MacAddress mac2 = MacAddress.fromString("aa:bb:cc:dd:ee:ff");
+ Inet6Address ll1 = (Inet6Address) InetAddress.getByName("fe80::1");
+ Inet6Address ll2 = (Inet6Address) InetAddress.getByName("fe80::abcd");
+ Inet6Address allRouters = NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
+
+ final ByteBuffer na = Ipv6Utils.buildNaPacket(mac1, mac2, ll1, ll2, 0, ll1);
+ final ByteBuffer ns = Ipv6Utils.buildNsPacket(mac1, mac2, ll1, ll2, ll1);
+ final ByteBuffer rs = Ipv6Utils.buildRsPacket(mac1, mac2, ll1, allRouters);
+
+ ByteBuffer received = checkIcmpSocketFilter(na /* passed */, rs /* dropped */,
+ TetheringUtils::setupNaSocket);
+
+ Struct.parse(Ipv6Header.class, received); // Skip IPv6 header.
+ Icmpv6Header icmpv6 = Struct.parse(Icmpv6Header.class, received);
+ assertEquals(NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT, icmpv6.type);
+
+ received = checkIcmpSocketFilter(ns /* passed */, rs /* dropped */,
+ TetheringUtils::setupNsSocket);
+
+ Struct.parse(Ipv6Header.class, received); // Skip IPv6 header.
+ icmpv6 = Struct.parse(Icmpv6Header.class, received);
+ assertEquals(NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION, icmpv6.type);
+ }
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index 64242ae..233f6db 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -23,49 +23,85 @@
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
import static android.net.NetworkStats.UID_TETHERING;
+import static android.net.ip.ConntrackMonitor.ConntrackEvent;
+import static android.net.netlink.ConntrackMessage.DYING_MASK;
+import static android.net.netlink.ConntrackMessage.ESTABLISHED_MASK;
+import static android.net.netlink.ConntrackMessage.Tuple;
+import static android.net.netlink.ConntrackMessage.TupleIpv4;
+import static android.net.netlink.ConntrackMessage.TupleProto;
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_DELETE;
+import static android.net.netlink.NetlinkConstants.IPCTNL_MSG_CT_NEW;
import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
+import static android.system.OsConstants.ETH_P_IP;
+import static android.system.OsConstants.ETH_P_IPV6;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker;
import static com.android.networkstack.tethering.BpfCoordinator.StatsType;
import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE;
import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_UID;
+import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM;
+import static com.android.networkstack.tethering.BpfUtils.UPSTREAM;
import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.usage.NetworkStatsManager;
import android.net.INetd;
import android.net.InetAddresses;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.NetworkStats;
import android.net.TetherOffloadRuleParcel;
import android.net.TetherStatsParcel;
+import android.net.ip.ConntrackMonitor;
+import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
import android.net.ip.IpServer;
+import android.net.netlink.NetlinkConstants;
+import android.net.util.InterfaceParams;
import android.net.util.SharedLog;
+import android.os.Build;
import android.os.Handler;
import android.os.test.TestLooper;
+import android.system.ErrnoException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.Struct;
+import com.android.networkstack.tethering.BpfCoordinator.BpfConntrackEventConsumer;
+import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.TestableNetworkStatsProviderCbBinder;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -73,61 +109,182 @@
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class BpfCoordinatorTest {
- private static final int DOWNSTREAM_IFINDEX = 10;
- private static final MacAddress DOWNSTREAM_MAC = MacAddress.ALL_ZEROS_ADDRESS;
- private static final InetAddress NEIGH_A = InetAddresses.parseNumericAddress("2001:db8::1");
- private static final InetAddress NEIGH_B = InetAddresses.parseNumericAddress("2001:db8::2");
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+ private static final int UPSTREAM_IFINDEX = 1001;
+ private static final int DOWNSTREAM_IFINDEX = 1002;
+
+ private static final String UPSTREAM_IFACE = "rmnet0";
+
+ private static final MacAddress DOWNSTREAM_MAC = MacAddress.fromString("12:34:56:78:90:ab");
private static final MacAddress MAC_A = MacAddress.fromString("00:00:00:00:00:0a");
private static final MacAddress MAC_B = MacAddress.fromString("11:22:33:00:00:0b");
+ private static final InetAddress NEIGH_A = InetAddresses.parseNumericAddress("2001:db8::1");
+ private static final InetAddress NEIGH_B = InetAddresses.parseNumericAddress("2001:db8::2");
+
+ private static final InterfaceParams UPSTREAM_IFACE_PARAMS = new InterfaceParams(
+ UPSTREAM_IFACE, UPSTREAM_IFINDEX, null /* macAddr, rawip */,
+ NetworkStackConstants.ETHER_MTU);
+
+ // The test fake BPF map class is needed because the test has no privilege to access the BPF
+ // map. All member functions which eventually call JNI to access the real native BPF map need
+ // to be overridden.
+ // TODO: consider moving to an individual file.
+ private class TestBpfMap<K extends Struct, V extends Struct> extends BpfMap<K, V> {
+ private final HashMap<K, V> mMap = new HashMap<K, V>();
+
+ TestBpfMap(final Class<K> key, final Class<V> value) {
+ super(key, value);
+ }
+
+ @Override
+ public void forEach(BiConsumer<K, V> action) throws ErrnoException {
+ // TODO: consider using mocked #getFirstKey and #getNextKey to iterate. It helps to
+ // implement the entry deletion in the iteration if required.
+ for (Map.Entry<K, V> entry : mMap.entrySet()) {
+ action.accept(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override
+ public void updateEntry(K key, V value) throws ErrnoException {
+ mMap.put(key, value);
+ }
+
+ @Override
+ public void insertEntry(K key, V value) throws ErrnoException,
+ IllegalArgumentException {
+ // The entry is created if and only if it doesn't exist. See BpfMap#insertEntry.
+ if (mMap.get(key) != null) {
+ throw new IllegalArgumentException(key + " already exist");
+ }
+ mMap.put(key, value);
+ }
+
+ @Override
+ public boolean deleteEntry(Struct key) throws ErrnoException {
+ return mMap.remove(key) != null;
+ }
+
+ @Override
+ public V getValue(@NonNull K key) throws ErrnoException {
+ // Return value for a given key. Otherwise, return null without an error ENOENT.
+ // BpfMap#getValue treats that the entry is not found as no error.
+ return mMap.get(key);
+ }
+
+ @Override
+ public void clear() throws ErrnoException {
+ // TODO: consider using mocked #getFirstKey and #deleteEntry to implement.
+ mMap.clear();
+ }
+ };
+
@Mock private NetworkStatsManager mStatsManager;
@Mock private INetd mNetd;
@Mock private IpServer mIpServer;
+ @Mock private IpServer mIpServer2;
@Mock private TetheringConfiguration mTetherConfig;
+ @Mock private ConntrackMonitor mConntrackMonitor;
+ @Mock private BpfMap<Tether4Key, Tether4Value> mBpfDownstream4Map;
+ @Mock private BpfMap<Tether4Key, Tether4Value> mBpfUpstream4Map;
+ @Mock private BpfMap<TetherDownstream6Key, Tether6Value> mBpfDownstream6Map;
+ @Mock private BpfMap<TetherUpstream6Key, Tether6Value> mBpfUpstream6Map;
// Late init since methods must be called by the thread that created this object.
private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
private BpfCoordinator.BpfTetherStatsProvider mTetherStatsProvider;
+
+ // Late init since the object must be initialized by the BPF coordinator instance because
+ // it has to access the non-static function of BPF coordinator.
+ private BpfConntrackEventConsumer mConsumer;
+
private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
ArgumentCaptor.forClass(ArrayList.class);
private final TestLooper mTestLooper = new TestLooper();
+ private final TestBpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap =
+ spy(new TestBpfMap<>(TetherStatsKey.class, TetherStatsValue.class));
+ private final TestBpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap =
+ spy(new TestBpfMap<>(TetherLimitKey.class, TetherLimitValue.class));
private BpfCoordinator.Dependencies mDeps =
- new BpfCoordinator.Dependencies() {
- @NonNull
- public Handler getHandler() {
- return new Handler(mTestLooper.getLooper());
- }
+ spy(new BpfCoordinator.Dependencies() {
+ @NonNull
+ public Handler getHandler() {
+ return new Handler(mTestLooper.getLooper());
+ }
- @NonNull
- public INetd getNetd() {
- return mNetd;
- }
+ @NonNull
+ public INetd getNetd() {
+ return mNetd;
+ }
- @NonNull
- public NetworkStatsManager getNetworkStatsManager() {
- return mStatsManager;
- }
+ @NonNull
+ public NetworkStatsManager getNetworkStatsManager() {
+ return mStatsManager;
+ }
- @NonNull
- public SharedLog getSharedLog() {
- return new SharedLog("test");
- }
+ @NonNull
+ public SharedLog getSharedLog() {
+ return new SharedLog("test");
+ }
- @Nullable
- public TetheringConfiguration getTetherConfig() {
- return mTetherConfig;
- }
- };
+ @Nullable
+ public TetheringConfiguration getTetherConfig() {
+ return mTetherConfig;
+ }
+
+ @NonNull
+ public ConntrackMonitor getConntrackMonitor(ConntrackEventConsumer consumer) {
+ return mConntrackMonitor;
+ }
+
+ @Nullable
+ public BpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() {
+ return mBpfDownstream4Map;
+ }
+
+ @Nullable
+ public BpfMap<Tether4Key, Tether4Value> getBpfUpstream4Map() {
+ return mBpfUpstream4Map;
+ }
+
+ @Nullable
+ public BpfMap<TetherDownstream6Key, Tether6Value> getBpfDownstream6Map() {
+ return mBpfDownstream6Map;
+ }
+
+ @Nullable
+ public BpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() {
+ return mBpfUpstream6Map;
+ }
+
+ @Nullable
+ public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() {
+ return mBpfStatsMap;
+ }
+
+ @Nullable
+ public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() {
+ return mBpfLimitMap;
+ }
+ });
@Before public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -138,6 +295,8 @@
mTestLooper.dispatchAll();
}
+ // TODO: Remove unnecessary calling on R because the BPF map accessing has been moved into
+ // module.
private void setupFunctioningNetdInterface() throws Exception {
when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
}
@@ -145,6 +304,8 @@
@NonNull
private BpfCoordinator makeBpfCoordinator() throws Exception {
final BpfCoordinator coordinator = new BpfCoordinator(mDeps);
+
+ mConsumer = coordinator.getBpfConntrackEventConsumerForTesting();
final ArgumentCaptor<BpfCoordinator.BpfTetherStatsProvider>
tetherStatsProviderCaptor =
ArgumentCaptor.forClass(BpfCoordinator.BpfTetherStatsProvider.class);
@@ -154,6 +315,7 @@
assertNotNull(mTetherStatsProvider);
mTetherStatsProviderCb = new TestableNetworkStatsProviderCbBinder();
mTetherStatsProvider.setProviderCallbackBinder(mTetherStatsProviderCb);
+
return coordinator;
}
@@ -177,14 +339,280 @@
return parcel;
}
- // Set up specific tether stats list and wait for the stats cache is updated by polling thread
+ // Update a stats entry or create if not exists.
+ private void updateStatsEntryToStatsMap(@NonNull TetherStatsParcel stats) throws Exception {
+ final TetherStatsKey key = new TetherStatsKey(stats.ifIndex);
+ final TetherStatsValue value = new TetherStatsValue(stats.rxPackets, stats.rxBytes,
+ 0L /* rxErrors */, stats.txPackets, stats.txBytes, 0L /* txErrors */);
+ mBpfStatsMap.updateEntry(key, value);
+ }
+
+ private void updateStatsEntry(@NonNull TetherStatsParcel stats) throws Exception {
+ if (mDeps.isAtLeastS()) {
+ updateStatsEntryToStatsMap(stats);
+ } else {
+ when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[] {stats});
+ }
+ }
+
+ // Update specific tether stats list and wait for the stats cache is updated by polling thread
// in the coordinator. Beware of that it is only used for the default polling interval.
- private void setTetherOffloadStatsList(TetherStatsParcel[] tetherStatsList) throws Exception {
- when(mNetd.tetherOffloadGetStats()).thenReturn(tetherStatsList);
+ // Note that the mocked tetherOffloadGetStats of netd replaces all stats entries because it
+ // doesn't store the previous entries.
+ private void updateStatsEntriesAndWaitForUpdate(@NonNull TetherStatsParcel[] tetherStatsList)
+ throws Exception {
+ if (mDeps.isAtLeastS()) {
+ for (TetherStatsParcel stats : tetherStatsList) {
+ updateStatsEntry(stats);
+ }
+ } else {
+ when(mNetd.tetherOffloadGetStats()).thenReturn(tetherStatsList);
+ }
+
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
waitForIdle();
}
+ // In tests, the stats need to be set before deleting the last rule.
+ // The reason is that BpfCoordinator#tetherOffloadRuleRemove reads the stats
+ // of the deleting interface after the last rule deleted. #tetherOffloadRuleRemove
+ // does the interface cleanup failed if there is no stats for the deleting interface.
+ // Note that the mocked tetherOffloadGetAndClearStats of netd replaces all stats entries
+ // because it doesn't store the previous entries.
+ private void updateStatsEntryForTetherOffloadGetAndClearStats(TetherStatsParcel stats)
+ throws Exception {
+ if (mDeps.isAtLeastS()) {
+ updateStatsEntryToStatsMap(stats);
+ } else {
+ when(mNetd.tetherOffloadGetAndClearStats(stats.ifIndex)).thenReturn(stats);
+ }
+ }
+
+ private void clearStatsInvocations() {
+ if (mDeps.isAtLeastS()) {
+ clearInvocations(mBpfStatsMap);
+ } else {
+ clearInvocations(mNetd);
+ }
+ }
+
+ private <T> T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) {
+ if (inOrder != null) {
+ return inOrder.verify(t);
+ } else {
+ return verify(t);
+ }
+ }
+
+ private void verifyTetherOffloadGetStats() throws Exception {
+ if (mDeps.isAtLeastS()) {
+ verify(mBpfStatsMap).forEach(any());
+ } else {
+ verify(mNetd).tetherOffloadGetStats();
+ }
+ }
+
+ private void verifyNeverTetherOffloadGetStats() throws Exception {
+ if (mDeps.isAtLeastS()) {
+ verify(mBpfStatsMap, never()).forEach(any());
+ } else {
+ verify(mNetd, never()).tetherOffloadGetStats();
+ }
+ }
+
+ private void verifyStartUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int downstreamIfIndex,
+ MacAddress downstreamMac, int upstreamIfindex) throws Exception {
+ if (!mDeps.isAtLeastS()) return;
+ final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfIndex, downstreamMac);
+ final Tether6Value value = new Tether6Value(upstreamIfindex,
+ MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS,
+ ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
+ verifyWithOrder(inOrder, mBpfUpstream6Map).insertEntry(key, value);
+ }
+
+ private void verifyStopUpstreamIpv6Forwarding(@Nullable InOrder inOrder, int downstreamIfIndex,
+ MacAddress downstreamMac)
+ throws Exception {
+ if (!mDeps.isAtLeastS()) return;
+ final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfIndex, downstreamMac);
+ verifyWithOrder(inOrder, mBpfUpstream6Map).deleteEntry(key);
+ }
+
+ private void verifyNoUpstreamIpv6ForwardingChange(@Nullable InOrder inOrder) throws Exception {
+ if (!mDeps.isAtLeastS()) return;
+ if (inOrder != null) {
+ inOrder.verify(mBpfUpstream6Map, never()).deleteEntry(any());
+ inOrder.verify(mBpfUpstream6Map, never()).insertEntry(any(), any());
+ inOrder.verify(mBpfUpstream6Map, never()).updateEntry(any(), any());
+ } else {
+ verify(mBpfUpstream6Map, never()).deleteEntry(any());
+ verify(mBpfUpstream6Map, never()).insertEntry(any(), any());
+ verify(mBpfUpstream6Map, never()).updateEntry(any(), any());
+ }
+ }
+
+ private void verifyTetherOffloadRuleAdd(@Nullable InOrder inOrder,
+ @NonNull Ipv6ForwardingRule rule) throws Exception {
+ if (mDeps.isAtLeastS()) {
+ verifyWithOrder(inOrder, mBpfDownstream6Map).updateEntry(
+ rule.makeTetherDownstream6Key(), rule.makeTether6Value());
+ } else {
+ verifyWithOrder(inOrder, mNetd).tetherOffloadRuleAdd(matches(rule));
+ }
+ }
+
+ private void verifyNeverTetherOffloadRuleAdd() throws Exception {
+ if (mDeps.isAtLeastS()) {
+ verify(mBpfDownstream6Map, never()).updateEntry(any(), any());
+ } else {
+ verify(mNetd, never()).tetherOffloadRuleAdd(any());
+ }
+ }
+
+ private void verifyTetherOffloadRuleRemove(@Nullable InOrder inOrder,
+ @NonNull final Ipv6ForwardingRule rule) throws Exception {
+ if (mDeps.isAtLeastS()) {
+ verifyWithOrder(inOrder, mBpfDownstream6Map).deleteEntry(
+ rule.makeTetherDownstream6Key());
+ } else {
+ verifyWithOrder(inOrder, mNetd).tetherOffloadRuleRemove(matches(rule));
+ }
+ }
+
+ private void verifyNeverTetherOffloadRuleRemove() throws Exception {
+ if (mDeps.isAtLeastS()) {
+ verify(mBpfDownstream6Map, never()).deleteEntry(any());
+ } else {
+ verify(mNetd, never()).tetherOffloadRuleRemove(any());
+ }
+ }
+
+ private void verifyTetherOffloadSetInterfaceQuota(@Nullable InOrder inOrder, int ifIndex,
+ long quotaBytes, boolean isInit) throws Exception {
+ if (mDeps.isAtLeastS()) {
+ final TetherStatsKey key = new TetherStatsKey(ifIndex);
+ verifyWithOrder(inOrder, mBpfStatsMap).getValue(key);
+ if (isInit) {
+ verifyWithOrder(inOrder, mBpfStatsMap).insertEntry(key, new TetherStatsValue(
+ 0L /* rxPackets */, 0L /* rxBytes */, 0L /* rxErrors */,
+ 0L /* txPackets */, 0L /* txBytes */, 0L /* txErrors */));
+ }
+ verifyWithOrder(inOrder, mBpfLimitMap).updateEntry(new TetherLimitKey(ifIndex),
+ new TetherLimitValue(quotaBytes));
+ } else {
+ verifyWithOrder(inOrder, mNetd).tetherOffloadSetInterfaceQuota(ifIndex, quotaBytes);
+ }
+ }
+
+ private void verifyNeverTetherOffloadSetInterfaceQuota(@NonNull InOrder inOrder)
+ throws Exception {
+ if (mDeps.isAtLeastS()) {
+ inOrder.verify(mBpfStatsMap, never()).getValue(any());
+ inOrder.verify(mBpfStatsMap, never()).insertEntry(any(), any());
+ inOrder.verify(mBpfLimitMap, never()).updateEntry(any(), any());
+ } else {
+ inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong());
+ }
+ }
+
+ private void verifyTetherOffloadGetAndClearStats(@NonNull InOrder inOrder, int ifIndex)
+ throws Exception {
+ if (mDeps.isAtLeastS()) {
+ inOrder.verify(mBpfStatsMap).getValue(new TetherStatsKey(ifIndex));
+ inOrder.verify(mBpfStatsMap).deleteEntry(new TetherStatsKey(ifIndex));
+ inOrder.verify(mBpfLimitMap).deleteEntry(new TetherLimitKey(ifIndex));
+ } else {
+ inOrder.verify(mNetd).tetherOffloadGetAndClearStats(ifIndex);
+ }
+ }
+
+ // S+ and R api minimum tests.
+ // The following tests are used to provide minimum checking for the APIs on different flow.
+ // The auto merge is not enabled on mainline prod. The code flow R may be verified at the
+ // late stage by manual cherry pick. It is risky if the R code flow has broken and be found at
+ // the last minute.
+ // TODO: remove once presubmit tests on R even the code is submitted on S.
+ private void checkTetherOffloadRuleAddAndRemove(boolean usingApiS) throws Exception {
+ setupFunctioningNetdInterface();
+
+ // Replace Dependencies#isAtLeastS() for testing R and S+ BPF map apis. Note that |mDeps|
+ // must be mocked before calling #makeBpfCoordinator which use |mDeps| to initialize the
+ // coordinator.
+ doReturn(usingApiS).when(mDeps).isAtLeastS();
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ final String mobileIface = "rmnet_data0";
+ final Integer mobileIfIndex = 100;
+ coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+
+ // InOrder is required because mBpfStatsMap may be accessed by both
+ // BpfCoordinator#tetherOffloadRuleAdd and BpfCoordinator#tetherOffloadGetAndClearStats.
+ // The #verifyTetherOffloadGetAndClearStats can't distinguish who has ever called
+ // mBpfStatsMap#getValue and get a wrong calling count which counts all.
+ final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
+ final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+ coordinator.tetherOffloadRuleAdd(mIpServer, rule);
+ verifyTetherOffloadRuleAdd(inOrder, rule);
+ verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
+ true /* isInit */);
+
+ // Removing the last rule on current upstream immediately sends the cleanup stuff to netd.
+ updateStatsEntryForTetherOffloadGetAndClearStats(
+ buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
+ coordinator.tetherOffloadRuleRemove(mIpServer, rule);
+ verifyTetherOffloadRuleRemove(inOrder, rule);
+ verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
+ }
+
+ // TODO: remove once presubmit tests on R even the code is submitted on S.
+ @Test
+ public void testTetherOffloadRuleAddAndRemoveSdkR() throws Exception {
+ checkTetherOffloadRuleAddAndRemove(false /* R */);
+ }
+
+ // TODO: remove once presubmit tests on R even the code is submitted on S.
+ @Test
+ public void testTetherOffloadRuleAddAndRemoveAtLeastSdkS() throws Exception {
+ checkTetherOffloadRuleAddAndRemove(true /* S+ */);
+ }
+
+ // TODO: remove once presubmit tests on R even the code is submitted on S.
+ private void checkTetherOffloadGetStats(boolean usingApiS) throws Exception {
+ setupFunctioningNetdInterface();
+
+ doReturn(usingApiS).when(mDeps).isAtLeastS();
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ coordinator.startPolling();
+
+ final String mobileIface = "rmnet_data0";
+ final Integer mobileIfIndex = 100;
+ coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
+
+ updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] {
+ buildTestTetherStatsParcel(mobileIfIndex, 1000, 100, 2000, 200)});
+
+ final NetworkStats expectedIfaceStats = new NetworkStats(0L, 1)
+ .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 1000, 100, 2000, 200));
+
+ final NetworkStats expectedUidStats = new NetworkStats(0L, 1)
+ .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 1000, 100, 2000, 200));
+
+ mTetherStatsProvider.pushTetherStats();
+ mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStats, expectedUidStats);
+ }
+
+ // TODO: remove once presubmit tests on R even the code is submitted on S.
+ @Test
+ public void testTetherOffloadGetStatsSdkR() throws Exception {
+ checkTetherOffloadGetStats(false /* R */);
+ }
+
+ // TODO: remove once presubmit tests on R even the code is submitted on S.
+ @Test
+ public void testTetherOffloadGetStatsAtLeastSdkS() throws Exception {
+ checkTetherOffloadGetStats(true /* S+ */);
+ }
+
@Test
public void testGetForwardedStats() throws Exception {
setupFunctioningNetdInterface();
@@ -205,7 +633,7 @@
// [1] Both interface stats are changed.
// Setup the tether stats of wlan and mobile interface. Note that move forward the time of
// the looper to make sure the new tether stats has been updated by polling update thread.
- setTetherOffloadStatsList(new TetherStatsParcel[] {
+ updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] {
buildTestTetherStatsParcel(wlanIfIndex, 1000, 100, 2000, 200),
buildTestTetherStatsParcel(mobileIfIndex, 3000, 300, 4000, 400)});
@@ -226,7 +654,7 @@
// [2] Only one interface stats is changed.
// The tether stats of mobile interface is accumulated and The tether stats of wlan
// interface is the same.
- setTetherOffloadStatsList(new TetherStatsParcel[] {
+ updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] {
buildTestTetherStatsParcel(wlanIfIndex, 1000, 100, 2000, 200),
buildTestTetherStatsParcel(mobileIfIndex, 3010, 320, 4030, 440)});
@@ -247,12 +675,12 @@
// Shutdown the coordinator and clear the invocation history, especially the
// tetherOffloadGetStats() calls.
coordinator.stopPolling();
- clearInvocations(mNetd);
+ clearStatsInvocations();
// Verify the polling update thread stopped.
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
waitForIdle();
- verify(mNetd, never()).tetherOffloadGetStats();
+ verifyNeverTetherOffloadGetStats();
}
@Test
@@ -272,16 +700,14 @@
mTetherStatsProviderCb.expectNotifyAlertReached();
// Verify that notifyAlertReached never fired if quota is not yet reached.
- when(mNetd.tetherOffloadGetStats()).thenReturn(
- new TetherStatsParcel[] {buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0)});
+ updateStatsEntry(buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
mTetherStatsProvider.onSetAlert(100);
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
waitForIdle();
mTetherStatsProviderCb.assertNoCallback();
// Verify that notifyAlertReached fired when quota is reached.
- when(mNetd.tetherOffloadGetStats()).thenReturn(
- new TetherStatsParcel[] {buildTestTetherStatsParcel(mobileIfIndex, 50, 0, 50, 0)});
+ updateStatsEntry(buildTestTetherStatsParcel(mobileIfIndex, 50, 0, 50, 0));
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
waitForIdle();
mTetherStatsProviderCb.expectNotifyAlertReached();
@@ -339,6 +765,34 @@
}
@Test
+ public void testRuleMakeTetherDownstream6Key() throws Exception {
+ final Integer mobileIfIndex = 100;
+ final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+
+ final TetherDownstream6Key key = rule.makeTetherDownstream6Key();
+ assertEquals(key.iif, (long) mobileIfIndex);
+ assertEquals(key.dstMac, MacAddress.ALL_ZEROS_ADDRESS); // rawip upstream
+ assertTrue(Arrays.equals(key.neigh6, NEIGH_A.getAddress()));
+ // iif (4) + dstMac(6) + padding(2) + neigh6 (16) = 28.
+ assertEquals(28, key.writeToBytes().length);
+ }
+
+ @Test
+ public void testRuleMakeTether6Value() throws Exception {
+ final Integer mobileIfIndex = 100;
+ final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
+
+ final Tether6Value value = rule.makeTether6Value();
+ assertEquals(value.oif, DOWNSTREAM_IFINDEX);
+ assertEquals(value.ethDstMac, MAC_A);
+ assertEquals(value.ethSrcMac, DOWNSTREAM_MAC);
+ assertEquals(value.ethProto, ETH_P_IPV6);
+ assertEquals(value.pmtu, NetworkStackConstants.ETHER_MTU);
+ // oif (4) + ethDstMac (6) + ethSrcMac (6) + ethProto (2) + pmtu (2) = 20.
+ assertEquals(20, value.writeToBytes().length);
+ }
+
+ @Test
public void testSetDataLimit() throws Exception {
setupFunctioningNetdInterface();
@@ -352,10 +806,11 @@
// Set the unlimited quota as default if the service has never applied a data limit for a
// given upstream. Note that the data limit only be applied on an upstream which has rules.
final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
- final InOrder inOrder = inOrder(mNetd);
+ final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
coordinator.tetherOffloadRuleAdd(mIpServer, rule);
- inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(rule));
- inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, QUOTA_UNLIMITED);
+ verifyTetherOffloadRuleAdd(inOrder, rule);
+ verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
+ true /* isInit */);
inOrder.verifyNoMoreInteractions();
// [2] Specific limit.
@@ -363,7 +818,8 @@
for (final long quota : new long[] {0, 1048576000, Long.MAX_VALUE, QUOTA_UNLIMITED}) {
mTetherStatsProvider.onSetLimit(mobileIface, quota);
waitForIdle();
- inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, quota);
+ verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, quota,
+ false /* isInit */);
inOrder.verifyNoMoreInteractions();
}
@@ -381,7 +837,7 @@
// TODO: Test the case in which the rules are changed from different IpServer objects.
@Test
- public void testSetDataLimitOnRuleChange() throws Exception {
+ public void testSetDataLimitOnRule6Change() throws Exception {
setupFunctioningNetdInterface();
final BpfCoordinator coordinator = makeBpfCoordinator();
@@ -393,35 +849,35 @@
// Applying a data limit to the current upstream does not take any immediate action.
// The data limit could be only set on an upstream which has rules.
final long limit = 12345;
- final InOrder inOrder = inOrder(mNetd);
+ final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
mTetherStatsProvider.onSetLimit(mobileIface, limit);
waitForIdle();
- inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong());
+ verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
// Adding the first rule on current upstream immediately sends the quota to netd.
final Ipv6ForwardingRule ruleA = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
coordinator.tetherOffloadRuleAdd(mIpServer, ruleA);
- inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(ruleA));
- inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, limit);
+ verifyTetherOffloadRuleAdd(inOrder, ruleA);
+ verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, limit, true /* isInit */);
inOrder.verifyNoMoreInteractions();
// Adding the second rule on current upstream does not send the quota to netd.
final Ipv6ForwardingRule ruleB = buildTestForwardingRule(mobileIfIndex, NEIGH_B, MAC_B);
coordinator.tetherOffloadRuleAdd(mIpServer, ruleB);
- inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(ruleB));
- inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong());
+ verifyTetherOffloadRuleAdd(inOrder, ruleB);
+ verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
// Removing the second rule on current upstream does not send the quota to netd.
coordinator.tetherOffloadRuleRemove(mIpServer, ruleB);
- inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ruleB));
- inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong());
+ verifyTetherOffloadRuleRemove(inOrder, ruleB);
+ verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
// Removing the last rule on current upstream immediately sends the cleanup stuff to netd.
- when(mNetd.tetherOffloadGetAndClearStats(mobileIfIndex))
- .thenReturn(buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
+ updateStatsEntryForTetherOffloadGetAndClearStats(
+ buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0));
coordinator.tetherOffloadRuleRemove(mIpServer, ruleA);
- inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ruleA));
- inOrder.verify(mNetd).tetherOffloadGetAndClearStats(mobileIfIndex);
+ verifyTetherOffloadRuleRemove(inOrder, ruleA);
+ verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
inOrder.verifyNoMoreInteractions();
}
@@ -438,7 +894,8 @@
coordinator.addUpstreamNameToLookupTable(ethIfIndex, ethIface);
coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
- final InOrder inOrder = inOrder(mNetd);
+ final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfUpstream6Map, mBpfLimitMap,
+ mBpfStatsMap);
// Before the rule test, here are the additional actions while the rules are changed.
// - After adding the first rule on a given upstream, the coordinator adds a data limit.
@@ -455,37 +912,43 @@
ethIfIndex, NEIGH_B, MAC_B);
coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleA);
- inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(ethernetRuleA));
- inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(ethIfIndex, QUOTA_UNLIMITED);
-
+ verifyTetherOffloadRuleAdd(inOrder, ethernetRuleA);
+ verifyTetherOffloadSetInterfaceQuota(inOrder, ethIfIndex, QUOTA_UNLIMITED,
+ true /* isInit */);
+ verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, ethIfIndex);
coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleB);
- inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(ethernetRuleB));
+ verifyTetherOffloadRuleAdd(inOrder, ethernetRuleB);
// [2] Update the existing rules from Ethernet to cellular.
final Ipv6ForwardingRule mobileRuleA = buildTestForwardingRule(
mobileIfIndex, NEIGH_A, MAC_A);
final Ipv6ForwardingRule mobileRuleB = buildTestForwardingRule(
mobileIfIndex, NEIGH_B, MAC_B);
- when(mNetd.tetherOffloadGetAndClearStats(ethIfIndex))
- .thenReturn(buildTestTetherStatsParcel(ethIfIndex, 10, 20, 30, 40));
+ updateStatsEntryForTetherOffloadGetAndClearStats(
+ buildTestTetherStatsParcel(ethIfIndex, 10, 20, 30, 40));
// Update the existing rules for upstream changes. The rules are removed and re-added one
// by one for updating upstream interface index by #tetherOffloadRuleUpdate.
coordinator.tetherOffloadRuleUpdate(mIpServer, mobileIfIndex);
- inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ethernetRuleA));
- inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(mobileRuleA));
- inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, QUOTA_UNLIMITED);
- inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ethernetRuleB));
- inOrder.verify(mNetd).tetherOffloadGetAndClearStats(ethIfIndex);
- inOrder.verify(mNetd).tetherOffloadRuleAdd(matches(mobileRuleB));
+ verifyTetherOffloadRuleRemove(inOrder, ethernetRuleA);
+ verifyTetherOffloadRuleRemove(inOrder, ethernetRuleB);
+ verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+ verifyTetherOffloadGetAndClearStats(inOrder, ethIfIndex);
+ verifyTetherOffloadRuleAdd(inOrder, mobileRuleA);
+ verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
+ true /* isInit */);
+ verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
+ mobileIfIndex);
+ verifyTetherOffloadRuleAdd(inOrder, mobileRuleB);
// [3] Clear all rules for a given IpServer.
- when(mNetd.tetherOffloadGetAndClearStats(mobileIfIndex))
- .thenReturn(buildTestTetherStatsParcel(mobileIfIndex, 50, 60, 70, 80));
+ updateStatsEntryForTetherOffloadGetAndClearStats(
+ buildTestTetherStatsParcel(mobileIfIndex, 50, 60, 70, 80));
coordinator.tetherOffloadRuleClear(mIpServer);
- inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(mobileRuleA));
- inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(mobileRuleB));
- inOrder.verify(mNetd).tetherOffloadGetAndClearStats(mobileIfIndex);
+ verifyTetherOffloadRuleRemove(inOrder, mobileRuleA);
+ verifyTetherOffloadRuleRemove(inOrder, mobileRuleB);
+ verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC);
+ verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
// [4] Force pushing stats update to verify that the last diff of stats is reported on all
// upstreams.
@@ -499,18 +962,17 @@
.addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 50, 60, 70, 80)));
}
- @Test
- public void testTetheringConfigDisable() throws Exception {
- setupFunctioningNetdInterface();
- when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false);
-
+ private void checkBpfDisabled() throws Exception {
+ // The caller may mock the global dependencies |mDeps| which is used in
+ // #makeBpfCoordinator for testing.
+ // See #testBpfDisabledbyNoBpfDownstream6Map.
final BpfCoordinator coordinator = makeBpfCoordinator();
coordinator.startPolling();
// The tether stats polling task should not be scheduled.
mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
waitForIdle();
- verify(mNetd, never()).tetherOffloadGetStats();
+ verifyNeverTetherOffloadGetStats();
// The interface name lookup table can't be added.
final String iface = "rmnet_data0";
@@ -523,7 +985,7 @@
final MacAddress mac = MacAddress.fromString("00:00:00:00:00:0a");
final Ipv6ForwardingRule rule = buildTestForwardingRule(ifIndex, neigh, mac);
coordinator.tetherOffloadRuleAdd(mIpServer, rule);
- verify(mNetd, never()).tetherOffloadRuleAdd(any());
+ verifyNeverTetherOffloadRuleAdd();
LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules =
coordinator.getForwardingRulesForTesting().get(mIpServer);
assertNull(rules);
@@ -535,28 +997,157 @@
rules.put(rule.address, rule);
coordinator.getForwardingRulesForTesting().put(mIpServer, rules);
coordinator.tetherOffloadRuleRemove(mIpServer, rule);
- verify(mNetd, never()).tetherOffloadRuleRemove(any());
+ verifyNeverTetherOffloadRuleRemove();
rules = coordinator.getForwardingRulesForTesting().get(mIpServer);
assertNotNull(rules);
assertEquals(1, rules.size());
// The rule can't be cleared.
coordinator.tetherOffloadRuleClear(mIpServer);
- verify(mNetd, never()).tetherOffloadRuleRemove(any());
+ verifyNeverTetherOffloadRuleRemove();
rules = coordinator.getForwardingRulesForTesting().get(mIpServer);
assertNotNull(rules);
assertEquals(1, rules.size());
// The rule can't be updated.
coordinator.tetherOffloadRuleUpdate(mIpServer, rule.upstreamIfindex + 1 /* new */);
- verify(mNetd, never()).tetherOffloadRuleRemove(any());
- verify(mNetd, never()).tetherOffloadRuleAdd(any());
+ verifyNeverTetherOffloadRuleRemove();
+ verifyNeverTetherOffloadRuleAdd();
rules = coordinator.getForwardingRulesForTesting().get(mIpServer);
assertNotNull(rules);
assertEquals(1, rules.size());
}
@Test
+ public void testBpfDisabledbyConfig() throws Exception {
+ setupFunctioningNetdInterface();
+ when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(false);
+
+ checkBpfDisabled();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBpfDisabledbyNoBpfDownstream6Map() throws Exception {
+ setupFunctioningNetdInterface();
+ doReturn(null).when(mDeps).getBpfDownstream6Map();
+
+ checkBpfDisabled();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBpfDisabledbyNoBpfUpstream6Map() throws Exception {
+ setupFunctioningNetdInterface();
+ doReturn(null).when(mDeps).getBpfUpstream6Map();
+
+ checkBpfDisabled();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBpfDisabledbyNoBpfDownstream4Map() throws Exception {
+ setupFunctioningNetdInterface();
+ doReturn(null).when(mDeps).getBpfDownstream4Map();
+
+ checkBpfDisabled();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBpfDisabledbyNoBpfUpstream4Map() throws Exception {
+ setupFunctioningNetdInterface();
+ doReturn(null).when(mDeps).getBpfUpstream4Map();
+
+ checkBpfDisabled();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBpfDisabledbyNoBpfStatsMap() throws Exception {
+ setupFunctioningNetdInterface();
+ doReturn(null).when(mDeps).getBpfStatsMap();
+
+ checkBpfDisabled();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBpfDisabledbyNoBpfLimitMap() throws Exception {
+ setupFunctioningNetdInterface();
+ doReturn(null).when(mDeps).getBpfLimitMap();
+
+ checkBpfDisabled();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBpfMapClear() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ verify(mBpfDownstream4Map).clear();
+ verify(mBpfUpstream4Map).clear();
+ verify(mBpfDownstream6Map).clear();
+ verify(mBpfUpstream6Map).clear();
+ verify(mBpfStatsMap).clear();
+ verify(mBpfLimitMap).clear();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testAttachDetachBpfProgram() throws Exception {
+ setupFunctioningNetdInterface();
+
+ // Static mocking for BpfUtils.
+ MockitoSession mockSession = ExtendedMockito.mockitoSession()
+ .mockStatic(BpfUtils.class)
+ .startMocking();
+ try {
+ final String intIface1 = "wlan1";
+ final String intIface2 = "rndis0";
+ final String extIface = "rmnet_data0";
+ final BpfUtils mockMarkerBpfUtils = staticMockMarker(BpfUtils.class);
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ // [1] Add the forwarding pair <wlan1, rmnet_data0>. Expect that attach both wlan1 and
+ // rmnet_data0.
+ coordinator.maybeAttachProgram(intIface1, extIface);
+ ExtendedMockito.verify(() -> BpfUtils.attachProgram(extIface, DOWNSTREAM));
+ ExtendedMockito.verify(() -> BpfUtils.attachProgram(intIface1, UPSTREAM));
+ ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils);
+ ExtendedMockito.clearInvocations(mockMarkerBpfUtils);
+
+ // [2] Add the forwarding pair <wlan1, rmnet_data0> again. Expect no more action.
+ coordinator.maybeAttachProgram(intIface1, extIface);
+ ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils);
+ ExtendedMockito.clearInvocations(mockMarkerBpfUtils);
+
+ // [3] Add the forwarding pair <rndis0, rmnet_data0>. Expect that attach rndis0 only.
+ coordinator.maybeAttachProgram(intIface2, extIface);
+ ExtendedMockito.verify(() -> BpfUtils.attachProgram(intIface2, UPSTREAM));
+ ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils);
+ ExtendedMockito.clearInvocations(mockMarkerBpfUtils);
+
+ // [4] Remove the forwarding pair <rndis0, rmnet_data0>. Expect detach rndis0 only.
+ coordinator.maybeDetachProgram(intIface2, extIface);
+ ExtendedMockito.verify(() -> BpfUtils.detachProgram(intIface2));
+ ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils);
+ ExtendedMockito.clearInvocations(mockMarkerBpfUtils);
+
+ // [5] Remove the forwarding pair <wlan1, rmnet_data0>. Expect that detach both wlan1
+ // and rmnet_data0.
+ coordinator.maybeDetachProgram(intIface1, extIface);
+ ExtendedMockito.verify(() -> BpfUtils.detachProgram(extIface));
+ ExtendedMockito.verify(() -> BpfUtils.detachProgram(intIface1));
+ ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils);
+ ExtendedMockito.clearInvocations(mockMarkerBpfUtils);
+ } finally {
+ mockSession.finishMocking();
+ }
+ }
+
+ @Test
public void testTetheringConfigSetPollingInterval() throws Exception {
setupFunctioningNetdInterface();
@@ -590,18 +1181,268 @@
// Start on a new polling time slot.
mTestLooper.moveTimeForward(pollingInterval);
waitForIdle();
- clearInvocations(mNetd);
+ clearStatsInvocations();
// Move time forward to 90% polling interval time. Expect that the polling thread has not
// scheduled yet.
mTestLooper.moveTimeForward((long) (pollingInterval * 0.9));
waitForIdle();
- verify(mNetd, never()).tetherOffloadGetStats();
+ verifyNeverTetherOffloadGetStats();
// Move time forward to the remaining 10% polling interval time. Expect that the polling
// thread has scheduled.
mTestLooper.moveTimeForward((long) (pollingInterval * 0.1));
waitForIdle();
- verify(mNetd).tetherOffloadGetStats();
+ verifyTetherOffloadGetStats();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testStartStopConntrackMonitoring() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ // [1] Don't stop monitoring if it has never started.
+ coordinator.stopMonitoring(mIpServer);
+ verify(mConntrackMonitor, never()).start();
+
+ // [2] Start monitoring.
+ coordinator.startMonitoring(mIpServer);
+ verify(mConntrackMonitor).start();
+ clearInvocations(mConntrackMonitor);
+
+ // [3] Stop monitoring.
+ coordinator.stopMonitoring(mIpServer);
+ verify(mConntrackMonitor).stop();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.Q)
+ @IgnoreAfter(Build.VERSION_CODES.R)
+ // Only run this test on Android R.
+ public void testStartStopConntrackMonitoring_R() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ coordinator.startMonitoring(mIpServer);
+ verify(mConntrackMonitor, never()).start();
+
+ coordinator.stopMonitoring(mIpServer);
+ verify(mConntrackMonitor, never()).stop();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testStartStopConntrackMonitoringWithTwoDownstreamIfaces() throws Exception {
+ setupFunctioningNetdInterface();
+
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+
+ // [1] Start monitoring at the first IpServer adding.
+ coordinator.startMonitoring(mIpServer);
+ verify(mConntrackMonitor).start();
+ clearInvocations(mConntrackMonitor);
+
+ // [2] Don't start monitoring at the second IpServer adding.
+ coordinator.startMonitoring(mIpServer2);
+ verify(mConntrackMonitor, never()).start();
+
+ // [3] Don't stop monitoring if any downstream interface exists.
+ coordinator.stopMonitoring(mIpServer2);
+ verify(mConntrackMonitor, never()).stop();
+
+ // [4] Stop monitoring if no downstream exists.
+ coordinator.stopMonitoring(mIpServer);
+ verify(mConntrackMonitor).stop();
+ }
+
+ // Test network topology:
+ //
+ // public network (rawip) private network
+ // | UE |
+ // +------------+ V +------------+------------+ V +------------+
+ // | Sever +---------+ Upstream | Downstream +---------+ Client |
+ // +------------+ +------------+------------+ +------------+
+ // remote ip public ip private ip
+ // 140.112.8.116:443 100.81.179.1:62449 192.168.80.12:62449
+ //
+ private static final Inet4Address REMOTE_ADDR =
+ (Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116");
+ private static final Inet4Address PUBLIC_ADDR =
+ (Inet4Address) InetAddresses.parseNumericAddress("100.81.179.1");
+ private static final Inet4Address PRIVATE_ADDR =
+ (Inet4Address) InetAddresses.parseNumericAddress("192.168.80.12");
+
+ // IPv4-mapped IPv6 addresses
+ // Remote addrress ::ffff:140.112.8.116
+ // Public addrress ::ffff:100.81.179.1
+ // Private addrress ::ffff:192.168.80.12
+ private static final byte[] REMOTE_ADDR_V4MAPPED_BYTES = new byte[] {
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+ (byte) 0x8c, (byte) 0x70, (byte) 0x08, (byte) 0x74 };
+ private static final byte[] PUBLIC_ADDR_V4MAPPED_BYTES = new byte[] {
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+ (byte) 0x64, (byte) 0x51, (byte) 0xb3, (byte) 0x01 };
+ private static final byte[] PRIVATE_ADDR_V4MAPPED_BYTES = new byte[] {
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+ (byte) 0xc0, (byte) 0xa8, (byte) 0x50, (byte) 0x0c };
+
+ // Generally, public port and private port are the same in the NAT conntrack message.
+ // TODO: consider using different private port and public port for testing.
+ private static final short REMOTE_PORT = (short) 443;
+ private static final short PUBLIC_PORT = (short) 62449;
+ private static final short PRIVATE_PORT = (short) 62449;
+
+ @NonNull
+ private Tether4Key makeUpstream4Key(int proto) {
+ if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
+ fail("Not support protocol " + proto);
+ }
+ return new Tether4Key(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC, (short) proto,
+ PRIVATE_ADDR.getAddress(), REMOTE_ADDR.getAddress(), PRIVATE_PORT, REMOTE_PORT);
+ }
+
+ @NonNull
+ private Tether4Key makeDownstream4Key(int proto) {
+ if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
+ fail("Not support protocol " + proto);
+ }
+ return new Tether4Key(UPSTREAM_IFINDEX,
+ MacAddress.ALL_ZEROS_ADDRESS /* dstMac (rawip) */, (short) proto,
+ REMOTE_ADDR.getAddress(), PUBLIC_ADDR.getAddress(), REMOTE_PORT, PUBLIC_PORT);
+ }
+
+ @NonNull
+ private Tether4Value makeUpstream4Value() {
+ return new Tether4Value(UPSTREAM_IFINDEX,
+ MacAddress.ALL_ZEROS_ADDRESS /* ethDstMac (rawip) */,
+ MacAddress.ALL_ZEROS_ADDRESS /* ethSrcMac (rawip) */, ETH_P_IP,
+ NetworkStackConstants.ETHER_MTU, PUBLIC_ADDR_V4MAPPED_BYTES,
+ REMOTE_ADDR_V4MAPPED_BYTES, PUBLIC_PORT, REMOTE_PORT, 0 /* lastUsed */);
+ }
+
+ @NonNull
+ private Tether4Value makeDownstream4Value() {
+ return new Tether4Value(DOWNSTREAM_IFINDEX, MAC_A /* client mac */, DOWNSTREAM_MAC,
+ ETH_P_IP, NetworkStackConstants.ETHER_MTU, REMOTE_ADDR_V4MAPPED_BYTES,
+ PRIVATE_ADDR_V4MAPPED_BYTES, REMOTE_PORT, PRIVATE_PORT, 0 /* lastUsed */);
+ }
+
+ @NonNull
+ private ConntrackEvent makeTestConntrackEvent(short msgType, int proto) {
+ if (msgType != IPCTNL_MSG_CT_NEW && msgType != IPCTNL_MSG_CT_DELETE) {
+ fail("Not support message type " + msgType);
+ }
+ if (proto != IPPROTO_TCP && proto != IPPROTO_UDP) {
+ fail("Not support protocol " + proto);
+ }
+
+ final int status = (msgType == IPCTNL_MSG_CT_NEW) ? ESTABLISHED_MASK : DYING_MASK;
+ final int timeoutSec = (msgType == IPCTNL_MSG_CT_NEW) ? 100 /* nonzero, new */
+ : 0 /* unused, delete */;
+ return new ConntrackEvent(
+ (short) (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8 | msgType),
+ new Tuple(new TupleIpv4(PRIVATE_ADDR, REMOTE_ADDR),
+ new TupleProto((byte) proto, PRIVATE_PORT, REMOTE_PORT)),
+ new Tuple(new TupleIpv4(REMOTE_ADDR, PUBLIC_ADDR),
+ new TupleProto((byte) proto, REMOTE_PORT, PUBLIC_PORT)),
+ status,
+ timeoutSec);
+ }
+
+ private void setUpstreamInformationTo(final BpfCoordinator coordinator) {
+ final LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(UPSTREAM_IFACE);
+ lp.addLinkAddress(new LinkAddress(PUBLIC_ADDR, 32 /* prefix length */));
+ coordinator.addUpstreamIfindexToMap(lp);
+ }
+
+ private void setDownstreamAndClientInformationTo(final BpfCoordinator coordinator) {
+ final ClientInfo clientInfo = new ClientInfo(DOWNSTREAM_IFINDEX, DOWNSTREAM_MAC,
+ PRIVATE_ADDR, MAC_A /* client mac */);
+ coordinator.tetherOffloadClientAdd(mIpServer, clientInfo);
+ }
+
+ // TODO: Test the IPv4 and IPv6 exist concurrently.
+ // TODO: Test the IPv4 rule delete failed.
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testSetDataLimitOnRule4Change() throws Exception {
+ final BpfCoordinator coordinator = makeBpfCoordinator();
+ coordinator.startPolling();
+
+ // Needed because tetherOffloadRuleRemove of api31.BpfCoordinatorShimImpl only decreases
+ // the count while the entry is deleted. In the other words, deleteEntry returns true.
+ doReturn(true).when(mBpfDownstream4Map).deleteEntry(any());
+
+ // Needed because BpfCoordinator#addUpstreamIfindexToMap queries interface parameter for
+ // interface index.
+ doReturn(UPSTREAM_IFACE_PARAMS).when(mDeps).getInterfaceParams(UPSTREAM_IFACE);
+
+ coordinator.addUpstreamNameToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE);
+ setUpstreamInformationTo(coordinator);
+ setDownstreamAndClientInformationTo(coordinator);
+
+ // Applying a data limit to the current upstream does not take any immediate action.
+ // The data limit could be only set on an upstream which has rules.
+ final long limit = 12345;
+ final InOrder inOrder = inOrder(mNetd, mBpfUpstream4Map, mBpfDownstream4Map, mBpfLimitMap,
+ mBpfStatsMap);
+ mTetherStatsProvider.onSetLimit(UPSTREAM_IFACE, limit);
+ waitForIdle();
+ verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
+
+ // Build TCP and UDP rules for testing. Note that the values of {TCP, UDP} are the same
+ // because the protocol is not an element of the value. Consider using different address
+ // or port to make them different for better testing.
+ // TODO: Make the values of {TCP, UDP} rules different.
+ final Tether4Key expectedUpstream4KeyTcp = makeUpstream4Key(IPPROTO_TCP);
+ final Tether4Key expectedDownstream4KeyTcp = makeDownstream4Key(IPPROTO_TCP);
+ final Tether4Value expectedUpstream4ValueTcp = makeUpstream4Value();
+ final Tether4Value expectedDownstream4ValueTcp = makeDownstream4Value();
+
+ final Tether4Key expectedUpstream4KeyUdp = makeUpstream4Key(IPPROTO_UDP);
+ final Tether4Key expectedDownstream4KeyUdp = makeDownstream4Key(IPPROTO_UDP);
+ final Tether4Value expectedUpstream4ValueUdp = makeUpstream4Value();
+ final Tether4Value expectedDownstream4ValueUdp = makeDownstream4Value();
+
+ // [1] Adding the first rule on current upstream immediately sends the quota.
+ mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_TCP));
+ verifyTetherOffloadSetInterfaceQuota(inOrder, UPSTREAM_IFINDEX, limit, true /* isInit */);
+ inOrder.verify(mBpfUpstream4Map)
+ .insertEntry(eq(expectedUpstream4KeyTcp), eq(expectedUpstream4ValueTcp));
+ inOrder.verify(mBpfDownstream4Map)
+ .insertEntry(eq(expectedDownstream4KeyTcp), eq(expectedDownstream4ValueTcp));
+ inOrder.verifyNoMoreInteractions();
+
+ // [2] Adding the second rule on current upstream does not send the quota.
+ mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, IPPROTO_UDP));
+ verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
+ inOrder.verify(mBpfUpstream4Map)
+ .insertEntry(eq(expectedUpstream4KeyUdp), eq(expectedUpstream4ValueUdp));
+ inOrder.verify(mBpfDownstream4Map)
+ .insertEntry(eq(expectedDownstream4KeyUdp), eq(expectedDownstream4ValueUdp));
+ inOrder.verifyNoMoreInteractions();
+
+ // [3] Removing the second rule on current upstream does not send the quota.
+ mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, IPPROTO_UDP));
+ verifyNeverTetherOffloadSetInterfaceQuota(inOrder);
+ inOrder.verify(mBpfUpstream4Map).deleteEntry(eq(expectedUpstream4KeyUdp));
+ inOrder.verify(mBpfDownstream4Map).deleteEntry(eq(expectedDownstream4KeyUdp));
+ inOrder.verifyNoMoreInteractions();
+
+ // [4] Removing the last rule on current upstream immediately sends the cleanup stuff.
+ updateStatsEntryForTetherOffloadGetAndClearStats(
+ buildTestTetherStatsParcel(UPSTREAM_IFINDEX, 0, 0, 0, 0));
+ mConsumer.accept(makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, IPPROTO_TCP));
+ inOrder.verify(mBpfUpstream4Map).deleteEntry(eq(expectedUpstream4KeyTcp));
+ inOrder.verify(mBpfDownstream4Map).deleteEntry(eq(expectedDownstream4KeyTcp));
+ verifyTetherOffloadGetAndClearStats(inOrder, UPSTREAM_IFINDEX);
+ inOrder.verifyNoMoreInteractions();
}
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
index 354e753..8cfa7d0 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java
@@ -53,6 +53,8 @@
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.util.SharedLog;
import android.os.Bundle;
@@ -87,11 +89,13 @@
private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app";
private static final String PROVISIONING_APP_RESPONSE = "app_response";
+ private static final String TEST_PACKAGE_NAME = "com.android.tethering.test";
@Mock private CarrierConfigManager mCarrierConfigManager;
@Mock private Context mContext;
@Mock private Resources mResources;
@Mock private SharedLog mLog;
+ @Mock private PackageManager mPm;
@Mock private EntitlementManager.OnUiEntitlementFailedListener mEntitlementFailedListener;
// Like so many Android system APIs, these cannot be mocked because it is marked final.
@@ -182,7 +186,7 @@
}
@Before
- public void setUp() {
+ public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mMockingSession = mockitoSession()
.initMocks(this)
@@ -196,6 +200,9 @@
eq(EntitlementManager.DISABLE_PROVISIONING_SYSPROP_KEY), anyBoolean()));
doReturn(null).when(
() -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY), anyString()));
+ doReturn(mPm).when(mContext).getPackageManager();
+ doReturn(TEST_PACKAGE_NAME).when(mContext).getPackageName();
+ doReturn(new PackageInfo()).when(mPm).getPackageInfo(anyString(), anyInt());
when(mResources.getStringArray(R.array.config_tether_dhcp_range))
.thenReturn(new String[0]);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
new file mode 100644
index 0000000..d045bf1
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java
@@ -0,0 +1,366 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.tethering;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.IConnectivityManager;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkRequest;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+
+import androidx.annotation.Nullable;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Simulates upstream switching and sending NetworkCallbacks and CONNECTIVITY_ACTION broadcasts.
+ *
+ * Unlike any real networking code, this class is single-threaded and entirely synchronous.
+ * The effects of all method calls (including sending fake broadcasts, sending callbacks, etc.) are
+ * performed immediately on the caller's thread before returning.
+ *
+ * TODO: this duplicates a fair amount of code from ConnectivityManager and ConnectivityService.
+ * Consider using a ConnectivityService object instead, as used in ConnectivityServiceTest.
+ *
+ * Things to consider:
+ * - ConnectivityService uses a real handler for realism, and these test use TestLooper (or even
+ * invoke callbacks directly inline) for determinism. Using a real ConnectivityService would
+ * require adding dispatchAll() calls and migrating to handlers.
+ * - ConnectivityService does not provide a way to order CONNECTIVITY_ACTION before or after the
+ * NetworkCallbacks for the same network change. That ability is useful because the upstream
+ * selection code in Tethering is vulnerable to race conditions, due to its reliance on multiple
+ * separate NetworkCallbacks and BroadcastReceivers, each of which trigger different types of
+ * updates. If/when the upstream selection code is refactored to a more level-triggered model
+ * (e.g., with an idempotent function that takes into account all state every time any part of
+ * that state changes), this may become less important or unnecessary.
+ */
+public class TestConnectivityManager extends ConnectivityManager {
+ public static final boolean BROADCAST_FIRST = false;
+ public static final boolean CALLBACKS_FIRST = true;
+
+ final Map<NetworkCallback, NetworkRequestInfo> mAllCallbacks = new ArrayMap<>();
+ final Map<NetworkCallback, NetworkRequestInfo> mTrackingDefault = new ArrayMap<>();
+ final Map<NetworkCallback, NetworkRequestInfo> mListening = new ArrayMap<>();
+ final Map<NetworkCallback, NetworkRequestInfo> mRequested = new ArrayMap<>();
+ final Map<NetworkCallback, Integer> mLegacyTypeMap = new ArrayMap<>();
+
+ private final NetworkRequest mDefaultRequest;
+ private final Context mContext;
+
+ private int mNetworkId = 100;
+ private TestNetworkAgent mDefaultNetwork = null;
+
+ /**
+ * Constructs a TestConnectivityManager.
+ * @param ctx the context to use. Must be a fake or a mock because otherwise the test will
+ * attempt to send real broadcasts and resulting in permission denials.
+ * @param svc an IConnectivityManager. Should be a fake or a mock.
+ * @param defaultRequest the default NetworkRequest that will be used by Tethering.
+ */
+ public TestConnectivityManager(Context ctx, IConnectivityManager svc,
+ NetworkRequest defaultRequest) {
+ super(ctx, svc);
+ mContext = ctx;
+ mDefaultRequest = defaultRequest;
+ }
+
+ class NetworkRequestInfo {
+ public final NetworkRequest request;
+ public final Handler handler;
+ NetworkRequestInfo(NetworkRequest r, Handler h) {
+ request = r;
+ handler = h;
+ }
+ }
+
+ boolean hasNoCallbacks() {
+ return mAllCallbacks.isEmpty()
+ && mTrackingDefault.isEmpty()
+ && mListening.isEmpty()
+ && mRequested.isEmpty()
+ && mLegacyTypeMap.isEmpty();
+ }
+
+ boolean onlyHasDefaultCallbacks() {
+ return (mAllCallbacks.size() == 1)
+ && (mTrackingDefault.size() == 1)
+ && mListening.isEmpty()
+ && mRequested.isEmpty()
+ && mLegacyTypeMap.isEmpty();
+ }
+
+ boolean isListeningForAll() {
+ final NetworkCapabilities empty = new NetworkCapabilities();
+ empty.clearAll();
+
+ for (NetworkRequestInfo nri : mListening.values()) {
+ if (nri.request.networkCapabilities.equalRequestableCapabilities(empty)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ int getNetworkId() {
+ return ++mNetworkId;
+ }
+
+ private void sendDefaultNetworkBroadcasts(TestNetworkAgent formerDefault,
+ TestNetworkAgent defaultNetwork) {
+ if (formerDefault != null) {
+ sendConnectivityAction(formerDefault.legacyType, false /* connected */);
+ }
+ if (defaultNetwork != null) {
+ sendConnectivityAction(defaultNetwork.legacyType, true /* connected */);
+ }
+ }
+
+ private void sendDefaultNetworkCallbacks(TestNetworkAgent formerDefault,
+ TestNetworkAgent defaultNetwork) {
+ for (NetworkCallback cb : mTrackingDefault.keySet()) {
+ final NetworkRequestInfo nri = mTrackingDefault.get(cb);
+ if (defaultNetwork != null) {
+ nri.handler.post(() -> cb.onAvailable(defaultNetwork.networkId));
+ nri.handler.post(() -> cb.onCapabilitiesChanged(
+ defaultNetwork.networkId, defaultNetwork.networkCapabilities));
+ nri.handler.post(() -> cb.onLinkPropertiesChanged(
+ defaultNetwork.networkId, defaultNetwork.linkProperties));
+ } else if (formerDefault != null) {
+ nri.handler.post(() -> cb.onLost(formerDefault.networkId));
+ }
+ }
+ }
+
+ void makeDefaultNetwork(TestNetworkAgent agent, boolean order, @Nullable Runnable inBetween) {
+ if (Objects.equals(mDefaultNetwork, agent)) return;
+
+ final TestNetworkAgent formerDefault = mDefaultNetwork;
+ mDefaultNetwork = agent;
+
+ if (order == CALLBACKS_FIRST) {
+ sendDefaultNetworkCallbacks(formerDefault, mDefaultNetwork);
+ if (inBetween != null) inBetween.run();
+ sendDefaultNetworkBroadcasts(formerDefault, mDefaultNetwork);
+ } else {
+ sendDefaultNetworkBroadcasts(formerDefault, mDefaultNetwork);
+ if (inBetween != null) inBetween.run();
+ sendDefaultNetworkCallbacks(formerDefault, mDefaultNetwork);
+ }
+ }
+
+ void makeDefaultNetwork(TestNetworkAgent agent, boolean order) {
+ makeDefaultNetwork(agent, order, null /* inBetween */);
+ }
+
+ void makeDefaultNetwork(TestNetworkAgent agent) {
+ makeDefaultNetwork(agent, BROADCAST_FIRST, null /* inBetween */);
+ }
+
+ @Override
+ public void requestNetwork(NetworkRequest req, NetworkCallback cb, Handler h) {
+ assertFalse(mAllCallbacks.containsKey(cb));
+ mAllCallbacks.put(cb, new NetworkRequestInfo(req, h));
+ if (mDefaultRequest.equals(req)) {
+ assertFalse(mTrackingDefault.containsKey(cb));
+ mTrackingDefault.put(cb, new NetworkRequestInfo(req, h));
+ } else {
+ assertFalse(mRequested.containsKey(cb));
+ mRequested.put(cb, new NetworkRequestInfo(req, h));
+ }
+ }
+
+ @Override
+ public void requestNetwork(NetworkRequest req, NetworkCallback cb) {
+ fail("Should never be called.");
+ }
+
+ @Override
+ public void requestNetwork(NetworkRequest req,
+ int timeoutMs, int legacyType, Handler h, NetworkCallback cb) {
+ assertFalse(mAllCallbacks.containsKey(cb));
+ mAllCallbacks.put(cb, new NetworkRequestInfo(req, h));
+ assertFalse(mRequested.containsKey(cb));
+ mRequested.put(cb, new NetworkRequestInfo(req, h));
+ assertFalse(mLegacyTypeMap.containsKey(cb));
+ if (legacyType != ConnectivityManager.TYPE_NONE) {
+ mLegacyTypeMap.put(cb, legacyType);
+ }
+ }
+
+ @Override
+ public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb, Handler h) {
+ assertFalse(mAllCallbacks.containsKey(cb));
+ mAllCallbacks.put(cb, new NetworkRequestInfo(req, h));
+ assertFalse(mListening.containsKey(cb));
+ mListening.put(cb, new NetworkRequestInfo(req, h));
+ }
+
+ @Override
+ public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb) {
+ fail("Should never be called.");
+ }
+
+ @Override
+ public void registerDefaultNetworkCallback(NetworkCallback cb, Handler h) {
+ fail("Should never be called.");
+ }
+
+ @Override
+ public void registerDefaultNetworkCallback(NetworkCallback cb) {
+ fail("Should never be called.");
+ }
+
+ @Override
+ public void unregisterNetworkCallback(NetworkCallback cb) {
+ if (mTrackingDefault.containsKey(cb)) {
+ mTrackingDefault.remove(cb);
+ } else if (mListening.containsKey(cb)) {
+ mListening.remove(cb);
+ } else if (mRequested.containsKey(cb)) {
+ mRequested.remove(cb);
+ mLegacyTypeMap.remove(cb);
+ } else {
+ fail("Unexpected callback removed");
+ }
+ mAllCallbacks.remove(cb);
+
+ assertFalse(mAllCallbacks.containsKey(cb));
+ assertFalse(mTrackingDefault.containsKey(cb));
+ assertFalse(mListening.containsKey(cb));
+ assertFalse(mRequested.containsKey(cb));
+ }
+
+ private void sendConnectivityAction(int type, boolean connected) {
+ NetworkInfo ni = new NetworkInfo(type, 0 /* subtype */, getNetworkTypeName(type),
+ "" /* subtypeName */);
+ NetworkInfo.DetailedState state = connected
+ ? NetworkInfo.DetailedState.CONNECTED
+ : NetworkInfo.DetailedState.DISCONNECTED;
+ ni.setDetailedState(state, "" /* reason */, "" /* extraInfo */);
+ Intent intent = new Intent(CONNECTIVITY_ACTION);
+ intent.putExtra(EXTRA_NETWORK_INFO, ni);
+ mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
+ public static class TestNetworkAgent {
+ public final TestConnectivityManager cm;
+ public final Network networkId;
+ public final NetworkCapabilities networkCapabilities;
+ public final LinkProperties linkProperties;
+ // TODO: delete when tethering no longer uses CONNECTIVITY_ACTION.
+ public final int legacyType;
+
+ public TestNetworkAgent(TestConnectivityManager cm, NetworkCapabilities nc) {
+ this.cm = cm;
+ this.networkId = new Network(cm.getNetworkId());
+ networkCapabilities = copy(nc);
+ linkProperties = new LinkProperties();
+ legacyType = toLegacyType(nc);
+ }
+
+ public TestNetworkAgent(TestConnectivityManager cm, UpstreamNetworkState state) {
+ this.cm = cm;
+ networkId = state.network;
+ networkCapabilities = state.networkCapabilities;
+ linkProperties = state.linkProperties;
+ this.legacyType = toLegacyType(networkCapabilities);
+ }
+
+ private static int toLegacyType(NetworkCapabilities nc) {
+ for (int type = 0; type < ConnectivityManager.TYPE_TEST; type++) {
+ if (matchesLegacyType(nc, type)) return type;
+ }
+ throw new IllegalArgumentException(("Can't determine legacy type for: ") + nc);
+ }
+
+ private static boolean matchesLegacyType(NetworkCapabilities nc, int legacyType) {
+ final NetworkCapabilities typeNc;
+ try {
+ typeNc = ConnectivityManager.networkCapabilitiesForType(legacyType);
+ } catch (IllegalArgumentException e) {
+ // networkCapabilitiesForType does not support all legacy types.
+ return false;
+ }
+ return typeNc.satisfiedByNetworkCapabilities(nc);
+ }
+
+ private boolean matchesLegacyType(int legacyType) {
+ return matchesLegacyType(networkCapabilities, legacyType);
+ }
+
+ public void fakeConnect() {
+ for (NetworkRequestInfo nri : cm.mRequested.values()) {
+ if (matchesLegacyType(nri.request.legacyType)) {
+ cm.sendConnectivityAction(legacyType, true /* connected */);
+ // In practice, a given network can match only one legacy type.
+ break;
+ }
+ }
+ for (NetworkCallback cb : cm.mListening.keySet()) {
+ final NetworkRequestInfo nri = cm.mListening.get(cb);
+ nri.handler.post(() -> cb.onAvailable(networkId));
+ nri.handler.post(() -> cb.onCapabilitiesChanged(
+ networkId, copy(networkCapabilities)));
+ nri.handler.post(() -> cb.onLinkPropertiesChanged(networkId, copy(linkProperties)));
+ }
+ // mTrackingDefault will be updated if/when the caller calls makeDefaultNetwork
+ }
+
+ public void fakeDisconnect() {
+ for (NetworkRequestInfo nri : cm.mRequested.values()) {
+ if (matchesLegacyType(nri.request.legacyType)) {
+ cm.sendConnectivityAction(legacyType, false /* connected */);
+ break;
+ }
+ }
+ for (NetworkCallback cb : cm.mListening.keySet()) {
+ cb.onLost(networkId);
+ }
+ // mTrackingDefault will be updated if/when the caller calls makeDefaultNetwork
+ }
+
+ public void sendLinkProperties() {
+ for (NetworkCallback cb : cm.mListening.keySet()) {
+ cb.onLinkPropertiesChanged(networkId, copy(linkProperties));
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("TestNetworkAgent: %s %s", networkId, networkCapabilities);
+ }
+ }
+
+ static NetworkCapabilities copy(NetworkCapabilities nc) {
+ return new NetworkCapabilities(nc);
+ }
+
+ static LinkProperties copy(LinkProperties lp) {
+ return new LinkProperties(lp);
+ }
+}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index 237e2c2..1f4e371 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -30,12 +30,16 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.when;
import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.util.SharedLog;
+import android.os.Build;
import android.provider.DeviceConfig;
import android.telephony.TelephonyManager;
@@ -43,9 +47,14 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.net.module.util.DeviceConfigUtils;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -60,13 +69,18 @@
public class TetheringConfigurationTest {
private final SharedLog mLog = new SharedLog("TetheringConfigurationTest");
+ @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app";
private static final String PROVISIONING_APP_RESPONSE = "app_response";
+ private static final String TEST_PACKAGE_NAME = "com.android.tethering.test";
+ private static final long TEST_PACKAGE_VERSION = 1234L;
@Mock private Context mContext;
@Mock private TelephonyManager mTelephonyManager;
@Mock private Resources mResources;
@Mock private Resources mResourcesForSubId;
+ @Mock private PackageManager mPackageManager;
private Context mMockContext;
private boolean mHasTelephonyManager;
private boolean mEnableLegacyDhcpServer;
@@ -100,6 +114,16 @@
}
return super.getSystemService(name);
}
+
+ @Override
+ public PackageManager getPackageManager() {
+ return mPackageManager;
+ }
+
+ @Override
+ public String getPackageName() {
+ return TEST_PACKAGE_NAME;
+ }
}
@Before
@@ -110,9 +134,15 @@
.mockStatic(DeviceConfig.class)
.strictness(Strictness.WARN)
.startMocking();
+ DeviceConfigUtils.resetPackageVersionCacheForTest();
doReturn(null).when(
() -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
eq(TetheringConfiguration.TETHER_ENABLE_LEGACY_DHCP_SERVER)));
+ setTetherForceUpstreamAutomaticFlagVersion(null);
+
+ final PackageInfo pi = new PackageInfo();
+ pi.setLongVersionCode(TEST_PACKAGE_VERSION);
+ doReturn(pi).when(mPackageManager).getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt());
when(mResources.getStringArray(R.array.config_tether_dhcp_range)).thenReturn(
new String[0]);
@@ -141,6 +171,7 @@
@After
public void tearDown() throws Exception {
mMockingSession.finishMocking();
+ DeviceConfigUtils.resetPackageVersionCacheForTest();
}
private TetheringConfiguration getTetheringConfiguration(int... legacyTetherUpstreamTypes) {
@@ -455,4 +486,52 @@
mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
assertTrue(testEnable.isSelectAllPrefixRangeEnabled());
}
+
+ @Test
+ public void testChooseUpstreamAutomatically() throws Exception {
+ when(mResources.getBoolean(R.bool.config_tether_upstream_automatic))
+ .thenReturn(true);
+ assertChooseUpstreamAutomaticallyIs(true);
+
+ when(mResources.getBoolean(R.bool.config_tether_upstream_automatic))
+ .thenReturn(false);
+ assertChooseUpstreamAutomaticallyIs(false);
+ }
+
+ // The flag override only works on R-
+ @Test @IgnoreAfter(Build.VERSION_CODES.R)
+ public void testChooseUpstreamAutomatically_FlagOverride() throws Exception {
+ when(mResources.getBoolean(R.bool.config_tether_upstream_automatic))
+ .thenReturn(false);
+ setTetherForceUpstreamAutomaticFlagVersion(TEST_PACKAGE_VERSION - 1);
+ assertTrue(DeviceConfigUtils.isFeatureEnabled(mMockContext, NAMESPACE_CONNECTIVITY,
+ TetheringConfiguration.TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION));
+
+ assertChooseUpstreamAutomaticallyIs(true);
+
+ setTetherForceUpstreamAutomaticFlagVersion(0L);
+ assertChooseUpstreamAutomaticallyIs(false);
+
+ setTetherForceUpstreamAutomaticFlagVersion(Long.MAX_VALUE);
+ assertChooseUpstreamAutomaticallyIs(false);
+ }
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testChooseUpstreamAutomatically_FlagOverrideAfterR() throws Exception {
+ when(mResources.getBoolean(R.bool.config_tether_upstream_automatic))
+ .thenReturn(false);
+ setTetherForceUpstreamAutomaticFlagVersion(TEST_PACKAGE_VERSION - 1);
+ assertChooseUpstreamAutomaticallyIs(false);
+ }
+
+ private void setTetherForceUpstreamAutomaticFlagVersion(Long version) {
+ doReturn(version == null ? null : Long.toString(version)).when(
+ () -> DeviceConfig.getProperty(eq(NAMESPACE_CONNECTIVITY),
+ eq(TetheringConfiguration.TETHER_FORCE_UPSTREAM_AUTOMATIC_VERSION)));
+ }
+
+ private void assertChooseUpstreamAutomaticallyIs(boolean value) {
+ assertEquals(value, new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID)
+ .chooseUpstreamAutomatically);
+ }
}
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 53c35f8..b207af9 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -25,6 +25,9 @@
import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
+import static android.net.ConnectivityManager.TYPE_NONE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
@@ -57,6 +60,8 @@
import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH;
import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH;
+import static com.android.networkstack.tethering.TestConnectivityManager.BROADCAST_FIRST;
+import static com.android.networkstack.tethering.TestConnectivityManager.CALLBACKS_FIRST;
import static com.android.networkstack.tethering.Tethering.UserRestrictionActionListener;
import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE;
import static com.android.networkstack.tethering.UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES;
@@ -65,6 +70,7 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.argThat;
@@ -73,7 +79,9 @@
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
@@ -98,10 +106,11 @@
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.hardware.usb.UsbManager;
-import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
import android.net.EthernetManager;
import android.net.EthernetManager.TetheredInterfaceCallback;
import android.net.EthernetManager.TetheredInterfaceRequest;
+import android.net.IConnectivityManager;
import android.net.IIntResultListener;
import android.net.INetd;
import android.net.ITetheringEventCallback;
@@ -163,6 +172,7 @@
import com.android.internal.util.StateMachine;
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
import com.android.testutils.MiscAsserts;
import org.junit.After;
@@ -172,6 +182,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -204,6 +215,10 @@
private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
private static final String PROVISIONING_NO_UI_APP_NAME = "no_ui_app";
+ private static final int CELLULAR_NETID = 100;
+ private static final int WIFI_NETID = 101;
+ private static final int DUN_NETID = 102;
+
private static final int DHCPSERVER_START_TIMEOUT_MS = 1000;
@Mock private ApplicationInfo mApplicationInfo;
@@ -216,7 +231,6 @@
@Mock private UsbManager mUsbManager;
@Mock private WifiManager mWifiManager;
@Mock private CarrierConfigManager mCarrierConfigManager;
- @Mock private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
@Mock private IPv6TetheringCoordinator mIPv6TetheringCoordinator;
@Mock private DadProxy mDadProxy;
@Mock private RouterAdvertisementDaemon mRouterAdvertisementDaemon;
@@ -224,8 +238,6 @@
@Mock private IDhcpServer mDhcpServer;
@Mock private INetd mNetd;
@Mock private UserManager mUserManager;
- @Mock private NetworkRequest mNetworkRequest;
- @Mock private ConnectivityManager mCm;
@Mock private EthernetManager mEm;
@Mock private TetheringNotificationUpdater mNotificationUpdater;
@Mock private BpfCoordinator mBpfCoordinator;
@@ -255,6 +267,11 @@
private OffloadController mOffloadCtrl;
private PrivateAddressCoordinator mPrivateAddressCoordinator;
private SoftApCallback mSoftApCallback;
+ private UpstreamNetworkMonitor mUpstreamNetworkMonitor;
+
+ private TestConnectivityManager mCm;
+ private NetworkRequest mNetworkRequest;
+ private NetworkCallback mDefaultNetworkCallback;
private class TestContext extends BroadcastInterceptingContext {
TestContext(Context base) {
@@ -323,14 +340,15 @@
assertTrue("Non-mocked interface " + ifName,
ifName.equals(TEST_USB_IFNAME)
|| ifName.equals(TEST_WLAN_IFNAME)
+ || ifName.equals(TEST_WIFI_IFNAME)
|| ifName.equals(TEST_MOBILE_IFNAME)
|| ifName.equals(TEST_P2P_IFNAME)
|| ifName.equals(TEST_NCM_IFNAME)
|| ifName.equals(TEST_ETH_IFNAME)
|| ifName.equals(TEST_BT_IFNAME));
final String[] ifaces = new String[] {
- TEST_USB_IFNAME, TEST_WLAN_IFNAME, TEST_MOBILE_IFNAME, TEST_P2P_IFNAME,
- TEST_NCM_IFNAME, TEST_ETH_IFNAME};
+ TEST_USB_IFNAME, TEST_WLAN_IFNAME, TEST_WIFI_IFNAME, TEST_MOBILE_IFNAME,
+ TEST_P2P_IFNAME, TEST_NCM_IFNAME, TEST_ETH_IFNAME};
return new InterfaceParams(ifName, ArrayUtils.indexOf(ifaces, ifName) + IFINDEX_OFFSET,
MacAddress.ALL_ZEROS_ADDRESS);
}
@@ -365,6 +383,11 @@
}
@Override
+ protected boolean isFeatureEnabled(Context ctx, String featureVersionFlag) {
+ return false;
+ }
+
+ @Override
protected Resources getResourcesForSubIdWrapper(Context ctx, int subId) {
return mResources;
}
@@ -402,7 +425,10 @@
@Override
public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx,
StateMachine target, SharedLog log, int what) {
+ // Use a real object instead of a mock so that some tests can use a real UNM and some
+ // can use a mock.
mUpstreamNetworkMonitorSM = target;
+ mUpstreamNetworkMonitor = spy(super.getUpstreamNetworkMonitor(ctx, target, log, what));
return mUpstreamNetworkMonitor;
}
@@ -481,15 +507,16 @@
}
}
- private static UpstreamNetworkState buildMobileUpstreamState(boolean withIPv4,
- boolean withIPv6, boolean with464xlat) {
+ private static LinkProperties buildUpstreamLinkProperties(String interfaceName,
+ boolean withIPv4, boolean withIPv6, boolean with464xlat) {
final LinkProperties prop = new LinkProperties();
- prop.setInterfaceName(TEST_MOBILE_IFNAME);
+ prop.setInterfaceName(interfaceName);
if (withIPv4) {
+ prop.addLinkAddress(new LinkAddress("10.1.2.3/15"));
prop.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0),
InetAddresses.parseNumericAddress("10.0.0.1"),
- TEST_MOBILE_IFNAME, RTN_UNICAST));
+ interfaceName, RTN_UNICAST));
}
if (withIPv6) {
@@ -499,23 +526,40 @@
NetworkConstants.RFC7421_PREFIX_LENGTH));
prop.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0),
InetAddresses.parseNumericAddress("2001:db8::1"),
- TEST_MOBILE_IFNAME, RTN_UNICAST));
+ interfaceName, RTN_UNICAST));
}
if (with464xlat) {
+ final String clatInterface = "v4-" + interfaceName;
final LinkProperties stackedLink = new LinkProperties();
- stackedLink.setInterfaceName(TEST_XLAT_MOBILE_IFNAME);
+ stackedLink.setInterfaceName(clatInterface);
stackedLink.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0),
InetAddresses.parseNumericAddress("192.0.0.1"),
- TEST_XLAT_MOBILE_IFNAME, RTN_UNICAST));
+ clatInterface, RTN_UNICAST));
prop.addStackedLink(stackedLink);
}
+ return prop;
+ }
- final NetworkCapabilities capabilities = new NetworkCapabilities()
- .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
- return new UpstreamNetworkState(prop, capabilities, new Network(100));
+ private static NetworkCapabilities buildUpstreamCapabilities(int transport, int... otherCaps) {
+ // TODO: add NOT_VCN_MANAGED.
+ final NetworkCapabilities nc = new NetworkCapabilities()
+ .addTransportType(transport)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ for (int cap : otherCaps) {
+ nc.addCapability(cap);
+ }
+ return nc;
+ }
+
+ private static UpstreamNetworkState buildMobileUpstreamState(boolean withIPv4,
+ boolean withIPv6, boolean with464xlat) {
+ return new UpstreamNetworkState(
+ buildUpstreamLinkProperties(TEST_MOBILE_IFNAME, withIPv4, withIPv6, with464xlat),
+ buildUpstreamCapabilities(TRANSPORT_CELLULAR),
+ new Network(CELLULAR_NETID));
}
private static UpstreamNetworkState buildMobileIPv4UpstreamState() {
@@ -534,6 +578,22 @@
return buildMobileUpstreamState(false, true, true);
}
+ private static UpstreamNetworkState buildWifiUpstreamState() {
+ return new UpstreamNetworkState(
+ buildUpstreamLinkProperties(TEST_WIFI_IFNAME, true /* IPv4 */, true /* IPv6 */,
+ false /* 464xlat */),
+ buildUpstreamCapabilities(TRANSPORT_WIFI),
+ new Network(WIFI_NETID));
+ }
+
+ private static UpstreamNetworkState buildDunUpstreamState() {
+ return new UpstreamNetworkState(
+ buildUpstreamLinkProperties(TEST_MOBILE_IFNAME, true /* IPv4 */, true /* IPv6 */,
+ false /* 464xlat */),
+ buildUpstreamCapabilities(TRANSPORT_CELLULAR, NET_CAPABILITY_DUN),
+ new Network(DUN_NETID));
+ }
+
// See FakeSettingsProvider#clearSettingsProvider() that this needs to be called before and
// after use.
@BeforeClass
@@ -567,6 +627,7 @@
when(mOffloadHardwareInterface.getForwardedStats(any())).thenReturn(mForwardedStats);
mServiceContext = new TestContext(mContext);
+ mServiceContext.setUseRegisteredHandlers(true);
mContentResolver = new MockContentResolver(mServiceContext);
mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
setTetheringSupported(true /* supported */);
@@ -579,9 +640,22 @@
};
mServiceContext.registerReceiver(mBroadcastReceiver,
new IntentFilter(ACTION_TETHER_STATE_CHANGED));
+
+ // TODO: add NOT_VCN_MANAGED here, but more importantly in the production code.
+ // TODO: even better, change TetheringDependencies.getDefaultNetworkRequest() to use
+ // registerSystemDefaultNetworkCallback() on S and above.
+ NetworkCapabilities defaultCaps = new NetworkCapabilities()
+ .addCapability(NET_CAPABILITY_INTERNET);
+ mNetworkRequest = new NetworkRequest(defaultCaps, TYPE_NONE, 1 /* requestId */,
+ NetworkRequest.Type.REQUEST);
+ mCm = spy(new TestConnectivityManager(mServiceContext, mock(IConnectivityManager.class),
+ mNetworkRequest));
+
mTethering = makeTethering();
verify(mStatsManager, times(1)).registerNetworkStatsProvider(anyString(), any());
verify(mNetd).registerUnsolicitedEventListener(any());
+ verifyDefaultNetworkRequestFiled();
+
final ArgumentCaptor<PhoneStateListener> phoneListenerCaptor =
ArgumentCaptor.forClass(PhoneStateListener.class);
verify(mTelephonyManager).listen(phoneListenerCaptor.capture(),
@@ -592,6 +666,9 @@
ArgumentCaptor.forClass(SoftApCallback.class);
verify(mWifiManager).registerSoftApCallback(any(), softApCallbackCaptor.capture());
mSoftApCallback = softApCallbackCaptor.getValue();
+
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)).thenReturn(true);
}
private void setTetheringSupported(final boolean supported) {
@@ -615,8 +692,8 @@
}
private void initTetheringUpstream(UpstreamNetworkState upstreamState) {
- when(mUpstreamNetworkMonitor.getCurrentPreferredUpstream()).thenReturn(upstreamState);
- when(mUpstreamNetworkMonitor.selectPreferredUpstreamType(any())).thenReturn(upstreamState);
+ doReturn(upstreamState).when(mUpstreamNetworkMonitor).getCurrentPreferredUpstream();
+ doReturn(upstreamState).when(mUpstreamNetworkMonitor).selectPreferredUpstreamType(any());
}
private Tethering makeTethering() {
@@ -649,6 +726,7 @@
final Intent intent = new Intent(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
intent.putExtra(EXTRA_WIFI_AP_STATE, state);
mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ mLooper.dispatchAll();
}
private void sendWifiApStateChanged(int state, String ifname, int ipmode) {
@@ -657,6 +735,7 @@
intent.putExtra(EXTRA_WIFI_AP_INTERFACE_NAME, ifname);
intent.putExtra(EXTRA_WIFI_AP_MODE, ipmode);
mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ mLooper.dispatchAll();
}
private static final String[] P2P_RECEIVER_PERMISSIONS_FOR_BROADCAST = {
@@ -683,6 +762,7 @@
mServiceContext.sendBroadcastAsUserMultiplePermissions(intent, UserHandle.ALL,
P2P_RECEIVER_PERMISSIONS_FOR_BROADCAST);
+ mLooper.dispatchAll();
}
private void sendUsbBroadcast(boolean connected, boolean configured, boolean function,
@@ -696,11 +776,26 @@
intent.putExtra(USB_FUNCTION_NCM, function);
}
mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ mLooper.dispatchAll();
}
private void sendConfigurationChanged() {
final Intent intent = new Intent(Intent.ACTION_CONFIGURATION_CHANGED);
mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ mLooper.dispatchAll();
+ }
+
+ private void verifyDefaultNetworkRequestFiled() {
+ ArgumentCaptor<NetworkCallback> captor = ArgumentCaptor.forClass(NetworkCallback.class);
+ verify(mCm, times(1)).requestNetwork(eq(mNetworkRequest),
+ captor.capture(), any(Handler.class));
+ mDefaultNetworkCallback = captor.getValue();
+ assertNotNull(mDefaultNetworkCallback);
+
+ // The default network request is only ever filed once.
+ verifyNoMoreInteractions(mCm);
+ mUpstreamNetworkMonitor.startTrackDefaultNetwork(mNetworkRequest, mEntitleMgr);
+ verifyNoMoreInteractions(mCm);
}
private void verifyInterfaceServingModeStarted(String ifname) throws Exception {
@@ -729,7 +824,6 @@
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
}
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED);
- mLooper.dispatchAll();
// If, and only if, Tethering received an interface status changed then
// it creates a IpServer and sends out a broadcast indicating that the
@@ -754,9 +848,7 @@
mTethering.interfaceStatusChanged(TEST_NCM_IFNAME, true);
}
- private void prepareUsbTethering(UpstreamNetworkState upstreamState) {
- initTetheringUpstream(upstreamState);
-
+ private void prepareUsbTethering() {
// Emulate pressing the USB tethering button in Settings UI.
final TetheringRequestParcel request = createTetheringRequestParcel(TETHERING_USB);
mTethering.startTethering(request, null);
@@ -771,14 +863,14 @@
@Test
public void testUsbConfiguredBroadcastStartsTethering() throws Exception {
UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState();
- prepareUsbTethering(upstreamState);
+ initTetheringUpstream(upstreamState);
+ prepareUsbTethering();
// This should produce no activity of any kind.
verifyNoMoreInteractions(mNetd);
// Pretend we then receive USB configured broadcast.
sendUsbBroadcast(true, true, true, TETHERING_USB);
- mLooper.dispatchAll();
// Now we should see the start of tethering mechanics (in this case:
// tetherMatchingInterfaces() which starts by fetching all interfaces).
verify(mNetd, times(1)).interfaceGetList();
@@ -807,7 +899,6 @@
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
}
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_LOCAL_ONLY);
- mLooper.dispatchAll();
verifyInterfaceServingModeStarted(TEST_WLAN_IFNAME);
verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
@@ -866,9 +957,9 @@
}
private void runUsbTethering(UpstreamNetworkState upstreamState) {
- prepareUsbTethering(upstreamState);
+ initTetheringUpstream(upstreamState);
+ prepareUsbTethering();
sendUsbBroadcast(true, true, true, TETHERING_USB);
- mLooper.dispatchAll();
}
private void assertSetIfaceToDadProxy(final int numOfCalls, final String ifaceName) {
@@ -1007,10 +1098,108 @@
verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(upstreamState.network);
}
+ @Test
+ public void testAutomaticUpstreamSelection() throws Exception {
+ // Enable automatic upstream selection.
+ when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(true);
+ sendConfigurationChanged();
+ mLooper.dispatchAll();
+
+ InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor);
+
+ // Start USB tethering with no current upstream.
+ prepareUsbTethering();
+ sendUsbBroadcast(true, true, true, TETHERING_USB);
+ inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks();
+ inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true);
+
+ // Pretend cellular connected and expect the upstream to be set.
+ TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
+ mobile.fakeConnect();
+ mCm.makeDefaultNetwork(mobile, BROADCAST_FIRST);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId);
+
+ // Switch upstreams a few times.
+ TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState());
+ wifi.fakeConnect();
+ mCm.makeDefaultNetwork(wifi, BROADCAST_FIRST);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId);
+
+ // This code has historically been racy, so test different orderings of CONNECTIVITY_ACTION
+ // broadcasts and callbacks, and add mLooper.dispatchAll() calls between the two.
+ final Runnable doDispatchAll = () -> mLooper.dispatchAll();
+
+ mCm.makeDefaultNetwork(mobile, BROADCAST_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId);
+
+ mCm.makeDefaultNetwork(wifi, BROADCAST_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId);
+
+ mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId);
+
+ mCm.makeDefaultNetwork(wifi, CALLBACKS_FIRST);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId);
+
+ mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId);
+
+ // Wifi disconnecting should not have any affect since it's not the current upstream.
+ wifi.fakeDisconnect();
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor, never()).setCurrentUpstream(any());
+
+ // Lose and regain upstream.
+ assertTrue(mUpstreamNetworkMonitor.getCurrentPreferredUpstream().linkProperties
+ .hasIPv4Address());
+ mCm.makeDefaultNetwork(null, BROADCAST_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ mobile.fakeDisconnect();
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null);
+
+ mobile = new TestNetworkAgent(mCm, buildMobile464xlatUpstreamState());
+ mobile.fakeConnect();
+ mCm.makeDefaultNetwork(mobile, BROADCAST_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId);
+
+ // Check the IP addresses to ensure that the upstream is indeed not the same as the previous
+ // mobile upstream, even though the netId is (unrealistically) the same.
+ assertFalse(mUpstreamNetworkMonitor.getCurrentPreferredUpstream().linkProperties
+ .hasIPv4Address());
+
+ // Lose and regain upstream again.
+ mCm.makeDefaultNetwork(null, CALLBACKS_FIRST, doDispatchAll);
+ mobile.fakeDisconnect();
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null);
+
+ mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState());
+ mobile.fakeConnect();
+ mCm.makeDefaultNetwork(mobile, CALLBACKS_FIRST, doDispatchAll);
+ mLooper.dispatchAll();
+ inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId);
+
+ assertTrue(mUpstreamNetworkMonitor.getCurrentPreferredUpstream().linkProperties
+ .hasIPv4Address());
+
+ // Check that the code does not crash if onLinkPropertiesChanged is received after onLost.
+ mobile.fakeDisconnect();
+ mobile.sendLinkProperties();
+ mLooper.dispatchAll();
+ }
+
private void runNcmTethering() {
prepareNcmTethering();
sendUsbBroadcast(true, true, true, TETHERING_NCM);
- mLooper.dispatchAll();
}
@Test
@@ -1058,7 +1247,6 @@
// tethering mode is to be started.
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED);
- mLooper.dispatchAll();
// There is 1 IpServer state change event: STATE_AVAILABLE
verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
@@ -1086,7 +1274,6 @@
// tethering mode is to be started.
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
- mLooper.dispatchAll();
verifyInterfaceServingModeStarted(TEST_WLAN_IFNAME);
verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER);
@@ -1104,7 +1291,7 @@
verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks();
// In tethering mode, in the default configuration, an explicit request
// for a mobile network is also made.
- verify(mUpstreamNetworkMonitor, times(1)).registerMobileNetworkRequest();
+ verify(mUpstreamNetworkMonitor, times(1)).setTryCell(true);
// There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_TETHERED
verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE);
verify(mNotificationUpdater, times(1)).onDownstreamChanged(eq(1 << TETHERING_WIFI));
@@ -1163,7 +1350,6 @@
// tethering mode is to be started.
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
- mLooper.dispatchAll();
// We verify get/set called three times here: twice for setup and once during
// teardown because all events happen over the course of the single
@@ -1487,7 +1673,6 @@
mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null);
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
- mLooper.dispatchAll();
tetherState = callback.pollTetherStatesChanged();
assertArrayEquals(tetherState.tetheredList, new String[] {TEST_WLAN_IFNAME});
callback.expectUpstreamChanged(upstreamState.network);
@@ -1509,7 +1694,6 @@
mLooper.dispatchAll();
mTethering.stopTethering(TETHERING_WIFI);
sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
- mLooper.dispatchAll();
tetherState = callback2.pollTetherStatesChanged();
assertArrayEquals(tetherState.availableList, new String[] {TEST_WLAN_IFNAME});
mLooper.dispatchAll();
@@ -1602,7 +1786,6 @@
mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
}
sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
- mLooper.dispatchAll();
verifyInterfaceServingModeStarted(TEST_P2P_IFNAME);
verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_AVAILABLE_TETHER);
@@ -1620,7 +1803,6 @@
// is being removed.
sendWifiP2pConnectionChanged(false, true, TEST_P2P_IFNAME);
mTethering.interfaceRemoved(TEST_P2P_IFNAME);
- mLooper.dispatchAll();
verify(mNetd, times(1)).tetherApplyDnsInterfaces();
verify(mNetd, times(1)).tetherInterfaceRemove(TEST_P2P_IFNAME);
@@ -1643,7 +1825,6 @@
mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
}
sendWifiP2pConnectionChanged(true, false, TEST_P2P_IFNAME);
- mLooper.dispatchAll();
verify(mNetd, never()).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
verify(mNetd, never()).tetherInterfaceAdd(TEST_P2P_IFNAME);
@@ -1655,7 +1836,6 @@
// is being removed.
sendWifiP2pConnectionChanged(false, false, TEST_P2P_IFNAME);
mTethering.interfaceRemoved(TEST_P2P_IFNAME);
- mLooper.dispatchAll();
verify(mNetd, never()).tetherApplyDnsInterfaces();
verify(mNetd, never()).tetherInterfaceRemove(TEST_P2P_IFNAME);
@@ -1691,7 +1871,6 @@
mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
}
sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
- mLooper.dispatchAll();
verify(mNetd, never()).interfaceSetCfg(any(InterfaceConfigurationParcel.class));
verify(mNetd, never()).tetherInterfaceAdd(TEST_P2P_IFNAME);
@@ -1721,12 +1900,12 @@
}
private void setDataSaverEnabled(boolean enabled) {
- final Intent intent = new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED);
- mServiceContext.sendBroadcastAsUser(intent, UserHandle.ALL);
-
final int status = enabled ? RESTRICT_BACKGROUND_STATUS_ENABLED
: RESTRICT_BACKGROUND_STATUS_DISABLED;
- when(mCm.getRestrictBackgroundStatus()).thenReturn(status);
+ doReturn(status).when(mCm).getRestrictBackgroundStatus();
+
+ final Intent intent = new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED);
+ mServiceContext.sendBroadcastAsUser(intent, UserHandle.ALL);
mLooper.dispatchAll();
}
@@ -1821,7 +2000,6 @@
// Expect that when USB comes up, the DHCP server is configured with the requested address.
mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true);
sendUsbBroadcast(true, true, true, TETHERING_USB);
- mLooper.dispatchAll();
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
any(), any());
verify(mNetd).interfaceSetCfg(argThat(cfg -> serverAddr.equals(cfg.ipv4Addr)));
@@ -1841,7 +2019,6 @@
verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS);
mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true);
sendUsbBroadcast(true, true, true, TETHERING_USB);
- mLooper.dispatchAll();
verify(mNetd).interfaceSetCfg(argThat(cfg -> serverAddr.equals(cfg.ipv4Addr)));
verify(mIpServerDependencies, times(1)).makeDhcpServer(any(), dhcpParamsCaptor.capture(),
any());
@@ -1880,7 +2057,8 @@
// Verify that onUpstreamCapabilitiesChanged won't be called if not current upstream network
// capabilities changed.
final UpstreamNetworkState upstreamState2 = new UpstreamNetworkState(
- upstreamState.linkProperties, upstreamState.networkCapabilities, new Network(101));
+ upstreamState.linkProperties, upstreamState.networkCapabilities,
+ new Network(WIFI_NETID));
stateMachine.handleUpstreamNetworkMonitorCallback(EVENT_ON_CAPABILITIES, upstreamState2);
verify(mNotificationUpdater, never()).onUpstreamCapabilitiesChanged(any());
}
@@ -1990,7 +2168,7 @@
public void testHandleIpConflict() throws Exception {
final Network wifiNetwork = new Network(200);
final Network[] allNetworks = { wifiNetwork };
- when(mCm.getAllNetworks()).thenReturn(allNetworks);
+ doReturn(allNetworks).when(mCm).getAllNetworks();
runUsbTethering(null);
final ArgumentCaptor<InterfaceConfigurationParcel> ifaceConfigCaptor =
ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
@@ -2017,7 +2195,7 @@
final Network btNetwork = new Network(201);
final Network mobileNetwork = new Network(202);
final Network[] allNetworks = { wifiNetwork, btNetwork, mobileNetwork };
- when(mCm.getAllNetworks()).thenReturn(allNetworks);
+ doReturn(allNetworks).when(mCm).getAllNetworks();
runUsbTethering(null);
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks(
any(), any());
@@ -2064,7 +2242,6 @@
mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true);
sendUsbBroadcast(true, true, true, TETHERING_USB);
- mLooper.dispatchAll();
assertContains(Arrays.asList(mTethering.getTetherableIfaces()), TEST_USB_IFNAME);
assertContains(Arrays.asList(mTethering.getTetherableIfaces()), TEST_ETH_IFNAME);
assertEquals(TETHER_ERROR_IFACE_CFG_ERROR, mTethering.getLastTetherError(TEST_USB_IFNAME));
@@ -2103,7 +2280,6 @@
// Run local only tethering.
mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true);
sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME);
- mLooper.dispatchAll();
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
any(), dhcpEventCbsCaptor.capture());
eventCallbacks = dhcpEventCbsCaptor.getValue();
@@ -2120,7 +2296,6 @@
// Run wifi tethering.
mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true);
sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED);
- mLooper.dispatchAll();
verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks(
any(), dhcpEventCbsCaptor.capture());
eventCallbacks = dhcpEventCbsCaptor.getValue();
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
index 232588c..bc21692 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java
@@ -29,7 +29,6 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
@@ -42,24 +41,26 @@
import static org.mockito.Mockito.when;
import android.content.Context;
-import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.IConnectivityManager;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
-import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.util.SharedLog;
import android.os.Handler;
+import android.os.Looper;
import android.os.Message;
+import android.os.test.TestLooper;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
+import com.android.networkstack.tethering.TestConnectivityManager.NetworkRequestInfo;
+import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent;
import org.junit.After;
import org.junit.Before;
@@ -71,10 +72,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.HashMap;
import java.util.HashSet;
-import java.util.Map;
-import java.util.Objects;
import java.util.Set;
@RunWith(AndroidJUnit4.class)
@@ -89,6 +87,13 @@
// any specific TRANSPORT_* is sufficient to identify this request.
private static final NetworkRequest sDefaultRequest = new NetworkRequest.Builder().build();
+ private static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR).addCapability(NET_CAPABILITY_INTERNET).build();
+ private static final NetworkCapabilities DUN_CAPABILITIES = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR).addCapability(NET_CAPABILITY_DUN).build();
+ private static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI).addCapability(NET_CAPABILITY_INTERNET).build();
+
@Mock private Context mContext;
@Mock private EntitlementManager mEntitleMgr;
@Mock private IConnectivityManager mCS;
@@ -98,6 +103,8 @@
private TestConnectivityManager mCM;
private UpstreamNetworkMonitor mUNM;
+ private final TestLooper mLooper = new TestLooper();
+
@Before public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
reset(mContext);
@@ -106,10 +113,9 @@
when(mLog.forSubComponent(anyString())).thenReturn(mLog);
when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
- mCM = spy(new TestConnectivityManager(mContext, mCS));
- mSM = new TestStateMachine();
- mUNM = new UpstreamNetworkMonitor(
- (ConnectivityManager) mCM, mSM, mLog, EVENT_UNM_UPDATE);
+ mCM = spy(new TestConnectivityManager(mContext, mCS, sDefaultRequest));
+ mSM = new TestStateMachine(mLooper.getLooper());
+ mUNM = new UpstreamNetworkMonitor(mCM, mSM, mLog, EVENT_UNM_UPDATE);
}
@After public void tearDown() throws Exception {
@@ -131,9 +137,9 @@
assertTrue(mCM.hasNoCallbacks());
assertFalse(mUNM.mobileNetworkRequested());
- mUNM.updateMobileRequiresDun(true);
+ mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */);
assertTrue(mCM.hasNoCallbacks());
- mUNM.updateMobileRequiresDun(false);
+ mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */);
assertTrue(mCM.hasNoCallbacks());
}
@@ -143,7 +149,7 @@
mUNM.startTrackDefaultNetwork(sDefaultRequest, mEntitleMgr);
mUNM.startObserveAllNetworks();
- assertEquals(1, mCM.trackingDefault.size());
+ assertEquals(1, mCM.mTrackingDefault.size());
mUNM.stop();
assertTrue(mCM.onlyHasDefaultCallbacks());
@@ -151,11 +157,11 @@
@Test
public void testListensForAllNetworks() throws Exception {
- assertTrue(mCM.listening.isEmpty());
+ assertTrue(mCM.mListening.isEmpty());
mUNM.startTrackDefaultNetwork(sDefaultRequest, mEntitleMgr);
mUNM.startObserveAllNetworks();
- assertFalse(mCM.listening.isEmpty());
+ assertFalse(mCM.mListening.isEmpty());
assertTrue(mCM.isListeningForAll());
mUNM.stop();
@@ -178,17 +184,17 @@
@Test
public void testRequestsMobileNetwork() throws Exception {
assertFalse(mUNM.mobileNetworkRequested());
- assertEquals(0, mCM.requested.size());
+ assertEquals(0, mCM.mRequested.size());
mUNM.startObserveAllNetworks();
assertFalse(mUNM.mobileNetworkRequested());
- assertEquals(0, mCM.requested.size());
+ assertEquals(0, mCM.mRequested.size());
- mUNM.updateMobileRequiresDun(false);
+ mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */);
assertFalse(mUNM.mobileNetworkRequested());
- assertEquals(0, mCM.requested.size());
+ assertEquals(0, mCM.mRequested.size());
- mUNM.registerMobileNetworkRequest();
+ mUNM.setTryCell(true);
assertTrue(mUNM.mobileNetworkRequested());
assertUpstreamTypeRequested(TYPE_MOBILE_HIPRI);
assertFalse(isDunRequested());
@@ -201,16 +207,16 @@
@Test
public void testDuplicateMobileRequestsIgnored() throws Exception {
assertFalse(mUNM.mobileNetworkRequested());
- assertEquals(0, mCM.requested.size());
+ assertEquals(0, mCM.mRequested.size());
mUNM.startObserveAllNetworks();
verify(mCM, times(1)).registerNetworkCallback(
any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
assertFalse(mUNM.mobileNetworkRequested());
- assertEquals(0, mCM.requested.size());
+ assertEquals(0, mCM.mRequested.size());
- mUNM.updateMobileRequiresDun(true);
- mUNM.registerMobileNetworkRequest();
+ mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */);
+ mUNM.setTryCell(true);
verify(mCM, times(1)).requestNetwork(
any(NetworkRequest.class), anyInt(), anyInt(), any(Handler.class),
any(NetworkCallback.class));
@@ -220,9 +226,9 @@
assertTrue(isDunRequested());
// Try a few things that must not result in any state change.
- mUNM.registerMobileNetworkRequest();
- mUNM.updateMobileRequiresDun(true);
- mUNM.registerMobileNetworkRequest();
+ mUNM.setTryCell(true);
+ mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */);
+ mUNM.setTryCell(true);
assertTrue(mUNM.mobileNetworkRequested());
assertUpstreamTypeRequested(TYPE_MOBILE_DUN);
@@ -237,17 +243,17 @@
@Test
public void testRequestsDunNetwork() throws Exception {
assertFalse(mUNM.mobileNetworkRequested());
- assertEquals(0, mCM.requested.size());
+ assertEquals(0, mCM.mRequested.size());
mUNM.startObserveAllNetworks();
assertFalse(mUNM.mobileNetworkRequested());
- assertEquals(0, mCM.requested.size());
+ assertEquals(0, mCM.mRequested.size());
- mUNM.updateMobileRequiresDun(true);
+ mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */);
assertFalse(mUNM.mobileNetworkRequested());
- assertEquals(0, mCM.requested.size());
+ assertEquals(0, mCM.mRequested.size());
- mUNM.registerMobileNetworkRequest();
+ mUNM.setTryCell(true);
assertTrue(mUNM.mobileNetworkRequested());
assertUpstreamTypeRequested(TYPE_MOBILE_DUN);
assertTrue(isDunRequested());
@@ -262,18 +268,18 @@
mUNM.startObserveAllNetworks();
// Test going from no-DUN to DUN correctly re-registers callbacks.
- mUNM.updateMobileRequiresDun(false);
- mUNM.registerMobileNetworkRequest();
+ mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */);
+ mUNM.setTryCell(true);
assertTrue(mUNM.mobileNetworkRequested());
assertUpstreamTypeRequested(TYPE_MOBILE_HIPRI);
assertFalse(isDunRequested());
- mUNM.updateMobileRequiresDun(true);
+ mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */);
assertTrue(mUNM.mobileNetworkRequested());
assertUpstreamTypeRequested(TYPE_MOBILE_DUN);
assertTrue(isDunRequested());
// Test going from DUN to no-DUN correctly re-registers callbacks.
- mUNM.updateMobileRequiresDun(false);
+ mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */);
assertTrue(mUNM.mobileNetworkRequested());
assertUpstreamTypeRequested(TYPE_MOBILE_HIPRI);
assertFalse(isDunRequested());
@@ -292,75 +298,80 @@
// There are no networks, so there is nothing to select.
assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
- final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, TRANSPORT_WIFI);
+ final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
wifiAgent.fakeConnect();
+ mLooper.dispatchAll();
// WiFi is up, we should prefer it.
assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes));
wifiAgent.fakeDisconnect();
+ mLooper.dispatchAll();
// There are no networks, so there is nothing to select.
assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
- final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
+ final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES);
cellAgent.fakeConnect();
+ mLooper.dispatchAll();
assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
preferredTypes.add(TYPE_MOBILE_DUN);
// This is coupled with preferred types in TetheringConfiguration.
- mUNM.updateMobileRequiresDun(true);
+ mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */);
// DUN is available, but only use regular cell: no upstream selected.
assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
preferredTypes.remove(TYPE_MOBILE_DUN);
// No WiFi, but our preferred flavour of cell is up.
preferredTypes.add(TYPE_MOBILE_HIPRI);
// This is coupled with preferred types in TetheringConfiguration.
- mUNM.updateMobileRequiresDun(false);
+ mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */);
assertSatisfiesLegacyType(TYPE_MOBILE_HIPRI,
mUNM.selectPreferredUpstreamType(preferredTypes));
// Check to see we filed an explicit request.
- assertEquals(1, mCM.requested.size());
- NetworkRequest netReq = (NetworkRequest) mCM.requested.values().toArray()[0];
+ assertEquals(1, mCM.mRequested.size());
+ NetworkRequest netReq = ((NetworkRequestInfo) mCM.mRequested.values().toArray()[0]).request;
assertTrue(netReq.networkCapabilities.hasTransport(TRANSPORT_CELLULAR));
assertFalse(netReq.networkCapabilities.hasCapability(NET_CAPABILITY_DUN));
// mobile is not permitted, we should not use HIPRI.
when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(false);
assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
- assertEquals(0, mCM.requested.size());
+ assertEquals(0, mCM.mRequested.size());
// mobile change back to permitted, HIRPI should come back
when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
assertSatisfiesLegacyType(TYPE_MOBILE_HIPRI,
mUNM.selectPreferredUpstreamType(preferredTypes));
wifiAgent.fakeConnect();
+ mLooper.dispatchAll();
// WiFi is up, and we should prefer it over cell.
assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes));
- assertEquals(0, mCM.requested.size());
+ assertEquals(0, mCM.mRequested.size());
preferredTypes.remove(TYPE_MOBILE_HIPRI);
preferredTypes.add(TYPE_MOBILE_DUN);
// This is coupled with preferred types in TetheringConfiguration.
- mUNM.updateMobileRequiresDun(true);
+ mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */);
assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes));
- final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
- dunAgent.networkCapabilities.addCapability(NET_CAPABILITY_DUN);
+ final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, DUN_CAPABILITIES);
dunAgent.fakeConnect();
+ mLooper.dispatchAll();
// WiFi is still preferred.
assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes));
// WiFi goes down, cell and DUN are still up but only DUN is preferred.
wifiAgent.fakeDisconnect();
+ mLooper.dispatchAll();
assertSatisfiesLegacyType(TYPE_MOBILE_DUN,
mUNM.selectPreferredUpstreamType(preferredTypes));
// Check to see we filed an explicit request.
- assertEquals(1, mCM.requested.size());
- netReq = (NetworkRequest) mCM.requested.values().toArray()[0];
+ assertEquals(1, mCM.mRequested.size());
+ netReq = ((NetworkRequestInfo) mCM.mRequested.values().toArray()[0]).request;
assertTrue(netReq.networkCapabilities.hasTransport(TRANSPORT_CELLULAR));
assertTrue(netReq.networkCapabilities.hasCapability(NET_CAPABILITY_DUN));
// mobile is not permitted, we should not use DUN.
when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(false);
assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes));
- assertEquals(0, mCM.requested.size());
+ assertEquals(0, mCM.mRequested.size());
// mobile change back to permitted, DUN should come back
when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
assertSatisfiesLegacyType(TYPE_MOBILE_DUN,
@@ -371,49 +382,72 @@
public void testGetCurrentPreferredUpstream() throws Exception {
mUNM.startTrackDefaultNetwork(sDefaultRequest, mEntitleMgr);
mUNM.startObserveAllNetworks();
- mUNM.updateMobileRequiresDun(false);
+ mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */);
+ mUNM.setTryCell(true);
// [0] Mobile connects, DUN not required -> mobile selected.
- final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
+ final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES);
cellAgent.fakeConnect();
mCM.makeDefaultNetwork(cellAgent);
+ mLooper.dispatchAll();
assertEquals(cellAgent.networkId, mUNM.getCurrentPreferredUpstream().network);
+ assertEquals(0, mCM.mRequested.size());
// [1] Mobile connects but not permitted -> null selected
when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(false);
assertEquals(null, mUNM.getCurrentPreferredUpstream());
when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
+ assertEquals(0, mCM.mRequested.size());
// [2] WiFi connects but not validated/promoted to default -> mobile selected.
- final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, TRANSPORT_WIFI);
+ final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
wifiAgent.fakeConnect();
+ mLooper.dispatchAll();
assertEquals(cellAgent.networkId, mUNM.getCurrentPreferredUpstream().network);
+ assertEquals(0, mCM.mRequested.size());
// [3] WiFi validates and is promoted to the default network -> WiFi selected.
mCM.makeDefaultNetwork(wifiAgent);
+ mLooper.dispatchAll();
assertEquals(wifiAgent.networkId, mUNM.getCurrentPreferredUpstream().network);
+ assertEquals(0, mCM.mRequested.size());
// [4] DUN required, no other changes -> WiFi still selected
- mUNM.updateMobileRequiresDun(true);
+ mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */);
assertEquals(wifiAgent.networkId, mUNM.getCurrentPreferredUpstream().network);
+ assertEquals(1, mCM.mRequested.size());
+ assertTrue(isDunRequested());
// [5] WiFi no longer validated, mobile becomes default, DUN required -> null selected.
mCM.makeDefaultNetwork(cellAgent);
+ mLooper.dispatchAll();
assertEquals(null, mUNM.getCurrentPreferredUpstream());
- // TODO: make sure that a DUN request has been filed. This is currently
- // triggered by code over in Tethering, but once that has been moved
- // into UNM we should test for this here.
+ assertEquals(1, mCM.mRequested.size());
+ assertTrue(isDunRequested());
// [6] DUN network arrives -> DUN selected
- final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
+ final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES);
dunAgent.networkCapabilities.addCapability(NET_CAPABILITY_DUN);
dunAgent.networkCapabilities.removeCapability(NET_CAPABILITY_INTERNET);
dunAgent.fakeConnect();
+ mLooper.dispatchAll();
assertEquals(dunAgent.networkId, mUNM.getCurrentPreferredUpstream().network);
+ assertEquals(1, mCM.mRequested.size());
// [7] Mobile is not permitted -> null selected
when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(false);
assertEquals(null, mUNM.getCurrentPreferredUpstream());
+ assertEquals(1, mCM.mRequested.size());
+
+ // [7] Mobile is permitted again -> DUN selected
+ when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true);
+ assertEquals(dunAgent.networkId, mUNM.getCurrentPreferredUpstream().network);
+ assertEquals(1, mCM.mRequested.size());
+
+ // [8] DUN no longer required -> request is withdrawn
+ mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */);
+ assertEquals(0, mCM.mRequested.size());
+ assertFalse(isDunRequested());
}
@Test
@@ -428,7 +462,7 @@
final Set<String> alreadySeen = new HashSet<>();
// [1] Pretend Wi-Fi connects.
- final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, TRANSPORT_WIFI);
+ final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
final LinkProperties wifiLp = wifiAgent.linkProperties;
wifiLp.setInterfaceName("wlan0");
final String[] wifi_addrs = {
@@ -443,6 +477,7 @@
}
wifiAgent.fakeConnect();
wifiAgent.sendLinkProperties();
+ mLooper.dispatchAll();
local = mUNM.getLocalPrefixes();
assertPrefixSet(local, INCLUDES, alreadySeen);
@@ -455,7 +490,7 @@
assertEquals(alreadySeen.size(), local.size());
// [2] Pretend mobile connects.
- final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
+ final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES);
final LinkProperties cellLp = cellAgent.linkProperties;
cellLp.setInterfaceName("rmnet_data0");
final String[] cell_addrs = {
@@ -467,6 +502,7 @@
}
cellAgent.fakeConnect();
cellAgent.sendLinkProperties();
+ mLooper.dispatchAll();
local = mUNM.getLocalPrefixes();
assertPrefixSet(local, INCLUDES, alreadySeen);
@@ -476,9 +512,7 @@
assertEquals(alreadySeen.size(), local.size());
// [3] Pretend DUN connects.
- final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
- dunAgent.networkCapabilities.addCapability(NET_CAPABILITY_DUN);
- dunAgent.networkCapabilities.removeCapability(NET_CAPABILITY_INTERNET);
+ final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, DUN_CAPABILITIES);
final LinkProperties dunLp = dunAgent.linkProperties;
dunLp.setInterfaceName("rmnet_data1");
final String[] dun_addrs = {
@@ -490,6 +524,7 @@
}
dunAgent.fakeConnect();
dunAgent.sendLinkProperties();
+ mLooper.dispatchAll();
local = mUNM.getLocalPrefixes();
assertPrefixSet(local, INCLUDES, alreadySeen);
@@ -501,6 +536,7 @@
// [4] Pretend Wi-Fi disconnected. It's addresses/prefixes should no
// longer be included (should be properly removed).
wifiAgent.fakeDisconnect();
+ mLooper.dispatchAll();
local = mUNM.getLocalPrefixes();
assertPrefixSet(local, EXCLUDES, wifiLinkPrefixes);
assertPrefixSet(local, INCLUDES, cellLinkPrefixes);
@@ -508,6 +544,7 @@
// [5] Pretend mobile disconnected.
cellAgent.fakeDisconnect();
+ mLooper.dispatchAll();
local = mUNM.getLocalPrefixes();
assertPrefixSet(local, EXCLUDES, wifiLinkPrefixes);
assertPrefixSet(local, EXCLUDES, cellLinkPrefixes);
@@ -515,6 +552,7 @@
// [6] Pretend DUN disconnected.
dunAgent.fakeDisconnect();
+ mLooper.dispatchAll();
local = mUNM.getLocalPrefixes();
assertTrue(local.isEmpty());
}
@@ -528,12 +566,13 @@
mUNM.startTrackDefaultNetwork(sDefaultRequest, mEntitleMgr);
mUNM.startObserveAllNetworks();
// Setup wifi and make wifi as default network.
- final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, TRANSPORT_WIFI);
+ final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES);
wifiAgent.fakeConnect();
mCM.makeDefaultNetwork(wifiAgent);
// Setup mobile network.
- final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
+ final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES);
cellAgent.fakeConnect();
+ mLooper.dispatchAll();
assertSatisfiesLegacyType(TYPE_MOBILE_HIPRI,
mUNM.selectPreferredUpstreamType(preferredTypes));
@@ -552,202 +591,21 @@
}
private void assertUpstreamTypeRequested(int upstreamType) throws Exception {
- assertEquals(1, mCM.requested.size());
- assertEquals(1, mCM.legacyTypeMap.size());
+ assertEquals(1, mCM.mRequested.size());
+ assertEquals(1, mCM.mLegacyTypeMap.size());
assertEquals(Integer.valueOf(upstreamType),
- mCM.legacyTypeMap.values().iterator().next());
+ mCM.mLegacyTypeMap.values().iterator().next());
}
private boolean isDunRequested() {
- for (NetworkRequest req : mCM.requested.values()) {
- if (req.networkCapabilities.hasCapability(NET_CAPABILITY_DUN)) {
+ for (NetworkRequestInfo nri : mCM.mRequested.values()) {
+ if (nri.request.networkCapabilities.hasCapability(NET_CAPABILITY_DUN)) {
return true;
}
}
return false;
}
- public static class TestConnectivityManager extends ConnectivityManager {
- public Map<NetworkCallback, Handler> allCallbacks = new HashMap<>();
- public Set<NetworkCallback> trackingDefault = new HashSet<>();
- public TestNetworkAgent defaultNetwork = null;
- public Map<NetworkCallback, NetworkRequest> listening = new HashMap<>();
- public Map<NetworkCallback, NetworkRequest> requested = new HashMap<>();
- public Map<NetworkCallback, Integer> legacyTypeMap = new HashMap<>();
-
- private int mNetworkId = 100;
-
- public TestConnectivityManager(Context ctx, IConnectivityManager svc) {
- super(ctx, svc);
- }
-
- boolean hasNoCallbacks() {
- return allCallbacks.isEmpty()
- && trackingDefault.isEmpty()
- && listening.isEmpty()
- && requested.isEmpty()
- && legacyTypeMap.isEmpty();
- }
-
- boolean onlyHasDefaultCallbacks() {
- return (allCallbacks.size() == 1)
- && (trackingDefault.size() == 1)
- && listening.isEmpty()
- && requested.isEmpty()
- && legacyTypeMap.isEmpty();
- }
-
- boolean isListeningForAll() {
- final NetworkCapabilities empty = new NetworkCapabilities();
- empty.clearAll();
-
- for (NetworkRequest req : listening.values()) {
- if (req.networkCapabilities.equalRequestableCapabilities(empty)) {
- return true;
- }
- }
- return false;
- }
-
- int getNetworkId() {
- return ++mNetworkId;
- }
-
- void makeDefaultNetwork(TestNetworkAgent agent) {
- if (Objects.equals(defaultNetwork, agent)) return;
-
- final TestNetworkAgent formerDefault = defaultNetwork;
- defaultNetwork = agent;
-
- for (NetworkCallback cb : trackingDefault) {
- if (defaultNetwork != null) {
- cb.onAvailable(defaultNetwork.networkId);
- cb.onCapabilitiesChanged(
- defaultNetwork.networkId, defaultNetwork.networkCapabilities);
- cb.onLinkPropertiesChanged(
- defaultNetwork.networkId, defaultNetwork.linkProperties);
- }
- }
- }
-
- @Override
- public void requestNetwork(NetworkRequest req, NetworkCallback cb, Handler h) {
- assertFalse(allCallbacks.containsKey(cb));
- allCallbacks.put(cb, h);
- if (sDefaultRequest.equals(req)) {
- assertFalse(trackingDefault.contains(cb));
- trackingDefault.add(cb);
- } else {
- assertFalse(requested.containsKey(cb));
- requested.put(cb, req);
- }
- }
-
- @Override
- public void requestNetwork(NetworkRequest req, NetworkCallback cb) {
- fail("Should never be called.");
- }
-
- @Override
- public void requestNetwork(NetworkRequest req,
- int timeoutMs, int legacyType, Handler h, NetworkCallback cb) {
- assertFalse(allCallbacks.containsKey(cb));
- allCallbacks.put(cb, h);
- assertFalse(requested.containsKey(cb));
- requested.put(cb, req);
- assertFalse(legacyTypeMap.containsKey(cb));
- if (legacyType != ConnectivityManager.TYPE_NONE) {
- legacyTypeMap.put(cb, legacyType);
- }
- }
-
- @Override
- public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb, Handler h) {
- assertFalse(allCallbacks.containsKey(cb));
- allCallbacks.put(cb, h);
- assertFalse(listening.containsKey(cb));
- listening.put(cb, req);
- }
-
- @Override
- public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb) {
- fail("Should never be called.");
- }
-
- @Override
- public void registerDefaultNetworkCallback(NetworkCallback cb, Handler h) {
- fail("Should never be called.");
- }
-
- @Override
- public void registerDefaultNetworkCallback(NetworkCallback cb) {
- fail("Should never be called.");
- }
-
- @Override
- public void unregisterNetworkCallback(NetworkCallback cb) {
- if (trackingDefault.contains(cb)) {
- trackingDefault.remove(cb);
- } else if (listening.containsKey(cb)) {
- listening.remove(cb);
- } else if (requested.containsKey(cb)) {
- requested.remove(cb);
- legacyTypeMap.remove(cb);
- } else {
- fail("Unexpected callback removed");
- }
- allCallbacks.remove(cb);
-
- assertFalse(allCallbacks.containsKey(cb));
- assertFalse(trackingDefault.contains(cb));
- assertFalse(listening.containsKey(cb));
- assertFalse(requested.containsKey(cb));
- }
- }
-
- public static class TestNetworkAgent {
- public final TestConnectivityManager cm;
- public final Network networkId;
- public final int transportType;
- public final NetworkCapabilities networkCapabilities;
- public final LinkProperties linkProperties;
-
- public TestNetworkAgent(TestConnectivityManager cm, int transportType) {
- this.cm = cm;
- this.networkId = new Network(cm.getNetworkId());
- this.transportType = transportType;
- networkCapabilities = new NetworkCapabilities();
- networkCapabilities.addTransportType(transportType);
- networkCapabilities.addCapability(NET_CAPABILITY_INTERNET);
- linkProperties = new LinkProperties();
- }
-
- public void fakeConnect() {
- for (NetworkCallback cb : cm.listening.keySet()) {
- cb.onAvailable(networkId);
- cb.onCapabilitiesChanged(networkId, copy(networkCapabilities));
- cb.onLinkPropertiesChanged(networkId, copy(linkProperties));
- }
- }
-
- public void fakeDisconnect() {
- for (NetworkCallback cb : cm.listening.keySet()) {
- cb.onLost(networkId);
- }
- }
-
- public void sendLinkProperties() {
- for (NetworkCallback cb : cm.listening.keySet()) {
- cb.onLinkPropertiesChanged(networkId, copy(linkProperties));
- }
- }
-
- @Override
- public String toString() {
- return String.format("TestNetworkAgent: %s %s", networkId, networkCapabilities);
- }
- }
-
public static class TestStateMachine extends StateMachine {
public final ArrayList<Message> messages = new ArrayList<>();
private final State mLoggingState = new LoggingState();
@@ -767,22 +625,14 @@
}
}
- public TestStateMachine() {
- super("UpstreamNetworkMonitor.TestStateMachine");
+ public TestStateMachine(Looper looper) {
+ super("UpstreamNetworkMonitor.TestStateMachine", looper);
addState(mLoggingState);
setInitialState(mLoggingState);
super.start();
}
}
- static NetworkCapabilities copy(NetworkCapabilities nc) {
- return new NetworkCapabilities(nc);
- }
-
- static LinkProperties copy(LinkProperties lp) {
- return new LinkProperties(lp);
- }
-
static void assertPrefixSet(Set<IpPrefix> prefixes, boolean expectation, String... expected) {
final Set<String> expectedSet = new HashSet<>();
Collections.addAll(expectedSet, expected);
@@ -797,4 +647,4 @@
expectation, prefixes.contains(new IpPrefix(expectedPrefix)));
}
}
-}
+}
\ No newline at end of file
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index 47b114b..3185f7e 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
java_test_host {
name: "CtsHostsideNetworkTests",
defaults: ["cts_defaults"],
diff --git a/tests/cts/hostside/TEST_MAPPING b/tests/cts/hostside/TEST_MAPPING
new file mode 100644
index 0000000..02d2d6e
--- /dev/null
+++ b/tests/cts/hostside/TEST_MAPPING
@@ -0,0 +1,18 @@
+{
+ "presubmit": [
+ {
+ "name": "CtsHostsideNetworkTests",
+ "options": [
+ {
+ "include-filter": "com.android.cts.net.HostsideRestrictBackgroundNetworkTests"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp
index 320a1fa..2751f6f 100644
--- a/tests/cts/hostside/aidl/Android.bp
+++ b/tests/cts/hostside/aidl/Android.bp
@@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
java_test_helper_library {
name: "CtsHostsideNetworkTestsAidl",
sdk_version: "current",
diff --git a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
index 5aafdf0..f523745 100644
--- a/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
+++ b/tests/cts/hostside/aidl/com/android/cts/net/hostside/IMyService.aidl
@@ -24,6 +24,6 @@
String checkNetworkStatus();
String getRestrictBackgroundStatus();
void sendNotification(int notificationId, String notificationType);
- void registerNetworkCallback(in INetworkCallback cb);
+ void registerNetworkCallback(in NetworkRequest request, in INetworkCallback cb);
void unregisterNetworkCallback();
}
diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp
index 9903756..112b9eb 100644
--- a/tests/cts/hostside/app/Android.bp
+++ b/tests/cts/hostside/app/Android.bp
@@ -14,6 +14,10 @@
// limitations under the License.
//
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
android_test_helper_app {
name: "CtsHostsideNetworkTestsApp",
defaults: ["cts_support_defaults"],
@@ -23,9 +27,11 @@
"androidx.test.rules",
"androidx.test.ext.junit",
"compatibility-device-util-axt",
+ "cts-net-utils",
"ctstestrunner-axt",
"ub-uiautomator",
"CtsHostsideNetworkTestsAidl",
+ "modules-utils-build",
],
libs: [
"android.test.runner",
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 71f6f2f..1afbfb0 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
@@ -25,7 +25,8 @@
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getConnectivityManager;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getContext;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getInstrumentation;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getWifiManager;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isAppStandbySupported;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isBatterySaverSupported;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isDozeModeSupported;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.restrictBackgroundValueToString;
@@ -46,12 +47,11 @@
import android.net.ConnectivityManager;
import android.net.NetworkInfo.DetailedState;
import android.net.NetworkInfo.State;
-import android.net.wifi.WifiManager;
+import android.net.NetworkRequest;
import android.os.BatteryManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.SystemClock;
-import android.provider.Settings;
import android.service.notification.NotificationListenerService;
import android.util.Log;
@@ -234,12 +234,16 @@
}
protected void assertForegroundNetworkAccess() throws Exception {
+ assertForegroundNetworkAccess(true);
+ }
+
+ protected void assertForegroundNetworkAccess(boolean expectAllowed) throws Exception {
assertForegroundState();
// We verified that app is in foreground state but if the screen turns-off while
// verifying for network access, the app will go into background state (in case app's
// foreground status was due to top activity). So, turn the screen on when verifying
// network connectivity.
- assertNetworkAccess(true /* expectAvailable */, true /* needScreenOn */);
+ assertNetworkAccess(expectAllowed /* expectAvailable */, true /* needScreenOn */);
}
protected void assertForegroundServiceNetworkAccess() throws Exception {
@@ -624,6 +628,9 @@
}
protected void setBatterySaverMode(boolean enabled) throws Exception {
+ if (!isBatterySaverSupported()) {
+ return;
+ }
Log.i(TAG, "Setting Battery Saver Mode to " + enabled);
if (enabled) {
turnBatteryOn();
@@ -635,8 +642,9 @@
}
protected void setDozeMode(boolean enabled) throws Exception {
- // Check doze mode is supported.
- assertTrue("Device does not support Doze Mode", isDozeModeSupported());
+ if (!isDozeModeSupported()) {
+ return;
+ }
Log.i(TAG, "Setting Doze Mode to " + enabled);
if (enabled) {
@@ -656,12 +664,18 @@
}
protected void setAppIdle(boolean enabled) throws Exception {
+ if (!isAppStandbySupported()) {
+ return;
+ }
Log.i(TAG, "Setting app idle to " + enabled);
executeSilentShellCommand("am set-inactive " + TEST_APP2_PKG + " " + enabled );
assertAppIdle(enabled);
}
protected void setAppIdleNoAssert(boolean enabled) throws Exception {
+ if (!isAppStandbySupported()) {
+ return;
+ }
Log.i(TAG, "Setting app idle to " + enabled);
executeSilentShellCommand("am set-inactive " + TEST_APP2_PKG + " " + enabled );
}
@@ -700,8 +714,10 @@
fail("app2 receiver is not ready");
}
- protected void registerNetworkCallback(INetworkCallback cb) throws Exception {
- mServiceClient.registerNetworkCallback(cb);
+ protected void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb)
+ throws Exception {
+ Log.i(TAG, "Registering network callback for request: " + request);
+ mServiceClient.registerNetworkCallback(request, cb);
}
protected void unregisterNetworkCallback() throws Exception {
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
index 5ecb399..66cb935 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java
@@ -24,6 +24,8 @@
import android.os.ParcelFileDescriptor;
import android.util.Log;
+import androidx.test.platform.app.InstrumentationRegistry;
+
import com.android.compatibility.common.util.OnFailureRule;
import org.junit.AssumptionViolatedException;
@@ -37,23 +39,20 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
-import androidx.test.platform.app.InstrumentationRegistry;
-
public class DumpOnFailureRule extends OnFailureRule {
private File mDumpDir = new File(Environment.getExternalStorageDirectory(),
"CtsHostsideNetworkTests");
@Override
public void onTestFailure(Statement base, Description description, Throwable throwable) {
- final String testName = description.getClassName() + "_" + description.getMethodName();
-
if (throwable instanceof AssumptionViolatedException) {
+ final String testName = description.getClassName() + "_" + description.getMethodName();
Log.d(TAG, "Skipping test " + testName + ": " + throwable);
return;
}
prepareDumpRootDir();
- final File dumpFile = new File(mDumpDir, "dump-" + testName);
+ final File dumpFile = new File(mDumpDir, "dump-" + getShortenedTestName(description));
Log.i(TAG, "Dumping debug info for " + description + ": " + dumpFile.getPath());
try (FileOutputStream out = new FileOutputStream(dumpFile)) {
for (String cmd : new String[] {
@@ -71,6 +70,17 @@
}
}
+ private String getShortenedTestName(Description description) {
+ final String qualifiedClassName = description.getClassName();
+ final String className = qualifiedClassName.substring(
+ qualifiedClassName.lastIndexOf(".") + 1);
+ final String shortenedClassName = className.chars()
+ .filter(Character::isUpperCase)
+ .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
+ .toString();
+ return shortenedClassName + "_" + description.getMethodName();
+ }
+
void dumpCommandOutput(FileOutputStream out, String cmd) {
final ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation()
.getUiAutomation().executeShellCommand(cmd);
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java
index 8fadf9e..5c99c67 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java
@@ -15,21 +15,20 @@
*/
package com.android.cts.net.hostside;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.resetMeteredNetwork;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setupMeteredNetwork;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setupActiveNetworkMeteredness;
import static com.android.cts.net.hostside.Property.METERED_NETWORK;
import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK;
import android.util.ArraySet;
-import android.util.Pair;
import com.android.compatibility.common.util.BeforeAfterRule;
+import com.android.compatibility.common.util.ThrowingRunnable;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
public class MeterednessConfigurationRule extends BeforeAfterRule {
- private Pair<String, Boolean> mSsidAndInitialMeteredness;
+ private ThrowingRunnable mMeterednessResetter;
@Override
public void onBefore(Statement base, Description description) throws Throwable {
@@ -48,13 +47,13 @@
}
public void configureNetworkMeteredness(boolean metered) throws Exception {
- mSsidAndInitialMeteredness = setupMeteredNetwork(metered);
+ mMeterednessResetter = setupActiveNetworkMeteredness(metered);
}
public void resetNetworkMeteredness() throws Exception {
- if (mSsidAndInitialMeteredness != null) {
- resetMeteredNetwork(mSsidAndInitialMeteredness.first,
- mSsidAndInitialMeteredness.second);
+ if (mMeterednessResetter != null) {
+ mMeterednessResetter.run();
+ mMeterednessResetter = null;
}
}
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java
index 0d0bc58..55eec11 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyActivity.java
@@ -17,6 +17,7 @@
package com.android.cts.net.hostside;
import android.app.Activity;
+import android.app.KeyguardManager;
import android.content.Intent;
import android.os.Bundle;
import android.view.WindowManager;
@@ -34,6 +35,11 @@
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+
+ // Dismiss the keyguard so that the tests can click on the VPN confirmation dialog.
+ // FLAG_DISMISS_KEYGUARD is not sufficient to do this because as soon as the dialog appears,
+ // this activity goes into the background and the keyguard reappears.
+ getSystemService(KeyguardManager.class).requestDismissKeyguard(this, null /* callback */);
}
@Override
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
index 6546e26..c37e8d5 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MyServiceClient.java
@@ -20,12 +20,11 @@
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
+import android.net.NetworkRequest;
import android.os.ConditionVariable;
import android.os.IBinder;
import android.os.RemoteException;
-import com.android.cts.net.hostside.IMyService;
-
public class MyServiceClient {
private static final int TIMEOUT_MS = 5000;
private static final String PACKAGE = MyServiceClient.class.getPackage().getName();
@@ -93,12 +92,14 @@
return mService.getRestrictBackgroundStatus();
}
- public void sendNotification(int notificationId, String notificationType) throws RemoteException {
+ public void sendNotification(int notificationId, String notificationType)
+ throws RemoteException {
mService.sendNotification(notificationId, notificationType);
}
- public void registerNetworkCallback(INetworkCallback cb) throws RemoteException {
- mService.registerNetworkCallback(cb);
+ public void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb)
+ throws RemoteException {
+ mService.registerNetworkCallback(request, cb);
}
public void unregisterNetworkCallback() throws RemoteException {
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
index 2ac29e7..36e2ffe 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java
@@ -17,21 +17,20 @@
package com.android.cts.net.hostside;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
+import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getActiveNetworkCapabilities;
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
-import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isActiveNetworkMetered;
import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import android.net.Network;
import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
import android.util.Log;
import org.junit.After;
@@ -186,7 +185,7 @@
public void setUp() throws Exception {
super.setUp();
- assumeTrue(isActiveNetworkMetered(true) || canChangeActiveNetworkMeteredness());
+ assumeTrue(canChangeActiveNetworkMeteredness());
registerBroadcastReceiver();
@@ -198,13 +197,18 @@
setBatterySaverMode(false);
setRestrictBackground(false);
- // Make wifi a metered network.
+ // Get transports of the active network, this has to be done before changing meteredness,
+ // since wifi will be disconnected when changing from non-metered to metered.
+ final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
+
+ // Mark network as metered.
mMeterednessConfiguration.configureNetworkMeteredness(true);
// Register callback
- registerNetworkCallback((INetworkCallback.Stub) mTestNetworkCallback);
- // Once the wifi is marked as metered, the wifi will reconnect. Wait for onAvailable()
- // callback to ensure wifi is connected before the test and store the default network.
+ registerNetworkCallback(new NetworkRequest.Builder()
+ .setCapabilities(networkCapabilities).build(), mTestNetworkCallback);
+ // Wait for onAvailable() callback to ensure network is available before the test
+ // and store the default network.
mNetwork = mTestNetworkCallback.expectAvailableCallbackAndGetNetwork();
// Check that the network is metered.
mTestNetworkCallback.expectCapabilitiesCallbackEventually(mNetwork,
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 3807d79..e62d557 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
@@ -20,45 +20,66 @@
import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_METERED;
+import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_NONE;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.app.ActivityManager;
import android.app.Instrumentation;
+import android.app.UiAutomation;
import android.content.Context;
import android.location.LocationManager;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.Network;
import android.net.NetworkCapabilities;
+import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.ActionListener;
+import android.os.PersistableBundle;
import android.os.Process;
-import android.text.TextUtils;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.data.ApnSetting;
import android.util.Log;
-import android.util.Pair;
-
-import com.android.compatibility.common.util.AppStandbyUtils;
-import com.android.compatibility.common.util.BatteryUtils;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.compatibility.common.util.AppStandbyUtils;
+import com.android.compatibility.common.util.BatteryUtils;
+import com.android.compatibility.common.util.ShellIdentityUtils;
+import com.android.compatibility.common.util.ThrowingRunnable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
public class NetworkPolicyTestUtils {
- private static final int TIMEOUT_CHANGE_METEREDNESS_MS = 5000;
+ // android.telephony.CarrierConfigManager.KEY_CARRIER_METERED_APN_TYPES_STRINGS
+ // TODO: Expose it as a @TestApi instead of copying the constant
+ private static final String KEY_CARRIER_METERED_APN_TYPES_STRINGS =
+ "carrier_metered_apn_types_strings";
+
+ private static final int TIMEOUT_CHANGE_METEREDNESS_MS = 10_000;
private static ConnectivityManager mCm;
private static WifiManager mWm;
+ private static CarrierConfigManager mCarrierConfigManager;
private static Boolean mBatterySaverSupported;
private static Boolean mDataSaverSupported;
@@ -85,11 +106,11 @@
if (mDataSaverSupported == null) {
assertMyRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED);
try {
- setRestrictBackground(true);
+ setRestrictBackgroundInternal(true);
mDataSaverSupported = !isMyRestrictBackgroundStatus(
RESTRICT_BACKGROUND_STATUS_DISABLED);
} finally {
- setRestrictBackground(false);
+ setRestrictBackgroundInternal(false);
}
}
return mDataSaverSupported;
@@ -135,54 +156,136 @@
}
public static boolean canChangeActiveNetworkMeteredness() {
- final Network activeNetwork = getConnectivityManager().getActiveNetwork();
- final NetworkCapabilities networkCapabilities
- = getConnectivityManager().getNetworkCapabilities(activeNetwork);
- return networkCapabilities.hasTransport(TRANSPORT_WIFI);
+ final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
+ return networkCapabilities.hasTransport(TRANSPORT_WIFI)
+ || networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
}
- public static Pair<String, Boolean> setupMeteredNetwork(boolean metered) throws Exception {
+ /**
+ * Updates the meteredness of the active network. Right now we can only change meteredness
+ * of either Wifi or cellular network, so if the active network is not either of these, this
+ * will throw an exception.
+ *
+ * @return a {@link ThrowingRunnable} object that can used to reset the meteredness change
+ * made by this method.
+ */
+ public static ThrowingRunnable setupActiveNetworkMeteredness(boolean metered) throws Exception {
if (isActiveNetworkMetered(metered)) {
return null;
}
- final boolean isLocationEnabled = isLocationEnabled();
- try {
- if (!isLocationEnabled) {
- setLocationEnabled(true);
- }
- final String ssid = unquoteSSID(getWifiManager().getConnectionInfo().getSSID());
- assertNotEquals(WifiManager.UNKNOWN_SSID, ssid);
+ final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
+ if (networkCapabilities.hasTransport(TRANSPORT_WIFI)) {
+ final String ssid = getWifiSsid();
setWifiMeteredStatus(ssid, metered);
- return Pair.create(ssid, !metered);
- } finally {
- // Reset the location enabled state
- if (!isLocationEnabled) {
- setLocationEnabled(false);
- }
+ return () -> setWifiMeteredStatus(ssid, !metered);
+ } else if (networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
+ final int subId = SubscriptionManager.getActiveDataSubscriptionId();
+ setCellularMeteredStatus(subId, metered);
+ return () -> setCellularMeteredStatus(subId, !metered);
+ } else {
+ // Right now, we don't have a way to change meteredness of networks other
+ // than Wi-Fi or Cellular, so just throw an exception.
+ throw new IllegalStateException("Can't change meteredness of current active network");
}
}
- public static void resetMeteredNetwork(String ssid, boolean metered) throws Exception {
- setWifiMeteredStatus(ssid, metered);
+ private static String getWifiSsid() {
+ final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
+ try {
+ uiAutomation.adoptShellPermissionIdentity();
+ final String ssid = getWifiManager().getConnectionInfo().getSSID();
+ assertNotEquals(WifiManager.UNKNOWN_SSID, ssid);
+ return ssid;
+ } finally {
+ uiAutomation.dropShellPermissionIdentity();
+ }
}
- public static void setWifiMeteredStatus(String ssid, boolean metered) throws Exception {
- assertFalse("SSID should not be empty", TextUtils.isEmpty(ssid));
- final String cmd = "cmd netpolicy set metered-network " + ssid + " " + metered;
- executeShellCommand(cmd);
- assertWifiMeteredStatus(ssid, metered);
- assertActiveNetworkMetered(metered);
+ static NetworkCapabilities getActiveNetworkCapabilities() {
+ final Network activeNetwork = getConnectivityManager().getActiveNetwork();
+ assertNotNull("No active network available", activeNetwork);
+ return getConnectivityManager().getNetworkCapabilities(activeNetwork);
}
- public static void assertWifiMeteredStatus(String ssid, boolean expectedMeteredStatus) {
- final String result = executeShellCommand("cmd netpolicy list wifi-networks");
- final String expectedLine = ssid + ";" + expectedMeteredStatus;
- assertTrue("Expected line: " + expectedLine + "; Actual result: " + result,
- result.contains(expectedLine));
+ private static void setWifiMeteredStatus(String ssid, boolean metered) throws Exception {
+ final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
+ try {
+ uiAutomation.adoptShellPermissionIdentity();
+ final WifiConfiguration currentConfig = getWifiConfiguration(ssid);
+ currentConfig.meteredOverride = metered
+ ? METERED_OVERRIDE_METERED : METERED_OVERRIDE_NONE;
+ BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
+ getWifiManager().save(currentConfig, createActionListener(
+ blockingQueue, Integer.MAX_VALUE));
+ Integer resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS,
+ TimeUnit.MILLISECONDS);
+ if (resultCode == null) {
+ fail("Timed out waiting for meteredness to change; ssid=" + ssid
+ + ", metered=" + metered);
+ } else if (resultCode != Integer.MAX_VALUE) {
+ fail("Error overriding the meteredness; ssid=" + ssid
+ + ", metered=" + metered + ", error=" + resultCode);
+ }
+ final boolean success = assertActiveNetworkMetered(metered, false /* throwOnFailure */);
+ if (!success) {
+ Log.i(TAG, "Retry connecting to wifi; ssid=" + ssid);
+ blockingQueue = new LinkedBlockingQueue<>();
+ getWifiManager().connect(currentConfig, createActionListener(
+ blockingQueue, Integer.MAX_VALUE));
+ resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS,
+ TimeUnit.MILLISECONDS);
+ if (resultCode == null) {
+ fail("Timed out waiting for wifi to connect; ssid=" + ssid);
+ } else if (resultCode != Integer.MAX_VALUE) {
+ fail("Error connecting to wifi; ssid=" + ssid
+ + ", error=" + resultCode);
+ }
+ assertActiveNetworkMetered(metered, true /* throwOnFailure */);
+ }
+ } finally {
+ uiAutomation.dropShellPermissionIdentity();
+ }
}
- // Copied from cts/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java
- public static void assertActiveNetworkMetered(boolean expectedMeteredStatus) throws Exception {
+ private static WifiConfiguration getWifiConfiguration(String ssid) {
+ final List<String> ssids = new ArrayList<>();
+ for (WifiConfiguration config : getWifiManager().getConfiguredNetworks()) {
+ if (config.SSID.equals(ssid)) {
+ return config;
+ }
+ ssids.add(config.SSID);
+ }
+ fail("Couldn't find the wifi config; ssid=" + ssid
+ + ", all=" + Arrays.toString(ssids.toArray()));
+ return null;
+ }
+
+ private static ActionListener createActionListener(BlockingQueue<Integer> blockingQueue,
+ int successCode) {
+ return new ActionListener() {
+ @Override
+ public void onSuccess() {
+ blockingQueue.offer(successCode);
+ }
+
+ @Override
+ public void onFailure(int reason) {
+ blockingQueue.offer(reason);
+ }
+ };
+ }
+
+ private static void setCellularMeteredStatus(int subId, boolean metered) throws Exception {
+ final PersistableBundle bundle = new PersistableBundle();
+ bundle.putStringArray(KEY_CARRIER_METERED_APN_TYPES_STRINGS,
+ new String[] {ApnSetting.TYPE_MMS_STRING});
+ ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(getCarrierConfigManager(),
+ (cm) -> cm.overrideConfig(subId, metered ? null : bundle));
+ assertActiveNetworkMetered(metered, true /* throwOnFailure */);
+ }
+
+ private static boolean assertActiveNetworkMetered(boolean expectedMeteredStatus,
+ boolean throwOnFailure) throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
final NetworkCallback networkCallback = new NetworkCallback() {
@Override
@@ -197,15 +300,31 @@
// with the current setting. Therefore, if the setting has already been changed,
// this method will return right away, and if not it will wait for the setting to change.
getConnectivityManager().registerDefaultNetworkCallback(networkCallback);
- if (!latch.await(TIMEOUT_CHANGE_METEREDNESS_MS, TimeUnit.MILLISECONDS)) {
- fail("Timed out waiting for active network metered status to change to "
- + expectedMeteredStatus + " ; network = "
- + getConnectivityManager().getActiveNetwork());
+ try {
+ if (!latch.await(TIMEOUT_CHANGE_METEREDNESS_MS, TimeUnit.MILLISECONDS)) {
+ final String errorMsg = "Timed out waiting for active network metered status "
+ + "to change to " + expectedMeteredStatus + "; network = "
+ + getConnectivityManager().getActiveNetwork();
+ if (throwOnFailure) {
+ fail(errorMsg);
+ }
+ Log.w(TAG, errorMsg);
+ return false;
+ }
+ return true;
+ } finally {
+ getConnectivityManager().unregisterNetworkCallback(networkCallback);
}
- getConnectivityManager().unregisterNetworkCallback(networkCallback);
}
public static void setRestrictBackground(boolean enabled) {
+ if (!isDataSaverSupported()) {
+ return;
+ }
+ setRestrictBackgroundInternal(enabled);
+ }
+
+ private static void setRestrictBackgroundInternal(boolean enabled) {
executeShellCommand("cmd netpolicy set restrict-background " + enabled);
final String output = executeShellCommand("cmd netpolicy get restrict-background");
final String expectedSuffix = enabled ? "enabled" : "disabled";
@@ -274,6 +393,14 @@
return mWm;
}
+ public static CarrierConfigManager getCarrierConfigManager() {
+ if (mCarrierConfigManager == null) {
+ mCarrierConfigManager = (CarrierConfigManager) getContext().getSystemService(
+ Context.CARRIER_CONFIG_SERVICE);
+ }
+ return mCarrierConfigManager;
+ }
+
public static Context getContext() {
return getInstrumentation().getContext();
}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
new file mode 100644
index 0000000..29d3c6e
--- /dev/null
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/RestrictedModeTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.net.hostside;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public final class RestrictedModeTest extends AbstractRestrictBackgroundNetworkTestCase {
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ setRestrictedMode(false);
+ super.tearDown();
+ }
+
+ private void setRestrictedMode(boolean enabled) throws Exception {
+ executeSilentShellCommand(
+ "settings put global restricted_networking_mode " + (enabled ? 1 : 0));
+ assertRestrictedModeState(enabled);
+ }
+
+ private void assertRestrictedModeState(boolean enabled) throws Exception {
+ assertDelayedShellCommand("cmd netpolicy get restricted-mode",
+ "Restricted mode status: " + (enabled ? "enabled" : "disabled"));
+ }
+
+ @Test
+ public void testNetworkAccess() throws Exception {
+ setRestrictedMode(false);
+
+ // go to foreground state and enable restricted mode
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+ setRestrictedMode(true);
+ assertForegroundNetworkAccess(false);
+
+ // go to background state
+ finishActivity();
+ assertBackgroundNetworkAccess(false);
+
+ // disable restricted mode and assert network access in foreground and background states
+ setRestrictedMode(false);
+ launchComponentAndAssertNetworkAccess(TYPE_COMPONENT_ACTIVTIY);
+ assertForegroundNetworkAccess(true);
+
+ // go to background state
+ finishActivity();
+ assertBackgroundNetworkAccess(true);
+ }
+}
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
index 81a431c..29efb74 100755
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -16,6 +16,8 @@
package com.android.cts.net.hostside;
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.os.Process.INVALID_UID;
import static android.system.OsConstants.AF_INET;
import static android.system.OsConstants.AF_INET6;
@@ -25,6 +27,9 @@
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.POLLIN;
import static android.system.OsConstants.SOCK_DGRAM;
+import static android.test.MoreAsserts.assertNotEqual;
+
+import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import android.annotation.Nullable;
import android.app.DownloadManager;
@@ -45,12 +50,18 @@
import android.net.NetworkRequest;
import android.net.Proxy;
import android.net.ProxyInfo;
+import android.net.TransportInfo;
import android.net.Uri;
+import android.net.VpnManager;
import android.net.VpnService;
+import android.net.VpnTransportInfo;
import android.net.wifi.WifiManager;
+import android.os.Handler;
+import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.SystemProperties;
+import android.os.UserHandle;
import android.provider.Settings;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject;
@@ -65,6 +76,8 @@
import android.util.Log;
import com.android.compatibility.common.util.BlockingBroadcastReceiver;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.testutils.TestableNetworkCallback;
import java.io.Closeable;
import java.io.FileDescriptor;
@@ -81,6 +94,7 @@
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
@@ -702,6 +716,27 @@
getInstrumentation().getTargetContext(), MyVpnService.ACTION_ESTABLISHED);
receiver.register();
+ // Test the behaviour of a variety of types of network callbacks.
+ final Network defaultNetwork = mCM.getActiveNetwork();
+ final TestableNetworkCallback systemDefaultCallback = new TestableNetworkCallback();
+ final TestableNetworkCallback otherUidCallback = new TestableNetworkCallback();
+ final TestableNetworkCallback myUidCallback = new TestableNetworkCallback();
+ if (SdkLevel.isAtLeastS()) {
+ final int otherUid =
+ UserHandle.of(5 /* userId */).getUid(Process.FIRST_APPLICATION_UID);
+ final Handler h = new Handler(Looper.getMainLooper());
+ runWithShellPermissionIdentity(() -> {
+ mCM.registerSystemDefaultNetworkCallback(systemDefaultCallback, h);
+ mCM.registerDefaultNetworkCallbackAsUid(otherUid, otherUidCallback, h);
+ mCM.registerDefaultNetworkCallbackAsUid(Process.myUid(), myUidCallback, h);
+ }, NETWORK_SETTINGS);
+ for (TestableNetworkCallback callback :
+ List.of(systemDefaultCallback, otherUidCallback, myUidCallback)) {
+ callback.expectAvailableCallbacks(defaultNetwork, false /* suspended */,
+ true /* validated */, false /* blocked */, TIMEOUT_MS);
+ }
+ }
+
FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
@@ -719,6 +754,26 @@
checkTrafficOnVpn();
+ final Network vpnNetwork = mCM.getActiveNetwork();
+ myUidCallback.expectAvailableThenValidatedCallbacks(vpnNetwork, TIMEOUT_MS);
+ assertEquals(vpnNetwork, mCM.getActiveNetwork());
+ assertNotEqual(defaultNetwork, vpnNetwork);
+ maybeExpectVpnTransportInfo(vpnNetwork);
+
+ if (SdkLevel.isAtLeastS()) {
+ // Check that system default network callback has not seen any network changes, even
+ // though the app's default network changed. Also check that otherUidCallback saw no
+ // network changes, because otherUid is in a different user and not subject to the VPN.
+ // This needs to be done before testing private DNS because checkStrictModePrivateDns
+ // will set the private DNS server to a nonexistent name, which will cause validation to
+ // fail and could cause the default network to switch (e.g., from wifi to cellular).
+ systemDefaultCallback.assertNoCallback();
+ otherUidCallback.assertNoCallback();
+ mCM.unregisterNetworkCallback(systemDefaultCallback);
+ mCM.unregisterNetworkCallback(otherUidCallback);
+ mCM.unregisterNetworkCallback(myUidCallback);
+ }
+
checkStrictModePrivateDns();
receiver.unregisterQuietly();
@@ -739,6 +794,8 @@
checkTrafficOnVpn();
+ maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
+
checkStrictModePrivateDns();
}
@@ -764,6 +821,10 @@
assertSocketStillOpen(remoteFd, TEST_HOST);
checkNoTrafficOnVpn();
+
+ final Network network = mCM.getActiveNetwork();
+ final NetworkCapabilities nc = mCM.getNetworkCapabilities(network);
+ assertFalse(nc.hasTransport(TRANSPORT_VPN));
}
public void testGetConnectionOwnerUidSecurity() throws Exception {
@@ -778,8 +839,11 @@
InetSocketAddress rem = new InetSocketAddress(s.getInetAddress(), s.getPort());
try {
int uid = mCM.getConnectionOwnerUid(OsConstants.IPPROTO_TCP, loc, rem);
- fail("Only an active VPN app may call this API.");
- } catch (SecurityException expected) {
+ assertEquals("Only an active VPN app should see connection information",
+ INVALID_UID, uid);
+ } catch (SecurityException acceptable) {
+ // R and below throw SecurityException if a non-active VPN calls this method.
+ // As long as we can't actually get socket information, either behaviour is fine.
return;
}
}
@@ -918,6 +982,8 @@
// VPN with no underlying networks should be metered by default.
assertTrue(isNetworkMetered(mNetwork));
assertTrue(mCM.isActiveNetworkMetered());
+
+ maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
}
public void testVpnMeterednessWithNullUnderlyingNetwork() throws Exception {
@@ -944,6 +1010,8 @@
assertEquals(isNetworkMetered(underlyingNetwork), isNetworkMetered(mNetwork));
// Meteredness based on VPN capabilities and CM#isActiveNetworkMetered should be in sync.
assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered());
+
+ maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
}
public void testVpnMeterednessWithNonNullUnderlyingNetwork() throws Exception {
@@ -971,6 +1039,8 @@
assertEquals(isNetworkMetered(underlyingNetwork), isNetworkMetered(mNetwork));
// Meteredness based on VPN capabilities and CM#isActiveNetworkMetered should be in sync.
assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered());
+
+ maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
}
public void testAlwaysMeteredVpnWithNullUnderlyingNetwork() throws Exception {
@@ -995,6 +1065,8 @@
// VPN's meteredness does not depend on underlying network since it is always metered.
assertTrue(isNetworkMetered(mNetwork));
assertTrue(mCM.isActiveNetworkMetered());
+
+ maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
}
public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception {
@@ -1020,6 +1092,8 @@
// VPN's meteredness does not depend on underlying network since it is always metered.
assertTrue(isNetworkMetered(mNetwork));
assertTrue(mCM.isActiveNetworkMetered());
+
+ maybeExpectVpnTransportInfo(mCM.getActiveNetwork());
}
public void testB141603906() throws Exception {
@@ -1069,6 +1143,15 @@
}
}
+ private void maybeExpectVpnTransportInfo(Network network) {
+ if (!SdkLevel.isAtLeastS()) return;
+ final NetworkCapabilities vpnNc = mCM.getNetworkCapabilities(network);
+ assertTrue(vpnNc.hasTransport(TRANSPORT_VPN));
+ final TransportInfo ti = vpnNc.getTransportInfo();
+ assertTrue(ti instanceof VpnTransportInfo);
+ assertEquals(VpnManager.TYPE_VPN_SERVICE, ((VpnTransportInfo) ti).type);
+ }
+
private void assertDefaultProxy(ProxyInfo expected) {
assertEquals("Incorrect proxy config.", expected, mCM.getDefaultProxy());
String expectedHost = expected == null ? null : expected.getHost();
diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp
index 8e27931..b448459 100644
--- a/tests/cts/hostside/app2/Android.bp
+++ b/tests/cts/hostside/app2/Android.bp
@@ -14,6 +14,10 @@
// limitations under the License.
//
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
android_test_helper_app {
name: "CtsHostsideNetworkTestsApp2",
defaults: ["cts_support_defaults"],
diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
index 590e17e..8a5e00f 100644
--- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
+++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java
@@ -90,7 +90,7 @@
}
@Override
- public void registerNetworkCallback(INetworkCallback cb) {
+ public void registerNetworkCallback(final NetworkRequest request, INetworkCallback cb) {
if (mNetworkCallback != null) {
Log.d(TAG, "unregister previous network callback: " + mNetworkCallback);
unregisterNetworkCallback();
@@ -138,7 +138,7 @@
}
}
};
- mCm.registerNetworkCallback(makeWifiNetworkRequest(), mNetworkCallback);
+ mCm.registerNetworkCallback(request, mNetworkCallback);
try {
cb.asBinder().linkToDeath(() -> unregisterNetworkCallback(), 0);
} catch (RemoteException e) {
@@ -156,13 +156,6 @@
}
};
- private NetworkRequest makeWifiNetworkRequest() {
- return new NetworkRequest.Builder()
- .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
- .build();
- }
-
@Override
public IBinder onBind(Intent intent) {
return mBinder;
diff --git a/tests/cts/hostside/certs/Android.bp b/tests/cts/hostside/certs/Android.bp
index ab4cf34..60b5476 100644
--- a/tests/cts/hostside/certs/Android.bp
+++ b/tests/cts/hostside/certs/Android.bp
@@ -1,3 +1,7 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
android_app_certificate {
name: "cts-net-app",
certificate: "cts-net-app",
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index ce20379..37420bf 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -152,8 +152,10 @@
// build a meaningful error message
StringBuilder errorBuilder = new StringBuilder("on-device tests failed:\n");
for (Map.Entry<TestDescription, TestResult> resultEntry :
- result.getTestResults().entrySet()) {
- if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
+ result.getTestResults().entrySet()) {
+ final TestStatus testStatus = resultEntry.getValue().getStatus();
+ if (!TestStatus.PASSED.equals(testStatus)
+ && !TestStatus.ASSUMPTION_FAILURE.equals(testStatus)) {
errorBuilder.append(resultEntry.getKey().toString());
errorBuilder.append(":\n");
errorBuilder.append(resultEntry.getValue().getStackTrace());
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
index ac28c7a..a629ccf 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideRestrictBackgroundNetworkTests.java
@@ -311,6 +311,14 @@
"testAppIdleAndBatterySaver_tempPowerSaveAndAppIdleWhitelists");
}
+ /**************************
+ * Restricted mode tests. *
+ **************************/
+ public void testRestrictedMode_networkAccess() throws Exception {
+ runDeviceTests(TEST_PKG, TEST_PKG + ".RestrictedModeTest",
+ "testNetworkAccess");
+ }
+
/*******************
* Helper methods. *
*******************/
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 528171a..cd69b13 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
java_defaults {
name: "CtsNetTestCasesDefaults",
defaults: ["cts_defaults"],
@@ -37,13 +41,14 @@
],
jarjar_rules: "jarjar-rules-shared.txt",
static_libs: [
+ "bouncycastle-unbundled",
"FrameworksNetCommonTests",
- "TestNetworkStackLib",
"core-tests-support",
"cts-net-utils",
"ctstestrunner-axt",
"junit",
"junit-params",
+ "modules-utils-build",
"net-utils-framework-common",
"truth-prebuilt",
],
@@ -60,6 +65,10 @@
android_test {
name: "CtsNetTestCases",
defaults: ["CtsNetTestCasesDefaults"],
+ // TODO: CTS should not depend on the entirety of the networkstack code.
+ static_libs: [
+ "NetworkStackApiCurrentLib",
+ ],
test_suites: [
"cts",
"general-tests",
@@ -73,12 +82,19 @@
android_test {
name: "CtsNetTestCasesLatestSdk",
defaults: ["CtsNetTestCasesDefaults"],
+ // TODO: CTS should not depend on the entirety of the networkstack code.
+ static_libs: [
+ "NetworkStackApiStableLib",
+ ],
jni_uses_sdk_apis: true,
min_sdk_version: "29",
target_sdk_version: "30",
test_suites: [
"general-tests",
- "mts",
+ "mts-dnsresolver",
+ "mts-networking",
+ "mts-tethering",
+ "mts-wifi",
],
test_config_template: "AndroidTestTemplate.xml",
}
diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp
index e43a5e8..5b37294 100644
--- a/tests/cts/net/api23Test/Android.bp
+++ b/tests/cts/net/api23Test/Android.bp
@@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
android_test {
name: "CtsNetApi23TestCases",
defaults: ["cts_defaults"],
diff --git a/tests/cts/net/appForApi23/Android.bp b/tests/cts/net/appForApi23/Android.bp
index cec6d7f..b39690f 100644
--- a/tests/cts/net/appForApi23/Android.bp
+++ b/tests/cts/net/appForApi23/Android.bp
@@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
android_test {
name: "CtsNetTestAppForApi23",
defaults: ["cts_defaults"],
diff --git a/tests/cts/net/jni/Android.bp b/tests/cts/net/jni/Android.bp
index 3953aeb..13f38d7 100644
--- a/tests/cts/net/jni/Android.bp
+++ b/tests/cts/net/jni/Android.bp
@@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
cc_library_shared {
name: "libnativedns_jni",
diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp
index 6defd35..5e9af8e 100644
--- a/tests/cts/net/native/dns/Android.bp
+++ b/tests/cts/net/native/dns/Android.bp
@@ -1,3 +1,7 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
cc_defaults {
name: "dns_async_defaults",
@@ -36,6 +40,7 @@
test_suites: [
"cts",
"general-tests",
- "mts",
+ "mts-dnsresolver",
+ "mts-networking",
],
}
diff --git a/tests/cts/net/native/qtaguid/Android.bp b/tests/cts/net/native/qtaguid/Android.bp
index 4861651..68bb14d 100644
--- a/tests/cts/net/native/qtaguid/Android.bp
+++ b/tests/cts/net/native/qtaguid/Android.bp
@@ -14,6 +14,10 @@
// Build the unit tests.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
cc_test {
name: "CtsNativeNetTestCases",
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index eb5048f..a889c41 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -26,6 +26,9 @@
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkRequest
import android.net.Uri
@@ -44,8 +47,10 @@
import android.text.TextUtils
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.runner.AndroidJUnit4
+import com.android.testutils.RecorderCallback
import com.android.testutils.TestHttpServer
import com.android.testutils.TestHttpServer.Request
+import com.android.testutils.TestableNetworkCallback
import com.android.testutils.isDevSdkInRange
import com.android.testutils.runAsShell
import fi.iki.elonen.NanoHTTPD.Response.Status
@@ -124,7 +129,20 @@
assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
assumeTrue(pm.hasSystemFeature(FEATURE_WIFI))
utils.ensureWifiConnected()
- utils.connectToCell()
+ val cellNetwork = utils.connectToCell()
+
+ // Verify cell network is validated
+ val cellReq = NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .build()
+ val cellCb = TestableNetworkCallback(timeoutMs = TEST_TIMEOUT_MS)
+ cm.registerNetworkCallback(cellReq, cellCb)
+ val cb = cellCb.eventuallyExpectOrNull<RecorderCallback.CallbackEntry.CapabilitiesChanged> {
+ it.network == cellNetwork && it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+ }
+ assertNotNull(cb, "Mobile network $cellNetwork has no access to the internet. " +
+ "Check the mobile data connection.")
// Have network validation use a local server that serves a HTTPS error / HTTP redirect
server.addResponse(Request(TEST_PORTAL_URL_PATH), Status.OK,
@@ -135,7 +153,8 @@
setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH))
setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH))
// URL expiration needs to be in the next 10 minutes
- setUrlExpirationDeviceConfig(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(9))
+ assertTrue(WIFI_CONNECT_TIMEOUT_MS < TimeUnit.MINUTES.toMillis(10))
+ setUrlExpirationDeviceConfig(System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS)
// Wait for a captive portal to be detected on the network
val wifiNetworkFuture = CompletableFuture<Network>()
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index cbf43e7..e8751d8 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -19,16 +19,35 @@
import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.content.pm.PackageManager.FEATURE_BLUETOOTH;
import static android.content.pm.PackageManager.FEATURE_ETHERNET;
import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
import static android.content.pm.PackageManager.FEATURE_USB_HOST;
+import static android.content.pm.PackageManager.FEATURE_WATCH;
import static android.content.pm.PackageManager.FEATURE_WIFI;
+import static android.content.pm.PackageManager.FEATURE_WIFI_DIRECT;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
+import static android.net.ConnectivityManager.TYPE_ETHERNET;
+import static android.net.ConnectivityManager.TYPE_MOBILE_CBS;
+import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
+import static android.net.ConnectivityManager.TYPE_MOBILE_EMERGENCY;
+import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA;
+import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
+import static android.net.ConnectivityManager.TYPE_MOBILE_IA;
+import static android.net.ConnectivityManager.TYPE_MOBILE_IMS;
+import static android.net.ConnectivityManager.TYPE_MOBILE_MMS;
+import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL;
+import static android.net.ConnectivityManager.TYPE_PROXY;
+import static android.net.ConnectivityManager.TYPE_VPN;
+import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.cts.util.CtsNetUtils.ConnectivityActionReceiver;
import static android.net.cts.util.CtsNetUtils.HTTP_PORT;
@@ -43,6 +62,9 @@
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
+import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_LOCKDOWN_VPN;
+import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_NONE;
+import static com.android.testutils.MiscAsserts.assertThrows;
import static com.android.testutils.TestPermissionUtil.runAsShell;
import static org.junit.Assert.assertEquals;
@@ -68,48 +90,65 @@
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
+import android.net.InetAddresses;
import android.net.IpSecManager;
import android.net.IpSecManager.UdpEncapsulationSocket;
+import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
-import android.net.NetworkConfig;
import android.net.NetworkInfo;
import android.net.NetworkInfo.DetailedState;
import android.net.NetworkInfo.State;
import android.net.NetworkRequest;
import android.net.NetworkUtils;
import android.net.SocketKeepalive;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkManager;
import android.net.cts.util.CtsNetUtils;
import android.net.util.KeepaliveUtils;
import android.net.wifi.WifiManager;
import android.os.Binder;
import android.os.Build;
+import android.os.Handler;
import android.os.Looper;
import android.os.MessageQueue;
+import android.os.Process;
import android.os.SystemClock;
import android.os.SystemProperties;
+import android.os.UserHandle;
import android.os.VintfRuntimeInfo;
import android.platform.test.annotations.AppModeFull;
import android.provider.Settings;
+import android.telephony.TelephonyManager;
import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
+import android.util.Range;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.ArrayUtils;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
+import com.android.networkstack.apishim.ConstantsShim;
+import com.android.networkstack.apishim.common.ConnectivityManagerShim;
+import com.android.testutils.CompatUtil;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRuleKt;
import com.android.testutils.RecorderCallback.CallbackEntry;
import com.android.testutils.SkipPresubmit;
import com.android.testutils.TestableNetworkCallback;
-import libcore.io.Streams;
-
import junit.framework.AssertionFailedError;
+import libcore.io.Streams;
+
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -130,7 +169,7 @@
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashMap;
+import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
@@ -142,6 +181,8 @@
@RunWith(AndroidJUnit4.class)
public class ConnectivityManagerTest {
+ @Rule
+ public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
private static final String TAG = ConnectivityManagerTest.class.getSimpleName();
@@ -154,9 +195,8 @@
private static final int MAX_KEEPALIVE_RETRY_COUNT = 3;
private static final int MIN_KEEPALIVE_INTERVAL = 10;
- // Changing meteredness on wifi involves reconnecting, which can take several seconds (involves
- // re-associating, DHCP...)
- private static final int NETWORK_CHANGE_METEREDNESS_TIMEOUT = 30_000;
+ private static final int NETWORK_CALLBACK_TIMEOUT_MS = 30_000;
+ private static final int NO_CALLBACK_TIMEOUT_MS = 100;
private static final int NUM_TRIES_MULTIPATH_PREF_CHECK = 20;
private static final long INTERVAL_MULTIPATH_PREF_CHECK_MS = 500;
// device could have only one interface: data, wifi.
@@ -176,56 +216,108 @@
private static final String KEEPALIVE_RESERVED_PER_SLOT_RES_NAME =
"config_reservedPrivilegedKeepaliveSlots";
+ private static final LinkAddress TEST_LINKADDR = new LinkAddress(
+ InetAddresses.parseNumericAddress("2001:db8::8"), 64);
+
private Context mContext;
private Instrumentation mInstrumentation;
private ConnectivityManager mCm;
+ private ConnectivityManagerShim mCmShim;
private WifiManager mWifiManager;
private PackageManager mPackageManager;
- private final HashMap<Integer, NetworkConfig> mNetworks =
- new HashMap<Integer, NetworkConfig>();
- boolean mWifiWasDisabled;
+ private final ArraySet<Integer> mNetworkTypes = new ArraySet<>();
private UiAutomation mUiAutomation;
private CtsNetUtils mCtsNetUtils;
+ // Used for cleanup purposes.
+ private final List<Range<Integer>> mVpnRequiredUidRanges = new ArrayList<>();
+
@Before
public void setUp() throws Exception {
mInstrumentation = InstrumentationRegistry.getInstrumentation();
mContext = mInstrumentation.getContext();
mCm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mCmShim = ConnectivityManagerShimImpl.newInstance(mContext);
mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
mPackageManager = mContext.getPackageManager();
mCtsNetUtils = new CtsNetUtils(mContext);
- mWifiWasDisabled = false;
- // Get com.android.internal.R.array.networkAttributes
- int resId = mContext.getResources().getIdentifier("networkAttributes", "array", "android");
- String[] naStrings = mContext.getResources().getStringArray(resId);
- //TODO: What is the "correct" way to determine if this is a wifi only device?
- boolean wifiOnly = SystemProperties.getBoolean("ro.radio.noril", false);
- for (String naString : naStrings) {
- try {
- NetworkConfig n = new NetworkConfig(naString);
- if (wifiOnly && ConnectivityManager.isNetworkTypeMobile(n.type)) {
- continue;
- }
- mNetworks.put(n.type, n);
- } catch (Exception e) {}
+ if (DevSdkIgnoreRuleKt.isDevSdkInRange(null /* minExclusive */,
+ Build.VERSION_CODES.R /* maxInclusive */)) {
+ addLegacySupportedNetworkTypes();
+ } else {
+ addSupportedNetworkTypes();
}
+
mUiAutomation = mInstrumentation.getUiAutomation();
assertNotNull("CTS requires a working Internet connection", mCm.getActiveNetwork());
}
+ private void addLegacySupportedNetworkTypes() {
+ // Network type support as expected for android R-
+ // Get com.android.internal.R.array.networkAttributes
+ int resId = mContext.getResources().getIdentifier("networkAttributes", "array", "android");
+ String[] naStrings = mContext.getResources().getStringArray(resId);
+ boolean wifiOnly = SystemProperties.getBoolean("ro.radio.noril", false);
+ for (String naString : naStrings) {
+ try {
+ final String[] splitConfig = naString.split(",");
+ // Format was name,type,radio,priority,restoreTime,dependencyMet
+ final int type = Integer.parseInt(splitConfig[1]);
+ if (wifiOnly && ConnectivityManager.isNetworkTypeMobile(type)) {
+ continue;
+ }
+ mNetworkTypes.add(type);
+ } catch (Exception e) {}
+ }
+ }
+
+ private void addSupportedNetworkTypes() {
+ final PackageManager pm = mContext.getPackageManager();
+ if (pm.hasSystemFeature(FEATURE_WIFI)) {
+ mNetworkTypes.add(TYPE_WIFI);
+ }
+ if (pm.hasSystemFeature(FEATURE_WIFI_DIRECT)) {
+ mNetworkTypes.add(TYPE_WIFI_P2P);
+ }
+ if (mContext.getSystemService(TelephonyManager.class).isDataCapable()) {
+ mNetworkTypes.add(TYPE_MOBILE);
+ mNetworkTypes.add(TYPE_MOBILE_MMS);
+ mNetworkTypes.add(TYPE_MOBILE_SUPL);
+ mNetworkTypes.add(TYPE_MOBILE_DUN);
+ mNetworkTypes.add(TYPE_MOBILE_HIPRI);
+ mNetworkTypes.add(TYPE_MOBILE_FOTA);
+ mNetworkTypes.add(TYPE_MOBILE_IMS);
+ mNetworkTypes.add(TYPE_MOBILE_CBS);
+ mNetworkTypes.add(TYPE_MOBILE_IA);
+ mNetworkTypes.add(TYPE_MOBILE_EMERGENCY);
+ }
+ if (pm.hasSystemFeature(FEATURE_BLUETOOTH)) {
+ mNetworkTypes.add(TYPE_BLUETOOTH);
+ }
+ if (pm.hasSystemFeature(FEATURE_WATCH)) {
+ mNetworkTypes.add(TYPE_PROXY);
+ }
+ if (mContext.getSystemService(Context.ETHERNET_SERVICE) != null) {
+ mNetworkTypes.add(TYPE_ETHERNET);
+ }
+ mNetworkTypes.add(TYPE_VPN);
+ }
+
@After
public void tearDown() throws Exception {
- // Return WiFi to its original disabled state after tests that explicitly connect.
- if (mWifiWasDisabled) {
- mCtsNetUtils.disconnectFromWifi(null);
- }
+ // Release any NetworkRequests filed to connect mobile data.
if (mCtsNetUtils.cellConnectAttempted()) {
mCtsNetUtils.disconnectFromCell();
}
+ if (shouldTestSApis()) {
+ runWithShellPermissionIdentity(
+ () -> mCmShim.setRequireVpnForUids(false, mVpnRequiredUidRanges),
+ NETWORK_SETTINGS);
+ }
+
// All tests in this class require a working Internet connection as they start. Make
// sure there is still one as they end that's ready to use for the next test to use.
final TestNetworkCallback callback = new TestNetworkCallback();
@@ -237,17 +329,6 @@
}
}
- /**
- * Make sure WiFi is connected to an access point if it is not already. If
- * WiFi is enabled as a result of this function, it will be disabled
- * automatically in tearDown().
- */
- private Network ensureWifiConnected() {
- mWifiWasDisabled = !mWifiManager.isWifiEnabled();
- // Even if wifi is enabled, the network may not be connected or ready yet
- return mCtsNetUtils.connectToWifi();
- }
-
@Test
public void testIsNetworkTypeValid() {
assertTrue(ConnectivityManager.isNetworkTypeValid(ConnectivityManager.TYPE_MOBILE));
@@ -456,9 +537,9 @@
}
private boolean shouldBeSupported(int networkType) {
- return mNetworks.containsKey(networkType) ||
- (networkType == ConnectivityManager.TYPE_VPN) ||
- (networkType == ConnectivityManager.TYPE_ETHERNET && shouldEthernetBeSupported());
+ return mNetworkTypes.contains(networkType)
+ || (networkType == ConnectivityManager.TYPE_VPN)
+ || (networkType == ConnectivityManager.TYPE_ETHERNET && shouldEthernetBeSupported());
}
@Test
@@ -521,25 +602,50 @@
final TestNetworkCallback defaultTrackingCallback = new TestNetworkCallback();
mCm.registerDefaultNetworkCallback(defaultTrackingCallback);
+ final TestNetworkCallback systemDefaultCallback = new TestNetworkCallback();
+ final TestNetworkCallback perUidCallback = new TestNetworkCallback();
+ final Handler h = new Handler(Looper.getMainLooper());
+ if (shouldTestSApis()) {
+ runWithShellPermissionIdentity(() -> {
+ mCmShim.registerSystemDefaultNetworkCallback(systemDefaultCallback, h);
+ mCmShim.registerDefaultNetworkCallbackAsUid(Process.myUid(), perUidCallback, h);
+ }, NETWORK_SETTINGS);
+ }
+
Network wifiNetwork = null;
try {
- ensureWifiConnected();
+ mCtsNetUtils.ensureWifiConnected();
// Now we should expect to get a network callback about availability of the wifi
// network even if it was already connected as a state-based action when the callback
// is registered.
wifiNetwork = callback.waitForAvailable();
- assertNotNull("Did not receive NetworkCallback.onAvailable for TRANSPORT_WIFI",
+ assertNotNull("Did not receive onAvailable for TRANSPORT_WIFI request",
wifiNetwork);
- assertNotNull("Did not receive NetworkCallback.onAvailable for any default network",
- defaultTrackingCallback.waitForAvailable());
+ final Network defaultNetwork = defaultTrackingCallback.waitForAvailable();
+ assertNotNull("Did not receive onAvailable on default network callback",
+ defaultNetwork);
+
+ if (shouldTestSApis()) {
+ assertNotNull("Did not receive onAvailable on system default network callback",
+ systemDefaultCallback.waitForAvailable());
+ final Network perUidNetwork = perUidCallback.waitForAvailable();
+ assertNotNull("Did not receive onAvailable on per-UID default network callback",
+ perUidNetwork);
+ assertEquals(defaultNetwork, perUidNetwork);
+ }
+
} catch (InterruptedException e) {
fail("Broadcast receiver or NetworkCallback wait was interrupted.");
} finally {
mCm.unregisterNetworkCallback(callback);
mCm.unregisterNetworkCallback(defaultTrackingCallback);
+ if (shouldTestSApis()) {
+ mCm.unregisterNetworkCallback(systemDefaultCallback);
+ mCm.unregisterNetworkCallback(perUidCallback);
+ }
}
}
@@ -558,23 +664,29 @@
// Create a ConnectivityActionReceiver that has an IntentFilter for our locally defined
// action, NETWORK_CALLBACK_ACTION.
- IntentFilter filter = new IntentFilter();
+ final IntentFilter filter = new IntentFilter();
filter.addAction(NETWORK_CALLBACK_ACTION);
- ConnectivityActionReceiver receiver = new ConnectivityActionReceiver(
+ final ConnectivityActionReceiver receiver = new ConnectivityActionReceiver(
mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.CONNECTED);
mContext.registerReceiver(receiver, filter);
// Create a broadcast PendingIntent for NETWORK_CALLBACK_ACTION.
- Intent intent = new Intent(NETWORK_CALLBACK_ACTION);
- PendingIntent pendingIntent = PendingIntent.getBroadcast(
- mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+ final Intent intent = new Intent(NETWORK_CALLBACK_ACTION)
+ .setPackage(mContext.getPackageName());
+ // While ConnectivityService would put extra info such as network or request id before
+ // broadcasting the inner intent. The MUTABLE flag needs to be added accordingly.
+ // TODO: replace with PendingIntent.FLAG_MUTABLE when this code compiles against S+ or
+ // shims.
+ final int pendingIntentFlagMutable = 1 << 25;
+ final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0 /*requestCode*/,
+ intent, PendingIntent.FLAG_CANCEL_CURRENT | pendingIntentFlagMutable);
// We will register for a WIFI network being available or lost.
mCm.registerNetworkCallback(makeWifiNetworkRequest(), pendingIntent);
try {
- ensureWifiConnected();
+ mCtsNetUtils.ensureWifiConnected();
// Now we expect to get the Intent delivered notifying of the availability of the wifi
// network even if it was already connected as a state-based action when the callback
@@ -655,6 +767,17 @@
return null;
}
+ /**
+ * Checks that enabling/disabling wifi causes CONNECTIVITY_ACTION broadcasts.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ public void testToggleWifiConnectivityAction() {
+ // toggleWifi calls connectToWifi and disconnectFromWifi, which both wait for
+ // CONNECTIVITY_ACTION broadcasts.
+ mCtsNetUtils.toggleWifi();
+ }
+
/** Verify restricted networks cannot be requested. */
@AppModeFull(reason = "CHANGE_NETWORK_STATE permission can't be granted to instant apps")
@Test
@@ -726,7 +849,9 @@
// with the current setting. Therefore, if the setting has already been changed,
// this method will return right away, and if not it will wait for the setting to change.
mCm.registerDefaultNetworkCallback(networkCallback);
- if (!latch.await(NETWORK_CHANGE_METEREDNESS_TIMEOUT, TimeUnit.MILLISECONDS)) {
+ // Changing meteredness on wifi involves reconnecting, which can take several seconds
+ // (involves re-associating, DHCP...).
+ if (!latch.await(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
fail("Timed out waiting for active network metered status to change to "
+ requestedMeteredness + " ; network = " + mCm.getActiveNetwork());
}
@@ -782,7 +907,7 @@
@Test
public void testGetMultipathPreference() throws Exception {
final ContentResolver resolver = mContext.getContentResolver();
- ensureWifiConnected();
+ mCtsNetUtils.ensureWifiConnected();
final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID());
final String oldMeteredSetting = getWifiMeteredStatus(ssid);
final String oldMeteredMultipathPreference = Settings.Global.getString(
@@ -796,7 +921,7 @@
waitForActiveNetworkMetered(TRANSPORT_WIFI, true);
// Wifi meterness changes from unmetered to metered will disconnect and reconnect since
// R.
- final Network network = ensureWifiConnected();
+ final Network network = mCtsNetUtils.ensureWifiConnected();
assertEquals(ssid, unquoteSSID(mWifiManager.getConnectionInfo().getSSID()));
assertEquals(mCm.getNetworkCapabilities(network).hasCapability(
NET_CAPABILITY_NOT_METERED), false);
@@ -1010,7 +1135,7 @@
return;
}
- final Network network = ensureWifiConnected();
+ final Network network = mCtsNetUtils.ensureWifiConnected();
if (getSupportedKeepalivesForNet(network) != 0) return;
final InetAddress srcAddr = getFirstV4Address(network);
assumeTrue("This test requires native IPv4", srcAddr != null);
@@ -1030,7 +1155,7 @@
return;
}
- final Network network = ensureWifiConnected();
+ final Network network = mCtsNetUtils.ensureWifiConnected();
if (getSupportedKeepalivesForNet(network) == 0) return;
final InetAddress srcAddr = getFirstV4Address(network);
assumeTrue("This test requires native IPv4", srcAddr != null);
@@ -1241,7 +1366,7 @@
return;
}
- final Network network = ensureWifiConnected();
+ final Network network = mCtsNetUtils.ensureWifiConnected();
final int supported = getSupportedKeepalivesForNet(network);
if (supported == 0) {
return;
@@ -1338,7 +1463,7 @@
return;
}
- final Network network = ensureWifiConnected();
+ final Network network = mCtsNetUtils.ensureWifiConnected();
final int supported = getSupportedKeepalivesForNet(network);
if (supported == 0) {
return;
@@ -1386,7 +1511,7 @@
// Ensure that NetworkUtils.queryUserAccess always returns false since this package should
// not have netd system permission to call this function.
- final Network wifiNetwork = ensureWifiConnected();
+ final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
assertFalse(NetworkUtils.queryUserAccess(Binder.getCallingUid(), wifiNetwork.netId));
// Ensure that this package cannot bind to any restricted network that's currently
@@ -1479,12 +1604,12 @@
}
private void waitForAvailable(@NonNull final TestableNetworkCallback cb) {
- cb.eventuallyExpect(CallbackEntry.AVAILABLE, AIRPLANE_MODE_CHANGE_TIMEOUT_MS,
+ cb.eventuallyExpect(CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
c -> c instanceof CallbackEntry.Available);
}
private void waitForLost(@NonNull final TestableNetworkCallback cb) {
- cb.eventuallyExpect(CallbackEntry.LOST, AIRPLANE_MODE_CHANGE_TIMEOUT_MS,
+ cb.eventuallyExpect(CallbackEntry.LOST, NETWORK_CALLBACK_TIMEOUT_MS,
c -> c instanceof CallbackEntry.Lost);
}
@@ -1531,4 +1656,259 @@
throw new AssertionFailedError("Captive portal server URL is invalid: " + e);
}
}
+
+ /**
+ * Verifies that apps are forbidden from getting ssid information from
+ * {@Code NetworkCapabilities} if they do not hold NETWORK_SETTINGS permission.
+ * See b/161370134.
+ */
+ @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
+ @Test
+ public void testSsidInNetworkCapabilities() throws Exception {
+ assumeTrue("testSsidInNetworkCapabilities cannot execute unless device supports WiFi",
+ mPackageManager.hasSystemFeature(FEATURE_WIFI));
+
+ final Network network = mCtsNetUtils.ensureWifiConnected();
+ final String ssid = unquoteSSID(mWifiManager.getConnectionInfo().getSSID());
+ assertNotNull("Ssid getting from WiifManager is null", ssid);
+ // This package should have no NETWORK_SETTINGS permission. Verify that no ssid is contained
+ // in the NetworkCapabilities.
+ verifySsidFromQueriedNetworkCapabilities(network, ssid, false /* hasSsid */);
+ verifySsidFromCallbackNetworkCapabilities(ssid, false /* hasSsid */);
+ // Adopt shell permission to allow to get ssid information.
+ runWithShellPermissionIdentity(() -> {
+ verifySsidFromQueriedNetworkCapabilities(network, ssid, true /* hasSsid */);
+ verifySsidFromCallbackNetworkCapabilities(ssid, true /* hasSsid */);
+ });
+ }
+
+ private void verifySsidFromQueriedNetworkCapabilities(@NonNull Network network,
+ @NonNull String ssid, boolean hasSsid) throws Exception {
+ // Verify if ssid is contained in NetworkCapabilities queried from ConnectivityManager.
+ final NetworkCapabilities nc = mCm.getNetworkCapabilities(network);
+ assertNotNull("NetworkCapabilities of the network is null", nc);
+ assertEquals(hasSsid, Pattern.compile(ssid).matcher(nc.toString()).find());
+ }
+
+ private void verifySsidFromCallbackNetworkCapabilities(@NonNull String ssid, boolean hasSsid)
+ throws Exception {
+ final CompletableFuture<NetworkCapabilities> foundNc = new CompletableFuture();
+ final NetworkCallback callback = new NetworkCallback() {
+ @Override
+ public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
+ foundNc.complete(nc);
+ }
+ };
+ try {
+ mCm.registerNetworkCallback(makeWifiNetworkRequest(), callback);
+ // Registering a callback here guarantees onCapabilitiesChanged is called immediately
+ // because WiFi network should be connected.
+ final NetworkCapabilities nc =
+ foundNc.get(NETWORK_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ // Verify if ssid is contained in the NetworkCapabilities received from callback.
+ assertNotNull("NetworkCapabilities of the network is null", nc);
+ assertEquals(hasSsid, Pattern.compile(ssid).matcher(nc.toString()).find());
+ } finally {
+ mCm.unregisterNetworkCallback(callback);
+ }
+ }
+
+ /**
+ * Verify background request can only be requested when acquiring
+ * {@link android.Manifest.permission.NETWORK_SETTINGS}.
+ */
+ @Test
+ public void testRequestBackgroundNetwork() {
+ // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
+ // shims, and @IgnoreUpTo does not check that.
+ assumeTrue(shouldTestSApis());
+
+ // Create a tun interface. Use the returned interface name as the specifier to create
+ // a test network request.
+ final TestNetworkManager tnm = runWithShellPermissionIdentity(() ->
+ mContext.getSystemService(TestNetworkManager.class),
+ android.Manifest.permission.MANAGE_TEST_NETWORKS);
+ final TestNetworkInterface testNetworkInterface = runWithShellPermissionIdentity(() ->
+ tnm.createTunInterface(new LinkAddress[]{TEST_LINKADDR}),
+ android.Manifest.permission.MANAGE_TEST_NETWORKS,
+ android.Manifest.permission.NETWORK_SETTINGS);
+ assertNotNull(testNetworkInterface);
+
+ final NetworkRequest testRequest = new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_TEST)
+ // Test networks do not have NOT_VPN or TRUSTED capabilities by default
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+ .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
+ .setNetworkSpecifier(CompatUtil.makeTestNetworkSpecifier(
+ testNetworkInterface.getInterfaceName()))
+ .build();
+
+ // Verify background network cannot be requested without NETWORK_SETTINGS permission.
+ final TestableNetworkCallback callback = new TestableNetworkCallback();
+ final Handler handler = new Handler(Looper.getMainLooper());
+ assertThrows(SecurityException.class,
+ () -> mCmShim.requestBackgroundNetwork(testRequest, handler, callback));
+
+ Network testNetwork = null;
+ try {
+ // Request background test network via Shell identity which has NETWORK_SETTINGS
+ // permission granted.
+ runWithShellPermissionIdentity(
+ () -> mCmShim.requestBackgroundNetwork(testRequest, handler, callback),
+ new String[] { android.Manifest.permission.NETWORK_SETTINGS });
+
+ // Register the test network agent which has no foreground request associated to it.
+ // And verify it can satisfy the background network request just fired.
+ final Binder binder = new Binder();
+ runWithShellPermissionIdentity(() ->
+ tnm.setupTestNetwork(testNetworkInterface.getInterfaceName(), binder),
+ new String[] { android.Manifest.permission.MANAGE_TEST_NETWORKS,
+ android.Manifest.permission.NETWORK_SETTINGS });
+ waitForAvailable(callback);
+ testNetwork = callback.getLastAvailableNetwork();
+ assertNotNull(testNetwork);
+
+ // The test network that has just connected is a foreground network,
+ // non-listen requests will get available callback before it can be put into
+ // background if no foreground request can be satisfied. Thus, wait for a short
+ // period is needed to let foreground capability go away.
+ callback.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
+ NETWORK_CALLBACK_TIMEOUT_MS,
+ c -> c instanceof CallbackEntry.CapabilitiesChanged
+ && !((CallbackEntry.CapabilitiesChanged) c).getCaps()
+ .hasCapability(NET_CAPABILITY_FOREGROUND));
+ final NetworkCapabilities nc = mCm.getNetworkCapabilities(testNetwork);
+ assertFalse("expected background network, but got " + nc,
+ nc.hasCapability(NET_CAPABILITY_FOREGROUND));
+ } finally {
+ final Network n = testNetwork;
+ runWithShellPermissionIdentity(() -> {
+ if (null != n) {
+ tnm.teardownTestNetwork(n);
+ callback.eventuallyExpect(CallbackEntry.LOST,
+ NETWORK_CALLBACK_TIMEOUT_MS,
+ lost -> n.equals(lost.getNetwork()));
+ }
+ testNetworkInterface.getFileDescriptor().close();
+ }, new String[] { android.Manifest.permission.MANAGE_TEST_NETWORKS });
+ mCm.unregisterNetworkCallback(callback);
+ }
+ }
+
+ private class DetailedBlockedStatusCallback extends TestableNetworkCallback {
+ public void expectAvailableCallbacks(Network network) {
+ super.expectAvailableCallbacks(network, false /* suspended */, true /* validated */,
+ BLOCKED_REASON_NONE, NETWORK_CALLBACK_TIMEOUT_MS);
+ }
+ public void expectBlockedStatusCallback(Network network, int blockedStatus) {
+ super.expectBlockedStatusCallback(blockedStatus, network, NETWORK_CALLBACK_TIMEOUT_MS);
+ }
+ public void onBlockedStatusChanged(Network network, int blockedReasons) {
+ getHistory().add(new CallbackEntry.BlockedStatusInt(network, blockedReasons));
+ }
+ }
+
+ private void setRequireVpnForUids(boolean requireVpn, Collection<Range<Integer>> ranges)
+ throws Exception {
+ mCmShim.setRequireVpnForUids(requireVpn, ranges);
+ for (Range<Integer> range : ranges) {
+ if (requireVpn) {
+ mVpnRequiredUidRanges.add(range);
+ } else {
+ assertTrue(mVpnRequiredUidRanges.remove(range));
+ }
+ }
+ }
+
+ private void doTestBlockedStatusCallback() throws Exception {
+ final DetailedBlockedStatusCallback myUidCallback = new DetailedBlockedStatusCallback();
+ final DetailedBlockedStatusCallback otherUidCallback = new DetailedBlockedStatusCallback();
+
+ final int myUid = Process.myUid();
+ final int otherUid = UserHandle.getUid(5, Process.FIRST_APPLICATION_UID);
+ final Handler handler = new Handler(Looper.getMainLooper());
+ mCm.registerDefaultNetworkCallback(myUidCallback, handler);
+ mCmShim.registerDefaultNetworkCallbackAsUid(otherUid, otherUidCallback, handler);
+
+ final Network defaultNetwork = mCm.getActiveNetwork();
+ final List<DetailedBlockedStatusCallback> allCallbacks =
+ List.of(myUidCallback, otherUidCallback);
+ for (DetailedBlockedStatusCallback callback : allCallbacks) {
+ callback.expectAvailableCallbacks(defaultNetwork);
+ }
+
+ final Range<Integer> myUidRange = new Range<>(myUid, myUid);
+ final Range<Integer> otherUidRange = new Range<>(otherUid, otherUid);
+
+ setRequireVpnForUids(true, List.of(myUidRange));
+ myUidCallback.expectBlockedStatusCallback(defaultNetwork, BLOCKED_REASON_LOCKDOWN_VPN);
+ otherUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS);
+
+ setRequireVpnForUids(true, List.of(myUidRange, otherUidRange));
+ myUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS);
+ otherUidCallback.expectBlockedStatusCallback(defaultNetwork, BLOCKED_REASON_LOCKDOWN_VPN);
+
+ // setRequireVpnForUids does no deduplication or refcounting. Removing myUidRange does not
+ // unblock myUid because it was added to the blocked ranges twice.
+ setRequireVpnForUids(false, List.of(myUidRange));
+ myUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS);
+ otherUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS);
+
+ setRequireVpnForUids(false, List.of(myUidRange, otherUidRange));
+ myUidCallback.expectBlockedStatusCallback(defaultNetwork, BLOCKED_REASON_NONE);
+ otherUidCallback.expectBlockedStatusCallback(defaultNetwork, BLOCKED_REASON_NONE);
+
+ myUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS);
+ otherUidCallback.assertNoCallback(NO_CALLBACK_TIMEOUT_MS);
+ }
+
+ @Test
+ public void testBlockedStatusCallback() {
+ // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
+ // shims, and @IgnoreUpTo does not check that.
+ assumeTrue(shouldTestSApis());
+ runWithShellPermissionIdentity(() -> doTestBlockedStatusCallback(), NETWORK_SETTINGS);
+ }
+
+ private void doTestLegacyLockdownEnabled() throws Exception {
+ NetworkInfo info = mCm.getActiveNetworkInfo();
+ assertNotNull(info);
+ assertEquals(DetailedState.CONNECTED, info.getDetailedState());
+
+ try {
+ mCmShim.setLegacyLockdownVpnEnabled(true);
+
+ // setLegacyLockdownVpnEnabled is asynchronous and only takes effect when the
+ // ConnectivityService handler thread processes it. Ensure it has taken effect by doing
+ // something that blocks until the handler thread is idle.
+ final TestableNetworkCallback callback = new TestableNetworkCallback();
+ mCm.registerDefaultNetworkCallback(callback);
+ waitForAvailable(callback);
+ mCm.unregisterNetworkCallback(callback);
+
+ // Test one of the effects of setLegacyLockdownVpnEnabled: the fact that any NetworkInfo
+ // in state CONNECTED is degraded to CONNECTING if the legacy VPN is not connected.
+ info = mCm.getActiveNetworkInfo();
+ assertNotNull(info);
+ assertEquals(DetailedState.CONNECTING, info.getDetailedState());
+ } finally {
+ mCmShim.setLegacyLockdownVpnEnabled(false);
+ }
+ }
+
+ @Test
+ public void testLegacyLockdownEnabled() {
+ // Cannot use @IgnoreUpTo(Build.VERSION_CODES.R) because this test also requires API 31
+ // shims, and @IgnoreUpTo does not check that.
+ assumeTrue(shouldTestSApis());
+ runWithShellPermissionIdentity(() -> doTestLegacyLockdownEnabled(), NETWORK_SETTINGS);
+ }
+
+ /**
+ * Whether to test S+ APIs. This requires a) that the test be running on an S+ device, and
+ * b) that the code be compiled against shims new enough to access these APIs.
+ */
+ private boolean shouldTestSApis() {
+ return SdkLevel.isAtLeastS() && ConstantsShim.VERSION > Build.VERSION_CODES.R;
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 9eab024..c6d8d65 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -55,10 +55,10 @@
import androidx.test.InstrumentationRegistry;
import com.android.internal.util.HexDump;
-import com.android.org.bouncycastle.x509.X509V1CertificateGenerator;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
import com.android.testutils.DevSdkIgnoreRunner;
+import org.bouncycastle.x509.X509V1CertificateGenerator;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
index 87aa2a3..f53a2a8 100644
--- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt
@@ -18,20 +18,16 @@
import android.app.Instrumentation
import android.content.Context
import android.net.ConnectivityManager
+import android.net.INetworkAgent
+import android.net.INetworkAgentRegistry
import android.net.InetAddresses
import android.net.IpPrefix
import android.net.KeepalivePacketData
import android.net.LinkAddress
import android.net.LinkProperties
+import android.net.NattKeepalivePacketData
import android.net.Network
import android.net.NetworkAgent
-import android.net.NetworkAgent.CMD_ADD_KEEPALIVE_PACKET_FILTER
-import android.net.NetworkAgent.CMD_PREVENT_AUTOMATIC_RECONNECT
-import android.net.NetworkAgent.CMD_REMOVE_KEEPALIVE_PACKET_FILTER
-import android.net.NetworkAgent.CMD_REPORT_NETWORK_STATUS
-import android.net.NetworkAgent.CMD_SAVE_ACCEPT_UNVALIDATED
-import android.net.NetworkAgent.CMD_START_SOCKET_KEEPALIVE
-import android.net.NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE
import android.net.NetworkAgent.INVALID_NETWORK
import android.net.NetworkAgent.VALID_NETWORK
import android.net.NetworkAgentConfig
@@ -41,6 +37,7 @@
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
@@ -49,13 +46,17 @@
import android.net.NetworkInfo
import android.net.NetworkProvider
import android.net.NetworkRequest
+import android.net.NetworkScore
import android.net.RouteInfo
import android.net.SocketKeepalive
-import android.net.StringNetworkSpecifier
import android.net.Uri
+import android.net.VpnManager
+import android.net.VpnTransportInfo
import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnAddKeepalivePacketFilter
import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnAutomaticReconnectDisabled
import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnBandwidthUpdateRequested
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
+import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnNetworkDestroyed
import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnNetworkUnwanted
import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnRemoveKeepalivePacketFilter
import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnSaveAcceptUnvalidated
@@ -64,35 +65,30 @@
import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnStopSocketKeepalive
import android.net.cts.NetworkAgentTest.TestableNetworkAgent.CallbackEntry.OnValidationStatus
import android.os.Build
-import android.os.Bundle
-import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.os.Message
-import android.os.Messenger
import android.util.DebugUtils.valueToString
import androidx.test.InstrumentationRegistry
-import androidx.test.runner.AndroidJUnit4
-import com.android.internal.util.AsyncChannel
+import com.android.modules.utils.build.SdkLevel
import com.android.net.module.util.ArrayTrackRecord
-import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.CompatUtil
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.RecorderCallback.CallbackEntry.Available
import com.android.testutils.RecorderCallback.CallbackEntry.Lost
import com.android.testutils.TestableNetworkCallback
import org.junit.After
import org.junit.Assert.assertArrayEquals
-import org.junit.Assert.fail
import org.junit.Before
-import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.argThat
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
+import org.mockito.Mockito.timeout
import org.mockito.Mockito.verify
import java.time.Duration
import java.util.Arrays
@@ -103,6 +99,7 @@
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
+import kotlin.test.fail
// This test doesn't really have a constraint on how fast the methods should return. If it's
// going to fail, it will simply wait forever, so setting a high timeout lowers the flake ratio
@@ -130,17 +127,17 @@
it.obj = obj
}
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
+// NetworkAgent is not updatable in R-, so this test does not need to be compatible with older
+// versions. NetworkAgent was also based on AsyncChannel before S so cannot be tested the same way.
+@IgnoreUpTo(Build.VERSION_CODES.R)
class NetworkAgentTest {
- @Rule @JvmField
- val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
-
private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
private val REMOTE_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.2")
private val mCM = realContext.getSystemService(ConnectivityManager::class.java)
private val mHandlerThread = HandlerThread("${javaClass.simpleName} handler thread")
- private val mFakeConnectivityService by lazy { FakeConnectivityService(mHandlerThread.looper) }
+ private val mFakeConnectivityService = FakeConnectivityService()
private class Provider(context: Context, looper: Looper) :
NetworkProvider(context, looper, "NetworkAgentTest NetworkProvider")
@@ -167,39 +164,30 @@
* This fake only supports speaking to one harnessed agent at a time because it
* only keeps track of one async channel.
*/
- private class FakeConnectivityService(looper: Looper) {
- private val CMD_EXPECT_DISCONNECT = 1
- private var disconnectExpected = false
- private val msgHistory = ArrayTrackRecord<Message>().newReadHead()
- private val asyncChannel = AsyncChannel()
- private val handler = object : Handler(looper) {
- override fun handleMessage(msg: Message) {
- msgHistory.add(Message.obtain(msg)) // make a copy as the original will be recycled
- when (msg.what) {
- CMD_EXPECT_DISCONNECT -> disconnectExpected = true
- AsyncChannel.CMD_CHANNEL_HALF_CONNECTED ->
- asyncChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION)
- AsyncChannel.CMD_CHANNEL_DISCONNECTED ->
- if (!disconnectExpected) {
- fail("Agent unexpectedly disconnected")
- } else {
- disconnectExpected = false
- }
- }
- }
+ private class FakeConnectivityService {
+ val mockRegistry = mock(INetworkAgentRegistry::class.java)
+ private var agentField: INetworkAgent? = null
+ private val registry = object : INetworkAgentRegistry.Stub(),
+ INetworkAgentRegistry by mockRegistry {
+ // asBinder has implementations in both INetworkAgentRegistry.Stub and mockRegistry, so
+ // it needs to be disambiguated. Just fail the test as it should be unused here.
+ // asBinder is used when sending the registry in binder transactions, so not in this
+ // test (the test just uses in-process direct calls). If it were used across processes,
+ // using the Stub super.asBinder() implementation would allow sending the registry in
+ // binder transactions, while recording incoming calls on the other mockito-generated
+ // methods.
+ override fun asBinder() = fail("asBinder should be unused in this test")
}
- fun connect(agentMsngr: Messenger) = asyncChannel.connect(realContext, handler, agentMsngr)
+ val agent: INetworkAgent
+ get() = agentField ?: fail("No INetworkAgent")
- fun disconnect() = asyncChannel.disconnect()
+ fun connect(agent: INetworkAgent) {
+ this.agentField = agent
+ agent.onRegistered(registry)
+ }
- fun sendMessage(what: Int, arg1: Int = 0, arg2: Int = 0, obj: Any? = null) =
- asyncChannel.sendMessage(Message(what, arg1, arg2, obj))
-
- fun expectMessage(what: Int) =
- assertNotNull(msgHistory.poll(DEFAULT_TIMEOUT_MS) { it.what == what })
-
- fun willExpectDisconnectOnce() = handler.sendEmptyMessage(CMD_EXPECT_DISCONNECT)
+ fun disconnect() = agent.onDisconnected()
}
private open class TestableNetworkAgent(
@@ -230,10 +218,10 @@
object OnAutomaticReconnectDisabled : CallbackEntry()
data class OnValidationStatus(val status: Int, val uri: Uri?) : CallbackEntry()
data class OnSignalStrengthThresholdsUpdated(val thresholds: IntArray) : CallbackEntry()
+ object OnNetworkCreated : CallbackEntry()
+ object OnNetworkDestroyed : CallbackEntry()
}
- fun getName(): String? = (nc.getNetworkSpecifier() as? StringNetworkSpecifier)?.specifier
-
override fun onBandwidthUpdateRequested() {
history.add(OnBandwidthUpdateRequested)
}
@@ -285,6 +273,14 @@
history.add(OnValidationStatus(status, uri))
}
+ override fun onNetworkCreated() {
+ history.add(OnNetworkCreated)
+ }
+
+ override fun onNetworkDestroyed() {
+ history.add(OnNetworkDestroyed)
+ }
+
// Expects the initial validation event that always occurs immediately after registering
// a NetworkAgent whose network does not require validation (which test networks do
// not, since they lack the INTERNET capability). It always contains the default argument
@@ -337,8 +333,11 @@
addCapability(NET_CAPABILITY_NOT_SUSPENDED)
addCapability(NET_CAPABILITY_NOT_ROAMING)
addCapability(NET_CAPABILITY_NOT_VPN)
+ if (SdkLevel.isAtLeastS()) {
+ addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ }
if (null != name) {
- setNetworkSpecifier(StringNetworkSpecifier(name))
+ setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name))
}
}
val lp = initialLp ?: LinkProperties().apply {
@@ -360,8 +359,10 @@
val callback = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
requestNetwork(request, callback)
val agent = createNetworkAgent(context, name)
+ agent.setTeardownDelayMs(0)
agent.register()
agent.markConnected()
+ agent.expectCallback<OnNetworkCreated>()
return agent to callback
}
@@ -381,6 +382,7 @@
assertFailsWith<IllegalStateException>("Must not be able to register an agent twice") {
agent.register()
}
+ agent.expectCallback<OnNetworkDestroyed>()
}
@Test
@@ -445,17 +447,15 @@
@Test
fun testSocketKeepalive(): Unit = createNetworkAgentWithFakeCS().let { agent ->
- val packet = object : KeepalivePacketData(
+ val packet = NattKeepalivePacketData(
LOCAL_IPV4_ADDRESS /* srcAddress */, 1234 /* srcPort */,
REMOTE_IPV4_ADDRESS /* dstAddress */, 4567 /* dstPort */,
- ByteArray(100 /* size */) { it.toByte() /* init */ }) {}
+ ByteArray(100 /* size */))
val slot = 4
val interval = 37
- mFakeConnectivityService.sendMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER,
- arg1 = slot, obj = packet)
- mFakeConnectivityService.sendMessage(CMD_START_SOCKET_KEEPALIVE,
- arg1 = slot, arg2 = interval, obj = packet)
+ mFakeConnectivityService.agent.onAddNattKeepalivePacketFilter(slot, packet)
+ mFakeConnectivityService.agent.onStartNattSocketKeepalive(slot, interval, packet)
agent.expectCallback<OnAddKeepalivePacketFilter>().let {
assertEquals(it.slot, slot)
@@ -472,13 +472,11 @@
// Check that when the agent sends a keepalive event, ConnectivityService receives the
// expected message.
agent.sendSocketKeepaliveEvent(slot, SocketKeepalive.ERROR_UNSUPPORTED)
- mFakeConnectivityService.expectMessage(NetworkAgent.EVENT_SOCKET_KEEPALIVE).let() {
- assertEquals(slot, it.arg1)
- assertEquals(SocketKeepalive.ERROR_UNSUPPORTED, it.arg2)
- }
+ verify(mFakeConnectivityService.mockRegistry, timeout(DEFAULT_TIMEOUT_MS))
+ .sendSocketKeepaliveEvent(slot, SocketKeepalive.ERROR_UNSUPPORTED)
- mFakeConnectivityService.sendMessage(CMD_STOP_SOCKET_KEEPALIVE, arg1 = slot)
- mFakeConnectivityService.sendMessage(CMD_REMOVE_KEEPALIVE_PACKET_FILTER, arg1 = slot)
+ mFakeConnectivityService.agent.onStopSocketKeepalive(slot)
+ mFakeConnectivityService.agent.onRemoveKeepalivePacketFilter(slot)
agent.expectCallback<OnStopSocketKeepalive>().let {
assertEquals(it.slot, slot)
}
@@ -518,12 +516,12 @@
val request1 = NetworkRequest.Builder()
.clearCapabilities()
.addTransportType(TRANSPORT_TEST)
- .setNetworkSpecifier(StringNetworkSpecifier(name1))
+ .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name1))
.build()
val request2 = NetworkRequest.Builder()
.clearCapabilities()
.addTransportType(TRANSPORT_TEST)
- .setNetworkSpecifier(StringNetworkSpecifier(name2))
+ .setNetworkSpecifier(CompatUtil.makeEthernetNetworkSpecifier(name2))
.build()
val callback1 = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
val callback2 = TestableNetworkCallback(timeoutMs = DEFAULT_TIMEOUT_MS)
@@ -563,7 +561,8 @@
@Test
@IgnoreUpTo(Build.VERSION_CODES.R)
- fun testSetUnderlyingNetworks() {
+ fun testSetUnderlyingNetworksAndVpnSpecifier() {
+ val mySessionId = "MySession12345"
val request = NetworkRequest.Builder()
.addTransportType(TRANSPORT_TEST)
.addTransportType(TRANSPORT_VPN)
@@ -577,6 +576,10 @@
addTransportType(TRANSPORT_TEST)
addTransportType(TRANSPORT_VPN)
removeCapability(NET_CAPABILITY_NOT_VPN)
+ setTransportInfo(VpnTransportInfo(VpnManager.TYPE_VPN_SERVICE, mySessionId))
+ if (SdkLevel.isAtLeastS()) {
+ addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+ }
}
val defaultNetwork = mCM.activeNetwork
assertNotNull(defaultNetwork)
@@ -591,6 +594,10 @@
// Check that the default network's transport is propagated to the VPN.
var vpnNc = mCM.getNetworkCapabilities(agent.network)
assertNotNull(vpnNc)
+ assertEquals(VpnManager.TYPE_VPN_SERVICE,
+ (vpnNc.transportInfo as VpnTransportInfo).type)
+ // TODO: b/183938194 please fix the issue and enable following check.
+ // assertEquals(mySessionId, (vpnNc.transportInfo as VpnTransportInfo).sessionId)
val testAndVpn = intArrayOf(TRANSPORT_TEST, TRANSPORT_VPN)
assertTrue(hasAllTransports(vpnNc, testAndVpn))
@@ -638,12 +645,13 @@
val mockContext = mock(Context::class.java)
val mockCm = mock(ConnectivityManager::class.java)
doReturn(mockCm).`when`(mockContext).getSystemService(Context.CONNECTIVITY_SERVICE)
- createConnectedNetworkAgent(mockContext)
- verify(mockCm).registerNetworkAgent(any(Messenger::class.java),
+ val agent = createNetworkAgent(mockContext)
+ agent.register()
+ verify(mockCm).registerNetworkAgent(any(),
argThat<NetworkInfo> { it.detailedState == NetworkInfo.DetailedState.CONNECTING },
any(LinkProperties::class.java),
any(NetworkCapabilities::class.java),
- anyInt() /* score */,
+ any(NetworkScore::class.java),
any(NetworkAgentConfig::class.java),
eq(NetworkProvider.ID_NONE))
}
@@ -651,7 +659,7 @@
@Test
fun testSetAcceptUnvalidated() {
createNetworkAgentWithFakeCS().let { agent ->
- mFakeConnectivityService.sendMessage(CMD_SAVE_ACCEPT_UNVALIDATED, 1)
+ mFakeConnectivityService.agent.onSaveAcceptUnvalidated(true)
agent.expectCallback<OnSaveAcceptUnvalidated>().let {
assertTrue(it.accept)
}
@@ -662,19 +670,18 @@
@Test
fun testSetAcceptUnvalidatedPreventAutomaticReconnect() {
createNetworkAgentWithFakeCS().let { agent ->
- mFakeConnectivityService.sendMessage(CMD_SAVE_ACCEPT_UNVALIDATED, 0)
- mFakeConnectivityService.sendMessage(CMD_PREVENT_AUTOMATIC_RECONNECT)
+ mFakeConnectivityService.agent.onSaveAcceptUnvalidated(false)
+ mFakeConnectivityService.agent.onPreventAutomaticReconnect()
agent.expectCallback<OnSaveAcceptUnvalidated>().let {
assertFalse(it.accept)
}
agent.expectCallback<OnAutomaticReconnectDisabled>()
agent.assertNoCallback()
// When automatic reconnect is turned off, the network is torn down and
- // ConnectivityService sends a disconnect. This in turn causes the agent
- // to send a DISCONNECTED message to CS.
- mFakeConnectivityService.willExpectDisconnectOnce()
+ // ConnectivityService disconnects. As part of the disconnect, ConnectivityService will
+ // also send itself a message to unregister the NetworkAgent from its internal
+ // structure.
mFakeConnectivityService.disconnect()
- mFakeConnectivityService.expectMessage(AsyncChannel.CMD_CHANNEL_DISCONNECTED)
agent.expectCallback<OnNetworkUnwanted>()
}
}
@@ -682,12 +689,10 @@
@Test
fun testPreventAutomaticReconnect() {
createNetworkAgentWithFakeCS().let { agent ->
- mFakeConnectivityService.sendMessage(CMD_PREVENT_AUTOMATIC_RECONNECT)
+ mFakeConnectivityService.agent.onPreventAutomaticReconnect()
agent.expectCallback<OnAutomaticReconnectDisabled>()
agent.assertNoCallback()
- mFakeConnectivityService.willExpectDisconnectOnce()
mFakeConnectivityService.disconnect()
- mFakeConnectivityService.expectMessage(AsyncChannel.CMD_CHANNEL_DISCONNECTED)
agent.expectCallback<OnNetworkUnwanted>()
}
}
@@ -695,18 +700,14 @@
@Test
fun testValidationStatus() = createNetworkAgentWithFakeCS().let { agent ->
val uri = Uri.parse("http://www.google.com")
- val bundle = Bundle().apply {
- putString(NetworkAgent.REDIRECT_URL_KEY, uri.toString())
- }
- mFakeConnectivityService.sendMessage(CMD_REPORT_NETWORK_STATUS,
- arg1 = VALID_NETWORK, obj = bundle)
+ mFakeConnectivityService.agent.onValidationStatusChanged(VALID_NETWORK,
+ uri.toString())
agent.expectCallback<OnValidationStatus>().let {
assertEquals(it.status, VALID_NETWORK)
assertEquals(it.uri, uri)
}
- mFakeConnectivityService.sendMessage(CMD_REPORT_NETWORK_STATUS,
- arg1 = INVALID_NETWORK, obj = Bundle())
+ mFakeConnectivityService.agent.onValidationStatusChanged(INVALID_NETWORK, null)
agent.expectCallback<OnValidationStatus>().let {
assertEquals(it.status, INVALID_NETWORK)
assertNull(it.uri)
diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
index d118c8a..9906c30 100644
--- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
+++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java
@@ -16,8 +16,12 @@
package android.net.cts;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_FOTA;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_SUPL;
import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
@@ -29,20 +33,26 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import android.annotation.NonNull;
import android.net.MacAddress;
import android.net.MatchAllNetworkSpecifier;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.NetworkSpecifier;
-import android.net.UidRange;
import android.net.wifi.WifiNetworkSpecifier;
import android.os.Build;
import android.os.PatternMatcher;
import android.os.Process;
import android.util.ArraySet;
+import android.util.Range;
import androidx.test.runner.AndroidJUnit4;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.networkstack.apishim.ConstantsShim;
+import com.android.networkstack.apishim.NetworkRequestShimImpl;
+import com.android.networkstack.apishim.common.NetworkRequestShim;
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -50,6 +60,8 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Set;
+
@RunWith(AndroidJUnit4.class)
public class NetworkRequestTest {
@Rule
@@ -152,29 +164,44 @@
.getRequestorPackageName());
}
+ private void addNotVcnManagedCapability(@NonNull NetworkCapabilities nc) {
+ if (SdkLevel.isAtLeastS()) {
+ nc.addCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED);
+ }
+ }
+
@Test
@IgnoreUpTo(Build.VERSION_CODES.Q)
public void testCanBeSatisfiedBy() {
final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */);
final LocalNetworkSpecifier specifier2 = new LocalNetworkSpecifier(5678 /* id */);
+ // Some requests are adding NOT_VCN_MANAGED capability automatically. Add it to the
+ // capabilities below for bypassing the check.
final NetworkCapabilities capCellularMmsInternet = new NetworkCapabilities()
.addTransportType(TRANSPORT_CELLULAR)
.addCapability(NET_CAPABILITY_MMS)
.addCapability(NET_CAPABILITY_INTERNET);
+ addNotVcnManagedCapability(capCellularMmsInternet);
final NetworkCapabilities capCellularVpnMmsInternet =
new NetworkCapabilities(capCellularMmsInternet).addTransportType(TRANSPORT_VPN);
+ addNotVcnManagedCapability(capCellularVpnMmsInternet);
final NetworkCapabilities capCellularMmsInternetSpecifier1 =
new NetworkCapabilities(capCellularMmsInternet).setNetworkSpecifier(specifier1);
+ addNotVcnManagedCapability(capCellularMmsInternetSpecifier1);
final NetworkCapabilities capVpnInternetSpecifier1 = new NetworkCapabilities()
.addCapability(NET_CAPABILITY_INTERNET)
.addTransportType(TRANSPORT_VPN)
.setNetworkSpecifier(specifier1);
+ addNotVcnManagedCapability(capVpnInternetSpecifier1);
final NetworkCapabilities capCellularMmsInternetMatchallspecifier =
new NetworkCapabilities(capCellularMmsInternet)
- .setNetworkSpecifier(new MatchAllNetworkSpecifier());
+ .setNetworkSpecifier(new MatchAllNetworkSpecifier());
+ addNotVcnManagedCapability(capCellularMmsInternetMatchallspecifier);
final NetworkCapabilities capCellularMmsInternetSpecifier2 =
- new NetworkCapabilities(capCellularMmsInternet).setNetworkSpecifier(specifier2);
+ new NetworkCapabilities(capCellularMmsInternet)
+ .setNetworkSpecifier(specifier2);
+ addNotVcnManagedCapability(capCellularMmsInternetSpecifier2);
final NetworkRequest requestCellularInternetSpecifier1 = new NetworkRequest.Builder()
.addTransportType(TRANSPORT_CELLULAR)
@@ -203,6 +230,14 @@
assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularVpnMmsInternet));
}
+ private void setUids(NetworkRequest.Builder builder, Set<Range<Integer>> ranges)
+ throws UnsupportedApiLevelException {
+ if (SdkLevel.isAtLeastS()) {
+ final NetworkRequestShim networkRequestShim = NetworkRequestShimImpl.newInstance();
+ networkRequestShim.setUids(builder, ranges);
+ }
+ }
+
@Test
@IgnoreUpTo(Build.VERSION_CODES.Q)
public void testInvariantInCanBeSatisfiedBy() {
@@ -210,15 +245,26 @@
// NetworkCapabilities.satisfiedByNetworkCapabilities().
final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */);
final int uid = Process.myUid();
- final ArraySet<UidRange> ranges = new ArraySet<>();
- ranges.add(new UidRange(uid, uid));
- final NetworkRequest requestCombination = new NetworkRequest.Builder()
+ final NetworkRequest.Builder nrBuilder = new NetworkRequest.Builder()
.addTransportType(TRANSPORT_CELLULAR)
.addCapability(NET_CAPABILITY_INTERNET)
.setLinkUpstreamBandwidthKbps(1000)
.setNetworkSpecifier(specifier1)
- .setSignalStrength(-123)
- .setUids(ranges).build();
+ .setSignalStrength(-123);
+
+ // The uid ranges should be set into the request, but setUids() takes a set of UidRange
+ // that is hidden and inaccessible from shims. Before, S setUids will be a no-op. But
+ // because NetworkRequest.Builder sets the UID of the request to the current UID, the
+ // request contains the current UID both on S and before S.
+ final Set<Range<Integer>> ranges = new ArraySet<>();
+ ranges.add(new Range<Integer>(uid, uid));
+ try {
+ setUids(nrBuilder, ranges);
+ } catch (UnsupportedApiLevelException e) {
+ // Not supported before API31.
+ }
+ final NetworkRequest requestCombination = nrBuilder.build();
+
final NetworkCapabilities capCell = new NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_CELLULAR).build();
assertCorrectlySatisfies(false, requestCombination, capCell);
@@ -239,7 +285,8 @@
final NetworkCapabilities capCellInternetBWSpecifier1Signal =
new NetworkCapabilities.Builder(capCellInternetBWSpecifier1)
- .setSignalStrength(-123).build();
+ .setSignalStrength(-123).build();
+ addNotVcnManagedCapability(capCellInternetBWSpecifier1Signal);
assertCorrectlySatisfies(true, requestCombination,
capCellInternetBWSpecifier1Signal);
@@ -273,4 +320,80 @@
assertEquals(Process.INVALID_UID, new NetworkRequest.Builder()
.clearCapabilities().build().getRequestorUid());
}
+
+ // TODO: 1. Refactor test cases with helper method.
+ // 2. Test capability that does not yet exist.
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBypassingVcnForNonInternetRequest() {
+ // Make an empty request. Verify the NOT_VCN_MANAGED is added.
+ final NetworkRequest emptyRequest = new NetworkRequest.Builder().build();
+ assertTrue(emptyRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make a request explicitly add NOT_VCN_MANAGED. Verify the NOT_VCN_MANAGED is preserved.
+ final NetworkRequest mmsAddNotVcnRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_MMS)
+ .addCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build();
+ assertTrue(mmsAddNotVcnRequest.hasCapability(
+ ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Similar to above, but the opposite order.
+ final NetworkRequest mmsAddNotVcnRequest2 = new NetworkRequest.Builder()
+ .addCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED)
+ .addCapability(NET_CAPABILITY_MMS)
+ .build();
+ assertTrue(mmsAddNotVcnRequest2.hasCapability(
+ ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make a request explicitly remove NOT_VCN_MANAGED. Verify the NOT_VCN_MANAGED is removed.
+ final NetworkRequest removeNotVcnRequest = new NetworkRequest.Builder()
+ .removeCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED).build();
+ assertFalse(removeNotVcnRequest.hasCapability(
+ ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make a request add some capability inside VCN supported capabilities.
+ // Verify the NOT_VCN_MANAGED is added.
+ final NetworkRequest notRoamRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_NOT_ROAMING).build();
+ assertTrue(notRoamRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make a internet request. Verify the NOT_VCN_MANAGED is added.
+ final NetworkRequest internetRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET).build();
+ assertTrue(internetRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make a internet request which explicitly removed NOT_VCN_MANAGED.
+ // Verify the NOT_VCN_MANAGED is removed.
+ final NetworkRequest internetRemoveNotVcnRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .removeCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED).build();
+ assertFalse(internetRemoveNotVcnRequest.hasCapability(
+ ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make a normal MMS request. Verify the request could bypass VCN.
+ final NetworkRequest mmsRequest =
+ new NetworkRequest.Builder().addCapability(NET_CAPABILITY_MMS).build();
+ assertFalse(mmsRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make a SUPL request along with internet. Verify NOT_VCN_MANAGED is not added since
+ // SUPL is not in the supported list.
+ final NetworkRequest suplWithInternetRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_SUPL)
+ .addCapability(NET_CAPABILITY_INTERNET).build();
+ assertFalse(suplWithInternetRequest.hasCapability(
+ ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make a FOTA request with explicitly add NOT_VCN_MANAGED capability. Verify
+ // NOT_VCN_MANAGED is preserved.
+ final NetworkRequest fotaRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_FOTA)
+ .addCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED).build();
+ assertTrue(fotaRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+
+ // Make a DUN request, which is in {@code VCN_SUPPORTED_CAPABILITIES}.
+ // Verify NOT_VCN_MANAGED is preserved.
+ final NetworkRequest dunRequest = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_DUN).build();
+ assertTrue(dunRequest.hasCapability(ConstantsShim.NET_CAPABILITY_NOT_VCN_MANAGED));
+ }
}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.java b/tests/cts/net/src/android/net/cts/NsdManagerTest.java
new file mode 100644
index 0000000..2bcfdc3
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.java
@@ -0,0 +1,594 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts;
+
+import android.content.Context;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.platform.test.annotations.AppModeFull;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.util.Arrays;
+import java.util.Random;
+import java.util.List;
+import java.util.ArrayList;
+
+@AppModeFull(reason = "Socket cannot bind in instant app mode")
+public class NsdManagerTest extends AndroidTestCase {
+
+ private static final String TAG = "NsdManagerTest";
+ private static final String SERVICE_TYPE = "_nmt._tcp";
+ private static final int TIMEOUT = 2000;
+
+ private static final boolean DBG = false;
+
+ NsdManager mNsdManager;
+
+ NsdManager.RegistrationListener mRegistrationListener;
+ NsdManager.DiscoveryListener mDiscoveryListener;
+ NsdManager.ResolveListener mResolveListener;
+ private NsdServiceInfo mResolvedService;
+
+ public NsdManagerTest() {
+ initRegistrationListener();
+ initDiscoveryListener();
+ initResolveListener();
+ }
+
+ private void initRegistrationListener() {
+ mRegistrationListener = new NsdManager.RegistrationListener() {
+ @Override
+ public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ setEvent("onRegistrationFailed", errorCode);
+ }
+
+ @Override
+ public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ setEvent("onUnregistrationFailed", errorCode);
+ }
+
+ @Override
+ public void onServiceRegistered(NsdServiceInfo serviceInfo) {
+ setEvent("onServiceRegistered", serviceInfo);
+ }
+
+ @Override
+ public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
+ setEvent("onServiceUnregistered", serviceInfo);
+ }
+ };
+ }
+
+ private void initDiscoveryListener() {
+ mDiscoveryListener = new NsdManager.DiscoveryListener() {
+ @Override
+ public void onStartDiscoveryFailed(String serviceType, int errorCode) {
+ setEvent("onStartDiscoveryFailed", errorCode);
+ }
+
+ @Override
+ public void onStopDiscoveryFailed(String serviceType, int errorCode) {
+ setEvent("onStopDiscoveryFailed", errorCode);
+ }
+
+ @Override
+ public void onDiscoveryStarted(String serviceType) {
+ NsdServiceInfo info = new NsdServiceInfo();
+ info.setServiceType(serviceType);
+ setEvent("onDiscoveryStarted", info);
+ }
+
+ @Override
+ public void onDiscoveryStopped(String serviceType) {
+ NsdServiceInfo info = new NsdServiceInfo();
+ info.setServiceType(serviceType);
+ setEvent("onDiscoveryStopped", info);
+ }
+
+ @Override
+ public void onServiceFound(NsdServiceInfo serviceInfo) {
+ setEvent("onServiceFound", serviceInfo);
+ }
+
+ @Override
+ public void onServiceLost(NsdServiceInfo serviceInfo) {
+ setEvent("onServiceLost", serviceInfo);
+ }
+ };
+ }
+
+ private void initResolveListener() {
+ mResolveListener = new NsdManager.ResolveListener() {
+ @Override
+ public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ setEvent("onResolveFailed", errorCode);
+ }
+
+ @Override
+ public void onServiceResolved(NsdServiceInfo serviceInfo) {
+ mResolvedService = serviceInfo;
+ setEvent("onServiceResolved", serviceInfo);
+ }
+ };
+ }
+
+
+
+ private final class EventData {
+ EventData(String callbackName, NsdServiceInfo info) {
+ mCallbackName = callbackName;
+ mSucceeded = true;
+ mErrorCode = 0;
+ mInfo = info;
+ }
+ EventData(String callbackName, int errorCode) {
+ mCallbackName = callbackName;
+ mSucceeded = false;
+ mErrorCode = errorCode;
+ mInfo = null;
+ }
+ private final String mCallbackName;
+ private final boolean mSucceeded;
+ private final int mErrorCode;
+ private final NsdServiceInfo mInfo;
+ }
+
+ private final List<EventData> mEventCache = new ArrayList<EventData>();
+
+ private void setEvent(String callbackName, int errorCode) {
+ if (DBG) Log.d(TAG, callbackName + " failed with " + String.valueOf(errorCode));
+ EventData eventData = new EventData(callbackName, errorCode);
+ synchronized (mEventCache) {
+ mEventCache.add(eventData);
+ mEventCache.notify();
+ }
+ }
+
+ private void setEvent(String callbackName, NsdServiceInfo info) {
+ if (DBG) Log.d(TAG, "Received event " + callbackName + " for " + info.getServiceName());
+ EventData eventData = new EventData(callbackName, info);
+ synchronized (mEventCache) {
+ mEventCache.add(eventData);
+ mEventCache.notify();
+ }
+ }
+
+ void clearEventCache() {
+ synchronized(mEventCache) {
+ mEventCache.clear();
+ }
+ }
+
+ int eventCacheSize() {
+ synchronized(mEventCache) {
+ return mEventCache.size();
+ }
+ }
+
+ private int mWaitId = 0;
+ private EventData waitForCallback(String callbackName) {
+
+ synchronized(mEventCache) {
+
+ mWaitId ++;
+ if (DBG) Log.d(TAG, "Waiting for " + callbackName + ", id=" + String.valueOf(mWaitId));
+
+ try {
+ long startTime = android.os.SystemClock.uptimeMillis();
+ long elapsedTime = 0;
+ int index = 0;
+ while (elapsedTime < TIMEOUT ) {
+ // first check if we've received that event
+ for (; index < mEventCache.size(); index++) {
+ EventData e = mEventCache.get(index);
+ if (e.mCallbackName.equals(callbackName)) {
+ if (DBG) Log.d(TAG, "exiting wait id=" + String.valueOf(mWaitId));
+ return e;
+ }
+ }
+
+ // Not yet received, just wait
+ mEventCache.wait(TIMEOUT - elapsedTime);
+ elapsedTime = android.os.SystemClock.uptimeMillis() - startTime;
+ }
+ // we exited the loop because of TIMEOUT; fail the call
+ if (DBG) Log.d(TAG, "timed out waiting id=" + String.valueOf(mWaitId));
+ return null;
+ } catch (InterruptedException e) {
+ return null; // wait timed out!
+ }
+ }
+ }
+
+ private EventData waitForNewEvents() throws InterruptedException {
+ if (DBG) Log.d(TAG, "Waiting for a bit, id=" + String.valueOf(mWaitId));
+
+ long startTime = android.os.SystemClock.uptimeMillis();
+ long elapsedTime = 0;
+ synchronized (mEventCache) {
+ int index = mEventCache.size();
+ while (elapsedTime < TIMEOUT ) {
+ // first check if we've received that event
+ for (; index < mEventCache.size(); index++) {
+ EventData e = mEventCache.get(index);
+ return e;
+ }
+
+ // Not yet received, just wait
+ mEventCache.wait(TIMEOUT - elapsedTime);
+ elapsedTime = android.os.SystemClock.uptimeMillis() - startTime;
+ }
+ }
+
+ return null;
+ }
+
+ private String mServiceName;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ if (DBG) Log.d(TAG, "Setup test ...");
+ mNsdManager = (NsdManager) getContext().getSystemService(Context.NSD_SERVICE);
+
+ Random rand = new Random();
+ mServiceName = new String("NsdTest");
+ for (int i = 0; i < 4; i++) {
+ mServiceName = mServiceName + String.valueOf(rand.nextInt(10));
+ }
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ if (DBG) Log.d(TAG, "Tear down test ...");
+ super.tearDown();
+ }
+
+ public void testNDSManager() throws Exception {
+ EventData lastEvent = null;
+
+ if (DBG) Log.d(TAG, "Starting test ...");
+
+ NsdServiceInfo si = new NsdServiceInfo();
+ si.setServiceType(SERVICE_TYPE);
+ si.setServiceName(mServiceName);
+
+ byte testByteArray[] = new byte[] {-128, 127, 2, 1, 0, 1, 2};
+ String String256 = "1_________2_________3_________4_________5_________6_________" +
+ "7_________8_________9_________10________11________12________13________" +
+ "14________15________16________17________18________19________20________" +
+ "21________22________23________24________25________123456";
+
+ // Illegal attributes
+ try {
+ si.setAttribute(null, (String) null);
+ fail("Could set null key");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ si.setAttribute("", (String) null);
+ fail("Could set empty key");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ si.setAttribute(String256, (String) null);
+ fail("Could set key with 255 characters");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ si.setAttribute("key", String256.substring(3));
+ fail("Could set key+value combination with more than 255 characters");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ si.setAttribute("key", String256.substring(4));
+ fail("Could set key+value combination with 255 characters");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ si.setAttribute(new String(new byte[]{0x19}), (String) null);
+ fail("Could set key with invalid character");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ si.setAttribute("=", (String) null);
+ fail("Could set key with invalid character");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ try {
+ si.setAttribute(new String(new byte[]{0x7F}), (String) null);
+ fail("Could set key with invalid character");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+
+ // Allowed attributes
+ si.setAttribute("booleanAttr", (String) null);
+ si.setAttribute("keyValueAttr", "value");
+ si.setAttribute("keyEqualsAttr", "=");
+ si.setAttribute(" whiteSpaceKeyValueAttr ", " value ");
+ si.setAttribute("binaryDataAttr", testByteArray);
+ si.setAttribute("nullBinaryDataAttr", (byte[]) null);
+ si.setAttribute("emptyBinaryDataAttr", new byte[]{});
+ si.setAttribute("longkey", String256.substring(9));
+
+ ServerSocket socket;
+ int localPort;
+
+ try {
+ socket = new ServerSocket(0);
+ localPort = socket.getLocalPort();
+ si.setPort(localPort);
+ } catch (IOException e) {
+ if (DBG) Log.d(TAG, "Could not open a local socket");
+ assertTrue(false);
+ return;
+ }
+
+ if (DBG) Log.d(TAG, "Port = " + String.valueOf(localPort));
+
+ clearEventCache();
+
+ mNsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
+ lastEvent = waitForCallback("onServiceRegistered"); // id = 1
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mSucceeded);
+ assertTrue(eventCacheSize() == 1);
+
+ // We may not always get the name that we tried to register;
+ // This events tells us the name that was registered.
+ String registeredName = lastEvent.mInfo.getServiceName();
+ si.setServiceName(registeredName);
+
+ clearEventCache();
+
+ mNsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+ mDiscoveryListener);
+
+ // Expect discovery started
+ lastEvent = waitForCallback("onDiscoveryStarted"); // id = 2
+
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mSucceeded);
+
+ // Remove this event, so accounting becomes easier later
+ synchronized (mEventCache) {
+ mEventCache.remove(lastEvent);
+ }
+
+ // Expect a service record to be discovered (and filter the ones
+ // that are unrelated to this test)
+ boolean found = false;
+ for (int i = 0; i < 32; i++) {
+
+ lastEvent = waitForCallback("onServiceFound"); // id = 3
+ if (lastEvent == null) {
+ // no more onServiceFound events are being reported!
+ break;
+ }
+
+ assertTrue(lastEvent.mSucceeded);
+
+ if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
+ lastEvent.mInfo.getServiceName());
+
+ if (lastEvent.mInfo.getServiceName().equals(registeredName)) {
+ // Save it, as it will get overwritten with new serviceFound events
+ si = lastEvent.mInfo;
+ found = true;
+ }
+
+ // Remove this event from the event cache, so it won't be found by subsequent
+ // calls to waitForCallback
+ synchronized (mEventCache) {
+ mEventCache.remove(lastEvent);
+ }
+ }
+
+ assertTrue(found);
+
+ // We've removed all serviceFound events, and we've removed the discoveryStarted
+ // event as well, so now the event cache should be empty!
+ assertTrue(eventCacheSize() == 0);
+
+ // Resolve the service
+ clearEventCache();
+ mNsdManager.resolveService(si, mResolveListener);
+ lastEvent = waitForCallback("onServiceResolved"); // id = 4
+
+ assertNotNull(mResolvedService);
+
+ // Check Txt attributes
+ assertEquals(8, mResolvedService.getAttributes().size());
+ assertTrue(mResolvedService.getAttributes().containsKey("booleanAttr"));
+ assertNull(mResolvedService.getAttributes().get("booleanAttr"));
+ assertEquals("value", new String(mResolvedService.getAttributes().get("keyValueAttr")));
+ assertEquals("=", new String(mResolvedService.getAttributes().get("keyEqualsAttr")));
+ assertEquals(" value ", new String(mResolvedService.getAttributes()
+ .get(" whiteSpaceKeyValueAttr ")));
+ assertEquals(String256.substring(9), new String(mResolvedService.getAttributes()
+ .get("longkey")));
+ assertTrue(Arrays.equals(testByteArray,
+ mResolvedService.getAttributes().get("binaryDataAttr")));
+ assertTrue(mResolvedService.getAttributes().containsKey("nullBinaryDataAttr"));
+ assertNull(mResolvedService.getAttributes().get("nullBinaryDataAttr"));
+ assertTrue(mResolvedService.getAttributes().containsKey("emptyBinaryDataAttr"));
+ assertNull(mResolvedService.getAttributes().get("emptyBinaryDataAttr"));
+
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mSucceeded);
+
+ if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": Port = " +
+ String.valueOf(lastEvent.mInfo.getPort()));
+
+ assertTrue(lastEvent.mInfo.getPort() == localPort);
+ assertTrue(eventCacheSize() == 1);
+
+ checkForAdditionalEvents();
+ clearEventCache();
+
+ // Unregister the service
+ mNsdManager.unregisterService(mRegistrationListener);
+ lastEvent = waitForCallback("onServiceUnregistered"); // id = 5
+
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mSucceeded);
+
+ // Expect a callback for service lost
+ lastEvent = waitForCallback("onServiceLost"); // id = 6
+
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mInfo.getServiceName().equals(registeredName));
+
+ // Register service again to see if we discover it
+ checkForAdditionalEvents();
+ clearEventCache();
+
+ si = new NsdServiceInfo();
+ si.setServiceType(SERVICE_TYPE);
+ si.setServiceName(mServiceName);
+ si.setPort(localPort);
+
+ // Create a new registration listener and register same service again
+ initRegistrationListener();
+
+ mNsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
+
+ lastEvent = waitForCallback("onServiceRegistered"); // id = 7
+
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mSucceeded);
+
+ registeredName = lastEvent.mInfo.getServiceName();
+
+ // Expect a record to be discovered
+ // Expect a service record to be discovered (and filter the ones
+ // that are unrelated to this test)
+ found = false;
+ for (int i = 0; i < 32; i++) {
+
+ lastEvent = waitForCallback("onServiceFound"); // id = 8
+ if (lastEvent == null) {
+ // no more onServiceFound events are being reported!
+ break;
+ }
+
+ assertTrue(lastEvent.mSucceeded);
+
+ if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
+ lastEvent.mInfo.getServiceName());
+
+ if (lastEvent.mInfo.getServiceName().equals(registeredName)) {
+ // Save it, as it will get overwritten with new serviceFound events
+ si = lastEvent.mInfo;
+ found = true;
+ }
+
+ // Remove this event from the event cache, so it won't be found by subsequent
+ // calls to waitForCallback
+ synchronized (mEventCache) {
+ mEventCache.remove(lastEvent);
+ }
+ }
+
+ assertTrue(found);
+
+ // Resolve the service
+ clearEventCache();
+ mNsdManager.resolveService(si, mResolveListener);
+ lastEvent = waitForCallback("onServiceResolved"); // id = 9
+
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mSucceeded);
+
+ if (DBG) Log.d(TAG, "id = " + String.valueOf(mWaitId) + ": ServiceName = " +
+ lastEvent.mInfo.getServiceName());
+
+ assertTrue(lastEvent.mInfo.getServiceName().equals(registeredName));
+
+ assertNotNull(mResolvedService);
+
+ // Check that we don't have any TXT records
+ assertEquals(0, mResolvedService.getAttributes().size());
+
+ checkForAdditionalEvents();
+ clearEventCache();
+
+ mNsdManager.stopServiceDiscovery(mDiscoveryListener);
+ lastEvent = waitForCallback("onDiscoveryStopped"); // id = 10
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mSucceeded);
+ assertTrue(checkCacheSize(1));
+
+ checkForAdditionalEvents();
+ clearEventCache();
+
+ mNsdManager.unregisterService(mRegistrationListener);
+
+ lastEvent = waitForCallback("onServiceUnregistered"); // id = 11
+ assertTrue(lastEvent != null);
+ assertTrue(lastEvent.mSucceeded);
+ assertTrue(checkCacheSize(1));
+ }
+
+ boolean checkCacheSize(int size) {
+ synchronized (mEventCache) {
+ int cacheSize = mEventCache.size();
+ if (cacheSize != size) {
+ Log.d(TAG, "id = " + mWaitId + ": event cache size = " + cacheSize);
+ for (int i = 0; i < cacheSize; i++) {
+ EventData e = mEventCache.get(i);
+ String sname = (e.mInfo != null) ? "(" + e.mInfo.getServiceName() + ")" : "";
+ Log.d(TAG, "eventName is " + e.mCallbackName + sname);
+ }
+ }
+ return (cacheSize == size);
+ }
+ }
+
+ boolean checkForAdditionalEvents() {
+ try {
+ EventData e = waitForNewEvents();
+ if (e != null) {
+ String sname = (e.mInfo != null) ? "(" + e.mInfo.getServiceName() + ")" : "";
+ Log.d(TAG, "ignoring unexpected event " + e.mCallbackName + sname);
+ }
+ return (e == null);
+ }
+ catch (InterruptedException ex) {
+ return false;
+ }
+ }
+}
+
diff --git a/tests/cts/net/src/android/net/cts/VpnServiceTest.java b/tests/cts/net/src/android/net/cts/VpnServiceTest.java
index 15af23c..5c7b5ca 100644
--- a/tests/cts/net/src/android/net/cts/VpnServiceTest.java
+++ b/tests/cts/net/src/android/net/cts/VpnServiceTest.java
@@ -47,6 +47,7 @@
assertEquals(1, count);
}
+ @AppModeFull(reason = "establish() requires prepare(), which requires PackageManager access")
public void testEstablish() throws Exception {
ParcelFileDescriptor descriptor = null;
try {
@@ -62,7 +63,7 @@
}
}
- @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ @AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager")
public void testProtect_DatagramSocket() throws Exception {
DatagramSocket socket = new DatagramSocket();
try {
@@ -77,6 +78,7 @@
}
}
+ @AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager")
public void testProtect_Socket() throws Exception {
Socket socket = new Socket();
try {
@@ -91,7 +93,7 @@
}
}
- @AppModeFull(reason = "Socket cannot bind in instant app mode")
+ @AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager")
public void testProtect_int() throws Exception {
DatagramSocket socket = new DatagramSocket();
ParcelFileDescriptor descriptor = ParcelFileDescriptor.fromDatagramSocket(socket);
diff --git a/tests/cts/net/util/Android.bp b/tests/cts/net/util/Android.bp
index c36d976..88a2068 100644
--- a/tests/cts/net/util/Android.bp
+++ b/tests/cts/net/util/Android.bp
@@ -15,6 +15,10 @@
//
// Common utilities for cts net tests.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
java_library {
name: "cts-net-utils",
srcs: ["java/**/*.java", "java/**/*.kt"],
@@ -23,4 +27,4 @@
"junit",
"net-tests-utils",
],
-}
\ No newline at end of file
+}
diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
index 0527011..88172d7 100644
--- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
+++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java
@@ -212,7 +212,7 @@
mContext.registerReceiver(receiver, filter);
boolean connected = false;
- final String err = "Wifi must be configured to connect to an access point for this test.";
+ final String err = "Wifi must be configured to connect to an access point for this test";
try {
clearWifiBlacklist();
SystemUtil.runShellCommand("svc wifi enable");
@@ -235,7 +235,7 @@
}
// Ensure we get an onAvailable callback and possibly a CONNECTIVITY_ACTION.
wifiNetwork = callback.waitForAvailable();
- assertNotNull(err, wifiNetwork);
+ assertNotNull(err + ": onAvailable callback not received", wifiNetwork);
connected = !expectLegacyBroadcast || receiver.waitForState();
} catch (InterruptedException ex) {
fail("connectToWifi was interrupted");
@@ -244,7 +244,7 @@
mContext.unregisterReceiver(receiver);
}
- assertTrue(err, connected);
+ assertTrue(err + ": CONNECTIVITY_ACTION not received", connected);
return wifiNetwork;
}
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index b1d4a60..fa52e9b 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
android_test {
name: "CtsTetheringTest",
defaults: ["cts_defaults"],
@@ -48,7 +52,7 @@
test_suites: [
"cts",
"general-tests",
- "mts",
+ "mts-tethering",
],
// Include both the 32 and 64 bit versions