diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java index 6ccbab7ed8..288b06ee52 100644 --- a/core/java/android/net/NetworkStats.java +++ b/core/java/android/net/NetworkStats.java @@ -24,6 +24,7 @@ import android.compat.annotation.UnsupportedAppUsage; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; +import android.os.Process; import android.os.SystemClock; import android.util.SparseBooleanArray; @@ -1487,8 +1488,31 @@ public final class NetworkStats implements Parcelable { continue; } - if (recycle.uid == tunUid) { - // Add up traffic through tunUid's underlying interfaces. + if (tunUid == Process.SYSTEM_UID) { + // Kernel-based VPN or VCN, traffic sent by apps on the VPN/VCN network + // + // Since the data is not UID-accounted on underlying networks, just use VPN/VCN + // network usage as ground truth. Encrypted traffic on the underlying networks will + // never be processed here because encrypted traffic on the underlying interfaces + // is not present in UID stats, and this method is only called on UID stats. + if (tunIface.equals(recycle.iface)) { + tunIfaceTotal.add(recycle); + underlyingIfacesTotal.add(recycle); + + // In steady state, there should always be one network, but edge cases may + // result in the network being null (network lost), and thus no underlying + // ifaces is possible. + if (perInterfaceTotal.length > 0) { + // While platform VPNs and VCNs have exactly one underlying network, that + // network may have multiple interfaces (eg for 464xlat). This layer does + // not have the required information to identify which of the interfaces + // were used. Select "any" of the interfaces. Since overhead is already + // lost, this number is an approximation anyways. + perInterfaceTotal[0].add(recycle); + } + } + } else if (recycle.uid == tunUid) { + // VpnService VPN, traffic sent by the VPN app over underlying networks for (int j = 0; j < underlyingIfaces.size(); j++) { if (Objects.equals(underlyingIfaces.get(j), recycle.iface)) { perInterfaceTotal[j].add(recycle); @@ -1497,7 +1521,7 @@ public final class NetworkStats implements Parcelable { } } } else if (tunIface.equals(recycle.iface)) { - // Add up all tunIface traffic excluding traffic from the vpn app itself. + // VpnService VPN; traffic sent by apps on the VPN network tunIfaceTotal.add(recycle); } } @@ -1532,9 +1556,13 @@ public final class NetworkStats implements Parcelable { // Consider only entries that go onto the VPN interface. continue; } - if (uid[i] == tunUid) { + + if (uid[i] == tunUid && tunUid != Process.SYSTEM_UID) { // Exclude VPN app from the redistribution, as it can choose to create packet // streams by writing to itself. + // + // However, for platform VPNs, do not exclude the system's usage of the VPN network, + // since it is never local-only, and never double counted continue; } tmpEntry.uid = uid[i]; @@ -1641,6 +1669,12 @@ public final class NetworkStats implements Parcelable { int tunUid, @NonNull List underlyingIfaces, @NonNull Entry[] moved) { + if (tunUid == Process.SYSTEM_UID) { + // No traffic recorded on a per-UID basis for in-kernel VPN/VCNs over underlying + // networks; thus no traffic to deduct. + return; + } + for (int i = 0; i < underlyingIfaces.size(); i++) { moved[i].uid = tunUid; // Add debug info diff --git a/core/java/android/net/nsd/NsdManager.java b/core/java/android/net/nsd/NsdManager.java index 5a25cfc00c..ae8d010194 100644 --- a/core/java/android/net/nsd/NsdManager.java +++ b/core/java/android/net/nsd/NsdManager.java @@ -23,6 +23,9 @@ import static com.android.internal.util.Preconditions.checkStringNotEmpty; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.annotation.SystemService; +import android.app.compat.CompatChanges; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; import android.content.Context; import android.os.Handler; import android.os.HandlerThread; @@ -125,6 +128,24 @@ public final class NsdManager { private static final String TAG = NsdManager.class.getSimpleName(); private static final boolean DBG = false; + /** + * When enabled, apps targeting < Android 12 are considered legacy for + * the NSD native daemon. + * The platform will only keep the daemon running as long as there are + * any legacy apps connected. + * + * After Android 12, directly communicate with native daemon might not + * work since the native damon won't always stay alive. + * Use the NSD APIs from NsdManager as the replacement is recommended. + * An another alternative could be bundling your own mdns solutions instead of + * depending on the system mdns native daemon. + * + * @hide + */ + @ChangeId + @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.S) + public static final long RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS = 191844585L; + /** * Broadcast intent action to indicate whether network service discovery is * enabled or disabled. An extra {@link #EXTRA_NSD_STATE} provides the state @@ -202,6 +223,9 @@ public final class NsdManager { /** @hide */ public static final int DAEMON_CLEANUP = BASE + 21; + /** @hide */ + public static final int DAEMON_STARTUP = BASE + 22; + /** @hide */ public static final int ENABLE = BASE + 24; /** @hide */ @@ -232,6 +256,8 @@ public final class NsdManager { EVENT_NAMES.put(RESOLVE_SERVICE, "RESOLVE_SERVICE"); EVENT_NAMES.put(RESOLVE_SERVICE_FAILED, "RESOLVE_SERVICE_FAILED"); EVENT_NAMES.put(RESOLVE_SERVICE_SUCCEEDED, "RESOLVE_SERVICE_SUCCEEDED"); + EVENT_NAMES.put(DAEMON_CLEANUP, "DAEMON_CLEANUP"); + EVENT_NAMES.put(DAEMON_STARTUP, "DAEMON_STARTUP"); EVENT_NAMES.put(ENABLE, "ENABLE"); EVENT_NAMES.put(DISABLE, "DISABLE"); EVENT_NAMES.put(NATIVE_DAEMON_EVENT, "NATIVE_DAEMON_EVENT"); @@ -494,6 +520,12 @@ public final class NsdManager { } catch (InterruptedException e) { fatal("Interrupted wait at init"); } + if (CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS)) { + return; + } + // Only proactively start the daemon if the target SDK < S, otherwise the internal service + // would automatically start/stop the native daemon as needed. + mAsyncChannel.sendMessage(DAEMON_STARTUP); } private static void fatal(String msg) { diff --git a/services/core/java/com/android/server/NsdService.java b/services/core/java/com/android/server/NsdService.java index a481a6a5d3..c9608a5517 100644 --- a/services/core/java/com/android/server/NsdService.java +++ b/services/core/java/com/android/server/NsdService.java @@ -61,7 +61,7 @@ public class NsdService extends INsdManager.Stub { private static final String MDNS_TAG = "mDnsConnector"; private static final boolean DBG = true; - private static final long CLEANUP_DELAY_MS = 3000; + private static final long CLEANUP_DELAY_MS = 10000; private final Context mContext; private final NsdSettings mNsdSettings; @@ -82,6 +82,8 @@ public class NsdService extends INsdManager.Stub { private static final int INVALID_ID = 0; private int mUniqueId = 1; + // The count of the connected legacy clients. + private int mLegacyClientCount = 0; private class NsdStateMachine extends StateMachine { @@ -94,19 +96,27 @@ public class NsdService extends INsdManager.Stub { return NsdManager.nameOf(what); } - void maybeStartDaemon() { + private void maybeStartDaemon() { mDaemon.maybeStart(); maybeScheduleStop(); } - void maybeScheduleStop() { - if (!isAnyRequestActive()) { - cancelStop(); - sendMessageDelayed(NsdManager.DAEMON_CLEANUP, mCleanupDelayMs); + private boolean isAnyRequestActive() { + return mIdToClientInfoMap.size() != 0; + } + + private void scheduleStop() { + sendMessageDelayed(NsdManager.DAEMON_CLEANUP, mCleanupDelayMs); + } + private void maybeScheduleStop() { + // The native daemon should stay alive and can't be cleanup + // if any legacy client connected. + if (!isAnyRequestActive() && mLegacyClientCount == 0) { + scheduleStop(); } } - void cancelStop() { + private void cancelStop() { this.removeMessages(NsdManager.DAEMON_CLEANUP); } @@ -164,11 +174,16 @@ public class NsdService extends INsdManager.Stub { if (DBG) Slog.d(TAG, "Client connection lost with reason: " + msg.arg1); break; } + cInfo = mClients.get(msg.replyTo); if (cInfo != null) { cInfo.expungeAllRequests(); mClients.remove(msg.replyTo); + if (cInfo.isLegacy()) { + mLegacyClientCount -= 1; + } } + maybeScheduleStop(); break; case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: AsyncChannel ac = new AsyncChannel(); @@ -197,6 +212,17 @@ public class NsdService extends INsdManager.Stub { case NsdManager.DAEMON_CLEANUP: mDaemon.maybeStop(); break; + // This event should be only sent by the legacy (target SDK < S) clients. + // Mark the sending client as legacy. + case NsdManager.DAEMON_STARTUP: + cInfo = mClients.get(msg.replyTo); + if (cInfo != null) { + cancelStop(); + cInfo.setLegacy(); + mLegacyClientCount += 1; + maybeStartDaemon(); + } + break; case NsdManager.NATIVE_DAEMON_EVENT: default: Slog.e(TAG, "Unhandled " + msg); @@ -235,7 +261,7 @@ public class NsdService extends INsdManager.Stub { public void exit() { // TODO: it is incorrect to stop the daemon without expunging all requests // and sending error callbacks to clients. - maybeScheduleStop(); + scheduleStop(); } private boolean requestLimitReached(ClientInfo clientInfo) { @@ -271,9 +297,6 @@ public class NsdService extends INsdManager.Stub { return NOT_HANDLED; case AsyncChannel.CMD_CHANNEL_DISCONNECTED: return NOT_HANDLED; - } - - switch (msg.what) { case NsdManager.DISABLE: //TODO: cleanup clients transitionTo(mDisabledState); @@ -531,10 +554,6 @@ public class NsdService extends INsdManager.Stub { } } - private boolean isAnyRequestActive() { - return mIdToClientInfoMap.size() != 0; - } - private String unescape(String s) { StringBuilder sb = new StringBuilder(s.length()); for (int i = 0; i < s.length(); ++i) { @@ -859,6 +878,9 @@ public class NsdService extends INsdManager.Stub { /* A map from client id to the type of the request we had received */ private final SparseIntArray mClientRequests = new SparseIntArray(); + // The target SDK of this client < Build.VERSION_CODES.S + private boolean mIsLegacy = false; + private ClientInfo(AsyncChannel c, Messenger m) { mChannel = c; mMessenger = m; @@ -871,6 +893,7 @@ public class NsdService extends INsdManager.Stub { sb.append("mChannel ").append(mChannel).append("\n"); sb.append("mMessenger ").append(mMessenger).append("\n"); sb.append("mResolvedService ").append(mResolvedService).append("\n"); + sb.append("mIsLegacy ").append(mIsLegacy).append("\n"); for(int i = 0; i< mClientIds.size(); i++) { int clientID = mClientIds.keyAt(i); sb.append("clientId ").append(clientID). @@ -880,6 +903,14 @@ public class NsdService extends INsdManager.Stub { return sb.toString(); } + private boolean isLegacy() { + return mIsLegacy; + } + + private void setLegacy() { + mIsLegacy = true; + } + // Remove any pending requests from the global map when we get rid of a client, // and send cancellations to the daemon. private void expungeAllRequests() { @@ -907,7 +938,6 @@ public class NsdService extends INsdManager.Stub { } mClientIds.clear(); mClientRequests.clear(); - mNsdStateMachine.maybeScheduleStop(); } // mClientIds is a sparse array of listener id -> mDnsClient id. For a given mDnsClient id, diff --git a/services/core/java/com/android/server/net/NetworkStatsService.java b/services/core/java/com/android/server/net/NetworkStatsService.java index 4ee867b7d0..097b0711ef 100644 --- a/services/core/java/com/android/server/net/NetworkStatsService.java +++ b/services/core/java/com/android/server/net/NetworkStatsService.java @@ -289,8 +289,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub { private String mActiveIface; /** Set of any ifaces associated with mobile networks since boot. */ - @GuardedBy("mStatsLock") - private String[] mMobileIfaces = new String[0]; + private volatile String[] mMobileIfaces = new String[0]; /** Set of all ifaces currently used by traffic that does not explicitly specify a Network. */ @GuardedBy("mStatsLock") @@ -935,7 +934,12 @@ public class NetworkStatsService extends INetworkStatsService.Stub { @Override public String[] getMobileIfaces() { - return mMobileIfaces; + // TODO (b/192758557): Remove debug log. + if (ArrayUtils.contains(mMobileIfaces, null)) { + throw new NullPointerException( + "null element in mMobileIfaces: " + Arrays.toString(mMobileIfaces)); + } + return mMobileIfaces.clone(); } @Override @@ -1084,7 +1088,8 @@ public class NetworkStatsService extends INetworkStatsService.Stub { } @Override - public long getIfaceStats(String iface, int type) { + public long getIfaceStats(@NonNull String iface, int type) { + Objects.requireNonNull(iface); long nativeIfaceStats = nativeGetIfaceStat(iface, type, checkBpfStatsEnable()); if (nativeIfaceStats == -1) { return nativeIfaceStats; @@ -1382,7 +1387,12 @@ public class NetworkStatsService extends INetworkStatsService.Stub { } } - mMobileIfaces = mobileIfaces.toArray(new String[mobileIfaces.size()]); + mMobileIfaces = mobileIfaces.toArray(new String[0]); + // TODO (b/192758557): Remove debug log. + if (ArrayUtils.contains(mMobileIfaces, null)) { + throw new NullPointerException( + "null element in mMobileIfaces: " + Arrays.toString(mMobileIfaces)); + } } private static int getSubIdForMobile(@NonNull NetworkStateSnapshot state) {