[BOT.2] Create a coordinator and stats provider to provide tether stats

Make BPF tethering offload coordinator, BpfCoordinator,
registers a network stats provider, BpfTetherStatsProvider, and
provide the tethering stats from the BPF map.

Bug: 150736748
Test: new test BpfCoordinatorTest

Change-Id: I22e71f87b67668f7e733e4f215d93bf5b2c9380d
This commit is contained in:
Hungming Chen
2020-03-12 21:24:01 +08:00
parent d0216998a4
commit 68f1c2a63f
6 changed files with 325 additions and 5 deletions

View File

@@ -65,6 +65,7 @@ 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.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;
@@ -314,11 +317,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 +759,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

@@ -0,0 +1,280 @@
/*
* 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.NetworkStats;
import android.net.NetworkStats.Entry;
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;
/**
* 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();
// TODO: Make it customizable.
private static final int DEFAULT_PERFORM_POLL_INTERVAL_MS = 5000;
private 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();
};
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);
}
}
/**
* 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.
*/
private 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
}
private 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

@@ -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;