Merge changes I2e556cf5,I5e42005f,I7b766ef6 into main

* changes:
  Remove unused method TetheringConfiguration#isRandomPrefixBaseEnabled()
  PrivateAddressCoordinator: introduce Dependencies class
  PrivateAddressCoordinator: move the special handling of WiFi P2P dedicated IP to IpServer
diff --git a/Cronet/OWNERS b/Cronet/OWNERS
deleted file mode 100644
index c24680e..0000000
--- a/Cronet/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-set noparent
-file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 94adc5b..b773ed8 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -316,6 +316,14 @@
         }
       ]
     },
+    {
+      "name": "CtsHostsideNetworkTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+      "options": [
+        {
+          "exclude-annotation": "androidx.test.filters.RequiresDevice"
+        }
+      ]
+    },
     // Test with APK modules only, in cases where APEX is not supported, or the other modules
     // were simply not updated
     {
@@ -414,15 +422,6 @@
           "exclude-annotation": "androidx.test.filters.RequiresDevice"
         }
       ]
-    },
-    // TODO: upgrade to presubmit. Postsubmit on virtual devices to monitor flakiness only.
-    {
-      "name": "CtsHostsideNetworkTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
-      "options": [
-        {
-          "exclude-annotation": "androidx.test.filters.RequiresDevice"
-        }
-      ]
     }
   ],
   "imports": [
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 15ad226..5cf5528 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -204,6 +204,7 @@
     use_embedded_native_libs: true,
     privapp_allowlist: ":privapp_allowlist_com.android.tethering",
     apex_available: ["com.android.tethering"],
+    updatable: true,
 }
 
 android_app {
@@ -221,6 +222,7 @@
     lint: {
         error_checks: ["NewApi"],
     },
+    updatable: true,
 }
 
 sdk {
diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
index 0df9047..af061e4 100644
--- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -198,4 +198,13 @@
     public String toString() {
         return "Netd used";
     }
+
+    @Override
+    public int getLastMaxConnectionAndResetToCurrent() {
+        return 0;
+    }
+
+    @Override
+    public void clearConnectionCounters() {
+    }
 }
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index e6e99f4..b460f0d 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -19,6 +19,7 @@
 import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
 
 import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
+import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_ACTIVE_SESSIONS_METRICS;
 
 import android.system.ErrnoException;
 import android.system.Os;
@@ -108,6 +109,22 @@
     // TODO: Add IPv6 rule count.
     private final SparseArray<Integer> mRule4CountOnUpstream = new SparseArray<>();
 
+    private final boolean mSupportActiveSessionsMetrics;
+    /**
+     * Tracks the current number of tethering connections and the maximum
+     * observed since the last metrics collection. Used to provide insights
+     * into the distribution of active tethering sessions for metrics reporting.
+
+     * These variables are accessed on the handler thread, which includes:
+     *  1. ConntrackEvents signaling the addition or removal of an IPv4 rule.
+     *  2. ConntrackEvents indicating the removal of a tethering client,
+     *     triggering the removal of associated rules.
+     *  3. Removal of the last IpServer, which resets counters to handle
+     *     potential synchronization issues.
+     */
+    private int mLastMaxConnectionCount = 0;
+    private int mCurrentConnectionCount = 0;
+
     public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) {
         mLog = deps.getSharedLog().forSubComponent(TAG);
 
@@ -156,6 +173,9 @@
         } catch (ErrnoException e) {
             mLog.e("Could not clear mBpfDevMap: " + e);
         }
+
+        mSupportActiveSessionsMetrics = deps.isFeatureEnabled(deps.getContext(),
+                TETHER_ACTIVE_SESSIONS_METRICS);
     }
 
     @Override
@@ -350,6 +370,12 @@
                 final int upstreamIfindex = (int) key.iif;
                 int count = mRule4CountOnUpstream.get(upstreamIfindex, 0 /* default */);
                 mRule4CountOnUpstream.put(upstreamIfindex, ++count);
+
+                if (mSupportActiveSessionsMetrics) {
+                    mCurrentConnectionCount++;
+                    mLastMaxConnectionCount = Math.max(mCurrentConnectionCount,
+                            mLastMaxConnectionCount);
+                }
             } else {
                 mBpfUpstream4Map.insertEntry(key, value);
             }
@@ -385,6 +411,10 @@
                 } else {
                     mRule4CountOnUpstream.put(upstreamIfindex, count);
                 }
+
+                if (mSupportActiveSessionsMetrics) {
+                    mCurrentConnectionCount--;
+                }
             } else {
                 if (!mBpfUpstream4Map.deleteEntry(key)) return false;  // Rule did not exist
             }
@@ -465,14 +495,16 @@
 
     @Override
     public String toString() {
-        return String.join(", ", new String[] {
-                mapStatus(mBpfDownstream6Map, "mBpfDownstream6Map"),
-                mapStatus(mBpfUpstream6Map, "mBpfUpstream6Map"),
-                mapStatus(mBpfDownstream4Map, "mBpfDownstream4Map"),
-                mapStatus(mBpfUpstream4Map, "mBpfUpstream4Map"),
-                mapStatus(mBpfStatsMap, "mBpfStatsMap"),
-                mapStatus(mBpfLimitMap, "mBpfLimitMap"),
-                mapStatus(mBpfDevMap, "mBpfDevMap")
+        return String.join(", ", new String[]{
+            mapStatus(mBpfDownstream6Map, "mBpfDownstream6Map"),
+            mapStatus(mBpfUpstream6Map, "mBpfUpstream6Map"),
+            mapStatus(mBpfDownstream4Map, "mBpfDownstream4Map"),
+            mapStatus(mBpfUpstream4Map, "mBpfUpstream4Map"),
+            mapStatus(mBpfStatsMap, "mBpfStatsMap"),
+            mapStatus(mBpfLimitMap, "mBpfLimitMap"),
+            mapStatus(mBpfDevMap, "mBpfDevMap"),
+            "mCurrentConnectionCount=" + mCurrentConnectionCount,
+            "mLastMaxConnectionCount=" + mLastMaxConnectionCount
         });
     }
 
@@ -507,4 +539,17 @@
 
         return 0;
     }
+
+    /** Get last max connection count and reset to current count. */
+    public int getLastMaxConnectionAndResetToCurrent() {
+        final int ret = mLastMaxConnectionCount;
+        mLastMaxConnectionCount = mCurrentConnectionCount;
+        return ret;
+    }
+
+    /** Clear current connection count. */
+    public void clearConnectionCounters() {
+        mCurrentConnectionCount = 0;
+        mLastMaxConnectionCount = 0;
+    }
 }
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
index 026b1c3..cb8bcc9 100644
--- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -202,5 +202,11 @@
      * Remove interface index mapping.
      */
     public abstract boolean removeDevMap(int ifIndex);
+
+    /** Get last max connection count and reset to current count. */
+    public abstract int getLastMaxConnectionAndResetToCurrent();
+
+    /** Clear current connection count. */
+    public abstract void clearConnectionCounters();
 }
 
diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
index 5aca642..411971d 100644
--- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
+++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java
@@ -430,7 +430,7 @@
     // Used to dispatch legacy ConnectivityManager methods that expect tethering to be able to
     // return results and perform operations synchronously.
     // TODO: remove once there are no callers of these legacy methods.
-    private class RequestDispatcher {
+    private static class RequestDispatcher {
         private final ConditionVariable mWaiting;
         public volatile int mRemoteResult;
 
@@ -446,8 +446,8 @@
             mWaiting = new ConditionVariable();
         }
 
-        int waitForResult(final RequestHelper request) {
-            getConnector(c -> request.runRequest(c, mListener));
+        int waitForResult(final RequestHelper request, final TetheringManager mgr) {
+            mgr.getConnector(c -> request.runRequest(c, mListener));
             if (!mWaiting.block(DEFAULT_TIMEOUT_MS)) {
                 throw new IllegalStateException("Callback timeout");
             }
@@ -603,7 +603,7 @@
             } catch (RemoteException e) {
                 throw new IllegalStateException(e);
             }
-        });
+        }, this);
     }
 
     /**
@@ -635,7 +635,7 @@
             } catch (RemoteException e) {
                 throw new IllegalStateException(e);
             }
-        });
+        }, this);
     }
 
     /**
@@ -663,7 +663,7 @@
             } catch (RemoteException e) {
                 throw new IllegalStateException(e);
             }
-        });
+        }, this);
     }
 
     /**
@@ -1751,7 +1751,7 @@
             } catch (RemoteException e) {
                 throw new IllegalStateException(e);
             }
-        });
+        }, this);
 
         return ret == TETHER_ERROR_NO_ERROR;
     }
@@ -1800,6 +1800,6 @@
             } catch (RemoteException e) {
                 throw new IllegalStateException(e);
             }
-        });
+        }, this);
     }
 }
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index dc8d07a..7a6df88 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -1159,6 +1159,7 @@
                 case CMD_SERVICE_FAILED_TO_START:
                     mLog.e("start serving fail, error: " + message.arg1);
                     transitionTo(mInitialState);
+                    break;
                 default:
                     return false;
             }
@@ -1404,8 +1405,28 @@
         @Override
         public void enter() {
             mLastError = TETHER_ERROR_NO_ERROR;
+            // TODO: clean this up after the synchronous state machine is fully rolled out. Clean up
+            // can be directly triggered after calling IpServer.stop() inside Tethering.java.
             sendInterfaceState(STATE_UNAVAILABLE);
         }
+
+        @Override
+        public boolean processMessage(Message message) {
+            switch (message.what) {
+                case CMD_IPV6_TETHER_UPDATE:
+                    // sendInterfaceState(STATE_UNAVAILABLE) triggers
+                    // handleInterfaceServingStateInactive which in turn cleans up IPv6 tethering
+                    // (and calls into IpServer one more time). At this point, this is the only
+                    // message we potentially see in this state because this IpServer has already
+                    // been removed from mTetherStates before transitioning to this State; however,
+                    // handleInterfaceServiceStateInactive passes a reference.
+                    // TODO: This can be removed once SyncStateMachine is rolled out and the
+                    // teardown path is cleaned up.
+                    return true;
+                default:
+                    return false;
+            }
+        }
     }
 
     class WaitingForRestartState extends State {
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 89e06da..75ab9ec 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -27,6 +27,7 @@
 import static android.system.OsConstants.ETH_P_IP;
 import static android.system.OsConstants.ETH_P_IPV6;
 
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_MIN_MTU;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
 import static com.android.net.module.util.ip.ConntrackMonitor.ConntrackEvent;
@@ -334,7 +335,6 @@
     };
 
     // TODO: add BpfMap<TetherDownstream64Key, TetherDownstream64Value> retrieving function.
-    @VisibleForTesting
     public abstract static class Dependencies {
         /** Get handler. */
         @NonNull public abstract Handler getHandler();
@@ -585,14 +585,10 @@
             if (mHandler.hasCallbacks(mScheduledConntrackMetricsSampling)) {
                 mHandler.removeCallbacks(mScheduledConntrackMetricsSampling);
             }
-            final int currentCount = mBpfConntrackEventConsumer.getCurrentConnectionCount();
-            if (currentCount != 0) {
-                Log.wtf(TAG, "Unexpected CurrentConnectionCount: " + currentCount);
-            }
             // Avoid sending metrics when tethering is about to close.
             // This leads to a missing final sample before disconnect
             // but avoids possibly duplicating the last metric in the upload.
-            mBpfConntrackEventConsumer.clearConnectionCounters();
+            mBpfCoordinatorShim.clearConnectionCounters();
         }
         // Stop scheduled polling stats and poll the latest stats from BPF maps.
         if (mHandler.hasCallbacks(mScheduledPollingStats)) {
@@ -1091,10 +1087,6 @@
         for (final Tether4Key k : deleteDownstreamRuleKeys) {
             mBpfCoordinatorShim.tetherOffloadRuleRemove(DOWNSTREAM, k);
         }
-        if (mSupportActiveSessionsMetrics) {
-            mBpfConntrackEventConsumer.decreaseCurrentConnectionCount(
-                    deleteUpstreamRuleKeys.size());
-        }
 
         // Cleanup each upstream interface by a set which avoids duplicated work on the same
         // upstream interface. Cleaning up the same interface twice (or more) here may raise
@@ -1367,10 +1359,6 @@
 
         pw.println();
         pw.println("mSupportActiveSessionsMetrics: " + mSupportActiveSessionsMetrics);
-        pw.println("getLastMaxConnectionCount: "
-                + mBpfConntrackEventConsumer.getLastMaxConnectionCount());
-        pw.println("getCurrentConnectionCount: "
-                + mBpfConntrackEventConsumer.getCurrentConnectionCount());
     }
 
     private void dumpStats(@NonNull IndentingPrintWriter pw) {
@@ -2062,21 +2050,6 @@
     // while TCP status is established.
     @VisibleForTesting
     class BpfConntrackEventConsumer implements ConntrackEventConsumer {
-        /**
-         * Tracks the current number of tethering connections and the maximum
-         * observed since the last metrics collection. Used to provide insights
-         * into the distribution of active tethering sessions for metrics reporting.
-
-         * These variables are accessed on the handler thread, which includes:
-         *  1. ConntrackEvents signaling the addition or removal of an IPv4 rule.
-         *  2. ConntrackEvents indicating the removal of a tethering client,
-         *     triggering the removal of associated rules.
-         *  3. Removal of the last IpServer, which resets counters to handle
-         *     potential synchronization issues.
-         */
-        private int mLastMaxConnectionCount = 0;
-        private int mCurrentConnectionCount = 0;
-
         // The upstream4 and downstream4 rules are built as the following tables. Only raw ip
         // upstream interface is supported. Note that the field "lastUsed" is only updated by
         // BPF program which records the last used time for a given rule.
@@ -2210,10 +2183,6 @@
                     return;
                 }
 
-                if (mSupportActiveSessionsMetrics) {
-                    decreaseCurrentConnectionCount(1);
-                }
-
                 maybeClearLimit(upstreamIndex);
                 return;
             }
@@ -2237,40 +2206,12 @@
                         + ", downstream: " + addedDownstream + ")");
                 return;
             }
-            if (mSupportActiveSessionsMetrics && addedUpstream && addedDownstream) {
-                mCurrentConnectionCount++;
-                mLastMaxConnectionCount = Math.max(mCurrentConnectionCount,
-                        mLastMaxConnectionCount);
-            }
         }
+    }
 
-        public int getLastMaxConnectionAndResetToCurrent() {
-            final int ret = mLastMaxConnectionCount;
-            mLastMaxConnectionCount = mCurrentConnectionCount;
-            return ret;
-        }
-
-        /** For dumping current state only. */
-        public int getLastMaxConnectionCount() {
-            return mLastMaxConnectionCount;
-        }
-
-        public int getCurrentConnectionCount() {
-            return mCurrentConnectionCount;
-        }
-
-        public void decreaseCurrentConnectionCount(int count) {
-            mCurrentConnectionCount -= count;
-            if (mCurrentConnectionCount < 0) {
-                Log.wtf(TAG, "Unexpected mCurrentConnectionCount: "
-                        + mCurrentConnectionCount);
-            }
-        }
-
-        public void clearConnectionCounters() {
-            mCurrentConnectionCount = 0;
-            mLastMaxConnectionCount = 0;
-        }
+    @VisibleForTesting(visibility = PRIVATE)
+    public int getLastMaxConnectionAndResetToCurrent() {
+        return mBpfCoordinatorShim.getLastMaxConnectionAndResetToCurrent();
     }
 
     @VisibleForTesting
@@ -2611,7 +2552,7 @@
 
     private void uploadConntrackMetricsSample() {
         mDeps.sendTetheringActiveSessionsReported(
-                mBpfConntrackEventConsumer.getLastMaxConnectionAndResetToCurrent());
+                mBpfCoordinatorShim.getLastMaxConnectionAndResetToCurrent());
     }
 
     private void schedulePollingStats() {
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index fc50faf..a744953 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -62,7 +62,6 @@
 import android.net.NetworkTemplate;
 import android.os.Handler;
 import android.os.HandlerThread;
-import android.os.Looper;
 import android.stats.connectivity.DownstreamType;
 import android.stats.connectivity.ErrorCode;
 import android.stats.connectivity.UpstreamType;
@@ -111,7 +110,11 @@
     private final SparseArray<NetworkTetheringReported.Builder> mBuilderMap = new SparseArray<>();
     private final SparseArray<Long> mDownstreamStartTime = new SparseArray<Long>();
     private final ArrayList<RecordUpstreamEvent> mUpstreamEventList = new ArrayList<>();
-    private final ArrayMap<UpstreamType, DataUsage> mUpstreamUsageBaseline = new ArrayMap<>();
+    // Store the last reported data usage for each upstream type to be used for calculating the
+    // usage delta. The keys are the upstream types, and the values are the tethering UID data
+    // usage for the corresponding types. Retrieve the baseline data usage when tethering is
+    // enabled, update it when the upstream changes, and clear it when tethering is disabled.
+    private final ArrayMap<UpstreamType, DataUsage> mLastReportedUpstreamUsage = new ArrayMap<>();
     private final Context mContext;
     private final Dependencies mDependencies;
     private final NetworkStatsManager mNetworkStatsManager;
@@ -157,10 +160,15 @@
 
         /**
          * @see Handler
+         *
+         * Note: This should only be called once, within the constructor, as it creates a new
+         * thread. Calling it multiple times could lead to a thread leak.
          */
         @NonNull
-        public Handler createHandler(Looper looper) {
-            return new Handler(looper);
+        public Handler createHandler() {
+            final HandlerThread thread = new HandlerThread(TAG);
+            thread.start();
+            return new Handler(thread.getLooper());
         }
     }
 
@@ -177,9 +185,7 @@
         mContext = context;
         mDependencies = dependencies;
         mNetworkStatsManager = mContext.getSystemService(NetworkStatsManager.class);
-        final HandlerThread thread = new HandlerThread(TAG);
-        thread.start();
-        mHandler = dependencies.createHandler(thread.getLooper());
+        mHandler = dependencies.createHandler();
     }
 
     @VisibleForTesting
@@ -282,22 +288,33 @@
      * Calculates the data usage difference between the current and previous usage for the
      * specified upstream type.
      *
+     * Note: This must be called before updating mCurrentUpstream when changing the upstream.
+     *
      * @return A DataUsage object containing the calculated difference in transmitted (tx) and
      *         received (rx) bytes.
      */
     private DataUsage calculateDataUsageDelta(@Nullable UpstreamType upstream) {
-        if (upstream != null && mDependencies.isUpstreamDataUsageMetricsEnabled(mContext)
-                && isUsageSupportedForUpstreamType(upstream)) {
-            final DataUsage oldUsage = mUpstreamUsageBaseline.getOrDefault(upstream, EMPTY);
-            if (oldUsage.equals(EMPTY)) {
-                Log.d(TAG, "No usage baseline for the upstream=" + upstream);
-                return EMPTY;
-            }
-            // TODO(b/352537247): Fix data usage which might be incorrect if the device uses
-            //  tethering with the same upstream for over 15 days.
-            return DataUsage.subtract(getCurrentDataUsageForUpstreamType(upstream), oldUsage);
+        if (!mDependencies.isUpstreamDataUsageMetricsEnabled(mContext)) {
+            return EMPTY;
         }
-        return EMPTY;
+
+        if (upstream == null || !isUsageSupportedForUpstreamType(upstream)) {
+            return EMPTY;
+        }
+
+        final DataUsage oldUsage = mLastReportedUpstreamUsage.getOrDefault(upstream, EMPTY);
+        if (oldUsage.equals(EMPTY)) {
+            Log.d(TAG, "No usage baseline for the upstream=" + upstream);
+            return EMPTY;
+        }
+        // TODO(b/370724247): Fix data usage which might be incorrect if the device uses
+        //  tethering with the same upstream for over 15 days.
+        // Need to refresh the baseline usage data. If the network switches back to Wi-Fi after
+        // using cellular data (Wi-Fi -> Cellular -> Wi-Fi), the old baseline might be
+        // inaccurate, leading to incorrect delta calculations.
+        final DataUsage newUsage = getCurrentDataUsageForUpstreamType(upstream);
+        mLastReportedUpstreamUsage.put(upstream, newUsage);
+        return DataUsage.subtract(newUsage, oldUsage);
     }
 
     /**
@@ -416,7 +433,6 @@
      * @param reported a NetworkTetheringReported object containing statistics to write
      */
     private void write(@NonNull final NetworkTetheringReported reported) {
-        final byte[] upstreamEvents = reported.getUpstreamEvents().toByteArray();
         mDependencies.write(reported);
         if (DBG) {
             Log.d(
@@ -430,7 +446,7 @@
                     + ", userType: "
                     + reported.getUserType().getNumber()
                     + ", upstreamTypes: "
-                    + Arrays.toString(upstreamEvents)
+                    + Arrays.toString(reported.getUpstreamEvents().toByteArray())
                     + ", durationMillis: "
                     + reported.getDurationMillis());
         }
@@ -444,25 +460,29 @@
     }
 
     private void handleInitUpstreamUsageBaseline() {
-        if (!(mDependencies.isUpstreamDataUsageMetricsEnabled(mContext)
-                && mUpstreamUsageBaseline.isEmpty())) {
+        if (!mDependencies.isUpstreamDataUsageMetricsEnabled(mContext)) {
+            return;
+        }
+
+        if (!mLastReportedUpstreamUsage.isEmpty()) {
+            Log.wtf(TAG, "The upstream usage baseline has been initialed.");
             return;
         }
 
         for (UpstreamType type : UpstreamType.values()) {
             if (!isUsageSupportedForUpstreamType(type)) continue;
-            mUpstreamUsageBaseline.put(type, getCurrentDataUsageForUpstreamType(type));
+            mLastReportedUpstreamUsage.put(type, getCurrentDataUsageForUpstreamType(type));
         }
     }
 
     @VisibleForTesting
     @NonNull
-    DataUsage getDataUsageFromUpstreamType(@NonNull UpstreamType type) {
+    DataUsage getLastReportedUsageFromUpstreamType(@NonNull UpstreamType type) {
         if (mHandler.getLooper().getThread() != Thread.currentThread()) {
             throw new IllegalStateException(
                     "Not running on Handler thread: " + Thread.currentThread().getName());
         }
-        return mUpstreamUsageBaseline.getOrDefault(type, EMPTY);
+        return mLastReportedUpstreamUsage.getOrDefault(type, EMPTY);
     }
 
 
@@ -497,7 +517,7 @@
         mUpstreamEventList.clear();
         mCurrentUpstream = null;
         mCurrentUpStreamStartTime = 0L;
-        mUpstreamUsageBaseline.clear();
+        mLastReportedUpstreamUsage.clear();
     }
 
     private DownstreamType downstreamTypeToEnum(final int ifaceType) {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index 5d22977..dd10cc3 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -2032,7 +2032,7 @@
         final BpfCoordinator coordinator = makeBpfCoordinator();
         initBpfCoordinatorForRule4(coordinator);
         resetNetdAndBpfMaps();
-        assertEquals(0, mConsumer.getLastMaxConnectionAndResetToCurrent());
+        assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
 
         // Prepare add/delete rule events.
         final ArrayList<ConntrackEvent> addRuleEvents = new ArrayList<>();
@@ -2049,49 +2049,44 @@
         // Add rules, verify counter increases.
         for (int i = 0; i < 5; i++) {
             mConsumer.accept(addRuleEvents.get(i));
-            assertConsumerCountersEquals(supportActiveSessionsMetrics ? i + 1 : 0);
+            assertEquals(supportActiveSessionsMetrics ? i + 1 : 0,
+                    coordinator.getLastMaxConnectionAndResetToCurrent());
         }
 
         // Add the same events again should not increase the counter because
         // all events are already exist.
         for (final ConntrackEvent event : addRuleEvents) {
             mConsumer.accept(event);
-            assertConsumerCountersEquals(supportActiveSessionsMetrics ? 5 : 0);
+            assertEquals(supportActiveSessionsMetrics ? 5 : 0,
+                    coordinator.getLastMaxConnectionAndResetToCurrent());
         }
 
         // Verify removing non-existent items won't change the counters.
         for (int i = 5; i < 8; i++) {
             mConsumer.accept(new TestConntrackEvent.Builder().setMsgType(
                     IPCTNL_MSG_CT_DELETE).setProto(IPPROTO_TCP).setRemotePort(i).build());
-            assertConsumerCountersEquals(supportActiveSessionsMetrics ? 5 : 0);
+            assertEquals(supportActiveSessionsMetrics ? 5 : 0,
+                    coordinator.getLastMaxConnectionAndResetToCurrent());
         }
 
         // Verify remove the rules decrease the counter.
         // Note the max counter returns the max, so it returns the count before deleting.
         for (int i = 0; i < 5; i++) {
             mConsumer.accept(delRuleEvents.get(i));
-            assertEquals(supportActiveSessionsMetrics ? 4 - i : 0,
-                    mConsumer.getCurrentConnectionCount());
-            assertEquals(supportActiveSessionsMetrics ? 5 - i : 0,
-                    mConsumer.getLastMaxConnectionCount());
-            assertEquals(supportActiveSessionsMetrics ? 5 - i : 0,
-                    mConsumer.getLastMaxConnectionAndResetToCurrent());
         }
+        // The maximum number of rules observed is still 5.
+        assertEquals(supportActiveSessionsMetrics ? 5 : 0,
+                coordinator.getLastMaxConnectionAndResetToCurrent());
+        // After the reset, the maximum number of rules observed is 0.
+        assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
 
         // Verify remove these rules again doesn't decrease the counter.
         for (int i = 0; i < 5; i++) {
             mConsumer.accept(delRuleEvents.get(i));
-            assertConsumerCountersEquals(0);
+            assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
         }
     }
 
-    // Helper method to assert all counter values inside consumer.
-    private void assertConsumerCountersEquals(int expectedCount) {
-        assertEquals(expectedCount, mConsumer.getCurrentConnectionCount());
-        assertEquals(expectedCount, mConsumer.getLastMaxConnectionCount());
-        assertEquals(expectedCount, mConsumer.getLastMaxConnectionAndResetToCurrent());
-    }
-
     @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS)
     // BPF IPv4 forwarding only supports on S+.
     @IgnoreUpTo(Build.VERSION_CODES.R)
@@ -2121,7 +2116,7 @@
         coordinator.tetherOffloadClientAdd(mIpServer, clientB);
         assertClientInfoExists(mIpServer, clientA);
         assertClientInfoExists(mIpServer, clientB);
-        assertEquals(0, mConsumer.getLastMaxConnectionAndResetToCurrent());
+        assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
 
         // Add some rules for both clients.
         final int addr1RuleCount = 5;
@@ -2145,31 +2140,24 @@
                     .build());
         }
 
-        assertConsumerCountersEquals(
-                supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0);
+        assertEquals(supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0,
+                coordinator.getLastMaxConnectionAndResetToCurrent());
 
         // Remove 1 client. Since the 1st poll will return the LastMaxCounter and
-        // update it to the current, the max counter will be kept at 1st poll, while
-        // the current counter reflect the rule decreasing.
+        // update it to the current, the max counter will be kept at 1st poll.
         coordinator.tetherOffloadClientRemove(mIpServer, clientA);
+        assertEquals(supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0,
+                coordinator.getLastMaxConnectionAndResetToCurrent());
+        // And the counter be updated at 2nd poll.
         assertEquals(supportActiveSessionsMetrics ? addr2RuleCount : 0,
-                mConsumer.getCurrentConnectionCount());
-        assertEquals(supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0,
-                mConsumer.getLastMaxConnectionCount());
-        assertEquals(supportActiveSessionsMetrics ? addr1RuleCount + addr2RuleCount : 0,
-                mConsumer.getLastMaxConnectionAndResetToCurrent());
-        // And all counters be updated at 2nd poll.
-        assertConsumerCountersEquals(supportActiveSessionsMetrics ? addr2RuleCount : 0);
+                coordinator.getLastMaxConnectionAndResetToCurrent());
 
         // Remove other client.
         coordinator.tetherOffloadClientRemove(mIpServer, clientB);
-        assertEquals(0, mConsumer.getCurrentConnectionCount());
         assertEquals(supportActiveSessionsMetrics ? addr2RuleCount : 0,
-                mConsumer.getLastMaxConnectionCount());
-        assertEquals(supportActiveSessionsMetrics ? addr2RuleCount : 0,
-                mConsumer.getLastMaxConnectionAndResetToCurrent());
-        // All counters reach zero at 2nd poll.
-        assertConsumerCountersEquals(0);
+                coordinator.getLastMaxConnectionAndResetToCurrent());
+        // Verify the counter reach zero at 2nd poll.
+        assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
     }
 
     @FeatureFlag(name = TETHER_ACTIVE_SESSIONS_METRICS)
@@ -2191,7 +2179,7 @@
         final BpfCoordinator coordinator = makeBpfCoordinator();
         initBpfCoordinatorForRule4(coordinator);
         resetNetdAndBpfMaps();
-        assertConsumerCountersEquals(0);
+        assertEquals(0, coordinator.getLastMaxConnectionAndResetToCurrent());
 
         // Prepare the counter value.
         for (int i = 0; i < 5; i++) {
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 34689bc..6b646ec 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
@@ -87,13 +87,13 @@
 import android.util.ArrayMap;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.networkstack.tethering.UpstreamNetworkState;
 import com.android.networkstack.tethering.metrics.TetheringMetrics.DataUsage;
 import com.android.networkstack.tethering.metrics.TetheringMetrics.Dependencies;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
 import com.android.testutils.HandlerUtils;
 
 import org.junit.After;
@@ -104,7 +104,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-@RunWith(AndroidJUnit4.class)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 public final class TetheringMetricsTest {
     @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
@@ -158,7 +159,7 @@
         mThread = new HandlerThread("TetheringMetricsTest");
         mThread.start();
         mHandler = new Handler(mThread.getLooper());
-        doReturn(mHandler).when(mDeps).createHandler(any());
+        doReturn(mHandler).when(mDeps).createHandler();
         // Set up the usage for upstream types.
         mMockUpstreamUsageBaseline.put(UT_CELLULAR, new DataUsage(100L, 200L));
         mMockUpstreamUsageBaseline.put(UT_WIFI, new DataUsage(400L, 800L));
@@ -498,7 +499,7 @@
     private void verifyEmptyUsageForAllUpstreamTypes() {
         mHandler.post(() -> {
             for (UpstreamType type : UpstreamType.values()) {
-                assertEquals(EMPTY, mTetheringMetrics.getDataUsageFromUpstreamType(type));
+                assertEquals(EMPTY, mTetheringMetrics.getLastReportedUsageFromUpstreamType(type));
             }
         });
         HandlerUtils.waitForIdle(mHandler, DEFAULT_TIMEOUT);
@@ -555,7 +556,8 @@
 
         mHandler.post(() -> {
             for (UpstreamType type : UpstreamType.values()) {
-                final DataUsage dataUsage = mTetheringMetrics.getDataUsageFromUpstreamType(type);
+                final DataUsage dataUsage =
+                        mTetheringMetrics.getLastReportedUsageFromUpstreamType(type);
                 if (TetheringMetrics.isUsageSupportedForUpstreamType(type)) {
                     assertEquals(mMockUpstreamUsageBaseline.get(type), dataUsage);
                 } else {
@@ -610,12 +612,21 @@
         incrementCurrentTime(cellDuration);
         updateUpstreamDataUsage(UT_CELLULAR, cellUsageDiff);
 
+        // Change the upstream back to Wi-FI and update the data usage
+        runAndWaitForIdle(() ->
+                mTetheringMetrics.maybeUpdateUpstreamType(buildUpstreamState(TRANSPORT_WIFI)));
+        final long wifiDuration2 = 50 * SECOND_IN_MILLIS;
+        final long wifiUsageDiff2 = 1000L;
+        incrementCurrentTime(wifiDuration2);
+        updateUpstreamDataUsage(UT_WIFI, wifiUsageDiff2);
+
         // Stop tethering and verify that the data usage is uploaded.
         updateErrorAndSendReport(TETHERING_WIFI, TETHER_ERROR_NO_ERROR);
         UpstreamEvents.Builder upstreamEvents = UpstreamEvents.newBuilder();
         addUpstreamEvent(upstreamEvents, UT_WIFI, wifiDuration, wifiUsageDiff, wifiUsageDiff);
         addUpstreamEvent(upstreamEvents, UT_BLUETOOTH, bluetoothDuration, btUsageDiff, btUsageDiff);
         addUpstreamEvent(upstreamEvents, UT_CELLULAR, cellDuration, cellUsageDiff, cellUsageDiff);
+        addUpstreamEvent(upstreamEvents, UT_WIFI, wifiDuration2, wifiUsageDiff2, wifiUsageDiff2);
         verifyReport(DownstreamType.DS_TETHERING_WIFI, ErrorCode.EC_NO_ERROR,
                 UserType.USER_SETTINGS, upstreamEvents,
                 currentTimeMillis() - wifiTetheringStartTime);
diff --git a/bpf/loader/initrc-doc/README.txt b/bpf/loader/initrc-doc/README.txt
index 42e1fc2..2b22326 100644
--- a/bpf/loader/initrc-doc/README.txt
+++ b/bpf/loader/initrc-doc/README.txt
@@ -1,20 +1,42 @@
 This directory contains comment stripped versions of
   //system/bpf/bpfloader/bpfloader.rc
-from previous versions of Android.
+or
+  //packages/modules/Connectivity/bpf/loader/netbpfload.rc
+(as appropriate) from previous versions of Android.
 
 Generated via:
-  (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android11-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
-  (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android12-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
-  (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android13-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
-  (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/android14-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
-  (cd ../../../../../system/bpf && git cat-file -p remotes/aosp/main:bpfloader/bpfloader.rc;              ) | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2.rc
+  (cd ../../../../../../system/bpf && git cat-file -p remotes/aosp/android11-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
+  (cd ../../../../../../system/bpf && git cat-file -p remotes/aosp/android12-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
+  (cd ../../../../../../system/bpf && git cat-file -p remotes/aosp/android13-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
+  (cd ../../../../../../system/bpf && git cat-file -p remotes/aosp/android14-release:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
+  git cat-file -p remotes/aosp/android14-qpr2-release:netbpfload/netbpfload.rc | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2-24Q1.rc
+  git cat-file -p remotes/aosp/android14-qpr3-release:netbpfload/netbpfload.rc | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR3-24Q2.rc
+  git cat-file -p remotes/aosp/android15-release:netbpfload/netbpfload.rc      | egrep -v '^ *#' > bpfloader-sdk35-15-V-24Q3.rc
+  git cat-file -p remotes/aosp/main:bpf/loader/netbpfload.rc                   | egrep -v '^ *#' > bpfloader-sdk35-15-V-QPR1-24Q4.rc
+
+see also:
+  https://android.googlesource.com/platform/system/bpf/+/refs/heads/android11-release/bpfloader/bpfloader.rc
+  https://android.googlesource.com/platform/system/bpf/+/refs/heads/android12-release/bpfloader/bpfloader.rc
+  https://android.googlesource.com/platform/system/bpf/+/refs/heads/android13-release/bpfloader/bpfloader.rc
+  https://android.googlesource.com/platform/system/bpf/+/refs/heads/android14-release/bpfloader/bpfloader.rc
+  https://android.googlesource.com/platform/system/bpf/+/refs/heads/android14-qpr1-release/bpfloader/bpfloader.rc
+  https://android.googlesource.com/platform/system/bpf/+/refs/heads/android14-qpr2-release/bpfloader/ (rc file is gone in QPR2)
+  https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/android14-qpr2-release/netbpfload/netbpfload.rc
+  https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/android14-qpr3-release/netbpfload/netbpfload.rc
+  https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/android15-release/netbpfload/netbpfload.rc
+  https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/android15-qpr1-release/netbpfload/netbpfload.rc
+  https://android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/main/netbpfload/netbpfload.rc
+or:
+  https://googleplex-android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/24Q1-release/netbpfload/netbpfload.rc
+  https://googleplex-android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/24Q2-release/netbpfload/netbpfload.rc
+  https://googleplex-android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/24Q3-release/netbpfload/netbpfload.rc
+  https://googleplex-android.googlesource.com/platform/packages/modules/Connectivity/+/refs/heads/24Q4-release/bpf/loader/netbpfload.rc
 
 this is entirely equivalent to:
   (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
   (cd /android1/system/bpf && git cat-file -p remotes/goog/sc-dev:bpfloader/bpfloader.rc;  ) | egrep -v '^ *#' > bpfloader-sdk31-12-S.rc
   (cd /android1/system/bpf && git cat-file -p remotes/goog/tm-dev:bpfloader/bpfloader.rc;  ) | egrep -v '^ *#' > bpfloader-sdk33-13-T.rc
   (cd /android1/system/bpf && git cat-file -p remotes/goog/udc-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk34-14-U.rc
-  (cd /android1/system/bpf && git cat-file -p remotes/goog/main:bpfloader/bpfloader.rc;    ) | egrep -v '^ *#' > bpfloader-sdk34-14-U-QPR2.rc
 
 it is also equivalent to:
   (cd /android1/system/bpf && git cat-file -p remotes/goog/rvc-qpr-dev:bpfloader/bpfloader.rc; ) | egrep -v '^ *#' > bpfloader-sdk30-11-R.rc
@@ -29,34 +51,66 @@
 
 Key takeaways:
 
-= R bpfloader:
+= R bpfloader (platform)
   - CHOWN + SYS_ADMIN
   - asynchronous startup
   - platform only
   - proc file setup handled by initrc
 
-= S bpfloader
+= S bpfloader (platform)
   - adds NET_ADMIN
   - synchronous startup
   - platform + mainline tethering offload
 
-= T bpfloader
+= T bpfloader (platform)
   - platform + mainline networking (including tethering offload)
   - supported btf for maps via exec of btfloader
 
-= U bpfloader
+= U bpfloader (platform)
   - proc file setup moved into bpfloader binary
   - explicitly specified user and groups:
     group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
     user root
 
-= U QPR2 bpfloader
+= U QPR2 [24Q1] bpfloader (platform netbpfload -> platform bpfloader)
   - drops support of btf for maps
   - invocation of /system/bin/netbpfload binary, which after handling *all*
     networking bpf related things executes the platform /system/bin/bpfloader
     which handles non-networking bpf.
+  - Note: this does not (by itself) call into apex NetBpfLoad
+
+= U QPR3 [24Q2] bpfloader (platform netbpfload -> apex netbpfload -> platform bpfloader)
+  - platform NetBpfload *always* execs into apex NetBpfLoad,
+  - shipped with mainline tethering apex that includes NetBpfLoad binary.
+
+= V [24Q3] bpfloader (apex netbpfload -> platform bpfloader)
+  - no significant changes, though it does hard require the apex NetBpfLoad
+    by virtue of the platform NetBpfLoad no longer being present.
+    ie. the apex must override the platform 'bpfloader' service for 35+:
+    the V FRC M-2024-08+ tethering apex does this.
+
+= V QPR1 [24Q4] bpfloader (apex netbpfload -> platform bpfloader)
+  - made netd start earlier (previously happened in parallel to zygote)
+  - renamed and moved the trigger out of netbpload.rc into
+    //system/core/rootdir/init.rc
+  - the new sequence is:
+      trigger post-fs-data        (logd available, starts apexd)
+      trigger load-bpf-programs   (does: exec_start bpfloader)
+      trigger bpf-progs-loaded    (does: start netd)
+      trigger zygote-start
+  - this is more or less irrelevant from the point of view of the bpfloader,
+    but it does mean netd init could fail and abort the boot earlier,
+    before 'A/B update_verifier marks a successful boot'.
+    Though note that due to netd being started asynchronously, it is racy.
 
 Note that there is now a copy of 'netbpfload' provided by the tethering apex
 mainline module at /apex/com.android.tethering/bin/netbpfload, which due
 to the use of execve("/system/bin/bpfloader") relies on T+ selinux which was
 added for btf map support (specifically the ability to exec the "btfloader").
+
+= mainline tethering apex M-2024-08+ overrides the platform service for V+
+  thus loading mainline (ie. networking) bpf programs from mainline 'NetBpfLoad'
+  and platform ones from platform 'bpfloader'.
+
+= mainline tethering apex M-2024-09+ changes T+ behaviour (U QPR3+ unaffected)
+  netd -> netd_updatable.so -> ctl.start=mdnsd_netbpfload -> load net bpf programs
diff --git a/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc b/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2-24Q1.rc
similarity index 100%
copy from bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
copy to bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2-24Q1.rc
diff --git a/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc b/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR3-24Q2.rc
similarity index 100%
rename from bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR2.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR3-24Q2.rc
diff --git a/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR3.rc b/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR3.rc
deleted file mode 100644
index 8f3f462..0000000
--- a/bpf/loader/initrc-doc/bpfloader-sdk34-14-U-QPR3.rc
+++ /dev/null
@@ -1,11 +0,0 @@
-on load_bpf_programs
-    exec_start bpfloader
-
-service bpfloader /system/bin/netbpfload
-    capabilities CHOWN SYS_ADMIN NET_ADMIN
-    group root graphics network_stack net_admin net_bw_acct net_bw_stats net_raw system
-    user root
-    rlimit memlock 1073741824 1073741824
-    oneshot
-    reboot_on_failure reboot,bpfloader-failed
-    updatable
diff --git a/bpf/loader/initrc-doc/bpfloader-sdk35-15-V.rc b/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-24Q3.rc
similarity index 100%
rename from bpf/loader/initrc-doc/bpfloader-sdk35-15-V.rc
rename to bpf/loader/initrc-doc/bpfloader-sdk35-15-V-24Q3.rc
diff --git a/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-QPR1-24Q4.rc b/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-QPR1-24Q4.rc
new file mode 100644
index 0000000..e2639ac
--- /dev/null
+++ b/bpf/loader/initrc-doc/bpfloader-sdk35-15-V-QPR1-24Q4.rc
@@ -0,0 +1,5 @@
+service bpfloader /system/bin/false
+    user root
+    oneshot
+    reboot_on_failure reboot,netbpfload-missing
+    updatable
diff --git a/bpf/netd/BpfHandler.cpp b/bpf/netd/BpfHandler.cpp
index 5dea851..8e4c2c6 100644
--- a/bpf/netd/BpfHandler.cpp
+++ b/bpf/netd/BpfHandler.cpp
@@ -201,7 +201,7 @@
     }
 }
 
-Status BpfHandler::init(const char* cg2_path) {
+static inline void waitForBpf() {
     // Note: netd *can* be restarted, so this might get called a second time after boot is complete
     // at which point we don't need to (and shouldn't) wait for (more importantly start) loading bpf
 
@@ -229,6 +229,21 @@
     }
 
     ALOGI("BPF programs are loaded");
+}
+
+Status BpfHandler::init(const char* cg2_path) {
+    // This wait is effectively a no-op on U QPR3+ devices (as netd starts
+    // *after* the synchronous 'exec_start bpfloader' which calls NetBpfLoad)
+    // but checking for U QPR3 is hard.
+    //
+    // Waiting should not be required on U QPR3+ devices,
+    // ...
+    //
+    // ...unless someone changed 'exec_start bpfloader' to 'start bpfloader'
+    // in the rc file.
+    //
+    // TODO: should be: if (!modules::sdklevel::IsAtLeastW())
+    if (android_get_device_api_level() <= __ANDROID_API_V__) waitForBpf();
 
     RETURN_IF_NOT_OK(initPrograms(cg2_path));
     RETURN_IF_NOT_OK(initMaps());
diff --git a/bpf/progs/offload.c b/bpf/progs/offload.c
index 7e1184d..631908a 100644
--- a/bpf/progs/offload.c
+++ b/bpf/progs/offload.c
@@ -85,9 +85,8 @@
 
     // Since the program never writes via DPA (direct packet access) auto-pull/unclone logic does
     // not trigger and thus we need to manually make sure we can read packet headers via DPA.
-    // Note: this is a blind best effort pull, which may fail or pull less - this doesn't matter.
     // It has to be done early cause it will invalidate any skb->data/data_end derived pointers.
-    try_make_writable(skb, l2_header_size + IP6_HLEN + TCP_HLEN);
+    if (bpf_skb_pull_data(skb, l2_header_size + IP6_HLEN)) return TC_ACT_PIPE;
 
     void* data = (void*)(long)skb->data;
     const void* data_end = (void*)(long)skb->data_end;
@@ -110,6 +109,14 @@
     // If hardware offload is running and programming flows based on conntrack entries,
     // try not to interfere with it.
     if (ip6->nexthdr == IPPROTO_TCP) {
+        // don't need to check return code, as it's effectively checked in the next 'if' below
+        bpf_skb_pull_data(skb, l2_header_size + IP6_HLEN + TCP_HLEN);
+
+        data = (void*)(long)skb->data;
+        data_end = (void*)(long)skb->data_end;
+        eth = is_ethernet ? data : NULL;  // used iff is_ethernet
+        ip6 = is_ethernet ? (void*)(eth + 1) : data;
+
         struct tcphdr* tcph = (void*)(ip6 + 1);
 
         // Make sure we can get at the tcp header
diff --git a/networksecurity/OWNERS b/networksecurity/OWNERS
index 1a4130a..0c838c0 100644
--- a/networksecurity/OWNERS
+++ b/networksecurity/OWNERS
@@ -1,4 +1,5 @@
 # Bug component: 1479456
 
+bessiej@google.com
 sandrom@google.com
 tweek@google.com
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
index cfeca5d..e52dd2f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
@@ -107,7 +107,7 @@
         final QueryTaskConfig nextRunConfig = currentConfig.getConfigForNextRun(queryMode);
         long timeToRun;
         if (mLastScheduledQueryTaskArgs == null && !forceEnableBackoff) {
-            timeToRun = now + nextRunConfig.delayUntilNextTaskWithoutBackoffMs;
+            timeToRun = now + nextRunConfig.delayBeforeTaskWithoutBackoffMs;
         } else {
             timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
                     nextRunConfig, now, minRemainingTtl, lastSentTime, numOfQueriesBeforeBackoff,
@@ -133,7 +133,7 @@
     private static long calculateTimeToRun(@Nullable ScheduledQueryTaskArgs taskArgs,
             QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime,
             int numOfQueriesBeforeBackoff, boolean forceEnableBackoff) {
-        final long baseDelayInMs = queryTaskConfig.delayUntilNextTaskWithoutBackoffMs;
+        final long baseDelayInMs = queryTaskConfig.delayBeforeTaskWithoutBackoffMs;
         if (!(forceEnableBackoff
                 || queryTaskConfig.shouldUseQueryBackoff(numOfQueriesBeforeBackoff))) {
             return lastSentTime + baseDelayInMs;
diff --git a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
index d2cd463..4e74159 100644
--- a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
+++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
@@ -55,22 +55,22 @@
     private final int queriesPerBurst;
     private final int timeBetweenBurstsInMs;
     private final int burstCounter;
-    final long delayUntilNextTaskWithoutBackoffMs;
+    final long delayBeforeTaskWithoutBackoffMs;
     private final boolean isFirstBurst;
-    private final long queryCount;
+    private final long queryIndex;
 
-    QueryTaskConfig(long queryCount, int transactionId,
+    QueryTaskConfig(long queryIndex, int transactionId,
             boolean expectUnicastResponse, boolean isFirstBurst, int burstCounter,
             int queriesPerBurst, int timeBetweenBurstsInMs,
-            long delayUntilNextTaskWithoutBackoffMs) {
+            long delayBeforeTaskWithoutBackoffMs) {
         this.transactionId = transactionId;
         this.expectUnicastResponse = expectUnicastResponse;
         this.queriesPerBurst = queriesPerBurst;
         this.timeBetweenBurstsInMs = timeBetweenBurstsInMs;
         this.burstCounter = burstCounter;
-        this.delayUntilNextTaskWithoutBackoffMs = delayUntilNextTaskWithoutBackoffMs;
+        this.delayBeforeTaskWithoutBackoffMs = delayBeforeTaskWithoutBackoffMs;
         this.isFirstBurst = isFirstBurst;
-        this.queryCount = queryCount;
+        this.queryIndex = queryIndex;
     }
 
     QueryTaskConfig(int queryMode) {
@@ -82,26 +82,26 @@
         // Config the scan frequency based on the scan mode.
         if (queryMode == AGGRESSIVE_QUERY_MODE) {
             this.timeBetweenBurstsInMs = INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
-            this.delayUntilNextTaskWithoutBackoffMs =
+            this.delayBeforeTaskWithoutBackoffMs =
                     TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
         } else if (queryMode == PASSIVE_QUERY_MODE) {
             // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then
             // in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
             // queries.
             this.timeBetweenBurstsInMs = MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
-            this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
+            this.delayBeforeTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
         } else {
             // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
             // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
             // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
             // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
             this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS;
-            this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
+            this.delayBeforeTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
         }
-        this.queryCount = 0;
+        this.queryIndex = 0;
     }
 
-    long getDelayUntilNextTaskWithoutBackoff(boolean isFirstQueryInBurst,
+    long getDelayBeforeNextTaskWithoutBackoff(boolean isFirstQueryInBurst,
             boolean isLastQueryInBurst, int queryMode) {
         if (isFirstQueryInBurst && queryMode == AGGRESSIVE_QUERY_MODE) {
             return 0;
@@ -137,7 +137,7 @@
      * Get new QueryTaskConfig for next run.
      */
     public QueryTaskConfig getConfigForNextRun(int queryMode) {
-        long newQueryCount = queryCount + 1;
+        long newQueryCount = queryIndex + 1;
         int newTransactionId = transactionId + 1;
         if (newTransactionId > UNSIGNED_SHORT_MAX_VALUE) {
             newTransactionId = 1;
@@ -162,7 +162,7 @@
                 getNextExpectUnicastResponse(isLastQueryInBurst, queryMode), newIsFirstBurst,
                 newBurstCounter, newQueriesPerBurst,
                 getNextTimeBetweenBurstsMs(isLastQueryInBurst, queryMode),
-                getDelayUntilNextTaskWithoutBackoff(
+                getDelayBeforeNextTaskWithoutBackoff(
                         isFirstQueryInBurst, isLastQueryInBurst, queryMode));
     }
 
@@ -174,6 +174,6 @@
         if (burstCounter != 0 || isFirstBurst) {
             return false;
         }
-        return queryCount > numOfQueriesBeforeBackoff;
+        return queryIndex > numOfQueriesBeforeBackoff;
     }
 }
diff --git a/service/ServiceConnectivityResources/Android.bp b/service/ServiceConnectivityResources/Android.bp
index 2621256..be9b2b5 100644
--- a/service/ServiceConnectivityResources/Android.bp
+++ b/service/ServiceConnectivityResources/Android.bp
@@ -33,6 +33,7 @@
         "com.android.tethering",
     ],
     certificate: ":com.android.connectivity.resources.certificate",
+    updatable: true,
 }
 
 android_app_certificate {
diff --git a/staticlibs/tests/unit/host/python/apf_utils_test.py b/staticlibs/tests/unit/host/python/apf_utils_test.py
index 96b967b..419b338 100644
--- a/staticlibs/tests/unit/host/python/apf_utils_test.py
+++ b/staticlibs/tests/unit/host/python/apf_utils_test.py
@@ -25,10 +25,14 @@
     get_apf_capabilities,
     get_apf_counter,
     get_apf_counters_from_dumpsys,
-    get_ipv4_address,
-    get_ipv6_address,
+    get_ipv4_addresses,
+    get_ipv6_addresses,
     get_hardware_address,
     is_send_raw_packet_downstream_supported,
+    is_packet_capture_supported,
+    start_capture_packets,
+    stop_capture_packets,
+    get_matched_packet_counts,
     send_raw_packet_downstream,
 )
 from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError
@@ -114,44 +118,53 @@
       get_hardware_address(self.mock_ad, "wlan0")
 
   @patch("net_tests_utils.host.python.adb_utils.adb_shell")
-  def test_get_ipv4_address_success(
+  def test_get_ipv4_addresses_success(
       self, mock_adb_shell: MagicMock
   ) -> None:
     mock_adb_shell.return_value = """
 54: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
-inet 192.168.195.162/24 brd 192.168.195.255 scope global wlan0
-valid_lft forever preferred_lft forever
+    inet 192.168.195.162/24 brd 192.168.195.255 scope global wlan0
+       valid_lft forever preferred_lft forever
+    inet 192.168.200.1/24 brd 192.168.200.255 scope global wlan0
+       valid_lft forever preferred_lft forever
 """
-    ip_address = get_ipv4_address(self.mock_ad, "wlan0")
-    asserts.assert_equal(ip_address, "192.168.195.162")
+    ip_addresses = get_ipv4_addresses(self.mock_ad, "wlan0")
+    asserts.assert_equal(ip_addresses, ["192.168.195.162", "192.168.200.1"])
 
   @patch("net_tests_utils.host.python.adb_utils.adb_shell")
-  def test_get_ipv4_address_not_found(
+  def test_get_ipv4_addresses_not_found(
       self, mock_adb_shell: MagicMock
   ) -> None:
-     mock_adb_shell.return_value = ""
-     with asserts.assert_raises(PatternNotFoundException):
-       get_ipv4_address(self.mock_ad, "wlan0")
+    mock_adb_shell.return_value = ""
+    ip_addresses = get_ipv4_addresses(self.mock_ad, "wlan0")
+    asserts.assert_equal(ip_addresses, [])
 
   @patch("net_tests_utils.host.python.adb_utils.adb_shell")
-  def test_get_ipv6_address_success(
+  def test_get_ipv6_addresses_success(
       self, mock_adb_shell: MagicMock
   ) -> None:
     mock_adb_shell.return_value = """
 54: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
-inet6 fe80::10a3:5dff:fe52:de32/64 scope link
-valid_lft forever preferred_lft forever
+    inet6 fe80::10a3:5dff:fe52:de32/64 scope link
+        valid_lft forever preferred_lft forever
+    inet6 2001:b400:e53f:164e:9c1e:780e:d1:4658/64 scope global dynamic mngtmpaddr noprefixroute
+        valid_lft 6995sec preferred_lft 6995sec
+    inet6 fe80::3aff:2199:2d8e:20d1/64 scope link noprefixroute
+        valid_lft forever preferred_lft forever
 """
-    ip_address = get_ipv6_address(self.mock_ad, "wlan0")
-    asserts.assert_equal(ip_address, "fe80::10a3:5dff:fe52:de32")
+    ip_addresses = get_ipv6_addresses(self.mock_ad, "wlan0")
+    asserts.assert_equal(ip_addresses,
+                         ["fe80::10a3:5dff:fe52:de32",
+                          "2001:b400:e53f:164e:9c1e:780e:d1:4658",
+                          "fe80::3aff:2199:2d8e:20d1"])
 
   @patch("net_tests_utils.host.python.adb_utils.adb_shell")
   def test_get_ipv6_address_not_found(
           self, mock_adb_shell: MagicMock
   ) -> None:
-      mock_adb_shell.return_value = ""
-      with asserts.assert_raises(PatternNotFoundException):
-          get_ipv6_address(self.mock_ad, "wlan0")
+    mock_adb_shell.return_value = ""
+    ip_addresses = get_ipv6_addresses(self.mock_ad, "wlan0")
+    asserts.assert_equal(ip_addresses, [])
 
   @patch("net_tests_utils.host.python.adb_utils.adb_shell")
   def test_send_raw_packet_downstream_success(
@@ -199,6 +212,144 @@
         "Send raw packet should not be supported.",
     )
 
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_start_capture_success(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.return_value = "success"  # Successful command output
+      start_capture_packets(
+          self.mock_ad, TEST_IFACE_NAME
+      )
+      mock_adb_shell.assert_called_once_with(
+          self.mock_ad,
+          "cmd network_stack capture start"
+          f" {TEST_IFACE_NAME}"
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_start_capture_failure(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.return_value = (  # Unexpected command output
+          "Any Unexpected Output"
+      )
+      with asserts.assert_raises(UnexpectedBehaviorError):
+          start_capture_packets(
+              self.mock_ad, TEST_IFACE_NAME
+          )
+      asserts.assert_true(
+          is_packet_capture_supported(self.mock_ad),
+          "Start capturing packets should be supported.",
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_start_capture_unsupported(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.side_effect = AdbError(
+          cmd="", stdout="Unknown command", stderr="", ret_code=3
+      )
+      with asserts.assert_raises(UnsupportedOperationException):
+          start_capture_packets(
+              self.mock_ad, TEST_IFACE_NAME
+          )
+      asserts.assert_false(
+          is_packet_capture_supported(self.mock_ad),
+          "Start capturing packets should not be supported.",
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_stop_capture_success(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.return_value = "success"  # Successful command output
+      stop_capture_packets(
+          self.mock_ad, TEST_IFACE_NAME
+      )
+      mock_adb_shell.assert_called_once_with(
+          self.mock_ad,
+          "cmd network_stack capture stop"
+          f" {TEST_IFACE_NAME}"
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_stop_capture_failure(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.return_value = (  # Unexpected command output
+          "Any Unexpected Output"
+      )
+      with asserts.assert_raises(UnexpectedBehaviorError):
+          stop_capture_packets(
+              self.mock_ad, TEST_IFACE_NAME
+          )
+      asserts.assert_true(
+          is_packet_capture_supported(self.mock_ad),
+          "Stop capturing packets should be supported.",
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_stop_capture_unsupported(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.side_effect = AdbError(
+          cmd="", stdout="Unknown command", stderr="", ret_code=3
+      )
+      with asserts.assert_raises(UnsupportedOperationException):
+          stop_capture_packets(
+              self.mock_ad, TEST_IFACE_NAME
+          )
+      asserts.assert_false(
+          is_packet_capture_supported(self.mock_ad),
+          "Stop capturing packets should not be supported.",
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_get_matched_packet_counts_success(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.return_value = "10"  # Successful command output
+      get_matched_packet_counts(
+          self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+      )
+      mock_adb_shell.assert_called_once_with(
+          self.mock_ad,
+          "cmd network_stack capture matched-packet-counts"
+          f" {TEST_IFACE_NAME} {TEST_PACKET_IN_HEX}"
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_get_matched_packet_counts_failure(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.return_value = (  # Unexpected command output
+          "Any Unexpected Output"
+      )
+      with asserts.assert_raises(UnexpectedBehaviorError):
+          get_matched_packet_counts(
+              self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+          )
+      asserts.assert_true(
+          is_packet_capture_supported(self.mock_ad),
+          "Get matched packet counts should be supported.",
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_get_matched_packet_counts_unsupported(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.side_effect = AdbError(
+          cmd="", stdout="Unknown command", stderr="", ret_code=3
+      )
+      with asserts.assert_raises(UnsupportedOperationException):
+          get_matched_packet_counts(
+              self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+          )
+      asserts.assert_false(
+          is_packet_capture_supported(self.mock_ad),
+          "Get matched packet counts should not be supported.",
+      )
+
   @parameterized.parameters(
       ("2,2048,1", ApfCapabilities(2, 2048, 1)),  # Valid input
       ("3,1024,0", ApfCapabilities(3, 1024, 0)),  # Valid input
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
index b312bcf..7fe60bd 100644
--- a/staticlibs/testutils/host/python/apf_utils.py
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -83,14 +83,13 @@
   ad.log.debug("Getting apf counters: " + str(result))
   return result
 
-def get_ipv4_address(
+def get_ipv4_addresses(
     ad: android_device.AndroidDevice, iface_name: str
-) -> str:
-  """Retrieves the IPv4 address of a given interface on an Android device.
+) -> list[str]:
+  """Retrieves the IPv4 addresses of a given interface on an Android device.
 
   This function executes an ADB shell command (`ip -4 address show`) to get the
   network interface information and extracts the IPv4 address from the output.
-  If devices has multiple IPv4 addresses, return the first one.
   If devices have no IPv4 address, raise PatternNotFoundException.
 
   Args:
@@ -98,35 +97,31 @@
       iface_name: The name of the network interface (e.g., "wlan0").
 
   Returns:
-      The IPv4 address of the interface as a string.
-
-  Raises:
-      PatternNotFoundException: If the IPv4 address is not found in the command
-      output.
+      The IPv4 addresses of the interface as a list of string.
+      Return empty list if no IPv4 address.
   """
   # output format:
   # 54: wlan2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
-  # inet 192.168.195.162/24 brd 192.168.195.255 scope global wlan2
-  # valid_lft forever preferred_lft forever
+  #     inet 192.168.195.162/24 brd 192.168.195.255 scope global wlan2
+  #         valid_lft forever preferred_lft forever
+  #     inet 192.168.1.1/24 brd 192.168.1.255 scope global wlan2
+  #         valid_lft forever preferred_lft forever
   output = adb_utils.adb_shell(ad, f"ip -4 address show {iface_name}")
   pattern = r"inet\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/\d+"
-  match = re.search(pattern, output)
+  matches = re.findall(pattern, output)
 
-  if match:
-    return match.group(1)  # Extract the IPv4 address string.
+  if matches:
+    return matches
   else:
-    raise PatternNotFoundException(
-      "Cannot get hardware address for " + iface_name
-    )
+    return []
 
-def get_ipv6_address(
+def get_ipv6_addresses(
     ad: android_device.AndroidDevice, iface_name: str
-) -> str:
-  """Retrieves the IPv6 address of a given interface on an Android device.
+) -> list[str]:
+  """Retrieves the IPv6 addresses of a given interface on an Android device.
 
   This function executes an ADB shell command (`ip -6 address show`) to get the
   network interface information and extracts the IPv6 address from the output.
-  If devices has multiple IPv6 addresses, return the first one.
   If devices have no IPv6 address, raise PatternNotFoundException.
 
   Args:
@@ -134,29 +129,23 @@
       iface_name: The name of the network interface (e.g., "wlan0").
 
   Returns:
-      The IPv6 address of the interface as a string.
-
-  Raises:
-      PatternNotFoundException: If the IPv6 address is not found in the command
-      output.
+      The IPv6 addresses of the interface as a list of string.
+      Return empty list if no IPv6 address.
   """
   # output format
   # 54: wlan2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
-  # inet6 fe80::10a3:5dff:fe52:de32/64 scope link
-  # valid_lft forever preferred_lft forever
+  #     inet6 fe80::10a3:5dff:fe52:de32/64 scope link
+  #         valid_lft forever preferred_lft forever
+  #     inet6 fe80::1233:aadb:3d32:1234/64 scope link
+  #         valid_lft forever preferred_lft forever
   output = adb_utils.adb_shell(ad, f"ip -6 address show {iface_name}")
-  if output is "":
-    raise PatternNotFoundException(
-      "Cannot get ipv6 address for " + iface_name
-    )
   pattern = r"inet6\s+([0-9a-fA-F:]+)\/\d+"
-  match = re.search(pattern, output)
-  if match:
-    return match.group(1)  # Extract the IPv6 address string.
+  matches = re.findall(pattern, output)
+
+  if matches:
+    return matches
   else:
-    raise PatternNotFoundException(
-      "Cannot get IPv6 address for " + iface_name
-    )
+    return []
 
 def get_hardware_address(
     ad: android_device.AndroidDevice, iface_name: str
@@ -189,6 +178,29 @@
         "Cannot get hardware address for " + iface_name
     )
 
+def is_packet_capture_supported(
+        ad: android_device.AndroidDevice,
+) -> bool:
+
+  # Invoke the shell command with empty argument and see how NetworkStack respond.
+  # If supported, an IllegalArgumentException with help page will be printed.
+  functions_with_args = (
+    # list all functions and args with (func, *args) tuple
+    (start_capture_packets, (ad, "")),
+    (stop_capture_packets, (ad, "")),
+    (get_matched_packet_counts, (ad, "", ""))
+  )
+
+  for func, args in functions_with_args:
+    try:
+      func(*args)
+    except UnsupportedOperationException:
+      return False
+    except Exception:
+      continue
+
+  # If no UnsupportOperationException is thrown, regard it as supported
+  return True
 
 def is_send_raw_packet_downstream_supported(
     ad: android_device.AndroidDevice,
@@ -235,25 +247,92 @@
         representation of a packet starting from L2 header.
   """
 
-  cmd = (
-      "cmd network_stack send-raw-packet-downstream"
-      f" {iface_name} {packet_in_hex}"
-  )
+  cmd = f"cmd network_stack send-raw-packet-downstream {iface_name} {packet_in_hex}"
 
   # Expect no output or Unknown command if NetworkStack is too old. Throw otherwise.
-  try:
-    output = adb_utils.adb_shell(ad, cmd)
-  except AdbError as e:
-    output = str(e.stdout)
-  if output:
-    if "Unknown command" in output:
-      raise UnsupportedOperationException(
-          "send-raw-packet-downstream command is not supported."
-      )
+  adb_output = AdbOutputHandler(ad, cmd).get_output()
+  if adb_output:
     raise assert_utils.UnexpectedBehaviorError(
-        f"Got unexpected output: {output} for command: {cmd}."
+      f"Got unexpected output: {adb_output} for command: {cmd}."
     )
 
+def start_capture_packets(
+        ad: android_device.AndroidDevice,
+        iface_name: str
+) -> None:
+  """Starts packet capturing on a specified network interface.
+
+  This function initiates packet capture on the given network interface of an
+  Android device using an ADB shell command. It handles potential errors
+  related to unsupported commands or unexpected output.
+  This command only supports downstream tethering interface.
+
+  Args:
+    ad: The Android device object.
+    iface_name: The name of the network interface (e.g., "wlan0").
+  """
+  cmd = f"cmd network_stack capture start {iface_name}"
+
+  # Expect no output or Unknown command if NetworkStack is too old. Throw otherwise.
+  adb_output = AdbOutputHandler(ad, cmd).get_output()
+  if adb_output != "success":
+    raise assert_utils.UnexpectedBehaviorError(
+      f"Got unexpected output: {adb_output} for command: {cmd}."
+    )
+
+def stop_capture_packets(
+        ad: android_device.AndroidDevice,
+        iface_name: str
+) -> None:
+  """Stops packet capturing on a specified network interface.
+
+  This function terminates packet capture on the given network interface of an
+  Android device using an ADB shell command. It handles potential errors
+  related to unsupported commands or unexpected output.
+
+  Args:
+    ad: The Android device object.
+    iface_name: The name of the network interface (e.g., "wlan0").
+  """
+  cmd = f"cmd network_stack capture stop {iface_name}"
+
+  # Expect no output or Unknown command if NetworkStack is too old. Throw otherwise.
+  adb_output = AdbOutputHandler(ad, cmd).get_output()
+  if adb_output != "success":
+    raise assert_utils.UnexpectedBehaviorError(
+      f"Got unexpected output: {adb_output} for command: {cmd}."
+    )
+
+def get_matched_packet_counts(
+        ad: android_device.AndroidDevice,
+        iface_name: str,
+        packet_in_hex: str
+) -> int:
+  """Gets the number of captured packets matching a specific hexadecimal pattern.
+
+  This function retrieves the count of captured packets on the specified
+  network interface that match a given hexadecimal pattern. It uses an ADB
+  shell command and handles potential errors related to unsupported commands,
+  unexpected output, or invalid output format.
+
+  Args:
+    ad: The Android device object.
+    iface_name: The name of the network interface (e.g., "wlan0").
+    packet_in_hex: The hexadecimal string representing the packet pattern.
+
+  Returns:
+    The number of matched packets as an integer.
+  """
+  cmd = f"cmd network_stack capture matched-packet-counts {iface_name} {packet_in_hex}"
+
+  # Expect no output or Unknown command if NetworkStack is too old. Throw otherwise.
+  adb_output = AdbOutputHandler(ad, cmd).get_output()
+  try:
+    return int(adb_output)
+  except ValueError as e:
+    raise assert_utils.UnexpectedBehaviorError(
+      f"Got unexpected exception: {e} for command: {cmd}."
+    )
 
 @dataclass
 class ApfCapabilities:
@@ -315,3 +394,19 @@
       f"Supported apf version {caps.apf_version_supported} < expected version"
       f" {expected_version}",
   )
+
+class AdbOutputHandler:
+  def __init__(self, ad, cmd):
+    self._ad = ad
+    self._cmd = cmd
+
+  def get_output(self) -> str:
+    try:
+      return adb_utils.adb_shell(self._ad, self._cmd)
+    except AdbError as e:
+      output = str(e.stdout)
+      if "Unknown command" in output:
+        raise UnsupportedOperationException(
+          f"{self._cmd} is not supported."
+        )
+      return output
\ No newline at end of file