Merge changes from topic "tether_offload_data_limit"

* changes:
  Make tethering module to use netd_aidl_interface-unstable-java
  [BOT.5] Move class Ipv6ForwardingRule from IpServer to the coordinator
  [BOT.3] Add unit test for polling network stats in the coordinator
  [BOT.2] Create a coordinator and stats provider to provide tether stats
  [BOT.1] Add a class ForwardedStats in TetheringUtils
This commit is contained in:
Nucca Chen
2020-06-01 23:33:24 +00:00
committed by Gerrit Code Review
9 changed files with 659 additions and 42 deletions

View File

@@ -25,7 +25,7 @@ java_defaults {
],
static_libs: [
"androidx.annotation_annotation",
"netd_aidl_interface-V3-java",
"netd_aidl_interface-unstable-java",
"netlink-client",
"networkstack-aidl-interfaces-java",
"android.hardware.tetheroffload.config-V1.0-java",

View File

@@ -33,7 +33,6 @@ import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.RouteInfo;
import android.net.TetherOffloadRuleParcel;
import android.net.TetheredClient;
import android.net.TetheringManager;
import android.net.TetheringRequestParcel;
@@ -65,6 +64,8 @@ import androidx.annotation.Nullable;
import com.android.internal.util.MessageUtils;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.networkstack.tethering.BpfCoordinator;
import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
import com.android.networkstack.tethering.PrivateAddressCoordinator;
import java.io.IOException;
@@ -225,6 +226,8 @@ public class IpServer extends StateMachine {
private final SharedLog mLog;
private final INetd mNetd;
@NonNull
private final BpfCoordinator mBpfCoordinator;
private final Callback mCallback;
private final InterfaceController mInterfaceCtrl;
private final PrivateAddressCoordinator mPrivateAddressCoordinator;
@@ -269,40 +272,6 @@ public class IpServer extends StateMachine {
}
}
static class Ipv6ForwardingRule {
public final int upstreamIfindex;
public final int downstreamIfindex;
public final Inet6Address address;
public final MacAddress srcMac;
public final MacAddress dstMac;
Ipv6ForwardingRule(int upstreamIfindex, int downstreamIfIndex, Inet6Address address,
MacAddress srcMac, MacAddress dstMac) {
this.upstreamIfindex = upstreamIfindex;
this.downstreamIfindex = downstreamIfIndex;
this.address = address;
this.srcMac = srcMac;
this.dstMac = dstMac;
}
public Ipv6ForwardingRule onNewUpstream(int newUpstreamIfindex) {
return new Ipv6ForwardingRule(newUpstreamIfindex, downstreamIfindex, address, srcMac,
dstMac);
}
// Don't manipulate TetherOffloadRuleParcel directly because implementing onNewUpstream()
// would be error-prone due to generated stable AIDL classes not having a copy constructor.
public TetherOffloadRuleParcel toTetherOffloadRuleParcel() {
final TetherOffloadRuleParcel parcel = new TetherOffloadRuleParcel();
parcel.inputInterfaceIndex = upstreamIfindex;
parcel.outputInterfaceIndex = downstreamIfindex;
parcel.destination = address.getAddress();
parcel.prefixLength = 128;
parcel.srcL2Address = srcMac.toByteArray();
parcel.dstL2Address = dstMac.toByteArray();
return parcel;
}
}
private final LinkedHashMap<Inet6Address, Ipv6ForwardingRule> mIpv6ForwardingRules =
new LinkedHashMap<>();
@@ -314,11 +283,13 @@ public class IpServer extends StateMachine {
// object. It helps to reduce the arguments of the constructor.
public IpServer(
String ifaceName, Looper looper, int interfaceType, SharedLog log,
INetd netd, Callback callback, boolean usingLegacyDhcp, boolean usingBpfOffload,
INetd netd, @NonNull BpfCoordinator coordinator, Callback callback,
boolean usingLegacyDhcp, boolean usingBpfOffload,
PrivateAddressCoordinator addressCoordinator, Dependencies deps) {
super(ifaceName, looper);
mLog = log.forSubComponent(ifaceName);
mNetd = netd;
mBpfCoordinator = coordinator;
mCallback = callback;
mInterfaceCtrl = new InterfaceController(ifaceName, mNetd, mLog);
mIfaceName = ifaceName;
@@ -754,6 +725,14 @@ public class IpServer extends StateMachine {
}
upstreamIfindex = mDeps.getIfindex(upstreamIface);
// Add upstream index to name mapping for the tether stats usage in the coordinator.
// Although this mapping could be added by both class Tethering and IpServer, adding
// mapping from IpServer guarantees that the mapping is added before the adding
// forwarding rules. That is because there are different state machines in both
// classes. It is hard to guarantee the link property update order between multiple
// state machines.
mBpfCoordinator.addUpstreamNameToLookupTable(upstreamIfindex, upstreamIface);
}
// If v6only is null, we pass in null to setRaParams(), which handles

View File

@@ -15,18 +15,93 @@
*/
package android.net.util;
import android.net.TetherStatsParcel;
import android.net.TetheringRequestParcel;
import androidx.annotation.NonNull;
import java.io.FileDescriptor;
import java.net.SocketException;
import java.util.Objects;
/**
* Native methods for tethering utilization.
* The classes and the methods for tethering utilization.
*
* {@hide}
*/
public class TetheringUtils {
/**
* The object which records offload Tx/Rx forwarded bytes/packets.
* TODO: Replace the inner class ForwardedStats of class OffloadHardwareInterface with
* this class as well.
*/
public static class ForwardedStats {
public final long rxBytes;
public final long rxPackets;
public final long txBytes;
public final long txPackets;
public ForwardedStats() {
rxBytes = 0;
rxPackets = 0;
txBytes = 0;
txPackets = 0;
}
public ForwardedStats(long rxBytes, long txBytes) {
this.rxBytes = rxBytes;
this.rxPackets = 0;
this.txBytes = txBytes;
this.txPackets = 0;
}
public ForwardedStats(long rxBytes, long rxPackets, long txBytes, long txPackets) {
this.rxBytes = rxBytes;
this.rxPackets = rxPackets;
this.txBytes = txBytes;
this.txPackets = txPackets;
}
public ForwardedStats(@NonNull TetherStatsParcel tetherStats) {
rxBytes = tetherStats.rxBytes;
rxPackets = tetherStats.rxPackets;
txBytes = tetherStats.txBytes;
txPackets = tetherStats.txPackets;
}
public ForwardedStats(@NonNull ForwardedStats other) {
rxBytes = other.rxBytes;
rxPackets = other.rxPackets;
txBytes = other.txBytes;
txPackets = other.txPackets;
}
/** Add Tx/Rx bytes/packets and return the result as a new object. */
@NonNull
public ForwardedStats add(@NonNull ForwardedStats other) {
return new ForwardedStats(rxBytes + other.rxBytes, rxPackets + other.rxPackets,
txBytes + other.txBytes, txPackets + other.txPackets);
}
/** Subtract Tx/Rx bytes/packets and return the result as a new object. */
@NonNull
public ForwardedStats subtract(@NonNull ForwardedStats other) {
// TODO: Perhaps throw an exception if any negative difference value just in case.
final long rxBytesDiff = Math.max(rxBytes - other.rxBytes, 0);
final long rxPacketsDiff = Math.max(rxPackets - other.rxPackets, 0);
final long txBytesDiff = Math.max(txBytes - other.txBytes, 0);
final long txPacketsDiff = Math.max(txPackets - other.txPackets, 0);
return new ForwardedStats(rxBytesDiff, rxPacketsDiff, txBytesDiff, txPacketsDiff);
}
/** Returns the string representation of this object. */
@NonNull
public String toString() {
return String.format("ForwardedStats(rxb: %d, rxp: %d, txb: %d, txp: %d)", rxBytes,
rxPackets, txBytes, txPackets);
}
}
/**
* Configures a socket for receiving ICMPv6 router solicitations and sending advertisements.
* @param fd the socket's {@link FileDescriptor}.

View File

@@ -0,0 +1,329 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.networkstack.tethering;
import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
import static android.net.NetworkStats.METERED_NO;
import static android.net.NetworkStats.ROAMING_NO;
import static android.net.NetworkStats.SET_DEFAULT;
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
import static android.net.NetworkStats.UID_TETHERING;
import android.app.usage.NetworkStatsManager;
import android.net.INetd;
import android.net.MacAddress;
import android.net.NetworkStats;
import android.net.NetworkStats.Entry;
import android.net.TetherOffloadRuleParcel;
import android.net.TetherStatsParcel;
import android.net.netstats.provider.NetworkStatsProvider;
import android.net.util.SharedLog;
import android.net.util.TetheringUtils.ForwardedStats;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.util.Log;
import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import java.net.Inet6Address;
/**
* This coordinator is responsible for providing BPF offload relevant functionality.
* - Get tethering stats.
*
* @hide
*/
public class BpfCoordinator {
private static final String TAG = BpfCoordinator.class.getSimpleName();
@VisibleForTesting
static final int DEFAULT_PERFORM_POLL_INTERVAL_MS = 5000; // TODO: Make it customizable.
@VisibleForTesting
enum StatsType {
STATS_PER_IFACE,
STATS_PER_UID,
}
@NonNull
private final Handler mHandler;
@NonNull
private final INetd mNetd;
@NonNull
private final SharedLog mLog;
@NonNull
private final Dependencies mDeps;
@Nullable
private final BpfTetherStatsProvider mStatsProvider;
private boolean mStarted = false;
// Maps upstream interface index to offloaded traffic statistics.
// Always contains the latest total bytes/packets, since each upstream was started, received
// from the BPF maps for each interface.
private SparseArray<ForwardedStats> mStats = new SparseArray<>();
// Maps upstream interface index to interface names.
// Store all interface name since boot. Used for lookup what interface name it is from the
// tether stats got from netd because netd reports interface index to present an interface.
// TODO: Remove the unused interface name.
private SparseArray<String> mInterfaceNames = new SparseArray<>();
// Runnable that used by scheduling next polling of stats.
private final Runnable mScheduledPollingTask = () -> {
updateForwardedStatsFromNetd();
maybeSchedulePollingStats();
};
@VisibleForTesting
static class Dependencies {
int getPerformPollInterval() {
// TODO: Consider make this configurable.
return DEFAULT_PERFORM_POLL_INTERVAL_MS;
}
}
BpfCoordinator(@NonNull Handler handler, @NonNull INetd netd,
@NonNull NetworkStatsManager nsm, @NonNull SharedLog log, @NonNull Dependencies deps) {
mHandler = handler;
mNetd = netd;
mLog = log.forSubComponent(TAG);
BpfTetherStatsProvider provider = new BpfTetherStatsProvider();
try {
nsm.registerNetworkStatsProvider(getClass().getSimpleName(), provider);
} catch (RuntimeException e) {
// TODO: Perhaps not allow to use BPF offload because the reregistration failure
// implied that no data limit could be applies on a metered upstream if any.
Log.wtf(TAG, "Cannot register offload stats provider: " + e);
provider = null;
}
mStatsProvider = provider;
mDeps = deps;
}
/**
* Start BPF tethering offload stats polling when the first upstream is started.
* Note that this can be only called on handler thread.
* TODO: Perhaps check BPF support before starting.
* TODO: Start the stats polling only if there is any client on the downstream.
*/
public void start() {
if (mStarted) return;
mStarted = true;
maybeSchedulePollingStats();
mLog.i("BPF tethering coordinator started");
}
/**
* Stop BPF tethering offload stats polling and cleanup upstream parameters.
* Note that this can be only called on handler thread.
*/
public void stop() {
if (!mStarted) return;
// Stop scheduled polling tasks and poll the latest stats from BPF maps.
if (mHandler.hasCallbacks(mScheduledPollingTask)) {
mHandler.removeCallbacks(mScheduledPollingTask);
}
updateForwardedStatsFromNetd();
mStarted = false;
mLog.i("BPF tethering coordinator stopped");
}
/**
* Add upstream name to lookup table. The lookup table is used for tether stats interface name
* lookup because the netd only reports interface index in BPF tether stats but the service
* expects the interface name in NetworkStats object.
* Note that this can be only called on handler thread.
*/
public void addUpstreamNameToLookupTable(int upstreamIfindex, String upstreamIface) {
if (upstreamIfindex == 0) return;
// The same interface index to name mapping may be added by different IpServer objects or
// re-added by reconnection on the same upstream interface. Ignore the duplicate one.
final String iface = mInterfaceNames.get(upstreamIfindex);
if (iface == null) {
mInterfaceNames.put(upstreamIfindex, upstreamIface);
} else if (iface != upstreamIface) {
Log.wtf(TAG, "The upstream interface name " + upstreamIface
+ " is different from the existing interface name "
+ iface + " for index " + upstreamIfindex);
}
}
/** IPv6 forwarding rule class. */
public static class Ipv6ForwardingRule {
public final int upstreamIfindex;
public final int downstreamIfindex;
public final Inet6Address address;
public final MacAddress srcMac;
public final MacAddress dstMac;
public Ipv6ForwardingRule(int upstreamIfindex, int downstreamIfIndex, Inet6Address address,
MacAddress srcMac, MacAddress dstMac) {
this.upstreamIfindex = upstreamIfindex;
this.downstreamIfindex = downstreamIfIndex;
this.address = address;
this.srcMac = srcMac;
this.dstMac = dstMac;
}
/** Return a new rule object which updates with new upstream index. */
public Ipv6ForwardingRule onNewUpstream(int newUpstreamIfindex) {
return new Ipv6ForwardingRule(newUpstreamIfindex, downstreamIfindex, address, srcMac,
dstMac);
}
/**
* Don't manipulate TetherOffloadRuleParcel directly because implementing onNewUpstream()
* would be error-prone due to generated stable AIDL classes not having a copy constructor.
*/
public TetherOffloadRuleParcel toTetherOffloadRuleParcel() {
final TetherOffloadRuleParcel parcel = new TetherOffloadRuleParcel();
parcel.inputInterfaceIndex = upstreamIfindex;
parcel.outputInterfaceIndex = downstreamIfindex;
parcel.destination = address.getAddress();
parcel.prefixLength = 128;
parcel.srcL2Address = srcMac.toByteArray();
parcel.dstL2Address = dstMac.toByteArray();
return parcel;
}
}
/**
* A BPF tethering stats provider to provide network statistics to the system.
* Note that this class's data may only be accessed on the handler thread.
*/
@VisibleForTesting
class BpfTetherStatsProvider extends NetworkStatsProvider {
// The offloaded traffic statistics per interface that has not been reported since the
// last call to pushTetherStats. Only the interfaces that were ever tethering upstreams
// and has pending tether stats delta are included in this NetworkStats object.
private NetworkStats mIfaceStats = new NetworkStats(0L, 0);
// The same stats as above, but counts network stats per uid.
private NetworkStats mUidStats = new NetworkStats(0L, 0);
@Override
public void onRequestStatsUpdate(int token) {
mHandler.post(() -> pushTetherStats());
}
@Override
public void onSetAlert(long quotaBytes) {
// no-op
}
@Override
public void onSetLimit(@NonNull String iface, long quotaBytes) {
// no-op
}
@VisibleForTesting
void pushTetherStats() {
try {
// The token is not used for now. See b/153606961.
notifyStatsUpdated(0 /* token */, mIfaceStats, mUidStats);
// Clear the accumulated tether stats delta after reported. Note that create a new
// empty object because NetworkStats#clear is @hide.
mIfaceStats = new NetworkStats(0L, 0);
mUidStats = new NetworkStats(0L, 0);
} catch (RuntimeException e) {
mLog.e("Cannot report network stats: ", e);
}
}
private void accumulateDiff(@NonNull NetworkStats ifaceDiff,
@NonNull NetworkStats uidDiff) {
mIfaceStats = mIfaceStats.add(ifaceDiff);
mUidStats = mUidStats.add(uidDiff);
}
}
@NonNull
private NetworkStats buildNetworkStats(@NonNull StatsType type, int ifIndex,
@NonNull ForwardedStats diff) {
NetworkStats stats = new NetworkStats(0L, 0);
final String iface = mInterfaceNames.get(ifIndex);
if (iface == null) {
// TODO: Use Log.wtf once the coordinator owns full control of tether stats from netd.
// For now, netd may add the empty stats for the upstream which is not monitored by
// the coordinator. Silently ignore it.
return stats;
}
final int uid = (type == StatsType.STATS_PER_UID) ? UID_TETHERING : UID_ALL;
// Note that the argument 'metered', 'roaming' and 'defaultNetwork' are not recorded for
// network stats snapshot. See NetworkStatsRecorder#recordSnapshotLocked.
return stats.addEntry(new Entry(iface, uid, SET_DEFAULT, TAG_NONE, METERED_NO,
ROAMING_NO, DEFAULT_NETWORK_NO, diff.rxBytes, diff.rxPackets,
diff.txBytes, diff.txPackets, 0L /* operations */));
}
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);
return;
}
for (TetherStatsParcel tetherStats : tetherStatsList) {
final Integer ifIndex = tetherStats.ifIndex;
final ForwardedStats curr = new ForwardedStats(tetherStats);
final ForwardedStats base = mStats.get(ifIndex);
final ForwardedStats diff = (base != null) ? curr.subtract(base) : curr;
// Update the local cache for counting tether stats delta.
mStats.put(ifIndex, curr);
// Update the accumulated tether stats delta to the stats provider for the service
// querying.
if (mStatsProvider != null) {
try {
mStatsProvider.accumulateDiff(
buildNetworkStats(StatsType.STATS_PER_IFACE, ifIndex, diff),
buildNetworkStats(StatsType.STATS_PER_UID, ifIndex, diff));
} catch (ArrayIndexOutOfBoundsException e) {
Log.wtf("Fail to update the accumulated stats delta for interface index "
+ ifIndex + " : ", e);
}
}
}
}
private void maybeSchedulePollingStats() {
if (!mStarted) return;
if (mHandler.hasCallbacks(mScheduledPollingTask)) {
mHandler.removeCallbacks(mScheduledPollingTask);
}
mHandler.postDelayed(mScheduledPollingTask, mDeps.getPerformPollInterval());
}
}

View File

@@ -232,6 +232,7 @@ public class Tethering {
private final TetheringThreadExecutor mExecutor;
private final TetheringNotificationUpdater mNotificationUpdater;
private final UserManager mUserManager;
private final BpfCoordinator mBpfCoordinator;
private final PrivateAddressCoordinator mPrivateAddressCoordinator;
private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID;
// All the usage of mTetheringEventCallback should run in the same thread.
@@ -284,6 +285,8 @@ public class Tethering {
mUpstreamNetworkMonitor = mDeps.getUpstreamNetworkMonitor(mContext, mTetherMasterSM, mLog,
TetherMasterSM.EVENT_UPSTREAM_CALLBACK);
mForwardedDownstreams = new LinkedHashSet<>();
mBpfCoordinator = mDeps.getBpfCoordinator(
mHandler, mNetd, mLog, new BpfCoordinator.Dependencies());
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_CARRIER_CONFIG_CHANGED);
@@ -1704,6 +1707,9 @@ public class Tethering {
chooseUpstreamType(true);
mTryCell = false;
}
// TODO: Check the upstream interface if it is managed by BPF offload.
mBpfCoordinator.start();
}
@Override
@@ -1716,6 +1722,7 @@ public class Tethering {
mTetherUpstream = null;
reportUpstreamChanged(null);
}
mBpfCoordinator.stop();
}
private boolean updateUpstreamWanted() {
@@ -2341,7 +2348,7 @@ public class Tethering {
mLog.log("adding TetheringInterfaceStateMachine for: " + iface);
final TetherState tetherState = new TetherState(
new IpServer(iface, mLooper, interfaceType, mLog, mNetd,
new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator,
makeControlCallback(), mConfig.enableLegacyDhcpServer,
mConfig.enableBpfOffload, mPrivateAddressCoordinator,
mDeps.getIpServerDependencies()));

View File

@@ -40,6 +40,17 @@ import java.util.ArrayList;
* @hide
*/
public abstract class TetheringDependencies {
/**
* Get a reference to the BpfCoordinator to be used by tethering.
*/
public @NonNull BpfCoordinator getBpfCoordinator(
@NonNull Handler handler, @NonNull INetd netd, @NonNull SharedLog log,
@NonNull BpfCoordinator.Dependencies deps) {
final NetworkStatsManager statsManager =
(NetworkStatsManager) getContext().getSystemService(Context.NETWORK_STATS_SERVICE);
return new BpfCoordinator(handler, netd, statsManager, log, deps);
}
/**
* Get a reference to the offload hardware interface to be used by tethering.
*/

View File

@@ -87,6 +87,7 @@ import android.text.TextUtils;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.networkstack.tethering.BpfCoordinator;
import com.android.networkstack.tethering.PrivateAddressCoordinator;
import org.junit.Before;
@@ -126,6 +127,7 @@ public class IpServerTest {
private final IpPrefix mBluetoothPrefix = new IpPrefix("192.168.44.0/24");
@Mock private INetd mNetd;
@Mock private BpfCoordinator mBpfCoordinator;
@Mock private IpServer.Callback mCallback;
@Mock private SharedLog mSharedLog;
@Mock private IDhcpServer mDhcpServer;
@@ -179,7 +181,7 @@ public class IpServerTest {
neighborCaptor.capture());
mIpServer = new IpServer(
IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog, mNetd,
IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog, mNetd, mBpfCoordinator,
mCallback, usingLegacyDhcp, usingBpfOffload, mAddressCoordinator, mDependencies);
mIpServer.start();
mNeighborEventConsumer = neighborCaptor.getValue();
@@ -222,8 +224,8 @@ public class IpServerTest {
when(mDependencies.getIpNeighborMonitor(any(), any(), any()))
.thenReturn(mIpNeighborMonitor);
mIpServer = new IpServer(IFACE_NAME, mLooper.getLooper(), TETHERING_BLUETOOTH, mSharedLog,
mNetd, mCallback, false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD,
mAddressCoordinator, mDependencies);
mNetd, mBpfCoordinator, mCallback, false /* usingLegacyDhcp */,
DEFAULT_USING_BPF_OFFLOAD, mAddressCoordinator, mDependencies);
mIpServer.start();
mLooper.dispatchAll();
verify(mCallback).updateInterfaceState(

View File

@@ -0,0 +1,207 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.networkstack.tethering;
import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
import static android.net.NetworkStats.METERED_NO;
import static android.net.NetworkStats.ROAMING_NO;
import static android.net.NetworkStats.SET_DEFAULT;
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
import static android.net.NetworkStats.UID_TETHERING;
import static com.android.networkstack.tethering.BpfCoordinator
.DEFAULT_PERFORM_POLL_INTERVAL_MS;
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 junit.framework.Assert.assertNotNull;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.annotation.NonNull;
import android.app.usage.NetworkStatsManager;
import android.net.INetd;
import android.net.NetworkStats;
import android.net.TetherStatsParcel;
import android.net.util.SharedLog;
import android.os.Handler;
import android.os.test.TestLooper;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.testutils.TestableNetworkStatsProviderCbBinder;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class BpfCoordinatorTest {
@Mock private NetworkStatsManager mStatsManager;
@Mock private INetd mNetd;
// Late init since methods must be called by the thread that created this object.
private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
private BpfCoordinator.BpfTetherStatsProvider mTetherStatsProvider;
private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
ArgumentCaptor.forClass(ArrayList.class);
private final TestLooper mTestLooper = new TestLooper();
private BpfCoordinator.Dependencies mDeps =
new BpfCoordinator.Dependencies() {
@Override
int getPerformPollInterval() {
return DEFAULT_PERFORM_POLL_INTERVAL_MS;
}
};
@Before public void setUp() {
MockitoAnnotations.initMocks(this);
}
private void waitForIdle() {
mTestLooper.dispatchAll();
}
private void setupFunctioningNetdInterface() throws Exception {
when(mNetd.tetherOffloadGetStats()).thenReturn(new TetherStatsParcel[0]);
}
@NonNull
private BpfCoordinator makeBpfCoordinator() throws Exception {
BpfCoordinator coordinator = new BpfCoordinator(
new Handler(mTestLooper.getLooper()), mNetd, mStatsManager, new SharedLog("test"),
mDeps);
final ArgumentCaptor<BpfCoordinator.BpfTetherStatsProvider>
tetherStatsProviderCaptor =
ArgumentCaptor.forClass(BpfCoordinator.BpfTetherStatsProvider.class);
verify(mStatsManager).registerNetworkStatsProvider(anyString(),
tetherStatsProviderCaptor.capture());
mTetherStatsProvider = tetherStatsProviderCaptor.getValue();
assertNotNull(mTetherStatsProvider);
mTetherStatsProviderCb = new TestableNetworkStatsProviderCbBinder();
mTetherStatsProvider.setProviderCallbackBinder(mTetherStatsProviderCb);
return coordinator;
}
@NonNull
private static NetworkStats.Entry buildTestEntry(@NonNull StatsType how,
@NonNull String iface, long rxBytes, long rxPackets, long txBytes, long txPackets) {
return new NetworkStats.Entry(iface, how == STATS_PER_IFACE ? UID_ALL : UID_TETHERING,
SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes,
rxPackets, txBytes, txPackets, 0L);
}
@NonNull
private static TetherStatsParcel buildTestTetherStatsParcel(@NonNull Integer ifIndex,
long rxBytes, long rxPackets, long txBytes, long txPackets) {
final TetherStatsParcel parcel = new TetherStatsParcel();
parcel.ifIndex = ifIndex;
parcel.rxBytes = rxBytes;
parcel.rxPackets = rxPackets;
parcel.txBytes = txBytes;
parcel.txPackets = txPackets;
return parcel;
}
private void setTetherOffloadStatsList(TetherStatsParcel[] tetherStatsList) throws Exception {
when(mNetd.tetherOffloadGetStats()).thenReturn(tetherStatsList);
mTestLooper.moveTimeForward(DEFAULT_PERFORM_POLL_INTERVAL_MS);
waitForIdle();
}
@Test
public void testGetForwardedStats() throws Exception {
setupFunctioningNetdInterface();
final BpfCoordinator coordinator = makeBpfCoordinator();
coordinator.start();
final String wlanIface = "wlan0";
final Integer wlanIfIndex = 100;
final String mobileIface = "rmnet_data0";
final Integer mobileIfIndex = 101;
// Add interface name to lookup table. In realistic case, the upstream interface name will
// be added by IpServer when IpServer has received with a new IPv6 upstream update event.
coordinator.addUpstreamNameToLookupTable(wlanIfIndex, wlanIface);
coordinator.addUpstreamNameToLookupTable(mobileIfIndex, mobileIface);
// [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[] {
buildTestTetherStatsParcel(wlanIfIndex, 1000, 100, 2000, 200),
buildTestTetherStatsParcel(mobileIfIndex, 3000, 300, 4000, 400)});
final NetworkStats expectedIfaceStats = new NetworkStats(0L, 2)
.addEntry(buildTestEntry(STATS_PER_IFACE, wlanIface, 1000, 100, 2000, 200))
.addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 3000, 300, 4000, 400));
final NetworkStats expectedUidStats = new NetworkStats(0L, 2)
.addEntry(buildTestEntry(STATS_PER_UID, wlanIface, 1000, 100, 2000, 200))
.addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 3000, 300, 4000, 400));
// Force pushing stats update to verify the stats reported.
// TODO: Perhaps make #expectNotifyStatsUpdated to use test TetherStatsParcel object for
// verifying the notification.
mTetherStatsProvider.pushTetherStats();
mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStats, expectedUidStats);
// [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[] {
buildTestTetherStatsParcel(wlanIfIndex, 1000, 100, 2000, 200),
buildTestTetherStatsParcel(mobileIfIndex, 3010, 320, 4030, 440)});
final NetworkStats expectedIfaceStatsDiff = new NetworkStats(0L, 2)
.addEntry(buildTestEntry(STATS_PER_IFACE, wlanIface, 0, 0, 0, 0))
.addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 10, 20, 30, 40));
final NetworkStats expectedUidStatsDiff = new NetworkStats(0L, 2)
.addEntry(buildTestEntry(STATS_PER_UID, wlanIface, 0, 0, 0, 0))
.addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 10, 20, 30, 40));
// Force pushing stats update to verify that only diff of stats is reported.
mTetherStatsProvider.pushTetherStats();
mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStatsDiff,
expectedUidStatsDiff);
// [3] Stop coordinator.
// Shutdown the coordinator and clear the invocation history, especially the
// tetherOffloadGetStats() calls.
coordinator.stop();
clearInvocations(mNetd);
// Verify the polling update thread stopped.
mTestLooper.moveTimeForward(DEFAULT_PERFORM_POLL_INTERVAL_MS);
waitForIdle();
verify(mNetd, never()).tetherOffloadGetStats();
}
}

View File

@@ -203,6 +203,7 @@ public class TetheringTest {
@Mock private ConnectivityManager mCm;
@Mock private EthernetManager mEm;
@Mock private TetheringNotificationUpdater mNotificationUpdater;
@Mock private BpfCoordinator mBpfCoordinator;
private final MockIpServerDependencies mIpServerDependencies =
spy(new MockIpServerDependencies());
@@ -336,6 +337,12 @@ public class TetheringTest {
mIpv6CoordinatorNotifyList = null;
}
@Override
public BpfCoordinator getBpfCoordinator(Handler handler, INetd netd,
SharedLog log, BpfCoordinator.Dependencies deps) {
return mBpfCoordinator;
}
@Override
public OffloadHardwareInterface getOffloadHardwareInterface(Handler h, SharedLog log) {
return mOffloadHardwareInterface;