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: [ static_libs: [
"androidx.annotation_annotation", "androidx.annotation_annotation",
"netd_aidl_interface-V3-java", "netd_aidl_interface-unstable-java",
"netlink-client", "netlink-client",
"networkstack-aidl-interfaces-java", "networkstack-aidl-interfaces-java",
"android.hardware.tetheroffload.config-V1.0-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.LinkProperties;
import android.net.MacAddress; import android.net.MacAddress;
import android.net.RouteInfo; import android.net.RouteInfo;
import android.net.TetherOffloadRuleParcel;
import android.net.TetheredClient; import android.net.TetheredClient;
import android.net.TetheringManager; import android.net.TetheringManager;
import android.net.TetheringRequestParcel; import android.net.TetheringRequestParcel;
@@ -65,6 +64,8 @@ import androidx.annotation.Nullable;
import com.android.internal.util.MessageUtils; import com.android.internal.util.MessageUtils;
import com.android.internal.util.State; import com.android.internal.util.State;
import com.android.internal.util.StateMachine; 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 com.android.networkstack.tethering.PrivateAddressCoordinator;
import java.io.IOException; import java.io.IOException;
@@ -225,6 +226,8 @@ public class IpServer extends StateMachine {
private final SharedLog mLog; private final SharedLog mLog;
private final INetd mNetd; private final INetd mNetd;
@NonNull
private final BpfCoordinator mBpfCoordinator;
private final Callback mCallback; private final Callback mCallback;
private final InterfaceController mInterfaceCtrl; private final InterfaceController mInterfaceCtrl;
private final PrivateAddressCoordinator mPrivateAddressCoordinator; 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 = private final LinkedHashMap<Inet6Address, Ipv6ForwardingRule> mIpv6ForwardingRules =
new LinkedHashMap<>(); new LinkedHashMap<>();
@@ -314,11 +283,13 @@ public class IpServer extends StateMachine {
// object. It helps to reduce the arguments of the constructor. // object. It helps to reduce the arguments of the constructor.
public IpServer( public IpServer(
String ifaceName, Looper looper, int interfaceType, SharedLog log, 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) { PrivateAddressCoordinator addressCoordinator, Dependencies deps) {
super(ifaceName, looper); super(ifaceName, looper);
mLog = log.forSubComponent(ifaceName); mLog = log.forSubComponent(ifaceName);
mNetd = netd; mNetd = netd;
mBpfCoordinator = coordinator;
mCallback = callback; mCallback = callback;
mInterfaceCtrl = new InterfaceController(ifaceName, mNetd, mLog); mInterfaceCtrl = new InterfaceController(ifaceName, mNetd, mLog);
mIfaceName = ifaceName; mIfaceName = ifaceName;
@@ -754,6 +725,14 @@ public class IpServer extends StateMachine {
} }
upstreamIfindex = mDeps.getIfindex(upstreamIface); 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 // If v6only is null, we pass in null to setRaParams(), which handles

View File

@@ -15,18 +15,93 @@
*/ */
package android.net.util; package android.net.util;
import android.net.TetherStatsParcel;
import android.net.TetheringRequestParcel; import android.net.TetheringRequestParcel;
import androidx.annotation.NonNull;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.net.SocketException; import java.net.SocketException;
import java.util.Objects; import java.util.Objects;
/** /**
* Native methods for tethering utilization. * The classes and the methods for tethering utilization.
* *
* {@hide} * {@hide}
*/ */
public class TetheringUtils { 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. * Configures a socket for receiving ICMPv6 router solicitations and sending advertisements.
* @param fd the socket's {@link FileDescriptor}. * @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 TetheringThreadExecutor mExecutor;
private final TetheringNotificationUpdater mNotificationUpdater; private final TetheringNotificationUpdater mNotificationUpdater;
private final UserManager mUserManager; private final UserManager mUserManager;
private final BpfCoordinator mBpfCoordinator;
private final PrivateAddressCoordinator mPrivateAddressCoordinator; private final PrivateAddressCoordinator mPrivateAddressCoordinator;
private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID; private int mActiveDataSubId = INVALID_SUBSCRIPTION_ID;
// All the usage of mTetheringEventCallback should run in the same thread. // All the usage of mTetheringEventCallback should run in the same thread.
@@ -284,6 +285,8 @@ public class Tethering {
mUpstreamNetworkMonitor = mDeps.getUpstreamNetworkMonitor(mContext, mTetherMasterSM, mLog, mUpstreamNetworkMonitor = mDeps.getUpstreamNetworkMonitor(mContext, mTetherMasterSM, mLog,
TetherMasterSM.EVENT_UPSTREAM_CALLBACK); TetherMasterSM.EVENT_UPSTREAM_CALLBACK);
mForwardedDownstreams = new LinkedHashSet<>(); mForwardedDownstreams = new LinkedHashSet<>();
mBpfCoordinator = mDeps.getBpfCoordinator(
mHandler, mNetd, mLog, new BpfCoordinator.Dependencies());
IntentFilter filter = new IntentFilter(); IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_CARRIER_CONFIG_CHANGED); filter.addAction(ACTION_CARRIER_CONFIG_CHANGED);
@@ -1704,6 +1707,9 @@ public class Tethering {
chooseUpstreamType(true); chooseUpstreamType(true);
mTryCell = false; mTryCell = false;
} }
// TODO: Check the upstream interface if it is managed by BPF offload.
mBpfCoordinator.start();
} }
@Override @Override
@@ -1716,6 +1722,7 @@ public class Tethering {
mTetherUpstream = null; mTetherUpstream = null;
reportUpstreamChanged(null); reportUpstreamChanged(null);
} }
mBpfCoordinator.stop();
} }
private boolean updateUpstreamWanted() { private boolean updateUpstreamWanted() {
@@ -2341,7 +2348,7 @@ public class Tethering {
mLog.log("adding TetheringInterfaceStateMachine for: " + iface); mLog.log("adding TetheringInterfaceStateMachine for: " + iface);
final TetherState tetherState = new TetherState( final TetherState tetherState = new TetherState(
new IpServer(iface, mLooper, interfaceType, mLog, mNetd, new IpServer(iface, mLooper, interfaceType, mLog, mNetd, mBpfCoordinator,
makeControlCallback(), mConfig.enableLegacyDhcpServer, makeControlCallback(), mConfig.enableLegacyDhcpServer,
mConfig.enableBpfOffload, mPrivateAddressCoordinator, mConfig.enableBpfOffload, mPrivateAddressCoordinator,
mDeps.getIpServerDependencies())); mDeps.getIpServerDependencies()));

View File

@@ -40,6 +40,17 @@ import java.util.ArrayList;
* @hide * @hide
*/ */
public abstract class TetheringDependencies { 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. * 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.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4; import androidx.test.runner.AndroidJUnit4;
import com.android.networkstack.tethering.BpfCoordinator;
import com.android.networkstack.tethering.PrivateAddressCoordinator; import com.android.networkstack.tethering.PrivateAddressCoordinator;
import org.junit.Before; import org.junit.Before;
@@ -126,6 +127,7 @@ public class IpServerTest {
private final IpPrefix mBluetoothPrefix = new IpPrefix("192.168.44.0/24"); private final IpPrefix mBluetoothPrefix = new IpPrefix("192.168.44.0/24");
@Mock private INetd mNetd; @Mock private INetd mNetd;
@Mock private BpfCoordinator mBpfCoordinator;
@Mock private IpServer.Callback mCallback; @Mock private IpServer.Callback mCallback;
@Mock private SharedLog mSharedLog; @Mock private SharedLog mSharedLog;
@Mock private IDhcpServer mDhcpServer; @Mock private IDhcpServer mDhcpServer;
@@ -179,7 +181,7 @@ public class IpServerTest {
neighborCaptor.capture()); neighborCaptor.capture());
mIpServer = new IpServer( mIpServer = new IpServer(
IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog, mNetd, IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog, mNetd, mBpfCoordinator,
mCallback, usingLegacyDhcp, usingBpfOffload, mAddressCoordinator, mDependencies); mCallback, usingLegacyDhcp, usingBpfOffload, mAddressCoordinator, mDependencies);
mIpServer.start(); mIpServer.start();
mNeighborEventConsumer = neighborCaptor.getValue(); mNeighborEventConsumer = neighborCaptor.getValue();
@@ -222,8 +224,8 @@ public class IpServerTest {
when(mDependencies.getIpNeighborMonitor(any(), any(), any())) when(mDependencies.getIpNeighborMonitor(any(), any(), any()))
.thenReturn(mIpNeighborMonitor); .thenReturn(mIpNeighborMonitor);
mIpServer = new IpServer(IFACE_NAME, mLooper.getLooper(), TETHERING_BLUETOOTH, mSharedLog, mIpServer = new IpServer(IFACE_NAME, mLooper.getLooper(), TETHERING_BLUETOOTH, mSharedLog,
mNetd, mCallback, false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD, mNetd, mBpfCoordinator, mCallback, false /* usingLegacyDhcp */,
mAddressCoordinator, mDependencies); DEFAULT_USING_BPF_OFFLOAD, mAddressCoordinator, mDependencies);
mIpServer.start(); mIpServer.start();
mLooper.dispatchAll(); mLooper.dispatchAll();
verify(mCallback).updateInterfaceState( 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 ConnectivityManager mCm;
@Mock private EthernetManager mEm; @Mock private EthernetManager mEm;
@Mock private TetheringNotificationUpdater mNotificationUpdater; @Mock private TetheringNotificationUpdater mNotificationUpdater;
@Mock private BpfCoordinator mBpfCoordinator;
private final MockIpServerDependencies mIpServerDependencies = private final MockIpServerDependencies mIpServerDependencies =
spy(new MockIpServerDependencies()); spy(new MockIpServerDependencies());
@@ -336,6 +337,12 @@ public class TetheringTest {
mIpv6CoordinatorNotifyList = null; mIpv6CoordinatorNotifyList = null;
} }
@Override
public BpfCoordinator getBpfCoordinator(Handler handler, INetd netd,
SharedLog log, BpfCoordinator.Dependencies deps) {
return mBpfCoordinator;
}
@Override @Override
public OffloadHardwareInterface getOffloadHardwareInterface(Handler h, SharedLog log) { public OffloadHardwareInterface getOffloadHardwareInterface(Handler h, SharedLog log) {
return mOffloadHardwareInterface; return mOffloadHardwareInterface;