From c8dbdf35debf616ca2ed13345c3bf7a3feace6da Mon Sep 17 00:00:00 2001 From: Varun Anand Date: Sun, 17 Feb 2019 23:43:25 -0800 Subject: [PATCH 1/2] Take all VPN underlying networks into account when migrating traffic for VPN uid. Bug: 113122541 Bug: 120145746 Test: atest FrameworksNetTests Test: Manually verified on device that stats from VPN UID are moved appropriately based on its declared underlying network set. Test: vogar --mode app_process --benchmark NetworkStatsBenchmark.java Change-Id: I9d8d0cc58d18002c1c96f8ddff780ef8dc452d21 --- core/java/android/net/NetworkStats.java | 330 +++++++++++------- .../server/net/NetworkStatsRecorder.java | 6 +- 2 files changed, 214 insertions(+), 122 deletions(-) diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java index 9cf582bef1..9e79606a24 100644 --- a/core/java/android/net/NetworkStats.java +++ b/core/java/android/net/NetworkStats.java @@ -18,6 +18,7 @@ package android.net; import static android.os.Process.CLAT_UID; +import android.annotation.NonNull; import android.annotation.UnsupportedAppUsage; import android.os.Parcel; import android.os.Parcelable; @@ -1175,133 +1176,217 @@ public class NetworkStats implements Parcelable { /** * VPN accounting. Move some VPN's underlying traffic to other UIDs that use tun0 iface. * - * This method should only be called on delta NetworkStats. Do not call this method on a - * snapshot {@link NetworkStats} object because the tunUid and/or the underlyingIface may - * change over time. + *

This method should only be called on delta NetworkStats. Do not call this method on a + * snapshot {@link NetworkStats} object because the tunUid and/or the underlyingIface may change + * over time. * - * This method performs adjustments for one active VPN package and one VPN iface at a time. - * - * It is possible for the VPN software to use multiple underlying networks. This method - * only migrates traffic for the primary underlying network. + *

This method performs adjustments for one active VPN package and one VPN iface at a time. * * @param tunUid uid of the VPN application * @param tunIface iface of the vpn tunnel - * @param underlyingIface the primary underlying network iface used by the VPN application - * @return true if it successfully adjusts the accounting for VPN, false otherwise + * @param underlyingIfaces underlying network ifaces used by the VPN application */ - public boolean migrateTun(int tunUid, String tunIface, String underlyingIface) { - Entry tunIfaceTotal = new Entry(); - Entry underlyingIfaceTotal = new Entry(); + public void migrateTun(int tunUid, @NonNull String tunIface, + @NonNull String[] underlyingIfaces) { + // Combined usage by all apps using VPN. + final Entry tunIfaceTotal = new Entry(); + // Usage by VPN, grouped by its {@code underlyingIfaces}. + final Entry[] perInterfaceTotal = new Entry[underlyingIfaces.length]; + // Usage by VPN, summed across all its {@code underlyingIfaces}. + final Entry underlyingIfacesTotal = new Entry(); - tunAdjustmentInit(tunUid, tunIface, underlyingIface, tunIfaceTotal, underlyingIfaceTotal); + for (int i = 0; i < perInterfaceTotal.length; i++) { + perInterfaceTotal[i] = new Entry(); + } - // If tunIface < underlyingIface, it leaves the overhead traffic in the VPN app. - // If tunIface > underlyingIface, the VPN app doesn't get credit for data compression. + tunAdjustmentInit(tunUid, tunIface, underlyingIfaces, tunIfaceTotal, perInterfaceTotal, + underlyingIfacesTotal); + + // If tunIface < underlyingIfacesTotal, it leaves the overhead traffic in the VPN app. + // If tunIface > underlyingIfacesTotal, the VPN app doesn't get credit for data compression. // Negative stats should be avoided. - Entry pool = tunGetPool(tunIfaceTotal, underlyingIfaceTotal); - if (pool.isEmpty()) { - return true; - } - Entry moved = - addTrafficToApplications(tunUid, tunIface, underlyingIface, tunIfaceTotal, pool); - deductTrafficFromVpnApp(tunUid, underlyingIface, moved); - - if (!moved.isEmpty()) { - Slog.wtf(TAG, "Failed to deduct underlying network traffic from VPN package. Moved=" - + moved); - return false; - } - return true; + final Entry[] moved = + addTrafficToApplications(tunUid, tunIface, underlyingIfaces, tunIfaceTotal, + perInterfaceTotal, underlyingIfacesTotal); + deductTrafficFromVpnApp(tunUid, underlyingIfaces, moved); } /** * Initializes the data used by the migrateTun() method. * - * This is the first pass iteration which does the following work: - * (1) Adds up all the traffic through the tunUid's underlyingIface - * (both foreground and background). - * (2) Adds up all the traffic through tun0 excluding traffic from the vpn app itself. + *

This is the first pass iteration which does the following work: + * + *

+ * + * @param tunUid uid of the VPN application + * @param tunIface iface of the vpn tunnel + * @param underlyingIfaces underlying network ifaces used by the VPN application + * @param tunIfaceTotal output parameter; combined data usage by all apps using VPN + * @param perInterfaceTotal output parameter; data usage by VPN app, grouped by its {@code + * underlyingIfaces} + * @param underlyingIfacesTotal output parameter; data usage by VPN, summed across all of its + * {@code underlyingIfaces} */ - private void tunAdjustmentInit(int tunUid, String tunIface, String underlyingIface, - Entry tunIfaceTotal, Entry underlyingIfaceTotal) { - Entry recycle = new Entry(); + private void tunAdjustmentInit(int tunUid, @NonNull String tunIface, + @NonNull String[] underlyingIfaces, @NonNull Entry tunIfaceTotal, + @NonNull Entry[] perInterfaceTotal, @NonNull Entry underlyingIfacesTotal) { + final Entry recycle = new Entry(); for (int i = 0; i < size; i++) { getValues(i, recycle); if (recycle.uid == UID_ALL) { throw new IllegalStateException( "Cannot adjust VPN accounting on an iface aggregated NetworkStats."); - } if (recycle.set == SET_DBG_VPN_IN || recycle.set == SET_DBG_VPN_OUT) { + } + if (recycle.set == SET_DBG_VPN_IN || recycle.set == SET_DBG_VPN_OUT) { throw new IllegalStateException( "Cannot adjust VPN accounting on a NetworkStats containing SET_DBG_VPN_*"); } - - if (recycle.uid == tunUid && recycle.tag == TAG_NONE - && Objects.equals(underlyingIface, recycle.iface)) { - underlyingIfaceTotal.add(recycle); + if (recycle.tag != TAG_NONE) { + // TODO(b/123666283): Take all tags for tunUid into account. + continue; } - if (recycle.uid != tunUid && recycle.tag == TAG_NONE - && Objects.equals(tunIface, recycle.iface)) { + if (recycle.uid == tunUid) { + // Add up traffic through tunUid's underlying interfaces. + for (int j = 0; j < underlyingIfaces.length; j++) { + if (Objects.equals(underlyingIfaces[j], recycle.iface)) { + perInterfaceTotal[j].add(recycle); + underlyingIfacesTotal.add(recycle); + break; + } + } + } else if (tunIface.equals(recycle.iface)) { // Add up all tunIface traffic excluding traffic from the vpn app itself. tunIfaceTotal.add(recycle); } } } - private static Entry tunGetPool(Entry tunIfaceTotal, Entry underlyingIfaceTotal) { - Entry pool = new Entry(); - pool.rxBytes = Math.min(tunIfaceTotal.rxBytes, underlyingIfaceTotal.rxBytes); - pool.rxPackets = Math.min(tunIfaceTotal.rxPackets, underlyingIfaceTotal.rxPackets); - pool.txBytes = Math.min(tunIfaceTotal.txBytes, underlyingIfaceTotal.txBytes); - pool.txPackets = Math.min(tunIfaceTotal.txPackets, underlyingIfaceTotal.txPackets); - pool.operations = Math.min(tunIfaceTotal.operations, underlyingIfaceTotal.operations); - return pool; - } + /** + * Distributes traffic across apps that are using given {@code tunIface}, and returns the total + * traffic that should be moved off of {@code tunUid} grouped by {@code underlyingIfaces}. + * + * @param tunUid uid of the VPN application + * @param tunIface iface of the vpn tunnel + * @param underlyingIfaces underlying network ifaces used by the VPN application + * @param tunIfaceTotal combined data usage across all apps using {@code tunIface} + * @param perInterfaceTotal data usage by VPN app, grouped by its {@code underlyingIfaces} + * @param underlyingIfacesTotal data usage by VPN, summed across all of its {@code + * underlyingIfaces} + */ + private Entry[] addTrafficToApplications(int tunUid, @NonNull String tunIface, + @NonNull String[] underlyingIfaces, @NonNull Entry tunIfaceTotal, + @NonNull Entry[] perInterfaceTotal, @NonNull Entry underlyingIfacesTotal) { + // Traffic that should be moved off of each underlying interface for tunUid (see + // deductTrafficFromVpnApp below). + final Entry[] moved = new Entry[underlyingIfaces.length]; + for (int i = 0; i < underlyingIfaces.length; i++) { + moved[i] = new Entry(); + } - private Entry addTrafficToApplications(int tunUid, String tunIface, String underlyingIface, - Entry tunIfaceTotal, Entry pool) { - Entry moved = new Entry(); - Entry tmpEntry = new Entry(); - tmpEntry.iface = underlyingIface; + final Entry tmpEntry = new Entry(); for (int i = 0; i < size; i++) { - // the vpn app is excluded from the redistribution but all moved traffic will be - // deducted from the vpn app (see deductTrafficFromVpnApp below). - if (Objects.equals(iface[i], tunIface) && uid[i] != tunUid) { - if (tunIfaceTotal.rxBytes > 0) { - tmpEntry.rxBytes = pool.rxBytes * rxBytes[i] / tunIfaceTotal.rxBytes; - } else { - tmpEntry.rxBytes = 0; - } - if (tunIfaceTotal.rxPackets > 0) { - tmpEntry.rxPackets = pool.rxPackets * rxPackets[i] / tunIfaceTotal.rxPackets; - } else { - tmpEntry.rxPackets = 0; - } - if (tunIfaceTotal.txBytes > 0) { - tmpEntry.txBytes = pool.txBytes * txBytes[i] / tunIfaceTotal.txBytes; - } else { - tmpEntry.txBytes = 0; - } - if (tunIfaceTotal.txPackets > 0) { - tmpEntry.txPackets = pool.txPackets * txPackets[i] / tunIfaceTotal.txPackets; - } else { - tmpEntry.txPackets = 0; - } - if (tunIfaceTotal.operations > 0) { - tmpEntry.operations = - pool.operations * operations[i] / tunIfaceTotal.operations; - } else { - tmpEntry.operations = 0; - } - tmpEntry.uid = uid[i]; - tmpEntry.tag = tag[i]; + if (!Objects.equals(iface[i], tunIface)) { + // Consider only entries that go onto the VPN interface. + continue; + } + if (uid[i] == tunUid) { + // Exclude VPN app from the redistribution, as it can choose to create packet + // streams by writing to itself. + continue; + } + tmpEntry.uid = uid[i]; + tmpEntry.tag = tag[i]; + tmpEntry.metered = metered[i]; + tmpEntry.roaming = roaming[i]; + tmpEntry.defaultNetwork = defaultNetwork[i]; + + // In a first pass, compute each UID's total share of data across all underlyingIfaces. + // This is computed on the basis of the share of each UID's usage over tunIface. + // TODO: Consider refactoring first pass into a separate helper method. + long totalRxBytes = 0; + if (tunIfaceTotal.rxBytes > 0) { + // Note - The multiplication below should not overflow since NetworkStatsService + // processes this every time device has transmitted/received amount equivalent to + // global threshold alert (~ 2MB) across all interfaces. + final long rxBytesAcrossUnderlyingIfaces = + underlyingIfacesTotal.rxBytes * rxBytes[i] / tunIfaceTotal.rxBytes; + // app must not be blamed for more than it consumed on tunIface + totalRxBytes = Math.min(rxBytes[i], rxBytesAcrossUnderlyingIfaces); + } + long totalRxPackets = 0; + if (tunIfaceTotal.rxPackets > 0) { + final long rxPacketsAcrossUnderlyingIfaces = + underlyingIfacesTotal.rxPackets * rxPackets[i] / tunIfaceTotal.rxPackets; + totalRxPackets = Math.min(rxPackets[i], rxPacketsAcrossUnderlyingIfaces); + } + long totalTxBytes = 0; + if (tunIfaceTotal.txBytes > 0) { + final long txBytesAcrossUnderlyingIfaces = + underlyingIfacesTotal.txBytes * txBytes[i] / tunIfaceTotal.txBytes; + totalTxBytes = Math.min(txBytes[i], txBytesAcrossUnderlyingIfaces); + } + long totalTxPackets = 0; + if (tunIfaceTotal.txPackets > 0) { + final long txPacketsAcrossUnderlyingIfaces = + underlyingIfacesTotal.txPackets * txPackets[i] / tunIfaceTotal.txPackets; + totalTxPackets = Math.min(txPackets[i], txPacketsAcrossUnderlyingIfaces); + } + long totalOperations = 0; + if (tunIfaceTotal.operations > 0) { + final long operationsAcrossUnderlyingIfaces = + underlyingIfacesTotal.operations * operations[i] / tunIfaceTotal.operations; + totalOperations = Math.min(operations[i], operationsAcrossUnderlyingIfaces); + } + // In a second pass, distribute these values across interfaces in the proportion that + // each interface represents of the total traffic of the underlying interfaces. + for (int j = 0; j < underlyingIfaces.length; j++) { + tmpEntry.iface = underlyingIfaces[j]; + tmpEntry.rxBytes = 0; + // Reset 'set' to correct value since it gets updated when adding debug info below. tmpEntry.set = set[i]; - tmpEntry.metered = metered[i]; - tmpEntry.roaming = roaming[i]; - tmpEntry.defaultNetwork = defaultNetwork[i]; + if (underlyingIfacesTotal.rxBytes > 0) { + tmpEntry.rxBytes = + totalRxBytes + * perInterfaceTotal[j].rxBytes + / underlyingIfacesTotal.rxBytes; + } + tmpEntry.rxPackets = 0; + if (underlyingIfacesTotal.rxPackets > 0) { + tmpEntry.rxPackets = + totalRxPackets + * perInterfaceTotal[j].rxPackets + / underlyingIfacesTotal.rxPackets; + } + tmpEntry.txBytes = 0; + if (underlyingIfacesTotal.txBytes > 0) { + tmpEntry.txBytes = + totalTxBytes + * perInterfaceTotal[j].txBytes + / underlyingIfacesTotal.txBytes; + } + tmpEntry.txPackets = 0; + if (underlyingIfacesTotal.txPackets > 0) { + tmpEntry.txPackets = + totalTxPackets + * perInterfaceTotal[j].txPackets + / underlyingIfacesTotal.txPackets; + } + tmpEntry.operations = 0; + if (underlyingIfacesTotal.operations > 0) { + tmpEntry.operations = + totalOperations + * perInterfaceTotal[j].operations + / underlyingIfacesTotal.operations; + } + combineValues(tmpEntry); if (tag[i] == TAG_NONE) { - moved.add(tmpEntry); + moved[j].add(tmpEntry); // Add debug info tmpEntry.set = SET_DBG_VPN_IN; combineValues(tmpEntry); @@ -1311,38 +1396,45 @@ public class NetworkStats implements Parcelable { return moved; } - private void deductTrafficFromVpnApp(int tunUid, String underlyingIface, Entry moved) { - // Add debug info - moved.uid = tunUid; - moved.set = SET_DBG_VPN_OUT; - moved.tag = TAG_NONE; - moved.iface = underlyingIface; - moved.metered = METERED_ALL; - moved.roaming = ROAMING_ALL; - moved.defaultNetwork = DEFAULT_NETWORK_ALL; - combineValues(moved); + private void deductTrafficFromVpnApp( + int tunUid, + @NonNull String[] underlyingIfaces, + @NonNull Entry[] moved) { + for (int i = 0; i < underlyingIfaces.length; i++) { + // Add debug info + moved[i].uid = tunUid; + moved[i].set = SET_DBG_VPN_OUT; + moved[i].tag = TAG_NONE; + moved[i].iface = underlyingIfaces[i]; + moved[i].metered = METERED_ALL; + moved[i].roaming = ROAMING_ALL; + moved[i].defaultNetwork = DEFAULT_NETWORK_ALL; + combineValues(moved[i]); - // Caveat: if the vpn software uses tag, the total tagged traffic may be greater than - // the TAG_NONE traffic. - // - // Relies on the fact that the underlying traffic only has state ROAMING_NO and METERED_NO, - // which should be the case as it comes directly from the /proc file. We only blend in the - // roaming data after applying these adjustments, by checking the NetworkIdentity of the - // underlying iface. - int idxVpnBackground = findIndex(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, - METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO); - if (idxVpnBackground != -1) { - tunSubtract(idxVpnBackground, this, moved); - } + // Caveat: if the vpn software uses tag, the total tagged traffic may be greater than + // the TAG_NONE traffic. + // + // Relies on the fact that the underlying traffic only has state ROAMING_NO and + // METERED_NO, which should be the case as it comes directly from the /proc file. + // We only blend in the roaming data after applying these adjustments, by checking the + // NetworkIdentity of the underlying iface. + final int idxVpnBackground = findIndex(underlyingIfaces[i], tunUid, SET_DEFAULT, + TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO); + if (idxVpnBackground != -1) { + // Note - tunSubtract also updates moved[i]; whatever traffic that's left is removed + // from foreground usage. + tunSubtract(idxVpnBackground, this, moved[i]); + } - int idxVpnForeground = findIndex(underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE, - METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO); - if (idxVpnForeground != -1) { - tunSubtract(idxVpnForeground, this, moved); + final int idxVpnForeground = findIndex(underlyingIfaces[i], tunUid, SET_FOREGROUND, + TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO); + if (idxVpnForeground != -1) { + tunSubtract(idxVpnForeground, this, moved[i]); + } } } - private static void tunSubtract(int i, NetworkStats left, Entry right) { + private static void tunSubtract(int i, @NonNull NetworkStats left, @NonNull Entry right) { long rxBytes = Math.min(left.rxBytes[i], right.rxBytes); left.rxBytes[i] -= rxBytes; right.rxBytes -= rxBytes; diff --git a/services/core/java/com/android/server/net/NetworkStatsRecorder.java b/services/core/java/com/android/server/net/NetworkStatsRecorder.java index a2e7e0cae9..bdff50053f 100644 --- a/services/core/java/com/android/server/net/NetworkStatsRecorder.java +++ b/services/core/java/com/android/server/net/NetworkStatsRecorder.java @@ -41,10 +41,10 @@ import com.android.internal.net.VpnInfo; import com.android.internal.util.FileRotator; import com.android.internal.util.IndentingPrintWriter; -import libcore.io.IoUtils; - import com.google.android.collect.Sets; +import libcore.io.IoUtils; + import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.File; @@ -234,7 +234,7 @@ public class NetworkStatsRecorder { if (vpnArray != null) { for (VpnInfo info : vpnArray) { - delta.migrateTun(info.ownerUid, info.vpnIface, info.primaryUnderlyingIface); + delta.migrateTun(info.ownerUid, info.vpnIface, info.underlyingIfaces); } } From 720133f79d5b9ca9a87c1371695afb7b381b4042 Mon Sep 17 00:00:00 2001 From: Varun Anand Date: Fri, 18 Jan 2019 19:22:48 -0800 Subject: [PATCH 2/2] NetworkStatsService: Fix getDetailedUidStats to take VPNs into account. This API is similar to one provided by NetworkStatsFactory with the difference that NSS also migrates traffic from VPN UID to other apps. Since traffic can only be migrated over NetworkStats delta, NSS therefore maintains NetworkStats snapshot across all UIDs/ifaces/tags. This snapshot gets updated whenever NSS records a new snapshot (based on various hooks such as VPN updating its underlying networks, network getting lost, etc.), or getDetailedUidStats API is invoked by one of its callers. Bug: 113122541 Bug: 120145746 Test: atest FrameworksNetTests Test: manually verified that battery stats are migrating traffic off of TUN (after patching above CL where we point BatteryStats to use this API). Change-Id: Ib0f0c2d4d41ee1d7a027ea9da457baaf198d649e --- core/java/android/net/NetworkStats.java | 29 +++++--- .../internal/net/NetworkStatsFactory.java | 5 ++ .../server/net/NetworkStatsService.java | 70 ++++++++++++++++++- 3 files changed, 94 insertions(+), 10 deletions(-) diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java index 9e79606a24..27e0414961 100644 --- a/core/java/android/net/NetworkStats.java +++ b/core/java/android/net/NetworkStats.java @@ -34,6 +34,7 @@ import libcore.util.EmptyArray; import java.io.CharArrayWriter; import java.io.PrintWriter; import java.util.Arrays; +import java.util.function.Predicate; import java.util.HashSet; import java.util.Map; import java.util.Objects; @@ -994,23 +995,33 @@ public class NetworkStats implements Parcelable { if (limitUid == UID_ALL && limitTag == TAG_ALL && limitIfaces == INTERFACES_ALL) { return; } + filter(e -> (limitUid == UID_ALL || limitUid == e.uid) + && (limitTag == TAG_ALL || limitTag == e.tag) + && (limitIfaces == INTERFACES_ALL + || ArrayUtils.contains(limitIfaces, e.iface))); + } + /** + * Only keep entries with {@link #set} value less than {@link #SET_DEBUG_START}. + * + *

This mutates the original structure in place. + */ + public void filterDebugEntries() { + filter(e -> e.set < SET_DEBUG_START); + } + + private void filter(Predicate predicate) { Entry entry = new Entry(); int nextOutputEntry = 0; for (int i = 0; i < size; i++) { entry = getValues(i, entry); - final boolean matches = - (limitUid == UID_ALL || limitUid == entry.uid) - && (limitTag == TAG_ALL || limitTag == entry.tag) - && (limitIfaces == INTERFACES_ALL - || ArrayUtils.contains(limitIfaces, entry.iface)); - - if (matches) { - setValues(nextOutputEntry, entry); + if (predicate.test(entry)) { + if (nextOutputEntry != i) { + setValues(nextOutputEntry, entry); + } nextOutputEntry++; } } - size = nextOutputEntry; } diff --git a/core/java/com/android/internal/net/NetworkStatsFactory.java b/core/java/com/android/internal/net/NetworkStatsFactory.java index f8483461c2..1f3b4c4121 100644 --- a/core/java/com/android/internal/net/NetworkStatsFactory.java +++ b/core/java/com/android/internal/net/NetworkStatsFactory.java @@ -256,6 +256,11 @@ public class NetworkStatsFactory { return stats; } + /** + * @deprecated Use NetworkStatsService#getDetailedUidStats which also accounts for + * VPN traffic + */ + @Deprecated public NetworkStats readNetworkStatsDetail() throws IOException { return readNetworkStatsDetail(UID_ALL, null, TAG_ALL, null); } diff --git a/services/core/java/com/android/server/net/NetworkStatsService.java b/services/core/java/com/android/server/net/NetworkStatsService.java index 8fa435cd93..4c07678173 100644 --- a/services/core/java/com/android/server/net/NetworkStatsService.java +++ b/services/core/java/com/android/server/net/NetworkStatsService.java @@ -292,6 +292,22 @@ public class NetworkStatsService extends INetworkStatsService.Stub { /** Data layer operation counters for splicing into other structures. */ private NetworkStats mUidOperations = new NetworkStats(0L, 10); + /** + * Snapshot containing most recent network stats for all UIDs across all interfaces and tags + * since boot. + * + *

Maintains migrated VPN stats which are result of performing TUN migration on {@link + * #mLastUidDetailSnapshot}. + */ + @GuardedBy("mStatsLock") + private NetworkStats mTunAdjustedStats; + /** + * Used by {@link #mTunAdjustedStats} to migrate VPN traffic over delta between this snapshot + * and latest snapshot. + */ + @GuardedBy("mStatsLock") + private NetworkStats mLastUidDetailSnapshot; + /** Must be set in factory by calling #setHandler. */ private Handler mHandler; private Handler.Callback mHandlerCallback; @@ -805,15 +821,39 @@ public class NetworkStatsService extends INetworkStatsService.Stub { @Override public NetworkStats getDetailedUidStats(String[] requiredIfaces) { try { + // Get the latest snapshot from NetworkStatsFactory. + // TODO: Querying for INTERFACES_ALL may incur performance penalty. Consider restricting + // this to limited set of ifaces. + NetworkStats uidDetailStats = getNetworkStatsUidDetail(INTERFACES_ALL); + + // Migrate traffic from VPN UID over delta and update mTunAdjustedStats. + NetworkStats result; + synchronized (mStatsLock) { + migrateTunTraffic(uidDetailStats, mVpnInfos); + result = mTunAdjustedStats.clone(); + } + + // Apply filter based on ifacesToQuery. final String[] ifacesToQuery = NetworkStatsFactory.augmentWithStackedInterfaces(requiredIfaces); - return getNetworkStatsUidDetail(ifacesToQuery); + result.filter(UID_ALL, ifacesToQuery, TAG_ALL); + return result; } catch (RemoteException e) { Log.wtf(TAG, "Error compiling UID stats", e); return new NetworkStats(0L, 0); } } + @VisibleForTesting + NetworkStats getTunAdjustedStats() { + synchronized (mStatsLock) { + if (mTunAdjustedStats == null) { + return null; + } + return mTunAdjustedStats.clone(); + } + } + @Override public String[] getMobileIfaces() { return mMobileIfaces; @@ -1288,6 +1328,34 @@ public class NetworkStatsService extends INetworkStatsService.Stub { // a race condition between the service handler thread and the observer's mStatsObservers.updateStats(xtSnapshot, uidSnapshot, new ArrayMap<>(mActiveIfaces), new ArrayMap<>(mActiveUidIfaces), vpnArray, currentTime); + + migrateTunTraffic(uidSnapshot, vpnArray); + } + + /** + * Updates {@link #mTunAdjustedStats} with the delta containing traffic migrated off of VPNs. + */ + @GuardedBy("mStatsLock") + private void migrateTunTraffic(NetworkStats uidDetailStats, VpnInfo[] vpnInfoArray) { + if (mTunAdjustedStats == null) { + // Either device booted or system server restarted, hence traffic cannot be migrated + // correctly without knowing the past state of VPN's underlying networks. + mTunAdjustedStats = uidDetailStats; + mLastUidDetailSnapshot = uidDetailStats; + return; + } + // Migrate delta traffic from VPN to other apps. + NetworkStats delta = uidDetailStats.subtract(mLastUidDetailSnapshot); + for (VpnInfo info : vpnInfoArray) { + delta.migrateTun(info.ownerUid, info.vpnIface, info.underlyingIfaces); + } + // Filter out debug entries as that may lead to over counting. + delta.filterDebugEntries(); + // Update #mTunAdjustedStats with migrated delta. + mTunAdjustedStats.combineAllValues(delta); + mTunAdjustedStats.setElapsedRealtime(uidDetailStats.getElapsedRealtime()); + // Update last snapshot. + mLastUidDetailSnapshot = uidDetailStats; } /**