Merge remote-tracking branch 'goog/rvc-qpr-dev-plus-aosp' into merge-sc-dev-plus-aosp-then-rvc-qpr-dev-plus-aosp
This CL merges rvc-qpr-dev-plus-aosp on top of sc-dev-plus-aosp on top of mainline-prod. It picks up changes that were merged into aosp/master before sc-dev-plus-aosp was on the path between AOSP and master. One such very simple CL is aosp/1554765. There were no merge conflicts. Current diffstat with aosp/master is: 97 files changed, 2219 insertions(+), 84 deletions(-) This includes 1800 lines of translations added in 72 files like Tethering/res/values-*/strings.xml Bug: 167645754 Test: no merge conflicts Test: didn't even try to build Ignore-AOSP-First: this is a merge from AOSP Change-Id: I63af0b95e2d0e6bddc217c29014c03ea0dbda6ec Merged-In: Ib0ac49609e444a53a6fee4575f5078e15f364eef
This commit is contained in:
1
OWNERS
1
OWNERS
@@ -2,5 +2,6 @@ codewiz@google.com
|
||||
jchalard@google.com
|
||||
junyulai@google.com
|
||||
lorenzo@google.com
|
||||
maze@google.com
|
||||
reminv@google.com
|
||||
satk@google.com
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
package {
|
||||
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||
}
|
||||
|
||||
java_defaults {
|
||||
name: "TetheringAndroidLibraryDefaults",
|
||||
sdk_version: "module_current",
|
||||
@@ -29,8 +33,7 @@ java_defaults {
|
||||
"androidx.annotation_annotation",
|
||||
"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",
|
||||
@@ -62,7 +65,10 @@ cc_library {
|
||||
"com.android.tethering",
|
||||
],
|
||||
min_sdk_version: "30",
|
||||
header_libs: ["bpf_syscall_wrappers"],
|
||||
header_libs: [
|
||||
"bpf_syscall_wrappers",
|
||||
"bpf_tethering_headers",
|
||||
],
|
||||
srcs: [
|
||||
"jni/*.cpp",
|
||||
],
|
||||
@@ -142,3 +148,10 @@ android_app {
|
||||
apex_available: ["com.android.tethering"],
|
||||
min_sdk_version: "30",
|
||||
}
|
||||
|
||||
sdk {
|
||||
name: "tethering-module-sdk",
|
||||
java_sdk_libs: [
|
||||
"framework-tethering",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -14,11 +14,15 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
package {
|
||||
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||
}
|
||||
|
||||
apex {
|
||||
name: "com.android.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: true,
|
||||
updatable: false,
|
||||
// min_sdk_version: "30",
|
||||
java_libs: [
|
||||
"framework-tethering",
|
||||
@@ -27,7 +31,10 @@ apex {
|
||||
jni_libs: [
|
||||
"libservice-connectivity",
|
||||
],
|
||||
bpfs: ["offload.o"],
|
||||
bpfs: [
|
||||
"offload.o",
|
||||
"test.o",
|
||||
],
|
||||
apps: ["Tethering"],
|
||||
manifest: "manifest.json",
|
||||
key: "com.android.tethering.key",
|
||||
|
||||
@@ -17,14 +17,21 @@
|
||||
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.
|
||||
@@ -60,6 +67,97 @@ public class BpfCoordinatorShimImpl
|
||||
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,
|
||||
MacAddress srcMac, MacAddress dstMac, int mtu) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean stopUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex) {
|
||||
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 String toString() {
|
||||
return "Netd used";
|
||||
|
||||
@@ -16,8 +16,14 @@
|
||||
|
||||
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.SparseArray;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -25,8 +31,17 @@ 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.TetherIngressKey;
|
||||
import com.android.networkstack.tethering.TetherIngressValue;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Bpf coordinator class for API shims.
|
||||
@@ -35,33 +50,63 @@ 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 of ingress queueing discipline which pre-processes the packets by the IPv6
|
||||
// forwarding rules.
|
||||
// BPF map for downstream IPv4 forwarding.
|
||||
@Nullable
|
||||
private final BpfMap<TetherIngressKey, TetherIngressValue> mBpfIngressMap;
|
||||
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;
|
||||
|
||||
public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) {
|
||||
mLog = deps.getSharedLog().forSubComponent(TAG);
|
||||
mBpfIngressMap = deps.getBpfIngressMap();
|
||||
mBpfDownstream4Map = deps.getBpfDownstream4Map();
|
||||
mBpfUpstream4Map = deps.getBpfUpstream4Map();
|
||||
mBpfDownstream6Map = deps.getBpfDownstream6Map();
|
||||
mBpfUpstream6Map = deps.getBpfUpstream6Map();
|
||||
mBpfStatsMap = deps.getBpfStatsMap();
|
||||
mBpfLimitMap = deps.getBpfLimitMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInitialized() {
|
||||
return mBpfIngressMap != null;
|
||||
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 TetherIngressKey key = rule.makeTetherIngressKey();
|
||||
final TetherIngressValue value = rule.makeTetherIngressValue();
|
||||
final TetherDownstream6Key key = rule.makeTetherDownstream6Key();
|
||||
final Tether6Value value = rule.makeTether6Value();
|
||||
|
||||
try {
|
||||
mBpfIngressMap.updateEntry(key, value);
|
||||
mBpfDownstream6Map.updateEntry(key, value);
|
||||
} catch (ErrnoException e) {
|
||||
mLog.e("Could not update entry: ", e);
|
||||
return false;
|
||||
@@ -70,10 +115,260 @@ public class BpfCoordinatorShimImpl
|
||||
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,
|
||||
MacAddress srcMac, MacAddress dstMac, int mtu) {
|
||||
if (!isInitialized()) return false;
|
||||
|
||||
final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfindex);
|
||||
final Tether6Value value = new Tether6Value(upstreamIfindex, srcMac,
|
||||
dstMac, 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) {
|
||||
if (!isInitialized()) return false;
|
||||
|
||||
final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfindex);
|
||||
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 {
|
||||
// The last used time field of the value is updated by the bpf program. Adding the same
|
||||
// map pair twice causes the unexpected refresh. Must be fixed before starting the
|
||||
// conntrack timeout extension implementation.
|
||||
// TODO: consider using insertEntry.
|
||||
if (downstream) {
|
||||
mBpfDownstream4Map.updateEntry(key, value);
|
||||
} else {
|
||||
mBpfUpstream4Map.updateEntry(key, value);
|
||||
}
|
||||
} catch (ErrnoException e) {
|
||||
mLog.e("Could not update entry: ", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key) {
|
||||
if (!isInitialized()) return false;
|
||||
|
||||
try {
|
||||
if (downstream) {
|
||||
mBpfDownstream4Map.deleteEntry(key);
|
||||
} 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;
|
||||
}
|
||||
|
||||
private String mapStatus(BpfMap m, String name) {
|
||||
return name + "{" + (m != null ? "OK" : "ERROR") + "}";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "mBpfIngressMap{"
|
||||
+ (mBpfIngressMap != null ? "initialized" : "not initialized") + "} "
|
||||
+ "}";
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,17 @@
|
||||
|
||||
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.
|
||||
@@ -54,5 +61,87 @@ public abstract class BpfCoordinatorShim {
|
||||
* @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 srcMac the source MAC address to use for packets
|
||||
* @oaram dstMac 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,
|
||||
MacAddress srcMac, MacAddress dstMac, int mtu);
|
||||
|
||||
/**
|
||||
* Stops IPv6 forwarding between the specified interfaces.
|
||||
|
||||
* @param downstreamIfindex the downstream interface index
|
||||
* @param upstreamIfindex the upstream interface index
|
||||
* @return true if operation succeeded or was a no-op, false otherwise
|
||||
*/
|
||||
public abstract boolean stopUpstreamIpv6Forwarding(int downstreamIfindex, int upstreamIfindex);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,30 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
//
|
||||
// struct definitions shared with JNI
|
||||
//
|
||||
package {
|
||||
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||
}
|
||||
|
||||
cc_library_headers {
|
||||
name: "bpf_tethering_headers",
|
||||
vendor_available: false,
|
||||
host_supported: false,
|
||||
export_include_dirs: ["."],
|
||||
cflags: [
|
||||
"-Wall",
|
||||
"-Werror",
|
||||
],
|
||||
sdk_version: "30",
|
||||
min_sdk_version: "30",
|
||||
apex_available: ["com.android.tethering"],
|
||||
visibility: [
|
||||
"//packages/modules/Connectivity/Tethering",
|
||||
],
|
||||
}
|
||||
|
||||
//
|
||||
// bpf kernel programs
|
||||
//
|
||||
@@ -31,3 +55,18 @@ bpf {
|
||||
"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
|
||||
"system/netd/libnetdbpf/include", // for bpf_shared.h
|
||||
"system/netd/libnetdutils/include", // for UidConstants.h
|
||||
],
|
||||
}
|
||||
|
||||
214
Tethering/bpf_progs/bpf_tethering.h
Normal file
214
Tethering/bpf_progs/bpf_tethering.h
Normal file
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* 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
|
||||
// TODO: extend this to include dstMac
|
||||
struct in6_addr neigh6; // The destination IPv6 address
|
||||
} TetherDownstream6Key;
|
||||
STRUCT_SIZE(TetherDownstream6Key, 4 + 16); // 20
|
||||
|
||||
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
|
||||
// TODO: extend this to include dstMac and src ip /64 subnet
|
||||
} TetherUpstream6Key;
|
||||
STRUCT_SIZE(TetherUpstream6Key, 4);
|
||||
|
||||
#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
|
||||
@@ -20,31 +20,102 @@
|
||||
#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)
|
||||
|
||||
// Used only by TetheringPrivilegedTests, not by production code.
|
||||
DEFINE_BPF_MAP_GRW(tether_ingress_map_TEST, HASH, TetherIngressKey, TetherIngressValue, 16,
|
||||
// ----- IPv6 Support -----
|
||||
|
||||
DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 64,
|
||||
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;
|
||||
DEFINE_BPF_MAP_GRW(tether_downstream64_map, HASH, TetherDownstream64Key, TetherDownstream64Value,
|
||||
64, 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) {
|
||||
const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
|
||||
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;
|
||||
|
||||
// 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 IPv6 frame
|
||||
if (skb->protocol != htons(ETH_P_IPV6)) return TC_ACT_OK;
|
||||
|
||||
@@ -55,43 +126,70 @@ static inline __always_inline int do_forward(struct __sk_buff* skb, bool is_ethe
|
||||
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,
|
||||
};
|
||||
|
||||
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
|
||||
@@ -102,9 +200,9 @@ static inline __always_inline int do_forward(struct __sk_buff* skb, bool is_ethe
|
||||
// (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;
|
||||
@@ -116,15 +214,15 @@ static inline __always_inline int do_forward(struct __sk_buff* skb, bool is_ethe
|
||||
// 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
|
||||
@@ -134,12 +232,16 @@ static inline __always_inline int do_forward(struct __sk_buff* skb, bool is_ethe
|
||||
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
|
||||
@@ -151,10 +253,11 @@ static inline __always_inline int do_forward(struct __sk_buff* skb, bool is_ethe
|
||||
// (-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.
|
||||
@@ -166,9 +269,16 @@ static inline __always_inline int do_forward(struct __sk_buff* skb, bool is_ethe
|
||||
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,
|
||||
@@ -183,29 +293,458 @@ int sched_cls_ingress_tether_ether(struct __sk_buff* skb) {
|
||||
// 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, 64, AID_NETWORK_STACK)
|
||||
|
||||
DEFINE_BPF_MAP_GRW(tether_upstream4_map, HASH, Tether4Key, Tether4Value, 64, AID_NETWORK_STACK)
|
||||
|
||||
static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool is_ethernet,
|
||||
const bool downstream, const bool updatetime) {
|
||||
const int l2_header_size = is_ethernet ? sizeof(struct ethhdr) : 0;
|
||||
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;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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) for (int i = 0; i < ETH_ALEN; ++i) k.dstMac[i] = eth->h_dest[i];
|
||||
|
||||
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
|
||||
|
||||
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_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_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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Full featured (optional) implementations for [4.14..5.8) kernels
|
||||
|
||||
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_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_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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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).
|
||||
//
|
||||
// [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.
|
||||
|
||||
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_downstream4_rawip$4_14", AID_ROOT, AID_NETWORK_STACK,
|
||||
sched_cls_tether_downstream4_rawip_4_14, KVER(4, 14, 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_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);
|
||||
}
|
||||
|
||||
DEFINE_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, 8, 0))
|
||||
(struct __sk_buff* skb) {
|
||||
return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ false);
|
||||
}
|
||||
|
||||
// Placeholder (no-op) implementations for older pre-4.14 kernels
|
||||
|
||||
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_downstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
|
||||
sched_cls_tether_downstream4_rawip_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;
|
||||
}
|
||||
|
||||
DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK,
|
||||
sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER(4, 14, 0))
|
||||
(struct __sk_buff* skb) {
|
||||
return TC_ACT_OK;
|
||||
}
|
||||
|
||||
// ----- XDP Support -----
|
||||
|
||||
#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 XDP_PASS;
|
||||
}
|
||||
|
||||
DEFINE_XDP_PROG("xdp/tether_downstream_rawip",
|
||||
xdp_tether_downstream_rawip) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
DEFINE_XDP_PROG("xdp/tether_upstream_ether",
|
||||
xdp_tether_upstream_ether) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
DEFINE_XDP_PROG("xdp/tether_upstream_rawip",
|
||||
xdp_tether_upstream_rawip) {
|
||||
return XDP_PASS;
|
||||
}
|
||||
|
||||
LICENSE("Apache 2.0");
|
||||
CRITICAL("netd");
|
||||
|
||||
47
Tethering/bpf_progs/test.c
Normal file
47
Tethering/bpf_progs/test.c
Normal file
@@ -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");
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
@@ -24,6 +24,7 @@ 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);
|
||||
|
||||
extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
|
||||
JNIEnv *env;
|
||||
@@ -36,6 +37,8 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
|
||||
|
||||
if (register_com_android_networkstack_tethering_BpfMap(env) < 0) return JNI_ERR;
|
||||
|
||||
if (register_com_android_networkstack_tethering_BpfCoordinator(env) < 0) return JNI_ERR;
|
||||
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,13 +67,12 @@ import com.android.internal.util.MessageUtils;
|
||||
import com.android.internal.util.State;
|
||||
import com.android.internal.util.StateMachine;
|
||||
import com.android.networkstack.tethering.BpfCoordinator;
|
||||
import com.android.networkstack.tethering.BpfCoordinator.ClientInfo;
|
||||
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
|
||||
import com.android.networkstack.tethering.PrivateAddressCoordinator;
|
||||
|
||||
import java.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 @@ public class IpServer extends StateMachine {
|
||||
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);
|
||||
@@ -941,11 +930,38 @@ public class IpServer extends StateMachine {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 +1127,19 @@ public class IpServer extends StateMachine {
|
||||
}
|
||||
}
|
||||
|
||||
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 +1175,7 @@ public class IpServer extends StateMachine {
|
||||
}
|
||||
|
||||
stopIPv4();
|
||||
stopConntrackMonitoring();
|
||||
|
||||
resetLinkProperties();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.networkstack.tethering.TetherStatsValue;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.SocketException;
|
||||
@@ -34,6 +36,10 @@ import java.util.Objects;
|
||||
* {@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 @@ public class TetheringUtils {
|
||||
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;
|
||||
|
||||
@@ -23,26 +23,30 @@ import static android.net.NetworkStats.SET_DEFAULT;
|
||||
import static android.net.NetworkStats.TAG_NONE;
|
||||
import static android.net.NetworkStats.UID_ALL;
|
||||
import static android.net.NetworkStats.UID_TETHERING;
|
||||
import static android.net.ip.ConntrackMonitor.ConntrackEvent;
|
||||
import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
|
||||
import static android.system.OsConstants.ETH_P_IP;
|
||||
import static android.system.OsConstants.ETH_P_IPV6;
|
||||
|
||||
import static com.android.networkstack.tethering.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;
|
||||
@@ -55,14 +59,21 @@ 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.
|
||||
@@ -74,10 +85,39 @@ import java.util.Objects;
|
||||
* @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");
|
||||
}
|
||||
|
||||
static final boolean DOWNSTREAM = true;
|
||||
static final boolean UPSTREAM = false;
|
||||
|
||||
private static final String TAG = BpfCoordinator.class.getSimpleName();
|
||||
private static final int DUMP_TIMEOUT_MS = 10_000;
|
||||
private static final String TETHER_INGRESS_FS_PATH =
|
||||
"/sys/fs/bpf/map_offload_tether_ingress_map";
|
||||
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 {
|
||||
@@ -93,6 +133,8 @@ public class BpfCoordinator {
|
||||
private final SharedLog mLog;
|
||||
@NonNull
|
||||
private final Dependencies mDeps;
|
||||
@NonNull
|
||||
private final ConntrackMonitor mConntrackMonitor;
|
||||
@Nullable
|
||||
private final BpfTetherStatsProvider mStatsProvider;
|
||||
@NonNull
|
||||
@@ -155,9 +197,30 @@ public class BpfCoordinator {
|
||||
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<>();
|
||||
|
||||
// Runnable that used by scheduling next polling of stats.
|
||||
private final Runnable mScheduledPollingTask = () -> {
|
||||
updateForwardedStatsFromNetd();
|
||||
updateForwardedStats();
|
||||
maybeSchedulePollingStats();
|
||||
};
|
||||
|
||||
@@ -178,6 +241,11 @@ public class BpfCoordinator {
|
||||
/** Get tethering configuration. */
|
||||
@Nullable public abstract TetheringConfiguration getTetherConfig();
|
||||
|
||||
/** Get conntrack monitor. */
|
||||
@NonNull public ConntrackMonitor getConntrackMonitor(ConntrackEventConsumer consumer) {
|
||||
return new ConntrackMonitor(getHandler(), getSharedLog(), consumer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check OS Build at least S.
|
||||
*
|
||||
@@ -189,13 +257,68 @@ public class BpfCoordinator {
|
||||
return SdkLevel.isAtLeastS();
|
||||
}
|
||||
|
||||
/** Get ingress BPF map. */
|
||||
@Nullable public BpfMap<TetherIngressKey, TetherIngressValue> getBpfIngressMap() {
|
||||
/** Get downstream4 BPF map. */
|
||||
@Nullable public BpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() {
|
||||
try {
|
||||
return new BpfMap<>(TETHER_INGRESS_FS_PATH,
|
||||
BpfMap.BPF_F_RDWR, TetherIngressKey.class, TetherIngressValue.class);
|
||||
return new BpfMap<>(TETHER_DOWNSTREAM4_MAP_PATH,
|
||||
BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class);
|
||||
} catch (ErrnoException e) {
|
||||
Log.e(TAG, "Cannot create ingress map: " + e);
|
||||
Log.e(TAG, "Cannot create downstream4 map: " + e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Get upstream4 BPF map. */
|
||||
@Nullable public BpfMap<Tether4Key, Tether4Value> getBpfUpstream4Map() {
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -208,6 +331,7 @@ public class BpfCoordinator {
|
||||
mNetd = mDeps.getNetd();
|
||||
mLog = mDeps.getSharedLog().forSubComponent(TAG);
|
||||
mIsBpfEnabled = isBpfEnabled();
|
||||
mConntrackMonitor = mDeps.getConntrackMonitor(new BpfConntrackEventConsumer());
|
||||
BpfTetherStatsProvider provider = new BpfTetherStatsProvider();
|
||||
try {
|
||||
mDeps.getNetworkStatsManager().registerNetworkStatsProvider(
|
||||
@@ -261,7 +385,7 @@ public class BpfCoordinator {
|
||||
if (mHandler.hasCallbacks(mScheduledPollingTask)) {
|
||||
mHandler.removeCallbacks(mScheduledPollingTask);
|
||||
}
|
||||
updateForwardedStatsFromNetd();
|
||||
updateForwardedStats();
|
||||
mPollingStarted = false;
|
||||
|
||||
mLog.i("Polling stopped");
|
||||
@@ -271,6 +395,58 @@ public class BpfCoordinator {
|
||||
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) {
|
||||
if (!isUsingBpf()) 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) {
|
||||
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.
|
||||
@@ -289,7 +465,7 @@ public class BpfCoordinator {
|
||||
}
|
||||
LinkedHashMap<Inet6Address, Ipv6ForwardingRule> rules = mIpv6ForwardingRules.get(ipServer);
|
||||
|
||||
// Setup the data limit on the given upstream if the first rule is added.
|
||||
// When the first rule is added to an upstream, setup upstream forwarding and data limit.
|
||||
final int upstreamIfindex = rule.upstreamIfindex;
|
||||
if (!isAnyRuleOnUpstream(upstreamIfindex)) {
|
||||
// If failed to set a data limit, probably should not use this upstream, because
|
||||
@@ -300,6 +476,19 @@ public class BpfCoordinator {
|
||||
final String iface = mInterfaceNames.get(upstreamIfindex);
|
||||
mLog.e("Setting data limit for " + iface + " failed.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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,
|
||||
NULL_MAC_ADDRESS, NULL_MAC_ADDRESS, NetworkStackConstants.ETHER_MTU)) {
|
||||
mLog.e("Failed to enable upstream IPv6 forwarding from "
|
||||
+ mInterfaceNames.get(downstream) + " to " + mInterfaceNames.get(upstream));
|
||||
}
|
||||
}
|
||||
|
||||
// Must update the adding rule after calling #isAnyRuleOnUpstream because it needs to
|
||||
@@ -316,13 +505,7 @@ public class BpfCoordinator {
|
||||
@NonNull final IpServer ipServer, @NonNull final Ipv6ForwardingRule rule) {
|
||||
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;
|
||||
@@ -338,19 +521,32 @@ public class BpfCoordinator {
|
||||
mIpv6ForwardingRules.remove(ipServer);
|
||||
}
|
||||
|
||||
// If no more rules between this upstream and downstream, stop upstream forwarding.
|
||||
if (!isAnyRuleFromDownstreamToUpstream(rule.downstreamIfindex, rule.upstreamIfindex)) {
|
||||
final int downstream = rule.downstreamIfindex;
|
||||
final int upstream = rule.upstreamIfindex;
|
||||
if (!mBpfCoordinatorShim.stopUpstreamIpv6Forwarding(downstream, upstream)) {
|
||||
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.
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,12 +579,22 @@ public class BpfCoordinator {
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -416,6 +622,80 @@ public class BpfCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = InterfaceParams.getByName(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
@@ -444,11 +724,15 @@ public class BpfCoordinator {
|
||||
|
||||
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();
|
||||
@@ -468,6 +752,11 @@ public class BpfCoordinator {
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -488,11 +777,97 @@ public class BpfCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
private String ipv6UpstreamRuletoString(TetherUpstream6Key key, Tether6Value value) {
|
||||
return String.format("%d(%s) -> %d(%s) %04x %s %s",
|
||||
key.iif, getIfName(key.iif), 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 IPv4 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("%d(%s) %d(%s) %s:%d -> %s:%d -> %s:%d",
|
||||
key.iif, getIfName(key.iif), value.oif, getIfName(value.oif),
|
||||
private4, key.srcPort, 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]: iif(iface) oif(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) {
|
||||
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 {
|
||||
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
|
||||
@@ -534,19 +909,19 @@ public class BpfCoordinator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a TetherIngressKey object built from the rule.
|
||||
* Return a TetherDownstream6Key object built from the rule.
|
||||
*/
|
||||
@NonNull
|
||||
public TetherIngressKey makeTetherIngressKey() {
|
||||
return new TetherIngressKey(upstreamIfindex, address.getAddress());
|
||||
public TetherDownstream6Key makeTetherDownstream6Key() {
|
||||
return new TetherDownstream6Key(upstreamIfindex, address.getAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a TetherIngressValue object built from the rule.
|
||||
* Return a Tether6Value object built from the rule.
|
||||
*/
|
||||
@NonNull
|
||||
public TetherIngressValue makeTetherIngressValue() {
|
||||
return new TetherIngressValue(downstreamIfindex, dstMac, srcMac, ETH_P_IPV6,
|
||||
public Tether6Value makeTether6Value() {
|
||||
return new Tether6Value(downstreamIfindex, dstMac, srcMac, ETH_P_IPV6,
|
||||
NetworkStackConstants.ETHER_MTU);
|
||||
}
|
||||
|
||||
@@ -569,6 +944,48 @@ public class BpfCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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.
|
||||
@@ -635,6 +1052,99 @@ public class BpfCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
@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.
|
||||
private 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(false, upstream4Key);
|
||||
mBpfCoordinatorShim.tetherOffloadRuleRemove(true, downstream4Key);
|
||||
return;
|
||||
}
|
||||
|
||||
final Tether4Value upstream4Value = makeTetherUpstream4Value(e, upstreamIndex);
|
||||
final Tether4Value downstream4Value = makeTetherDownstream4Value(e, tetherClient,
|
||||
upstreamIndex);
|
||||
|
||||
mBpfCoordinatorShim.tetherOffloadRuleAdd(false, upstream4Key, upstream4Value);
|
||||
mBpfCoordinatorShim.tetherOffloadRuleAdd(true, downstream4Key, downstream4Value);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBpfEnabled() {
|
||||
final TetheringConfiguration config = mDeps.getTetherConfig();
|
||||
return (config != null) ? config.isBpfOffloadEnabled() : true /* default value */;
|
||||
@@ -660,20 +1170,13 @@ public class BpfCoordinator {
|
||||
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.
|
||||
@@ -686,7 +1189,7 @@ public class BpfCoordinator {
|
||||
if (ifIndex == 0) return;
|
||||
|
||||
final long quotaBytes = getQuotaBytes(iface);
|
||||
sendDataLimitToNetd(ifIndex, quotaBytes);
|
||||
sendDataLimitToBpfMap(ifIndex, quotaBytes);
|
||||
}
|
||||
|
||||
// Handle the data limit update while adding forwarding rules.
|
||||
@@ -697,7 +1200,7 @@ public class BpfCoordinator {
|
||||
return false;
|
||||
}
|
||||
final long quotaBytes = getQuotaBytes(iface);
|
||||
return sendDataLimitToNetd(ifIndex, quotaBytes);
|
||||
return sendDataLimitToBpfMap(ifIndex, quotaBytes);
|
||||
}
|
||||
|
||||
private boolean isAnyRuleOnUpstream(int upstreamIfindex) {
|
||||
@@ -710,6 +1213,19 @@ public class BpfCoordinator {
|
||||
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;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private NetworkStats buildNetworkStats(@NonNull StatsType type, int ifIndex,
|
||||
@NonNull final ForwardedStats diff) {
|
||||
@@ -743,10 +1259,11 @@ public class BpfCoordinator {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -778,16 +1295,15 @@ public class BpfCoordinator {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -828,4 +1344,6 @@ public class BpfCoordinator {
|
||||
final SparseArray<String> getInterfaceNamesForTesting() {
|
||||
return mInterfaceNames;
|
||||
}
|
||||
|
||||
private static native String[] getBpfCounterNames();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ 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;
|
||||
@@ -40,6 +41,10 @@ import java.util.function.BiConsumer;
|
||||
* @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;
|
||||
@@ -76,6 +81,21 @@ public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable
|
||||
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.
|
||||
*/
|
||||
@@ -118,6 +138,11 @@ public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable
|
||||
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());
|
||||
@@ -197,10 +222,24 @@ public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
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;
|
||||
|
||||
@@ -418,7 +418,8 @@ public class EntitlementManager {
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,15 +21,13 @@ 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.util.Objects;
|
||||
|
||||
/** The value of BpfMap which is used for bpf offload. */
|
||||
public class TetherIngressValue extends Struct {
|
||||
@Field(order = 0, type = Type.U32)
|
||||
public final long oif; // The output interface index.
|
||||
/** 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)
|
||||
@@ -42,7 +40,7 @@ public class TetherIngressValue extends Struct {
|
||||
@Field(order = 4, type = Type.U16)
|
||||
public final int pmtu; // The maximum L3 output path/route mtu.
|
||||
|
||||
public TetherIngressValue(final long oif, @NonNull final MacAddress ethDstMac,
|
||||
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);
|
||||
@@ -54,24 +52,6 @@ public class TetherIngressValue extends Struct {
|
||||
this.pmtu = pmtu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
|
||||
if (!(obj instanceof TetherIngressValue)) return false;
|
||||
|
||||
final TetherIngressValue that = (TetherIngressValue) obj;
|
||||
|
||||
return oif == that.oif && ethDstMac.equals(that.ethDstMac)
|
||||
&& ethSrcMac.equals(that.ethSrcMac) && ethProto == that.ethProto
|
||||
&& pmtu == that.pmtu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(oif, ethDstMac, ethSrcMac, ethProto, pmtu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("oif: %d, dstMac: %s, srcMac: %s, proto: %d, pmtu: %d", oif,
|
||||
@@ -26,14 +26,14 @@ import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** The key of BpfMap which is used for bpf offload. */
|
||||
public class TetherIngressKey extends Struct {
|
||||
public class TetherDownstream6Key extends Struct {
|
||||
@Field(order = 0, type = Type.U32)
|
||||
public final long iif; // The input interface index.
|
||||
|
||||
@Field(order = 1, type = Type.ByteArray, arraysize = 16)
|
||||
public final byte[] neigh6; // The destination IPv6 address.
|
||||
|
||||
public TetherIngressKey(final long iif, final byte[] neigh6) {
|
||||
public TetherDownstream6Key(final long iif, final byte[] neigh6) {
|
||||
try {
|
||||
final Inet6Address unused = (Inet6Address) InetAddress.getByAddress(neigh6);
|
||||
} catch (ClassCastException | UnknownHostException e) {
|
||||
@@ -48,9 +48,9 @@ public class TetherIngressKey extends Struct {
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
|
||||
if (!(obj instanceof TetherIngressKey)) return false;
|
||||
if (!(obj instanceof TetherDownstream6Key)) return false;
|
||||
|
||||
final TetherIngressKey that = (TetherIngressKey) obj;
|
||||
final TetherDownstream6Key that = (TetherDownstream6Key) obj;
|
||||
|
||||
return iif == that.iif && Arrays.equals(neigh6, that.neigh6);
|
||||
}
|
||||
@@ -66,7 +66,7 @@ public class TetherIngressKey extends Struct {
|
||||
return String.format("iif: %d, neigh: %s", iif, Inet6Address.getByAddress(neigh6));
|
||||
} catch (UnknownHostException e) {
|
||||
// Should not happen because construtor already verify neigh6.
|
||||
throw new IllegalStateException("Invalid TetherIngressKey");
|
||||
throw new IllegalStateException("Invalid TetherDownstream6Key");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/** 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.
|
||||
|
||||
public TetherUpstream6Key(int iif) {
|
||||
this.iif = iif;
|
||||
}
|
||||
}
|
||||
@@ -1636,6 +1636,13 @@ public class Tethering {
|
||||
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) {
|
||||
@@ -1682,12 +1689,14 @@ public class Tethering {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2218,6 +2227,13 @@ public class Tethering {
|
||||
&& !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")
|
||||
@@ -2228,6 +2244,11 @@ public class Tethering {
|
||||
return;
|
||||
}
|
||||
|
||||
if (argsContain(args, "bpf")) {
|
||||
dumpBpf(pw);
|
||||
return;
|
||||
}
|
||||
|
||||
pw.println("Tethering:");
|
||||
pw.increaseIndent();
|
||||
|
||||
@@ -2279,10 +2300,7 @@ public class Tethering {
|
||||
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();
|
||||
@@ -2405,6 +2423,19 @@ public class Tethering {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,6 @@ public class TetheringService extends Service {
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public Tethering makeTethering(TetheringDependencies deps) {
|
||||
System.loadLibrary("tetherutilsjni");
|
||||
return new Tethering(deps);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
package {
|
||||
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||
}
|
||||
|
||||
filegroup {
|
||||
name: "TetheringTestsJarJarRules",
|
||||
srcs: ["jarjar-rules.txt"],
|
||||
|
||||
@@ -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 @@ android_test {
|
||||
test_config: "AndroidTest_Coverage.xml",
|
||||
defaults: ["libnetworkstackutilsjni_deps"],
|
||||
static_libs: [
|
||||
"NetdStaticLibTestsLib",
|
||||
"NetworkStaticLibTestsLib",
|
||||
"NetworkStackTestsLib",
|
||||
"TetheringTestsLib",
|
||||
@@ -81,6 +86,7 @@ android_test {
|
||||
"libstaticjvmtiagent",
|
||||
// For NetworkStackUtils included in NetworkStackBase
|
||||
"libnetworkstackutilsjni",
|
||||
"libtetherutilsjni",
|
||||
],
|
||||
jarjar_rules: ":TetheringTestsJarJarRules",
|
||||
compile_multilib: "both",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
package {
|
||||
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||
}
|
||||
|
||||
java_defaults {
|
||||
name: "TetheringPrivilegedTestsJniDefaults",
|
||||
jni_libs: [
|
||||
|
||||
@@ -35,7 +35,6 @@ import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
@@ -51,10 +50,12 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
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_INGRESS_FS_PATH =
|
||||
"/sys/fs/bpf/map_offload_tether_ingress_map_TEST";
|
||||
private static final String TETHER_DOWNSTREAM6_FS_PATH =
|
||||
"/sys/fs/bpf/tethering/map_test_tether_downstream6_map";
|
||||
|
||||
private ArrayMap<TetherIngressKey, TetherIngressValue> mTestData;
|
||||
private ArrayMap<TetherDownstream6Key, Tether6Value> mTestData;
|
||||
|
||||
private BpfMap<TetherDownstream6Key, Tether6Value> mTestMap;
|
||||
|
||||
@BeforeClass
|
||||
public static void setupOnce() {
|
||||
@@ -63,66 +64,54 @@ public final class BpfMapTest {
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
// TODO: Simply the test map creation and deletion.
|
||||
// - Make the map a class member (mTestMap)
|
||||
// - Open the test map RW in setUp
|
||||
// - Close the test map in tearDown.
|
||||
cleanTestMap();
|
||||
|
||||
mTestData = new ArrayMap<>();
|
||||
mTestData.put(createTetherIngressKey(101, "2001:db8::1"),
|
||||
createTetherIngressValue(11, "00:00:00:00:00:0a", "11:11:11:00:00:0b", ETH_P_IPV6,
|
||||
1280));
|
||||
mTestData.put(createTetherIngressKey(102, "2001:db8::2"),
|
||||
createTetherIngressValue(22, "00:00:00:00:00:0c", "22:22:22:00:00:0d", ETH_P_IPV6,
|
||||
1400));
|
||||
mTestData.put(createTetherIngressKey(103, "2001:db8::3"),
|
||||
createTetherIngressValue(33, "00:00:00:00:00:0e", "33:33:33:00:00:0f", ETH_P_IPV6,
|
||||
1500));
|
||||
mTestData.put(createTetherDownstream6Key(101, "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, "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, "2001:db8::3"),
|
||||
createTether6Value(33, "00:00:00:00:00:0e", "33:33:33:00:00:0f",
|
||||
ETH_P_IPV6, 1500));
|
||||
|
||||
initTestMap();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
cleanTestMap();
|
||||
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 BpfMap<TetherIngressKey, TetherIngressValue> getTestMap() throws Exception {
|
||||
return new BpfMap<>(
|
||||
TETHER_INGRESS_FS_PATH, BpfMap.BPF_F_RDWR,
|
||||
TetherIngressKey.class, TetherIngressValue.class);
|
||||
}
|
||||
|
||||
private void cleanTestMap() throws Exception {
|
||||
try (BpfMap<TetherIngressKey, TetherIngressValue> bpfMap = getTestMap()) {
|
||||
bpfMap.forEach((key, value) -> {
|
||||
try {
|
||||
assertTrue(bpfMap.deleteEntry(key));
|
||||
} catch (ErrnoException e) {
|
||||
fail("Fail to delete the key " + key + ": " + e);
|
||||
}
|
||||
});
|
||||
assertNull(bpfMap.getFirstKey());
|
||||
}
|
||||
}
|
||||
|
||||
private TetherIngressKey createTetherIngressKey(long iif, String address) throws Exception {
|
||||
private TetherDownstream6Key createTetherDownstream6Key(long iif, String address)
|
||||
throws Exception {
|
||||
final InetAddress ipv6Address = InetAddress.getByName(address);
|
||||
|
||||
return new TetherIngressKey(iif, ipv6Address.getAddress());
|
||||
return new TetherDownstream6Key(iif, ipv6Address.getAddress());
|
||||
}
|
||||
|
||||
private TetherIngressValue createTetherIngressValue(long oif, String src, String dst, int proto,
|
||||
int pmtu) throws Exception {
|
||||
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 TetherIngressValue(oif, dstMac, srcMac, proto, pmtu);
|
||||
return new Tether6Value(oif, dstMac, srcMac, proto, pmtu);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFd() throws Exception {
|
||||
try (BpfMap readOnlyMap = new BpfMap<>(TETHER_INGRESS_FS_PATH, BpfMap.BPF_F_RDONLY,
|
||||
TetherIngressKey.class, TetherIngressValue.class)) {
|
||||
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));
|
||||
@@ -131,8 +120,8 @@ public final class BpfMapTest {
|
||||
assertEquals(OsConstants.EPERM, expected.errno);
|
||||
}
|
||||
}
|
||||
try (BpfMap writeOnlyMap = new BpfMap<>(TETHER_INGRESS_FS_PATH, BpfMap.BPF_F_WRONLY,
|
||||
TetherIngressKey.class, TetherIngressValue.class)) {
|
||||
try (BpfMap writeOnlyMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_WRONLY,
|
||||
TetherDownstream6Key.class, Tether6Value.class)) {
|
||||
assertNotNull(writeOnlyMap);
|
||||
try {
|
||||
writeOnlyMap.getFirstKey();
|
||||
@@ -141,214 +130,238 @@ public final class BpfMapTest {
|
||||
assertEquals(OsConstants.EPERM, expected.errno);
|
||||
}
|
||||
}
|
||||
try (BpfMap readWriteMap = new BpfMap<>(TETHER_INGRESS_FS_PATH, BpfMap.BPF_F_RDWR,
|
||||
TetherIngressKey.class, TetherIngressValue.class)) {
|
||||
try (BpfMap readWriteMap = new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR,
|
||||
TetherDownstream6Key.class, Tether6Value.class)) {
|
||||
assertNotNull(readWriteMap);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFirstKey() throws Exception {
|
||||
try (BpfMap<TetherIngressKey, TetherIngressValue> bpfMap = getTestMap()) {
|
||||
// getFirstKey on an empty map returns null.
|
||||
assertFalse(bpfMap.containsKey(mTestData.keyAt(0)));
|
||||
assertNull(bpfMap.getFirstKey());
|
||||
assertNull(bpfMap.getValue(mTestData.keyAt(0)));
|
||||
public void testIsEmpty() throws Exception {
|
||||
assertNull(mTestMap.getFirstKey());
|
||||
assertTrue(mTestMap.isEmpty());
|
||||
|
||||
// getFirstKey on a non-empty map returns the first key.
|
||||
bpfMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0));
|
||||
assertEquals(mTestData.keyAt(0), bpfMap.getFirstKey());
|
||||
}
|
||||
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 {
|
||||
try (BpfMap<TetherIngressKey, TetherIngressValue> bpfMap = getTestMap()) {
|
||||
// [1] If the passed-in key is not found on empty map, return null.
|
||||
final TetherIngressKey nonexistentKey = createTetherIngressKey(1234, "2001:db8::10");
|
||||
assertNull(bpfMap.getNextKey(nonexistentKey));
|
||||
// [1] If the passed-in key is not found on empty map, return null.
|
||||
final TetherDownstream6Key nonexistentKey =
|
||||
createTetherDownstream6Key(1234, "2001:db8::10");
|
||||
assertNull(mTestMap.getNextKey(nonexistentKey));
|
||||
|
||||
// [2] If the passed-in key is null on empty map, throw NullPointerException.
|
||||
try {
|
||||
bpfMap.getNextKey(null);
|
||||
fail("Getting next key with null key should throw NullPointerException");
|
||||
} catch (NullPointerException expected) { }
|
||||
// [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<TetherIngressKey, TetherIngressValue> resultMap = new ArrayMap<>();
|
||||
bpfMap.insertEntry(mTestData.keyAt(0), mTestData.valueAt(0));
|
||||
resultMap.put(mTestData.keyAt(0), mTestData.valueAt(0));
|
||||
// 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 TetherIngressKey lastKey = bpfMap.getFirstKey();
|
||||
assertNull(bpfMap.getNextKey(lastKey));
|
||||
// [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.
|
||||
bpfMap.insertEntry(mTestData.keyAt(1), mTestData.valueAt(1));
|
||||
resultMap.put(mTestData.keyAt(1), mTestData.valueAt(1));
|
||||
// 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.
|
||||
TetherIngressKey nextKey = bpfMap.getFirstKey();
|
||||
while (nextKey != null) {
|
||||
if (resultMap.remove(nextKey).equals(nextKey)) {
|
||||
fail("Unexpected result: " + nextKey);
|
||||
}
|
||||
nextKey = bpfMap.getNextKey(nextKey);
|
||||
// [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);
|
||||
}
|
||||
assertTrue(resultMap.isEmpty());
|
||||
|
||||
// [5] If the passed-in key is not found on non-empty map, return the first key.
|
||||
assertEquals(bpfMap.getFirstKey(), bpfMap.getNextKey(nonexistentKey));
|
||||
|
||||
// [6] If the passed-in key is null on non-empty map, throw NullPointerException.
|
||||
try {
|
||||
bpfMap.getNextKey(null);
|
||||
fail("Getting next key with null key should throw NullPointerException");
|
||||
} catch (NullPointerException expected) { }
|
||||
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 testUpdateBpfMap() throws Exception {
|
||||
try (BpfMap<TetherIngressKey, TetherIngressValue> bpfMap = getTestMap()) {
|
||||
final TetherDownstream6Key key = mTestData.keyAt(0);
|
||||
final Tether6Value value = mTestData.valueAt(0);
|
||||
final Tether6Value value2 = mTestData.valueAt(1);
|
||||
assertFalse(mTestMap.deleteEntry(key));
|
||||
|
||||
final TetherIngressKey key = mTestData.keyAt(0);
|
||||
final TetherIngressValue value = mTestData.valueAt(0);
|
||||
final TetherIngressValue value2 = mTestData.valueAt(1);
|
||||
assertFalse(bpfMap.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 create an entry if it does not exist already.
|
||||
bpfMap.updateEntry(key, value);
|
||||
assertTrue(bpfMap.containsKey(key));
|
||||
final TetherIngressValue result = bpfMap.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);
|
||||
|
||||
// updateEntry will update an entry that already exists.
|
||||
bpfMap.updateEntry(key, value2);
|
||||
assertTrue(bpfMap.containsKey(key));
|
||||
final TetherIngressValue result2 = bpfMap.getValue(key);
|
||||
assertEquals(value2, result2);
|
||||
|
||||
assertTrue(bpfMap.deleteEntry(key));
|
||||
assertFalse(bpfMap.containsKey(key));
|
||||
}
|
||||
assertTrue(mTestMap.deleteEntry(key));
|
||||
assertFalse(mTestMap.containsKey(key));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInsertReplaceEntry() throws Exception {
|
||||
try (BpfMap<TetherIngressKey, TetherIngressValue> bpfMap = getTestMap()) {
|
||||
final TetherDownstream6Key key = mTestData.keyAt(0);
|
||||
final Tether6Value value = mTestData.valueAt(0);
|
||||
final Tether6Value value2 = mTestData.valueAt(1);
|
||||
|
||||
final TetherIngressKey key = mTestData.keyAt(0);
|
||||
final TetherIngressValue value = mTestData.valueAt(0);
|
||||
final TetherIngressValue 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));
|
||||
|
||||
try {
|
||||
bpfMap.replaceEntry(key, value);
|
||||
fail("Replacing non-existent key " + key + " should throw NoSuchElementException");
|
||||
} catch (NoSuchElementException expected) { }
|
||||
assertFalse(bpfMap.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) { }
|
||||
|
||||
bpfMap.insertEntry(key, value);
|
||||
assertTrue(bpfMap.containsKey(key));
|
||||
final TetherIngressValue result = bpfMap.getValue(key);
|
||||
assertEquals(value, result);
|
||||
try {
|
||||
bpfMap.insertEntry(key, value);
|
||||
fail("Inserting existing key " + key + " should throw IllegalStateException");
|
||||
} catch (IllegalStateException expected) { }
|
||||
|
||||
bpfMap.replaceEntry(key, value2);
|
||||
assertTrue(bpfMap.containsKey(key));
|
||||
final TetherIngressValue result2 = bpfMap.getValue(key);
|
||||
assertEquals(value2, result2);
|
||||
}
|
||||
mTestMap.replaceEntry(key, value2);
|
||||
assertTrue(mTestMap.containsKey(key));
|
||||
final Tether6Value result2 = mTestMap.getValue(key);
|
||||
assertEquals(value2, result2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIterateBpfMap() throws Exception {
|
||||
try (BpfMap<TetherIngressKey, TetherIngressValue> bpfMap = getTestMap()) {
|
||||
final ArrayMap<TetherIngressKey, TetherIngressValue> resultMap =
|
||||
new ArrayMap<>(mTestData);
|
||||
final ArrayMap<TetherDownstream6Key, Tether6Value> resultMap =
|
||||
new ArrayMap<>(mTestData);
|
||||
|
||||
for (int i = 0; i < resultMap.size(); i++) {
|
||||
bpfMap.insertEntry(resultMap.keyAt(i), resultMap.valueAt(i));
|
||||
}
|
||||
|
||||
bpfMap.forEach((key, value) -> {
|
||||
if (!value.equals(resultMap.remove(key))) {
|
||||
fail("Unexpected result: " + key + ", value: " + value);
|
||||
}
|
||||
});
|
||||
assertTrue(resultMap.isEmpty());
|
||||
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 {
|
||||
try (BpfMap<TetherIngressKey, TetherIngressValue> bpfMap = getTestMap()) {
|
||||
// Can't use an int because variables used in a lambda must be final.
|
||||
final AtomicInteger count = new AtomicInteger();
|
||||
bpfMap.forEach((key, value) -> count.incrementAndGet());
|
||||
// Expect that the consumer was never called.
|
||||
assertEquals(0, count.get());
|
||||
}
|
||||
// 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 {
|
||||
try (BpfMap<TetherIngressKey, TetherIngressValue> bpfMap = getTestMap()) {
|
||||
final ArrayMap<TetherIngressKey, TetherIngressValue> resultMap =
|
||||
new ArrayMap<>(mTestData);
|
||||
final ArrayMap<TetherDownstream6Key, Tether6Value> resultMap =
|
||||
new ArrayMap<>(mTestData);
|
||||
|
||||
for (int i = 0; i < resultMap.size(); i++) {
|
||||
bpfMap.insertEntry(resultMap.keyAt(i), resultMap.valueAt(i));
|
||||
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());
|
||||
}
|
||||
|
||||
// Can't use an int because variables used in a lambda must be final.
|
||||
final AtomicInteger count = new AtomicInteger();
|
||||
bpfMap.forEach((key, value) -> {
|
||||
try {
|
||||
assertTrue(bpfMap.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(bpfMap.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 {
|
||||
try (BpfMap<TetherIngressKey, TetherIngressValue> bpfMap = getTestMap()) {
|
||||
final ArrayMap<TetherIngressKey, TetherIngressValue> testData = new ArrayMap<>();
|
||||
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(createTetherIngressKey(i, "2001:db8::1"), createTetherIngressValue(
|
||||
100, "de:ad:be:ef:00:01", "de:ad:be:ef:00:02", ETH_P_IPV6, 1500));
|
||||
}
|
||||
// Build test data for TEST_MAP_SIZE + 1 entries.
|
||||
for (int i = 1; i <= TEST_MAP_SIZE + 1; i++) {
|
||||
testData.put(createTetherDownstream6Key(i, "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++) {
|
||||
bpfMap.insertEntry(testData.keyAt(i), testData.valueAt(i));
|
||||
}
|
||||
// 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 {
|
||||
bpfMap.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);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 @@ java_defaults {
|
||||
// For mockito extended
|
||||
"libdexmakerjvmtiagent",
|
||||
"libstaticjvmtiagent",
|
||||
"libtetherutilsjni",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -104,8 +104,15 @@ 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.TetherIngressKey;
|
||||
import com.android.networkstack.tethering.TetherIngressValue;
|
||||
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;
|
||||
@@ -168,7 +175,13 @@ public class IpServerTest {
|
||||
@Mock private PrivateAddressCoordinator mAddressCoordinator;
|
||||
@Mock private NetworkStatsManager mStatsManager;
|
||||
@Mock private TetheringConfiguration mTetherConfig;
|
||||
@Mock private BpfMap<TetherIngressKey, TetherIngressValue> mBpfIngressMap;
|
||||
@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;
|
||||
|
||||
@@ -193,9 +206,6 @@ public class IpServerTest {
|
||||
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) {
|
||||
@@ -289,9 +299,40 @@ public class IpServerTest {
|
||||
return mTetherConfig;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public ConntrackMonitor getConntrackMonitor(
|
||||
ConntrackMonitor.ConntrackEventConsumer consumer) {
|
||||
return mConntrackMonitor;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public BpfMap<TetherIngressKey, TetherIngressValue> getBpfIngressMap() {
|
||||
return mBpfIngressMap;
|
||||
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));
|
||||
@@ -754,15 +795,15 @@ public class IpServerTest {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static TetherIngressKey makeIngressKey(int upstreamIfindex,
|
||||
private static TetherDownstream6Key makeDownstream6Key(int upstreamIfindex,
|
||||
@NonNull final InetAddress dst) {
|
||||
return new TetherIngressKey(upstreamIfindex, dst.getAddress());
|
||||
return new TetherDownstream6Key(upstreamIfindex, dst.getAddress());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static TetherIngressValue makeIngressValue(@NonNull final MacAddress dstMac) {
|
||||
return new TetherIngressValue(TEST_IFACE_PARAMS.index, dstMac, TEST_IFACE_PARAMS.macAddr,
|
||||
ETH_P_IPV6, NetworkStackConstants.ETHER_MTU);
|
||||
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) {
|
||||
@@ -776,8 +817,8 @@ public class IpServerTest {
|
||||
private void verifyTetherOffloadRuleAdd(@Nullable InOrder inOrder, int upstreamIfindex,
|
||||
@NonNull final InetAddress dst, @NonNull final MacAddress dstMac) throws Exception {
|
||||
if (mBpfDeps.isAtLeastS()) {
|
||||
verifyWithOrder(inOrder, mBpfIngressMap).updateEntry(
|
||||
makeIngressKey(upstreamIfindex, dst), makeIngressValue(dstMac));
|
||||
verifyWithOrder(inOrder, mBpfDownstream6Map).updateEntry(
|
||||
makeDownstream6Key(upstreamIfindex, dst), makeDownstream6Value(dstMac));
|
||||
} else {
|
||||
verifyWithOrder(inOrder, mNetd).tetherOffloadRuleAdd(matches(upstreamIfindex, dst,
|
||||
dstMac));
|
||||
@@ -787,8 +828,9 @@ public class IpServerTest {
|
||||
private void verifyNeverTetherOffloadRuleAdd(int upstreamIfindex,
|
||||
@NonNull final InetAddress dst, @NonNull final MacAddress dstMac) throws Exception {
|
||||
if (mBpfDeps.isAtLeastS()) {
|
||||
verify(mBpfIngressMap, never()).updateEntry(makeIngressKey(upstreamIfindex, dst),
|
||||
makeIngressValue(dstMac));
|
||||
verify(mBpfDownstream6Map, never()).updateEntry(
|
||||
makeDownstream6Key(upstreamIfindex, dst),
|
||||
makeDownstream6Value(dstMac));
|
||||
} else {
|
||||
verify(mNetd, never()).tetherOffloadRuleAdd(matches(upstreamIfindex, dst, dstMac));
|
||||
}
|
||||
@@ -796,12 +838,64 @@ public class IpServerTest {
|
||||
|
||||
private void verifyNeverTetherOffloadRuleAdd() throws Exception {
|
||||
if (mBpfDeps.isAtLeastS()) {
|
||||
verify(mBpfIngressMap, never()).updateEntry(any(), any());
|
||||
verify(mBpfDownstream6Map, never()).updateEntry(any(), any());
|
||||
} else {
|
||||
verify(mNetd, never()).tetherOffloadRuleAdd(any());
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyTetherOffloadRuleRemove(@Nullable InOrder inOrder, int upstreamIfindex,
|
||||
@NonNull final InetAddress dst, @NonNull final MacAddress dstMac) throws Exception {
|
||||
if (mBpfDeps.isAtLeastS()) {
|
||||
verifyWithOrder(inOrder, mBpfDownstream6Map).deleteEntry(makeDownstream6Key(
|
||||
upstreamIfindex, 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);
|
||||
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);
|
||||
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();
|
||||
@@ -810,12 +904,19 @@ public class IpServerTest {
|
||||
}
|
||||
|
||||
private void resetNetdBpfMapAndCoordinator() throws Exception {
|
||||
reset(mNetd, mBpfIngressMap, mBpfCoordinator);
|
||||
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
|
||||
@@ -826,7 +927,6 @@ public class IpServerTest {
|
||||
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");
|
||||
@@ -836,63 +936,71 @@ public class IpServerTest {
|
||||
final MacAddress macB = MacAddress.fromString("11:22:33:00:00:0b");
|
||||
|
||||
resetNetdBpfMapAndCoordinator();
|
||||
verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfIngressMap);
|
||||
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, mBpfIngressMap);
|
||||
verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
|
||||
|
||||
// Events on this interface are received and sent to netd.
|
||||
recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
|
||||
verify(mBpfCoordinator).tetherOffloadRuleAdd(
|
||||
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
|
||||
verifyTetherOffloadRuleAdd(null, UPSTREAM_IFINDEX, neighA, macA);
|
||||
verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
|
||||
resetNetdBpfMapAndCoordinator();
|
||||
|
||||
recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
|
||||
verify(mBpfCoordinator).tetherOffloadRuleAdd(
|
||||
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
|
||||
verifyTetherOffloadRuleAdd(null, UPSTREAM_IFINDEX, neighB, macB);
|
||||
verifyNoUpstreamIpv6ForwardingChange(null);
|
||||
resetNetdBpfMapAndCoordinator();
|
||||
|
||||
// Link-local and multicast neighbors are ignored.
|
||||
recvNewNeigh(myIfindex, neighLL, NUD_REACHABLE, macA);
|
||||
verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfIngressMap);
|
||||
verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
|
||||
recvNewNeigh(myIfindex, neighMC, NUD_REACHABLE, macA);
|
||||
verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfIngressMap);
|
||||
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));
|
||||
verifyTetherOffloadRuleRemove(null, UPSTREAM_IFINDEX, 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));
|
||||
verifyTetherOffloadRuleRemove(null, UPSTREAM_IFINDEX, neighB, macNull);
|
||||
verifyStopUpstreamIpv6Forwarding(null);
|
||||
resetNetdBpfMapAndCoordinator();
|
||||
|
||||
// Upstream changes result in updating the rules.
|
||||
recvNewNeigh(myIfindex, neighA, NUD_REACHABLE, macA);
|
||||
verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
|
||||
recvNewNeigh(myIfindex, neighB, NUD_REACHABLE, macB);
|
||||
resetNetdBpfMapAndCoordinator();
|
||||
|
||||
InOrder inOrder = inOrder(mNetd, mBpfIngressMap);
|
||||
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));
|
||||
verifyTetherOffloadRuleRemove(inOrder, UPSTREAM_IFINDEX, neighA, macA);
|
||||
verifyTetherOffloadRuleRemove(inOrder, UPSTREAM_IFINDEX, neighB, macB);
|
||||
verifyStopUpstreamIpv6Forwarding(inOrder);
|
||||
verifyTetherOffloadRuleAdd(inOrder, UPSTREAM_IFINDEX2, neighA, macA);
|
||||
inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighB, macB));
|
||||
verifyStartUpstreamIpv6Forwarding(inOrder, UPSTREAM_IFINDEX2);
|
||||
verifyTetherOffloadRuleAdd(inOrder, UPSTREAM_IFINDEX2, neighB, macB);
|
||||
verifyNoUpstreamIpv6ForwardingChange(inOrder);
|
||||
resetNetdBpfMapAndCoordinator();
|
||||
|
||||
// When the upstream is lost, rules are removed.
|
||||
@@ -902,8 +1010,9 @@ public class IpServerTest {
|
||||
// - 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));
|
||||
verifyTetherOffloadRuleRemove(null, UPSTREAM_IFINDEX2, neighA, macA);
|
||||
verifyTetherOffloadRuleRemove(null, UPSTREAM_IFINDEX2, neighB, macB);
|
||||
verifyStopUpstreamIpv6Forwarding(inOrder);
|
||||
resetNetdBpfMapAndCoordinator();
|
||||
|
||||
// If the upstream is IPv4-only, no rules are added.
|
||||
@@ -912,7 +1021,8 @@ public class IpServerTest {
|
||||
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, mBpfIngressMap);
|
||||
verifyNoUpstreamIpv6ForwardingChange(null);
|
||||
verifyNoMoreInteractions(mBpfCoordinator, mNetd, mBpfDownstream6Map, mBpfUpstream6Map);
|
||||
|
||||
// Rules can be added again once upstream IPv6 connectivity is available.
|
||||
lp.setInterfaceName(UPSTREAM_IFACE);
|
||||
@@ -921,6 +1031,7 @@ public class IpServerTest {
|
||||
verify(mBpfCoordinator).tetherOffloadRuleAdd(
|
||||
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
|
||||
verifyTetherOffloadRuleAdd(null, UPSTREAM_IFINDEX, neighB, macB);
|
||||
verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
|
||||
verify(mBpfCoordinator, never()).tetherOffloadRuleAdd(
|
||||
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
|
||||
verifyNeverTetherOffloadRuleAdd(UPSTREAM_IFINDEX, neighA, macA);
|
||||
@@ -929,7 +1040,8 @@ public class IpServerTest {
|
||||
resetNetdBpfMapAndCoordinator();
|
||||
dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0);
|
||||
verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer);
|
||||
verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighB, macB));
|
||||
verifyTetherOffloadRuleRemove(null, UPSTREAM_IFINDEX, neighB, macB);
|
||||
verifyStopUpstreamIpv6Forwarding(null);
|
||||
|
||||
// When the interface goes down, rules are removed.
|
||||
lp.setInterfaceName(UPSTREAM_IFACE);
|
||||
@@ -939,6 +1051,7 @@ public class IpServerTest {
|
||||
verify(mBpfCoordinator).tetherOffloadRuleAdd(
|
||||
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macA));
|
||||
verifyTetherOffloadRuleAdd(null, UPSTREAM_IFINDEX, neighA, macA);
|
||||
verifyStartUpstreamIpv6Forwarding(null, UPSTREAM_IFINDEX);
|
||||
verify(mBpfCoordinator).tetherOffloadRuleAdd(
|
||||
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macB));
|
||||
verifyTetherOffloadRuleAdd(null, UPSTREAM_IFINDEX, neighB, macB);
|
||||
@@ -947,8 +1060,9 @@ public class IpServerTest {
|
||||
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, neighA, macA);
|
||||
verifyTetherOffloadRuleRemove(null, UPSTREAM_IFINDEX, neighB, macB);
|
||||
verifyStopUpstreamIpv6Forwarding(null);
|
||||
verify(mIpNeighborMonitor).stop();
|
||||
resetNetdBpfMapAndCoordinator();
|
||||
}
|
||||
@@ -977,12 +1091,14 @@ public class IpServerTest {
|
||||
verify(mBpfCoordinator).tetherOffloadRuleAdd(
|
||||
mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neigh, macA));
|
||||
verifyTetherOffloadRuleAdd(null, UPSTREAM_IFINDEX, 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));
|
||||
verifyTetherOffloadRuleRemove(null, UPSTREAM_IFINDEX, neigh, macNull);
|
||||
verifyStopUpstreamIpv6Forwarding(null);
|
||||
resetNetdBpfMapAndCoordinator();
|
||||
|
||||
// [2] Disable BPF offload.
|
||||
@@ -994,11 +1110,13 @@ public class IpServerTest {
|
||||
recvNewNeigh(myIfindex, neigh, NUD_REACHABLE, macA);
|
||||
verify(mBpfCoordinator, never()).tetherOffloadRuleAdd(any(), any());
|
||||
verifyNeverTetherOffloadRuleAdd();
|
||||
verifyNoUpstreamIpv6ForwardingChange(null);
|
||||
resetNetdBpfMapAndCoordinator();
|
||||
|
||||
recvDelNeigh(myIfindex, neigh, NUD_STALE, macA);
|
||||
verify(mBpfCoordinator, never()).tetherOffloadRuleRemove(any(), any());
|
||||
verify(mNetd, never()).tetherOffloadRuleRemove(any());
|
||||
verifyNeverTetherOffloadRuleRemove();
|
||||
verifyNoUpstreamIpv6ForwardingChange(null);
|
||||
resetNetdBpfMapAndCoordinator();
|
||||
}
|
||||
|
||||
|
||||
@@ -56,11 +56,14 @@ 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.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;
|
||||
@@ -68,6 +71,7 @@ import androidx.test.filters.SmallTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.net.module.util.NetworkStackConstants;
|
||||
import com.android.net.module.util.Struct;
|
||||
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
|
||||
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
|
||||
import com.android.testutils.TestableNetworkStatsProviderCbBinder;
|
||||
@@ -85,7 +89,10 @@ 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
|
||||
@@ -97,11 +104,64 @@ public class BpfCoordinatorTest {
|
||||
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");
|
||||
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
|
||||
@Mock private NetworkStatsManager mStatsManager;
|
||||
@Mock private INetd mNetd;
|
||||
@Mock private IpServer mIpServer;
|
||||
@Mock private IpServer mIpServer2;
|
||||
@Mock private TetheringConfiguration mTetherConfig;
|
||||
@Mock private BpfMap<TetherIngressKey, TetherIngressValue> mBpfIngressMap;
|
||||
@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;
|
||||
@@ -109,6 +169,10 @@ public class BpfCoordinatorTest {
|
||||
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 =
|
||||
spy(new BpfCoordinator.Dependencies() {
|
||||
@NonNull
|
||||
@@ -136,9 +200,39 @@ public class BpfCoordinatorTest {
|
||||
return mTetherConfig;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public ConntrackMonitor getConntrackMonitor(ConntrackEventConsumer consumer) {
|
||||
return mConntrackMonitor;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public BpfMap<TetherIngressKey, TetherIngressValue> getBpfIngressMap() {
|
||||
return mBpfIngressMap;
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -190,14 +284,63 @@ public class BpfCoordinatorTest {
|
||||
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);
|
||||
@@ -206,11 +349,57 @@ public class BpfCoordinatorTest {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
int upstreamIfindex) throws Exception {
|
||||
if (!mDeps.isAtLeastS()) return;
|
||||
final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfIndex);
|
||||
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)
|
||||
throws Exception {
|
||||
if (!mDeps.isAtLeastS()) return;
|
||||
final TetherUpstream6Key key = new TetherUpstream6Key(downstreamIfIndex);
|
||||
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, mBpfIngressMap).updateEntry(
|
||||
rule.makeTetherIngressKey(), rule.makeTetherIngressValue());
|
||||
verifyWithOrder(inOrder, mBpfDownstream6Map).updateEntry(
|
||||
rule.makeTetherDownstream6Key(), rule.makeTether6Value());
|
||||
} else {
|
||||
verifyWithOrder(inOrder, mNetd).tetherOffloadRuleAdd(matches(rule));
|
||||
}
|
||||
@@ -218,14 +407,76 @@ public class BpfCoordinatorTest {
|
||||
|
||||
private void verifyNeverTetherOffloadRuleAdd() throws Exception {
|
||||
if (mDeps.isAtLeastS()) {
|
||||
verify(mBpfIngressMap, never()).updateEntry(any(), any());
|
||||
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(@Nullable 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(@Nullable 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 checkTetherOffloadRuleAdd(boolean usingApiS) throws Exception {
|
||||
private void checkTetherOffloadRuleAddAndRemove(boolean usingApiS) throws Exception {
|
||||
setupFunctioningNetdInterface();
|
||||
|
||||
// Replace Dependencies#isAtLeastS() for testing R and S+ BPF map apis. Note that |mDeps|
|
||||
@@ -238,21 +489,72 @@ public class BpfCoordinatorTest {
|
||||
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(null, 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 testTetherOffloadRuleAddSdkR() throws Exception {
|
||||
checkTetherOffloadRuleAdd(false /* R */);
|
||||
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 testTetherOffloadRuleAddAtLeastSdkS() throws Exception {
|
||||
checkTetherOffloadRuleAdd(true /* S+ */);
|
||||
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
|
||||
@@ -275,7 +577,7 @@ public class BpfCoordinatorTest {
|
||||
// [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)});
|
||||
|
||||
@@ -296,7 +598,7 @@ public class BpfCoordinatorTest {
|
||||
// [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)});
|
||||
|
||||
@@ -317,12 +619,12 @@ public class BpfCoordinatorTest {
|
||||
// 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
|
||||
@@ -342,16 +644,14 @@ public class BpfCoordinatorTest {
|
||||
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();
|
||||
@@ -409,11 +709,11 @@ public class BpfCoordinatorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRuleMakeTetherIngressKey() throws Exception {
|
||||
public void testRuleMakeTetherDownstream6Key() throws Exception {
|
||||
final Integer mobileIfIndex = 100;
|
||||
final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
|
||||
|
||||
final TetherIngressKey key = rule.makeTetherIngressKey();
|
||||
final TetherDownstream6Key key = rule.makeTetherDownstream6Key();
|
||||
assertEquals(key.iif, (long) mobileIfIndex);
|
||||
assertTrue(Arrays.equals(key.neigh6, NEIGH_A.getAddress()));
|
||||
// iif (4) + neigh6 (16) = 20.
|
||||
@@ -421,11 +721,11 @@ public class BpfCoordinatorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRuleMakeTetherIngressValue() throws Exception {
|
||||
public void testRuleMakeTether6Value() throws Exception {
|
||||
final Integer mobileIfIndex = 100;
|
||||
final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A);
|
||||
|
||||
final TetherIngressValue value = rule.makeTetherIngressValue();
|
||||
final Tether6Value value = rule.makeTether6Value();
|
||||
assertEquals(value.oif, DOWNSTREAM_IFINDEX);
|
||||
assertEquals(value.ethDstMac, MAC_A);
|
||||
assertEquals(value.ethSrcMac, DOWNSTREAM_MAC);
|
||||
@@ -449,10 +749,11 @@ public class BpfCoordinatorTest {
|
||||
// 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, mBpfIngressMap);
|
||||
final InOrder inOrder = inOrder(mNetd, mBpfDownstream6Map, mBpfLimitMap, mBpfStatsMap);
|
||||
coordinator.tetherOffloadRuleAdd(mIpServer, rule);
|
||||
verifyTetherOffloadRuleAdd(inOrder, rule);
|
||||
inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, QUOTA_UNLIMITED);
|
||||
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
|
||||
true /* isInit */);
|
||||
inOrder.verifyNoMoreInteractions();
|
||||
|
||||
// [2] Specific limit.
|
||||
@@ -460,7 +761,8 @@ public class BpfCoordinatorTest {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -490,35 +792,35 @@ public class BpfCoordinatorTest {
|
||||
// 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, mBpfIngressMap);
|
||||
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);
|
||||
verifyTetherOffloadRuleAdd(inOrder, ruleA);
|
||||
inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, limit);
|
||||
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, limit, true /* isInit */);
|
||||
inOrder.verifyNoMoreInteractions();
|
||||
|
||||
// Adding the second rule on current upstream does not send the quota to netd.
|
||||
final Ipv6ForwardingRule ruleB = buildTestForwardingRule(mobileIfIndex, NEIGH_B, MAC_B);
|
||||
coordinator.tetherOffloadRuleAdd(mIpServer, ruleB);
|
||||
verifyTetherOffloadRuleAdd(inOrder, ruleB);
|
||||
inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong());
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -535,7 +837,8 @@ public class BpfCoordinatorTest {
|
||||
coordinator.addUpstreamNameToLookupTable(ethIfIndex, ethIface);
|
||||
coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
|
||||
|
||||
final InOrder inOrder = inOrder(mNetd, mBpfIngressMap);
|
||||
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.
|
||||
@@ -553,8 +856,9 @@ public class BpfCoordinatorTest {
|
||||
|
||||
coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleA);
|
||||
verifyTetherOffloadRuleAdd(inOrder, ethernetRuleA);
|
||||
inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(ethIfIndex, QUOTA_UNLIMITED);
|
||||
|
||||
verifyTetherOffloadSetInterfaceQuota(inOrder, ethIfIndex, QUOTA_UNLIMITED,
|
||||
true /* isInit */);
|
||||
verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, ethIfIndex);
|
||||
coordinator.tetherOffloadRuleAdd(mIpServer, ethernetRuleB);
|
||||
verifyTetherOffloadRuleAdd(inOrder, ethernetRuleB);
|
||||
|
||||
@@ -563,26 +867,30 @@ public class BpfCoordinatorTest {
|
||||
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));
|
||||
verifyTetherOffloadRuleRemove(inOrder, ethernetRuleA);
|
||||
verifyTetherOffloadRuleRemove(inOrder, ethernetRuleB);
|
||||
verifyStopUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX);
|
||||
verifyTetherOffloadGetAndClearStats(inOrder, ethIfIndex);
|
||||
verifyTetherOffloadRuleAdd(inOrder, mobileRuleA);
|
||||
inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, QUOTA_UNLIMITED);
|
||||
inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ethernetRuleB));
|
||||
inOrder.verify(mNetd).tetherOffloadGetAndClearStats(ethIfIndex);
|
||||
verifyTetherOffloadSetInterfaceQuota(inOrder, mobileIfIndex, QUOTA_UNLIMITED,
|
||||
true /* isInit */);
|
||||
verifyStartUpstreamIpv6Forwarding(inOrder, DOWNSTREAM_IFINDEX, 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);
|
||||
verifyTetherOffloadGetAndClearStats(inOrder, mobileIfIndex);
|
||||
|
||||
// [4] Force pushing stats update to verify that the last diff of stats is reported on all
|
||||
// upstreams.
|
||||
@@ -599,14 +907,14 @@ public class BpfCoordinatorTest {
|
||||
private void checkBpfDisabled() throws Exception {
|
||||
// The caller may mock the global dependencies |mDeps| which is used in
|
||||
// #makeBpfCoordinator for testing.
|
||||
// See #testBpfDisabledbyNoBpfIngressMap.
|
||||
// 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";
|
||||
@@ -631,21 +939,21 @@ public class BpfCoordinatorTest {
|
||||
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());
|
||||
verifyNeverTetherOffloadRuleRemove();
|
||||
verifyNeverTetherOffloadRuleAdd();
|
||||
rules = coordinator.getForwardingRulesForTesting().get(mIpServer);
|
||||
assertNotNull(rules);
|
||||
@@ -662,9 +970,36 @@ public class BpfCoordinatorTest {
|
||||
|
||||
@Test
|
||||
@IgnoreUpTo(Build.VERSION_CODES.R)
|
||||
public void testBpfDisabledbyNoBpfIngressMap() throws Exception {
|
||||
public void testBpfDisabledbyNoBpfDownstream6Map() throws Exception {
|
||||
setupFunctioningNetdInterface();
|
||||
doReturn(null).when(mDeps).getBpfIngressMap();
|
||||
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 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();
|
||||
}
|
||||
@@ -703,18 +1038,62 @@ public class BpfCoordinatorTest {
|
||||
// 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
|
||||
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
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,6 +586,9 @@ public class TetheringTest {
|
||||
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) {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
// TODO: use a java_library in the bootclasspath instead
|
||||
filegroup {
|
||||
name: "framework-connectivity-sources",
|
||||
srcs: [
|
||||
"src/**/*.java",
|
||||
"src/**/*.aidl",
|
||||
],
|
||||
path: "src",
|
||||
visibility: [
|
||||
"//frameworks/base",
|
||||
"//packages/modules/Connectivity:__subpackages__",
|
||||
],
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* 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 perNmissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.connectivity.aidl;
|
||||
|
||||
import android.net.NattKeepalivePacketData;
|
||||
import android.net.TcpKeepalivePacketData;
|
||||
|
||||
import com.android.connectivity.aidl.INetworkAgentRegistry;
|
||||
|
||||
/**
|
||||
* Interface to notify NetworkAgent of connectivity events.
|
||||
* @hide
|
||||
*/
|
||||
oneway interface INetworkAgent {
|
||||
void onRegistered(in INetworkAgentRegistry registry);
|
||||
void onDisconnected();
|
||||
void onBandwidthUpdateRequested();
|
||||
void onValidationStatusChanged(int validationStatus,
|
||||
in @nullable String captivePortalUrl);
|
||||
void onSaveAcceptUnvalidated(boolean acceptUnvalidated);
|
||||
void onStartNattSocketKeepalive(int slot, int intervalDurationMs,
|
||||
in NattKeepalivePacketData packetData);
|
||||
void onStartTcpSocketKeepalive(int slot, int intervalDurationMs,
|
||||
in TcpKeepalivePacketData packetData);
|
||||
void onStopSocketKeepalive(int slot);
|
||||
void onSignalStrengthThresholdsUpdated(in int[] thresholds);
|
||||
void onPreventAutomaticReconnect();
|
||||
void onAddNattKeepalivePacketFilter(int slot,
|
||||
in NattKeepalivePacketData packetData);
|
||||
void onAddTcpKeepalivePacketFilter(int slot,
|
||||
in TcpKeepalivePacketData packetData);
|
||||
void onRemoveKeepalivePacketFilter(int slot);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* 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 perNmissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.connectivity.aidl;
|
||||
|
||||
import android.net.LinkProperties;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkInfo;
|
||||
|
||||
/**
|
||||
* Interface for NetworkAgents to send network network properties.
|
||||
* @hide
|
||||
*/
|
||||
oneway interface INetworkAgentRegistry {
|
||||
void sendNetworkCapabilities(in NetworkCapabilities nc);
|
||||
void sendLinkProperties(in LinkProperties lp);
|
||||
// TODO: consider replacing this by "markConnected()" and removing
|
||||
void sendNetworkInfo(in NetworkInfo info);
|
||||
void sendScore(int score);
|
||||
void sendExplicitlySelected(boolean explicitlySelected, boolean acceptPartial);
|
||||
void sendSocketKeepaliveEvent(int slot, int reason);
|
||||
void sendUnderlyingNetworks(in @nullable List<Network> networks);
|
||||
}
|
||||
@@ -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"],
|
||||
|
||||
18
tests/cts/hostside/TEST_MAPPING
Normal file
18
tests/cts/hostside/TEST_MAPPING
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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 class MeterednessConfigurationRule extends BeforeAfterRule {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,16 +17,13 @@
|
||||
package com.android.cts.net.hostside;
|
||||
|
||||
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
|
||||
|
||||
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.canChangeActiveNetworkMeteredness;
|
||||
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setRestrictBackground;
|
||||
import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isActiveNetworkMetered;
|
||||
import static com.android.cts.net.hostside.Property.BATTERY_SAVER_MODE;
|
||||
import static com.android.cts.net.hostside.Property.DATA_SAVER_MODE;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
@@ -186,7 +183,7 @@ public class NetworkCallbackTest extends AbstractRestrictBackgroundNetworkTestCa
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
assumeTrue(isActiveNetworkMetered(true) || canChangeActiveNetworkMeteredness());
|
||||
assumeTrue(canChangeActiveNetworkMeteredness());
|
||||
|
||||
registerBroadcastReceiver();
|
||||
|
||||
@@ -198,13 +195,13 @@ public class NetworkCallbackTest extends AbstractRestrictBackgroundNetworkTestCa
|
||||
setBatterySaverMode(false);
|
||||
setRestrictBackground(false);
|
||||
|
||||
// Make wifi a metered network.
|
||||
// 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.
|
||||
// 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,
|
||||
|
||||
@@ -20,6 +20,7 @@ import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLE
|
||||
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 com.android.compatibility.common.util.SystemUtil.runShellCommand;
|
||||
@@ -28,6 +29,7 @@ import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTest
|
||||
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;
|
||||
|
||||
@@ -40,25 +42,36 @@ import android.net.ConnectivityManager.NetworkCallback;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.PersistableBundle;
|
||||
import android.os.Process;
|
||||
import android.telephony.CarrierConfigManager;
|
||||
import android.telephony.SubscriptionManager;
|
||||
import android.telephony.data.ApnSetting;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
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.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
|
||||
public class NetworkPolicyTestUtils {
|
||||
|
||||
// android.telephony.CarrierConfigManager.KEY_CARRIER_METERED_APN_TYPES_STRINGS
|
||||
// TODO: Expose it as a @TestApi instead of copying the constant
|
||||
private static final String KEY_CARRIER_METERED_APN_TYPES_STRINGS =
|
||||
"carrier_metered_apn_types_strings";
|
||||
|
||||
private static final int TIMEOUT_CHANGE_METEREDNESS_MS = 10_000;
|
||||
|
||||
private static ConnectivityManager mCm;
|
||||
private static WifiManager mWm;
|
||||
private static CarrierConfigManager mCarrierConfigManager;
|
||||
|
||||
private static Boolean mBatterySaverSupported;
|
||||
private static Boolean mDataSaverSupported;
|
||||
@@ -135,16 +148,40 @@ public class NetworkPolicyTestUtils {
|
||||
}
|
||||
|
||||
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 NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
|
||||
if (networkCapabilities.hasTransport(TRANSPORT_WIFI)) {
|
||||
final String ssid = getWifiSsid();
|
||||
setWifiMeteredStatus(ssid, metered);
|
||||
return () -> setWifiMeteredStatus(ssid, !metered);
|
||||
} else if (networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
|
||||
final int subId = SubscriptionManager.getActiveDataSubscriptionId();
|
||||
setCellularMeteredStatus(subId, metered);
|
||||
return () -> setCellularMeteredStatus(subId, !metered);
|
||||
} else {
|
||||
// Right now, we don't have a way to change meteredness of networks other
|
||||
// than Wi-Fi or Cellular, so just throw an exception.
|
||||
throw new IllegalStateException("Can't change meteredness of current active network");
|
||||
}
|
||||
}
|
||||
|
||||
private static String getWifiSsid() {
|
||||
final boolean isLocationEnabled = isLocationEnabled();
|
||||
try {
|
||||
if (!isLocationEnabled) {
|
||||
@@ -152,8 +189,7 @@ public class NetworkPolicyTestUtils {
|
||||
}
|
||||
final String ssid = unquoteSSID(getWifiManager().getConnectionInfo().getSSID());
|
||||
assertNotEquals(WifiManager.UNKNOWN_SSID, ssid);
|
||||
setWifiMeteredStatus(ssid, metered);
|
||||
return Pair.create(ssid, !metered);
|
||||
return ssid;
|
||||
} finally {
|
||||
// Reset the location enabled state
|
||||
if (!isLocationEnabled) {
|
||||
@@ -162,11 +198,13 @@ public class NetworkPolicyTestUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void resetMeteredNetwork(String ssid, boolean metered) throws Exception {
|
||||
setWifiMeteredStatus(ssid, metered);
|
||||
private static NetworkCapabilities getActiveNetworkCapabilities() {
|
||||
final Network activeNetwork = getConnectivityManager().getActiveNetwork();
|
||||
assertNotNull("No active network available", activeNetwork);
|
||||
return getConnectivityManager().getNetworkCapabilities(activeNetwork);
|
||||
}
|
||||
|
||||
public static void setWifiMeteredStatus(String ssid, boolean metered) throws Exception {
|
||||
private 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);
|
||||
@@ -174,15 +212,24 @@ public class NetworkPolicyTestUtils {
|
||||
assertActiveNetworkMetered(metered);
|
||||
}
|
||||
|
||||
public static void assertWifiMeteredStatus(String ssid, boolean expectedMeteredStatus) {
|
||||
private 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 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);
|
||||
}
|
||||
|
||||
// Copied from cts/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java
|
||||
public static void assertActiveNetworkMetered(boolean expectedMeteredStatus) throws Exception {
|
||||
private static void assertActiveNetworkMetered(boolean expectedMeteredStatus) throws Exception {
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
final NetworkCallback networkCallback = new NetworkCallback() {
|
||||
@Override
|
||||
@@ -197,12 +244,15 @@ public class NetworkPolicyTestUtils {
|
||||
// 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)) {
|
||||
fail("Timed out waiting for active network metered status to change to "
|
||||
+ expectedMeteredStatus + "; network = "
|
||||
+ getConnectivityManager().getActiveNetwork());
|
||||
}
|
||||
} finally {
|
||||
getConnectivityManager().unregisterNetworkCallback(networkCallback);
|
||||
}
|
||||
getConnectivityManager().unregisterNetworkCallback(networkCallback);
|
||||
}
|
||||
|
||||
public static void setRestrictBackground(boolean enabled) {
|
||||
@@ -274,6 +324,14 @@ public class NetworkPolicyTestUtils {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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_ICMPV6;
|
||||
import static android.system.OsConstants.IPPROTO_TCP;
|
||||
import static android.system.OsConstants.POLLIN;
|
||||
import static android.system.OsConstants.SOCK_DGRAM;
|
||||
import static android.test.MoreAsserts.assertNotEqual;
|
||||
|
||||
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.app.DownloadManager;
|
||||
@@ -45,9 +50,14 @@ import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkRequest;
|
||||
import android.net.Proxy;
|
||||
import android.net.ProxyInfo;
|
||||
import android.net.TransportInfo;
|
||||
import android.net.Uri;
|
||||
import android.net.VpnManager;
|
||||
import android.net.VpnService;
|
||||
import android.net.VpnTransportInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.Process;
|
||||
import android.os.SystemProperties;
|
||||
@@ -687,6 +697,34 @@ public class VpnTest extends InstrumentationTestCase {
|
||||
setAndVerifyPrivateDns(initialMode);
|
||||
}
|
||||
|
||||
private class NeverChangeNetworkCallback extends NetworkCallback {
|
||||
private CountDownLatch mLatch = new CountDownLatch(1);
|
||||
private volatile Network mFirstNetwork;
|
||||
private volatile Network mOtherNetwork;
|
||||
|
||||
public void onAvailable(Network n) {
|
||||
// Don't assert here, as it crashes the test with a hard to debug message.
|
||||
if (mFirstNetwork == null) {
|
||||
mFirstNetwork = n;
|
||||
mLatch.countDown();
|
||||
} else if (mOtherNetwork == null) {
|
||||
mOtherNetwork = n;
|
||||
}
|
||||
}
|
||||
|
||||
public Network getFirstNetwork() throws Exception {
|
||||
assertTrue(
|
||||
"System default callback got no network after " + TIMEOUT_MS + "ms. "
|
||||
+ "Please ensure the device has a working Internet connection.",
|
||||
mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
|
||||
return mFirstNetwork;
|
||||
}
|
||||
|
||||
public void assertNeverChanged() {
|
||||
assertNull(mOtherNetwork);
|
||||
}
|
||||
}
|
||||
|
||||
public void testDefault() throws Exception {
|
||||
if (!supportedHardware()) return;
|
||||
// If adb TCP port opened, this test may running by adb over network.
|
||||
@@ -702,6 +740,14 @@ public class VpnTest extends InstrumentationTestCase {
|
||||
getInstrumentation().getTargetContext(), MyVpnService.ACTION_ESTABLISHED);
|
||||
receiver.register();
|
||||
|
||||
|
||||
// Expect the system default network not to change.
|
||||
final NeverChangeNetworkCallback neverChangeCallback = new NeverChangeNetworkCallback();
|
||||
final Network defaultNetwork = mCM.getActiveNetwork();
|
||||
runWithShellPermissionIdentity(() ->
|
||||
mCM.registerSystemDefaultNetworkCallback(neverChangeCallback,
|
||||
new Handler(Looper.getMainLooper())), NETWORK_SETTINGS);
|
||||
|
||||
FileDescriptor fd = openSocketFdInOtherApp(TEST_HOST, 80, TIMEOUT_MS);
|
||||
|
||||
startVpn(new String[] {"192.0.2.2/32", "2001:db8:1:2::ffe/128"},
|
||||
@@ -719,6 +765,20 @@ public class VpnTest extends InstrumentationTestCase {
|
||||
|
||||
checkTrafficOnVpn();
|
||||
|
||||
expectVpnTransportInfo(mCM.getActiveNetwork());
|
||||
|
||||
// Check that system default network callback has not seen any network changes, even though
|
||||
// the app's default network changed. 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 cause the default network to switch (e.g.,
|
||||
// from wifi to cellular).
|
||||
assertEquals(defaultNetwork, neverChangeCallback.getFirstNetwork());
|
||||
assertNotEqual(defaultNetwork, mCM.getActiveNetwork());
|
||||
neverChangeCallback.assertNeverChanged();
|
||||
runWithShellPermissionIdentity(
|
||||
() -> mCM.unregisterNetworkCallback(neverChangeCallback),
|
||||
NETWORK_SETTINGS);
|
||||
|
||||
checkStrictModePrivateDns();
|
||||
|
||||
receiver.unregisterQuietly();
|
||||
@@ -739,6 +799,8 @@ public class VpnTest extends InstrumentationTestCase {
|
||||
|
||||
checkTrafficOnVpn();
|
||||
|
||||
expectVpnTransportInfo(mCM.getActiveNetwork());
|
||||
|
||||
checkStrictModePrivateDns();
|
||||
}
|
||||
|
||||
@@ -764,6 +826,10 @@ public class VpnTest extends InstrumentationTestCase {
|
||||
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 +844,11 @@ public class VpnTest extends InstrumentationTestCase {
|
||||
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 +987,8 @@ public class VpnTest extends InstrumentationTestCase {
|
||||
// VPN with no underlying networks should be metered by default.
|
||||
assertTrue(isNetworkMetered(mNetwork));
|
||||
assertTrue(mCM.isActiveNetworkMetered());
|
||||
|
||||
expectVpnTransportInfo(mCM.getActiveNetwork());
|
||||
}
|
||||
|
||||
public void testVpnMeterednessWithNullUnderlyingNetwork() throws Exception {
|
||||
@@ -944,6 +1015,8 @@ public class VpnTest extends InstrumentationTestCase {
|
||||
assertEquals(isNetworkMetered(underlyingNetwork), isNetworkMetered(mNetwork));
|
||||
// Meteredness based on VPN capabilities and CM#isActiveNetworkMetered should be in sync.
|
||||
assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered());
|
||||
|
||||
expectVpnTransportInfo(mCM.getActiveNetwork());
|
||||
}
|
||||
|
||||
public void testVpnMeterednessWithNonNullUnderlyingNetwork() throws Exception {
|
||||
@@ -971,6 +1044,8 @@ public class VpnTest extends InstrumentationTestCase {
|
||||
assertEquals(isNetworkMetered(underlyingNetwork), isNetworkMetered(mNetwork));
|
||||
// Meteredness based on VPN capabilities and CM#isActiveNetworkMetered should be in sync.
|
||||
assertEquals(isNetworkMetered(mNetwork), mCM.isActiveNetworkMetered());
|
||||
|
||||
expectVpnTransportInfo(mCM.getActiveNetwork());
|
||||
}
|
||||
|
||||
public void testAlwaysMeteredVpnWithNullUnderlyingNetwork() throws Exception {
|
||||
@@ -995,6 +1070,8 @@ public class VpnTest extends InstrumentationTestCase {
|
||||
// VPN's meteredness does not depend on underlying network since it is always metered.
|
||||
assertTrue(isNetworkMetered(mNetwork));
|
||||
assertTrue(mCM.isActiveNetworkMetered());
|
||||
|
||||
expectVpnTransportInfo(mCM.getActiveNetwork());
|
||||
}
|
||||
|
||||
public void testAlwaysMeteredVpnWithNonNullUnderlyingNetwork() throws Exception {
|
||||
@@ -1020,6 +1097,8 @@ public class VpnTest extends InstrumentationTestCase {
|
||||
// VPN's meteredness does not depend on underlying network since it is always metered.
|
||||
assertTrue(isNetworkMetered(mNetwork));
|
||||
assertTrue(mCM.isActiveNetworkMetered());
|
||||
|
||||
expectVpnTransportInfo(mCM.getActiveNetwork());
|
||||
}
|
||||
|
||||
public void testB141603906() throws Exception {
|
||||
@@ -1069,6 +1148,14 @@ public class VpnTest extends InstrumentationTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
private void expectVpnTransportInfo(Network network) {
|
||||
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();
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -138,7 +138,7 @@ public class MyService extends Service {
|
||||
}
|
||||
}
|
||||
};
|
||||
mCm.registerNetworkCallback(makeWifiNetworkRequest(), mNetworkCallback);
|
||||
mCm.registerNetworkCallback(makeNetworkRequest(), mNetworkCallback);
|
||||
try {
|
||||
cb.asBinder().linkToDeath(() -> unregisterNetworkCallback(), 0);
|
||||
} catch (RemoteException e) {
|
||||
@@ -156,9 +156,8 @@ public class MyService extends Service {
|
||||
}
|
||||
};
|
||||
|
||||
private NetworkRequest makeWifiNetworkRequest() {
|
||||
private NetworkRequest makeNetworkRequest() {
|
||||
return new NetworkRequest.Builder()
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
package {
|
||||
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||
}
|
||||
|
||||
android_app_certificate {
|
||||
name: "cts-net-app",
|
||||
certificate: "cts-net-app",
|
||||
|
||||
@@ -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"],
|
||||
@@ -44,6 +48,7 @@ java_defaults {
|
||||
"ctstestrunner-axt",
|
||||
"junit",
|
||||
"junit-params",
|
||||
"modules-utils-build",
|
||||
"net-utils-framework-common",
|
||||
"truth-prebuilt",
|
||||
],
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
package {
|
||||
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||
}
|
||||
|
||||
cc_defaults {
|
||||
name: "dns_async_defaults",
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
|
||||
// Build the unit tests.
|
||||
|
||||
package {
|
||||
default_applicable_licenses: ["Android-Apache-2.0"],
|
||||
}
|
||||
|
||||
cc_test {
|
||||
name: "CtsNativeNetTestCases",
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ import android.net.ConnectivityManager.NetworkCallback
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
|
||||
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
|
||||
import android.net.NetworkCapabilities.TRANSPORT_WIFI
|
||||
import android.net.NetworkRequest
|
||||
import android.net.Uri
|
||||
@@ -44,8 +47,10 @@ import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
|
||||
import android.text.TextUtils
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import com.android.testutils.RecorderCallback
|
||||
import com.android.testutils.TestHttpServer
|
||||
import com.android.testutils.TestHttpServer.Request
|
||||
import com.android.testutils.TestableNetworkCallback
|
||||
import com.android.testutils.isDevSdkInRange
|
||||
import com.android.testutils.runAsShell
|
||||
import fi.iki.elonen.NanoHTTPD.Response.Status
|
||||
@@ -124,7 +129,20 @@ class CaptivePortalTest {
|
||||
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 @@ class CaptivePortalTest {
|
||||
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>()
|
||||
|
||||
@@ -25,10 +25,12 @@ import static android.content.pm.PackageManager.FEATURE_USB_HOST;
|
||||
import static android.content.pm.PackageManager.FEATURE_WIFI;
|
||||
import static android.content.pm.PackageManager.GET_PERMISSIONS;
|
||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||
import static android.net.NetworkCapabilities.NET_CAPABILITY_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 +45,7 @@ import static android.system.OsConstants.AF_UNSPEC;
|
||||
|
||||
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
|
||||
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
|
||||
import static com.android.testutils.MiscAsserts.assertThrows;
|
||||
import static com.android.testutils.TestPermissionUtil.runAsShell;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -68,8 +71,10 @@ import android.content.pm.PackageManager;
|
||||
import android.content.res.Resources;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.ConnectivityManager.NetworkCallback;
|
||||
import android.net.InetAddresses;
|
||||
import android.net.IpSecManager;
|
||||
import android.net.IpSecManager.UdpEncapsulationSocket;
|
||||
import android.net.LinkAddress;
|
||||
import android.net.LinkProperties;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
@@ -80,11 +85,15 @@ import android.net.NetworkInfo.State;
|
||||
import android.net.NetworkRequest;
|
||||
import android.net.NetworkUtils;
|
||||
import android.net.SocketKeepalive;
|
||||
import android.net.StringNetworkSpecifier;
|
||||
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.SystemClock;
|
||||
@@ -100,16 +109,20 @@ 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.testutils.DevSdkIgnoreRule;
|
||||
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
|
||||
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;
|
||||
|
||||
@@ -142,6 +155,8 @@ import java.util.regex.Pattern;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ConnectivityManagerTest {
|
||||
@Rule
|
||||
public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
|
||||
|
||||
private static final String TAG = ConnectivityManagerTest.class.getSimpleName();
|
||||
|
||||
@@ -154,9 +169,7 @@ public class ConnectivityManagerTest {
|
||||
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 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,6 +189,9 @@ public class ConnectivityManagerTest {
|
||||
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;
|
||||
@@ -183,7 +199,6 @@ public class ConnectivityManagerTest {
|
||||
private PackageManager mPackageManager;
|
||||
private final HashMap<Integer, NetworkConfig> mNetworks =
|
||||
new HashMap<Integer, NetworkConfig>();
|
||||
boolean mWifiWasDisabled;
|
||||
private UiAutomation mUiAutomation;
|
||||
private CtsNetUtils mCtsNetUtils;
|
||||
|
||||
@@ -195,7 +210,6 @@ public class ConnectivityManagerTest {
|
||||
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");
|
||||
@@ -218,10 +232,7 @@ public class ConnectivityManagerTest {
|
||||
|
||||
@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();
|
||||
}
|
||||
@@ -237,17 +248,6 @@ public class ConnectivityManagerTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
@@ -521,25 +521,43 @@ public class ConnectivityManagerTest {
|
||||
final TestNetworkCallback defaultTrackingCallback = new TestNetworkCallback();
|
||||
mCm.registerDefaultNetworkCallback(defaultTrackingCallback);
|
||||
|
||||
final TestNetworkCallback systemDefaultTrackingCallback = new TestNetworkCallback();
|
||||
if (SdkLevel.isAtLeastS()) {
|
||||
runWithShellPermissionIdentity(() ->
|
||||
mCm.registerSystemDefaultNetworkCallback(systemDefaultTrackingCallback,
|
||||
new Handler(Looper.getMainLooper())),
|
||||
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",
|
||||
assertNotNull("Did not receive onAvailable on default network callback",
|
||||
defaultTrackingCallback.waitForAvailable());
|
||||
|
||||
if (SdkLevel.isAtLeastS()) {
|
||||
assertNotNull("Did not receive onAvailable on system default network callback",
|
||||
systemDefaultTrackingCallback.waitForAvailable());
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
fail("Broadcast receiver or NetworkCallback wait was interrupted.");
|
||||
} finally {
|
||||
mCm.unregisterNetworkCallback(callback);
|
||||
mCm.unregisterNetworkCallback(defaultTrackingCallback);
|
||||
if (SdkLevel.isAtLeastS()) {
|
||||
runWithShellPermissionIdentity(
|
||||
() -> mCm.unregisterNetworkCallback(systemDefaultTrackingCallback),
|
||||
NETWORK_SETTINGS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,23 +576,29 @@ public class ConnectivityManagerTest {
|
||||
|
||||
// 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 +679,17 @@ public class ConnectivityManagerTest {
|
||||
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 +761,9 @@ public class ConnectivityManagerTest {
|
||||
// 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 +819,7 @@ public class ConnectivityManagerTest {
|
||||
@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 +833,7 @@ public class ConnectivityManagerTest {
|
||||
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 +1047,7 @@ public class ConnectivityManagerTest {
|
||||
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 +1067,7 @@ public class ConnectivityManagerTest {
|
||||
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 +1278,7 @@ public class ConnectivityManagerTest {
|
||||
return;
|
||||
}
|
||||
|
||||
final Network network = ensureWifiConnected();
|
||||
final Network network = mCtsNetUtils.ensureWifiConnected();
|
||||
final int supported = getSupportedKeepalivesForNet(network);
|
||||
if (supported == 0) {
|
||||
return;
|
||||
@@ -1338,7 +1375,7 @@ public class ConnectivityManagerTest {
|
||||
return;
|
||||
}
|
||||
|
||||
final Network network = ensureWifiConnected();
|
||||
final Network network = mCtsNetUtils.ensureWifiConnected();
|
||||
final int supported = getSupportedKeepalivesForNet(network);
|
||||
if (supported == 0) {
|
||||
return;
|
||||
@@ -1386,7 +1423,7 @@ public class ConnectivityManagerTest {
|
||||
|
||||
// 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 +1516,12 @@ public class ConnectivityManagerTest {
|
||||
}
|
||||
|
||||
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 +1568,72 @@ public class ConnectivityManagerTest {
|
||||
throw new AssertionFailedError("Captive portal server URL is invalid: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify background request can only be requested when acquiring
|
||||
* {@link android.Manifest.permission.NETWORK_SETTINGS}.
|
||||
*/
|
||||
@Test
|
||||
@IgnoreUpTo(Build.VERSION_CODES.R)
|
||||
public void testRequestBackgroundNetwork() throws Exception {
|
||||
// Create a tun interface. Use the returned interface name as the specifier to create
|
||||
// a test network request.
|
||||
final TestNetworkInterface testNetworkInterface = runWithShellPermissionIdentity(() -> {
|
||||
final TestNetworkManager tnm =
|
||||
mContext.getSystemService(TestNetworkManager.class);
|
||||
return 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(
|
||||
new StringNetworkSpecifier(testNetworkInterface.getInterfaceName()))
|
||||
.build();
|
||||
|
||||
// Verify background network cannot be requested without NETWORK_SETTINGS permission.
|
||||
final TestableNetworkCallback callback = new TestableNetworkCallback();
|
||||
assertThrows(SecurityException.class,
|
||||
() -> mCm.requestBackgroundNetwork(testRequest, null, callback));
|
||||
|
||||
try {
|
||||
// Request background test network via Shell identity which has NETWORK_SETTINGS
|
||||
// permission granted.
|
||||
runWithShellPermissionIdentity(
|
||||
() -> mCm.requestBackgroundNetwork(testRequest, null, callback),
|
||||
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(() -> {
|
||||
final TestNetworkManager tnm =
|
||||
mContext.getSystemService(TestNetworkManager.class);
|
||||
tnm.setupTestNetwork(testNetworkInterface.getInterfaceName(), binder);
|
||||
}, android.Manifest.permission.MANAGE_TEST_NETWORKS,
|
||||
android.Manifest.permission.NETWORK_SETTINGS);
|
||||
waitForAvailable(callback);
|
||||
final Network 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 {
|
||||
mCm.unregisterNetworkCallback(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ import android.platform.test.annotations.AppModeFull;
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
|
||||
import com.android.internal.util.HexDump;
|
||||
import com.android.org.bouncycastle.x509.X509V1CertificateGenerator;
|
||||
import com.android.internal.org.bouncycastle.x509.X509V1CertificateGenerator;
|
||||
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
|
||||
import com.android.testutils.DevSdkIgnoreRunner;
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ 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
|
||||
@@ -63,19 +65,17 @@ import android.os.Looper
|
||||
import android.os.Message
|
||||
import android.util.DebugUtils.valueToString
|
||||
import androidx.test.InstrumentationRegistry
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import com.android.connectivity.aidl.INetworkAgent
|
||||
import com.android.connectivity.aidl.INetworkAgentRegistry
|
||||
import com.android.net.module.util.ArrayTrackRecord
|
||||
import com.android.testutils.DevSdkIgnoreRule
|
||||
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.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
@@ -123,11 +123,11 @@ private fun Message(what: Int, arg1: Int, arg2: Int, obj: Any?) = Message.obtain
|
||||
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.R)
|
||||
|
||||
private val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
|
||||
private val REMOTE_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.2")
|
||||
|
||||
@@ -543,7 +543,7 @@ class NetworkAgentTest {
|
||||
|
||||
@Test
|
||||
@IgnoreUpTo(Build.VERSION_CODES.R)
|
||||
fun testSetUnderlyingNetworks() {
|
||||
fun testSetUnderlyingNetworksAndVpnSpecifier() {
|
||||
val request = NetworkRequest.Builder()
|
||||
.addTransportType(TRANSPORT_TEST)
|
||||
.addTransportType(TRANSPORT_VPN)
|
||||
@@ -557,6 +557,7 @@ class NetworkAgentTest {
|
||||
addTransportType(TRANSPORT_TEST)
|
||||
addTransportType(TRANSPORT_VPN)
|
||||
removeCapability(NET_CAPABILITY_NOT_VPN)
|
||||
setTransportInfo(VpnTransportInfo(VpnManager.TYPE_VPN_SERVICE))
|
||||
}
|
||||
val defaultNetwork = mCM.activeNetwork
|
||||
assertNotNull(defaultNetwork)
|
||||
@@ -571,6 +572,8 @@ class NetworkAgentTest {
|
||||
// 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)
|
||||
|
||||
val testAndVpn = intArrayOf(TRANSPORT_TEST, TRANSPORT_VPN)
|
||||
assertTrue(hasAllTransports(vpnNc, testAndVpn))
|
||||
|
||||
@@ -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 @@ java_library {
|
||||
"junit",
|
||||
"net-tests-utils",
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ public final class CtsNetUtils {
|
||||
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 @@ public final class CtsNetUtils {
|
||||
}
|
||||
// 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 @@ public final class CtsNetUtils {
|
||||
mContext.unregisterReceiver(receiver);
|
||||
}
|
||||
|
||||
assertTrue(err, connected);
|
||||
assertTrue(err + ": CONNECTIVITY_ACTION not received", connected);
|
||||
return wifiNetwork;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user