Merge "Use IDnsResolver instead of INetd for resolver related binder commands"
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index ae93cf0..4a64128 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -1934,6 +1934,8 @@
             @NonNull Callback callback) {
         ParcelFileDescriptor dup;
         try {
+            // Dup is needed here as the pfd inside the socket is owned by the IpSecService,
+            // which cannot be obtained by the app process.
             dup = ParcelFileDescriptor.dup(socket.getFileDescriptor());
         } catch (IOException ignored) {
             // Construct an invalid fd, so that if the user later calls start(), it will fail with
@@ -1975,6 +1977,7 @@
             @NonNull Callback callback) {
         ParcelFileDescriptor dup;
         try {
+            // TODO: Consider remove unnecessary dup.
             dup = pfd.dup();
         } catch (IOException ignored) {
             // Construct an invalid fd, so that if the user later calls start(), it will fail with
diff --git a/core/java/android/net/DnsResolver.java b/core/java/android/net/DnsResolver.java
index 5980251..f9e0af2 100644
--- a/core/java/android/net/DnsResolver.java
+++ b/core/java/android/net/DnsResolver.java
@@ -205,6 +205,7 @@
         if (cancellationSignal != null && cancellationSignal.isCanceled()) {
             return;
         }
+        final Object lock = new Object();
         final FileDescriptor queryfd;
         try {
             queryfd = resNetworkSend((network != null
@@ -214,8 +215,8 @@
             return;
         }
 
-        maybeAddCancellationSignal(cancellationSignal, queryfd);
-        registerFDListener(executor, queryfd, callback);
+        maybeAddCancellationSignal(cancellationSignal, queryfd, lock);
+        registerFDListener(executor, queryfd, callback, cancellationSignal, lock);
     }
 
     /**
@@ -242,6 +243,7 @@
         if (cancellationSignal != null && cancellationSignal.isCanceled()) {
             return;
         }
+        final Object lock = new Object();
         final FileDescriptor queryfd;
         try {
             queryfd = resNetworkQuery((network != null
@@ -251,31 +253,37 @@
             return;
         }
 
-        maybeAddCancellationSignal(cancellationSignal, queryfd);
-        registerFDListener(executor, queryfd, callback);
+        maybeAddCancellationSignal(cancellationSignal, queryfd, lock);
+        registerFDListener(executor, queryfd, callback, cancellationSignal, lock);
     }
 
     private <T> void registerFDListener(@NonNull Executor executor,
-            @NonNull FileDescriptor queryfd, @NonNull AnswerCallback<T> answerCallback) {
+            @NonNull FileDescriptor queryfd, @NonNull AnswerCallback<T> answerCallback,
+            @Nullable CancellationSignal cancellationSignal, @NonNull Object lock) {
         Looper.getMainLooper().getQueue().addOnFileDescriptorEventListener(
                 queryfd,
                 FD_EVENTS,
                 (fd, events) -> {
                     executor.execute(() -> {
-                        byte[] answerbuf = null;
-                        try {
-                            answerbuf = resNetworkResult(fd);
-                        } catch (ErrnoException e) {
-                            Log.e(TAG, "resNetworkResult:" + e.toString());
-                            answerCallback.onQueryException(e);
-                            return;
-                        }
+                        synchronized (lock) {
+                            if (cancellationSignal != null && cancellationSignal.isCanceled()) {
+                                return;
+                            }
+                            byte[] answerbuf = null;
+                            try {
+                                answerbuf = resNetworkResult(fd);
+                            } catch (ErrnoException e) {
+                                Log.e(TAG, "resNetworkResult:" + e.toString());
+                                answerCallback.onQueryException(e);
+                                return;
+                            }
 
-                        try {
-                            answerCallback.onAnswer(
-                                    answerCallback.parser.parse(answerbuf));
-                        } catch (ParseException e) {
-                            answerCallback.onParseException(e);
+                            try {
+                                answerCallback.onAnswer(
+                                        answerCallback.parser.parse(answerbuf));
+                            } catch (ParseException e) {
+                                answerCallback.onParseException(e);
+                            }
                         }
                     });
                     // Unregister this fd listener
@@ -284,14 +292,16 @@
     }
 
     private void maybeAddCancellationSignal(@Nullable CancellationSignal cancellationSignal,
-            @NonNull FileDescriptor queryfd) {
+            @NonNull FileDescriptor queryfd, @NonNull Object lock) {
         if (cancellationSignal == null) return;
-        cancellationSignal.setOnCancelListener(
-                () -> {
-                    Looper.getMainLooper().getQueue()
-                            .removeOnFileDescriptorEventListener(queryfd);
-                    resNetworkCancel(queryfd);
-            });
+        cancellationSignal.setOnCancelListener(() -> {
+            synchronized (lock)  {
+                if (!queryfd.valid()) return;
+                Looper.getMainLooper().getQueue()
+                        .removeOnFileDescriptorEventListener(queryfd);
+                resNetworkCancel(queryfd);
+            }
+        });
     }
 
     private static class DnsAddressAnswer extends DnsPacket {
diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp
index d7a981e..82acf6f 100644
--- a/core/jni/android_net_NetUtils.cpp
+++ b/core/jni/android_net_NetUtils.cpp
@@ -470,6 +470,7 @@
     std::vector<uint8_t> buf(MAXPACKETSIZE, 0);
 
     int res = resNetworkResult(fd, &rcode, buf.data(), MAXPACKETSIZE);
+    jniSetFileDescriptorOfFD(env, javaFd, -1);
     if (res < 0) {
         throwErrnoException(env, "resNetworkResult", -res);
         return nullptr;
@@ -490,6 +491,7 @@
 static void android_net_utils_resNetworkCancel(JNIEnv *env, jobject thiz, jobject javaFd) {
     int fd = jniGetFDFromFileDescriptor(env, javaFd);
     resNetworkCancel(fd);
+    jniSetFileDescriptorOfFD(env, javaFd, -1);
 }
 
 static jobject android_net_utils_getTcpRepairWindow(JNIEnv *env, jobject thiz, jobject javaFd) {
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index c559191..699cf60 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -4309,7 +4309,7 @@
 
     /**
      * @return VPN information for accounting, or null if we can't retrieve all required
-     *         information, e.g primary underlying iface.
+     *         information, e.g underlying ifaces.
      */
     @Nullable
     private VpnInfo createVpnInfo(Vpn vpn) {
@@ -4321,17 +4321,24 @@
         // see VpnService.setUnderlyingNetworks()'s javadoc about how to interpret
         // the underlyingNetworks list.
         if (underlyingNetworks == null) {
-            NetworkAgentInfo defaultNetwork = getDefaultNetwork();
-            if (defaultNetwork != null && defaultNetwork.linkProperties != null) {
-                info.primaryUnderlyingIface = getDefaultNetwork().linkProperties.getInterfaceName();
-            }
-        } else if (underlyingNetworks.length > 0) {
-            LinkProperties linkProperties = getLinkProperties(underlyingNetworks[0]);
-            if (linkProperties != null) {
-                info.primaryUnderlyingIface = linkProperties.getInterfaceName();
+            NetworkAgentInfo defaultNai = getDefaultNetwork();
+            if (defaultNai != null && defaultNai.linkProperties != null) {
+                underlyingNetworks = new Network[] { defaultNai.network };
             }
         }
-        return info.primaryUnderlyingIface == null ? null : info;
+        if (underlyingNetworks != null && underlyingNetworks.length > 0) {
+            List<String> interfaces = new ArrayList<>();
+            for (Network network : underlyingNetworks) {
+                LinkProperties lp = getLinkProperties(network);
+                if (lp != null) {
+                    interfaces.add(lp.getInterfaceName());
+                }
+            }
+            if (!interfaces.isEmpty()) {
+                info.underlyingIfaces = interfaces.toArray(new String[interfaces.size()]);
+            }
+        }
+        return info.underlyingIfaces == null ? null : info;
     }
 
     /**
diff --git a/services/core/java/com/android/server/connectivity/KeepaliveTracker.java b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java
index ce887eb..d7a57b9 100644
--- a/services/core/java/com/android/server/connectivity/KeepaliveTracker.java
+++ b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java
@@ -154,12 +154,19 @@
             // keepalives are sent cannot be reused by another app even if the fd gets closed by
             // the user. A null is acceptable here for backward compatibility of PacketKeepalive
             // API.
-            // TODO: don't accept null fd after legacy packetKeepalive API is removed.
             try {
                 if (fd != null) {
                     mFd = Os.dup(fd);
                 }  else {
-                    Log.d(TAG, "uid/pid " + mUid + "/" + mPid + " calls with null fd");
+                    Log.d(TAG, toString() + " calls with null fd");
+                    if (!mPrivileged) {
+                        throw new SecurityException(
+                                "null fd is not allowed for unprivileged access.");
+                    }
+                    if (mType == TYPE_TCP) {
+                        throw new IllegalArgumentException(
+                                "null fd is not allowed for tcp socket keepalives.");
+                    }
                     mFd = null;
                 }
             } catch (ErrnoException e) {
@@ -480,7 +487,6 @@
             }
         } else {
             // Keepalive successfully stopped, or error.
-            ki.mStartedState = KeepaliveInfo.NOT_STARTED;
             if (reason == SUCCESS) {
                 // The message indicated success stopping : don't call handleStopKeepalive.
                 if (DBG) Log.d(TAG, "Successfully stopped keepalive " + slot + " on " + nai.name());
@@ -490,6 +496,7 @@
                 handleStopKeepalive(nai, slot, reason);
                 if (DBG) Log.d(TAG, "Keepalive " + slot + " on " + nai.name() + " error " + reason);
             }
+            ki.mStartedState = KeepaliveInfo.NOT_STARTED;
         }
     }
 
@@ -531,7 +538,8 @@
         try {
             ki = new KeepaliveInfo(cb, nai, packet, intervalSeconds,
                     KeepaliveInfo.TYPE_NATT, fd);
-        } catch (InvalidSocketException e) {
+        } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) {
+            Log.e(TAG, "Fail to construct keepalive", e);
             notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
             return;
         }
@@ -570,7 +578,8 @@
         try {
             ki = new KeepaliveInfo(cb, nai, packet, intervalSeconds,
                     KeepaliveInfo.TYPE_TCP, fd);
-        } catch (InvalidSocketException e) {
+        } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) {
+            Log.e(TAG, "Fail to construct keepalive e=" + e);
             notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
             return;
         }
diff --git a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
index 828a1e5..ac3d6de 100644
--- a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
@@ -19,6 +19,7 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.telephony.SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
 
 import android.app.Notification;
 import android.app.NotificationManager;
@@ -26,9 +27,12 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
+import android.net.NetworkSpecifier;
+import android.net.StringNetworkSpecifier;
 import android.net.wifi.WifiInfo;
 import android.os.UserHandle;
 import android.telephony.AccessNetworkConstants.TransportType;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.Slog;
@@ -195,7 +199,20 @@
                     title = r.getString(R.string.network_available_sign_in, 0);
                     // TODO: Change this to pull from NetworkInfo once a printable
                     // name has been added to it
-                    details = mTelephonyManager.getNetworkOperatorName();
+                    NetworkSpecifier specifier = nai.networkCapabilities.getNetworkSpecifier();
+                    int subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
+                    if (specifier instanceof StringNetworkSpecifier) {
+                        try {
+                            subId = Integer.parseInt(
+                                    ((StringNetworkSpecifier) specifier).specifier);
+                        } catch (NumberFormatException e) {
+                            Slog.e(TAG, "NumberFormatException on "
+                                    + ((StringNetworkSpecifier) specifier).specifier);
+                        }
+                    }
+
+                    details = mTelephonyManager.createForSubscriptionId(subId)
+                            .getNetworkOperatorName();
                     break;
                 default:
                     title = r.getString(R.string.network_available_sign_in, 0);
diff --git a/tests/net/java/android/net/NetworkStatsTest.java b/tests/net/java/android/net/NetworkStatsTest.java
index b5b0384..c16a0f4 100644
--- a/tests/net/java/android/net/NetworkStatsTest.java
+++ b/tests/net/java/android/net/NetworkStatsTest.java
@@ -569,7 +569,7 @@
             .addValues(underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE, METERED_NO, ROAMING_NO,
                     DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L);
 
-        assertTrue(delta.toString(), delta.migrateTun(tunUid, tunIface, underlyingIface));
+        delta.migrateTun(tunUid, tunIface, new String[] {underlyingIface});
         assertEquals(20, delta.size());
 
         // tunIface and TEST_IFACE entries are not changed.
@@ -650,7 +650,7 @@
             .addValues(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
                     DEFAULT_NETWORK_NO,  75500L, 37L, 130000L, 70L, 0L);
 
-        assertTrue(delta.migrateTun(tunUid, tunIface, underlyingIface));
+        delta.migrateTun(tunUid, tunIface, new String[]{underlyingIface});
         assertEquals(9, delta.size());
 
         // tunIface entries should not be changed.
@@ -813,6 +813,37 @@
     }
 
     @Test
+    public void testFilterDebugEntries() {
+        NetworkStats.Entry entry1 = new NetworkStats.Entry(
+                "test1", 10100, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry2 = new NetworkStats.Entry(
+                "test2", 10101, SET_DBG_VPN_IN, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry3 = new NetworkStats.Entry(
+                "test2", 10101, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats.Entry entry4 = new NetworkStats.Entry(
+                "test2", 10101, SET_DBG_VPN_OUT, TAG_NONE, METERED_NO, ROAMING_NO,
+                DEFAULT_NETWORK_NO, 50000L, 25L, 100000L, 50L, 0L);
+
+        NetworkStats stats = new NetworkStats(TEST_START, 4)
+                .addValues(entry1)
+                .addValues(entry2)
+                .addValues(entry3)
+                .addValues(entry4);
+
+        stats.filterDebugEntries();
+
+        assertEquals(2, stats.size());
+        assertEquals(entry1, stats.getValues(0, null));
+        assertEquals(entry3, stats.getValues(1, null));
+    }
+
+    @Test
     public void testApply464xlatAdjustments() {
         final String v4Iface = "v4-wlan0";
         final String baseIface = "wlan0";
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index a478424..afefd5e 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -4070,8 +4070,6 @@
         // TODO: 1. Move this outside of ConnectivityServiceTest.
         //       2. Make test to verify that Nat-T keepalive socket is created by IpSecService.
         //       3. Mock ipsec service.
-        //       4. Find a free port instead of a fixed port.
-        final int srcPort = 12345;
         final InetAddress myIPv4 = InetAddress.getByName("192.0.2.129");
         final InetAddress notMyIPv4 = InetAddress.getByName("192.0.2.35");
         final InetAddress myIPv6 = InetAddress.getByName("2001:db8::1");
@@ -4082,7 +4080,8 @@
         final int invalidKaInterval = 9;
 
         final IpSecManager mIpSec = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE);
-        final UdpEncapsulationSocket testSocket = mIpSec.openUdpEncapsulationSocket(srcPort);
+        final UdpEncapsulationSocket testSocket = mIpSec.openUdpEncapsulationSocket();
+        final int srcPort = testSocket.getPort();
 
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("wlan12");
@@ -4202,6 +4201,7 @@
 
         // Check that keepalive slots start from 1 and increment. The first one gets slot 1.
         mWiFiNetworkAgent.setExpectedKeepaliveSlot(1);
+        int srcPort2 = 0;
         try (SocketKeepalive ka = mCm.createSocketKeepalive(
                 myNet, testSocket, myIPv4, dstIPv4, executor, callback)) {
             ka.start(validKaInterval);
@@ -4209,7 +4209,8 @@
 
             // The second one gets slot 2.
             mWiFiNetworkAgent.setExpectedKeepaliveSlot(2);
-            final UdpEncapsulationSocket testSocket2 = mIpSec.openUdpEncapsulationSocket(6789);
+            final UdpEncapsulationSocket testSocket2 = mIpSec.openUdpEncapsulationSocket();
+            srcPort2 = testSocket2.getPort();
             TestSocketKeepaliveCallback callback2 = new TestSocketKeepaliveCallback(executor);
             try (SocketKeepalive ka2 = mCm.createSocketKeepalive(
                     myNet, testSocket2, myIPv4, dstIPv4, executor, callback2)) {
@@ -4227,6 +4228,10 @@
             }
         }
 
+        // Check that there is no port leaked after all keepalives and sockets are closed.
+        assertFalse(isUdpPortInUse(srcPort));
+        assertFalse(isUdpPortInUse(srcPort2));
+
         mWiFiNetworkAgent.disconnect();
         waitFor(mWiFiNetworkAgent.getDisconnectedCV());
         mWiFiNetworkAgent = null;
@@ -4309,7 +4314,6 @@
     }
 
     private void doTestNattSocketKeepalivesFdWithExecutor(Executor executor) throws Exception {
-        final int srcPort = 12345;
         final InetAddress myIPv4 = InetAddress.getByName("192.0.2.129");
         final InetAddress anyIPv4 = InetAddress.getByName("0.0.0.0");
         final InetAddress dstIPv4 = InetAddress.getByName("8.8.8.8");
@@ -4328,7 +4332,8 @@
 
         // Prepare the target file descriptor, keep only one instance.
         final IpSecManager mIpSec = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE);
-        final UdpEncapsulationSocket testSocket = mIpSec.openUdpEncapsulationSocket(srcPort);
+        final UdpEncapsulationSocket testSocket = mIpSec.openUdpEncapsulationSocket();
+        final int srcPort = testSocket.getPort();
         final ParcelFileDescriptor testPfd =
                 ParcelFileDescriptor.dup(testSocket.getFileDescriptor());
         testSocket.close();
diff --git a/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java
index bce526d..e35c34a 100644
--- a/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -57,6 +57,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -216,11 +217,16 @@
         expectNetworkStatsUidDetail(buildEmptyStats());
         expectSystemReady();
 
+        assertNull(mService.getTunAdjustedStats());
         mService.systemReady();
+        // Verify that system ready fetches realtime stats and initializes tun adjusted stats.
+        verify(mNetManager).getNetworkStatsUidDetail(UID_ALL, INTERFACES_ALL);
+        assertNotNull("failed to initialize TUN adjusted stats", mService.getTunAdjustedStats());
+        assertEquals(0, mService.getTunAdjustedStats().size());
+
         mSession = mService.openSession();
         assertNotNull("openSession() failed", mSession);
 
-
         // catch INetworkManagementEventObserver during systemReady()
         ArgumentCaptor<INetworkManagementEventObserver> networkObserver =
               ArgumentCaptor.forClass(INetworkManagementEventObserver.class);
@@ -733,11 +739,13 @@
 
         NetworkStats stats = mService.getDetailedUidStats(ifaceFilter);
 
-        verify(mNetManager, times(1)).getNetworkStatsUidDetail(eq(UID_ALL), argThat(ifaces ->
-                ifaces != null && ifaces.length == 2
-                        && ArrayUtils.contains(ifaces, TEST_IFACE)
-                        && ArrayUtils.contains(ifaces, stackedIface)));
-
+        // mNetManager#getNetworkStatsUidDetail(UID_ALL, INTERFACES_ALL) has following invocations:
+        // 1) NetworkStatsService#systemReady from #setUp.
+        // 2) mService#forceUpdateIfaces in the test above.
+        // 3) Finally, mService#getDetailedUidStats.
+        verify(mNetManager, times(3)).getNetworkStatsUidDetail(UID_ALL, INTERFACES_ALL);
+        assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), TEST_IFACE));
+        assertTrue(ArrayUtils.contains(stats.getUniqueIfaces(), stackedIface));
         assertEquals(2, stats.size());
         assertEquals(uidStats, stats.getValues(0, null));
         assertEquals(tetheredStats1, stats.getValues(1, null));
@@ -927,7 +935,7 @@
         // WiFi network is connected and VPN is using WiFi (which has TEST_IFACE).
         expectDefaultSettings();
         NetworkState[] networkStates = new NetworkState[] {buildWifiState(), buildVpnState()};
-        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(TEST_IFACE)};
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
         expectNetworkStatsUidDetail(buildEmptyStats());
         expectBandwidthControlCheck();
 
@@ -947,8 +955,10 @@
         expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
                 .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 1000L, 100L, 1L)
                 .addValues(TUN_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 500L, 50L, 500L, 50L, 1L)
-                .addValues(
-                    TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 1650L, 150L, 1650L, 150L, 2L));
+                // VPN received 1650 bytes over WiFi in background (SET_DEFAULT).
+                .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 1650L, 150L, 0L, 0L, 1L)
+                // VPN sent 1650 bytes over WiFi in foreground (SET_FOREGROUND).
+                .addValues(TEST_IFACE, UID_VPN, SET_FOREGROUND, TAG_NONE, 0L, 0L, 1650L, 150L, 1L));
 
         forcePollAndWaitForIdle();
 
@@ -962,7 +972,7 @@
         // WiFi network is connected and VPN is using WiFi (which has TEST_IFACE).
         expectDefaultSettings();
         NetworkState[] networkStates = new NetworkState[] {buildWifiState(), buildVpnState()};
-        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(TEST_IFACE)};
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
         expectNetworkStatsUidDetail(buildEmptyStats());
         expectBandwidthControlCheck();
 
@@ -993,6 +1003,132 @@
     }
 
     @Test
+    public void vpnWithTwoUnderlyingIfaces_packetDuplication() throws Exception {
+        // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and
+        // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set.
+        // Additionally, VPN is duplicating traffic across both WiFi and Cell.
+        expectDefaultSettings();
+        NetworkState[] networkStates =
+                new NetworkState[] {
+                    buildWifiState(), buildMobile4gState(TEST_IFACE2), buildVpnState()
+                };
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})};
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectBandwidthControlCheck();
+
+        mService.forceUpdateIfaces(
+                new Network[] {WIFI_NETWORK, VPN_NETWORK},
+                vpnInfos,
+                networkStates,
+                getActiveIface(networkStates));
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were sent/received by UID_RED and UID_BLUE over VPN.
+        // VPN sent/received 4400 bytes (400 packets) over both WiFi and Cell (8800 bytes in total).
+        // Of 8800 bytes over WiFi/Cell, expect:
+        // - 500 bytes rx/tx each over WiFi/Cell attributed to both UID_RED and UID_BLUE.
+        // - 1200 bytes rx/tx each over WiFi/Cell for VPN_UID.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 4)
+                .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 1000L, 100L, 2L)
+                .addValues(TUN_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 1000L, 100L, 1000L, 100L, 2L)
+                .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 2200L, 200L, 2200L, 200L, 2L)
+                .addValues(
+                    TEST_IFACE2, UID_VPN, SET_DEFAULT, TAG_NONE, 2200L, 200L, 2200L, 200L, 2L));
+
+        forcePollAndWaitForIdle();
+
+        assertUidTotal(sTemplateWifi, UID_RED, 500L, 50L, 500L, 50L, 1);
+        assertUidTotal(sTemplateWifi, UID_BLUE, 500L, 50L, 500L, 50L, 1);
+        assertUidTotal(sTemplateWifi, UID_VPN, 1200L, 100L, 1200L, 100L, 2);
+
+        assertUidTotal(buildTemplateMobileWildcard(), UID_RED, 500L, 50L, 500L, 50L, 1);
+        assertUidTotal(buildTemplateMobileWildcard(), UID_BLUE, 500L, 50L, 500L, 50L, 1);
+        assertUidTotal(buildTemplateMobileWildcard(), UID_VPN, 1200L, 100L, 1200L, 100L, 2);
+    }
+
+    @Test
+    public void vpnWithTwoUnderlyingIfaces_splitTraffic() throws Exception {
+        // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and
+        // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set.
+        // Additionally, VPN is arbitrarily splitting traffic across WiFi and Cell.
+        expectDefaultSettings();
+        NetworkState[] networkStates =
+                new NetworkState[] {
+                    buildWifiState(), buildMobile4gState(TEST_IFACE2), buildVpnState()
+                };
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})};
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectBandwidthControlCheck();
+
+        mService.forceUpdateIfaces(
+                new Network[] {WIFI_NETWORK, VPN_NETWORK},
+                vpnInfos,
+                networkStates,
+                getActiveIface(networkStates));
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were sent/received by UID_RED over VPN.
+        // VPN sent/received 660 bytes (60 packets) over WiFi and 440 bytes (40 packets) over Cell.
+        // For UID_RED, expect 600 bytes attributed over WiFi and 400 bytes over Cell for both
+        // rx/tx.
+        // For UID_VPN, expect 60 bytes attributed over WiFi and 40 bytes over Cell for both rx/tx.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 3)
+              .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 1000L, 100L, 2L)
+              .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 660L, 60L, 660L, 60L, 1L)
+              .addValues(TEST_IFACE2, UID_VPN, SET_DEFAULT, TAG_NONE, 440L, 40L, 440L, 40L, 1L));
+
+        forcePollAndWaitForIdle();
+
+        assertUidTotal(sTemplateWifi, UID_RED, 600L, 60L, 600L, 60L, 1);
+        assertUidTotal(sTemplateWifi, UID_VPN, 60L, 0L, 60L, 0L, 1);
+
+        assertUidTotal(buildTemplateMobileWildcard(), UID_RED, 400L, 40L, 400L, 40L, 1);
+        assertUidTotal(buildTemplateMobileWildcard(), UID_VPN, 40L, 0L, 40L, 0L, 1);
+    }
+
+    @Test
+    public void vpnWithTwoUnderlyingIfaces_splitTrafficWithCompression() throws Exception {
+        // WiFi and Cell networks are connected and VPN is using WiFi (which has TEST_IFACE) and
+        // Cell (which has TEST_IFACE2) and has declared both of them in its underlying network set.
+        // Additionally, VPN is arbitrarily splitting compressed traffic across WiFi and Cell.
+        expectDefaultSettings();
+        NetworkState[] networkStates =
+                new NetworkState[] {
+                    buildWifiState(), buildMobile4gState(TEST_IFACE2), buildVpnState()
+                };
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE, TEST_IFACE2})};
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectBandwidthControlCheck();
+
+        mService.forceUpdateIfaces(
+                new Network[] {WIFI_NETWORK, VPN_NETWORK},
+                vpnInfos,
+                networkStates,
+                getActiveIface(networkStates));
+        // create some traffic (assume 10 bytes of MTU for VPN interface:
+        // 1000 bytes (100 packets) were sent/received by UID_RED over VPN.
+        // VPN sent/received 600 bytes (60 packets) over WiFi and 200 bytes (20 packets) over Cell.
+        // For UID_RED, expect 600 bytes attributed over WiFi and 200 bytes over Cell for both
+        // rx/tx.
+        // UID_VPN gets nothing attributed to it (avoiding negative stats).
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 4)
+              .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 1000L, 100L, 1L)
+              .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 600L, 60L, 600L, 60L, 0L)
+              .addValues(TEST_IFACE2, UID_VPN, SET_DEFAULT, TAG_NONE, 200L, 20L, 200L, 20L, 0L));
+
+        forcePollAndWaitForIdle();
+
+        assertUidTotal(sTemplateWifi, UID_RED, 600L, 60L, 600L, 60L, 0);
+        assertUidTotal(sTemplateWifi, UID_VPN, 0L, 0L, 0L, 0L, 0);
+
+        assertUidTotal(buildTemplateMobileWildcard(), UID_RED, 200L, 20L, 200L, 20L, 0);
+        assertUidTotal(buildTemplateMobileWildcard(), UID_VPN, 0L, 0L, 0L, 0L, 0);
+    }
+
+    @Test
     public void vpnWithIncorrectUnderlyingIface() throws Exception {
         // WiFi and Cell networks are connected and VPN is using Cell (which has TEST_IFACE2),
         // but has declared only WiFi (TEST_IFACE) in its underlying network set.
@@ -1001,7 +1137,7 @@
                 new NetworkState[] {
                     buildWifiState(), buildMobile4gState(TEST_IFACE2), buildVpnState()
                 };
-        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(TEST_IFACE)};
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
         expectNetworkStatsUidDetail(buildEmptyStats());
         expectBandwidthControlCheck();
 
@@ -1030,6 +1166,134 @@
     }
 
     @Test
+    public void recordSnapshot_migratesTunTrafficAndUpdatesTunAdjustedStats() throws Exception {
+        assertEquals(0, mService.getTunAdjustedStats().size());
+        // VPN using WiFi (TEST_IFACE).
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+        expectBandwidthControlCheck();
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were downloaded by UID_RED over VPN.
+        // VPN received 1100 bytes (100 packets) over WiFi.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+              .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 0L, 0L, 0L)
+              .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 1100L, 100L, 0L, 0L, 0L));
+
+        // this should lead to NSS#recordSnapshotLocked
+        mService.forceUpdateIfaces(
+                new Network[0], vpnInfos, new NetworkState[0], null /* activeIface */);
+
+        // Verify TUN adjusted stats have traffic migrated correctly.
+        // Of 1100 bytes VPN received over WiFi, expect 1000 bytes attributed to UID_RED and 100
+        // bytes attributed to UID_VPN.
+        NetworkStats tunAdjStats = mService.getTunAdjustedStats();
+        assertValues(
+                tunAdjStats, TEST_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 1000L, 100L, 0L, 0L, 0);
+        assertValues(
+                tunAdjStats, TEST_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 100L, 0L, 0L, 0L, 0);
+    }
+
+    @Test
+    public void getDetailedUidStats_migratesTunTrafficAndUpdatesTunAdjustedStats()
+            throws Exception {
+        assertEquals(0, mService.getTunAdjustedStats().size());
+        // VPN using WiFi (TEST_IFACE).
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+        expectBandwidthControlCheck();
+        mService.forceUpdateIfaces(
+                new Network[0], vpnInfos, new NetworkState[0], null /* activeIface */);
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were downloaded by UID_RED over VPN.
+        // VPN received 1100 bytes (100 packets) over WiFi.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+              .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 0L, 0L, 0L)
+              .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 1100L, 100L, 0L, 0L, 0L));
+
+        mService.getDetailedUidStats(INTERFACES_ALL);
+
+        // Verify internally maintained TUN adjusted stats
+        NetworkStats tunAdjStats = mService.getTunAdjustedStats();
+        // Verify stats for TEST_IFACE (WiFi):
+        // Of 1100 bytes VPN received over WiFi, expect 1000 bytes attributed to UID_RED and 100
+        // bytes attributed to UID_VPN.
+        assertValues(
+                tunAdjStats, TEST_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 1000L, 100L, 0L, 0L, 0);
+        assertValues(
+                tunAdjStats, TEST_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 100L, 0L, 0L, 0L, 0);
+        // Verify stats for TUN_IFACE; only UID_RED should have usage on it.
+        assertValues(
+                tunAdjStats, TUN_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 1000L, 100L, 0L, 0L, 0);
+        assertValues(
+                tunAdjStats, TUN_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 0L, 0L, 0L, 0L, 0);
+
+        // lets assume that since last time, VPN received another 1100 bytes (same assumptions as
+        // before i.e. UID_RED downloaded another 1000 bytes).
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        // Note - NetworkStatsFactory returns counters that are monotonically increasing.
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+              .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 2000L, 200L, 0L, 0L, 0L)
+              .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 2200L, 200L, 0L, 0L, 0L));
+
+        mService.getDetailedUidStats(INTERFACES_ALL);
+
+        tunAdjStats = mService.getTunAdjustedStats();
+        // verify TEST_IFACE stats:
+        assertValues(
+                tunAdjStats, TEST_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 2000L, 200L, 0L, 0L, 0);
+        assertValues(
+                tunAdjStats, TEST_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 200L, 0L, 0L, 0L, 0);
+        // verify TUN_IFACE stats:
+        assertValues(
+                tunAdjStats, TUN_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 2000L, 200L, 0L, 0L, 0);
+        assertValues(
+                tunAdjStats, TUN_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 0L, 0L, 0L, 0L, 0);
+    }
+
+    @Test
+    public void getDetailedUidStats_returnsCorrectStatsWithVpnRunning() throws Exception {
+        // VPN using WiFi (TEST_IFACE).
+        VpnInfo[] vpnInfos = new VpnInfo[] {createVpnInfo(new String[] {TEST_IFACE})};
+        expectBandwidthControlCheck();
+        mService.forceUpdateIfaces(
+                new Network[0], vpnInfos, new NetworkState[0], null /* activeIface */);
+        // create some traffic (assume 10 bytes of MTU for VPN interface and 1 byte encryption
+        // overhead per packet):
+        // 1000 bytes (100 packets) were downloaded by UID_RED over VPN.
+        // VPN received 1100 bytes (100 packets) over WiFi.
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+              .addValues(TUN_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 1000L, 100L, 0L, 0L, 0L)
+              .addValues(TEST_IFACE, UID_VPN, SET_DEFAULT, TAG_NONE, 1100L, 100L, 0L, 0L, 0L));
+
+        // Query realtime stats for TEST_IFACE.
+        NetworkStats queriedStats =
+                mService.getDetailedUidStats(new String[] {TEST_IFACE});
+
+        assertEquals(HOUR_IN_MILLIS, queriedStats.getElapsedRealtime());
+        // verify that returned stats are only for TEST_IFACE and VPN traffic is migrated correctly.
+        assertEquals(new String[] {TEST_IFACE}, queriedStats.getUniqueIfaces());
+        assertValues(
+                queriedStats, TEST_IFACE, UID_RED, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 1000L, 100L, 0L, 0L, 0);
+        assertValues(
+                queriedStats, TEST_IFACE, UID_VPN, SET_ALL, TAG_NONE, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL, 100L, 0L, 0L, 0L, 0);
+    }
+
+    @Test
     public void testRegisterUsageCallback() throws Exception {
         // pretend that wifi network comes online; service should ask about full
         // network state, and poll any existing interfaces before updating.
@@ -1382,11 +1646,11 @@
         return new NetworkState(info, prop, new NetworkCapabilities(), VPN_NETWORK, null, null);
     }
 
-    private static VpnInfo createVpnInfo(String underlyingIface) {
+    private static VpnInfo createVpnInfo(String[] underlyingIfaces) {
         VpnInfo info = new VpnInfo();
         info.ownerUid = UID_VPN;
         info.vpnIface = TUN_IFACE;
-        info.primaryUnderlyingIface = underlyingIface;
+        info.underlyingIfaces = underlyingIfaces;
         return info;
     }