Merge "Move fail in testFirewallBlocking"
diff --git a/Cronet/tests/mts/Android.bp b/Cronet/tests/mts/Android.bp
index 1502b0b..d13f695 100644
--- a/Cronet/tests/mts/Android.bp
+++ b/Cronet/tests/mts/Android.bp
@@ -19,6 +19,7 @@
 
 java_genrule {
     name: "net-http-test-jarjar-rules",
+    defaults: ["CronetTestJavaDefaults"],
     tool_files: [
         ":NetHttpTestsLibPreJarJar{.jar}",
         "jarjar_excludes.txt",
@@ -36,6 +37,7 @@
 
 android_library {
     name: "NetHttpTestsLibPreJarJar",
+    defaults: ["CronetTestJavaDefaults"],
     srcs: [":cronet_aml_javatests_sources"],
     sdk_version: "module_current",
     min_sdk_version: "30",
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java b/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
index 3e02543..e0a9878 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHalHidlImpl.java
@@ -288,9 +288,12 @@
         IOffloadConfig config = null;
         try {
             config = IOffloadConfig.getService(true /*retry*/);
-        } catch (RemoteException | NoSuchElementException e) {
+        } catch (RemoteException e) {
             log.e("getIOffloadConfig error " + e);
             return null;
+        } catch (NoSuchElementException e) {
+            log.i("getIOffloadConfig Tether Offload HAL not present/implemented");
+            return null;
         }
 
         IOffloadControl control = null;
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
index ea20063..de15c5b 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
@@ -307,7 +307,7 @@
         if (mIOffload == null) {
             mIOffload = mDeps.getOffload();
             if (mIOffload == null) {
-                mLog.e("No tethering offload HAL service found.");
+                mLog.i("No tethering offload HAL service found.");
                 return OFFLOAD_HAL_VERSION_NONE;
             }
             mLog.i("Tethering offload version "
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index c181994..de25ff5 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -20,7 +20,6 @@
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
-import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
 import static android.net.TetheringManager.TETHERING_BLUETOOTH;
@@ -160,23 +159,13 @@
     }
 
     /**
-     * Returns the greater of two start times.
-     * @param first the first start time
-     * @param second the second start time
-     * @return the greater start time
-     */
-    private long getGreaterStartTime(long first, long second) {
-        return first > second ? first : second;
-    }
-
-    /**
      * Updates the upstream events builder with a new upstream event.
      * @param upstreamEventsBuilder the builder for the upstream events list
      * @param start the start time of the upstream event
      * @param stop the stop time of the upstream event
      * @param upstream the type of upstream type (e.g. Wifi, Cellular, Bluetooth, ...)
      */
-    private void updateUpstreamEvents(final UpstreamEvents.Builder upstreamEventsBuilder,
+    private void addUpstreamEvent(final UpstreamEvents.Builder upstreamEventsBuilder,
             final long start, final long stop, @Nullable final UpstreamType upstream) {
         final UpstreamEvent.Builder upstreamEventBuilder = UpstreamEvent.newBuilder()
                 .setUpstreamType(upstream == null ? UpstreamType.UT_NO_NETWORK : upstream)
@@ -201,21 +190,21 @@
      * @param downstreamStartTime the start time of the downstream event to find relevant upstream
      * events for
      */
-    private void updateStatsBuilderToWrite(final NetworkTetheringReported.Builder statsBuilder,
+    private void noteDownstreamStopped(final NetworkTetheringReported.Builder statsBuilder,
                     final long downstreamStartTime) {
         UpstreamEvents.Builder upstreamEventsBuilder = UpstreamEvents.newBuilder();
         for (RecordUpstreamEvent event : mUpstreamEventList) {
             if (downstreamStartTime > event.mStopTime) continue;
 
-            final long startTime = getGreaterStartTime(downstreamStartTime, event.mStartTime);
+            final long startTime = Math.max(downstreamStartTime, event.mStartTime);
             // Handle completed upstream events.
-            updateUpstreamEvents(upstreamEventsBuilder, startTime, event.mStopTime,
+            addUpstreamEvent(upstreamEventsBuilder, startTime, event.mStopTime,
                     event.mUpstreamType);
         }
-        final long startTime = getGreaterStartTime(downstreamStartTime, mCurrentUpStreamStartTime);
+        final long startTime = Math.max(downstreamStartTime, mCurrentUpStreamStartTime);
         final long stopTime = timeNow();
         // Handle the last upstream event.
-        updateUpstreamEvents(upstreamEventsBuilder, startTime, stopTime, mCurrentUpstream);
+        addUpstreamEvent(upstreamEventsBuilder, startTime, stopTime, mCurrentUpstream);
         statsBuilder.setUpstreamEvents(upstreamEventsBuilder);
         statsBuilder.setDurationMillis(stopTime - downstreamStartTime);
     }
@@ -237,7 +226,7 @@
             return;
         }
 
-        updateStatsBuilderToWrite(statsBuilder, mDownstreamStartTime.get(downstreamType));
+        noteDownstreamStopped(statsBuilder, mDownstreamStartTime.get(downstreamType));
         write(statsBuilder.build());
 
         mBuilderMap.remove(downstreamType);
@@ -365,34 +354,19 @@
         if (nc == null) return UpstreamType.UT_NO_NETWORK;
 
         final int typeCount = nc.getTransportTypes().length;
+        // It's possible for a VCN network to be mapped to UT_UNKNOWN, as it may consist of both
+        // Wi-Fi and cellular transport.
+        // TODO: It's necessary to define a new upstream type for VCN, which can be identified by
+        // NET_CAPABILITY_NOT_VCN_MANAGED.
+        if (typeCount > 1) return UpstreamType.UT_UNKNOWN;
 
-        boolean hasCellular = nc.hasTransport(TRANSPORT_CELLULAR);
-        boolean hasWifi = nc.hasTransport(TRANSPORT_WIFI);
-        boolean hasBT = nc.hasTransport(TRANSPORT_BLUETOOTH);
-        boolean hasEthernet = nc.hasTransport(TRANSPORT_ETHERNET);
-        boolean hasVpn = nc.hasTransport(TRANSPORT_VPN);
-        boolean hasWifiAware = nc.hasTransport(TRANSPORT_WIFI_AWARE);
-        boolean hasLopan = nc.hasTransport(TRANSPORT_LOWPAN);
+        if (nc.hasTransport(TRANSPORT_CELLULAR)) return UpstreamType.UT_CELLULAR;
+        if (nc.hasTransport(TRANSPORT_WIFI)) return UpstreamType.UT_WIFI;
+        if (nc.hasTransport(TRANSPORT_BLUETOOTH)) return UpstreamType.UT_BLUETOOTH;
+        if (nc.hasTransport(TRANSPORT_ETHERNET)) return UpstreamType.UT_ETHERNET;
+        if (nc.hasTransport(TRANSPORT_WIFI_AWARE)) return UpstreamType.UT_WIFI_AWARE;
+        if (nc.hasTransport(TRANSPORT_LOWPAN)) return UpstreamType.UT_LOWPAN;
 
-        if (typeCount == 3 && hasCellular && hasWifi && hasVpn) {
-            return UpstreamType.UT_WIFI_CELLULAR_VPN;
-        }
-
-        if (typeCount == 2 && hasVpn) {
-            if (hasCellular) return UpstreamType.UT_CELLULAR_VPN;
-            if (hasWifi) return UpstreamType.UT_WIFI_VPN;
-            if (hasBT) return UpstreamType.UT_BLUETOOTH_VPN;
-            if (hasEthernet) return UpstreamType.UT_ETHERNET_VPN;
-        }
-
-        if (typeCount == 1) {
-            if (hasCellular) return UpstreamType.UT_CELLULAR;
-            if (hasWifi) return UpstreamType.UT_WIFI;
-            if (hasBT) return UpstreamType.UT_BLUETOOTH;
-            if (hasEthernet) return UpstreamType.UT_ETHERNET;
-            if (hasWifiAware) return UpstreamType.UT_WIFI_AWARE;
-            if (hasLopan) return UpstreamType.UT_LOWPAN;
-        }
         return UpstreamType.UT_UNKNOWN;
     }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
index aa2d16c..77950ac 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/metrics/TetheringMetricsTest.java
@@ -20,7 +20,6 @@
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
 import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
-import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
 import static android.net.TetheringManager.TETHERING_BLUETOOTH;
@@ -278,17 +277,7 @@
         runUpstreamTypesTest(buildUpstreamState(TRANSPORT_ETHERNET), UpstreamType.UT_ETHERNET);
         runUpstreamTypesTest(buildUpstreamState(TRANSPORT_WIFI_AWARE), UpstreamType.UT_WIFI_AWARE);
         runUpstreamTypesTest(buildUpstreamState(TRANSPORT_LOWPAN), UpstreamType.UT_LOWPAN);
-        runUpstreamTypesTest(buildUpstreamState(TRANSPORT_CELLULAR, TRANSPORT_VPN),
-                UpstreamType.UT_CELLULAR_VPN);
-        runUpstreamTypesTest(buildUpstreamState(TRANSPORT_WIFI, TRANSPORT_VPN),
-                UpstreamType.UT_WIFI_VPN);
-        runUpstreamTypesTest(buildUpstreamState(TRANSPORT_BLUETOOTH, TRANSPORT_VPN),
-                UpstreamType.UT_BLUETOOTH_VPN);
-        runUpstreamTypesTest(buildUpstreamState(TRANSPORT_ETHERNET, TRANSPORT_VPN),
-                UpstreamType.UT_ETHERNET_VPN);
-        runUpstreamTypesTest(buildUpstreamState(TRANSPORT_CELLULAR, TRANSPORT_WIFI, TRANSPORT_VPN),
-                UpstreamType.UT_WIFI_CELLULAR_VPN);
-        runUpstreamTypesTest(buildUpstreamState(TRANSPORT_CELLULAR, TRANSPORT_WIFI, TRANSPORT_VPN,
+        runUpstreamTypesTest(buildUpstreamState(TRANSPORT_CELLULAR, TRANSPORT_WIFI,
                 TRANSPORT_BLUETOOTH), UpstreamType.UT_UNKNOWN);
     }
 
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index e068d8a..ce3315b 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -300,7 +300,8 @@
     bpf_packet_trace_ringbuf_submit(pkt);
 }
 
-static __always_inline inline bool skip_owner_match(struct __sk_buff* skb, const unsigned kver) {
+static __always_inline inline bool skip_owner_match(struct __sk_buff* skb, bool egress,
+                                                    const unsigned kver) {
     uint32_t flag = 0;
     if (skb->protocol == htons(ETH_P_IP)) {
         uint8_t proto;
@@ -330,7 +331,8 @@
     } else {
         return false;
     }
-    return flag & TCP_FLAG_RST;  // false on read failure
+    // Always allow RST's, and additionally allow ingress FINs
+    return flag & (TCP_FLAG_RST | (egress ? 0 : TCP_FLAG_FIN));  // false on read failure
 }
 
 static __always_inline inline BpfConfig getConfig(uint32_t configKey) {
@@ -352,7 +354,7 @@
                                                   bool egress, const unsigned kver) {
     if (is_system_uid(uid)) return PASS;
 
-    if (skip_owner_match(skb, kver)) return PASS;
+    if (skip_owner_match(skb, egress, kver)) return PASS;
 
     BpfConfig enabledRules = getConfig(UID_RULES_CONFIGURATION_KEY);
 
@@ -446,11 +448,6 @@
         return match;
     }
 
-    if (key.tag) {
-        update_stats_with_config(skb, egress, &key, *selectedMap);
-        key.tag = 0;
-    }
-
     do_packet_tracing(skb, egress, uid, tag, enable_tracing, kver);
     update_stats_with_config(skb, egress, &key, *selectedMap);
     update_app_uid_stats_map(skb, egress, &uid);
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
index cdcb0f8..1bc8ca5 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -109,12 +109,12 @@
     return newLine;
 }
 
-int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>* lines,
+int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>& lines,
                                        const BpfMap<StatsKey, StatsValue>& statsMap,
                                        const BpfMap<uint32_t, IfaceValue>& ifaceMap) {
     int64_t unknownIfaceBytesTotal = 0;
     const auto processDetailUidStats =
-            [lines, &unknownIfaceBytesTotal, &ifaceMap](
+            [&lines, &unknownIfaceBytesTotal, &ifaceMap](
                     const StatsKey& key,
                     const BpfMap<StatsKey, StatsValue>& statsMap) -> Result<void> {
         char ifname[IFNAMSIZ];
@@ -126,7 +126,13 @@
         if (!statsEntry.ok()) {
             return base::ResultError(statsEntry.error().message(), statsEntry.error().code());
         }
-        lines->push_back(populateStatsEntry(key, statsEntry.value(), ifname));
+        stats_line newLine = populateStatsEntry(key, statsEntry.value(), ifname);
+        lines.push_back(newLine);
+        if (newLine.tag) {
+            // account tagged traffic in the untagged stats (for historical reasons?)
+            newLine.tag = 0;
+            lines.push_back(newLine);
+        }
         return Result<void>();
     };
     Result<void> res = statsMap.iterate(processDetailUidStats);
@@ -180,7 +186,7 @@
     // TODO: the above comment feels like it may be obsolete / out of date,
     // since we no longer swap the map via netd binder rpc - though we do
     // still swap it.
-    int ret = parseBpfNetworkStatsDetailInternal(lines, *inactiveStatsMap, ifaceIndexNameMap);
+    int ret = parseBpfNetworkStatsDetailInternal(*lines, *inactiveStatsMap, ifaceIndexNameMap);
     if (ret) {
         ALOGE("parse detail network stats failed: %s", strerror(errno));
         return ret;
@@ -195,11 +201,11 @@
     return 0;
 }
 
-int parseBpfNetworkStatsDevInternal(std::vector<stats_line>* lines,
+int parseBpfNetworkStatsDevInternal(std::vector<stats_line>& lines,
                                     const BpfMap<uint32_t, StatsValue>& statsMap,
                                     const BpfMap<uint32_t, IfaceValue>& ifaceMap) {
     int64_t unknownIfaceBytesTotal = 0;
-    const auto processDetailIfaceStats = [lines, &unknownIfaceBytesTotal, &ifaceMap, &statsMap](
+    const auto processDetailIfaceStats = [&lines, &unknownIfaceBytesTotal, &ifaceMap, &statsMap](
                                              const uint32_t& key, const StatsValue& value,
                                              const BpfMap<uint32_t, StatsValue>&) {
         char ifname[IFNAMSIZ];
@@ -211,7 +217,7 @@
                 .tag = (uint32_t)TAG_NONE,
                 .counterSet = (uint32_t)SET_ALL,
         };
-        lines->push_back(populateStatsEntry(fakeKey, value, ifname));
+        lines.push_back(populateStatsEntry(fakeKey, value, ifname));
         return Result<void>();
     };
     Result<void> res = statsMap.iterateWithValue(processDetailIfaceStats);
@@ -228,29 +234,28 @@
 int parseBpfNetworkStatsDev(std::vector<stats_line>* lines) {
     static BpfMapRO<uint32_t, IfaceValue> ifaceIndexNameMap(IFACE_INDEX_NAME_MAP_PATH);
     static BpfMapRO<uint32_t, StatsValue> ifaceStatsMap(IFACE_STATS_MAP_PATH);
-    return parseBpfNetworkStatsDevInternal(lines, ifaceStatsMap, ifaceIndexNameMap);
+    return parseBpfNetworkStatsDevInternal(*lines, ifaceStatsMap, ifaceIndexNameMap);
 }
 
-void groupNetworkStats(std::vector<stats_line>* lines) {
-    if (lines->size() <= 1) return;
-    std::sort(lines->begin(), lines->end());
+void groupNetworkStats(std::vector<stats_line>& lines) {
+    if (lines.size() <= 1) return;
+    std::sort(lines.begin(), lines.end());
 
     // Similar to std::unique(), but aggregates the duplicates rather than discarding them.
-    size_t nextOutput = 0;
-    for (size_t i = 1; i < lines->size(); i++) {
-        if (lines->at(nextOutput) == lines->at(i)) {
-            lines->at(nextOutput) += lines->at(i);
+    size_t currentOutput = 0;
+    for (size_t i = 1; i < lines.size(); i++) {
+        // note that == operator only compares the 'key' portion: iface/uid/tag/set
+        if (lines[currentOutput] == lines[i]) {
+            // while += operator only affects the 'data' portion: {rx,tx}{Bytes,Packets}
+            lines[currentOutput] += lines[i];
         } else {
-            nextOutput++;
-            if (nextOutput != i) {
-                lines->at(nextOutput) = lines->at(i);
-            }
+            // okay, we're done aggregating the current line, move to the next one
+            lines[++currentOutput] = lines[i];
         }
     }
 
-    if (lines->size() != nextOutput + 1) {
-        lines->resize(nextOutput + 1);
-    }
+    // possibly shrink the vector - currentOutput is the last line with valid data
+    lines.resize(currentOutput + 1);
 }
 
 // True if lhs equals to rhs, only compare iface, uid, tag and set.
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
index bf42b62..ccd3f5e 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
@@ -228,7 +228,7 @@
     populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
     populateFakeStats(TEST_UID1, 0, IFACE_INDEX2, TEST_COUNTERSET1, value1, mFakeStatsMap);
     populateFakeStats(TEST_UID2, 0, IFACE_INDEX3, TEST_COUNTERSET1, value1, mFakeStatsMap);
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
     ASSERT_EQ((unsigned long)3, lines.size());
 }
 
@@ -290,8 +290,8 @@
                       mFakeStatsMap);
     populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
     std::vector<stats_line> lines;
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
-    ASSERT_EQ((unsigned long)4, lines.size());
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((unsigned long)7, lines.size());
 }
 
 TEST_F(BpfNetworkStatsHelperTest, TestGetStatsWithSkippedIface) {
@@ -310,7 +310,7 @@
     populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET1, value1, mFakeStatsMap);
     populateFakeStats(TEST_UID2, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
     std::vector<stats_line> lines;
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
     ASSERT_EQ((unsigned long)4, lines.size());
 }
 
@@ -349,7 +349,7 @@
     ASSERT_EQ(-1, unknownIfaceBytesTotal);
     std::vector<stats_line> lines;
     // TODO: find a way to test the total of unknown Iface Bytes go above limit.
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
     ASSERT_EQ((unsigned long)1, lines.size());
     expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines.front());
 }
@@ -381,7 +381,7 @@
     EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value2, BPF_ANY));
     std::vector<stats_line> lines;
     ASSERT_EQ(0,
-              parseBpfNetworkStatsDevInternal(&lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+              parseBpfNetworkStatsDevInternal(lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
     ASSERT_EQ((unsigned long)4, lines.size());
 
     expectStatsLineEqual(value1, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
@@ -409,25 +409,32 @@
             .txPackets = TEST_PACKET0,
             .txBytes = TEST_BYTES0,
     };
-    StatsValue value3 = {
+    StatsValue value3 = {  // value1 *2
             .rxPackets = TEST_PACKET0 * 2,
             .rxBytes = TEST_BYTES0 * 2,
             .txPackets = TEST_PACKET1 * 2,
             .txBytes = TEST_BYTES1 * 2,
     };
+    StatsValue value5 = {  // value2 + value3
+            .rxPackets = TEST_PACKET1 + TEST_PACKET0 * 2,
+            .rxBytes = TEST_BYTES1 + TEST_BYTES0 * 2,
+            .txPackets = TEST_PACKET0 + TEST_PACKET1 * 2,
+            .txBytes = TEST_BYTES0 + TEST_BYTES1 * 2,
+    };
 
     std::vector<stats_line> lines;
 
     // Test empty stats.
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
     ASSERT_EQ((size_t) 0, lines.size());
     lines.clear();
 
     // Test 1 line stats.
     populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
-    ASSERT_EQ((size_t) 1, lines.size());
-    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[0]);
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 2, lines.size());  // TEST_TAG != 0 -> 1 entry becomes 2 lines
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines[0]);
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[1]);
     lines.clear();
 
     // These items should not be grouped.
@@ -436,23 +443,27 @@
     populateFakeStats(TEST_UID1, TEST_TAG + 1, IFACE_INDEX1, TEST_COUNTERSET0, value2,
                       mFakeStatsMap);
     populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
-    ASSERT_EQ((size_t) 5, lines.size());
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 9, lines.size());
     lines.clear();
 
     // These items should be grouped.
     populateFakeStats(TEST_UID1, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1, mFakeStatsMap);
     populateFakeStats(TEST_UID2, TEST_TAG, IFACE_INDEX3, TEST_COUNTERSET0, value1, mFakeStatsMap);
 
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
-    ASSERT_EQ((size_t) 5, lines.size());
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 9, lines.size());
 
     // Verify Sorted & Grouped.
-    expectStatsLineEqual(value3, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[0]);
-    expectStatsLineEqual(value2, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET1, TEST_TAG, lines[1]);
-    expectStatsLineEqual(value2, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG + 1, lines[2]);
-    expectStatsLineEqual(value3, IFACE_NAME1, TEST_UID2, TEST_COUNTERSET0, TEST_TAG, lines[3]);
-    expectStatsLineEqual(value2, IFACE_NAME2, TEST_UID1, TEST_COUNTERSET0, TEST_TAG, lines[4]);
+    expectStatsLineEqual(value5, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0,            lines[0]);
+    expectStatsLineEqual(value2, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET1, 0,            lines[1]);
+    expectStatsLineEqual(value3, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG,     lines[2]);
+    expectStatsLineEqual(value2, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET1, TEST_TAG,     lines[3]);
+    expectStatsLineEqual(value2, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, TEST_TAG + 1, lines[4]);
+    expectStatsLineEqual(value3, IFACE_NAME1, TEST_UID2, TEST_COUNTERSET0, 0,            lines[5]);
+    expectStatsLineEqual(value3, IFACE_NAME1, TEST_UID2, TEST_COUNTERSET0, TEST_TAG,     lines[6]);
+    expectStatsLineEqual(value2, IFACE_NAME2, TEST_UID1, TEST_COUNTERSET0, 0,            lines[7]);
+    expectStatsLineEqual(value2, IFACE_NAME2, TEST_UID1, TEST_COUNTERSET0, TEST_TAG,     lines[8]);
     lines.clear();
 
     // Perform test on IfaceStats.
@@ -466,7 +477,7 @@
     EXPECT_RESULT_OK(mFakeIfaceStatsMap.writeValue(ifaceStatsKey, value1, BPF_ANY));
 
     ASSERT_EQ(0,
-              parseBpfNetworkStatsDevInternal(&lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
+              parseBpfNetworkStatsDevInternal(lines, mFakeIfaceStatsMap, mFakeIfaceIndexNameMap));
     ASSERT_EQ((size_t) 2, lines.size());
 
     expectStatsLineEqual(value3, IFACE_NAME1, UID_ALL, SET_ALL, TAG_NONE, lines[0]);
@@ -485,39 +496,48 @@
             .txPackets = TEST_PACKET1,
             .txBytes = TEST_BYTES1,
     };
+    StatsValue value4 = {  // value1 * 4
+            .rxPackets = TEST_PACKET0 * 4,
+            .rxBytes = TEST_BYTES0 * 4,
+            .txPackets = TEST_PACKET1 * 4,
+            .txBytes = TEST_BYTES1 * 4,
+    };
 
     // Mutate uid, 0 < TEST_UID1 < INT_MAX < INT_MIN < UINT_MAX.
-    populateFakeStats(0, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
-    populateFakeStats(UINT_MAX, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
-    populateFakeStats(INT_MIN, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
-    populateFakeStats(INT_MAX, TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(0,         TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(UINT_MAX,  TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(INT_MIN,   TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(INT_MAX,   TEST_TAG, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
 
     // Mutate tag, 0 < TEST_TAG < INT_MAX < INT_MIN < UINT_MAX.
-    populateFakeStats(TEST_UID1, INT_MAX, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
-    populateFakeStats(TEST_UID1, INT_MIN, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
-    populateFakeStats(TEST_UID1, 0, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, INT_MAX,  IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, INT_MIN,  IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
+    populateFakeStats(TEST_UID1, 0,        IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
     populateFakeStats(TEST_UID1, UINT_MAX, IFACE_INDEX1, TEST_COUNTERSET0, value1, mFakeStatsMap);
 
     // TODO: Mutate counterSet and enlarge TEST_MAP_SIZE if overflow on counterSet is possible.
 
     std::vector<stats_line> lines;
-    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(&lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
-    ASSERT_EQ((size_t) 8, lines.size());
+    ASSERT_EQ(0, parseBpfNetworkStatsDetailInternal(lines, mFakeStatsMap, mFakeIfaceIndexNameMap));
+    ASSERT_EQ((size_t) 12, lines.size());
 
     // Uid 0 first
-    expectStatsLineEqual(value1, IFACE_NAME1, 0, TEST_COUNTERSET0, TEST_TAG, lines[0]);
+    expectStatsLineEqual(value1, IFACE_NAME1, 0,         TEST_COUNTERSET0, 0,        lines[0]);
+    expectStatsLineEqual(value1, IFACE_NAME1, 0,         TEST_COUNTERSET0, TEST_TAG, lines[1]);
 
     // Test uid, mutate tag.
-    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0, lines[1]);
-    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, INT_MAX, lines[2]);
-    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, INT_MIN, lines[3]);
-    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, UINT_MAX, lines[4]);
+    expectStatsLineEqual(value4, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, 0,        lines[2]);
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, INT_MAX,  lines[3]);
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, INT_MIN,  lines[4]);
+    expectStatsLineEqual(value1, IFACE_NAME1, TEST_UID1, TEST_COUNTERSET0, UINT_MAX, lines[5]);
 
     // Mutate uid.
-    expectStatsLineEqual(value1, IFACE_NAME1, INT_MAX, TEST_COUNTERSET0, TEST_TAG, lines[5]);
-    expectStatsLineEqual(value1, IFACE_NAME1, INT_MIN, TEST_COUNTERSET0, TEST_TAG, lines[6]);
-    expectStatsLineEqual(value1, IFACE_NAME1, UINT_MAX, TEST_COUNTERSET0, TEST_TAG, lines[7]);
-    lines.clear();
+    expectStatsLineEqual(value1, IFACE_NAME1, INT_MAX,   TEST_COUNTERSET0, 0,        lines[6]);
+    expectStatsLineEqual(value1, IFACE_NAME1, INT_MAX,   TEST_COUNTERSET0, TEST_TAG, lines[7]);
+    expectStatsLineEqual(value1, IFACE_NAME1, INT_MIN,   TEST_COUNTERSET0, 0,        lines[8]);
+    expectStatsLineEqual(value1, IFACE_NAME1, INT_MIN,   TEST_COUNTERSET0, TEST_TAG, lines[9]);
+    expectStatsLineEqual(value1, IFACE_NAME1, UINT_MAX,  TEST_COUNTERSET0, 0,        lines[10]);
+    expectStatsLineEqual(value1, IFACE_NAME1, UINT_MAX,  TEST_COUNTERSET0, TEST_TAG, lines[11]);
 }
 }  // namespace bpf
 }  // namespace android
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
index 1ffa927..133009f 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/BpfNetworkStats.h
@@ -63,7 +63,7 @@
                              const BpfMap<uint32_t, StatsValue>& ifaceStatsMap,
                              const BpfMap<uint32_t, IfaceValue>& ifaceNameMap);
 // For test only
-int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>* lines,
+int parseBpfNetworkStatsDetailInternal(std::vector<stats_line>& lines,
                                        const BpfMap<StatsKey, StatsValue>& statsMap,
                                        const BpfMap<uint32_t, IfaceValue>& ifaceMap);
 // For test only
@@ -106,7 +106,7 @@
 }
 
 // For test only
-int parseBpfNetworkStatsDevInternal(std::vector<stats_line>* lines,
+int parseBpfNetworkStatsDevInternal(std::vector<stats_line>& lines,
                                     const BpfMap<uint32_t, StatsValue>& statsMap,
                                     const BpfMap<uint32_t, IfaceValue>& ifaceMap);
 
@@ -115,7 +115,7 @@
 int parseBpfNetworkStatsDetail(std::vector<stats_line>* lines);
 
 int parseBpfNetworkStatsDev(std::vector<stats_line>* lines);
-void groupNetworkStats(std::vector<stats_line>* lines);
+void groupNetworkStats(std::vector<stats_line>& lines);
 int cleanStatsMap();
 }  // namespace bpf
 }  // namespace android
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index f87804b..5298aef 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -174,6 +174,7 @@
             this.searchOptions = searchOptions;
             if (listeners.put(listener, searchOptions) == null) {
                 for (MdnsResponse existingResponse : instanceNameToResponse.values()) {
+                    if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
                     final MdnsServiceInfo info =
                             buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
                     listener.onServiceNameDiscovered(info);
@@ -199,6 +200,13 @@
         }
     }
 
+    private boolean responseMatchesOptions(@NonNull MdnsResponse response,
+            @NonNull MdnsSearchOptions options) {
+        if (options.getResolveInstanceName() == null) return true;
+        // DNS is case-insensitive, so ignore case in the comparison
+        return options.getResolveInstanceName().equalsIgnoreCase(response.getServiceInstanceName());
+    }
+
     /**
      * Unregisters {@code listener} from receiving discovery event of mDNS service instances.
      *
@@ -274,6 +282,7 @@
                 buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
 
         for (int i = 0; i < listeners.size(); i++) {
+            if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
             final MdnsServiceBrowserListener listener = listeners.keyAt(i);
             if (newServiceFound) {
                 listener.onServiceNameDiscovered(serviceInfo);
@@ -295,6 +304,7 @@
             return;
         }
         for (int i = 0; i < listeners.size(); i++) {
+            if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
             final MdnsServiceBrowserListener listener = listeners.keyAt(i);
             final MdnsServiceInfo serviceInfo =
                     buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
@@ -512,6 +522,10 @@
                                 == 0) {
                             iter.remove();
                             for (int i = 0; i < listeners.size(); i++) {
+                                if (!responseMatchesOptions(existingResponse,
+                                        listeners.valueAt(i)))  {
+                                    continue;
+                                }
                                 final MdnsServiceBrowserListener listener = listeners.keyAt(i);
                                 String serviceInstanceName =
                                         existingResponse.getServiceInstanceName();
diff --git a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
index 19f2fb8..e63e423 100644
--- a/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
+++ b/service/jni/com_android_server_connectivity_ClatCoordinator.cpp
@@ -233,7 +233,7 @@
     }
 
     struct ifreq ifr = {
-            .ifr_flags = IFF_TUN,
+            .ifr_flags = static_cast<short>(IFF_TUN | IFF_TUN_EXCL),
     };
     strlcpy(ifr.ifr_name, v4interface.c_str(), sizeof(ifr.ifr_name));
 
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 8786638..9d234d3 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -2176,15 +2176,12 @@
                 c -> c instanceof CallbackEntry.Available);
     }
 
-    private void waitForAvailable(
+    private void waitForTransport(
             @NonNull final TestableNetworkCallback cb, final int expectedTransport) {
-        cb.eventuallyExpect(
-                CallbackEntry.AVAILABLE, NETWORK_CALLBACK_TIMEOUT_MS,
-                entry -> {
-                    final NetworkCapabilities nc = mCm.getNetworkCapabilities(entry.getNetwork());
-                    return nc.hasTransport(expectedTransport);
-                }
-        );
+        cb.eventuallyExpect(CallbackEntry.NETWORK_CAPS_UPDATED,
+                NETWORK_CALLBACK_TIMEOUT_MS,
+                entry -> ((CallbackEntry.CapabilitiesChanged) entry).getCaps()
+                        .hasTransport(expectedTransport));
     }
 
     private void waitForAvailable(
@@ -2672,7 +2669,8 @@
 
             // Validate that an unmetered network is used over other networks.
             waitForAvailable(defaultCallback, wifiNetwork);
-            waitForAvailable(systemDefaultCallback, wifiNetwork);
+            systemDefaultCallback.eventuallyExpect(CallbackEntry.AVAILABLE,
+                    NETWORK_CALLBACK_TIMEOUT_MS, cb -> wifiNetwork.equals(cb.getNetwork()));
 
             // Validate that when setting unmetered to metered, unmetered is lost and replaced by
             // the network with the TEST transport. Also wait for validation here, in case there
@@ -2684,11 +2682,14 @@
             // callbacks may be received. Eventually, metered Wi-Fi should be the final available
             // callback in any case therefore confirm its receipt before continuing to assure the
             // system is in the expected state.
-            waitForAvailable(systemDefaultCallback, TRANSPORT_WIFI);
+            waitForTransport(systemDefaultCallback, TRANSPORT_WIFI);
         }, /* cleanup */ () -> {
-            // Validate that removing the test network will fallback to the default network.
+                // Validate that removing the test network will fallback to the default network.
                 runWithShellPermissionIdentity(tnt::teardown);
-                defaultCallback.expect(CallbackEntry.LOST, tnt, NETWORK_CALLBACK_TIMEOUT_MS);
+                // The other callbacks (LP or NC changes) would receive before LOST callback. Use
+                // eventuallyExpect to check callback for avoiding test flake.
+                defaultCallback.eventuallyExpect(CallbackEntry.LOST, NETWORK_CALLBACK_TIMEOUT_MS,
+                        lost -> tnt.getNetwork().equals(lost.getNetwork()));
                 waitForAvailable(defaultCallback);
             }, /* cleanup */ () -> {
                 setWifiMeteredStatusAndWait(ssid, oldMeteredValue, false /* waitForValidation */);
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 5227c2f..c599d9d 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -1656,14 +1656,14 @@
                 ArgumentCaptor.forClass(IkeSessionCallback.class);
 
         // Verify retry is scheduled
-        final long expectedDelay = mTestDeps.getNextRetryDelaySeconds(retryIndex) * 1000;
+        final long expectedDelayMs = mTestDeps.getNextRetryDelayMs(retryIndex);
         final ArgumentCaptor<Long> delayCaptor = ArgumentCaptor.forClass(Long.class);
         verify(mExecutor, atLeastOnce()).schedule(any(Runnable.class), delayCaptor.capture(),
                 eq(TimeUnit.MILLISECONDS));
         final List<Long> delays = delayCaptor.getAllValues();
-        assertEquals(expectedDelay, (long) delays.get(delays.size() - 1));
+        assertEquals(expectedDelayMs, (long) delays.get(delays.size() - 1));
 
-        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS + expectedDelay))
+        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS + expectedDelayMs))
                 .createIkeSession(any(), any(), any(), any(), ikeCbCaptor.capture(), any());
 
         // Forget the mIkev2SessionCreator#createIkeSession call and mExecutor#schedule call
@@ -2966,9 +2966,9 @@
         }
 
         @Override
-        public long getNextRetryDelaySeconds(int retryCount) {
+        public long getNextRetryDelayMs(int retryCount) {
             // Simply return retryCount as the delay seconds for retrying.
-            return retryCount;
+            return retryCount * 1000;
         }
 
         @Override
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index 5d58f5d..746994f 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -992,6 +992,71 @@
                 mockNetwork);
     }
 
+    @Test
+    public void testProcessResponse_ResolveExcludesOtherServices() {
+        client = new MdnsServiceTypeClient(
+                SERVICE_TYPE, mockSocketClient, currentThreadExecutor, mockNetwork);
+
+        final String requestedInstance = "instance1";
+        final String otherInstance = "instance2";
+        final String ipV4Address = "192.0.2.0";
+        final String ipV6Address = "2001:db8::";
+
+        final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
+                // Use different case in the options
+                .setResolveInstanceName("Instance1").build();
+
+        client.startSendAndReceive(mockListenerOne, resolveOptions);
+        client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions());
+
+        // Complete response from instanceName
+        client.processResponse(createResponse(
+                requestedInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+                        Collections.emptyMap() /* textAttributes */, TEST_TTL),
+                INTERFACE_INDEX, mockNetwork);
+
+        // Complete response from otherInstanceName
+        client.processResponse(createResponse(
+                otherInstance, ipV4Address, 5353, SERVICE_TYPE_LABELS,
+                        Collections.emptyMap() /* textAttributes */, TEST_TTL),
+                INTERFACE_INDEX, mockNetwork);
+
+        // Address update from otherInstanceName
+        client.processResponse(createResponse(
+                otherInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
+                Collections.emptyMap(), TEST_TTL), INTERFACE_INDEX, mockNetwork);
+
+        // Goodbye from otherInstanceName
+        client.processResponse(createResponse(
+                otherInstance, ipV6Address, 5353, SERVICE_TYPE_LABELS,
+                Collections.emptyMap(), 0L /* ttl */), INTERFACE_INDEX, mockNetwork);
+
+        // mockListenerOne gets notified for the requested instance
+        verify(mockListenerOne).onServiceNameDiscovered(matchServiceName(requestedInstance));
+        verify(mockListenerOne).onServiceFound(matchServiceName(requestedInstance));
+
+        // ...but does not get any callback for the other instance
+        verify(mockListenerOne, never()).onServiceFound(matchServiceName(otherInstance));
+        verify(mockListenerOne, never()).onServiceNameDiscovered(matchServiceName(otherInstance));
+        verify(mockListenerOne, never()).onServiceUpdated(matchServiceName(otherInstance));
+        verify(mockListenerOne, never()).onServiceRemoved(matchServiceName(otherInstance));
+
+        // mockListenerTwo gets notified for both though
+        final InOrder inOrder = inOrder(mockListenerTwo);
+        inOrder.verify(mockListenerTwo).onServiceNameDiscovered(
+                matchServiceName(requestedInstance));
+        inOrder.verify(mockListenerTwo).onServiceFound(matchServiceName(requestedInstance));
+
+        inOrder.verify(mockListenerTwo).onServiceNameDiscovered(matchServiceName(otherInstance));
+        inOrder.verify(mockListenerTwo).onServiceFound(matchServiceName(otherInstance));
+        inOrder.verify(mockListenerTwo).onServiceUpdated(matchServiceName(otherInstance));
+        inOrder.verify(mockListenerTwo).onServiceRemoved(matchServiceName(otherInstance));
+    }
+
+    private static MdnsServiceInfo matchServiceName(String name) {
+        return argThat(info -> info.getServiceInstanceName().equals(name));
+    }
+
     // verifies that the right query was enqueued with the right delay, and send query by executing
     // the runnable.
     private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse) {