diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java index 80eb042795..89e06da2de 100644 --- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java +++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java @@ -87,6 +87,7 @@ import com.android.net.module.util.netlink.NetlinkConstants; import com.android.net.module.util.netlink.NetlinkUtils; import com.android.networkstack.tethering.apishim.common.BpfCoordinatorShim; import com.android.networkstack.tethering.util.TetheringUtils.ForwardedStats; +import com.android.server.ConnectivityStatsLog; import java.io.IOException; import java.net.Inet4Address; @@ -151,6 +152,13 @@ public class BpfCoordinator { @VisibleForTesting static final int CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS = 60_000; + // The interval is set to 5 minutes to strike a balance between minimizing + // the amount of metrics data uploaded and providing sufficient resolution + // to track changes in forwarding rules. This choice considers the minimum + // push metrics sampling interval of 5 minutes and the 3-minute timeout + // for forwarding rules. + @VisibleForTesting + static final int CONNTRACK_METRICS_UPDATE_INTERVAL_MS = 300_000; @VisibleForTesting static final int NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED = 432_000; @VisibleForTesting @@ -319,6 +327,12 @@ public class BpfCoordinator { private final boolean mSupportActiveSessionsMetrics; + // Runnable that used by scheduling next refreshing of conntrack metrics sampling. + private final Runnable mScheduledConntrackMetricsSampling = () -> { + uploadConntrackMetricsSample(); + scheduleConntrackMetricsSampling(); + }; + // TODO: add BpfMap retrieving function. @VisibleForTesting public abstract static class Dependencies { @@ -481,6 +495,12 @@ public class BpfCoordinator { } } + /** Send a TetheringActiveSessionsReported event. */ + public void sendTetheringActiveSessionsReported(int lastMaxSessionCount) { + ConnectivityStatsLog.write(ConnectivityStatsLog.TETHERING_ACTIVE_SESSIONS_REPORTED, + lastMaxSessionCount); + } + /** * @see DeviceConfigUtils#isTetheringFeatureEnabled */ @@ -530,38 +550,48 @@ public class BpfCoordinator { } /** - * Start BPF tethering offload stats and conntrack timeout polling. + * Start BPF tethering offload stats and conntrack polling. * Note that this can be only called on handler thread. */ - private void startStatsAndConntrackTimeoutPolling() { + private void startStatsAndConntrackPolling() { schedulePollingStats(); scheduleConntrackTimeoutUpdate(); + if (mSupportActiveSessionsMetrics) { + scheduleConntrackMetricsSampling(); + } mLog.i("Polling started."); } /** - * Stop BPF tethering offload stats and conntrack timeout polling. + * Stop BPF tethering offload stats and conntrack polling. * The data limit cleanup and the tether stats maps cleanup are not implemented here. * These cleanups rely on all IpServers calling #removeIpv6DownstreamRule. After the * last rule is removed from the upstream, #removeIpv6DownstreamRule does the cleanup * functionality. * Note that this can be only called on handler thread. */ - private void stopStatsAndConntrackTimeoutPolling() { + private void stopStatsAndConntrackPolling() { // Stop scheduled polling conntrack timeout. if (mHandler.hasCallbacks(mScheduledConntrackTimeoutUpdate)) { mHandler.removeCallbacks(mScheduledConntrackTimeoutUpdate); } - // Clear counters in case there is any counter unsync problem + // Stop scheduled polling conntrack metrics sampling and + // clear counters in case there is any counter unsync problem // previously due to possible bpf failures. // Normally this won't happen because all clients are cleared before // reaching here. See IpServer.BaseServingState#exit(). if (mSupportActiveSessionsMetrics) { + if (mHandler.hasCallbacks(mScheduledConntrackMetricsSampling)) { + mHandler.removeCallbacks(mScheduledConntrackMetricsSampling); + } final int currentCount = mBpfConntrackEventConsumer.getCurrentConnectionCount(); if (currentCount != 0) { Log.wtf(TAG, "Unexpected CurrentConnectionCount: " + currentCount); } + // Avoid sending metrics when tethering is about to close. + // This leads to a missing final sample before disconnect + // but avoids possibly duplicating the last metric in the upload. mBpfConntrackEventConsumer.clearConnectionCounters(); } // Stop scheduled polling stats and poll the latest stats from BPF maps. @@ -897,7 +927,7 @@ public class BpfCoordinator { // Start monitoring and polling when the first IpServer is added. if (mServedIpServers.isEmpty()) { - startStatsAndConntrackTimeoutPolling(); + startStatsAndConntrackPolling(); startConntrackMonitoring(); mIpNeighborMonitor.start(); mLog.i("Neighbor monitoring started."); @@ -920,7 +950,7 @@ public class BpfCoordinator { // Stop monitoring and polling when the last IpServer is removed. if (mServedIpServers.isEmpty()) { - stopStatsAndConntrackTimeoutPolling(); + stopStatsAndConntrackPolling(); stopConntrackMonitoring(); mIpNeighborMonitor.stop(); mLog.i("Neighbor monitoring stopped."); @@ -2579,6 +2609,11 @@ public class BpfCoordinator { }); } + private void uploadConntrackMetricsSample() { + mDeps.sendTetheringActiveSessionsReported( + mBpfConntrackEventConsumer.getLastMaxConnectionAndResetToCurrent()); + } + private void schedulePollingStats() { if (mHandler.hasCallbacks(mScheduledPollingStats)) { mHandler.removeCallbacks(mScheduledPollingStats); @@ -2596,6 +2631,15 @@ public class BpfCoordinator { CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS); } + private void scheduleConntrackMetricsSampling() { + if (mHandler.hasCallbacks(mScheduledConntrackMetricsSampling)) { + mHandler.removeCallbacks(mScheduledConntrackMetricsSampling); + } + + mHandler.postDelayed(mScheduledConntrackMetricsSampling, + CONNTRACK_METRICS_UPDATE_INTERVAL_MS); + } + // Return IPv6 downstream forwarding rule map. This is used for testing only. // Note that this can be only called on handler thread. @NonNull 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 dfa99367f6..5d22977858 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java @@ -48,6 +48,7 @@ import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWNEIGH; import static com.android.net.module.util.netlink.StructNdMsg.NUD_FAILED; import static com.android.net.module.util.netlink.StructNdMsg.NUD_REACHABLE; import static com.android.net.module.util.netlink.StructNdMsg.NUD_STALE; +import static com.android.networkstack.tethering.BpfCoordinator.CONNTRACK_METRICS_UPDATE_INTERVAL_MS; import static com.android.networkstack.tethering.BpfCoordinator.CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS; import static com.android.networkstack.tethering.BpfCoordinator.INVALID_MTU; import static com.android.networkstack.tethering.BpfCoordinator.NF_CONNTRACK_TCP_TIMEOUT_ESTABLISHED; @@ -572,6 +573,11 @@ public class BpfCoordinatorTest { return mBpfErrorMap; } + @Override + public void sendTetheringActiveSessionsReported(int lastMaxSessionCount) { + // No-op. + } + @Override public boolean isFeatureEnabled(Context context, String name) { return mFeatureFlags.getOrDefault(name, false); @@ -2166,6 +2172,70 @@ public class BpfCoordinatorTest { assertConsumerCountersEquals(0); } + @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS) + // BPF IPv4 forwarding only supports on S+. + @IgnoreUpTo(Build.VERSION_CODES.R) + @Test + public void testSendActiveSessionsReported_metricsEnabled() throws Exception { + doTestSendActiveSessionsReported(true); + } + + @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS, enabled = false) + @Test + public void testSendActiveSessionsReported_metricsDisabled() throws Exception { + doTestSendActiveSessionsReported(false); + } + + private void doTestSendActiveSessionsReported(final boolean supportActiveSessionsMetrics) + throws Exception { + final BpfCoordinator coordinator = makeBpfCoordinator(); + initBpfCoordinatorForRule4(coordinator); + resetNetdAndBpfMaps(); + assertConsumerCountersEquals(0); + + // Prepare the counter value. + for (int i = 0; i < 5; i++) { + mConsumer.accept(new TestConntrackEvent.Builder().setMsgType( + IPCTNL_MSG_CT_NEW).setProto(IPPROTO_TCP).setRemotePort(i).build()); + } + + // Then delete some 3 rules, 2 rules remaining. + // The max count is 5 while current rules count is 2. + for (int i = 0; i < 3; i++) { + mConsumer.accept(new TestConntrackEvent.Builder().setMsgType( + IPCTNL_MSG_CT_DELETE).setProto(IPPROTO_TCP).setRemotePort(i).build()); + } + + // Verify the method is not invoked when timer is not expired. + waitForIdle(); + verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt()); + + // Verify metrics will be sent upon timer expiry. + mTestLooper.moveTimeForward(CONNTRACK_METRICS_UPDATE_INTERVAL_MS); + waitForIdle(); + if (supportActiveSessionsMetrics) { + verify(mDeps).sendTetheringActiveSessionsReported(5); + } else { + verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt()); + } + + // Verify next uploaded metrics will reflect the decreased rules count. + mTestLooper.moveTimeForward(CONNTRACK_METRICS_UPDATE_INTERVAL_MS); + waitForIdle(); + if (supportActiveSessionsMetrics) { + verify(mDeps).sendTetheringActiveSessionsReported(2); + } else { + verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt()); + } + + // Verify no metrics uploaded if polling stopped. + clearInvocations(mDeps); + coordinator.removeIpServer(mIpServer); + mTestLooper.moveTimeForward(CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS); + waitForIdle(); + verify(mDeps, never()).sendTetheringActiveSessionsReported(anyInt()); + } + private void setElapsedRealtimeNanos(long nanoSec) { mElapsedRealtimeNanos = nanoSec; }