diff options
author | android-build-team Robot <android-build-team-robot@google.com> | 2021-04-05 16:32:09 +0000 |
---|---|---|
committer | android-build-team Robot <android-build-team-robot@google.com> | 2021-04-05 16:32:09 +0000 |
commit | e8f470df66c96404ef0dd70bc87b2c26782fef9b (patch) | |
tree | 9821a8992c30a86dda7bace4d78bcc1366072dec | |
parent | 235bacb28d720bb82652649672fc33073eef1a91 (diff) | |
parent | cf257bb21799f54ba11d63c44905be671596bdc7 (diff) | |
download | Connectivity-e8f470df66c96404ef0dd70bc87b2c26782fef9b.tar.gz |
Snap for 7259092 from cf257bb21799f54ba11d63c44905be671596bdc7 to mainline-media-release
Change-Id: Ieaa39f56c4859e03850b477b76294268b17bf0d8
32 files changed, 2031 insertions, 329 deletions
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 4e615a197e..f27c831689 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 @@ -159,6 +159,18 @@ public class BpfCoordinatorShimImpl } @Override + public boolean attachProgram(String iface, boolean downstream) { + /* no op */ + return true; + } + + @Override + public boolean detachProgram(String iface) { + /* 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 4dc1c51f68..4f7fe65a6a 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 @@ -31,6 +31,7 @@ import androidx.annotation.Nullable; import com.android.networkstack.tethering.BpfCoordinator.Dependencies; import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule; import com.android.networkstack.tethering.BpfMap; +import com.android.networkstack.tethering.BpfUtils; import com.android.networkstack.tethering.Tether4Key; import com.android.networkstack.tethering.Tether4Value; import com.android.networkstack.tethering.Tether6Value; @@ -42,6 +43,7 @@ import com.android.networkstack.tethering.TetherStatsValue; import com.android.networkstack.tethering.TetherUpstream6Key; import java.io.FileDescriptor; +import java.io.IOException; /** * Bpf coordinator class for API shims. @@ -84,12 +86,46 @@ public class BpfCoordinatorShimImpl public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) { mLog = deps.getSharedLog().forSubComponent(TAG); + mBpfDownstream4Map = deps.getBpfDownstream4Map(); mBpfUpstream4Map = deps.getBpfUpstream4Map(); mBpfDownstream6Map = deps.getBpfDownstream6Map(); mBpfUpstream6Map = deps.getBpfUpstream6Map(); mBpfStatsMap = deps.getBpfStatsMap(); mBpfLimitMap = deps.getBpfLimitMap(); + + // Clear the stubs of the maps for handling the system service crash if any. + // Doesn't throw the exception and clear the stubs as many as possible. + try { + if (mBpfDownstream4Map != null) mBpfDownstream4Map.clear(); + } catch (ErrnoException e) { + mLog.e("Could not clear mBpfDownstream4Map: " + e); + } + try { + if (mBpfUpstream4Map != null) mBpfUpstream4Map.clear(); + } catch (ErrnoException e) { + mLog.e("Could not clear mBpfUpstream4Map: " + e); + } + try { + if (mBpfDownstream6Map != null) mBpfDownstream6Map.clear(); + } catch (ErrnoException e) { + mLog.e("Could not clear mBpfDownstream6Map: " + e); + } + try { + if (mBpfUpstream6Map != null) mBpfUpstream6Map.clear(); + } catch (ErrnoException e) { + mLog.e("Could not clear mBpfUpstream6Map: " + e); + } + try { + if (mBpfStatsMap != null) mBpfStatsMap.clear(); + } catch (ErrnoException e) { + mLog.e("Could not clear mBpfStatsMap: " + e); + } + try { + if (mBpfLimitMap != null) mBpfLimitMap.clear(); + } catch (ErrnoException e) { + mLog.e("Could not clear mBpfLimitMap: " + e); + } } @Override @@ -324,6 +360,32 @@ public class BpfCoordinatorShimImpl return true; } + @Override + public boolean attachProgram(String iface, boolean downstream) { + if (!isInitialized()) return false; + + try { + BpfUtils.attachProgram(iface, downstream); + } catch (IOException e) { + mLog.e("Could not attach program: " + e); + return false; + } + return true; + } + + @Override + public boolean detachProgram(String iface) { + if (!isInitialized()) return false; + + try { + BpfUtils.detachProgram(iface); + } catch (IOException e) { + mLog.e("Could not detach program: " + e); + return false; + } + return true; + } + private String mapStatus(BpfMap m, String name) { return name + "{" + (m != null ? "OK" : "ERROR") + "}"; } 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 c61c44902f..b7b4c47cc3 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 @@ -143,5 +143,19 @@ public abstract class BpfCoordinatorShim { * Deletes a tethering IPv4 offload rule from the appropriate BPF map. */ public abstract boolean tetherOffloadRuleRemove(boolean downstream, @NonNull Tether4Key key); + + /** + * Attach BPF program. + * + * TODO: consider using InterfaceParams to replace interface name. + */ + public abstract boolean attachProgram(@NonNull String iface, boolean downstream); + + /** + * Detach BPF program. + * + * TODO: consider using InterfaceParams to replace interface name. + */ + public abstract boolean detachProgram(@NonNull String iface); } diff --git a/Tethering/bpf_progs/Android.bp b/Tethering/bpf_progs/Android.bp index 2b10f89761..289d75d0b0 100644 --- a/Tethering/bpf_progs/Android.bp +++ b/Tethering/bpf_progs/Android.bp @@ -51,8 +51,6 @@ bpf { 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 ], } @@ -66,7 +64,5 @@ bpf { 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/offload.c b/Tethering/bpf_progs/offload.c index bfa1e3a192..5f29d4f60a 100644 --- a/Tethering/bpf_progs/offload.c +++ b/Tethering/bpf_progs/offload.c @@ -72,11 +72,11 @@ 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 { \ +#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; \ + if (count) __sync_fetch_and_add(count, 1); \ + return ret; \ } while(0) #define TC_DROP(counter) COUNT_AND_RETURN(counter, TC_ACT_SHOT) @@ -600,13 +600,7 @@ static inline __always_inline int do_forward4(struct __sk_buff* skb, const bool 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); -} +// Full featured (required) implementations for 5.8+ kernels (these are S+ by definition) DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", AID_ROOT, AID_NETWORK_STACK, sched_cls_tether_downstream4_rawip_5_8, KVER(5, 8, 0)) @@ -614,28 +608,27 @@ DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_rawip$5_8", AID_ROOT, AID_NETW 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)) +DEFINE_BPF_PROG_KVER("schedcls/tether_downstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream4_ether_5_8, KVER(5, 8, 0)) (struct __sk_buff* skb) { return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ true); } +DEFINE_BPF_PROG_KVER("schedcls/tether_upstream4_ether$5_8", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream4_ether_5_8, KVER(5, 8, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ true); +} + +// Full featured (optional) implementations for 4.14-S, 4.19-S & 5.4-S kernels +// (optional, because we need to be able to fallback for 4.14/4.19/5.4 pre-S kernels) + DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt", AID_ROOT, AID_NETWORK_STACK, sched_cls_tether_downstream4_rawip_opt, @@ -644,24 +637,33 @@ DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$opt", return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ true); } -DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt", +DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt", AID_ROOT, AID_NETWORK_STACK, - sched_cls_tether_upstream4_ether_opt, + sched_cls_tether_upstream4_rawip_opt, KVER(4, 14, 0), KVER(5, 8, 0)) (struct __sk_buff* skb) { - return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ true); + return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ true); } -DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt", +DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$opt", AID_ROOT, AID_NETWORK_STACK, - sched_cls_tether_upstream4_rawip_opt, + sched_cls_tether_downstream4_ether_opt, KVER(4, 14, 0), KVER(5, 8, 0)) (struct __sk_buff* skb) { - return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ true); + return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ true); +} + +DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$opt", + AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream4_ether_opt, + KVER(4, 14, 0), KVER(5, 8, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ true); } // Partial (TCP-only: will not update 'last_used' field) implementations for 4.14+ kernels. -// These will be loaded only if the above optional ones failed (loading of *these* must succeed). +// These will be loaded only if the above optional ones failed (loading of *these* must succeed +// for 5.4+, since that is always an R patched kernel). // // [Note: as a result TCP connections will not have their conntrack timeout refreshed, however, // since /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established defaults to 432000 (seconds), @@ -671,52 +673,79 @@ DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$opt", // 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)) +// RAWIP: Required for 5.4-R kernels -- which always support bpf_skb_change_head(). + +DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0)) (struct __sk_buff* skb) { - return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ false); + return do_forward4(skb, /* is_ethernet */ false, /* 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)) +DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$5_4", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream4_rawip_5_4, KVER(5, 4, 0), KVER(5, 8, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ false); +} + +// RAWIP: Optional for 4.14/4.19 (R) kernels -- which support bpf_skb_change_head(). +// [Note: fallback for 4.14/4.19 (P/Q) kernels is below in stub section] + +DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$4_14", + AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream4_rawip_4_14, + KVER(4, 14, 0), KVER(5, 4, 0)) (struct __sk_buff* skb) { return do_forward4(skb, /* is_ethernet */ false, /* downstream */ true, /* updatetime */ false); } +DEFINE_OPTIONAL_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$4_14", + AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream4_rawip_4_14, + KVER(4, 14, 0), KVER(5, 4, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ false, /* downstream */ false, /* updatetime */ false); +} + +// ETHER: Required for 4.14-Q/R, 4.19-Q/R & 5.4-R kernels. + +DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0)) +(struct __sk_buff* skb) { + return do_forward4(skb, /* is_ethernet */ true, /* downstream */ true, /* updatetime */ false); +} + DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_ether$4_14", AID_ROOT, AID_NETWORK_STACK, sched_cls_tether_upstream4_ether_4_14, KVER(4, 14, 0), KVER(5, 8, 0)) (struct __sk_buff* skb) { return do_forward4(skb, /* is_ethernet */ true, /* downstream */ false, /* updatetime */ false); } -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 Q kernels -// Placeholder (no-op) implementations for older pre-4.14 kernels +// RAWIP: 4.9-P/Q, 4.14-P/Q & 4.19-Q kernels -- without bpf_skb_change_head() for tc programs -DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK, - sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER(4, 14, 0)) +DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0)) (struct __sk_buff* skb) { return TC_ACT_OK; } -DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK, - sched_cls_tether_downstream4_rawip_stub, KVER_NONE, KVER(4, 14, 0)) +DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER(5, 4, 0)) (struct __sk_buff* skb) { return TC_ACT_OK; } -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)) +// ETHER: 4.9-P/Q kernel + +DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_downstream4_ether$stub", AID_ROOT, AID_NETWORK_STACK, + sched_cls_tether_downstream4_ether_stub, KVER_NONE, KVER(4, 14, 0)) (struct __sk_buff* skb) { return TC_ACT_OK; } -DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", AID_ROOT, AID_NETWORK_STACK, - sched_cls_tether_upstream4_rawip_stub, KVER_NONE, KVER(4, 14, 0)) +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; } @@ -726,27 +755,69 @@ DEFINE_BPF_PROG_KVER_RANGE("schedcls/tether_upstream4_rawip$stub", AID_ROOT, AID DEFINE_BPF_MAP_GRW(tether_xdp_devmap, DEVMAP_HASH, uint32_t, uint32_t, 64, AID_NETWORK_STACK) +static inline __always_inline int do_xdp_forward6(struct xdp_md *ctx, const bool is_ethernet, + const bool downstream) { + return XDP_PASS; +} + +static inline __always_inline int do_xdp_forward4(struct xdp_md *ctx, const bool is_ethernet, + const bool downstream) { + return XDP_PASS; +} + +static inline __always_inline int do_xdp_forward_ether(struct xdp_md *ctx, const bool downstream) { + const void* data = (void*)(long)ctx->data; + const void* data_end = (void*)(long)ctx->data_end; + const struct ethhdr* eth = data; + + // Make sure we actually have an ethernet header + if ((void*)(eth + 1) > data_end) return XDP_PASS; + + if (eth->h_proto == htons(ETH_P_IPV6)) + return do_xdp_forward6(ctx, /* is_ethernet */ true, downstream); + if (eth->h_proto == htons(ETH_P_IP)) + return do_xdp_forward4(ctx, /* is_ethernet */ true, downstream); + + // Anything else we don't know how to handle... + return XDP_PASS; +} + +static inline __always_inline int do_xdp_forward_rawip(struct xdp_md *ctx, const bool downstream) { + const void* data = (void*)(long)ctx->data; + const void* data_end = (void*)(long)ctx->data_end; + + // The top nibble of both IPv4 and IPv6 headers is the IP version. + if (data_end - data < 1) return XDP_PASS; + const uint8_t v = (*(uint8_t*)data) >> 4; + + if (v == 6) return do_xdp_forward6(ctx, /* is_ethernet */ false, downstream); + if (v == 4) return do_xdp_forward4(ctx, /* is_ethernet */ false, downstream); + + // Anything else we don't know how to handle... + return XDP_PASS; +} + #define DEFINE_XDP_PROG(str, func) \ DEFINE_BPF_PROG_KVER(str, AID_ROOT, AID_NETWORK_STACK, func, KVER(5, 9, 0))(struct xdp_md *ctx) DEFINE_XDP_PROG("xdp/tether_downstream_ether", xdp_tether_downstream_ether) { - return XDP_PASS; + return do_xdp_forward_ether(ctx, /* downstream */ true); } DEFINE_XDP_PROG("xdp/tether_downstream_rawip", xdp_tether_downstream_rawip) { - return XDP_PASS; + return do_xdp_forward_rawip(ctx, /* downstream */ true); } DEFINE_XDP_PROG("xdp/tether_upstream_ether", xdp_tether_upstream_ether) { - return XDP_PASS; + return do_xdp_forward_ether(ctx, /* downstream */ false); } DEFINE_XDP_PROG("xdp/tether_upstream_rawip", xdp_tether_upstream_rawip) { - return XDP_PASS; + return do_xdp_forward_rawip(ctx, /* downstream */ false); } LICENSE("Apache 2.0"); diff --git a/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp b/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp new file mode 100644 index 0000000000..308dfb9c98 --- /dev/null +++ b/Tethering/jni/com_android_networkstack_tethering_BpfUtils.cpp @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <arpa/inet.h> +#include <jni.h> +#include <linux/if_arp.h> +#include <linux/if_ether.h> +#include <linux/netlink.h> +#include <linux/pkt_cls.h> +#include <linux/pkt_sched.h> +#include <linux/rtnetlink.h> +#include <nativehelper/JNIHelp.h> +#include <net/if.h> +#include <stdio.h> +#include <sys/socket.h> + +// TODO: use unique_fd. +#define BPF_FD_JUST_USE_INT +#include "BpfSyscallWrappers.h" +#include "bpf_tethering.h" +#include "nativehelper/scoped_utf_chars.h" + +// The maximum length of TCA_BPF_NAME. Sync from net/sched/cls_bpf.c. +#define CLS_BPF_NAME_LEN 256 + +namespace android { +// Sync from system/netd/server/NetlinkCommands.h +const uint16_t NETLINK_REQUEST_FLAGS = NLM_F_REQUEST | NLM_F_ACK; +const sockaddr_nl KERNEL_NLADDR = {AF_NETLINK, 0, 0, 0}; + +// TODO: move to frameworks/libs/net/common/native for sharing with +// system/netd/server/OffloadUtils.{c, h}. +static void sendAndProcessNetlinkResponse(JNIEnv* env, const void* req, int len) { + int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); // TODO: use unique_fd + if (fd == -1) { + jniThrowExceptionFmt(env, "java/io/IOException", + "socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE): %s", + strerror(errno)); + return; + } + + static constexpr int on = 1; + if (setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &on, sizeof(on))) { + jniThrowExceptionFmt(env, "java/io/IOException", + "setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, %d)", on); + close(fd); + return; + } + + // this is needed to get valid strace netlink parsing, it allocates the pid + if (bind(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR))) { + jniThrowExceptionFmt(env, "java/io/IOException", "bind(fd, {AF_NETLINK, 0, 0}): %s", + strerror(errno)); + close(fd); + return; + } + + // we do not want to receive messages from anyone besides the kernel + if (connect(fd, (const struct sockaddr*)&KERNEL_NLADDR, sizeof(KERNEL_NLADDR))) { + jniThrowExceptionFmt(env, "java/io/IOException", "connect(fd, {AF_NETLINK, 0, 0}): %s", + strerror(errno)); + close(fd); + return; + } + + int rv = send(fd, req, len, 0); + + if (rv == -1) { + jniThrowExceptionFmt(env, "java/io/IOException", "send(fd, req, len, 0): %s", + strerror(errno)); + close(fd); + return; + } + + if (rv != len) { + jniThrowExceptionFmt(env, "java/io/IOException", "send(fd, req, len, 0): %s", + strerror(EMSGSIZE)); + close(fd); + return; + } + + struct { + nlmsghdr h; + nlmsgerr e; + char buf[256]; + } resp = {}; + + rv = recv(fd, &resp, sizeof(resp), MSG_TRUNC); + + if (rv == -1) { + jniThrowExceptionFmt(env, "java/io/IOException", "recv() failed: %s", strerror(errno)); + close(fd); + return; + } + + if (rv < (int)NLMSG_SPACE(sizeof(struct nlmsgerr))) { + jniThrowExceptionFmt(env, "java/io/IOException", "recv() returned short packet: %d", rv); + close(fd); + return; + } + + if (resp.h.nlmsg_len != (unsigned)rv) { + jniThrowExceptionFmt(env, "java/io/IOException", + "recv() returned invalid header length: %d != %d", resp.h.nlmsg_len, + rv); + close(fd); + return; + } + + if (resp.h.nlmsg_type != NLMSG_ERROR) { + jniThrowExceptionFmt(env, "java/io/IOException", + "recv() did not return NLMSG_ERROR message: %d", resp.h.nlmsg_type); + close(fd); + return; + } + + if (resp.e.error) { // returns 0 on success + jniThrowExceptionFmt(env, "java/io/IOException", "NLMSG_ERROR message return error: %s", + strerror(-resp.e.error)); + } + close(fd); + return; +} + +static int hardwareAddressType(const char* interface) { + int fd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (fd < 0) return -errno; + + struct ifreq ifr = {}; + // We use strncpy() instead of strlcpy() since kernel has to be able + // to handle non-zero terminated junk passed in by userspace anyway, + // and this way too long interface names (more than IFNAMSIZ-1 = 15 + // characters plus terminating NULL) will not get truncated to 15 + // characters and zero-terminated and thus potentially erroneously + // match a truncated interface if one were to exist. + strncpy(ifr.ifr_name, interface, sizeof(ifr.ifr_name)); + + int rv; + if (ioctl(fd, SIOCGIFHWADDR, &ifr, sizeof(ifr))) { + rv = -errno; + } else { + rv = ifr.ifr_hwaddr.sa_family; + } + + close(fd); + return rv; +} + +static jboolean com_android_networkstack_tethering_BpfUtils_isEthernet(JNIEnv* env, jobject clazz, + jstring iface) { + ScopedUtfChars interface(env, iface); + + int rv = hardwareAddressType(interface.c_str()); + if (rv < 0) { + jniThrowExceptionFmt(env, "java/io/IOException", + "Get hardware address type of interface %s failed: %s", + interface.c_str(), strerror(-rv)); + return false; + } + + switch (rv) { + case ARPHRD_ETHER: + return true; + case ARPHRD_NONE: + case ARPHRD_RAWIP: // in Linux 4.14+ rmnet support was upstreamed and this is 519 + case 530: // this is ARPHRD_RAWIP on some Android 4.9 kernels with rmnet + return false; + default: + jniThrowExceptionFmt(env, "java/io/IOException", + "Unknown hardware address type %s on interface %s", rv, + interface.c_str()); + return false; + } +} + +// tc filter add dev .. in/egress prio 1 protocol ipv6/ip bpf object-pinned /sys/fs/bpf/... +// direct-action +static void com_android_networkstack_tethering_BpfUtils_tcFilterAddDevBpf( + JNIEnv* env, jobject clazz, jint ifIndex, jboolean ingress, jshort prio, jshort proto, + jstring bpfProgPath) { + ScopedUtfChars pathname(env, bpfProgPath); + + const int bpfFd = bpf::retrieveProgram(pathname.c_str()); + if (bpfFd == -1) { + jniThrowExceptionFmt(env, "java/io/IOException", "retrieveProgram failed %s", + strerror(errno)); + return; + } + + struct { + nlmsghdr n; + tcmsg t; + struct { + nlattr attr; + // The maximum classifier name length is defined as IFNAMSIZ. + // See tcf_proto_ops in include/net/sch_generic.h. + char str[NLMSG_ALIGN(IFNAMSIZ)]; + } kind; + struct { + nlattr attr; + struct { + nlattr attr; + __u32 u32; + } fd; + struct { + nlattr attr; + char str[NLMSG_ALIGN(CLS_BPF_NAME_LEN)]; + } name; + struct { + nlattr attr; + __u32 u32; + } flags; + } options; + } req = { + .n = + { + .nlmsg_len = sizeof(req), + .nlmsg_type = RTM_NEWTFILTER, + .nlmsg_flags = NETLINK_REQUEST_FLAGS | NLM_F_EXCL | NLM_F_CREATE, + }, + .t = + { + .tcm_family = AF_UNSPEC, + .tcm_ifindex = ifIndex, + .tcm_handle = TC_H_UNSPEC, + .tcm_parent = TC_H_MAKE(TC_H_CLSACT, + ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS), + .tcm_info = static_cast<__u32>((static_cast<uint16_t>(prio) << 16) | + htons(static_cast<uint16_t>(proto))), + }, + .kind = + { + .attr = + { + .nla_len = sizeof(req.kind), + .nla_type = TCA_KIND, + }, + // Classifier name. See cls_bpf_ops in net/sched/cls_bpf.c. + .str = "bpf", + }, + .options = + { + .attr = + { + .nla_len = sizeof(req.options), + .nla_type = NLA_F_NESTED | TCA_OPTIONS, + }, + .fd = + { + .attr = + { + .nla_len = sizeof(req.options.fd), + .nla_type = TCA_BPF_FD, + }, + .u32 = static_cast<__u32>(bpfFd), + }, + .name = + { + .attr = + { + .nla_len = sizeof(req.options.name), + .nla_type = TCA_BPF_NAME, + }, + // Visible via 'tc filter show', but + // is overwritten by strncpy below + .str = "placeholder", + }, + .flags = + { + .attr = + { + .nla_len = sizeof(req.options.flags), + .nla_type = TCA_BPF_FLAGS, + }, + .u32 = TCA_BPF_FLAG_ACT_DIRECT, + }, + }, + }; + + snprintf(req.options.name.str, sizeof(req.options.name.str), "%s:[*fsobj]", + basename(pathname.c_str())); + + // The exception may be thrown from sendAndProcessNetlinkResponse. Close the file descriptor of + // BPF program before returning the function in any case. + sendAndProcessNetlinkResponse(env, &req, sizeof(req)); + close(bpfFd); +} + +// tc filter del dev .. in/egress prio .. protocol .. +static void com_android_networkstack_tethering_BpfUtils_tcFilterDelDev(JNIEnv* env, jobject clazz, + jint ifIndex, + jboolean ingress, + jshort prio, jshort proto) { + const struct { + nlmsghdr n; + tcmsg t; + } req = { + .n = + { + .nlmsg_len = sizeof(req), + .nlmsg_type = RTM_DELTFILTER, + .nlmsg_flags = NETLINK_REQUEST_FLAGS, + }, + .t = + { + .tcm_family = AF_UNSPEC, + .tcm_ifindex = ifIndex, + .tcm_handle = TC_H_UNSPEC, + .tcm_parent = TC_H_MAKE(TC_H_CLSACT, + ingress ? TC_H_MIN_INGRESS : TC_H_MIN_EGRESS), + .tcm_info = static_cast<__u32>((static_cast<uint16_t>(prio) << 16) | + htons(static_cast<uint16_t>(proto))), + }, + }; + + sendAndProcessNetlinkResponse(env, &req, sizeof(req)); +} + +/* + * JNI registration. + */ +static const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + {"isEthernet", "(Ljava/lang/String;)Z", + (void*)com_android_networkstack_tethering_BpfUtils_isEthernet}, + {"tcFilterAddDevBpf", "(IZSSLjava/lang/String;)V", + (void*)com_android_networkstack_tethering_BpfUtils_tcFilterAddDevBpf}, + {"tcFilterDelDev", "(IZSS)V", + (void*)com_android_networkstack_tethering_BpfUtils_tcFilterDelDev}, +}; + +int register_com_android_networkstack_tethering_BpfUtils(JNIEnv* env) { + return jniRegisterNativeMethods(env, "com/android/networkstack/tethering/BpfUtils", gMethods, + NELEM(gMethods)); +} + +}; // namespace android diff --git a/Tethering/jni/onload.cpp b/Tethering/jni/onload.cpp index e31da60ef5..02e602d99e 100644 --- a/Tethering/jni/onload.cpp +++ b/Tethering/jni/onload.cpp @@ -25,6 +25,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); +int register_com_android_networkstack_tethering_BpfUtils(JNIEnv* env); extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { JNIEnv *env; @@ -39,6 +40,8 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { if (register_com_android_networkstack_tethering_BpfCoordinator(env) < 0) return JNI_ERR; + if (register_com_android_networkstack_tethering_BpfUtils(env) < 0) return JNI_ERR; + return JNI_VERSION_1_6; } diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java index 194737af21..e5380e008d 100644 --- a/Tethering/src/android/net/ip/IpServer.java +++ b/Tethering/src/android/net/ip/IpServer.java @@ -1291,6 +1291,7 @@ public class IpServer extends StateMachine { // Sometimes interfaces are gone before we get // to remove their rules, which generates errors. // Just do the best we can. + mBpfCoordinator.maybeDetachProgram(mIfaceName, upstreamIface); try { mNetd.ipfwdRemoveInterfaceForward(mIfaceName, upstreamIface); } catch (RemoteException | ServiceSpecificException e) { @@ -1334,6 +1335,7 @@ public class IpServer extends StateMachine { mUpstreamIfaceSet = newUpstreamIfaceSet; for (String ifname : added) { + mBpfCoordinator.maybeAttachProgram(mIfaceName, ifname); try { mNetd.tetherAddForward(mIfaceName, ifname); mNetd.ipfwdAddInterfaceForward(mIfaceName, ifname); diff --git a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java index 7c0b7cc751..afccd0a5d9 100644 --- a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java +++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java @@ -37,7 +37,7 @@ import android.system.StructTimeval; import android.util.Log; import com.android.internal.annotations.GuardedBy; -import com.android.internal.util.TrafficStatsConstants; +import com.android.net.module.util.NetworkStackConstants; import java.io.FileDescriptor; import java.io.IOException; @@ -589,7 +589,7 @@ public class RouterAdvertisementDaemon { final int send_timout_ms = 300; final int oldTag = TrafficStats.getAndSetThreadStatsTag( - TrafficStatsConstants.TAG_SYSTEM_NEIGHBOR); + NetworkStackConstants.TAG_SYSTEM_NEIGHBOR); try { mSocket = Os.socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6); // Setting SNDTIMEO is purely for defensive purposes. diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java index 985328fe60..7fb73b4904 100644 --- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java +++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java @@ -28,6 +28,8 @@ import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED import static android.system.OsConstants.ETH_P_IP; import static android.system.OsConstants.ETH_P_IPV6; +import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM; +import static com.android.networkstack.tethering.BpfUtils.UPSTREAM; import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS; import android.app.usage.NetworkStatsManager; @@ -92,9 +94,6 @@ public class BpfCoordinator { 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 MacAddress NULL_MAC_ADDRESS = MacAddress.fromString( @@ -118,7 +117,6 @@ public class BpfCoordinator { return makeMapPath((downstream ? "downstream" : "upstream") + ipVersion); } - @VisibleForTesting enum StatsType { STATS_PER_IFACE, @@ -218,12 +216,16 @@ public class BpfCoordinator { // is okay for now because there have only one upstream generally. private final HashMap<Inet4Address, Integer> mIpv4UpstreamIndices = new HashMap<>(); + // Map for upstream and downstream pair. + private final HashMap<String, HashSet<String>> mForwardingPairs = new HashMap<>(); + // Runnable that used by scheduling next polling of stats. private final Runnable mScheduledPollingTask = () -> { updateForwardedStats(); maybeSchedulePollingStats(); }; + // TODO: add BpfMap<TetherDownstream64Key, TetherDownstream64Value> retrieving function. @VisibleForTesting public abstract static class Dependencies { /** Get handler. */ @@ -259,6 +261,7 @@ public class BpfCoordinator { /** Get downstream4 BPF map. */ @Nullable public BpfMap<Tether4Key, Tether4Value> getBpfDownstream4Map() { + if (!isAtLeastS()) return null; try { return new BpfMap<>(TETHER_DOWNSTREAM4_MAP_PATH, BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class); @@ -270,6 +273,7 @@ public class BpfCoordinator { /** Get upstream4 BPF map. */ @Nullable public BpfMap<Tether4Key, Tether4Value> getBpfUpstream4Map() { + if (!isAtLeastS()) return null; try { return new BpfMap<>(TETHER_UPSTREAM4_MAP_PATH, BpfMap.BPF_F_RDWR, Tether4Key.class, Tether4Value.class); @@ -281,6 +285,7 @@ public class BpfCoordinator { /** Get downstream6 BPF map. */ @Nullable public BpfMap<TetherDownstream6Key, Tether6Value> getBpfDownstream6Map() { + if (!isAtLeastS()) return null; try { return new BpfMap<>(TETHER_DOWNSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR, TetherDownstream6Key.class, Tether6Value.class); @@ -292,6 +297,7 @@ public class BpfCoordinator { /** Get upstream6 BPF map. */ @Nullable public BpfMap<TetherUpstream6Key, Tether6Value> getBpfUpstream6Map() { + if (!isAtLeastS()) return null; try { return new BpfMap<>(TETHER_UPSTREAM6_FS_PATH, BpfMap.BPF_F_RDWR, TetherUpstream6Key.class, Tether6Value.class); @@ -303,6 +309,7 @@ public class BpfCoordinator { /** Get stats BPF map. */ @Nullable public BpfMap<TetherStatsKey, TetherStatsValue> getBpfStatsMap() { + if (!isAtLeastS()) return null; try { return new BpfMap<>(TETHER_STATS_MAP_PATH, BpfMap.BPF_F_RDWR, TetherStatsKey.class, TetherStatsValue.class); @@ -314,6 +321,7 @@ public class BpfCoordinator { /** Get limit BPF map. */ @Nullable public BpfMap<TetherLimitKey, TetherLimitValue> getBpfLimitMap() { + if (!isAtLeastS()) return null; try { return new BpfMap<>(TETHER_LIMIT_MAP_PATH, BpfMap.BPF_F_RDWR, TetherLimitKey.class, TetherLimitValue.class); @@ -418,7 +426,8 @@ public class BpfCoordinator { * See NetlinkMonitor#handlePacket, NetlinkMessage#parseNfMessage. */ public void startMonitoring(@NonNull final IpServer ipServer) { - if (!isUsingBpf()) return; + // TODO: Wrap conntrackMonitor starting function into mBpfCoordinatorShim. + if (!isUsingBpf() || !mDeps.isAtLeastS()) return; if (mMonitoringIpServers.contains(ipServer)) { Log.wtf(TAG, "The same downstream " + ipServer.interfaceName() @@ -439,6 +448,9 @@ public class BpfCoordinator { * Note that this can be only called on handler thread. */ public void stopMonitoring(@NonNull final IpServer ipServer) { + // TODO: Wrap conntrackMonitor stopping function into mBpfCoordinatorShim. + if (!isUsingBpf() || !mDeps.isAtLeastS()) return; + mMonitoringIpServers.remove(ipServer); if (!mMonitoringIpServers.isEmpty()) return; @@ -690,6 +702,37 @@ public class BpfCoordinator { } } + /** + * Attach BPF program + * + * TODO: consider error handling if the attach program failed. + */ + public void maybeAttachProgram(@NonNull String intIface, @NonNull String extIface) { + if (forwardingPairExists(intIface, extIface)) return; + + boolean firstDownstreamForThisUpstream = !isAnyForwardingPairOnUpstream(extIface); + forwardingPairAdd(intIface, extIface); + + mBpfCoordinatorShim.attachProgram(intIface, UPSTREAM); + // Attach if the upstream is the first time to be used in a forwarding pair. + if (firstDownstreamForThisUpstream) { + mBpfCoordinatorShim.attachProgram(extIface, DOWNSTREAM); + } + } + + /** + * Detach BPF program + */ + public void maybeDetachProgram(@NonNull String intIface, @NonNull String extIface) { + forwardingPairRemove(intIface, extIface); + + // Detaching program may fail because the interface has been removed already. + mBpfCoordinatorShim.detachProgram(intIface); + // Detach if no more forwarding pair is using the upstream. + if (!isAnyForwardingPairOnUpstream(extIface)) { + mBpfCoordinatorShim.detachProgram(extIface); + } + } // TODO: make mInterfaceNames accessible to the shim and move this code to there. private String getIfName(long ifindex) { @@ -771,7 +814,7 @@ public class BpfCoordinator { final int upstreamIfindex = rule.upstreamIfindex; pw.println(String.format("%d(%s) %d(%s) %s %s %s", upstreamIfindex, mInterfaceNames.get(upstreamIfindex), rule.downstreamIfindex, - downstreamIface, rule.address, rule.srcMac, rule.dstMac)); + downstreamIface, rule.address.getHostAddress(), rule.srcMac, rule.dstMac)); } pw.decreaseIndent(); } @@ -795,7 +838,7 @@ public class BpfCoordinator { } map.forEach((k, v) -> pw.println(ipv6UpstreamRuletoString(k, v))); } catch (ErrnoException e) { - pw.println("Error dumping IPv4 map: " + e); + pw.println("Error dumping IPv6 upstream map: " + e); } } @@ -808,9 +851,10 @@ public class BpfCoordinator { } 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); + return String.format("[%s] %d(%s) %s:%d -> %d(%s) %s:%d -> %s:%d", + key.dstMac, key.iif, getIfName(key.iif), private4, key.srcPort, + value.oif, getIfName(value.oif), + public4, value.srcPort, dst4, key.dstPort); } private void dumpIpv4ForwardingRules(IndentingPrintWriter pw) { @@ -823,7 +867,7 @@ public class BpfCoordinator { pw.println("No IPv4 rules"); return; } - pw.println("[IPv4]: iif(iface) oif(iface) src nat dst"); + pw.println("IPv4: [inDstMac] iif(iface) src -> nat -> dst"); pw.increaseIndent(); map.forEach((k, v) -> pw.println(ipv4RuleToString(k, v))); } catch (ErrnoException e) { @@ -842,6 +886,10 @@ public class BpfCoordinator { } private void dumpCounters(@NonNull IndentingPrintWriter pw) { + if (!mDeps.isAtLeastS()) { + pw.println("No counter support"); + return; + } try (BpfMap<U32Struct, U32Struct> map = new BpfMap<>(TETHER_ERROR_MAP_PATH, BpfMap.BPF_F_RDONLY, U32Struct.class, U32Struct.class)) { @@ -1226,6 +1274,33 @@ public class BpfCoordinator { return false; } + private void forwardingPairAdd(@NonNull String intIface, @NonNull String extIface) { + if (!mForwardingPairs.containsKey(extIface)) { + mForwardingPairs.put(extIface, new HashSet<String>()); + } + mForwardingPairs.get(extIface).add(intIface); + } + + private void forwardingPairRemove(@NonNull String intIface, @NonNull String extIface) { + HashSet<String> downstreams = mForwardingPairs.get(extIface); + if (downstreams == null) return; + if (!downstreams.remove(intIface)) return; + + if (downstreams.isEmpty()) { + mForwardingPairs.remove(extIface); + } + } + + private boolean forwardingPairExists(@NonNull String intIface, @NonNull String extIface) { + if (!mForwardingPairs.containsKey(extIface)) return false; + + return mForwardingPairs.get(extIface).contains(intIface); + } + + private boolean isAnyForwardingPairOnUpstream(@NonNull String extIface) { + return mForwardingPairs.containsKey(extIface); + } + @NonNull private NetworkStats buildNetworkStats(@NonNull StatsType type, int ifIndex, @NonNull final ForwardedStats diff) { diff --git a/Tethering/src/com/android/networkstack/tethering/BpfUtils.java b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java new file mode 100644 index 0000000000..0b44249458 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.networkstack.tethering; + +import static android.system.OsConstants.ETH_P_IP; +import static android.system.OsConstants.ETH_P_IPV6; + +import android.net.util.InterfaceParams; + +import androidx.annotation.NonNull; + +import java.io.IOException; + +/** + * The classes and the methods for BPF utilization. + * + * {@hide} + */ +public class BpfUtils { + static { + System.loadLibrary("tetherutilsjni"); + } + + // For better code clarity when used for 'bool ingress' parameter. + static final boolean EGRESS = false; + static final boolean INGRESS = true; + + // For better code clarify when used for 'bool downstream' parameter. + // + // This is talking about the direction of travel of the offloaded packets. + // + // Upstream means packets heading towards the internet/uplink (upload), + // thus for tethering this is attached to ingress on the downstream interface, + // while for clat this is attached to egress on the v4-* clat interface. + // + // Downstream means packets coming from the internet/uplink (download), thus + // for both clat and tethering this is attached to ingress on the upstream interface. + static final boolean DOWNSTREAM = true; + static final boolean UPSTREAM = false; + + // The priority of clat/tether hooks - smaller is higher priority. + // TC tether is higher priority then TC clat to match XDP winning over TC. + // Sync from system/netd/server/OffloadUtils.h. + static final short PRIO_TETHER6 = 1; + static final short PRIO_TETHER4 = 2; + // note that the above must be lower than PRIO_CLAT from netd's OffloadUtils.cpp + + private static String makeProgPath(boolean downstream, int ipVersion, boolean ether) { + String path = "/sys/fs/bpf/tethering/prog_offload_schedcls_tether_" + + (downstream ? "downstream" : "upstream") + + ipVersion + "_" + + (ether ? "ether" : "rawip"); + return path; + } + + /** + * Attach BPF program + * + * TODO: use interface index to replace interface name. + */ + public static void attachProgram(@NonNull String iface, boolean downstream) + throws IOException { + final InterfaceParams params = InterfaceParams.getByName(iface); + if (params == null) { + throw new IOException("Fail to get interface params for interface " + iface); + } + + boolean ether; + try { + ether = isEthernet(iface); + } catch (IOException e) { + throw new IOException("isEthernet(" + params.index + "[" + iface + "]) failure: " + e); + } + + try { + // tc filter add dev .. ingress prio 1 protocol ipv6 bpf object-pinned /sys/fs/bpf/... + // direct-action + tcFilterAddDevBpf(params.index, INGRESS, PRIO_TETHER6, (short) ETH_P_IPV6, + makeProgPath(downstream, 6, ether)); + } catch (IOException e) { + throw new IOException("tc filter add dev (" + params.index + "[" + iface + + "]) ingress prio PRIO_TETHER6 protocol ipv6 failure: " + e); + } + + try { + // tc filter add dev .. ingress prio 2 protocol ip bpf object-pinned /sys/fs/bpf/... + // direct-action + tcFilterAddDevBpf(params.index, INGRESS, PRIO_TETHER4, (short) ETH_P_IP, + makeProgPath(downstream, 4, ether)); + } catch (IOException e) { + throw new IOException("tc filter add dev (" + params.index + "[" + iface + + "]) ingress prio PRIO_TETHER4 protocol ip failure: " + e); + } + } + + /** + * Detach BPF program + * + * TODO: use interface index to replace interface name. + */ + public static void detachProgram(@NonNull String iface) throws IOException { + final InterfaceParams params = InterfaceParams.getByName(iface); + if (params == null) { + throw new IOException("Fail to get interface params for interface " + iface); + } + + try { + // tc filter del dev .. ingress prio 1 protocol ipv6 + tcFilterDelDev(params.index, INGRESS, PRIO_TETHER6, (short) ETH_P_IPV6); + } catch (IOException e) { + throw new IOException("tc filter del dev (" + params.index + "[" + iface + + "]) ingress prio PRIO_TETHER6 protocol ipv6 failure: " + e); + } + + try { + // tc filter del dev .. ingress prio 2 protocol ip + tcFilterDelDev(params.index, INGRESS, PRIO_TETHER4, (short) ETH_P_IP); + } catch (IOException e) { + throw new IOException("tc filter del dev (" + params.index + "[" + iface + + "]) ingress prio PRIO_TETHER4 protocol ip failure: " + e); + } + } + + private static native boolean isEthernet(String iface) throws IOException; + + private static native void tcFilterAddDevBpf(int ifIndex, boolean ingress, short prio, + short proto, String bpfProgPath) throws IOException; + + private static native void tcFilterDelDev(int ifIndex, boolean ingress, short prio, + short proto) throws IOException; +} diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java index ac5857d1c8..c6e8fd631f 100644 --- a/Tethering/src/com/android/networkstack/tethering/Tethering.java +++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java @@ -442,7 +442,8 @@ public class Tethering { // NOTE: This is always invoked on the mLooper thread. private void updateConfiguration() { mConfig = mDeps.generateTetheringConfiguration(mContext, mLog, mActiveDataSubId); - mUpstreamNetworkMonitor.updateMobileRequiresDun(mConfig.isDunRequired); + mUpstreamNetworkMonitor.setUpstreamConfig(mConfig.chooseUpstreamAutomatically, + mConfig.isDunRequired); reportConfigurationChanged(mConfig.toStableParcelable()); } @@ -1559,7 +1560,7 @@ public class Tethering { config.preferredUpstreamIfaceTypes); if (ns == null) { if (tryCell) { - mUpstreamNetworkMonitor.registerMobileNetworkRequest(); + mUpstreamNetworkMonitor.setTryCell(true); // We think mobile should be coming up; don't set a retry. } else { sendMessageDelayed(CMD_RETRY_UPSTREAM, UPSTREAM_SETTLE_TIME_MS); @@ -1852,7 +1853,7 @@ public class Tethering { // longer desired, release any mobile requests. final boolean previousUpstreamWanted = updateUpstreamWanted(); if (previousUpstreamWanted && !mUpstreamWanted) { - mUpstreamNetworkMonitor.releaseMobileNetworkRequest(); + mUpstreamNetworkMonitor.setTryCell(false); } break; } diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java index b17065cb78..513f07c735 100644 --- a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java +++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java @@ -60,7 +60,7 @@ import java.util.Set; * Calling #startObserveAllNetworks() to observe all networks. Listening all * networks is necessary while the expression of preferred upstreams remains * a list of legacy connectivity types. In future, this can be revisited. - * Calling #registerMobileNetworkRequest() to bring up mobile DUN/HIPRI network. + * Calling #setTryCell() to request bringing up mobile DUN or HIPRI. * * The methods and data members of this class are only to be accessed and * modified from the tethering main state machine thread. Any other @@ -114,7 +114,14 @@ public class UpstreamNetworkMonitor { private NetworkCallback mListenAllCallback; private NetworkCallback mDefaultNetworkCallback; private NetworkCallback mMobileNetworkCallback; + + /** Whether Tethering has requested a cellular upstream. */ + private boolean mTryCell; + /** Whether the carrier requires DUN. */ private boolean mDunRequired; + /** Whether automatic upstream selection is enabled. */ + private boolean mAutoUpstream; + // Whether the current default upstream is mobile or not. private boolean mIsDefaultCellularUpstream; // The current system default network (not really used yet). @@ -190,23 +197,49 @@ public class UpstreamNetworkMonitor { mNetworkMap.clear(); } - /** Setup or teardown DUN connection according to |dunRequired|. */ - public void updateMobileRequiresDun(boolean dunRequired) { - final boolean valueChanged = (mDunRequired != dunRequired); + private void reevaluateUpstreamRequirements(boolean tryCell, boolean autoUpstream, + boolean dunRequired) { + final boolean mobileRequestRequired = tryCell && (dunRequired || !autoUpstream); + final boolean dunRequiredChanged = (mDunRequired != dunRequired); + + mTryCell = tryCell; mDunRequired = dunRequired; - if (valueChanged && mobileNetworkRequested()) { - releaseMobileNetworkRequest(); + mAutoUpstream = autoUpstream; + + if (mobileRequestRequired && !mobileNetworkRequested()) { registerMobileNetworkRequest(); + } else if (mobileNetworkRequested() && !mobileRequestRequired) { + releaseMobileNetworkRequest(); + } else if (mobileNetworkRequested() && dunRequiredChanged) { + releaseMobileNetworkRequest(); + if (mobileRequestRequired) { + registerMobileNetworkRequest(); + } } } + /** + * Informs UpstreamNetworkMonitor that a cellular upstream is desired. + * + * This may result in filing a NetworkRequest for DUN if it is required, or for MOBILE_HIPRI if + * automatic upstream selection is disabled and MOBILE_HIPRI is the preferred upstream. + */ + public void setTryCell(boolean tryCell) { + reevaluateUpstreamRequirements(tryCell, mAutoUpstream, mDunRequired); + } + + /** Informs UpstreamNetworkMonitor of upstream configuration parameters. */ + public void setUpstreamConfig(boolean autoUpstream, boolean dunRequired) { + reevaluateUpstreamRequirements(mTryCell, autoUpstream, dunRequired); + } + /** Whether mobile network is requested. */ public boolean mobileNetworkRequested() { return (mMobileNetworkCallback != null); } /** Request mobile network if mobile upstream is permitted. */ - public void registerMobileNetworkRequest() { + private void registerMobileNetworkRequest() { if (!isCellularUpstreamPermitted()) { mLog.i("registerMobileNetworkRequest() is not permitted"); releaseMobileNetworkRequest(); @@ -241,14 +274,16 @@ public class UpstreamNetworkMonitor { // TODO: Change the timeout from 0 (no onUnavailable callback) to some // moderate callback timeout. This might be useful for updating some UI. // Additionally, we log a message to aid in any subsequent debugging. - mLog.i("requesting mobile upstream network: " + mobileUpstreamRequest); + mLog.i("requesting mobile upstream network: " + mobileUpstreamRequest + + " mTryCell=" + mTryCell + " mAutoUpstream=" + mAutoUpstream + + " mDunRequired=" + mDunRequired); cm().requestNetwork(mobileUpstreamRequest, 0, legacyType, mHandler, mMobileNetworkCallback); } /** Release mobile network request. */ - public void releaseMobileNetworkRequest() { + private void releaseMobileNetworkRequest() { if (mMobileNetworkCallback == null) return; cm().unregisterNetworkCallback(mMobileNetworkCallback); diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp index 20a5f1855e..edb6356a4a 100644 --- a/Tethering/tests/mts/Android.bp +++ b/Tethering/tests/mts/Android.bp @@ -52,7 +52,7 @@ android_test { // Tag this module as a mts test artifact test_suites: [ "general-tests", - "mts", + "mts-tethering", ], // Include both the 32 and 64 bit versions diff --git a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java new file mode 100644 index 0000000000..14dae5c562 --- /dev/null +++ b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.ip; + +import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN; +import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_MTU; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_RDNSS; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_RA_HEADER_LEN; +import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT; +import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST; +import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN; +import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.app.Instrumentation; +import android.content.Context; +import android.net.INetd; +import android.net.IpPrefix; +import android.net.MacAddress; +import android.net.ip.RouterAdvertisementDaemon.RaParams; +import android.net.util.InterfaceParams; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.net.module.util.Ipv6Utils; +import com.android.net.module.util.Struct; +import com.android.net.module.util.structs.EthernetHeader; +import com.android.net.module.util.structs.Icmpv6Header; +import com.android.net.module.util.structs.Ipv6Header; +import com.android.net.module.util.structs.LlaOption; +import com.android.net.module.util.structs.MtuOption; +import com.android.net.module.util.structs.PrefixInformationOption; +import com.android.net.module.util.structs.RaHeader; +import com.android.net.module.util.structs.RdnssOption; +import com.android.testutils.TapPacketReader; +import com.android.testutils.TapPacketReaderRule; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; + +import java.net.Inet6Address; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.HashSet; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public final class RouterAdvertisementDaemonTest { + private static final String TAG = RouterAdvertisementDaemonTest.class.getSimpleName(); + private static final int DATA_BUFFER_LEN = 4096; + private static final int PACKET_TIMEOUT_MS = 5_000; + + @Rule + public final TapPacketReaderRule mTetheredReader = new TapPacketReaderRule( + DATA_BUFFER_LEN, false /* autoStart */); + + private InterfaceParams mTetheredParams; + private HandlerThread mHandlerThread; + private Handler mHandler; + private TapPacketReader mTetheredPacketReader; + private RouterAdvertisementDaemon mRaDaemon; + + private static INetd sNetd; + + @BeforeClass + public static void setupOnce() { + System.loadLibrary("tetherutilsjni"); + + final Instrumentation inst = InstrumentationRegistry.getInstrumentation(); + final IBinder netdIBinder = + (IBinder) inst.getContext().getSystemService(Context.NETD_SERVICE); + sNetd = INetd.Stub.asInterface(netdIBinder); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mHandlerThread = new HandlerThread(getClass().getSimpleName()); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + + setupTapInterfaces(); + + // Looper must be prepared here since AndroidJUnitRunner runs tests on separate threads. + if (Looper.myLooper() == null) Looper.prepare(); + + mRaDaemon = new RouterAdvertisementDaemon(mTetheredParams); + sNetd.networkAddInterface(INetd.LOCAL_NET_ID, mTetheredParams.name); + } + + @After + public void tearDown() throws Exception { + mTetheredReader.stop(); + if (mHandlerThread != null) { + mHandlerThread.quitSafely(); + mHandlerThread.join(PACKET_TIMEOUT_MS); + } + + if (mTetheredParams != null) { + sNetd.networkRemoveInterface(INetd.LOCAL_NET_ID, mTetheredParams.name); + } + } + + private void setupTapInterfaces() { + // Create tethered test iface. + mTetheredReader.start(mHandler); + mTetheredParams = InterfaceParams.getByName(mTetheredReader.iface.getInterfaceName()); + assertNotNull(mTetheredParams); + mTetheredPacketReader = mTetheredReader.getReader(); + mHandler.post(mTetheredPacketReader::start); + } + + private class TestRaPacket { + final RaParams mNewParams, mOldParams; + + TestRaPacket(final RaParams oldParams, final RaParams newParams) { + mOldParams = oldParams; + mNewParams = newParams; + } + + public boolean isPacketMatched(final byte[] pkt) throws Exception { + if (pkt.length < (ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_RA_HEADER_LEN)) { + return false; + } + final ByteBuffer buf = ByteBuffer.wrap(pkt); + + // Parse Ethernet header + final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf); + if (ethHdr.etherType != ETHER_TYPE_IPV6) return false; + + // Parse IPv6 header + final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf); + assertEquals((ipv6Hdr.vtf >> 28), 6 /* ip version*/); + + final int payLoadLength = pkt.length - ETHER_HEADER_LEN - IPV6_HEADER_LEN; + assertEquals(payLoadLength, ipv6Hdr.payloadLength); + + // Parse ICMPv6 header + final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf); + if (icmpv6Hdr.type != (short) ICMPV6_ROUTER_ADVERTISEMENT) return false; + + // Parse RA header + final RaHeader raHdr = Struct.parse(RaHeader.class, buf); + assertEquals(mNewParams.hopLimit, raHdr.hopLimit); + + while (buf.position() < pkt.length) { + final int currentPos = buf.position(); + final int type = Byte.toUnsignedInt(buf.get()); + final int length = Byte.toUnsignedInt(buf.get()); + switch (type) { + case ICMPV6_ND_OPTION_PIO: + assertEquals(4, length); + + final ByteBuffer pioBuf = ByteBuffer.wrap(buf.array(), currentPos, + Struct.getSize(PrefixInformationOption.class)); + final PrefixInformationOption pio = + Struct.parse(PrefixInformationOption.class, pioBuf); + assertEquals((byte) 0xc0, pio.flags); // L & A set + + final InetAddress address = InetAddress.getByAddress(pio.prefix); + final IpPrefix prefix = new IpPrefix(address, pio.prefixLen); + if (mNewParams.prefixes.contains(prefix)) { + assertTrue(pio.validLifetime > 0); + assertTrue(pio.preferredLifetime > 0); + } else if (mOldParams != null && mOldParams.prefixes.contains(prefix)) { + assertEquals(0, pio.validLifetime); + assertEquals(0, pio.preferredLifetime); + } else { + fail("Unepxected prefix: " + prefix); + } + + // Move ByteBuffer position to the next option. + buf.position(currentPos + Struct.getSize(PrefixInformationOption.class)); + break; + case ICMPV6_ND_OPTION_MTU: + assertEquals(1, length); + + final ByteBuffer mtuBuf = ByteBuffer.wrap(buf.array(), currentPos, + Struct.getSize(MtuOption.class)); + final MtuOption mtu = Struct.parse(MtuOption.class, mtuBuf); + assertEquals(mNewParams.mtu, mtu.mtu); + + // Move ByteBuffer position to the next option. + buf.position(currentPos + Struct.getSize(MtuOption.class)); + break; + case ICMPV6_ND_OPTION_RDNSS: + final int rdnssHeaderLen = Struct.getSize(RdnssOption.class); + final ByteBuffer RdnssBuf = ByteBuffer.wrap(buf.array(), currentPos, + rdnssHeaderLen); + final RdnssOption rdnss = Struct.parse(RdnssOption.class, RdnssBuf); + final String msg = + rdnss.lifetime > 0 ? "Unknown dns" : "Unknown deprecated dns"; + final HashSet<Inet6Address> dnses = + rdnss.lifetime > 0 ? mNewParams.dnses : mOldParams.dnses; + assertNotNull(msg, dnses); + + // Check DNS servers included in this option. + buf.position(currentPos + rdnssHeaderLen); // skip the rdnss option header + final int numOfDnses = (length - 1) / 2; + for (int i = 0; i < numOfDnses; i++) { + byte[] rawAddress = new byte[IPV6_ADDR_LEN]; + buf.get(rawAddress); + final Inet6Address dns = + (Inet6Address) InetAddress.getByAddress(rawAddress); + if (!dnses.contains(dns)) fail("Unexpected dns: " + dns); + } + // Unnecessary to move ByteBuffer position here, since the position has been + // moved forward correctly after reading DNS servers from ByteBuffer. + break; + case ICMPV6_ND_OPTION_SLLA: + // Do nothing, just move ByteBuffer position to the next option. + buf.position(currentPos + Struct.getSize(LlaOption.class)); + break; + default: + fail("Unknown RA option type " + type); + } + } + return true; + } + } + + private RaParams createRaParams(final String ipv6Address) throws Exception { + final RaParams params = new RaParams(); + final Inet6Address address = (Inet6Address) InetAddress.getByName(ipv6Address); + params.dnses.add(address); + params.prefixes.add(new IpPrefix(address, 64)); + + return params; + } + + private boolean assertRaPacket(final TestRaPacket testRa) + throws Exception { + byte[] packet; + while ((packet = mTetheredPacketReader.poll(PACKET_TIMEOUT_MS)) != null) { + if (testRa.isPacketMatched(packet)) return true; + } + return false; + } + + private ByteBuffer createRsPacket(final String srcIp) throws Exception { + final MacAddress dstMac = MacAddress.fromString("33:33:03:04:05:06"); + final MacAddress srcMac = mTetheredParams.macAddr; + final ByteBuffer slla = LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, srcMac); + + return Ipv6Utils.buildRsPacket(srcMac, dstMac, (Inet6Address) InetAddress.getByName(srcIp), + IPV6_ADDR_ALL_NODES_MULTICAST, slla); + } + + @Test + public void testUnSolicitRouterAdvertisement() throws Exception { + assertTrue(mRaDaemon.start()); + final RaParams params1 = createRaParams("2001:1122:3344::5566"); + mRaDaemon.buildNewRa(null, params1); + assertRaPacket(new TestRaPacket(null, params1)); + + final RaParams params2 = createRaParams("2006:3344:5566::7788"); + mRaDaemon.buildNewRa(params1, params2); + assertRaPacket(new TestRaPacket(params1, params2)); + } + + @Test + public void testSolicitRouterAdvertisement() throws Exception { + assertTrue(mRaDaemon.start()); + final RaParams params1 = createRaParams("2001:1122:3344::5566"); + mRaDaemon.buildNewRa(null, params1); + assertRaPacket(new TestRaPacket(null, params1)); + + final ByteBuffer rs = createRsPacket("fe80::1122:3344:5566:7788"); + mTetheredPacketReader.sendResponse(rs); + assertRaPacket(new TestRaPacket(null, params1)); + } +} diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java index b45db7edff..adf1f671ca 100644 --- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java +++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java @@ -247,7 +247,7 @@ public class IpServerTest { lp.setInterfaceName(upstreamIface); dispatchTetherConnectionChanged(upstreamIface, lp, 0); } - reset(mNetd, mCallback, mAddressCoordinator); + reset(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator); when(mAddressCoordinator.requestDownstreamAddress(any(), anyBoolean())).thenReturn( mTestAddress); } @@ -471,10 +471,14 @@ public class IpServerTest { // Telling the state machine about its upstream interface triggers // a little more configuration. dispatchTetherConnectionChanged(UPSTREAM_IFACE); - InOrder inOrder = inOrder(mNetd); + InOrder inOrder = inOrder(mNetd, mBpfCoordinator); + + // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>. + inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE); - verifyNoMoreInteractions(mNetd, mCallback); + + verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator); } @Test @@ -482,12 +486,19 @@ public class IpServerTest { initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE); dispatchTetherConnectionChanged(UPSTREAM_IFACE2); - InOrder inOrder = inOrder(mNetd); + InOrder inOrder = inOrder(mNetd, mBpfCoordinator); + + // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>. + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE); + + // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2>. + inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2); - verifyNoMoreInteractions(mNetd, mCallback); + + verifyNoMoreInteractions(mNetd, mCallback, mBpfCoordinator); } @Test @@ -497,10 +508,20 @@ public class IpServerTest { doThrow(RemoteException.class).when(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2); dispatchTetherConnectionChanged(UPSTREAM_IFACE2); - InOrder inOrder = inOrder(mNetd); + InOrder inOrder = inOrder(mNetd, mBpfCoordinator); + + // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>. + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE); + + // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on + // tetherAddForward. + inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2); + + // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> to fallback. + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2); } @@ -513,11 +534,21 @@ public class IpServerTest { IFACE_NAME, UPSTREAM_IFACE2); dispatchTetherConnectionChanged(UPSTREAM_IFACE2); - InOrder inOrder = inOrder(mNetd); + InOrder inOrder = inOrder(mNetd, mBpfCoordinator); + + // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE>. + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE); + + // Add the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> and expect that failed on + // ipfwdAddInterfaceForward. + inOrder.verify(mBpfCoordinator).maybeAttachProgram(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).tetherAddForward(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).ipfwdAddInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2); + + // Remove the forwarding pair <IFACE_NAME, UPSTREAM_IFACE2> to fallback. + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE2); inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE2); } @@ -527,19 +558,22 @@ public class IpServerTest { initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE); dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED); - InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator); + InOrder inOrder = inOrder(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator); + inOrder.verify(mBpfCoordinator).maybeDetachProgram(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).ipfwdRemoveInterfaceForward(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mNetd).tetherRemoveForward(IFACE_NAME, UPSTREAM_IFACE); + inOrder.verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer); inOrder.verify(mNetd).tetherApplyDnsInterfaces(); inOrder.verify(mNetd).tetherInterfaceRemove(IFACE_NAME); inOrder.verify(mNetd).networkRemoveInterface(INetd.LOCAL_NET_ID, IFACE_NAME); inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName))); inOrder.verify(mAddressCoordinator).releaseDownstream(any()); + inOrder.verify(mBpfCoordinator).stopMonitoring(mIpServer); inOrder.verify(mCallback).updateInterfaceState( mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR); inOrder.verify(mCallback).updateLinkProperties( eq(mIpServer), any(LinkProperties.class)); - verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator); + verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator, mBpfCoordinator); } @Test diff --git a/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java b/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java index 91c7771cc7..9968b5f173 100644 --- a/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java +++ b/Tethering/tests/unit/src/android/net/util/TetheringUtilsTest.java @@ -17,27 +17,49 @@ package android.net.util; import static android.net.TetheringManager.TETHERING_USB; import static android.net.TetheringManager.TETHERING_WIFI; +import static android.system.OsConstants.AF_UNIX; +import static android.system.OsConstants.EAGAIN; +import static android.system.OsConstants.SOCK_CLOEXEC; +import static android.system.OsConstants.SOCK_DGRAM; +import static android.system.OsConstants.SOCK_NONBLOCK; +import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import android.net.LinkAddress; +import android.net.MacAddress; import android.net.TetheringRequestParcel; +import android.system.ErrnoException; +import android.system.Os; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.net.module.util.Ipv6Utils; +import com.android.net.module.util.NetworkStackConstants; +import com.android.net.module.util.Struct; +import com.android.net.module.util.structs.EthernetHeader; +import com.android.net.module.util.structs.Icmpv6Header; +import com.android.net.module.util.structs.Ipv6Header; import com.android.testutils.MiscAsserts; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.FileDescriptor; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.nio.ByteBuffer; + @RunWith(AndroidJUnit4.class) @SmallTest public class TetheringUtilsTest { private static final LinkAddress TEST_SERVER_ADDR = new LinkAddress("192.168.43.1/24"); private static final LinkAddress TEST_CLIENT_ADDR = new LinkAddress("192.168.43.5/24"); + private static final int PACKET_SIZE = 1500; + private TetheringRequestParcel mTetheringRequest; @Before @@ -84,4 +106,82 @@ public class TetheringUtilsTest { MiscAsserts.assertFieldCountEquals(5, TetheringRequestParcel.class); } + + // Writes the specified packet to a filedescriptor, skipping the Ethernet header. + // Needed because the Ipv6Utils methods for building packets always include the Ethernet header, + // but socket filters applied by TetheringUtils expect the packet to start from the IP header. + private int writePacket(FileDescriptor fd, ByteBuffer pkt) throws Exception { + pkt.flip(); + int offset = Struct.getSize(EthernetHeader.class); + int len = pkt.capacity() - offset; + return Os.write(fd, pkt.array(), offset, len); + } + + // Reads a packet from the filedescriptor. + private ByteBuffer readIpPacket(FileDescriptor fd) throws Exception { + ByteBuffer buf = ByteBuffer.allocate(PACKET_SIZE); + Os.read(fd, buf); + return buf; + } + + private interface SocketFilter { + void apply(FileDescriptor fd) throws Exception; + } + + private ByteBuffer checkIcmpSocketFilter(ByteBuffer passed, ByteBuffer dropped, + SocketFilter filter) throws Exception { + FileDescriptor in = new FileDescriptor(); + FileDescriptor out = new FileDescriptor(); + Os.socketpair(AF_UNIX, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, in, out); + + // Before the filter is applied, it doesn't drop anything. + int len = writePacket(out, dropped); + ByteBuffer received = readIpPacket(in); + assertEquals(len, received.position()); + + // Install the socket filter. Then write two packets, the first expected to be dropped and + // the second expected to be passed. Check that only the second makes it through. + filter.apply(in); + writePacket(out, dropped); + len = writePacket(out, passed); + received = readIpPacket(in); + assertEquals(len, received.position()); + received.flip(); + + // Check there are no more packets to read. + try { + readIpPacket(in); + } catch (ErrnoException expected) { + assertEquals(EAGAIN, expected.errno); + } + + return received; + } + + @Test + public void testIcmpSocketFilters() throws Exception { + MacAddress mac1 = MacAddress.fromString("11:22:33:44:55:66"); + MacAddress mac2 = MacAddress.fromString("aa:bb:cc:dd:ee:ff"); + Inet6Address ll1 = (Inet6Address) InetAddress.getByName("fe80::1"); + Inet6Address ll2 = (Inet6Address) InetAddress.getByName("fe80::abcd"); + Inet6Address allRouters = NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST; + + final ByteBuffer na = Ipv6Utils.buildNaPacket(mac1, mac2, ll1, ll2, 0, ll1); + final ByteBuffer ns = Ipv6Utils.buildNsPacket(mac1, mac2, ll1, ll2, ll1); + final ByteBuffer rs = Ipv6Utils.buildRsPacket(mac1, mac2, ll1, allRouters); + + ByteBuffer received = checkIcmpSocketFilter(na /* passed */, rs /* dropped */, + TetheringUtils::setupNaSocket); + + Struct.parse(Ipv6Header.class, received); // Skip IPv6 header. + Icmpv6Header icmpv6 = Struct.parse(Icmpv6Header.class, received); + assertEquals(NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT, icmpv6.type); + + received = checkIcmpSocketFilter(ns /* passed */, rs /* dropped */, + TetheringUtils::setupNsSocket); + + Struct.parse(Ipv6Header.class, received); // Skip IPv6 header. + icmpv6 = Struct.parse(Icmpv6Header.class, received); + assertEquals(NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION, icmpv6.type); + } } diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java index 1270e50c42..293d0df67e 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java @@ -26,9 +26,12 @@ import static android.net.NetworkStats.UID_TETHERING; import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED; import static android.system.OsConstants.ETH_P_IPV6; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker; import static com.android.networkstack.tethering.BpfCoordinator.StatsType; import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE; import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_UID; +import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM; +import static com.android.networkstack.tethering.BpfUtils.UPSTREAM; import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS; import static org.junit.Assert.assertEquals; @@ -70,13 +73,17 @@ import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.net.module.util.NetworkStackConstants; import com.android.net.module.util.Struct; import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule; +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import com.android.testutils.TestableNetworkStatsProviderCbBinder; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -84,6 +91,7 @@ import org.mockito.ArgumentMatcher; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; import java.net.Inet6Address; import java.net.InetAddress; @@ -97,6 +105,9 @@ import java.util.function.BiConsumer; @RunWith(AndroidJUnit4.class) @SmallTest public class BpfCoordinatorTest { + @Rule + public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(); + private static final int DOWNSTREAM_IFINDEX = 10; private static final MacAddress DOWNSTREAM_MAC = MacAddress.ALL_ZEROS_ADDRESS; private static final InetAddress NEIGH_A = InetAddresses.parseNumericAddress("2001:db8::1"); @@ -150,6 +161,12 @@ public class BpfCoordinatorTest { // BpfMap#getValue treats that the entry is not found as no error. return mMap.get(key); } + + @Override + public void clear() throws ErrnoException { + // TODO: consider using mocked #getFirstKey and #deleteEntry to implement. + mMap.clear(); + } }; @Mock private NetworkStatsManager mStatsManager; @@ -988,6 +1005,24 @@ public class BpfCoordinatorTest { @Test @IgnoreUpTo(Build.VERSION_CODES.R) + public void testBpfDisabledbyNoBpfDownstream4Map() throws Exception { + setupFunctioningNetdInterface(); + doReturn(null).when(mDeps).getBpfDownstream4Map(); + + checkBpfDisabled(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testBpfDisabledbyNoBpfUpstream4Map() throws Exception { + setupFunctioningNetdInterface(); + doReturn(null).when(mDeps).getBpfUpstream4Map(); + + checkBpfDisabled(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) public void testBpfDisabledbyNoBpfStatsMap() throws Exception { setupFunctioningNetdInterface(); doReturn(null).when(mDeps).getBpfStatsMap(); @@ -1005,6 +1040,73 @@ public class BpfCoordinatorTest { } @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testBpfMapClear() throws Exception { + setupFunctioningNetdInterface(); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + verify(mBpfDownstream4Map).clear(); + verify(mBpfUpstream4Map).clear(); + verify(mBpfDownstream6Map).clear(); + verify(mBpfUpstream6Map).clear(); + verify(mBpfStatsMap).clear(); + verify(mBpfLimitMap).clear(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testAttachDetachBpfProgram() throws Exception { + setupFunctioningNetdInterface(); + + // Static mocking for BpfUtils. + MockitoSession mockSession = ExtendedMockito.mockitoSession() + .mockStatic(BpfUtils.class) + .startMocking(); + try { + final String intIface1 = "wlan1"; + final String intIface2 = "rndis0"; + final String extIface = "rmnet_data0"; + final BpfUtils mockMarkerBpfUtils = staticMockMarker(BpfUtils.class); + final BpfCoordinator coordinator = makeBpfCoordinator(); + + // [1] Add the forwarding pair <wlan1, rmnet_data0>. Expect that attach both wlan1 and + // rmnet_data0. + coordinator.maybeAttachProgram(intIface1, extIface); + ExtendedMockito.verify(() -> BpfUtils.attachProgram(extIface, DOWNSTREAM)); + ExtendedMockito.verify(() -> BpfUtils.attachProgram(intIface1, UPSTREAM)); + ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); + ExtendedMockito.clearInvocations(mockMarkerBpfUtils); + + // [2] Add the forwarding pair <wlan1, rmnet_data0> again. Expect no more action. + coordinator.maybeAttachProgram(intIface1, extIface); + ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); + ExtendedMockito.clearInvocations(mockMarkerBpfUtils); + + // [3] Add the forwarding pair <rndis0, rmnet_data0>. Expect that attach rndis0 only. + coordinator.maybeAttachProgram(intIface2, extIface); + ExtendedMockito.verify(() -> BpfUtils.attachProgram(intIface2, UPSTREAM)); + ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); + ExtendedMockito.clearInvocations(mockMarkerBpfUtils); + + // [4] Remove the forwarding pair <rndis0, rmnet_data0>. Expect detach rndis0 only. + coordinator.maybeDetachProgram(intIface2, extIface); + ExtendedMockito.verify(() -> BpfUtils.detachProgram(intIface2)); + ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); + ExtendedMockito.clearInvocations(mockMarkerBpfUtils); + + // [5] Remove the forwarding pair <wlan1, rmnet_data0>. Expect that detach both wlan1 + // and rmnet_data0. + coordinator.maybeDetachProgram(intIface1, extIface); + ExtendedMockito.verify(() -> BpfUtils.detachProgram(extIface)); + ExtendedMockito.verify(() -> BpfUtils.detachProgram(intIface1)); + ExtendedMockito.verifyNoMoreInteractions(mockMarkerBpfUtils); + ExtendedMockito.clearInvocations(mockMarkerBpfUtils); + } finally { + mockSession.finishMocking(); + } + } + + @Test public void testTetheringConfigSetPollingInterval() throws Exception { setupFunctioningNetdInterface(); @@ -1054,6 +1156,7 @@ public class BpfCoordinatorTest { } @Test + @IgnoreUpTo(Build.VERSION_CODES.R) public void testStartStopConntrackMonitoring() throws Exception { setupFunctioningNetdInterface(); @@ -1074,6 +1177,23 @@ public class BpfCoordinatorTest { } @Test + @IgnoreUpTo(Build.VERSION_CODES.Q) + @IgnoreAfter(Build.VERSION_CODES.R) + // Only run this test on Android R. + public void testStartStopConntrackMonitoring_R() throws Exception { + setupFunctioningNetdInterface(); + + final BpfCoordinator coordinator = makeBpfCoordinator(); + + coordinator.startMonitoring(mIpServer); + verify(mConntrackMonitor, never()).start(); + + coordinator.stopMonitoring(mIpServer); + verify(mConntrackMonitor, never()).stop(); + } + + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) public void testStartStopConntrackMonitoringWithTwoDownstreamIfaces() throws Exception { setupFunctioningNetdInterface(); diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java index 3a6350c026..a7287a2d5c 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java @@ -16,29 +16,32 @@ package com.android.networkstack.tethering; -import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; - import static org.junit.Assert.assertFalse; import static org.junit.Assert.fail; import android.content.Context; +import android.content.Intent; import android.net.ConnectivityManager; import android.net.IConnectivityManager; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; +import android.net.NetworkInfo; import android.net.NetworkRequest; import android.os.Handler; +import android.os.UserHandle; +import android.util.ArrayMap; -import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Objects; -import java.util.Set; /** * Simulates upstream switching and sending NetworkCallbacks and CONNECTIVITY_ACTION broadcasts. * + * Unlike any real networking code, this class is single-threaded and entirely synchronous. + * The effects of all method calls (including sending fake broadcasts, sending callbacks, etc.) are + * performed immediately on the caller's thread before returning. + * * TODO: this duplicates a fair amount of code from ConnectivityManager and ConnectivityService. * Consider using a ConnectivityService object instead, as used in ConnectivityServiceTest. * @@ -55,44 +58,63 @@ import java.util.Set; * that state changes), this may become less important or unnecessary. */ public class TestConnectivityManager extends ConnectivityManager { - public Map<NetworkCallback, Handler> allCallbacks = new HashMap<>(); - public Set<NetworkCallback> trackingDefault = new HashSet<>(); - public TestNetworkAgent defaultNetwork = null; - public Map<NetworkCallback, NetworkRequest> listening = new HashMap<>(); - public Map<NetworkCallback, NetworkRequest> requested = new HashMap<>(); - public Map<NetworkCallback, Integer> legacyTypeMap = new HashMap<>(); + final Map<NetworkCallback, NetworkRequestInfo> mAllCallbacks = new ArrayMap<>(); + final Map<NetworkCallback, NetworkRequestInfo> mTrackingDefault = new ArrayMap<>(); + final Map<NetworkCallback, NetworkRequestInfo> mListening = new ArrayMap<>(); + final Map<NetworkCallback, NetworkRequestInfo> mRequested = new ArrayMap<>(); + final Map<NetworkCallback, Integer> mLegacyTypeMap = new ArrayMap<>(); private final NetworkRequest mDefaultRequest; - private int mNetworkId = 100; + private final Context mContext; + private int mNetworkId = 100; + private TestNetworkAgent mDefaultNetwork = null; + + /** + * Constructs a TestConnectivityManager. + * @param ctx the context to use. Must be a fake or a mock because otherwise the test will + * attempt to send real broadcasts and resulting in permission denials. + * @param svc an IConnectivityManager. Should be a fake or a mock. + * @param defaultRequest the default NetworkRequest that will be used by Tethering. + */ public TestConnectivityManager(Context ctx, IConnectivityManager svc, NetworkRequest defaultRequest) { super(ctx, svc); + mContext = ctx; mDefaultRequest = defaultRequest; } + class NetworkRequestInfo { + public final NetworkRequest request; + public final Handler handler; + NetworkRequestInfo(NetworkRequest r, Handler h) { + request = r; + handler = h; + } + } + boolean hasNoCallbacks() { - return allCallbacks.isEmpty() - && trackingDefault.isEmpty() - && listening.isEmpty() - && requested.isEmpty() - && legacyTypeMap.isEmpty(); + return mAllCallbacks.isEmpty() + && mTrackingDefault.isEmpty() + && mListening.isEmpty() + && mRequested.isEmpty() + && mLegacyTypeMap.isEmpty(); } boolean onlyHasDefaultCallbacks() { - return (allCallbacks.size() == 1) - && (trackingDefault.size() == 1) - && listening.isEmpty() - && requested.isEmpty() - && legacyTypeMap.isEmpty(); + return (mAllCallbacks.size() == 1) + && (mTrackingDefault.size() == 1) + && mListening.isEmpty() + && mRequested.isEmpty() + && mLegacyTypeMap.isEmpty(); } boolean isListeningForAll() { final NetworkCapabilities empty = new NetworkCapabilities(); empty.clearAll(); - for (NetworkRequest req : listening.values()) { - if (req.networkCapabilities.equalRequestableCapabilities(empty)) { + for (NetworkRequestInfo nri : mListening.values()) { + if (nri.request.networkCapabilities.equalRequestableCapabilities(empty)) { return true; } } @@ -103,33 +125,52 @@ public class TestConnectivityManager extends ConnectivityManager { return ++mNetworkId; } - void makeDefaultNetwork(TestNetworkAgent agent) { - if (Objects.equals(defaultNetwork, agent)) return; - - final TestNetworkAgent formerDefault = defaultNetwork; - defaultNetwork = agent; + private void sendDefaultNetworkBroadcasts(TestNetworkAgent formerDefault, + TestNetworkAgent defaultNetwork) { + if (formerDefault != null) { + sendConnectivityAction(formerDefault.legacyType, false /* connected */); + } + if (defaultNetwork != null) { + sendConnectivityAction(defaultNetwork.legacyType, true /* connected */); + } + } - for (NetworkCallback cb : trackingDefault) { + private void sendDefaultNetworkCallbacks(TestNetworkAgent formerDefault, + TestNetworkAgent defaultNetwork) { + for (NetworkCallback cb : mTrackingDefault.keySet()) { + final NetworkRequestInfo nri = mTrackingDefault.get(cb); if (defaultNetwork != null) { - cb.onAvailable(defaultNetwork.networkId); - cb.onCapabilitiesChanged( - defaultNetwork.networkId, defaultNetwork.networkCapabilities); - cb.onLinkPropertiesChanged( - defaultNetwork.networkId, defaultNetwork.linkProperties); + nri.handler.post(() -> cb.onAvailable(defaultNetwork.networkId)); + nri.handler.post(() -> cb.onCapabilitiesChanged( + defaultNetwork.networkId, defaultNetwork.networkCapabilities)); + nri.handler.post(() -> cb.onLinkPropertiesChanged( + defaultNetwork.networkId, defaultNetwork.linkProperties)); + } else if (formerDefault != null) { + nri.handler.post(() -> cb.onLost(formerDefault.networkId)); } } } + void makeDefaultNetwork(TestNetworkAgent agent) { + if (Objects.equals(mDefaultNetwork, agent)) return; + + final TestNetworkAgent formerDefault = mDefaultNetwork; + mDefaultNetwork = agent; + + sendDefaultNetworkCallbacks(formerDefault, mDefaultNetwork); + sendDefaultNetworkBroadcasts(formerDefault, mDefaultNetwork); + } + @Override public void requestNetwork(NetworkRequest req, NetworkCallback cb, Handler h) { - assertFalse(allCallbacks.containsKey(cb)); - allCallbacks.put(cb, h); + assertFalse(mAllCallbacks.containsKey(cb)); + mAllCallbacks.put(cb, new NetworkRequestInfo(req, h)); if (mDefaultRequest.equals(req)) { - assertFalse(trackingDefault.contains(cb)); - trackingDefault.add(cb); + assertFalse(mTrackingDefault.containsKey(cb)); + mTrackingDefault.put(cb, new NetworkRequestInfo(req, h)); } else { - assertFalse(requested.containsKey(cb)); - requested.put(cb, req); + assertFalse(mRequested.containsKey(cb)); + mRequested.put(cb, new NetworkRequestInfo(req, h)); } } @@ -141,22 +182,22 @@ public class TestConnectivityManager extends ConnectivityManager { @Override public void requestNetwork(NetworkRequest req, int timeoutMs, int legacyType, Handler h, NetworkCallback cb) { - assertFalse(allCallbacks.containsKey(cb)); - allCallbacks.put(cb, h); - assertFalse(requested.containsKey(cb)); - requested.put(cb, req); - assertFalse(legacyTypeMap.containsKey(cb)); + assertFalse(mAllCallbacks.containsKey(cb)); + mAllCallbacks.put(cb, new NetworkRequestInfo(req, h)); + assertFalse(mRequested.containsKey(cb)); + mRequested.put(cb, new NetworkRequestInfo(req, h)); + assertFalse(mLegacyTypeMap.containsKey(cb)); if (legacyType != ConnectivityManager.TYPE_NONE) { - legacyTypeMap.put(cb, legacyType); + mLegacyTypeMap.put(cb, legacyType); } } @Override public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb, Handler h) { - assertFalse(allCallbacks.containsKey(cb)); - allCallbacks.put(cb, h); - assertFalse(listening.containsKey(cb)); - listening.put(cb, req); + assertFalse(mAllCallbacks.containsKey(cb)); + mAllCallbacks.put(cb, new NetworkRequestInfo(req, h)); + assertFalse(mListening.containsKey(cb)); + mListening.put(cb, new NetworkRequestInfo(req, h)); } @Override @@ -176,57 +217,113 @@ public class TestConnectivityManager extends ConnectivityManager { @Override public void unregisterNetworkCallback(NetworkCallback cb) { - if (trackingDefault.contains(cb)) { - trackingDefault.remove(cb); - } else if (listening.containsKey(cb)) { - listening.remove(cb); - } else if (requested.containsKey(cb)) { - requested.remove(cb); - legacyTypeMap.remove(cb); + if (mTrackingDefault.containsKey(cb)) { + mTrackingDefault.remove(cb); + } else if (mListening.containsKey(cb)) { + mListening.remove(cb); + } else if (mRequested.containsKey(cb)) { + mRequested.remove(cb); + mLegacyTypeMap.remove(cb); } else { fail("Unexpected callback removed"); } - allCallbacks.remove(cb); + mAllCallbacks.remove(cb); - assertFalse(allCallbacks.containsKey(cb)); - assertFalse(trackingDefault.contains(cb)); - assertFalse(listening.containsKey(cb)); - assertFalse(requested.containsKey(cb)); + assertFalse(mAllCallbacks.containsKey(cb)); + assertFalse(mTrackingDefault.containsKey(cb)); + assertFalse(mListening.containsKey(cb)); + assertFalse(mRequested.containsKey(cb)); + } + + private void sendConnectivityAction(int type, boolean connected) { + NetworkInfo ni = new NetworkInfo(type, 0 /* subtype */, getNetworkTypeName(type), + "" /* subtypeName */); + NetworkInfo.DetailedState state = connected + ? NetworkInfo.DetailedState.CONNECTED + : NetworkInfo.DetailedState.DISCONNECTED; + ni.setDetailedState(state, "" /* reason */, "" /* extraInfo */); + Intent intent = new Intent(CONNECTIVITY_ACTION); + intent.putExtra(EXTRA_NETWORK_INFO, ni); + mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); } public static class TestNetworkAgent { public final TestConnectivityManager cm; public final Network networkId; - public final int transportType; public final NetworkCapabilities networkCapabilities; public final LinkProperties linkProperties; + // TODO: delete when tethering no longer uses CONNECTIVITY_ACTION. + public final int legacyType; - public TestNetworkAgent(TestConnectivityManager cm, int transportType) { + public TestNetworkAgent(TestConnectivityManager cm, NetworkCapabilities nc) { this.cm = cm; this.networkId = new Network(cm.getNetworkId()); - this.transportType = transportType; - networkCapabilities = new NetworkCapabilities(); - networkCapabilities.addTransportType(transportType); - networkCapabilities.addCapability(NET_CAPABILITY_INTERNET); + networkCapabilities = copy(nc); linkProperties = new LinkProperties(); + legacyType = toLegacyType(nc); + } + + public TestNetworkAgent(TestConnectivityManager cm, UpstreamNetworkState state) { + this.cm = cm; + networkId = state.network; + networkCapabilities = state.networkCapabilities; + linkProperties = state.linkProperties; + this.legacyType = toLegacyType(networkCapabilities); + } + + private static int toLegacyType(NetworkCapabilities nc) { + for (int type = 0; type < ConnectivityManager.TYPE_TEST; type++) { + if (matchesLegacyType(nc, type)) return type; + } + throw new IllegalArgumentException(("Can't determine legacy type for: ") + nc); + } + + private static boolean matchesLegacyType(NetworkCapabilities nc, int legacyType) { + final NetworkCapabilities typeNc; + try { + typeNc = ConnectivityManager.networkCapabilitiesForType(legacyType); + } catch (IllegalArgumentException e) { + // networkCapabilitiesForType does not support all legacy types. + return false; + } + return typeNc.satisfiedByNetworkCapabilities(nc); + } + + private boolean matchesLegacyType(int legacyType) { + return matchesLegacyType(networkCapabilities, legacyType); } public void fakeConnect() { - for (NetworkCallback cb : cm.listening.keySet()) { - cb.onAvailable(networkId); - cb.onCapabilitiesChanged(networkId, copy(networkCapabilities)); - cb.onLinkPropertiesChanged(networkId, copy(linkProperties)); + for (NetworkRequestInfo nri : cm.mRequested.values()) { + if (matchesLegacyType(nri.request.legacyType)) { + cm.sendConnectivityAction(legacyType, true /* connected */); + // In practice, a given network can match only one legacy type. + break; + } + } + for (NetworkCallback cb : cm.mListening.keySet()) { + final NetworkRequestInfo nri = cm.mListening.get(cb); + nri.handler.post(() -> cb.onAvailable(networkId)); + nri.handler.post(() -> cb.onCapabilitiesChanged( + networkId, copy(networkCapabilities))); + nri.handler.post(() -> cb.onLinkPropertiesChanged(networkId, copy(linkProperties))); } } public void fakeDisconnect() { - for (NetworkCallback cb : cm.listening.keySet()) { + for (NetworkRequestInfo nri : cm.mRequested.values()) { + if (matchesLegacyType(nri.request.legacyType)) { + cm.sendConnectivityAction(legacyType, false /* connected */); + break; + } + } + for (NetworkCallback cb : cm.mListening.keySet()) { cb.onLost(networkId); } } public void sendLinkProperties() { - for (NetworkCallback cb : cm.listening.keySet()) { + for (NetworkCallback cb : cm.mListening.keySet()) { cb.onLinkPropertiesChanged(networkId, copy(linkProperties)); } } 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 40b7c55f71..a7d61bd341 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java @@ -78,6 +78,7 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -165,6 +166,7 @@ import com.android.internal.util.ArrayUtils; import com.android.internal.util.StateMachine; import com.android.internal.util.test.BroadcastInterceptingContext; import com.android.internal.util.test.FakeSettingsProvider; +import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent; import com.android.testutils.MiscAsserts; import org.junit.After; @@ -174,6 +176,7 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -501,6 +504,7 @@ public class TetheringTest { prop.setInterfaceName(interfaceName); if (withIPv4) { + prop.addLinkAddress(new LinkAddress("10.1.2.3/15")); prop.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), InetAddresses.parseNumericAddress("10.0.0.1"), interfaceName, RTN_UNICAST)); @@ -614,6 +618,7 @@ public class TetheringTest { when(mOffloadHardwareInterface.getForwardedStats(any())).thenReturn(mForwardedStats); mServiceContext = new TestContext(mContext); + mServiceContext.setUseRegisteredHandlers(true); mContentResolver = new MockContentResolver(mServiceContext); mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); setTetheringSupported(true /* supported */); @@ -712,6 +717,7 @@ public class TetheringTest { final Intent intent = new Intent(WifiManager.WIFI_AP_STATE_CHANGED_ACTION); intent.putExtra(EXTRA_WIFI_AP_STATE, state); mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + mLooper.dispatchAll(); } private void sendWifiApStateChanged(int state, String ifname, int ipmode) { @@ -720,6 +726,7 @@ public class TetheringTest { intent.putExtra(EXTRA_WIFI_AP_INTERFACE_NAME, ifname); intent.putExtra(EXTRA_WIFI_AP_MODE, ipmode); mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + mLooper.dispatchAll(); } private static final String[] P2P_RECEIVER_PERMISSIONS_FOR_BROADCAST = { @@ -746,6 +753,7 @@ public class TetheringTest { mServiceContext.sendBroadcastAsUserMultiplePermissions(intent, UserHandle.ALL, P2P_RECEIVER_PERMISSIONS_FOR_BROADCAST); + mLooper.dispatchAll(); } private void sendUsbBroadcast(boolean connected, boolean configured, boolean function, @@ -759,11 +767,13 @@ public class TetheringTest { intent.putExtra(USB_FUNCTION_NCM, function); } mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + mLooper.dispatchAll(); } private void sendConfigurationChanged() { final Intent intent = new Intent(Intent.ACTION_CONFIGURATION_CHANGED); mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); + mLooper.dispatchAll(); } private void verifyDefaultNetworkRequestFiled() { @@ -805,7 +815,6 @@ public class TetheringTest { mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true); } sendWifiApStateChanged(WIFI_AP_STATE_ENABLED); - mLooper.dispatchAll(); // If, and only if, Tethering received an interface status changed then // it creates a IpServer and sends out a broadcast indicating that the @@ -830,9 +839,7 @@ public class TetheringTest { mTethering.interfaceStatusChanged(TEST_NCM_IFNAME, true); } - private void prepareUsbTethering(UpstreamNetworkState upstreamState) { - initTetheringUpstream(upstreamState); - + private void prepareUsbTethering() { // Emulate pressing the USB tethering button in Settings UI. final TetheringRequestParcel request = createTetheringRequestParcel(TETHERING_USB); mTethering.startTethering(request, null); @@ -847,14 +854,14 @@ public class TetheringTest { @Test public void testUsbConfiguredBroadcastStartsTethering() throws Exception { UpstreamNetworkState upstreamState = buildMobileIPv4UpstreamState(); - prepareUsbTethering(upstreamState); + initTetheringUpstream(upstreamState); + prepareUsbTethering(); // This should produce no activity of any kind. verifyNoMoreInteractions(mNetd); // Pretend we then receive USB configured broadcast. sendUsbBroadcast(true, true, true, TETHERING_USB); - mLooper.dispatchAll(); // Now we should see the start of tethering mechanics (in this case: // tetherMatchingInterfaces() which starts by fetching all interfaces). verify(mNetd, times(1)).interfaceGetList(); @@ -883,7 +890,6 @@ public class TetheringTest { mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true); } sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_LOCAL_ONLY); - mLooper.dispatchAll(); verifyInterfaceServingModeStarted(TEST_WLAN_IFNAME); verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER); @@ -942,9 +948,9 @@ public class TetheringTest { } private void runUsbTethering(UpstreamNetworkState upstreamState) { - prepareUsbTethering(upstreamState); + initTetheringUpstream(upstreamState); + prepareUsbTethering(); sendUsbBroadcast(true, true, true, TETHERING_USB); - mLooper.dispatchAll(); } private void assertSetIfaceToDadProxy(final int numOfCalls, final String ifaceName) { @@ -1083,10 +1089,75 @@ public class TetheringTest { verify(mUpstreamNetworkMonitor, times(1)).setCurrentUpstream(upstreamState.network); } + @Test + public void testAutomaticUpstreamSelection() throws Exception { + // Enable automatic upstream selection. + when(mResources.getBoolean(R.bool.config_tether_upstream_automatic)).thenReturn(true); + sendConfigurationChanged(); + mLooper.dispatchAll(); + + InOrder inOrder = inOrder(mCm, mUpstreamNetworkMonitor); + + // Start USB tethering with no current upstream. + prepareUsbTethering(); + sendUsbBroadcast(true, true, true, TETHERING_USB); + inOrder.verify(mUpstreamNetworkMonitor).startObserveAllNetworks(); + inOrder.verify(mUpstreamNetworkMonitor).setTryCell(true); + + // Pretend cellular connected and expect the upstream to be set. + TestNetworkAgent mobile = new TestNetworkAgent(mCm, buildMobileDualStackUpstreamState()); + mobile.fakeConnect(); + mCm.makeDefaultNetwork(mobile); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId); + + // Switch upstreams a few times. + // TODO: there may be a race where if the effects of the CONNECTIVITY_ACTION happen before + // UpstreamNetworkMonitor gets onCapabilitiesChanged on CALLBACK_DEFAULT_INTERNET, the + // upstream does not change. Extend TestConnectivityManager to simulate this condition and + // write a test for this. + TestNetworkAgent wifi = new TestNetworkAgent(mCm, buildWifiUpstreamState()); + wifi.fakeConnect(); + mCm.makeDefaultNetwork(wifi); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(wifi.networkId); + + mCm.makeDefaultNetwork(mobile); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId); + + // Wifi disconnecting should not have any affect since it's not the current upstream. + wifi.fakeDisconnect(); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor, never()).setCurrentUpstream(any()); + + // Lose and regain upstream. + assertTrue(mUpstreamNetworkMonitor.getCurrentPreferredUpstream().linkProperties + .hasIPv4Address()); + mobile.fakeDisconnect(); + mCm.makeDefaultNetwork(null); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null); + + mobile = new TestNetworkAgent(mCm, buildMobile464xlatUpstreamState()); + mobile.fakeConnect(); + mCm.makeDefaultNetwork(mobile); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(mobile.networkId); + + // Check the IP addresses to ensure that the upstream is indeed not the same as the previous + // mobile upstream, even though the netId is (unrealistically) the same. + assertFalse(mUpstreamNetworkMonitor.getCurrentPreferredUpstream().linkProperties + .hasIPv4Address()); + mobile.fakeDisconnect(); + mCm.makeDefaultNetwork(null); + mLooper.dispatchAll(); + inOrder.verify(mUpstreamNetworkMonitor).setCurrentUpstream(null); + } + private void runNcmTethering() { prepareNcmTethering(); sendUsbBroadcast(true, true, true, TETHERING_NCM); - mLooper.dispatchAll(); } @Test @@ -1134,7 +1205,6 @@ public class TetheringTest { // tethering mode is to be started. mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true); sendWifiApStateChanged(WIFI_AP_STATE_ENABLED); - mLooper.dispatchAll(); // There is 1 IpServer state change event: STATE_AVAILABLE verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE); @@ -1162,7 +1232,6 @@ public class TetheringTest { // tethering mode is to be started. mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true); sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED); - mLooper.dispatchAll(); verifyInterfaceServingModeStarted(TEST_WLAN_IFNAME); verifyTetheringBroadcast(TEST_WLAN_IFNAME, EXTRA_AVAILABLE_TETHER); @@ -1180,7 +1249,7 @@ public class TetheringTest { verify(mUpstreamNetworkMonitor, times(1)).startObserveAllNetworks(); // In tethering mode, in the default configuration, an explicit request // for a mobile network is also made. - verify(mUpstreamNetworkMonitor, times(1)).registerMobileNetworkRequest(); + verify(mUpstreamNetworkMonitor, times(1)).setTryCell(true); // There are 2 IpServer state change events: STATE_AVAILABLE -> STATE_TETHERED verify(mNotificationUpdater, times(1)).onDownstreamChanged(DOWNSTREAM_NONE); verify(mNotificationUpdater, times(1)).onDownstreamChanged(eq(1 << TETHERING_WIFI)); @@ -1239,7 +1308,6 @@ public class TetheringTest { // tethering mode is to be started. mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true); sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED); - mLooper.dispatchAll(); // We verify get/set called three times here: twice for setup and once during // teardown because all events happen over the course of the single @@ -1563,7 +1631,6 @@ public class TetheringTest { mTethering.startTethering(createTetheringRequestParcel(TETHERING_WIFI), null); sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED); - mLooper.dispatchAll(); tetherState = callback.pollTetherStatesChanged(); assertArrayEquals(tetherState.tetheredList, new String[] {TEST_WLAN_IFNAME}); callback.expectUpstreamChanged(upstreamState.network); @@ -1585,7 +1652,6 @@ public class TetheringTest { mLooper.dispatchAll(); mTethering.stopTethering(TETHERING_WIFI); sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED); - mLooper.dispatchAll(); tetherState = callback2.pollTetherStatesChanged(); assertArrayEquals(tetherState.availableList, new String[] {TEST_WLAN_IFNAME}); mLooper.dispatchAll(); @@ -1678,7 +1744,6 @@ public class TetheringTest { mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true); } sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME); - mLooper.dispatchAll(); verifyInterfaceServingModeStarted(TEST_P2P_IFNAME); verifyTetheringBroadcast(TEST_P2P_IFNAME, EXTRA_AVAILABLE_TETHER); @@ -1696,7 +1761,6 @@ public class TetheringTest { // is being removed. sendWifiP2pConnectionChanged(false, true, TEST_P2P_IFNAME); mTethering.interfaceRemoved(TEST_P2P_IFNAME); - mLooper.dispatchAll(); verify(mNetd, times(1)).tetherApplyDnsInterfaces(); verify(mNetd, times(1)).tetherInterfaceRemove(TEST_P2P_IFNAME); @@ -1719,7 +1783,6 @@ public class TetheringTest { mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true); } sendWifiP2pConnectionChanged(true, false, TEST_P2P_IFNAME); - mLooper.dispatchAll(); verify(mNetd, never()).interfaceSetCfg(any(InterfaceConfigurationParcel.class)); verify(mNetd, never()).tetherInterfaceAdd(TEST_P2P_IFNAME); @@ -1731,7 +1794,6 @@ public class TetheringTest { // is being removed. sendWifiP2pConnectionChanged(false, false, TEST_P2P_IFNAME); mTethering.interfaceRemoved(TEST_P2P_IFNAME); - mLooper.dispatchAll(); verify(mNetd, never()).tetherApplyDnsInterfaces(); verify(mNetd, never()).tetherInterfaceRemove(TEST_P2P_IFNAME); @@ -1767,7 +1829,6 @@ public class TetheringTest { mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true); } sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME); - mLooper.dispatchAll(); verify(mNetd, never()).interfaceSetCfg(any(InterfaceConfigurationParcel.class)); verify(mNetd, never()).tetherInterfaceAdd(TEST_P2P_IFNAME); @@ -1897,7 +1958,6 @@ public class TetheringTest { // Expect that when USB comes up, the DHCP server is configured with the requested address. mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true); sendUsbBroadcast(true, true, true, TETHERING_USB); - mLooper.dispatchAll(); verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS).times(1)).startWithCallbacks( any(), any()); verify(mNetd).interfaceSetCfg(argThat(cfg -> serverAddr.equals(cfg.ipv4Addr))); @@ -1917,7 +1977,6 @@ public class TetheringTest { verify(mUsbManager, times(1)).setCurrentFunctions(UsbManager.FUNCTION_RNDIS); mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true); sendUsbBroadcast(true, true, true, TETHERING_USB); - mLooper.dispatchAll(); verify(mNetd).interfaceSetCfg(argThat(cfg -> serverAddr.equals(cfg.ipv4Addr))); verify(mIpServerDependencies, times(1)).makeDhcpServer(any(), dhcpParamsCaptor.capture(), any()); @@ -2141,7 +2200,6 @@ public class TetheringTest { mTethering.interfaceStatusChanged(TEST_USB_IFNAME, true); sendUsbBroadcast(true, true, true, TETHERING_USB); - mLooper.dispatchAll(); assertContains(Arrays.asList(mTethering.getTetherableIfaces()), TEST_USB_IFNAME); assertContains(Arrays.asList(mTethering.getTetherableIfaces()), TEST_ETH_IFNAME); assertEquals(TETHER_ERROR_IFACE_CFG_ERROR, mTethering.getLastTetherError(TEST_USB_IFNAME)); @@ -2180,7 +2238,6 @@ public class TetheringTest { // Run local only tethering. mTethering.interfaceStatusChanged(TEST_P2P_IFNAME, true); sendWifiP2pConnectionChanged(true, true, TEST_P2P_IFNAME); - mLooper.dispatchAll(); verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks( any(), dhcpEventCbsCaptor.capture()); eventCallbacks = dhcpEventCbsCaptor.getValue(); @@ -2197,7 +2254,6 @@ public class TetheringTest { // Run wifi tethering. mTethering.interfaceStatusChanged(TEST_WLAN_IFNAME, true); sendWifiApStateChanged(WIFI_AP_STATE_ENABLED, TEST_WLAN_IFNAME, IFACE_IP_MODE_TETHERED); - mLooper.dispatchAll(); verify(mDhcpServer, timeout(DHCPSERVER_START_TIMEOUT_MS)).startWithCallbacks( any(), dhcpEventCbsCaptor.capture()); eventCallbacks = dhcpEventCbsCaptor.getValue(); diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java index e358f5a2cd..bc216925d8 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java @@ -41,7 +41,6 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.content.Context; -import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.IConnectivityManager; import android.net.IpPrefix; @@ -51,13 +50,16 @@ import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.net.util.SharedLog; import android.os.Handler; +import android.os.Looper; import android.os.Message; +import android.os.test.TestLooper; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.State; import com.android.internal.util.StateMachine; +import com.android.networkstack.tethering.TestConnectivityManager.NetworkRequestInfo; import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent; import org.junit.After; @@ -85,6 +87,13 @@ public class UpstreamNetworkMonitorTest { // any specific TRANSPORT_* is sufficient to identify this request. private static final NetworkRequest sDefaultRequest = new NetworkRequest.Builder().build(); + private static final NetworkCapabilities CELL_CAPABILITIES = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_CELLULAR).addCapability(NET_CAPABILITY_INTERNET).build(); + private static final NetworkCapabilities DUN_CAPABILITIES = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_CELLULAR).addCapability(NET_CAPABILITY_DUN).build(); + private static final NetworkCapabilities WIFI_CAPABILITIES = new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_WIFI).addCapability(NET_CAPABILITY_INTERNET).build(); + @Mock private Context mContext; @Mock private EntitlementManager mEntitleMgr; @Mock private IConnectivityManager mCS; @@ -94,6 +103,8 @@ public class UpstreamNetworkMonitorTest { private TestConnectivityManager mCM; private UpstreamNetworkMonitor mUNM; + private final TestLooper mLooper = new TestLooper(); + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); reset(mContext); @@ -103,9 +114,8 @@ public class UpstreamNetworkMonitorTest { when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true); mCM = spy(new TestConnectivityManager(mContext, mCS, sDefaultRequest)); - mSM = new TestStateMachine(); - mUNM = new UpstreamNetworkMonitor( - (ConnectivityManager) mCM, mSM, mLog, EVENT_UNM_UPDATE); + mSM = new TestStateMachine(mLooper.getLooper()); + mUNM = new UpstreamNetworkMonitor(mCM, mSM, mLog, EVENT_UNM_UPDATE); } @After public void tearDown() throws Exception { @@ -127,9 +137,9 @@ public class UpstreamNetworkMonitorTest { assertTrue(mCM.hasNoCallbacks()); assertFalse(mUNM.mobileNetworkRequested()); - mUNM.updateMobileRequiresDun(true); + mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */); assertTrue(mCM.hasNoCallbacks()); - mUNM.updateMobileRequiresDun(false); + mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */); assertTrue(mCM.hasNoCallbacks()); } @@ -139,7 +149,7 @@ public class UpstreamNetworkMonitorTest { mUNM.startTrackDefaultNetwork(sDefaultRequest, mEntitleMgr); mUNM.startObserveAllNetworks(); - assertEquals(1, mCM.trackingDefault.size()); + assertEquals(1, mCM.mTrackingDefault.size()); mUNM.stop(); assertTrue(mCM.onlyHasDefaultCallbacks()); @@ -147,11 +157,11 @@ public class UpstreamNetworkMonitorTest { @Test public void testListensForAllNetworks() throws Exception { - assertTrue(mCM.listening.isEmpty()); + assertTrue(mCM.mListening.isEmpty()); mUNM.startTrackDefaultNetwork(sDefaultRequest, mEntitleMgr); mUNM.startObserveAllNetworks(); - assertFalse(mCM.listening.isEmpty()); + assertFalse(mCM.mListening.isEmpty()); assertTrue(mCM.isListeningForAll()); mUNM.stop(); @@ -174,17 +184,17 @@ public class UpstreamNetworkMonitorTest { @Test public void testRequestsMobileNetwork() throws Exception { assertFalse(mUNM.mobileNetworkRequested()); - assertEquals(0, mCM.requested.size()); + assertEquals(0, mCM.mRequested.size()); mUNM.startObserveAllNetworks(); assertFalse(mUNM.mobileNetworkRequested()); - assertEquals(0, mCM.requested.size()); + assertEquals(0, mCM.mRequested.size()); - mUNM.updateMobileRequiresDun(false); + mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */); assertFalse(mUNM.mobileNetworkRequested()); - assertEquals(0, mCM.requested.size()); + assertEquals(0, mCM.mRequested.size()); - mUNM.registerMobileNetworkRequest(); + mUNM.setTryCell(true); assertTrue(mUNM.mobileNetworkRequested()); assertUpstreamTypeRequested(TYPE_MOBILE_HIPRI); assertFalse(isDunRequested()); @@ -197,16 +207,16 @@ public class UpstreamNetworkMonitorTest { @Test public void testDuplicateMobileRequestsIgnored() throws Exception { assertFalse(mUNM.mobileNetworkRequested()); - assertEquals(0, mCM.requested.size()); + assertEquals(0, mCM.mRequested.size()); mUNM.startObserveAllNetworks(); verify(mCM, times(1)).registerNetworkCallback( any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class)); assertFalse(mUNM.mobileNetworkRequested()); - assertEquals(0, mCM.requested.size()); + assertEquals(0, mCM.mRequested.size()); - mUNM.updateMobileRequiresDun(true); - mUNM.registerMobileNetworkRequest(); + mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */); + mUNM.setTryCell(true); verify(mCM, times(1)).requestNetwork( any(NetworkRequest.class), anyInt(), anyInt(), any(Handler.class), any(NetworkCallback.class)); @@ -216,9 +226,9 @@ public class UpstreamNetworkMonitorTest { assertTrue(isDunRequested()); // Try a few things that must not result in any state change. - mUNM.registerMobileNetworkRequest(); - mUNM.updateMobileRequiresDun(true); - mUNM.registerMobileNetworkRequest(); + mUNM.setTryCell(true); + mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */); + mUNM.setTryCell(true); assertTrue(mUNM.mobileNetworkRequested()); assertUpstreamTypeRequested(TYPE_MOBILE_DUN); @@ -233,17 +243,17 @@ public class UpstreamNetworkMonitorTest { @Test public void testRequestsDunNetwork() throws Exception { assertFalse(mUNM.mobileNetworkRequested()); - assertEquals(0, mCM.requested.size()); + assertEquals(0, mCM.mRequested.size()); mUNM.startObserveAllNetworks(); assertFalse(mUNM.mobileNetworkRequested()); - assertEquals(0, mCM.requested.size()); + assertEquals(0, mCM.mRequested.size()); - mUNM.updateMobileRequiresDun(true); + mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */); assertFalse(mUNM.mobileNetworkRequested()); - assertEquals(0, mCM.requested.size()); + assertEquals(0, mCM.mRequested.size()); - mUNM.registerMobileNetworkRequest(); + mUNM.setTryCell(true); assertTrue(mUNM.mobileNetworkRequested()); assertUpstreamTypeRequested(TYPE_MOBILE_DUN); assertTrue(isDunRequested()); @@ -258,18 +268,18 @@ public class UpstreamNetworkMonitorTest { mUNM.startObserveAllNetworks(); // Test going from no-DUN to DUN correctly re-registers callbacks. - mUNM.updateMobileRequiresDun(false); - mUNM.registerMobileNetworkRequest(); + mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */); + mUNM.setTryCell(true); assertTrue(mUNM.mobileNetworkRequested()); assertUpstreamTypeRequested(TYPE_MOBILE_HIPRI); assertFalse(isDunRequested()); - mUNM.updateMobileRequiresDun(true); + mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */); assertTrue(mUNM.mobileNetworkRequested()); assertUpstreamTypeRequested(TYPE_MOBILE_DUN); assertTrue(isDunRequested()); // Test going from DUN to no-DUN correctly re-registers callbacks. - mUNM.updateMobileRequiresDun(false); + mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */); assertTrue(mUNM.mobileNetworkRequested()); assertUpstreamTypeRequested(TYPE_MOBILE_HIPRI); assertFalse(isDunRequested()); @@ -288,75 +298,80 @@ public class UpstreamNetworkMonitorTest { // There are no networks, so there is nothing to select. assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes)); - final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, TRANSPORT_WIFI); + final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES); wifiAgent.fakeConnect(); + mLooper.dispatchAll(); // WiFi is up, we should prefer it. assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes)); wifiAgent.fakeDisconnect(); + mLooper.dispatchAll(); // There are no networks, so there is nothing to select. assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes)); - final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR); + final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES); cellAgent.fakeConnect(); + mLooper.dispatchAll(); assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes)); preferredTypes.add(TYPE_MOBILE_DUN); // This is coupled with preferred types in TetheringConfiguration. - mUNM.updateMobileRequiresDun(true); + mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */); // DUN is available, but only use regular cell: no upstream selected. assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes)); preferredTypes.remove(TYPE_MOBILE_DUN); // No WiFi, but our preferred flavour of cell is up. preferredTypes.add(TYPE_MOBILE_HIPRI); // This is coupled with preferred types in TetheringConfiguration. - mUNM.updateMobileRequiresDun(false); + mUNM.setUpstreamConfig(false /* autoUpstream */, false /* dunRequired */); assertSatisfiesLegacyType(TYPE_MOBILE_HIPRI, mUNM.selectPreferredUpstreamType(preferredTypes)); // Check to see we filed an explicit request. - assertEquals(1, mCM.requested.size()); - NetworkRequest netReq = (NetworkRequest) mCM.requested.values().toArray()[0]; + assertEquals(1, mCM.mRequested.size()); + NetworkRequest netReq = ((NetworkRequestInfo) mCM.mRequested.values().toArray()[0]).request; assertTrue(netReq.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)); assertFalse(netReq.networkCapabilities.hasCapability(NET_CAPABILITY_DUN)); // mobile is not permitted, we should not use HIPRI. when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(false); assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes)); - assertEquals(0, mCM.requested.size()); + assertEquals(0, mCM.mRequested.size()); // mobile change back to permitted, HIRPI should come back when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true); assertSatisfiesLegacyType(TYPE_MOBILE_HIPRI, mUNM.selectPreferredUpstreamType(preferredTypes)); wifiAgent.fakeConnect(); + mLooper.dispatchAll(); // WiFi is up, and we should prefer it over cell. assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes)); - assertEquals(0, mCM.requested.size()); + assertEquals(0, mCM.mRequested.size()); preferredTypes.remove(TYPE_MOBILE_HIPRI); preferredTypes.add(TYPE_MOBILE_DUN); // This is coupled with preferred types in TetheringConfiguration. - mUNM.updateMobileRequiresDun(true); + mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */); assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes)); - final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR); - dunAgent.networkCapabilities.addCapability(NET_CAPABILITY_DUN); + final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, DUN_CAPABILITIES); dunAgent.fakeConnect(); + mLooper.dispatchAll(); // WiFi is still preferred. assertSatisfiesLegacyType(TYPE_WIFI, mUNM.selectPreferredUpstreamType(preferredTypes)); // WiFi goes down, cell and DUN are still up but only DUN is preferred. wifiAgent.fakeDisconnect(); + mLooper.dispatchAll(); assertSatisfiesLegacyType(TYPE_MOBILE_DUN, mUNM.selectPreferredUpstreamType(preferredTypes)); // Check to see we filed an explicit request. - assertEquals(1, mCM.requested.size()); - netReq = (NetworkRequest) mCM.requested.values().toArray()[0]; + assertEquals(1, mCM.mRequested.size()); + netReq = ((NetworkRequestInfo) mCM.mRequested.values().toArray()[0]).request; assertTrue(netReq.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)); assertTrue(netReq.networkCapabilities.hasCapability(NET_CAPABILITY_DUN)); // mobile is not permitted, we should not use DUN. when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(false); assertSatisfiesLegacyType(TYPE_NONE, mUNM.selectPreferredUpstreamType(preferredTypes)); - assertEquals(0, mCM.requested.size()); + assertEquals(0, mCM.mRequested.size()); // mobile change back to permitted, DUN should come back when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true); assertSatisfiesLegacyType(TYPE_MOBILE_DUN, @@ -367,49 +382,72 @@ public class UpstreamNetworkMonitorTest { public void testGetCurrentPreferredUpstream() throws Exception { mUNM.startTrackDefaultNetwork(sDefaultRequest, mEntitleMgr); mUNM.startObserveAllNetworks(); - mUNM.updateMobileRequiresDun(false); + mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */); + mUNM.setTryCell(true); // [0] Mobile connects, DUN not required -> mobile selected. - final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR); + final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES); cellAgent.fakeConnect(); mCM.makeDefaultNetwork(cellAgent); + mLooper.dispatchAll(); assertEquals(cellAgent.networkId, mUNM.getCurrentPreferredUpstream().network); + assertEquals(0, mCM.mRequested.size()); // [1] Mobile connects but not permitted -> null selected when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(false); assertEquals(null, mUNM.getCurrentPreferredUpstream()); when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true); + assertEquals(0, mCM.mRequested.size()); // [2] WiFi connects but not validated/promoted to default -> mobile selected. - final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, TRANSPORT_WIFI); + final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES); wifiAgent.fakeConnect(); + mLooper.dispatchAll(); assertEquals(cellAgent.networkId, mUNM.getCurrentPreferredUpstream().network); + assertEquals(0, mCM.mRequested.size()); // [3] WiFi validates and is promoted to the default network -> WiFi selected. mCM.makeDefaultNetwork(wifiAgent); + mLooper.dispatchAll(); assertEquals(wifiAgent.networkId, mUNM.getCurrentPreferredUpstream().network); + assertEquals(0, mCM.mRequested.size()); // [4] DUN required, no other changes -> WiFi still selected - mUNM.updateMobileRequiresDun(true); + mUNM.setUpstreamConfig(false /* autoUpstream */, true /* dunRequired */); assertEquals(wifiAgent.networkId, mUNM.getCurrentPreferredUpstream().network); + assertEquals(1, mCM.mRequested.size()); + assertTrue(isDunRequested()); // [5] WiFi no longer validated, mobile becomes default, DUN required -> null selected. mCM.makeDefaultNetwork(cellAgent); + mLooper.dispatchAll(); assertEquals(null, mUNM.getCurrentPreferredUpstream()); - // TODO: make sure that a DUN request has been filed. This is currently - // triggered by code over in Tethering, but once that has been moved - // into UNM we should test for this here. + assertEquals(1, mCM.mRequested.size()); + assertTrue(isDunRequested()); // [6] DUN network arrives -> DUN selected - final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR); + final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES); dunAgent.networkCapabilities.addCapability(NET_CAPABILITY_DUN); dunAgent.networkCapabilities.removeCapability(NET_CAPABILITY_INTERNET); dunAgent.fakeConnect(); + mLooper.dispatchAll(); assertEquals(dunAgent.networkId, mUNM.getCurrentPreferredUpstream().network); + assertEquals(1, mCM.mRequested.size()); // [7] Mobile is not permitted -> null selected when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(false); assertEquals(null, mUNM.getCurrentPreferredUpstream()); + assertEquals(1, mCM.mRequested.size()); + + // [7] Mobile is permitted again -> DUN selected + when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true); + assertEquals(dunAgent.networkId, mUNM.getCurrentPreferredUpstream().network); + assertEquals(1, mCM.mRequested.size()); + + // [8] DUN no longer required -> request is withdrawn + mUNM.setUpstreamConfig(true /* autoUpstream */, false /* dunRequired */); + assertEquals(0, mCM.mRequested.size()); + assertFalse(isDunRequested()); } @Test @@ -424,7 +462,7 @@ public class UpstreamNetworkMonitorTest { final Set<String> alreadySeen = new HashSet<>(); // [1] Pretend Wi-Fi connects. - final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, TRANSPORT_WIFI); + final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES); final LinkProperties wifiLp = wifiAgent.linkProperties; wifiLp.setInterfaceName("wlan0"); final String[] wifi_addrs = { @@ -439,6 +477,7 @@ public class UpstreamNetworkMonitorTest { } wifiAgent.fakeConnect(); wifiAgent.sendLinkProperties(); + mLooper.dispatchAll(); local = mUNM.getLocalPrefixes(); assertPrefixSet(local, INCLUDES, alreadySeen); @@ -451,7 +490,7 @@ public class UpstreamNetworkMonitorTest { assertEquals(alreadySeen.size(), local.size()); // [2] Pretend mobile connects. - final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR); + final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES); final LinkProperties cellLp = cellAgent.linkProperties; cellLp.setInterfaceName("rmnet_data0"); final String[] cell_addrs = { @@ -463,6 +502,7 @@ public class UpstreamNetworkMonitorTest { } cellAgent.fakeConnect(); cellAgent.sendLinkProperties(); + mLooper.dispatchAll(); local = mUNM.getLocalPrefixes(); assertPrefixSet(local, INCLUDES, alreadySeen); @@ -472,9 +512,7 @@ public class UpstreamNetworkMonitorTest { assertEquals(alreadySeen.size(), local.size()); // [3] Pretend DUN connects. - final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR); - dunAgent.networkCapabilities.addCapability(NET_CAPABILITY_DUN); - dunAgent.networkCapabilities.removeCapability(NET_CAPABILITY_INTERNET); + final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, DUN_CAPABILITIES); final LinkProperties dunLp = dunAgent.linkProperties; dunLp.setInterfaceName("rmnet_data1"); final String[] dun_addrs = { @@ -486,6 +524,7 @@ public class UpstreamNetworkMonitorTest { } dunAgent.fakeConnect(); dunAgent.sendLinkProperties(); + mLooper.dispatchAll(); local = mUNM.getLocalPrefixes(); assertPrefixSet(local, INCLUDES, alreadySeen); @@ -497,6 +536,7 @@ public class UpstreamNetworkMonitorTest { // [4] Pretend Wi-Fi disconnected. It's addresses/prefixes should no // longer be included (should be properly removed). wifiAgent.fakeDisconnect(); + mLooper.dispatchAll(); local = mUNM.getLocalPrefixes(); assertPrefixSet(local, EXCLUDES, wifiLinkPrefixes); assertPrefixSet(local, INCLUDES, cellLinkPrefixes); @@ -504,6 +544,7 @@ public class UpstreamNetworkMonitorTest { // [5] Pretend mobile disconnected. cellAgent.fakeDisconnect(); + mLooper.dispatchAll(); local = mUNM.getLocalPrefixes(); assertPrefixSet(local, EXCLUDES, wifiLinkPrefixes); assertPrefixSet(local, EXCLUDES, cellLinkPrefixes); @@ -511,6 +552,7 @@ public class UpstreamNetworkMonitorTest { // [6] Pretend DUN disconnected. dunAgent.fakeDisconnect(); + mLooper.dispatchAll(); local = mUNM.getLocalPrefixes(); assertTrue(local.isEmpty()); } @@ -524,12 +566,13 @@ public class UpstreamNetworkMonitorTest { mUNM.startTrackDefaultNetwork(sDefaultRequest, mEntitleMgr); mUNM.startObserveAllNetworks(); // Setup wifi and make wifi as default network. - final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, TRANSPORT_WIFI); + final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, WIFI_CAPABILITIES); wifiAgent.fakeConnect(); mCM.makeDefaultNetwork(wifiAgent); // Setup mobile network. - final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR); + final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, CELL_CAPABILITIES); cellAgent.fakeConnect(); + mLooper.dispatchAll(); assertSatisfiesLegacyType(TYPE_MOBILE_HIPRI, mUNM.selectPreferredUpstreamType(preferredTypes)); @@ -548,15 +591,15 @@ public class UpstreamNetworkMonitorTest { } private void assertUpstreamTypeRequested(int upstreamType) throws Exception { - assertEquals(1, mCM.requested.size()); - assertEquals(1, mCM.legacyTypeMap.size()); + assertEquals(1, mCM.mRequested.size()); + assertEquals(1, mCM.mLegacyTypeMap.size()); assertEquals(Integer.valueOf(upstreamType), - mCM.legacyTypeMap.values().iterator().next()); + mCM.mLegacyTypeMap.values().iterator().next()); } private boolean isDunRequested() { - for (NetworkRequest req : mCM.requested.values()) { - if (req.networkCapabilities.hasCapability(NET_CAPABILITY_DUN)) { + for (NetworkRequestInfo nri : mCM.mRequested.values()) { + if (nri.request.networkCapabilities.hasCapability(NET_CAPABILITY_DUN)) { return true; } } @@ -582,8 +625,8 @@ public class UpstreamNetworkMonitorTest { } } - public TestStateMachine() { - super("UpstreamNetworkMonitor.TestStateMachine"); + public TestStateMachine(Looper looper) { + super("UpstreamNetworkMonitor.TestStateMachine", looper); addState(mLoggingState); setInitialState(mLoggingState); super.start(); diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java index f423503c56..48923b9acb 100644 --- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java @@ -25,7 +25,8 @@ import static com.android.cts.net.hostside.NetworkPolicyTestUtils.executeShellCo import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getConnectivityManager; import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getContext; import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getInstrumentation; -import static com.android.cts.net.hostside.NetworkPolicyTestUtils.getWifiManager; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isAppStandbySupported; +import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isBatterySaverSupported; import static com.android.cts.net.hostside.NetworkPolicyTestUtils.isDozeModeSupported; import static com.android.cts.net.hostside.NetworkPolicyTestUtils.restrictBackgroundValueToString; @@ -46,12 +47,10 @@ import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.NetworkInfo.DetailedState; import android.net.NetworkInfo.State; -import android.net.wifi.WifiManager; import android.os.BatteryManager; import android.os.Binder; import android.os.Bundle; import android.os.SystemClock; -import android.provider.Settings; import android.service.notification.NotificationListenerService; import android.util.Log; @@ -628,6 +627,9 @@ public abstract class AbstractRestrictBackgroundNetworkTestCase { } protected void setBatterySaverMode(boolean enabled) throws Exception { + if (!isBatterySaverSupported()) { + return; + } Log.i(TAG, "Setting Battery Saver Mode to " + enabled); if (enabled) { turnBatteryOn(); @@ -639,8 +641,9 @@ public abstract class AbstractRestrictBackgroundNetworkTestCase { } protected void setDozeMode(boolean enabled) throws Exception { - // Check doze mode is supported. - assertTrue("Device does not support Doze Mode", isDozeModeSupported()); + if (!isDozeModeSupported()) { + return; + } Log.i(TAG, "Setting Doze Mode to " + enabled); if (enabled) { @@ -660,12 +663,18 @@ public abstract class AbstractRestrictBackgroundNetworkTestCase { } protected void setAppIdle(boolean enabled) throws Exception { + if (!isAppStandbySupported()) { + return; + } Log.i(TAG, "Setting app idle to " + enabled); executeSilentShellCommand("am set-inactive " + TEST_APP2_PKG + " " + enabled ); assertAppIdle(enabled); } protected void setAppIdleNoAssert(boolean enabled) throws Exception { + if (!isAppStandbySupported()) { + return; + } Log.i(TAG, "Setting app idle to " + enabled); executeSilentShellCommand("am set-inactive " + TEST_APP2_PKG + " " + enabled ); } diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java index 5ecb399da0..66cb935a1c 100644 --- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/DumpOnFailureRule.java @@ -24,6 +24,8 @@ import android.os.FileUtils; import android.os.ParcelFileDescriptor; import android.util.Log; +import androidx.test.platform.app.InstrumentationRegistry; + import com.android.compatibility.common.util.OnFailureRule; import org.junit.AssumptionViolatedException; @@ -37,23 +39,20 @@ import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import androidx.test.platform.app.InstrumentationRegistry; - public class DumpOnFailureRule extends OnFailureRule { private File mDumpDir = new File(Environment.getExternalStorageDirectory(), "CtsHostsideNetworkTests"); @Override public void onTestFailure(Statement base, Description description, Throwable throwable) { - final String testName = description.getClassName() + "_" + description.getMethodName(); - if (throwable instanceof AssumptionViolatedException) { + final String testName = description.getClassName() + "_" + description.getMethodName(); Log.d(TAG, "Skipping test " + testName + ": " + throwable); return; } prepareDumpRootDir(); - final File dumpFile = new File(mDumpDir, "dump-" + testName); + final File dumpFile = new File(mDumpDir, "dump-" + getShortenedTestName(description)); Log.i(TAG, "Dumping debug info for " + description + ": " + dumpFile.getPath()); try (FileOutputStream out = new FileOutputStream(dumpFile)) { for (String cmd : new String[] { @@ -71,6 +70,17 @@ public class DumpOnFailureRule extends OnFailureRule { } } + private String getShortenedTestName(Description description) { + final String qualifiedClassName = description.getClassName(); + final String className = qualifiedClassName.substring( + qualifiedClassName.lastIndexOf(".") + 1); + final String shortenedClassName = className.chars() + .filter(Character::isUpperCase) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + return shortenedClassName + "_" + description.getMethodName(); + } + void dumpCommandOutput(FileOutputStream out, String cmd) { final ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation() .getUiAutomation().executeShellCommand(cmd); diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/NetworkPolicyTestUtils.java index b61535bb54..0a134080ff 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 @@ -22,12 +22,13 @@ import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELI import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_METERED; +import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_NONE; import static com.android.compatibility.common.util.SystemUtil.runShellCommand; import static com.android.cts.net.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -35,19 +36,21 @@ import static org.junit.Assert.fail; import android.app.ActivityManager; import android.app.Instrumentation; +import android.app.UiAutomation; import android.content.Context; import android.location.LocationManager; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.Network; import android.net.NetworkCapabilities; +import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiManager; +import android.net.wifi.WifiManager.ActionListener; import android.os.PersistableBundle; import android.os.Process; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionManager; import android.telephony.data.ApnSetting; -import android.text.TextUtils; import android.util.Log; import androidx.test.platform.app.InstrumentationRegistry; @@ -57,7 +60,12 @@ import com.android.compatibility.common.util.BatteryUtils; import com.android.compatibility.common.util.ShellIdentityUtils; import com.android.compatibility.common.util.ThrowingRunnable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; public class NetworkPolicyTestUtils { @@ -98,11 +106,11 @@ public class NetworkPolicyTestUtils { if (mDataSaverSupported == null) { assertMyRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED); try { - setRestrictBackground(true); + setRestrictBackgroundInternal(true); mDataSaverSupported = !isMyRestrictBackgroundStatus( RESTRICT_BACKGROUND_STATUS_DISABLED); } finally { - setRestrictBackground(false); + setRestrictBackgroundInternal(false); } } return mDataSaverSupported; @@ -182,19 +190,14 @@ public class NetworkPolicyTestUtils { } private static String getWifiSsid() { - final boolean isLocationEnabled = isLocationEnabled(); + final UiAutomation uiAutomation = getInstrumentation().getUiAutomation(); try { - if (!isLocationEnabled) { - setLocationEnabled(true); - } - final String ssid = unquoteSSID(getWifiManager().getConnectionInfo().getSSID()); + uiAutomation.adoptShellPermissionIdentity(); + final String ssid = getWifiManager().getConnectionInfo().getSSID(); assertNotEquals(WifiManager.UNKNOWN_SSID, ssid); return ssid; } finally { - // Reset the location enabled state - if (!isLocationEnabled) { - setLocationEnabled(false); - } + uiAutomation.dropShellPermissionIdentity(); } } @@ -205,18 +208,71 @@ public class NetworkPolicyTestUtils { } 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); - assertWifiMeteredStatus(ssid, metered); - assertActiveNetworkMetered(metered); + final UiAutomation uiAutomation = getInstrumentation().getUiAutomation(); + try { + uiAutomation.adoptShellPermissionIdentity(); + final WifiConfiguration currentConfig = getWifiConfiguration(ssid); + currentConfig.meteredOverride = metered + ? METERED_OVERRIDE_METERED : METERED_OVERRIDE_NONE; + BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>(); + getWifiManager().save(currentConfig, createActionListener( + blockingQueue, Integer.MAX_VALUE)); + Integer resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS, + TimeUnit.MILLISECONDS); + if (resultCode == null) { + fail("Timed out waiting for meteredness to change; ssid=" + ssid + + ", metered=" + metered); + } else if (resultCode != Integer.MAX_VALUE) { + fail("Error overriding the meteredness; ssid=" + ssid + + ", metered=" + metered + ", error=" + resultCode); + } + final boolean success = assertActiveNetworkMetered(metered, false /* throwOnFailure */); + if (!success) { + Log.i(TAG, "Retry connecting to wifi; ssid=" + ssid); + blockingQueue = new LinkedBlockingQueue<>(); + getWifiManager().connect(currentConfig, createActionListener( + blockingQueue, Integer.MAX_VALUE)); + resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS, + TimeUnit.MILLISECONDS); + if (resultCode == null) { + fail("Timed out waiting for wifi to connect; ssid=" + ssid); + } else if (resultCode != Integer.MAX_VALUE) { + fail("Error connecting to wifi; ssid=" + ssid + + ", error=" + resultCode); + } + assertActiveNetworkMetered(metered, true /* throwOnFailure */); + } + } finally { + uiAutomation.dropShellPermissionIdentity(); + } } - 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 WifiConfiguration getWifiConfiguration(String ssid) { + final List<String> ssids = new ArrayList<>(); + for (WifiConfiguration config : getWifiManager().getConfiguredNetworks()) { + if (config.SSID.equals(ssid)) { + return config; + } + ssids.add(config.SSID); + } + fail("Couldn't find the wifi config; ssid=" + ssid + + ", all=" + Arrays.toString(ssids.toArray())); + return null; + } + + private static ActionListener createActionListener(BlockingQueue<Integer> blockingQueue, + int successCode) { + return new ActionListener() { + @Override + public void onSuccess() { + blockingQueue.offer(successCode); + } + + @Override + public void onFailure(int reason) { + blockingQueue.offer(reason); + } + }; } private static void setCellularMeteredStatus(int subId, boolean metered) throws Exception { @@ -225,11 +281,11 @@ public class NetworkPolicyTestUtils { new String[] {ApnSetting.TYPE_MMS_STRING}); ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(getCarrierConfigManager(), (cm) -> cm.overrideConfig(subId, metered ? null : bundle)); - assertActiveNetworkMetered(metered); + assertActiveNetworkMetered(metered, true /* throwOnFailure */); } - // Copied from cts/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java - private static void assertActiveNetworkMetered(boolean expectedMeteredStatus) throws Exception { + private static boolean assertActiveNetworkMetered(boolean expectedMeteredStatus, + boolean throwOnFailure) throws Exception { final CountDownLatch latch = new CountDownLatch(1); final NetworkCallback networkCallback = new NetworkCallback() { @Override @@ -246,16 +302,29 @@ public class NetworkPolicyTestUtils { getConnectivityManager().registerDefaultNetworkCallback(networkCallback); 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()); + final String errorMsg = "Timed out waiting for active network metered status " + + "to change to " + expectedMeteredStatus + "; network = " + + getConnectivityManager().getActiveNetwork(); + if (throwOnFailure) { + fail(errorMsg); + } + Log.w(TAG, errorMsg); + return false; } + return true; } finally { getConnectivityManager().unregisterNetworkCallback(networkCallback); } } public static void setRestrictBackground(boolean enabled) { + if (!isDataSaverSupported()) { + return; + } + setRestrictBackgroundInternal(enabled); + } + + private static void setRestrictBackgroundInternal(boolean enabled) { executeShellCommand("cmd netpolicy set restrict-background " + enabled); final String output = executeShellCommand("cmd netpolicy get restrict-background"); final String expectedSuffix = enabled ? "enabled" : "disabled"; diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java index ce203795f9..37420bfb1c 100644 --- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java +++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java @@ -152,8 +152,10 @@ abstract class HostsideNetworkTestCase extends DeviceTestCase implements IAbiRec // build a meaningful error message StringBuilder errorBuilder = new StringBuilder("on-device tests failed:\n"); for (Map.Entry<TestDescription, TestResult> resultEntry : - result.getTestResults().entrySet()) { - if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) { + result.getTestResults().entrySet()) { + final TestStatus testStatus = resultEntry.getValue().getStatus(); + if (!TestStatus.PASSED.equals(testStatus) + && !TestStatus.ASSUMPTION_FAILURE.equals(testStatus)) { errorBuilder.append(resultEntry.getKey().toString()); errorBuilder.append(":\n"); errorBuilder.append(resultEntry.getValue().getStackTrace()); diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp index 7df8228e40..4a1020d962 100644 --- a/tests/cts/net/Android.bp +++ b/tests/cts/net/Android.bp @@ -96,7 +96,10 @@ android_test { target_sdk_version: "30", test_suites: [ "general-tests", - "mts", + "mts-dnsresolver", + "mts-networking", + "mts-tethering", + "mts-wifi", ], test_config_template: "AndroidTestTemplate.xml", } diff --git a/tests/cts/net/native/dns/Android.bp b/tests/cts/net/native/dns/Android.bp index 1bc5a86068..5e9af8e55a 100644 --- a/tests/cts/net/native/dns/Android.bp +++ b/tests/cts/net/native/dns/Android.bp @@ -40,6 +40,7 @@ cc_test { test_suites: [ "cts", "general-tests", - "mts", + "mts-dnsresolver", + "mts-networking", ], } diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java index e3208e7f42..bfab497316 100644 --- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java +++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java @@ -19,12 +19,29 @@ package android.net.cts; import static android.Manifest.permission.CONNECTIVITY_INTERNAL; import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS; import static android.Manifest.permission.NETWORK_SETTINGS; +import static android.content.pm.PackageManager.FEATURE_BLUETOOTH; import static android.content.pm.PackageManager.FEATURE_ETHERNET; import static android.content.pm.PackageManager.FEATURE_TELEPHONY; import static android.content.pm.PackageManager.FEATURE_USB_HOST; +import static android.content.pm.PackageManager.FEATURE_WATCH; import static android.content.pm.PackageManager.FEATURE_WIFI; +import static android.content.pm.PackageManager.FEATURE_WIFI_DIRECT; import static android.content.pm.PackageManager.GET_PERMISSIONS; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.net.ConnectivityManager.TYPE_BLUETOOTH; +import static android.net.ConnectivityManager.TYPE_ETHERNET; +import static android.net.ConnectivityManager.TYPE_MOBILE_CBS; +import static android.net.ConnectivityManager.TYPE_MOBILE_DUN; +import static android.net.ConnectivityManager.TYPE_MOBILE_EMERGENCY; +import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA; +import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI; +import static android.net.ConnectivityManager.TYPE_MOBILE_IA; +import static android.net.ConnectivityManager.TYPE_MOBILE_IMS; +import static android.net.ConnectivityManager.TYPE_MOBILE_MMS; +import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL; +import static android.net.ConnectivityManager.TYPE_PROXY; +import static android.net.ConnectivityManager.TYPE_VPN; +import static android.net.ConnectivityManager.TYPE_WIFI_P2P; import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND; import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS; import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; @@ -78,7 +95,6 @@ import android.net.LinkAddress; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; -import android.net.NetworkConfig; import android.net.NetworkInfo; import android.net.NetworkInfo.DetailedState; import android.net.NetworkInfo.State; @@ -100,7 +116,9 @@ import android.os.SystemProperties; import android.os.VintfRuntimeInfo; import android.platform.test.annotations.AppModeFull; import android.provider.Settings; +import android.telephony.TelephonyManager; import android.text.TextUtils; +import android.util.ArraySet; import android.util.Log; import android.util.Pair; @@ -115,6 +133,7 @@ import com.android.networkstack.apishim.common.ConnectivityManagerShim; import com.android.testutils.CompatUtil; import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; +import com.android.testutils.DevSdkIgnoreRuleKt; import com.android.testutils.RecorderCallback.CallbackEntry; import com.android.testutils.SkipPresubmit; import com.android.testutils.TestableNetworkCallback; @@ -146,7 +165,6 @@ import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; @@ -201,8 +219,7 @@ public class ConnectivityManagerTest { private ConnectivityManagerShim mCmShim; private WifiManager mWifiManager; private PackageManager mPackageManager; - private final HashMap<Integer, NetworkConfig> mNetworks = - new HashMap<Integer, NetworkConfig>(); + private final ArraySet<Integer> mNetworkTypes = new ArraySet<>(); private UiAutomation mUiAutomation; private CtsNetUtils mCtsNetUtils; @@ -216,23 +233,67 @@ public class ConnectivityManagerTest { mPackageManager = mContext.getPackageManager(); mCtsNetUtils = new CtsNetUtils(mContext); + if (DevSdkIgnoreRuleKt.isDevSdkInRange(null /* minExclusive */, + Build.VERSION_CODES.R /* maxInclusive */)) { + addLegacySupportedNetworkTypes(); + } else { + addSupportedNetworkTypes(); + } + + mUiAutomation = mInstrumentation.getUiAutomation(); + + assertNotNull("CTS requires a working Internet connection", mCm.getActiveNetwork()); + } + + private void addLegacySupportedNetworkTypes() { + // Network type support as expected for android R- // Get com.android.internal.R.array.networkAttributes int resId = mContext.getResources().getIdentifier("networkAttributes", "array", "android"); String[] naStrings = mContext.getResources().getStringArray(resId); - //TODO: What is the "correct" way to determine if this is a wifi only device? boolean wifiOnly = SystemProperties.getBoolean("ro.radio.noril", false); for (String naString : naStrings) { try { - NetworkConfig n = new NetworkConfig(naString); - if (wifiOnly && ConnectivityManager.isNetworkTypeMobile(n.type)) { + final String[] splitConfig = naString.split(","); + // Format was name,type,radio,priority,restoreTime,dependencyMet + final int type = Integer.parseInt(splitConfig[1]); + if (wifiOnly && ConnectivityManager.isNetworkTypeMobile(type)) { continue; } - mNetworks.put(n.type, n); + mNetworkTypes.add(type); } catch (Exception e) {} } - mUiAutomation = mInstrumentation.getUiAutomation(); + } - assertNotNull("CTS requires a working Internet connection", mCm.getActiveNetwork()); + private void addSupportedNetworkTypes() { + final PackageManager pm = mContext.getPackageManager(); + if (pm.hasSystemFeature(FEATURE_WIFI)) { + mNetworkTypes.add(TYPE_WIFI); + } + if (pm.hasSystemFeature(FEATURE_WIFI_DIRECT)) { + mNetworkTypes.add(TYPE_WIFI_P2P); + } + if (mContext.getSystemService(TelephonyManager.class).isDataCapable()) { + mNetworkTypes.add(TYPE_MOBILE); + mNetworkTypes.add(TYPE_MOBILE_MMS); + mNetworkTypes.add(TYPE_MOBILE_SUPL); + mNetworkTypes.add(TYPE_MOBILE_DUN); + mNetworkTypes.add(TYPE_MOBILE_HIPRI); + mNetworkTypes.add(TYPE_MOBILE_FOTA); + mNetworkTypes.add(TYPE_MOBILE_IMS); + mNetworkTypes.add(TYPE_MOBILE_CBS); + mNetworkTypes.add(TYPE_MOBILE_IA); + mNetworkTypes.add(TYPE_MOBILE_EMERGENCY); + } + if (pm.hasSystemFeature(FEATURE_BLUETOOTH)) { + mNetworkTypes.add(TYPE_BLUETOOTH); + } + if (pm.hasSystemFeature(FEATURE_WATCH)) { + mNetworkTypes.add(TYPE_PROXY); + } + if (mContext.getSystemService(Context.ETHERNET_SERVICE) != null) { + mNetworkTypes.add(TYPE_ETHERNET); + } + mNetworkTypes.add(TYPE_VPN); } @After @@ -461,9 +522,9 @@ public class ConnectivityManagerTest { } private boolean shouldBeSupported(int networkType) { - return mNetworks.containsKey(networkType) || - (networkType == ConnectivityManager.TYPE_VPN) || - (networkType == ConnectivityManager.TYPE_ETHERNET && shouldEthernetBeSupported()); + return mNetworkTypes.contains(networkType) + || (networkType == ConnectivityManager.TYPE_VPN) + || (networkType == ConnectivityManager.TYPE_ETHERNET && shouldEthernetBeSupported()); } @Test @@ -1603,15 +1664,16 @@ public class ConnectivityManagerTest { // Verify background network cannot be requested without NETWORK_SETTINGS permission. final TestableNetworkCallback callback = new TestableNetworkCallback(); + final Handler handler = new Handler(Looper.getMainLooper()); assertThrows(SecurityException.class, - () -> mCmShim.requestBackgroundNetwork(testRequest, null, callback)); + () -> mCmShim.requestBackgroundNetwork(testRequest, handler, callback)); Network testNetwork = null; try { // Request background test network via Shell identity which has NETWORK_SETTINGS // permission granted. runWithShellPermissionIdentity( - () -> mCmShim.requestBackgroundNetwork(testRequest, null, callback), + () -> mCmShim.requestBackgroundNetwork(testRequest, handler, callback), new String[] { android.Manifest.permission.NETWORK_SETTINGS }); // Register the test network agent which has no foreground request associated to it. diff --git a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt index 827a05eae4..ef529f8289 100644 --- a/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt +++ b/tests/cts/net/src/android/net/cts/NetworkAgentTest.kt @@ -18,6 +18,8 @@ package android.net.cts import android.app.Instrumentation import android.content.Context import android.net.ConnectivityManager +import android.net.INetworkAgent +import android.net.INetworkAgentRegistry import android.net.InetAddresses import android.net.IpPrefix import android.net.KeepalivePacketData @@ -44,6 +46,7 @@ import android.net.NetworkCapabilities.TRANSPORT_VPN import android.net.NetworkInfo import android.net.NetworkProvider import android.net.NetworkRequest +import android.net.NetworkScore import android.net.RouteInfo import android.net.SocketKeepalive import android.net.Uri @@ -65,8 +68,6 @@ import android.os.Looper import android.os.Message import android.util.DebugUtils.valueToString import androidx.test.InstrumentationRegistry -import com.android.connectivity.aidl.INetworkAgent -import com.android.connectivity.aidl.INetworkAgentRegistry import com.android.modules.utils.build.SdkLevel import com.android.net.module.util.ArrayTrackRecord import com.android.testutils.CompatUtil @@ -81,7 +82,6 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.argThat import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito.doReturn @@ -632,7 +632,7 @@ class NetworkAgentTest { argThat<NetworkInfo> { it.detailedState == NetworkInfo.DetailedState.CONNECTING }, any(LinkProperties::class.java), any(NetworkCapabilities::class.java), - anyInt() /* score */, + any(NetworkScore::class.java), any(NetworkAgentConfig::class.java), eq(NetworkProvider.ID_NONE)) } diff --git a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java index 30c4e72e10..9906c30ba7 100644 --- a/tests/cts/net/src/android/net/cts/NetworkRequestTest.java +++ b/tests/cts/net/src/android/net/cts/NetworkRequestTest.java @@ -39,17 +39,20 @@ import android.net.MatchAllNetworkSpecifier; import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.net.NetworkSpecifier; -import android.net.UidRange; import android.net.wifi.WifiNetworkSpecifier; import android.os.Build; import android.os.PatternMatcher; import android.os.Process; import android.util.ArraySet; +import android.util.Range; import androidx.test.runner.AndroidJUnit4; import com.android.modules.utils.build.SdkLevel; import com.android.networkstack.apishim.ConstantsShim; +import com.android.networkstack.apishim.NetworkRequestShimImpl; +import com.android.networkstack.apishim.common.NetworkRequestShim; +import com.android.networkstack.apishim.common.UnsupportedApiLevelException; import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; @@ -57,6 +60,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Set; + @RunWith(AndroidJUnit4.class) public class NetworkRequestTest { @Rule @@ -225,6 +230,14 @@ public class NetworkRequestTest { assertTrue(requestCellularInternet.canBeSatisfiedBy(capCellularVpnMmsInternet)); } + private void setUids(NetworkRequest.Builder builder, Set<Range<Integer>> ranges) + throws UnsupportedApiLevelException { + if (SdkLevel.isAtLeastS()) { + final NetworkRequestShim networkRequestShim = NetworkRequestShimImpl.newInstance(); + networkRequestShim.setUids(builder, ranges); + } + } + @Test @IgnoreUpTo(Build.VERSION_CODES.Q) public void testInvariantInCanBeSatisfiedBy() { @@ -232,15 +245,26 @@ public class NetworkRequestTest { // NetworkCapabilities.satisfiedByNetworkCapabilities(). final LocalNetworkSpecifier specifier1 = new LocalNetworkSpecifier(1234 /* id */); final int uid = Process.myUid(); - final ArraySet<UidRange> ranges = new ArraySet<>(); - ranges.add(new UidRange(uid, uid)); - final NetworkRequest requestCombination = new NetworkRequest.Builder() + final NetworkRequest.Builder nrBuilder = new NetworkRequest.Builder() .addTransportType(TRANSPORT_CELLULAR) .addCapability(NET_CAPABILITY_INTERNET) .setLinkUpstreamBandwidthKbps(1000) .setNetworkSpecifier(specifier1) - .setSignalStrength(-123) - .setUids(ranges).build(); + .setSignalStrength(-123); + + // The uid ranges should be set into the request, but setUids() takes a set of UidRange + // that is hidden and inaccessible from shims. Before, S setUids will be a no-op. But + // because NetworkRequest.Builder sets the UID of the request to the current UID, the + // request contains the current UID both on S and before S. + final Set<Range<Integer>> ranges = new ArraySet<>(); + ranges.add(new Range<Integer>(uid, uid)); + try { + setUids(nrBuilder, ranges); + } catch (UnsupportedApiLevelException e) { + // Not supported before API31. + } + final NetworkRequest requestCombination = nrBuilder.build(); + final NetworkCapabilities capCell = new NetworkCapabilities.Builder() .addTransportType(TRANSPORT_CELLULAR).build(); assertCorrectlySatisfies(false, requestCombination, capCell); diff --git a/tests/cts/net/src/android/net/cts/VpnServiceTest.java b/tests/cts/net/src/android/net/cts/VpnServiceTest.java index 15af23c6f8..5c7b5ca082 100644 --- a/tests/cts/net/src/android/net/cts/VpnServiceTest.java +++ b/tests/cts/net/src/android/net/cts/VpnServiceTest.java @@ -47,6 +47,7 @@ public class VpnServiceTest extends AndroidTestCase { assertEquals(1, count); } + @AppModeFull(reason = "establish() requires prepare(), which requires PackageManager access") public void testEstablish() throws Exception { ParcelFileDescriptor descriptor = null; try { @@ -62,7 +63,7 @@ public class VpnServiceTest extends AndroidTestCase { } } - @AppModeFull(reason = "Socket cannot bind in instant app mode") + @AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager") public void testProtect_DatagramSocket() throws Exception { DatagramSocket socket = new DatagramSocket(); try { @@ -77,6 +78,7 @@ public class VpnServiceTest extends AndroidTestCase { } } + @AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager") public void testProtect_Socket() throws Exception { Socket socket = new Socket(); try { @@ -91,7 +93,7 @@ public class VpnServiceTest extends AndroidTestCase { } } - @AppModeFull(reason = "Socket cannot bind in instant app mode") + @AppModeFull(reason = "Protecting sockets requires prepare(), which requires PackageManager") public void testProtect_int() throws Exception { DatagramSocket socket = new DatagramSocket(); ParcelFileDescriptor descriptor = ParcelFileDescriptor.fromDatagramSocket(socket); diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp index 824c874330..fa52e9bb0b 100644 --- a/tests/cts/tethering/Android.bp +++ b/tests/cts/tethering/Android.bp @@ -52,7 +52,7 @@ android_test { test_suites: [ "cts", "general-tests", - "mts", + "mts-tethering", ], // Include both the 32 and 64 bit versions |