diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java index 5cf0384de7..dc5fd6da17 100644 --- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java +++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java @@ -17,14 +17,18 @@ package com.android.networkstack.tethering.apishim.api30; import android.net.INetd; +import android.net.TetherStatsParcel; import android.net.util.SharedLog; import android.os.RemoteException; import android.os.ServiceSpecificException; +import android.util.SparseArray; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.networkstack.tethering.BpfCoordinator.Dependencies; import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule; +import com.android.networkstack.tethering.TetherStatsValue; /** * Bpf coordinator class for API shims. @@ -60,6 +64,47 @@ public class BpfCoordinatorShimImpl return true; }; + @Override + public boolean tetherOffloadRuleRemove(@NonNull final Ipv6ForwardingRule rule) { + try { + mNetd.tetherOffloadRuleRemove(rule.toTetherOffloadRuleParcel()); + } catch (RemoteException | ServiceSpecificException e) { + mLog.e("Could not remove IPv6 forwarding rule: ", e); + return false; + } + return true; + } + + @Override + @Nullable + public SparseArray tetherOffloadGetStats() { + final TetherStatsParcel[] tetherStatsList; + try { + // The reported tether stats are total data usage for all currently-active upstream + // interfaces since tethering start. There will only ever be one entry for a given + // interface index. + tetherStatsList = mNetd.tetherOffloadGetStats(); + } catch (RemoteException | ServiceSpecificException e) { + mLog.e("Fail to fetch tethering stats from netd: " + e); + return null; + } + + return toTetherStatsValueSparseArray(tetherStatsList); + } + + @NonNull + private SparseArray toTetherStatsValueSparseArray( + @NonNull final TetherStatsParcel[] parcels) { + final SparseArray tetherStatsList = new SparseArray(); + + for (TetherStatsParcel p : parcels) { + tetherStatsList.put(p.ifIndex, new TetherStatsValue(p.rxPackets, p.rxBytes, + 0 /* rxErrors */, p.txPackets, p.txBytes, 0 /* txErrors */)); + } + + return tetherStatsList; + } + @Override public String toString() { return "Netd used"; diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java index 03616cab1c..f6630d67f8 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 @@ -18,6 +18,8 @@ package com.android.networkstack.tethering.apishim.api31; import android.net.util.SharedLog; import android.system.ErrnoException; +import android.system.OsConstants; +import android.util.SparseArray; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -27,6 +29,8 @@ import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule; import com.android.networkstack.tethering.BpfMap; import com.android.networkstack.tethering.TetherIngressKey; import com.android.networkstack.tethering.TetherIngressValue; +import com.android.networkstack.tethering.TetherStatsKey; +import com.android.networkstack.tethering.TetherStatsValue; /** * Bpf coordinator class for API shims. @@ -43,14 +47,19 @@ public class BpfCoordinatorShimImpl @Nullable private final BpfMap mBpfIngressMap; + // BPF map of tethering statistics of the upstream interface since tethering startup. + @Nullable + private final BpfMap mBpfStatsMap; + public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) { mLog = deps.getSharedLog().forSubComponent(TAG); mBpfIngressMap = deps.getBpfIngressMap(); + mBpfStatsMap = deps.getBpfStatsMap(); } @Override public boolean isInitialized() { - return mBpfIngressMap != null; + return mBpfIngressMap != null && mBpfStatsMap != null; } @Override @@ -70,10 +79,45 @@ public class BpfCoordinatorShimImpl return true; } + @Override + public boolean tetherOffloadRuleRemove(@NonNull final Ipv6ForwardingRule rule) { + if (!isInitialized()) return false; + + try { + mBpfIngressMap.deleteEntry(rule.makeTetherIngressKey()); + } catch (ErrnoException e) { + // Silent if the rule did not exist. + if (e.errno != OsConstants.ENOENT) { + mLog.e("Could not update entry: ", e); + return false; + } + } + return true; + } + + @Override + @Nullable + public SparseArray tetherOffloadGetStats() { + if (!isInitialized()) return null; + + final SparseArray tetherStatsList = new SparseArray(); + try { + // The reported tether stats are total data usage for all currently-active upstream + // interfaces since tethering start. + mBpfStatsMap.forEach((key, value) -> tetherStatsList.put((int) key.ifindex, value)); + } catch (ErrnoException e) { + mLog.e("Fail to fetch tethering stats from BPF map: ", e); + return null; + } + return tetherStatsList; + } + @Override public String toString() { return "mBpfIngressMap{" - + (mBpfIngressMap != null ? "initialized" : "not initialized") + "} " + + (mBpfIngressMap != null ? "initialized" : "not initialized") + "}, " + + "mBpfStatsMap{" + + (mBpfStatsMap != null ? "initialized" : "not initialized") + "} " + "}"; } } diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java index bcb644c5a6..2ce3252da1 100644 --- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java +++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java @@ -16,10 +16,14 @@ package com.android.networkstack.tethering.apishim.common; +import android.util.SparseArray; + import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.android.networkstack.tethering.BpfCoordinator.Dependencies; import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule; +import com.android.networkstack.tethering.TetherStatsValue; /** * Bpf coordinator class for API shims. @@ -54,5 +58,26 @@ public abstract class BpfCoordinatorShim { * @param rule The rule to add or update. */ public abstract boolean tetherOffloadRuleAdd(@NonNull Ipv6ForwardingRule rule); + + /** + * Deletes a tethering offload rule from the BPF map. + * + * Currently, only downstream /128 IPv6 entries are supported. An existing rule will be deleted + * if the destination IP address and the source interface match. It is not an error if there is + * no matching rule to delete. + * + * @param rule The rule to delete. + */ + public abstract boolean tetherOffloadRuleRemove(@NonNull Ipv6ForwardingRule rule); + + /** + * Return BPF tethering offload statistics. + * + * @return an array of TetherStatsValue's, where each entry contains the upstream interface + * index and its tethering statistics since tethering was first started. + * There will only ever be one entry for a given interface index. + */ + @Nullable + public abstract SparseArray tetherOffloadGetStats(); } diff --git a/Tethering/src/android/net/util/TetheringUtils.java b/Tethering/src/android/net/util/TetheringUtils.java index 53b54f7de0..706d78c1cc 100644 --- a/Tethering/src/android/net/util/TetheringUtils.java +++ b/Tethering/src/android/net/util/TetheringUtils.java @@ -21,6 +21,8 @@ import android.util.Log; import androidx.annotation.NonNull; +import com.android.networkstack.tethering.TetherStatsValue; + import java.io.FileDescriptor; import java.net.Inet6Address; import java.net.SocketException; @@ -91,6 +93,13 @@ public class TetheringUtils { txPackets = tetherStats.txPackets; } + public ForwardedStats(@NonNull TetherStatsValue tetherStats) { + rxBytes = tetherStats.rxBytes; + rxPackets = tetherStats.rxPackets; + txBytes = tetherStats.txBytes; + txPackets = tetherStats.txPackets; + } + public ForwardedStats(@NonNull ForwardedStats other) { rxBytes = other.rxBytes; rxPackets = other.rxPackets; diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java index d890e088bf..4f918ecea3 100644 --- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java +++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java @@ -78,6 +78,8 @@ public class BpfCoordinator { private static final int DUMP_TIMEOUT_MS = 10_000; private static final String TETHER_INGRESS_FS_PATH = "/sys/fs/bpf/map_offload_tether_ingress_map"; + private static final String TETHER_STATS_MAP_PATH = + "/sys/fs/bpf/map_offload_tether_stats_map"; @VisibleForTesting enum StatsType { @@ -157,7 +159,7 @@ public class BpfCoordinator { // Runnable that used by scheduling next polling of stats. private final Runnable mScheduledPollingTask = () -> { - updateForwardedStatsFromNetd(); + updateForwardedStats(); maybeSchedulePollingStats(); }; @@ -199,6 +201,17 @@ public class BpfCoordinator { return null; } } + + /** Get stats BPF map. */ + @Nullable public BpfMap getBpfStatsMap() { + try { + return new BpfMap<>(TETHER_STATS_MAP_PATH, + BpfMap.BPF_F_RDWR, TetherStatsKey.class, TetherStatsValue.class); + } catch (ErrnoException e) { + Log.e(TAG, "Cannot create stats map: " + e); + return null; + } + } } @VisibleForTesting @@ -261,7 +274,7 @@ public class BpfCoordinator { if (mHandler.hasCallbacks(mScheduledPollingTask)) { mHandler.removeCallbacks(mScheduledPollingTask); } - updateForwardedStatsFromNetd(); + updateForwardedStats(); mPollingStarted = false; mLog.i("Polling stopped"); @@ -316,13 +329,7 @@ public class BpfCoordinator { @NonNull final IpServer ipServer, @NonNull final Ipv6ForwardingRule rule) { if (!isUsingBpf()) return; - try { - // TODO: Perhaps avoid to remove a non-existent rule. - mNetd.tetherOffloadRuleRemove(rule.toTetherOffloadRuleParcel()); - } catch (RemoteException | ServiceSpecificException e) { - mLog.e("Could not remove IPv6 forwarding rule: ", e); - return; - } + if (!mBpfCoordinatorShim.tetherOffloadRuleRemove(rule)) return; LinkedHashMap rules = mIpv6ForwardingRules.get(ipServer); if (rules == null) return; @@ -344,8 +351,14 @@ public class BpfCoordinator { try { final TetherStatsParcel stats = mNetd.tetherOffloadGetAndClearStats(upstreamIfindex); + SparseArray tetherStatsList = + new SparseArray(); + tetherStatsList.put(stats.ifIndex, new TetherStatsValue(stats.rxPackets, + stats.rxBytes, 0 /* rxErrors */, stats.txPackets, stats.txBytes, + 0 /* txErrors */)); + // Update the last stats delta and delete the local cache for a given upstream. - updateQuotaAndStatsFromSnapshot(new TetherStatsParcel[] {stats}); + updateQuotaAndStatsFromSnapshot(tetherStatsList); mStats.remove(upstreamIfindex); } catch (RemoteException | ServiceSpecificException e) { Log.wtf(TAG, "Exception when cleanup tether stats for upstream index " @@ -743,10 +756,11 @@ public class BpfCoordinator { } private void updateQuotaAndStatsFromSnapshot( - @NonNull final TetherStatsParcel[] tetherStatsList) { + @NonNull final SparseArray tetherStatsList) { long usedAlertQuota = 0; - for (TetherStatsParcel tetherStats : tetherStatsList) { - final Integer ifIndex = tetherStats.ifIndex; + for (int i = 0; i < tetherStatsList.size(); i++) { + final Integer ifIndex = tetherStatsList.keyAt(i); + final TetherStatsValue tetherStats = tetherStatsList.valueAt(i); final ForwardedStats curr = new ForwardedStats(tetherStats); final ForwardedStats base = mStats.get(ifIndex); final ForwardedStats diff = (base != null) ? curr.subtract(base) : curr; @@ -778,16 +792,15 @@ public class BpfCoordinator { // TODO: Count the used limit quota for notifying data limit reached. } - private void updateForwardedStatsFromNetd() { - final TetherStatsParcel[] tetherStatsList; - try { - // The reported tether stats are total data usage for all currently-active upstream - // interfaces since tethering start. - tetherStatsList = mNetd.tetherOffloadGetStats(); - } catch (RemoteException | ServiceSpecificException e) { - mLog.e("Problem fetching tethering stats: ", e); + private void updateForwardedStats() { + final SparseArray tetherStatsList = + mBpfCoordinatorShim.tetherOffloadGetStats(); + + if (tetherStatsList == null) { + mLog.e("Problem fetching tethering stats"); return; } + updateQuotaAndStatsFromSnapshot(tetherStatsList); } diff --git a/Tethering/src/com/android/networkstack/tethering/BpfMap.java b/Tethering/src/com/android/networkstack/tethering/BpfMap.java index 69ad1b60eb..78d212c027 100644 --- a/Tethering/src/com/android/networkstack/tethering/BpfMap.java +++ b/Tethering/src/com/android/networkstack/tethering/BpfMap.java @@ -23,6 +23,7 @@ import android.system.ErrnoException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; import com.android.net.module.util.Struct; import java.nio.ByteBuffer; @@ -76,6 +77,21 @@ public class BpfMap implements AutoCloseable mValueSize = Struct.getSize(value); } + /** + * Constructor for testing only. + * The derived class implements an internal mocked map. It need to implement all functions + * which are related with the native BPF map because the BPF map handler is not initialized. + * See BpfCoordinatorTest#TestBpfMap. + */ + @VisibleForTesting + protected BpfMap(final Class key, final Class value) { + mMapFd = -1; + mKeyClass = key; + mValueClass = value; + mKeySize = Struct.getSize(key); + mValueSize = Struct.getSize(value); + } + /** * Update an existing or create a new key -> value entry in an eBbpf map. */ diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java b/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java new file mode 100644 index 0000000000..5442480a01 --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherStatsKey.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +/** The key of BpfMap which is used for tethering stats. */ +public class TetherStatsKey extends Struct { + @Field(order = 0, type = Type.U32) + public final long ifindex; // upstream interface index + + public TetherStatsKey(final long ifindex) { + this.ifindex = ifindex; + } + + // TODO: remove equals, hashCode and toString once aosp/1536721 is merged. + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + + if (!(obj instanceof TetherStatsKey)) return false; + + final TetherStatsKey that = (TetherStatsKey) obj; + + return ifindex == that.ifindex; + } + + @Override + public int hashCode() { + return Long.hashCode(ifindex); + } + + @Override + public String toString() { + return String.format("ifindex: %d", ifindex); + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java b/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java new file mode 100644 index 0000000000..844d2e8f7e --- /dev/null +++ b/Tethering/src/com/android/networkstack/tethering/TetherStatsValue.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.networkstack.tethering; + +import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.Field; +import com.android.net.module.util.Struct.Type; + +/** The key of BpfMap which is used for tethering stats. */ +public class TetherStatsValue extends Struct { + // Use the signed long variable to store the uint64 stats from stats BPF map. + // U63 is enough for each data element even at 5Gbps for ~468 years. + // 2^63 / (5 * 1000 * 1000 * 1000) * 8 / 86400 / 365 = 468. + @Field(order = 0, type = Type.U63) + public final long rxPackets; + @Field(order = 1, type = Type.U63) + public final long rxBytes; + @Field(order = 2, type = Type.U63) + public final long rxErrors; + @Field(order = 3, type = Type.U63) + public final long txPackets; + @Field(order = 4, type = Type.U63) + public final long txBytes; + @Field(order = 5, type = Type.U63) + public final long txErrors; + + public TetherStatsValue(final long rxPackets, final long rxBytes, final long rxErrors, + final long txPackets, final long txBytes, final long txErrors) { + this.rxPackets = rxPackets; + this.rxBytes = rxBytes; + this.rxErrors = rxErrors; + this.txPackets = txPackets; + this.txBytes = txBytes; + this.txErrors = txErrors; + } + + // TODO: remove equals, hashCode and toString once aosp/1536721 is merged. + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + + if (!(obj instanceof TetherStatsValue)) return false; + + final TetherStatsValue that = (TetherStatsValue) obj; + + return rxPackets == that.rxPackets + && rxBytes == that.rxBytes + && rxErrors == that.rxErrors + && txPackets == that.txPackets + && txBytes == that.txBytes + && txErrors == that.txErrors; + } + + @Override + public int hashCode() { + return Long.hashCode(rxPackets) ^ Long.hashCode(rxBytes) ^ Long.hashCode(rxErrors) + ^ Long.hashCode(txPackets) ^ Long.hashCode(txBytes) ^ Long.hashCode(txErrors); + } + + @Override + public String toString() { + return String.format("rxPackets: %s, rxBytes: %s, rxErrors: %s, txPackets: %s, " + + "txBytes: %s, txErrors: %s", rxPackets, rxBytes, rxErrors, txPackets, + txBytes, txErrors); + } +} diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java index dae19b710b..91a518eebc 100644 --- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java +++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java @@ -106,6 +106,8 @@ import com.android.networkstack.tethering.BpfMap; import com.android.networkstack.tethering.PrivateAddressCoordinator; import com.android.networkstack.tethering.TetherIngressKey; import com.android.networkstack.tethering.TetherIngressValue; +import com.android.networkstack.tethering.TetherStatsKey; +import com.android.networkstack.tethering.TetherStatsValue; import com.android.networkstack.tethering.TetheringConfiguration; import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; @@ -169,6 +171,7 @@ public class IpServerTest { @Mock private NetworkStatsManager mStatsManager; @Mock private TetheringConfiguration mTetherConfig; @Mock private BpfMap mBpfIngressMap; + @Mock private BpfMap mBpfStatsMap; @Captor private ArgumentCaptor mDhcpParamsCaptor; @@ -293,6 +296,11 @@ public class IpServerTest { public BpfMap getBpfIngressMap() { return mBpfIngressMap; } + + @Nullable + public BpfMap getBpfStatsMap() { + return mBpfStatsMap; + } }; mBpfCoordinator = spy(new BpfCoordinator(mBpfDeps)); @@ -802,6 +810,28 @@ public class IpServerTest { } } + private void verifyTetherOffloadRuleRemove(@Nullable InOrder inOrder, int upstreamIfindex, + @NonNull final InetAddress dst, @NonNull final MacAddress dstMac) throws Exception { + if (mBpfDeps.isAtLeastS()) { + verifyWithOrder(inOrder, mBpfIngressMap).deleteEntry(makeIngressKey(upstreamIfindex, + dst)); + } else { + // |dstMac| is not required for deleting rules. Used bacause tetherOffloadRuleRemove + // uses a whole rule to be a argument. + // See system/netd/server/TetherController.cpp/TetherController#removeOffloadRule. + verifyWithOrder(inOrder, mNetd).tetherOffloadRuleRemove(matches(upstreamIfindex, dst, + dstMac)); + } + } + + private void verifyNeverTetherOffloadRuleRemove() throws Exception { + if (mBpfDeps.isAtLeastS()) { + verify(mBpfIngressMap, never()).deleteEntry(any()); + } else { + verify(mNetd, never()).tetherOffloadRuleRemove(any()); + } + } + @NonNull private static TetherStatsParcel buildEmptyTetherStatsParcel(int ifIndex) { TetherStatsParcel parcel = new TetherStatsParcel(); @@ -869,14 +899,14 @@ public class IpServerTest { recvNewNeigh(myIfindex, neighA, NUD_FAILED, null); verify(mBpfCoordinator).tetherOffloadRuleRemove( mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighA, macNull)); - verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighA, macNull)); + verifyTetherOffloadRuleRemove(null, UPSTREAM_IFINDEX, neighA, macNull); resetNetdBpfMapAndCoordinator(); // A neighbor that is deleted causes the rule to be removed. recvDelNeigh(myIfindex, neighB, NUD_STALE, macB); verify(mBpfCoordinator).tetherOffloadRuleRemove( mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neighB, macNull)); - verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighB, macNull)); + verifyTetherOffloadRuleRemove(null, UPSTREAM_IFINDEX, neighB, macNull); resetNetdBpfMapAndCoordinator(); // Upstream changes result in updating the rules. @@ -889,9 +919,9 @@ public class IpServerTest { lp.setInterfaceName(UPSTREAM_IFACE2); dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp, -1); verify(mBpfCoordinator).tetherOffloadRuleUpdate(mIpServer, UPSTREAM_IFINDEX2); - inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighA, macA)); + verifyTetherOffloadRuleRemove(inOrder, UPSTREAM_IFINDEX, neighA, macA); verifyTetherOffloadRuleAdd(inOrder, UPSTREAM_IFINDEX2, neighA, macA); - inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighB, macB)); + verifyTetherOffloadRuleRemove(inOrder, UPSTREAM_IFINDEX, neighB, macB); verifyTetherOffloadRuleAdd(inOrder, UPSTREAM_IFINDEX2, neighB, macB); resetNetdBpfMapAndCoordinator(); @@ -902,8 +932,8 @@ public class IpServerTest { // - processMessage CMD_IPV6_TETHER_UPDATE for the IPv6 upstream is lost. // See dispatchTetherConnectionChanged. verify(mBpfCoordinator, times(2)).tetherOffloadRuleClear(mIpServer); - verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX2, neighA, macA)); - verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX2, neighB, macB)); + verifyTetherOffloadRuleRemove(null, UPSTREAM_IFINDEX2, neighA, macA); + verifyTetherOffloadRuleRemove(null, UPSTREAM_IFINDEX2, neighB, macB); resetNetdBpfMapAndCoordinator(); // If the upstream is IPv4-only, no rules are added. @@ -929,7 +959,7 @@ public class IpServerTest { resetNetdBpfMapAndCoordinator(); dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0); verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer); - verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighB, macB)); + verifyTetherOffloadRuleRemove(null, UPSTREAM_IFINDEX, neighB, macB); // When the interface goes down, rules are removed. lp.setInterfaceName(UPSTREAM_IFACE); @@ -947,8 +977,8 @@ public class IpServerTest { mIpServer.stop(); mLooper.dispatchAll(); verify(mBpfCoordinator).tetherOffloadRuleClear(mIpServer); - verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighA, macA)); - verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neighB, macB)); + verifyTetherOffloadRuleRemove(null, UPSTREAM_IFINDEX, neighA, macA); + verifyTetherOffloadRuleRemove(null, UPSTREAM_IFINDEX, neighB, macB); verify(mIpNeighborMonitor).stop(); resetNetdBpfMapAndCoordinator(); } @@ -982,7 +1012,7 @@ public class IpServerTest { recvDelNeigh(myIfindex, neigh, NUD_STALE, macA); verify(mBpfCoordinator).tetherOffloadRuleRemove( mIpServer, makeForwardingRule(UPSTREAM_IFINDEX, neigh, macNull)); - verify(mNetd).tetherOffloadRuleRemove(matches(UPSTREAM_IFINDEX, neigh, macNull)); + verifyTetherOffloadRuleRemove(null, UPSTREAM_IFINDEX, neigh, macNull); resetNetdBpfMapAndCoordinator(); // [2] Disable BPF offload. @@ -998,7 +1028,7 @@ public class IpServerTest { recvDelNeigh(myIfindex, neigh, NUD_STALE, macA); verify(mBpfCoordinator, never()).tetherOffloadRuleRemove(any(), any()); - verify(mNetd, never()).tetherOffloadRuleRemove(any()); + verifyNeverTetherOffloadRuleRemove(); resetNetdBpfMapAndCoordinator(); } diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java index b920fa8b67..5ca220c788 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java @@ -61,6 +61,7 @@ import android.net.util.SharedLog; import android.os.Build; import android.os.Handler; import android.os.test.TestLooper; +import android.system.ErrnoException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -68,6 +69,7 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.net.module.util.NetworkStackConstants; +import com.android.net.module.util.Struct; import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import com.android.testutils.TestableNetworkStatsProviderCbBinder; @@ -85,7 +87,10 @@ import java.net.Inet6Address; import java.net.InetAddress; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.BiConsumer; @RunWith(AndroidJUnit4.class) @SmallTest @@ -97,6 +102,32 @@ public class BpfCoordinatorTest { private static final MacAddress MAC_A = MacAddress.fromString("00:00:00:00:00:0a"); private static final MacAddress MAC_B = MacAddress.fromString("11:22:33:00:00:0b"); + // The test fake BPF map class is needed because the test has no privilege to access the BPF + // map. All member functions which eventually call JNI to access the real native BPF map need + // to be overridden. + // TODO: consider moving to an individual file. + private class TestBpfMap extends BpfMap { + private final HashMap mMap = new HashMap(); + + TestBpfMap(final Class key, final Class value) { + super(key, value); + } + + @Override + public void forEach(BiConsumer action) throws ErrnoException { + // TODO: consider using mocked #getFirstKey and #getNextKey to iterate. It helps to + // implement the entry deletion in the iteration if required. + for (Map.Entry entry : mMap.entrySet()) { + action.accept(entry.getKey(), entry.getValue()); + } + } + + @Override + public void updateEntry(K key, V value) throws ErrnoException { + mMap.put(key, value); + } + }; + @Mock private NetworkStatsManager mStatsManager; @Mock private INetd mNetd; @Mock private IpServer mIpServer; @@ -109,6 +140,8 @@ public class BpfCoordinatorTest { private final ArgumentCaptor mStringArrayCaptor = ArgumentCaptor.forClass(ArrayList.class); private final TestLooper mTestLooper = new TestLooper(); + private final TestBpfMap mBpfStatsMap = + spy(new TestBpfMap<>(TetherStatsKey.class, TetherStatsValue.class)); private BpfCoordinator.Dependencies mDeps = spy(new BpfCoordinator.Dependencies() { @NonNull @@ -140,6 +173,11 @@ public class BpfCoordinatorTest { public BpfMap getBpfIngressMap() { return mBpfIngressMap; } + + @Nullable + public BpfMap getBpfStatsMap() { + return mBpfStatsMap; + } }); @Before public void setUp() { @@ -190,14 +228,44 @@ public class BpfCoordinatorTest { return parcel; } - // Set up specific tether stats list and wait for the stats cache is updated by polling thread + // Update a stats entry or create if not exists. + private void updateStatsEntry(@NonNull TetherStatsParcel stats) throws Exception { + if (mDeps.isAtLeastS()) { + final TetherStatsKey key = new TetherStatsKey(stats.ifIndex); + final TetherStatsValue value = new TetherStatsValue(stats.rxPackets, stats.rxBytes, + 0L /* rxErrors */, stats.txPackets, stats.txBytes, 0L /* txErrors */); + mBpfStatsMap.updateEntry(key, value); + } else { + when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[] {stats}); + } + } + + // Update specific tether stats list and wait for the stats cache is updated by polling thread // in the coordinator. Beware of that it is only used for the default polling interval. - private void setTetherOffloadStatsList(TetherStatsParcel[] tetherStatsList) throws Exception { - when(mNetd.tetherOffloadGetStats()).thenReturn(tetherStatsList); + // Note that the mocked tetherOffloadGetStats of netd replaces all stats entries because it + // doesn't store the previous entries. + private void updateStatsEntriesAndWaitForUpdate(@NonNull TetherStatsParcel[] tetherStatsList) + throws Exception { + if (mDeps.isAtLeastS()) { + for (TetherStatsParcel stats : tetherStatsList) { + updateStatsEntry(stats); + } + } else { + when(mNetd.tetherOffloadGetStats()).thenReturn(tetherStatsList); + } + mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); waitForIdle(); } + private void clearStatsInvocations() { + if (mDeps.isAtLeastS()) { + clearInvocations(mBpfStatsMap); + } else { + clearInvocations(mNetd); + } + } + private T verifyWithOrder(@Nullable InOrder inOrder, @NonNull T t) { if (inOrder != null) { return inOrder.verify(t); @@ -206,6 +274,22 @@ public class BpfCoordinatorTest { } } + private void verifyTetherOffloadGetStats() throws Exception { + if (mDeps.isAtLeastS()) { + verify(mBpfStatsMap).forEach(any()); + } else { + verify(mNetd).tetherOffloadGetStats(); + } + } + + private void verifyNeverTetherOffloadGetStats() throws Exception { + if (mDeps.isAtLeastS()) { + verify(mBpfStatsMap, never()).forEach(any()); + } else { + verify(mNetd, never()).tetherOffloadGetStats(); + } + } + private void verifyTetherOffloadRuleAdd(@Nullable InOrder inOrder, @NonNull Ipv6ForwardingRule rule) throws Exception { if (mDeps.isAtLeastS()) { @@ -224,8 +308,30 @@ public class BpfCoordinatorTest { } } + private void verifyTetherOffloadRuleRemove(@Nullable InOrder inOrder, + @NonNull final Ipv6ForwardingRule rule) throws Exception { + if (mDeps.isAtLeastS()) { + verifyWithOrder(inOrder, mBpfIngressMap).deleteEntry(rule.makeTetherIngressKey()); + } else { + verifyWithOrder(inOrder, mNetd).tetherOffloadRuleRemove(matches(rule)); + } + } + + private void verifyNeverTetherOffloadRuleRemove() throws Exception { + if (mDeps.isAtLeastS()) { + verify(mBpfIngressMap, never()).deleteEntry(any()); + } else { + verify(mNetd, never()).tetherOffloadRuleRemove(any()); + } + } + + // S+ and R api minimum tests. + // The following tests are used to provide minimum checking for the APIs on different flow. + // The auto merge is not enabled on mainline prod. The code flow R may be verified at the + // late stage by manual cherry pick. It is risky if the R code flow has broken and be found at + // the last minute. // TODO: remove once presubmit tests on R even the code is submitted on S. - private void checkTetherOffloadRuleAdd(boolean usingApiS) throws Exception { + private void checkTetherOffloadRuleAddAndRemove(boolean usingApiS) throws Exception { setupFunctioningNetdInterface(); // Replace Dependencies#isAtLeastS() for testing R and S+ BPF map apis. Note that |mDeps| @@ -241,18 +347,61 @@ public class BpfCoordinatorTest { final Ipv6ForwardingRule rule = buildTestForwardingRule(mobileIfIndex, NEIGH_A, MAC_A); coordinator.tetherOffloadRuleAdd(mIpServer, rule); verifyTetherOffloadRuleAdd(null, rule); + + // Removing the last rule on current upstream immediately sends the cleanup stuff to netd. + when(mNetd.tetherOffloadGetAndClearStats(mobileIfIndex)) + .thenReturn(buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0)); + coordinator.tetherOffloadRuleRemove(mIpServer, rule); + verifyTetherOffloadRuleRemove(null, rule); } // TODO: remove once presubmit tests on R even the code is submitted on S. @Test - public void testTetherOffloadRuleAddSdkR() throws Exception { - checkTetherOffloadRuleAdd(false /* R */); + public void testTetherOffloadRuleAddAndRemoveSdkR() throws Exception { + checkTetherOffloadRuleAddAndRemove(false /* R */); } // TODO: remove once presubmit tests on R even the code is submitted on S. @Test - public void testTetherOffloadRuleAddAtLeastSdkS() throws Exception { - checkTetherOffloadRuleAdd(true /* S+ */); + public void testTetherOffloadRuleAddAndRemoveAtLeastSdkS() throws Exception { + checkTetherOffloadRuleAddAndRemove(true /* S+ */); + } + + // TODO: remove once presubmit tests on R even the code is submitted on S. + private void checkTetherOffloadGetStats(boolean usingApiS) throws Exception { + setupFunctioningNetdInterface(); + + doReturn(usingApiS).when(mDeps).isAtLeastS(); + final BpfCoordinator coordinator = makeBpfCoordinator(); + coordinator.startPolling(); + + final String mobileIface = "rmnet_data0"; + final Integer mobileIfIndex = 100; + coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface); + + updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] { + buildTestTetherStatsParcel(mobileIfIndex, 1000, 100, 2000, 200)}); + + final NetworkStats expectedIfaceStats = new NetworkStats(0L, 1) + .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 1000, 100, 2000, 200)); + + final NetworkStats expectedUidStats = new NetworkStats(0L, 1) + .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 1000, 100, 2000, 200)); + + mTetherStatsProvider.pushTetherStats(); + mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStats, expectedUidStats); + } + + // TODO: remove once presubmit tests on R even the code is submitted on S. + @Test + public void testTetherOffloadGetStatsSdkR() throws Exception { + checkTetherOffloadGetStats(false /* R */); + } + + // TODO: remove once presubmit tests on R even the code is submitted on S. + @Test + public void testTetherOffloadGetStatsAtLeastSdkS() throws Exception { + checkTetherOffloadGetStats(true /* S+ */); } @Test @@ -275,7 +424,7 @@ public class BpfCoordinatorTest { // [1] Both interface stats are changed. // Setup the tether stats of wlan and mobile interface. Note that move forward the time of // the looper to make sure the new tether stats has been updated by polling update thread. - setTetherOffloadStatsList(new TetherStatsParcel[] { + updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] { buildTestTetherStatsParcel(wlanIfIndex, 1000, 100, 2000, 200), buildTestTetherStatsParcel(mobileIfIndex, 3000, 300, 4000, 400)}); @@ -296,7 +445,7 @@ public class BpfCoordinatorTest { // [2] Only one interface stats is changed. // The tether stats of mobile interface is accumulated and The tether stats of wlan // interface is the same. - setTetherOffloadStatsList(new TetherStatsParcel[] { + updateStatsEntriesAndWaitForUpdate(new TetherStatsParcel[] { buildTestTetherStatsParcel(wlanIfIndex, 1000, 100, 2000, 200), buildTestTetherStatsParcel(mobileIfIndex, 3010, 320, 4030, 440)}); @@ -317,12 +466,12 @@ public class BpfCoordinatorTest { // Shutdown the coordinator and clear the invocation history, especially the // tetherOffloadGetStats() calls. coordinator.stopPolling(); - clearInvocations(mNetd); + clearStatsInvocations(); // Verify the polling update thread stopped. mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); waitForIdle(); - verify(mNetd, never()).tetherOffloadGetStats(); + verifyNeverTetherOffloadGetStats(); } @Test @@ -342,16 +491,14 @@ public class BpfCoordinatorTest { mTetherStatsProviderCb.expectNotifyAlertReached(); // Verify that notifyAlertReached never fired if quota is not yet reached. - when(mNetd.tetherOffloadGetStats()).thenReturn( - new TetherStatsParcel[] {buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0)}); + updateStatsEntry(buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0)); mTetherStatsProvider.onSetAlert(100); mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); waitForIdle(); mTetherStatsProviderCb.assertNoCallback(); // Verify that notifyAlertReached fired when quota is reached. - when(mNetd.tetherOffloadGetStats()).thenReturn( - new TetherStatsParcel[] {buildTestTetherStatsParcel(mobileIfIndex, 50, 0, 50, 0)}); + updateStatsEntry(buildTestTetherStatsParcel(mobileIfIndex, 50, 0, 50, 0)); mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); waitForIdle(); mTetherStatsProviderCb.expectNotifyAlertReached(); @@ -510,14 +657,14 @@ public class BpfCoordinatorTest { // Removing the second rule on current upstream does not send the quota to netd. coordinator.tetherOffloadRuleRemove(mIpServer, ruleB); - inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ruleB)); + verifyTetherOffloadRuleRemove(inOrder, ruleB); inOrder.verify(mNetd, never()).tetherOffloadSetInterfaceQuota(anyInt(), anyLong()); // Removing the last rule on current upstream immediately sends the cleanup stuff to netd. when(mNetd.tetherOffloadGetAndClearStats(mobileIfIndex)) .thenReturn(buildTestTetherStatsParcel(mobileIfIndex, 0, 0, 0, 0)); coordinator.tetherOffloadRuleRemove(mIpServer, ruleA); - inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ruleA)); + verifyTetherOffloadRuleRemove(inOrder, ruleA); inOrder.verify(mNetd).tetherOffloadGetAndClearStats(mobileIfIndex); inOrder.verifyNoMoreInteractions(); } @@ -569,10 +716,10 @@ public class BpfCoordinatorTest { // Update the existing rules for upstream changes. The rules are removed and re-added one // by one for updating upstream interface index by #tetherOffloadRuleUpdate. coordinator.tetherOffloadRuleUpdate(mIpServer, mobileIfIndex); - inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ethernetRuleA)); + verifyTetherOffloadRuleRemove(inOrder, ethernetRuleA); verifyTetherOffloadRuleAdd(inOrder, mobileRuleA); inOrder.verify(mNetd).tetherOffloadSetInterfaceQuota(mobileIfIndex, QUOTA_UNLIMITED); - inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(ethernetRuleB)); + verifyTetherOffloadRuleRemove(inOrder, ethernetRuleB); inOrder.verify(mNetd).tetherOffloadGetAndClearStats(ethIfIndex); verifyTetherOffloadRuleAdd(inOrder, mobileRuleB); @@ -580,8 +727,8 @@ public class BpfCoordinatorTest { when(mNetd.tetherOffloadGetAndClearStats(mobileIfIndex)) .thenReturn(buildTestTetherStatsParcel(mobileIfIndex, 50, 60, 70, 80)); coordinator.tetherOffloadRuleClear(mIpServer); - inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(mobileRuleA)); - inOrder.verify(mNetd).tetherOffloadRuleRemove(matches(mobileRuleB)); + verifyTetherOffloadRuleRemove(inOrder, mobileRuleA); + verifyTetherOffloadRuleRemove(inOrder, mobileRuleB); inOrder.verify(mNetd).tetherOffloadGetAndClearStats(mobileIfIndex); // [4] Force pushing stats update to verify that the last diff of stats is reported on all @@ -606,7 +753,7 @@ public class BpfCoordinatorTest { // The tether stats polling task should not be scheduled. mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS); waitForIdle(); - verify(mNetd, never()).tetherOffloadGetStats(); + verifyNeverTetherOffloadGetStats(); // The interface name lookup table can't be added. final String iface = "rmnet_data0"; @@ -631,21 +778,21 @@ public class BpfCoordinatorTest { rules.put(rule.address, rule); coordinator.getForwardingRulesForTesting().put(mIpServer, rules); coordinator.tetherOffloadRuleRemove(mIpServer, rule); - verify(mNetd, never()).tetherOffloadRuleRemove(any()); + verifyNeverTetherOffloadRuleRemove(); rules = coordinator.getForwardingRulesForTesting().get(mIpServer); assertNotNull(rules); assertEquals(1, rules.size()); // The rule can't be cleared. coordinator.tetherOffloadRuleClear(mIpServer); - verify(mNetd, never()).tetherOffloadRuleRemove(any()); + verifyNeverTetherOffloadRuleRemove(); rules = coordinator.getForwardingRulesForTesting().get(mIpServer); assertNotNull(rules); assertEquals(1, rules.size()); // The rule can't be updated. coordinator.tetherOffloadRuleUpdate(mIpServer, rule.upstreamIfindex + 1 /* new */); - verify(mNetd, never()).tetherOffloadRuleRemove(any()); + verifyNeverTetherOffloadRuleRemove(); verifyNeverTetherOffloadRuleAdd(); rules = coordinator.getForwardingRulesForTesting().get(mIpServer); assertNotNull(rules); @@ -669,6 +816,15 @@ public class BpfCoordinatorTest { checkBpfDisabled(); } + @Test + @IgnoreUpTo(Build.VERSION_CODES.R) + public void testBpfDisabledbyNoBpfStatsMap() throws Exception { + setupFunctioningNetdInterface(); + doReturn(null).when(mDeps).getBpfStatsMap(); + + checkBpfDisabled(); + } + @Test public void testTetheringConfigSetPollingInterval() throws Exception { setupFunctioningNetdInterface(); @@ -703,18 +859,18 @@ public class BpfCoordinatorTest { // Start on a new polling time slot. mTestLooper.moveTimeForward(pollingInterval); waitForIdle(); - clearInvocations(mNetd); + clearStatsInvocations(); // Move time forward to 90% polling interval time. Expect that the polling thread has not // scheduled yet. mTestLooper.moveTimeForward((long) (pollingInterval * 0.9)); waitForIdle(); - verify(mNetd, never()).tetherOffloadGetStats(); + verifyNeverTetherOffloadGetStats(); // Move time forward to the remaining 10% polling interval time. Expect that the polling // thread has scheduled. mTestLooper.moveTimeForward((long) (pollingInterval * 0.1)); waitForIdle(); - verify(mNetd).tetherOffloadGetStats(); + verifyTetherOffloadGetStats(); } }