diff --git a/OWNERS b/OWNERS index 0e1e65df81..22b5561933 100644 --- a/OWNERS +++ b/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 diff --git a/Tethering/Android.bp b/Tethering/Android.bp index 7427481172..742fd02bb0 100644 --- a/Tethering/Android.bp +++ b/Tethering/Android.bp @@ -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", + ], +} diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp index c99121cae7..e9deeff42d 100644 --- a/Tethering/apex/Android.bp +++ b/Tethering/apex/Android.bp @@ -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", diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java index 5cf0384de7..4e615a197e 100644 --- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java +++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java @@ -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 tetherOffloadGetStats() { + final TetherStatsParcel[] tetherStatsList; + try { + // The reported tether stats are total data usage for all currently-active upstream + // interfaces since tethering start. There will only ever be one entry for a given + // interface index. + tetherStatsList = mNetd.tetherOffloadGetStats(); + } catch (RemoteException | ServiceSpecificException e) { + mLog.e("Fail to fetch tethering stats from netd: " + e); + return null; + } + + return toTetherStatsValueSparseArray(tetherStatsList); + } + + @Override + public boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes) { + try { + mNetd.tetherOffloadSetInterfaceQuota(ifIndex, quotaBytes); + } catch (RemoteException | ServiceSpecificException e) { + mLog.e("Exception when updating quota " + quotaBytes + ": ", e); + return false; + } + return true; + } + + @NonNull + private SparseArray toTetherStatsValueSparseArray( + @NonNull final TetherStatsParcel[] parcels) { + final SparseArray tetherStatsList = new SparseArray(); + + for (TetherStatsParcel p : parcels) { + tetherStatsList.put(p.ifIndex, new TetherStatsValue(p.rxPackets, p.rxBytes, + 0 /* rxErrors */, p.txPackets, p.txBytes, 0 /* txErrors */)); + } + + return tetherStatsList; + } + + @Override + @Nullable + public TetherStatsValue tetherOffloadGetAndClearStats(int ifIndex) { + try { + final TetherStatsParcel stats = + mNetd.tetherOffloadGetAndClearStats(ifIndex); + return new TetherStatsValue(stats.rxPackets, stats.rxBytes, 0 /* rxErrors */, + stats.txPackets, stats.txBytes, 0 /* txErrors */); + } catch (RemoteException | ServiceSpecificException e) { + mLog.e("Exception when cleanup tether stats for upstream index " + + ifIndex + ": ", e); + return null; + } + } + + @Override + public boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key, + @NonNull Tether4Value value) { + /* no op */ + return true; + } + + @Override + public boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key) { + /* no op */ + return true; + } + @Override public String toString() { return "Netd used"; diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java index 03616cab1c..4dc1c51f68 100644 --- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java +++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java @@ -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 mBpfIngressMap; + private final BpfMap mBpfDownstream4Map; + + // BPF map for upstream IPv4 forwarding. + @Nullable + private final BpfMap mBpfUpstream4Map; + + // BPF map for downstream IPv6 forwarding. + @Nullable + private final BpfMap mBpfDownstream6Map; + + // BPF map for upstream IPv6 forwarding. + @Nullable + private final BpfMap mBpfUpstream6Map; + + // BPF map of tethering statistics of the upstream interface since tethering startup. + @Nullable + private final BpfMap mBpfStatsMap; + + // BPF map of per-interface quota for tethering offload. + @Nullable + private final BpfMap mBpfLimitMap; 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 tetherOffloadGetStats() { + if (!isInitialized()) return null; + + final SparseArray tetherStatsList = new SparseArray(); + try { + // The reported tether stats are total data usage for all currently-active upstream + // interfaces since tethering start. + mBpfStatsMap.forEach((key, value) -> tetherStatsList.put((int) key.ifindex, value)); + } catch (ErrnoException e) { + mLog.e("Fail to fetch tethering stats from BPF map: ", e); + return null; + } + return tetherStatsList; + } + + @Override + public boolean tetherOffloadSetInterfaceQuota(int ifIndex, long quotaBytes) { + if (!isInitialized()) return false; + + // The common case is an update, where the stats already exist, + // hence we read first, even though writing with BPF_NOEXIST + // first would make the code simpler. + long rxBytes, txBytes; + TetherStatsValue statsValue = null; + + try { + statsValue = mBpfStatsMap.getValue(new TetherStatsKey(ifIndex)); + } catch (ErrnoException e) { + // The BpfMap#getValue doesn't throw an errno ENOENT exception. Catch other error + // while trying to get stats entry. + mLog.e("Could not get stats entry of interface index " + ifIndex + ": ", e); + return false; + } + + if (statsValue != null) { + // Ok, there was a stats entry. + rxBytes = statsValue.rxBytes; + txBytes = statsValue.txBytes; + } else { + // No stats entry - create one with zeroes. + try { + // This function is the *only* thing that can create entries. + // BpfMap#insertEntry use BPF_NOEXIST to create the entry. The entry is created + // if and only if it doesn't exist. + mBpfStatsMap.insertEntry(new TetherStatsKey(ifIndex), new TetherStatsValue( + 0 /* rxPackets */, 0 /* rxBytes */, 0 /* rxErrors */, 0 /* txPackets */, + 0 /* txBytes */, 0 /* txErrors */)); + } catch (ErrnoException | IllegalArgumentException e) { + mLog.e("Could not create stats entry: ", e); + return false; + } + rxBytes = 0; + txBytes = 0; + } + + // rxBytes + txBytes won't overflow even at 5gbps for ~936 years. + long newLimit = rxBytes + txBytes + quotaBytes; + + // if adding limit (e.g., if limit is QUOTA_UNLIMITED) caused overflow: clamp to 'infinity' + if (newLimit < rxBytes + txBytes) newLimit = QUOTA_UNLIMITED; + + try { + mBpfLimitMap.updateEntry(new TetherLimitKey(ifIndex), new TetherLimitValue(newLimit)); + } catch (ErrnoException e) { + mLog.e("Fail to set quota " + quotaBytes + " for interface index " + ifIndex + ": ", e); + return false; + } + + return true; + } + + @Override + @Nullable + public TetherStatsValue tetherOffloadGetAndClearStats(int ifIndex) { + if (!isInitialized()) return null; + + // getAndClearTetherOffloadStats is called after all offload rules have already been + // deleted for the given upstream interface. Before starting to do cleanup stuff in this + // function, use synchronizeKernelRCU to make sure that all the current running eBPF + // programs are finished on all CPUs, especially the unfinished packet processing. After + // synchronizeKernelRCU returned, we can safely read or delete on the stats map or the + // limit map. + final int res = synchronizeKernelRCU(); + if (res != 0) { + // Error log but don't return. Do as much cleanup as possible. + mLog.e("synchronize_rcu() failed: " + res); + } + + TetherStatsValue statsValue = null; + try { + statsValue = mBpfStatsMap.getValue(new TetherStatsKey(ifIndex)); + } catch (ErrnoException e) { + mLog.e("Could not get stats entry for interface index " + ifIndex + ": ", e); + return null; + } + + if (statsValue == null) { + mLog.e("Could not get stats entry for interface index " + ifIndex); + return null; + } + + try { + mBpfStatsMap.deleteEntry(new TetherStatsKey(ifIndex)); + } catch (ErrnoException e) { + mLog.e("Could not delete stats entry for interface index " + ifIndex + ": ", e); + return null; + } + + try { + mBpfLimitMap.deleteEntry(new TetherLimitKey(ifIndex)); + } catch (ErrnoException e) { + mLog.e("Could not delete limit for interface index " + ifIndex + ": ", e); + return null; + } + + return statsValue; + } + + @Override + public boolean tetherOffloadRuleAdd(boolean downstream, @NonNull Tether4Key key, + @NonNull Tether4Value value) { + if (!isInitialized()) return false; + + try { + // 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; } } diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java index bcb644c5a6..c61c44902f 100644 --- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java +++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java @@ -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 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); } diff --git a/Tethering/bpf_progs/Android.bp b/Tethering/bpf_progs/Android.bp index d54f861486..2b10f89761 100644 --- a/Tethering/bpf_progs/Android.bp +++ b/Tethering/bpf_progs/Android.bp @@ -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 + ], +} diff --git a/Tethering/bpf_progs/bpf_tethering.h b/Tethering/bpf_progs/bpf_tethering.h new file mode 100644 index 0000000000..efda228479 --- /dev/null +++ b/Tethering/bpf_progs/bpf_tethering.h @@ -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 +#include +#include +#include + +// Common definitions for BPF code in the tethering mainline module. +// These definitions are available to: +// - The BPF programs in Tethering/bpf_progs/ +// - JNI code that depends on the bpf_tethering_headers library. + +#define BPF_TETHER_ERRORS \ + ERR(INVALID_IP_VERSION) \ + ERR(LOW_TTL) \ + ERR(INVALID_TCP_HEADER) \ + ERR(TCP_CONTROL_PACKET) \ + ERR(NON_GLOBAL_SRC) \ + ERR(NON_GLOBAL_DST) \ + ERR(LOCAL_SRC_DST) \ + ERR(NO_STATS_ENTRY) \ + ERR(NO_LIMIT_ENTRY) \ + ERR(BELOW_IPV4_MTU) \ + ERR(BELOW_IPV6_MTU) \ + ERR(LIMIT_REACHED) \ + ERR(CHANGE_HEAD_FAILED) \ + ERR(TOO_SHORT) \ + ERR(HAS_IP_OPTIONS) \ + ERR(IS_IP_FRAG) \ + ERR(CHECKSUM) \ + ERR(NON_TCP_UDP) \ + ERR(NON_TCP) \ + ERR(SHORT_L4_HEADER) \ + ERR(SHORT_TCP_HEADER) \ + ERR(SHORT_UDP_HEADER) \ + ERR(UDP_CSUM_ZERO) \ + ERR(TRUNCATED_IPV4) \ + ERR(_MAX) + +#define ERR(x) BPF_TETHER_ERR_ ##x, +enum { + BPF_TETHER_ERRORS +}; +#undef ERR + +#define ERR(x) #x, +static const char *bpf_tether_errors[] = { + BPF_TETHER_ERRORS +}; +#undef ERR + +// This header file is shared by eBPF kernel programs (C) and netd (C++) and +// some of the maps are also accessed directly from Java mainline module code. +// +// Hence: explicitly pad all relevant structures and assert that their size +// is the sum of the sizes of their fields. +#define STRUCT_SIZE(name, size) _Static_assert(sizeof(name) == (size), "Incorrect struct size.") + + +#define BPF_PATH_TETHER BPF_PATH "tethering/" + +#define TETHER_STATS_MAP_PATH BPF_PATH_TETHER "map_offload_tether_stats_map" + +typedef uint32_t TetherStatsKey; // upstream ifindex + +typedef struct { + uint64_t rxPackets; + uint64_t rxBytes; + uint64_t rxErrors; + uint64_t txPackets; + uint64_t txBytes; + uint64_t txErrors; +} TetherStatsValue; +STRUCT_SIZE(TetherStatsValue, 6 * 8); // 48 + +#define TETHER_LIMIT_MAP_PATH BPF_PATH_TETHER "map_offload_tether_limit_map" + +typedef uint32_t TetherLimitKey; // upstream ifindex +typedef uint64_t TetherLimitValue; // in bytes + +#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME "prog_offload_schedcls_tether_downstream6_rawip" +#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME "prog_offload_schedcls_tether_downstream6_ether" + +#define TETHER_DOWNSTREAM6_TC_PROG_RAWIP_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_RAWIP_NAME +#define TETHER_DOWNSTREAM6_TC_PROG_ETHER_PATH BPF_PATH_TETHER TETHER_DOWNSTREAM6_TC_PROG_ETHER_NAME + +#define TETHER_DOWNSTREAM6_MAP_PATH BPF_PATH_TETHER "map_offload_tether_downstream6_map" + +// For now tethering offload only needs to support downstreams that use 6-byte MAC addresses, +// because all downstream types that are currently supported (WiFi, USB, Bluetooth and +// Ethernet) have 6-byte MAC addresses. + +typedef struct { + uint32_t iif; // The input interface index + // 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 diff --git a/Tethering/bpf_progs/offload.c b/Tethering/bpf_progs/offload.c index d8dc60dc1a..6aca319f3d 100644 --- a/Tethering/bpf_progs/offload.c +++ b/Tethering/bpf_progs/offload.c @@ -20,31 +20,102 @@ #include #include +// bionic kernel uapi linux/udp.h header is munged... +#define __kernel_udphdr udphdr +#include + #include "bpf_helpers.h" #include "bpf_net_helpers.h" -#include "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"); diff --git a/Tethering/bpf_progs/test.c b/Tethering/bpf_progs/test.c new file mode 100644 index 0000000000..3f0df2eae7 --- /dev/null +++ b/Tethering/bpf_progs/test.c @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "bpf_helpers.h" +#include "bpf_net_helpers.h" +#include "bpf_tethering.h" + +// Used only by TetheringPrivilegedTests, not by production code. +DEFINE_BPF_MAP_GRW(tether_downstream6_map, HASH, TetherDownstream6Key, Tether6Value, 16, + AID_NETWORK_STACK) + +DEFINE_BPF_PROG_KVER("xdp/drop_ipv4_udp_ether", AID_ROOT, AID_NETWORK_STACK, + xdp_test, KVER(5, 9, 0)) +(struct xdp_md *ctx) { + void *data = (void *)(long)ctx->data; + void *data_end = (void *)(long)ctx->data_end; + + struct ethhdr *eth = data; + int hsize = sizeof(*eth); + + struct iphdr *ip = data + hsize; + hsize += sizeof(struct iphdr); + + if (data + hsize > data_end) return XDP_PASS; + if (eth->h_proto != htons(ETH_P_IP)) return XDP_PASS; + if (ip->protocol == IPPROTO_UDP) return XDP_DROP; + return XDP_PASS; +} + +LICENSE("Apache 2.0"); diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp index a10729daff..2631d08774 100644 --- a/Tethering/common/TetheringLib/Android.bp +++ b/Tethering/common/TetheringLib/Android.bp @@ -13,6 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + java_sdk_library { name: "framework-tethering", defaults: ["framework-module-defaults"], diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfCoordinator.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfCoordinator.cpp new file mode 100644 index 0000000000..27357f88d6 --- /dev/null +++ b/Tethering/jni/com_android_networkstack_tethering_BpfCoordinator.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "bpf_tethering.h" + +namespace android { + +static jobjectArray getBpfCounterNames(JNIEnv *env) { + size_t size = BPF_TETHER_ERR__MAX; + jobjectArray ret = env->NewObjectArray(size, env->FindClass("java/lang/String"), nullptr); + for (int i = 0; i < size; i++) { + env->SetObjectArrayElement(ret, i, env->NewStringUTF(bpf_tether_errors[i])); + } + return ret; +} + +/* + * JNI registration. + */ +static const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + { "getBpfCounterNames", "()[Ljava/lang/String;", (void*) getBpfCounterNames }, +}; + +int register_com_android_networkstack_tethering_BpfCoordinator(JNIEnv* env) { + return jniRegisterNativeMethods(env, + "com/android/networkstack/tethering/BpfCoordinator", + gMethods, NELEM(gMethods)); +} + +}; // namespace android diff --git a/Tethering/jni/onload.cpp b/Tethering/jni/onload.cpp index 3766de9076..e31da60ef5 100644 --- a/Tethering/jni/onload.cpp +++ b/Tethering/jni/onload.cpp @@ -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; } diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java index 52d59fcdc1..194737af21 100644 --- a/Tethering/src/android/net/ip/IpServer.java +++ b/Tethering/src/android/net/ip/IpServer.java @@ -67,13 +67,12 @@ import com.android.internal.util.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(); } diff --git a/Tethering/src/android/net/util/TetheringUtils.java b/Tethering/src/android/net/util/TetheringUtils.java index 53b54f7de0..9e7cc2fc14 100644 --- a/Tethering/src/android/net/util/TetheringUtils.java +++ b/Tethering/src/android/net/util/TetheringUtils.java @@ -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; diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java index d890e088bf..985328fe60 100644 --- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java +++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java @@ -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> mIpv6ForwardingRules = new LinkedHashMap<>(); + // Map of downstream client maps. Each of these maps represents the IPv4 clients for a given + // downstream. Needed to build IPv4 forwarding rules when conntrack events are received. + // Each map: + // - Is owned by the IpServer that is responsible for that downstream. + // - Must only be modified by that IpServer. + // - Is created when the IpServer adds its first client, and deleted when the IpServer deletes + // its last client. + // Note that relying on the client address for finding downstream is okay for now because the + // client address is unique. See PrivateAddressCoordinator#requestDownstreamAddress. + // TODO: Refactor if any possible that the client address is not unique. + private final HashMap> + mTetherClients = new HashMap<>(); + + // Set for which downstream is monitoring the conntrack netlink message. + private final Set mMonitoringIpServers = new HashSet<>(); + + // Map of upstream interface IPv4 address to interface index. + // TODO: consider making the key to be unique because the upstream address is not unique. It + // is okay for now because there have only one upstream generally. + private final HashMap mIpv4UpstreamIndices = new HashMap<>(); + // 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 getBpfIngressMap() { + /** Get downstream4 BPF map. */ + @Nullable public BpfMap 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 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 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 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 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 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 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 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 tetherStatsList = new SparseArray(); + tetherStatsList.put(upstreamIfindex, statsValue); + + // Update the last stats delta and delete the local cache for a given upstream. + updateQuotaAndStatsFromSnapshot(tetherStatsList); + mStats.remove(upstreamIfindex); } } @@ -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(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 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()); + } + + HashMap clients = mTetherClients.get(ipServer); + clients.put(client.clientAddress, client); + } + + /** + * Remove downstream client. + */ + public void tetherOffloadClientRemove(@NonNull final IpServer ipServer, + @NonNull final ClientInfo client) { + if (!isUsingBpf()) return; + + HashMap clients = mTetherClients.get(ipServer); + if (clients == null) return; + + // If no rule is removed, return early. Avoid unnecessary work on a non-existent rule + // which may have never been added or removed already. + if (clients.remove(client.clientAddress) == null) return; + + // Remove the downstream entry if it has no more rule. + if (clients.isEmpty()) { + mTetherClients.remove(ipServer); + } + } + + /** + * Call when UpstreamNetworkState may be changed. + * If upstream has ipv4 for tethering, update this new UpstreamNetworkState to map. The + * upstream interface index and its address mapping is prepared for building IPv4 + * offload rule. + * + * TODO: Delete the unused upstream interface mapping. + * TODO: Support ether ip upstream interface. + */ + public void addUpstreamIfindexToMap(LinkProperties lp) { + if (!mPollingStarted) return; + + // This will not work on a network that is using 464xlat because hasIpv4Address will not be + // true. + // TODO: need to consider 464xlat. + if (lp == null || !lp.hasIpv4Address()) return; + + // Support raw ip upstream interface only. + final InterfaceParams params = InterfaceParams.getByName(lp.getInterfaceName()); + if (params == null || params.hasMacAddress) return; + + Collection addresses = lp.getAddresses(); + for (InetAddress addr: addresses) { + if (addr instanceof Inet4Address) { + Inet4Address i4addr = (Inet4Address) addr; + if (!i4addr.isAnyLocalAddress() && !i4addr.isLinkLocalAddress() + && !i4addr.isLoopbackAddress() && !i4addr.isMulticastAddress()) { + mIpv4UpstreamIndices.put(i4addr, params.index); + } + } + } + } + + + // 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(""); - } 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> 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 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 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 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 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 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 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 tetherStatsList = + mBpfCoordinatorShim.tetherOffloadGetStats(); + + if (tetherStatsList == null) { + mLog.e("Problem fetching tethering stats"); return; } + updateQuotaAndStatsFromSnapshot(tetherStatsList); } @@ -828,4 +1344,6 @@ public class BpfCoordinator { final SparseArray getInterfaceNamesForTesting() { return mInterfaceNames; } + + private static native String[] getBpfCounterNames(); } diff --git a/Tethering/src/com/android/networkstack/tethering/BpfMap.java b/Tethering/src/com/android/networkstack/tethering/BpfMap.java index 69ad1b60eb..e9b4ccf2f4 100644 --- a/Tethering/src/com/android/networkstack/tethering/BpfMap.java +++ b/Tethering/src/com/android/networkstack/tethering/BpfMap.java @@ -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 the value of the map. */ public class BpfMap implements AutoCloseable { + static { + System.loadLibrary("tetherutilsjni"); + } + // Following definitions from kernel include/uapi/linux/bpf.h public static final int BPF_F_RDWR = 0; public static final int BPF_F_RDONLY = 1 << 3; @@ -76,6 +81,21 @@ public class BpfMap 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 key, final Class value) { + mMapFd = -1; + mKeyClass = key; + mValueClass = value; + mKeySize = Struct.getSize(key); + mValueSize = Struct.getSize(value); + } + /** * Update an existing or create a new key -> value entry in an eBbpf map. */ @@ -118,6 +138,11 @@ public class BpfMap 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 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; diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java index bb7322f2a0..b1e3cfed52 100644 --- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java +++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java @@ -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; diff --git a/Tethering/src/com/android/networkstack/tethering/Tether4Key.java b/Tethering/src/com/android/networkstack/tethering/Tether4Key.java new file mode 100644 index 0000000000..a01ea34c68 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/Tether4Key.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import android.net.MacAddress; + +import androidx.annotation.NonNull; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +import java.net.Inet4Address; +import java.net.UnknownHostException; +import java.util.Objects; + +/** Key type for downstream & upstream IPv4 forwarding maps. */ +public class Tether4Key extends Struct { + @Field(order = 0, type = Type.U32) + public final long iif; + + @Field(order = 1, type = Type.EUI48) + public final MacAddress dstMac; + + @Field(order = 2, type = Type.U8, padding = 1) + public final short l4proto; + + @Field(order = 3, type = Type.ByteArray, arraysize = 4) + public final byte[] src4; + + @Field(order = 4, type = Type.ByteArray, arraysize = 4) + public final byte[] dst4; + + @Field(order = 5, type = Type.UBE16) + public final int srcPort; + + @Field(order = 6, type = Type.UBE16) + public final int dstPort; + + public Tether4Key(final long iif, @NonNull final MacAddress dstMac, final short l4proto, + final byte[] src4, final byte[] dst4, final int srcPort, + final int dstPort) { + Objects.requireNonNull(dstMac); + + this.iif = iif; + this.dstMac = dstMac; + this.l4proto = l4proto; + this.src4 = src4; + this.dst4 = dst4; + this.srcPort = srcPort; + this.dstPort = dstPort; + } + + @Override + public String toString() { + try { + return String.format( + "iif: %d, dstMac: %s, l4proto: %d, src4: %s, dst4: %s, " + + "srcPort: %d, dstPort: %d", + iif, dstMac, l4proto, + Inet4Address.getByAddress(src4), Inet4Address.getByAddress(dst4), + Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort)); + } catch (UnknownHostException | IllegalArgumentException e) { + return String.format("Invalid IP address", e); + } + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/Tether4Value.java b/Tethering/src/com/android/networkstack/tethering/Tether4Value.java new file mode 100644 index 0000000000..03a226ce45 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/Tether4Value.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import android.net.MacAddress; + +import androidx.annotation.NonNull; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Objects; + +/** Value type for downstream & upstream IPv4 forwarding maps. */ +public class Tether4Value extends Struct { + @Field(order = 0, type = Type.U32) + public final long oif; + + // The ethhdr struct which is defined in uapi/linux/if_ether.h + @Field(order = 1, type = Type.EUI48) + public final MacAddress ethDstMac; + @Field(order = 2, type = Type.EUI48) + public final MacAddress ethSrcMac; + @Field(order = 3, type = Type.UBE16) + public final int ethProto; // Packet type ID field. + + @Field(order = 4, type = Type.U16) + public final int pmtu; + + @Field(order = 5, type = Type.ByteArray, arraysize = 16) + public final byte[] src46; + + @Field(order = 6, type = Type.ByteArray, arraysize = 16) + public final byte[] dst46; + + @Field(order = 7, type = Type.UBE16) + public final int srcPort; + + @Field(order = 8, type = Type.UBE16) + public final int dstPort; + + // TODO: consider using U64. + @Field(order = 9, type = Type.U63) + public final long lastUsed; + + public Tether4Value(final long oif, @NonNull final MacAddress ethDstMac, + @NonNull final MacAddress ethSrcMac, final int ethProto, final int pmtu, + final byte[] src46, final byte[] dst46, final int srcPort, + final int dstPort, final long lastUsed) { + Objects.requireNonNull(ethDstMac); + Objects.requireNonNull(ethSrcMac); + + this.oif = oif; + this.ethDstMac = ethDstMac; + this.ethSrcMac = ethSrcMac; + this.ethProto = ethProto; + this.pmtu = pmtu; + this.src46 = src46; + this.dst46 = dst46; + this.srcPort = srcPort; + this.dstPort = dstPort; + this.lastUsed = lastUsed; + } + + @Override + public String toString() { + try { + return String.format( + "oif: %d, ethDstMac: %s, ethSrcMac: %s, ethProto: %d, pmtu: %d, " + + "src46: %s, dst46: %s, srcPort: %d, dstPort: %d, " + + "lastUsed: %d", + oif, ethDstMac, ethSrcMac, ethProto, pmtu, + InetAddress.getByAddress(src46), InetAddress.getByAddress(dst46), + Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort), + lastUsed); + } catch (UnknownHostException | IllegalArgumentException e) { + return String.format("Invalid IP address", e); + } + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherIngressValue.java b/Tethering/src/com/android/networkstack/tethering/Tether6Value.java similarity index 66% rename from Tethering/src/com/android/networkstack/tethering/TetherIngressValue.java rename to Tethering/src/com/android/networkstack/tethering/Tether6Value.java index e2116fc2d5..b3107fdd74 100644 --- a/Tethering/src/com/android/networkstack/tethering/TetherIngressValue.java +++ b/Tethering/src/com/android/networkstack/tethering/Tether6Value.java @@ -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, diff --git a/Tethering/src/com/android/networkstack/tethering/TetherIngressKey.java b/Tethering/src/com/android/networkstack/tethering/TetherDownstream6Key.java similarity index 86% rename from Tethering/src/com/android/networkstack/tethering/TetherIngressKey.java rename to Tethering/src/com/android/networkstack/tethering/TetherDownstream6Key.java index 78683c5e1c..3860cba745 100644 --- a/Tethering/src/com/android/networkstack/tethering/TetherIngressKey.java +++ b/Tethering/src/com/android/networkstack/tethering/TetherDownstream6Key.java @@ -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"); } } } diff --git a/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java b/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java new file mode 100644 index 0000000000..bc9bb474a2 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +/** The key of BpfMap which is used for tethering per-interface limit. */ +public class TetherLimitKey extends Struct { + @Field(order = 0, type = Type.U32) + public final long ifindex; // upstream interface index + + public TetherLimitKey(final long ifindex) { + this.ifindex = ifindex; + } + + // TODO: remove equals, hashCode and toString once aosp/1536721 is merged. + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + + if (!(obj instanceof TetherLimitKey)) return false; + + final TetherLimitKey that = (TetherLimitKey) obj; + + return ifindex == that.ifindex; + } + + @Override + public int hashCode() { + return Long.hashCode(ifindex); + } + + @Override + public String toString() { + return String.format("ifindex: %d", ifindex); + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java b/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java new file mode 100644 index 0000000000..ed7e7d4324 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +/** The value of BpfMap which is used for tethering per-interface limit. */ +public class TetherLimitValue extends Struct { + // Use the signed long variable to store the int64 limit on limit BPF map. + // S64 is enough for each interface limit even at 5Gbps for ~468 years. + // 2^63 / (5 * 1000 * 1000 * 1000) * 8 / 86400 / 365 = 468. + // Note that QUOTA_UNLIMITED (-1) indicates there is no limit. + @Field(order = 0, type = Type.S64) + public final long limit; + + public TetherLimitValue(final long limit) { + this.limit = limit; + } + + // TODO: remove equals, hashCode and toString once aosp/1536721 is merged. + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + + if (!(obj instanceof TetherLimitValue)) return false; + + final TetherLimitValue that = (TetherLimitValue) obj; + + return limit == that.limit; + } + + @Override + public int hashCode() { + return Long.hashCode(limit); + } + + @Override + public String toString() { + return String.format("limit: %d", limit); + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java b/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java new file mode 100644 index 0000000000..5442480a01 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +/** The key of BpfMap which is used for tethering stats. */ +public class TetherStatsKey extends Struct { + @Field(order = 0, type = Type.U32) + public final long ifindex; // upstream interface index + + public TetherStatsKey(final long ifindex) { + this.ifindex = ifindex; + } + + // TODO: remove equals, hashCode and toString once aosp/1536721 is merged. + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + + if (!(obj instanceof TetherStatsKey)) return false; + + final TetherStatsKey that = (TetherStatsKey) obj; + + return ifindex == that.ifindex; + } + + @Override + public int hashCode() { + return Long.hashCode(ifindex); + } + + @Override + public String toString() { + return String.format("ifindex: %d", ifindex); + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java b/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java new file mode 100644 index 0000000000..844d2e8f7e --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +/** The key of BpfMap which is used for tethering stats. */ +public class TetherStatsValue extends Struct { + // Use the signed long variable to store the uint64 stats from stats BPF map. + // U63 is enough for each data element even at 5Gbps for ~468 years. + // 2^63 / (5 * 1000 * 1000 * 1000) * 8 / 86400 / 365 = 468. + @Field(order = 0, type = Type.U63) + public final long rxPackets; + @Field(order = 1, type = Type.U63) + public final long rxBytes; + @Field(order = 2, type = Type.U63) + public final long rxErrors; + @Field(order = 3, type = Type.U63) + public final long txPackets; + @Field(order = 4, type = Type.U63) + public final long txBytes; + @Field(order = 5, type = Type.U63) + public final long txErrors; + + public TetherStatsValue(final long rxPackets, final long rxBytes, final long rxErrors, + final long txPackets, final long txBytes, final long txErrors) { + this.rxPackets = rxPackets; + this.rxBytes = rxBytes; + this.rxErrors = rxErrors; + this.txPackets = txPackets; + this.txBytes = txBytes; + this.txErrors = txErrors; + } + + // TODO: remove equals, hashCode and toString once aosp/1536721 is merged. + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + + if (!(obj instanceof TetherStatsValue)) return false; + + final TetherStatsValue that = (TetherStatsValue) obj; + + return rxPackets == that.rxPackets + && rxBytes == that.rxBytes + && rxErrors == that.rxErrors + && txPackets == that.txPackets + && txBytes == that.txBytes + && txErrors == that.txErrors; + } + + @Override + public int hashCode() { + return Long.hashCode(rxPackets) ^ Long.hashCode(rxBytes) ^ Long.hashCode(rxErrors) + ^ Long.hashCode(txPackets) ^ Long.hashCode(txBytes) ^ Long.hashCode(txErrors); + } + + @Override + public String toString() { + return String.format("rxPackets: %s, rxBytes: %s, rxErrors: %s, txPackets: %s, " + + "txBytes: %s, txErrors: %s", rxPackets, rxBytes, rxErrors, txPackets, + txBytes, txErrors); + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java b/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java new file mode 100644 index 0000000000..c736f2a883 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherUpstream6Key.java @@ -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; + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java index fdd1c40949..ac5857d1c8 100644 --- a/Tethering/src/com/android/networkstack/tethering/Tethering.java +++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java @@ -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); } diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringService.java b/Tethering/src/com/android/networkstack/tethering/TetheringService.java index d637ba7557..f3cd549186 100644 --- a/Tethering/src/com/android/networkstack/tethering/TetheringService.java +++ b/Tethering/src/com/android/networkstack/tethering/TetheringService.java @@ -82,7 +82,6 @@ public class TetheringService extends Service { */ @VisibleForTesting public Tethering makeTethering(TetheringDependencies deps) { - System.loadLibrary("tetherutilsjni"); return new Tethering(deps); } diff --git a/Tethering/tests/Android.bp b/Tethering/tests/Android.bp index 731144cee0..8f31c577a6 100644 --- a/Tethering/tests/Android.bp +++ b/Tethering/tests/Android.bp @@ -14,6 +14,10 @@ // limitations under the License. // +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + filegroup { name: "TetheringTestsJarJarRules", srcs: ["jarjar-rules.txt"], diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp index 42f83bfcd7..f63df2c8df 100644 --- a/Tethering/tests/integration/Android.bp +++ b/Tethering/tests/integration/Android.bp @@ -13,6 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. // +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + java_defaults { name: "TetheringIntegrationTestsDefaults", srcs: [ @@ -70,6 +74,7 @@ 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", diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp index f925b0a53f..20a5f1855e 100644 --- a/Tethering/tests/mts/Android.bp +++ b/Tethering/tests/mts/Android.bp @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + android_test { // This tests for functionality that is not required for devices that // don't use Tethering mainline module. diff --git a/Tethering/tests/privileged/Android.bp b/Tethering/tests/privileged/Android.bp index 9217345dc2..75fdd6ef23 100644 --- a/Tethering/tests/privileged/Android.bp +++ b/Tethering/tests/privileged/Android.bp @@ -14,6 +14,10 @@ // limitations under the License. // +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + java_defaults { name: "TetheringPrivilegedTestsJniDefaults", jni_libs: [ diff --git a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java index 1ddbaa9a20..62302c37c8 100644 --- a/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java +++ b/Tethering/tests/privileged/src/com/android/networkstack/tethering/BpfMapTest.java @@ -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 mTestData; + private ArrayMap mTestData; + + private BpfMap 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 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 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 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 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 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 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 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 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 bpfMap = getTestMap()) { - final ArrayMap resultMap = - new ArrayMap<>(mTestData); + final ArrayMap 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 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 bpfMap = getTestMap()) { - final ArrayMap resultMap = - new ArrayMap<>(mTestData); + final ArrayMap 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 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 bpfMap = getTestMap()) { - final ArrayMap testData = new ArrayMap<>(); + final ArrayMap testData = + new ArrayMap<>(); - // Build test data for TEST_MAP_SIZE + 1 entries. - for (int i = 1; i <= TEST_MAP_SIZE + 1; i++) { - testData.put(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); } } } diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp index 5e4fe524f2..d469b3717e 100644 --- a/Tethering/tests/unit/Android.bp +++ b/Tethering/tests/unit/Android.bp @@ -15,6 +15,10 @@ // // Tests in this folder are included both in unit tests and CTS. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + java_library { name: "TetheringCommonTests", srcs: [ @@ -69,6 +73,7 @@ java_defaults { // For mockito extended "libdexmakerjvmtiagent", "libstaticjvmtiagent", + "libtetherutilsjni", ], } diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java index dae19b710b..b45db7edff 100644 --- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java +++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java @@ -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 mBpfIngressMap; + @Mock private ConntrackMonitor mConntrackMonitor; + @Mock private BpfMap mBpfDownstream4Map; + @Mock private BpfMap mBpfUpstream4Map; + @Mock private BpfMap mBpfDownstream6Map; + @Mock private BpfMap mBpfUpstream6Map; + @Mock private BpfMap mBpfStatsMap; + @Mock private BpfMap mBpfLimitMap; @Captor private ArgumentCaptor 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 getBpfIngressMap() { - return mBpfIngressMap; + public BpfMap getBpfDownstream4Map() { + return mBpfDownstream4Map; + } + + @Nullable + public BpfMap getBpfUpstream4Map() { + return mBpfUpstream4Map; + } + + @Nullable + public BpfMap getBpfDownstream6Map() { + return mBpfDownstream6Map; + } + + @Nullable + public BpfMap getBpfUpstream6Map() { + return mBpfUpstream6Map; + } + + @Nullable + public BpfMap getBpfStatsMap() { + return mBpfStatsMap; + } + + @Nullable + public BpfMap getBpfLimitMap() { + return mBpfLimitMap; } }; 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 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(); } diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java index b920fa8b67..1270e50c42 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java @@ -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 extends BpfMap { + private final HashMap mMap = new HashMap(); + + TestBpfMap(final Class key, final Class value) { + super(key, value); + } + + @Override + public void forEach(BiConsumer action) throws ErrnoException { + // TODO: consider using mocked #getFirstKey and #getNextKey to iterate. It helps to + // implement the entry deletion in the iteration if required. + for (Map.Entry entry : mMap.entrySet()) { + action.accept(entry.getKey(), entry.getValue()); + } + } + + @Override + public void updateEntry(K key, V value) throws ErrnoException { + mMap.put(key, value); + } + + @Override + public void insertEntry(K key, V value) throws ErrnoException, + IllegalArgumentException { + // The entry is created if and only if it doesn't exist. See BpfMap#insertEntry. + if (mMap.get(key) != null) { + throw new IllegalArgumentException(key + " already exist"); + } + mMap.put(key, value); + } + + @Override + public boolean deleteEntry(Struct key) throws ErrnoException { + return mMap.remove(key) != null; + } + + @Override + public V getValue(@NonNull K key) throws ErrnoException { + // Return value for a given key. Otherwise, return null without an error ENOENT. + // BpfMap#getValue treats that the entry is not found as no error. + return mMap.get(key); + } + }; + @Mock private NetworkStatsManager mStatsManager; @Mock private INetd mNetd; @Mock private IpServer mIpServer; + @Mock private IpServer mIpServer2; @Mock private TetheringConfiguration mTetherConfig; - @Mock private BpfMap mBpfIngressMap; + @Mock private ConntrackMonitor mConntrackMonitor; + @Mock private BpfMap mBpfDownstream4Map; + @Mock private BpfMap mBpfUpstream4Map; + @Mock private BpfMap mBpfDownstream6Map; + @Mock private BpfMap 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 mStringArrayCaptor = ArgumentCaptor.forClass(ArrayList.class); private final TestLooper mTestLooper = new TestLooper(); + private final TestBpfMap mBpfStatsMap = + spy(new TestBpfMap<>(TetherStatsKey.class, TetherStatsValue.class)); + private final TestBpfMap mBpfLimitMap = + spy(new TestBpfMap<>(TetherLimitKey.class, TetherLimitValue.class)); private BpfCoordinator.Dependencies mDeps = spy(new BpfCoordinator.Dependencies() { @NonNull @@ -136,9 +200,39 @@ public class BpfCoordinatorTest { return mTetherConfig; } + @NonNull + public ConntrackMonitor getConntrackMonitor(ConntrackEventConsumer consumer) { + return mConntrackMonitor; + } + @Nullable - public BpfMap getBpfIngressMap() { - return mBpfIngressMap; + public BpfMap getBpfDownstream4Map() { + return mBpfDownstream4Map; + } + + @Nullable + public BpfMap getBpfUpstream4Map() { + return mBpfUpstream4Map; + } + + @Nullable + public BpfMap getBpfDownstream6Map() { + return mBpfDownstream6Map; + } + + @Nullable + public BpfMap getBpfUpstream6Map() { + return mBpfUpstream6Map; + } + + @Nullable + public BpfMap getBpfStatsMap() { + return mBpfStatsMap; + } + + @Nullable + public BpfMap getBpfLimitMap() { + return mBpfLimitMap; } }); @@ -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 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(); } } diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java index f4b3749132..60fddb51ab 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java @@ -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) { diff --git a/framework/Android.bp b/framework/Android.bp deleted file mode 100644 index 8db8d7699a..0000000000 --- a/framework/Android.bp +++ /dev/null @@ -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__", - ], -} \ No newline at end of file diff --git a/framework/src/com/android/connectivity/aidl/INetworkAgent.aidl b/framework/src/com/android/connectivity/aidl/INetworkAgent.aidl deleted file mode 100644 index 1af9e769b7..0000000000 --- a/framework/src/com/android/connectivity/aidl/INetworkAgent.aidl +++ /dev/null @@ -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); -} diff --git a/framework/src/com/android/connectivity/aidl/INetworkAgentRegistry.aidl b/framework/src/com/android/connectivity/aidl/INetworkAgentRegistry.aidl deleted file mode 100644 index d42a34055c..0000000000 --- a/framework/src/com/android/connectivity/aidl/INetworkAgentRegistry.aidl +++ /dev/null @@ -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 networks); -} diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp index 47b114b64d..3185f7edcd 100644 --- a/tests/cts/hostside/Android.bp +++ b/tests/cts/hostside/Android.bp @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + java_test_host { name: "CtsHostsideNetworkTests", defaults: ["cts_defaults"], diff --git a/tests/cts/hostside/TEST_MAPPING b/tests/cts/hostside/TEST_MAPPING new file mode 100644 index 0000000000..02d2d6ec12 --- /dev/null +++ b/tests/cts/hostside/TEST_MAPPING @@ -0,0 +1,18 @@ +{ + "presubmit": [ + { + "name": "CtsHostsideNetworkTests", + "options": [ + { + "include-filter": "com.android.cts.net.HostsideRestrictBackgroundNetworkTests" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + }, + { + "exclude-annotation": "android.platform.test.annotations.FlakyTest" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/cts/hostside/aidl/Android.bp b/tests/cts/hostside/aidl/Android.bp index 320a1fa443..2751f6ff82 100644 --- a/tests/cts/hostside/aidl/Android.bp +++ b/tests/cts/hostside/aidl/Android.bp @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + java_test_helper_library { name: "CtsHostsideNetworkTestsAidl", sdk_version: "current", diff --git a/tests/cts/hostside/app/Android.bp b/tests/cts/hostside/app/Android.bp index 99037567b5..813e6c716f 100644 --- a/tests/cts/hostside/app/Android.bp +++ b/tests/cts/hostside/app/Android.bp @@ -14,6 +14,10 @@ // limitations under the License. // +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + android_test_helper_app { name: "CtsHostsideNetworkTestsApp", defaults: ["cts_support_defaults"], diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java index 8fadf9e295..5c99c679c8 100644 --- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/MeterednessConfigurationRule.java @@ -15,21 +15,20 @@ */ package com.android.cts.net.hostside; -import static com.android.cts.net.hostside.NetworkPolicyTestUtils.resetMeteredNetwork; -import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setupMeteredNetwork; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.setupActiveNetworkMeteredness; import static com.android.cts.net.hostside.Property.METERED_NETWORK; import static com.android.cts.net.hostside.Property.NON_METERED_NETWORK; import android.util.ArraySet; -import android.util.Pair; import com.android.compatibility.common.util.BeforeAfterRule; +import com.android.compatibility.common.util.ThrowingRunnable; import org.junit.runner.Description; import org.junit.runners.model.Statement; public class MeterednessConfigurationRule extends BeforeAfterRule { - private Pair 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; } } } diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java index 2ac29e77ff..955317bbf6 100644 --- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkCallbackTest.java @@ -17,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, diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java index 3041dfa76b..b61535bb54 100644 --- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java @@ -20,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 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(); } diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java index 81a431cfda..a663cd6f73 100755 --- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java @@ -16,6 +16,8 @@ package com.android.cts.net.hostside; +import static android.Manifest.permission.NETWORK_SETTINGS; +import static android.net.NetworkCapabilities.TRANSPORT_VPN; import static android.os.Process.INVALID_UID; import static android.system.OsConstants.AF_INET; import static android.system.OsConstants.AF_INET6; @@ -25,6 +27,9 @@ import static android.system.OsConstants.IPPROTO_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(); diff --git a/tests/cts/hostside/app2/Android.bp b/tests/cts/hostside/app2/Android.bp index 8e279311a8..b448459b6a 100644 --- a/tests/cts/hostside/app2/Android.bp +++ b/tests/cts/hostside/app2/Android.bp @@ -14,6 +14,10 @@ // limitations under the License. // +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + android_test_helper_app { name: "CtsHostsideNetworkTestsApp2", defaults: ["cts_support_defaults"], diff --git a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java index 590e17e5e5..1c9ff05a5e 100644 --- a/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java +++ b/tests/cts/hostside/app2/src/com/android/cts/net/hostside/app2/MyService.java @@ -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(); } diff --git a/tests/cts/hostside/certs/Android.bp b/tests/cts/hostside/certs/Android.bp index ab4cf340d0..60b54769ea 100644 --- a/tests/cts/hostside/certs/Android.bp +++ b/tests/cts/hostside/certs/Android.bp @@ -1,3 +1,7 @@ +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + android_app_certificate { name: "cts-net-app", certificate: "cts-net-app", diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp index 528171a036..13b5e9a94f 100644 --- a/tests/cts/net/Android.bp +++ b/tests/cts/net/Android.bp @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + java_defaults { name: "CtsNetTestCasesDefaults", defaults: ["cts_defaults"], @@ -44,6 +48,7 @@ java_defaults { "ctstestrunner-axt", "junit", "junit-params", + "modules-utils-build", "net-utils-framework-common", "truth-prebuilt", ], diff --git a/tests/cts/net/api23Test/Android.bp b/tests/cts/net/api23Test/Android.bp index e43a5e82d0..5b372944c6 100644 --- a/tests/cts/net/api23Test/Android.bp +++ b/tests/cts/net/api23Test/Android.bp @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + android_test { name: "CtsNetApi23TestCases", defaults: ["cts_defaults"], diff --git a/tests/cts/net/appForApi23/Android.bp b/tests/cts/net/appForApi23/Android.bp index cec6d7f5a1..b39690f14f 100644 --- a/tests/cts/net/appForApi23/Android.bp +++ b/tests/cts/net/appForApi23/Android.bp @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + android_test { name: "CtsNetTestAppForApi23", defaults: ["cts_defaults"], diff --git a/tests/cts/net/jni/Android.bp b/tests/cts/net/jni/Android.bp index 3953aeb701..13f38d77cb 100644 --- a/tests/cts/net/jni/Android.bp +++ b/tests/cts/net/jni/Android.bp @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + cc_library_shared { name: "libnativedns_jni", diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp index 6defd359ab..1bc5a86068 100644 --- a/tests/cts/net/native/dns/Android.bp +++ b/tests/cts/net/native/dns/Android.bp @@ -1,3 +1,7 @@ +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + cc_defaults { name: "dns_async_defaults", diff --git a/tests/cts/net/native/qtaguid/Android.bp b/tests/cts/net/native/qtaguid/Android.bp index 4861651504..68bb14da87 100644 --- a/tests/cts/net/native/qtaguid/Android.bp +++ b/tests/cts/net/native/qtaguid/Android.bp @@ -14,6 +14,10 @@ // Build the unit tests. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + cc_test { name: "CtsNativeNetTestCases", diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt index eb5048fa9b..a889c41f53 100644 --- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt +++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt @@ -26,6 +26,9 @@ import android.net.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 { + 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() diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java index cbf43e7b63..ce874d197a 100644 --- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java +++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java @@ -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 mNetworks = new HashMap(); - 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); + } + } } diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java index 9eab024cf0..8f2d93d9aa 100644 --- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java +++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java @@ -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; diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt index 69d90aab1e..aea33cad54 100644 --- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt +++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt @@ -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)) diff --git a/tests/cts/net/util/Android.bp b/tests/cts/net/util/Android.bp index c36d976423..88a206858d 100644 --- a/tests/cts/net/util/Android.bp +++ b/tests/cts/net/util/Android.bp @@ -15,6 +15,10 @@ // // Common utilities for cts net tests. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + java_library { name: "cts-net-utils", srcs: ["java/**/*.java", "java/**/*.kt"], @@ -23,4 +27,4 @@ java_library { "junit", "net-tests-utils", ], -} \ No newline at end of file +} diff --git a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java index 05270115b1..88172d7540 100644 --- a/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java +++ b/tests/cts/net/util/java/android/net/cts/util/CtsNetUtils.java @@ -212,7 +212,7 @@ 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; } diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp index b1d4a6052b..824c874330 100644 --- a/tests/cts/tethering/Android.bp +++ b/tests/cts/tethering/Android.bp @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + android_test { name: "CtsTetheringTest", defaults: ["cts_defaults"],