Merge "Document reason for not using SingleWriterBpfMap in ClatCoordinator" into main
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index 332979b..4877a4b 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -270,5 +270,5 @@
 static inline bool is_system_uid(uint32_t uid) {
     // MIN_SYSTEM_UID is AID_ROOT == 0, so uint32_t is *always* >= 0
     // MAX_SYSTEM_UID is AID_NOBODY == 9999, while AID_APP_START == 10000
-    return (uid < AID_APP_START);
+    return ((uid % AID_USER_OFFSET) < AID_APP_START);
 }
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 2f675a9..d8cccb2 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -180,8 +180,18 @@
         return new ArrayList<>(mHostAddresses);
     }
 
-    /** Set the host addresses */
+    /**
+     * Set the host addresses.
+     *
+     * <p>When registering hosts/services, there can only be one registration including address
+     * records for a given hostname.
+     *
+     * <p>For example, if a client registers a service with the hostname "MyHost" and the address
+     * records of 192.168.1.1 and 192.168.1.2, then other registrations for the hostname "MyHost"
+     * must not have any address record, otherwise there will be a conflict.
+     */
     public void setHostAddresses(@NonNull List<InetAddress> addresses) {
+        // TODO: b/284905335 - Notify the client when there is a conflict.
         mHostAddresses.clear();
         mHostAddresses.addAll(addresses);
     }
diff --git a/framework/src/android/net/BpfNetMapsUtils.java b/framework/src/android/net/BpfNetMapsUtils.java
index 4099e2a..282a11e 100644
--- a/framework/src/android/net/BpfNetMapsUtils.java
+++ b/framework/src/android/net/BpfNetMapsUtils.java
@@ -68,6 +68,7 @@
 import android.os.Build;
 import android.os.Process;
 import android.os.ServiceSpecificException;
+import android.os.UserHandle;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.Pair;
@@ -330,9 +331,9 @@
     ) {
         throwIfPreT("isUidBlockedByFirewallChains is not available on pre-T devices");
 
-        // System uid is not blocked by firewall chains, see bpf_progs/netd.c
-        // TODO: use UserHandle.isCore() once it is accessible
-        if (uid < Process.FIRST_APPLICATION_UID) {
+        // System uids are not blocked by firewall chains, see bpf_progs/netd.c
+        // TODO: b/348513058 - use UserHandle.isCore() once it is accessible
+        if (UserHandle.getAppId(uid) < Process.FIRST_APPLICATION_UID) {
             return false;
         }
 
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 4eaf973..ffaf41f 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -71,23 +71,29 @@
 import android.telephony.TelephonyManager;
 import android.util.ArrayMap;
 import android.util.Log;
+import android.util.LruCache;
 import android.util.Range;
 import android.util.SparseIntArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
 
 import libcore.net.event.NetworkEventDispatcher;
 
 import java.io.IOException;
 import java.io.UncheckedIOException;
+import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Method;
 import java.net.DatagramSocket;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Socket;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
@@ -1231,6 +1237,19 @@
     @GuardedBy("mTetheringEventCallbacks")
     private TetheringManager mTetheringManager;
 
+    // Cache of the most recently used NetworkCallback classes (not instances) -> method flags.
+    // 100 is chosen kind arbitrarily as an unlikely number of different types of NetworkCallback
+    // overrides that a process may have, and should generally not be reached (for example, the
+    // system server services.jar has been observed with dexdump to have only 16 when this was
+    // added, and a very large system services app only had 18).
+    // If this number is exceeded, the code will still function correctly, but re-registering
+    // using a network callback class that was used before, but 100+ other classes have been used in
+    // the meantime, will be a bit slower (as slow as the first registration) because
+    // getDeclaredMethodsFlag must re-examine the callback class to determine what methods it
+    // overrides.
+    private static final LruCache<Class<? extends NetworkCallback>, Integer> sMethodFlagsCache =
+            new LruCache<>(100);
+
     private final Object mEnabledConnectivityManagerFeaturesLock = new Object();
     // mEnabledConnectivityManagerFeatures is lazy-loaded in this ConnectivityManager instance, but
     // fetched from ConnectivityService, where it is loaded in ConnectivityService startup, so it
@@ -3996,6 +4015,55 @@
      */
     public static class NetworkCallback {
         /**
+         * Bitmask of method flags with all flags set.
+         * @hide
+         */
+        public static final int DECLARED_METHODS_ALL = ~0;
+
+        /**
+         * Bitmask of method flags with no flag set.
+         * @hide
+         */
+        public static final int DECLARED_METHODS_NONE = 0;
+
+        // Tracks whether an instance was created via reflection without calling the constructor.
+        private final boolean mConstructorWasCalled;
+
+        /**
+         * Annotation for NetworkCallback methods to verify filtering is configured properly.
+         *
+         * This is only used in tests to ensure that tests fail when a new callback is added, or
+         * callbacks are modified, without updating
+         * {@link NetworkCallbackMethodsHolder#NETWORK_CB_METHODS} properly.
+         * @hide
+         */
+        @Retention(RetentionPolicy.RUNTIME)
+        @Target(ElementType.METHOD)
+        @VisibleForTesting
+        public @interface FilteredCallback {
+            /**
+             * The NetworkCallback.METHOD_* ID of this method.
+             */
+            int methodId();
+
+            /**
+             * The ConnectivityManager.CALLBACK_* message that this method is directly called by.
+             *
+             * If this method is not called by any message, this should be
+             * {@link #CALLBACK_TRANSITIVE_CALLS_ONLY}.
+             */
+            int calledByCallbackId();
+
+            /**
+             * If this method may call other NetworkCallback methods, an array of methods it calls.
+             *
+             * Only direct calls (not transitive calls) should be included. The IDs must be
+             * NetworkCallback.METHOD_* IDs.
+             */
+            int[] mayCall() default {};
+        }
+
+        /**
          * No flags associated with this callback.
          * @hide
          */
@@ -4058,6 +4126,7 @@
                 throw new IllegalArgumentException("Invalid flags");
             }
             mFlags = flags;
+            mConstructorWasCalled = true;
         }
 
         /**
@@ -4075,7 +4144,9 @@
          *
          * @hide
          */
+        @FilteredCallback(methodId = METHOD_ONPRECHECK, calledByCallbackId = CALLBACK_PRECHECK)
         public void onPreCheck(@NonNull Network network) {}
+        private static final int METHOD_ONPRECHECK = 1;
 
         /**
          * Called when the framework connects and has declared a new network ready for use.
@@ -4090,6 +4161,11 @@
          * @param blocked Whether access to the {@link Network} is blocked due to system policy.
          * @hide
          */
+        @FilteredCallback(methodId = METHOD_ONAVAILABLE_5ARGS,
+                calledByCallbackId = CALLBACK_AVAILABLE,
+                mayCall = { METHOD_ONAVAILABLE_4ARGS,
+                        METHOD_ONLOCALNETWORKINFOCHANGED,
+                        METHOD_ONBLOCKEDSTATUSCHANGED_INT })
         public final void onAvailable(@NonNull Network network,
                 @NonNull NetworkCapabilities networkCapabilities,
                 @NonNull LinkProperties linkProperties,
@@ -4102,6 +4178,7 @@
             if (null != localInfo) onLocalNetworkInfoChanged(network, localInfo);
             onBlockedStatusChanged(network, blocked);
         }
+        private static final int METHOD_ONAVAILABLE_5ARGS = 2;
 
         /**
          * Legacy variant of onAvailable that takes a boolean blocked reason.
@@ -4114,6 +4191,13 @@
          *
          * @hide
          */
+        @FilteredCallback(methodId = METHOD_ONAVAILABLE_4ARGS,
+                calledByCallbackId = CALLBACK_TRANSITIVE_CALLS_ONLY,
+                mayCall = { METHOD_ONAVAILABLE_1ARG,
+                        METHOD_ONNETWORKSUSPENDED,
+                        METHOD_ONCAPABILITIESCHANGED,
+                        METHOD_ONLINKPROPERTIESCHANGED
+                })
         public void onAvailable(@NonNull Network network,
                 @NonNull NetworkCapabilities networkCapabilities,
                 @NonNull LinkProperties linkProperties,
@@ -4127,6 +4211,7 @@
             onLinkPropertiesChanged(network, linkProperties);
             // No call to onBlockedStatusChanged here. That is done by the caller.
         }
+        private static final int METHOD_ONAVAILABLE_4ARGS = 3;
 
         /**
          * Called when the framework connects and has declared a new network ready for use.
@@ -4157,7 +4242,10 @@
          *
          * @param network The {@link Network} of the satisfying network.
          */
+        @FilteredCallback(methodId = METHOD_ONAVAILABLE_1ARG,
+                calledByCallbackId = CALLBACK_TRANSITIVE_CALLS_ONLY)
         public void onAvailable(@NonNull Network network) {}
+        private static final int METHOD_ONAVAILABLE_1ARG = 4;
 
         /**
          * Called when the network is about to be lost, typically because there are no outstanding
@@ -4176,7 +4264,9 @@
          *                    connected for graceful handover; note that the network may still
          *                    suffer a hard loss at any time.
          */
+        @FilteredCallback(methodId = METHOD_ONLOSING, calledByCallbackId = CALLBACK_LOSING)
         public void onLosing(@NonNull Network network, int maxMsToLive) {}
+        private static final int METHOD_ONLOSING = 5;
 
         /**
          * Called when a network disconnects or otherwise no longer satisfies this request or
@@ -4197,7 +4287,9 @@
          *
          * @param network The {@link Network} lost.
          */
+        @FilteredCallback(methodId = METHOD_ONLOST, calledByCallbackId = CALLBACK_LOST)
         public void onLost(@NonNull Network network) {}
+        private static final int METHOD_ONLOST = 6;
 
         /**
          * Called if no network is found within the timeout time specified in
@@ -4207,7 +4299,9 @@
          * {@link NetworkRequest} will have already been removed and released, as if
          * {@link #unregisterNetworkCallback(NetworkCallback)} had been called.
          */
+        @FilteredCallback(methodId = METHOD_ONUNAVAILABLE, calledByCallbackId = CALLBACK_UNAVAIL)
         public void onUnavailable() {}
+        private static final int METHOD_ONUNAVAILABLE = 7;
 
         /**
          * Called when the network corresponding to this request changes capabilities but still
@@ -4224,8 +4318,11 @@
          * @param networkCapabilities The new {@link NetworkCapabilities} for this
          *                            network.
          */
+        @FilteredCallback(methodId = METHOD_ONCAPABILITIESCHANGED,
+                calledByCallbackId = CALLBACK_CAP_CHANGED)
         public void onCapabilitiesChanged(@NonNull Network network,
                 @NonNull NetworkCapabilities networkCapabilities) {}
+        private static final int METHOD_ONCAPABILITIESCHANGED = 8;
 
         /**
          * Called when the network corresponding to this request changes {@link LinkProperties}.
@@ -4240,8 +4337,11 @@
          * @param network The {@link Network} whose link properties have changed.
          * @param linkProperties The new {@link LinkProperties} for this network.
          */
+        @FilteredCallback(methodId = METHOD_ONLINKPROPERTIESCHANGED,
+                calledByCallbackId = CALLBACK_IP_CHANGED)
         public void onLinkPropertiesChanged(@NonNull Network network,
                 @NonNull LinkProperties linkProperties) {}
+        private static final int METHOD_ONLINKPROPERTIESCHANGED = 9;
 
         /**
          * Called when there is a change in the {@link LocalNetworkInfo} for this network.
@@ -4253,8 +4353,11 @@
          * @param localNetworkInfo the new {@link LocalNetworkInfo} for this network.
          * @hide
          */
+        @FilteredCallback(methodId = METHOD_ONLOCALNETWORKINFOCHANGED,
+                calledByCallbackId = CALLBACK_LOCAL_NETWORK_INFO_CHANGED)
         public void onLocalNetworkInfoChanged(@NonNull Network network,
                 @NonNull LocalNetworkInfo localNetworkInfo) {}
+        private static final int METHOD_ONLOCALNETWORKINFOCHANGED = 10;
 
         /**
          * Called when the network the framework connected to for this request suspends data
@@ -4273,7 +4376,10 @@
          *
          * @hide
          */
+        @FilteredCallback(methodId = METHOD_ONNETWORKSUSPENDED,
+                calledByCallbackId = CALLBACK_SUSPENDED)
         public void onNetworkSuspended(@NonNull Network network) {}
+        private static final int METHOD_ONNETWORKSUSPENDED = 11;
 
         /**
          * Called when the network the framework connected to for this request
@@ -4287,7 +4393,9 @@
          *
          * @hide
          */
+        @FilteredCallback(methodId = METHOD_ONNETWORKRESUMED, calledByCallbackId = CALLBACK_RESUMED)
         public void onNetworkResumed(@NonNull Network network) {}
+        private static final int METHOD_ONNETWORKRESUMED = 12;
 
         /**
          * Called when access to the specified network is blocked or unblocked.
@@ -4300,7 +4408,10 @@
          * @param network The {@link Network} whose blocked status has changed.
          * @param blocked The blocked status of this {@link Network}.
          */
+        @FilteredCallback(methodId = METHOD_ONBLOCKEDSTATUSCHANGED_BOOL,
+                calledByCallbackId = CALLBACK_TRANSITIVE_CALLS_ONLY)
         public void onBlockedStatusChanged(@NonNull Network network, boolean blocked) {}
+        private static final int METHOD_ONBLOCKEDSTATUSCHANGED_BOOL = 13;
 
         /**
          * Called when access to the specified network is blocked or unblocked, or the reason for
@@ -4318,10 +4429,14 @@
          * @param blocked The blocked status of this {@link Network}.
          * @hide
          */
+        @FilteredCallback(methodId = METHOD_ONBLOCKEDSTATUSCHANGED_INT,
+                calledByCallbackId = CALLBACK_BLK_CHANGED,
+                mayCall = { METHOD_ONBLOCKEDSTATUSCHANGED_BOOL })
         @SystemApi(client = MODULE_LIBRARIES)
         public void onBlockedStatusChanged(@NonNull Network network, @BlockedReason int blocked) {
             onBlockedStatusChanged(network, blocked != 0);
         }
+        private static final int METHOD_ONBLOCKEDSTATUSCHANGED_INT = 14;
 
         private NetworkRequest networkRequest;
         private final int mFlags;
@@ -4349,6 +4464,7 @@
         }
     }
 
+    private static final int CALLBACK_TRANSITIVE_CALLS_ONLY     = 0;
     /** @hide */
     public static final int CALLBACK_PRECHECK                   = 1;
     /** @hide */
@@ -4374,9 +4490,11 @@
     /** @hide */
     public static final int CALLBACK_LOCAL_NETWORK_INFO_CHANGED = 12;
 
+
     /** @hide */
     public static String getCallbackName(int whichCallback) {
         switch (whichCallback) {
+            case CALLBACK_TRANSITIVE_CALLS_ONLY: return "CALLBACK_TRANSITIVE_CALLS_ONLY";
             case CALLBACK_PRECHECK:     return "CALLBACK_PRECHECK";
             case CALLBACK_AVAILABLE:    return "CALLBACK_AVAILABLE";
             case CALLBACK_LOSING:       return "CALLBACK_LOSING";
@@ -4394,6 +4512,68 @@
         }
     }
 
+    /** @hide */
+    @VisibleForTesting
+    public static class NetworkCallbackMethod {
+        @NonNull
+        public final String mName;
+        @NonNull
+        public final Class<?>[] mParameterTypes;
+        // Bitmask of CALLBACK_* that may transitively call this method.
+        public final int mCallbacksCallingThisMethod;
+
+        public NetworkCallbackMethod(@NonNull String name, @NonNull Class<?>[] parameterTypes,
+                int callbacksCallingThisMethod) {
+            mName = name;
+            mParameterTypes = parameterTypes;
+            mCallbacksCallingThisMethod = callbacksCallingThisMethod;
+        }
+    }
+
+    // Holder class for the list of NetworkCallbackMethod. This ensures the list is only created
+    // once on first usage, and not just on ConnectivityManager class initialization.
+    /** @hide */
+    @VisibleForTesting
+    public static class NetworkCallbackMethodsHolder {
+        public static final NetworkCallbackMethod[] NETWORK_CB_METHODS =
+                new NetworkCallbackMethod[] {
+                        method("onPreCheck", 1 << CALLBACK_PRECHECK, Network.class),
+                        // Note the final overload of onAvailable is not included, since it cannot
+                        // match any overridden method.
+                        method("onAvailable", 1 << CALLBACK_AVAILABLE, Network.class),
+                        method("onAvailable", 1 << CALLBACK_AVAILABLE,
+                                Network.class, NetworkCapabilities.class,
+                                LinkProperties.class, boolean.class),
+                        method("onLosing", 1 << CALLBACK_LOSING, Network.class, int.class),
+                        method("onLost", 1 << CALLBACK_LOST, Network.class),
+                        method("onUnavailable", 1 << CALLBACK_UNAVAIL),
+                        method("onCapabilitiesChanged",
+                                1 << CALLBACK_CAP_CHANGED | 1 << CALLBACK_AVAILABLE,
+                                Network.class, NetworkCapabilities.class),
+                        method("onLinkPropertiesChanged",
+                                1 << CALLBACK_IP_CHANGED | 1 << CALLBACK_AVAILABLE,
+                                Network.class, LinkProperties.class),
+                        method("onLocalNetworkInfoChanged",
+                                1 << CALLBACK_LOCAL_NETWORK_INFO_CHANGED | 1 << CALLBACK_AVAILABLE,
+                                Network.class, LocalNetworkInfo.class),
+                        method("onNetworkSuspended",
+                                1 << CALLBACK_SUSPENDED | 1 << CALLBACK_AVAILABLE, Network.class),
+                        method("onNetworkResumed",
+                                1 << CALLBACK_RESUMED, Network.class),
+                        method("onBlockedStatusChanged",
+                                1 << CALLBACK_BLK_CHANGED | 1 << CALLBACK_AVAILABLE,
+                                Network.class, boolean.class),
+                        method("onBlockedStatusChanged",
+                                1 << CALLBACK_BLK_CHANGED | 1 << CALLBACK_AVAILABLE,
+                                Network.class, int.class),
+                };
+
+        private static NetworkCallbackMethod method(
+                String name, int callbacksCallingThisMethod, Class<?>... args) {
+            return new NetworkCallbackMethod(name, args, callbacksCallingThisMethod);
+        }
+    }
+
     private static class CallbackHandler extends Handler {
         private static final String TAG = "ConnectivityManager.CallbackHandler";
         private static final boolean DBG = false;
@@ -4513,6 +4693,14 @@
         if (reqType != TRACK_DEFAULT && reqType != TRACK_SYSTEM_DEFAULT && need == null) {
             throw new IllegalArgumentException("null NetworkCapabilities");
         }
+
+
+        final boolean useDeclaredMethods = isFeatureEnabled(
+                FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS);
+        // Set all bits if the feature is disabled
+        int declaredMethodsFlag = useDeclaredMethods
+                ? tryGetDeclaredMethodsFlag(callback)
+                : NetworkCallback.DECLARED_METHODS_ALL;
         final NetworkRequest request;
         final String callingPackageName = mContext.getOpPackageName();
         try {
@@ -4529,11 +4717,12 @@
                 if (reqType == LISTEN) {
                     request = mService.listenForNetwork(
                             need, messenger, binder, callbackFlags, callingPackageName,
-                            getAttributionTag());
+                            getAttributionTag(), declaredMethodsFlag);
                 } else {
                     request = mService.requestNetwork(
                             asUid, need, reqType.ordinal(), messenger, timeoutMs, binder,
-                            legacyType, callbackFlags, callingPackageName, getAttributionTag());
+                            legacyType, callbackFlags, callingPackageName, getAttributionTag(),
+                            declaredMethodsFlag);
                 }
                 if (request != null) {
                     sCallbacks.put(request, callback);
@@ -4548,6 +4737,108 @@
         return request;
     }
 
+    private int tryGetDeclaredMethodsFlag(@NonNull NetworkCallback cb) {
+        if (!cb.mConstructorWasCalled) {
+            // Do not use the optimization if the callback was created via reflection or mocking,
+            // as for example with dexmaker-mockito-inline methods will be instrumented without
+            // using subclasses. This does not catch all cases as it is still possible to call the
+            // constructor when creating mocks, but by default constructors are not called in that
+            // case.
+            return NetworkCallback.DECLARED_METHODS_ALL;
+        }
+        try {
+            return getDeclaredMethodsFlag(cb.getClass());
+        } catch (LinkageError e) {
+            // This may happen if some methods reference inaccessible classes in their arguments
+            // (for example b/261807130).
+            Log.w(TAG, "Could not get methods from NetworkCallback class", e);
+            // Fall through
+        } catch (Throwable e) {
+            // Log.wtf would be best but this is in app process, so the TerribleFailureHandler may
+            // have unknown effects, possibly crashing the app (default behavior on eng builds or
+            // if the WTF_IS_FATAL setting is set).
+            Log.e(TAG, "Unexpected error while getting methods from NetworkCallback class", e);
+            // Fall through
+        }
+        return NetworkCallback.DECLARED_METHODS_ALL;
+    }
+
+    private static int getDeclaredMethodsFlag(@NonNull Class<? extends NetworkCallback> clazz) {
+        final Integer cachedFlags = sMethodFlagsCache.get(clazz);
+        // As this is not synchronized, it is possible that this method will calculate the
+        // flags for a given class multiple times, but that is fine. LruCache itself is thread-safe.
+        if (cachedFlags != null) {
+            return cachedFlags;
+        }
+
+        int flag = 0;
+        // This uses getMethods instead of getDeclaredMethods, to make sure that if A overrides B
+        // that overrides NetworkCallback, A.getMethods also returns methods declared by B.
+        for (Method classMethod : clazz.getMethods()) {
+            final Class<?> declaringClass = classMethod.getDeclaringClass();
+            if (declaringClass == NetworkCallback.class) {
+                // The callback is as defined by NetworkCallback and not overridden
+                continue;
+            }
+            if (declaringClass == Object.class) {
+                // Optimization: no need to try to match callbacks for methods declared by Object
+                continue;
+            }
+            flag |= getCallbackIdsCallingThisMethod(classMethod);
+        }
+
+        if (flag == 0) {
+            // dexmaker-mockito-inline (InlineDexmakerMockMaker), for example for mockito-extended,
+            // modifies bytecode of classes in-place to add hooks instead of creating subclasses,
+            // which would not be detected. When no method is found, fall back to enabling callbacks
+            // for all methods.
+            // This will not catch the case where both NetworkCallback bytecode is modified and a
+            // subclass of NetworkCallback that has some overridden methods are used. But this kind
+            // of bytecode injection is only possible in debuggable processes, with a JVMTI debug
+            // agent attached, so it should not cause real issues.
+            // There may be legitimate cases where an empty callback is filed with no method
+            // overridden, for example requestNetwork(requestForCell, new NetworkCallback()) which
+            // would ensure that one cell network stays up. But there is no way to differentiate
+            // such NetworkCallbacks from a mock that called the constructor, so this code will
+            // register the callback with DECLARED_METHODS_ALL and turn off the optimization in that
+            // case. Apps are not expected to do this often anyway since the usefulness is very
+            // limited.
+            flag = NetworkCallback.DECLARED_METHODS_ALL;
+        }
+        sMethodFlagsCache.put(clazz, flag);
+        return flag;
+    }
+
+    /**
+     * Find out which of the base methods in NetworkCallback will call this method.
+     *
+     * For example, in the case of onLinkPropertiesChanged, this will be
+     * (1 << CALLBACK_IP_CHANGED) | (1 << CALLBACK_AVAILABLE).
+     */
+    private static int getCallbackIdsCallingThisMethod(@NonNull Method method) {
+        for (NetworkCallbackMethod baseMethod : NetworkCallbackMethodsHolder.NETWORK_CB_METHODS) {
+            if (!baseMethod.mName.equals(method.getName())) {
+                continue;
+            }
+            Class<?>[] methodParams = method.getParameterTypes();
+
+            // As per JLS 8.4.8.1., a method m1 must have a subsignature of method m2 to override
+            // it. And as per JLS 8.4.2, this means the erasure of the signature of m2 must be the
+            // same as the signature of m1. Since type erasure is done at compile time, with
+            // reflection the erased types are already observed, so the (erased) parameter types
+            // must be equal.
+            // So for example a method that is identical to a NetworkCallback method, except with
+            // one parameter being a subclass of the parameter in the original method, will never
+            // be called since it is not an override (the erasure of the arguments are not the same)
+            // Therefore, the method is an override only if methodParams is exactly equal to
+            // the base method's parameter types.
+            if (Arrays.equals(baseMethod.mParameterTypes, methodParams)) {
+                return baseMethod.mCallbacksCallingThisMethod;
+            }
+        }
+        return 0;
+    }
+
     private boolean isFeatureEnabled(@ConnectivityManagerFeature long connectivityManagerFeature) {
         synchronized (mEnabledConnectivityManagerFeaturesLock) {
             if (mEnabledConnectivityManagerFeatures == null) {
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index f9de8ed..988cc92 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -153,7 +153,8 @@
 
     NetworkRequest requestNetwork(int uid, in NetworkCapabilities networkCapabilities, int reqType,
             in Messenger messenger, int timeoutSec, in IBinder binder, int legacy,
-            int callbackFlags, String callingPackageName, String callingAttributionTag);
+            int callbackFlags, String callingPackageName, String callingAttributionTag,
+            int declaredMethodsFlag);
 
     NetworkRequest pendingRequestForNetwork(in NetworkCapabilities networkCapabilities,
             in PendingIntent operation, String callingPackageName, String callingAttributionTag);
@@ -162,7 +163,7 @@
 
     NetworkRequest listenForNetwork(in NetworkCapabilities networkCapabilities,
             in Messenger messenger, in IBinder binder, int callbackFlags, String callingPackageName,
-            String callingAttributionTag);
+            String callingAttributionTag, int declaredMethodsFlag);
 
     void pendingListenForNetwork(in NetworkCapabilities networkCapabilities,
             in PendingIntent operation, String callingPackageName,
diff --git a/netbpfload/NetBpfLoad.cpp b/netbpfload/NetBpfLoad.cpp
index e9c6d8a..0d4a5c4 100644
--- a/netbpfload/NetBpfLoad.cpp
+++ b/netbpfload/NetBpfLoad.cpp
@@ -57,6 +57,7 @@
 using base::StartsWith;
 using base::EndsWith;
 using std::string;
+using std::vector;
 
 static bool exists(const char* const path) {
     int v = access(path, F_OK);
@@ -248,6 +249,22 @@
     return tv;
 }
 
+static bool isWear() {
+    static string wearSdkStr = base::GetProperty("ro.cw_build.wear_sdk.version", "");
+    static int wearSdkInt = base::GetIntProperty("ro.cw_build.wear_sdk.version", 0);
+    static string buildChars = base::GetProperty("ro.build.characteristics", "");
+    static vector<string> v = base::Tokenize(buildChars, ",");
+    static bool watch = (std::find(v.begin(), v.end(), "watch") != v.end());
+    static bool wear = (wearSdkInt > 0) || watch;
+    static bool logged = false;
+    if (!logged) {
+        logged = true;
+        ALOGI("isWear(ro.cw_build.wear_sdk.version=%d[%s] ro.build.characteristics='%s'): %s",
+              wearSdkInt, wearSdkStr.c_str(), buildChars.c_str(), wear ? "true" : "false");
+    }
+    return wear;
+}
+
 static int doLoad(char** argv, char * const envp[]) {
     const bool runningAsRoot = !getuid();  // true iff U QPR3 or V+
 
@@ -402,7 +419,8 @@
          * and 32-bit userspace on 64-bit kernel bpf ringbuffer compatibility is broken.
          */
         ALOGE("64-bit userspace required on 6.2+ kernels.");
-        if (!isTV()) return 1;
+        // Stuff won't work reliably, but exempt TVs & Arm Wear devices
+        if (!isTV() && !(isWear() && isArm())) return 1;
     }
 
     // Ensure we can determine the Android build type.
diff --git a/service/lint-baseline.xml b/service/lint-baseline.xml
index 3e11d52..b09589c 100644
--- a/service/lint-baseline.xml
+++ b/service/lint-baseline.xml
@@ -3,17 +3,6 @@
 
     <issue
         id="NewApi"
-        message="Call requires API level 33 (current min is 30): `getUidRule`"
-        errorLine1="        return BpfNetMapsReader.getUidRule(sUidOwnerMap, childChain, uid);"
-        errorLine2="                                ~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/service/src/com/android/server/BpfNetMaps.java"
-            line="643"
-            column="33"/>
-    </issue>
-
-    <issue
-        id="NewApi"
         message="Call requires API level 31 (current min is 30): `BpfBitmap`"
         errorLine1="                return new BpfBitmap(BLOCKED_PORTS_MAP_PATH);"
         errorLine2="                       ~~~~~~~~~~~~~">
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index b3e7d8c..44868b2d 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -580,6 +580,7 @@
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
      *                                  cause of the failure.
      */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     public int getUidRule(final int childChain, final int uid) {
         return BpfNetMapsUtils.getUidRule(sUidOwnerMap, childChain, uid);
     }
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 3dee305..2a3058c 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -46,6 +46,7 @@
 import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
 import static android.net.ConnectivityManager.FIREWALL_RULE_DEFAULT;
 import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
+import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_NONE;
 import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
 import static android.net.ConnectivityManager.TYPE_ETHERNET;
 import static android.net.ConnectivityManager.TYPE_MOBILE;
@@ -65,6 +66,7 @@
 import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
 import static android.net.ConnectivityManager.getNetworkTypeName;
 import static android.net.ConnectivityManager.isNetworkTypeValid;
+import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_ALL;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
 import static android.net.INetd.PERMISSION_INTERNET;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS;
@@ -136,8 +138,6 @@
 import static com.android.server.connectivity.ConnectivityFlags.REQUEST_RESTRICTED_WIFI;
 import static com.android.server.connectivity.ConnectivityFlags.INGRESS_TO_VPN_ADDRESS_FILTERING;
 
-import static java.util.Map.Entry;
-
 import android.Manifest;
 import android.annotation.CheckResult;
 import android.annotation.NonNull;
@@ -1788,7 +1788,7 @@
         mDefaultRequest = new NetworkRequestInfo(
                 Process.myUid(), defaultInternetRequest, null,
                 null /* binder */, NetworkCallback.FLAG_INCLUDE_LOCATION_INFO,
-                null /* attributionTags */);
+                null /* attributionTags */, DECLARED_METHODS_NONE);
         mNetworkRequests.put(defaultInternetRequest, mDefaultRequest);
         mDefaultNetworkRequests.add(mDefaultRequest);
         mNetworkRequestInfoLogs.log("REGISTER " + mDefaultRequest);
@@ -2175,7 +2175,7 @@
             handleRegisterNetworkRequest(new NetworkRequestInfo(
                     Process.myUid(), networkRequest, null /* messenger */, null /* binder */,
                     NetworkCallback.FLAG_INCLUDE_LOCATION_INFO,
-                    null /* attributionTags */));
+                    null /* attributionTags */, DECLARED_METHODS_NONE));
         } else {
             handleReleaseNetworkRequest(networkRequest, Process.SYSTEM_UID,
                     /* callOnUnavailable */ false);
@@ -7577,6 +7577,8 @@
         // Preference order of this request.
         final int mPreferenceOrder;
 
+        final int mDeclaredMethodsFlags;
+
         // In order to preserve the mapping of NetworkRequest-to-callback when apps register
         // callbacks using a returned NetworkRequest, the original NetworkRequest needs to be
         // maintained for keying off of. This is only a concern when the original nri
@@ -7630,21 +7632,22 @@
             mCallbackFlags = NetworkCallback.FLAG_NONE;
             mCallingAttributionTag = callingAttributionTag;
             mPreferenceOrder = preferenceOrder;
+            mDeclaredMethodsFlags = DECLARED_METHODS_NONE;
         }
 
         NetworkRequestInfo(int asUid, @NonNull final NetworkRequest r, @Nullable final Messenger m,
                 @Nullable final IBinder binder,
                 @NetworkCallback.Flag int callbackFlags,
-                @Nullable String callingAttributionTag) {
+                @Nullable String callingAttributionTag, int declaredMethodsFlags) {
             this(asUid, Collections.singletonList(r), r, m, binder, callbackFlags,
-                    callingAttributionTag);
+                    callingAttributionTag, declaredMethodsFlags);
         }
 
         NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r,
                 @NonNull final NetworkRequest requestForCallback, @Nullable final Messenger m,
                 @Nullable final IBinder binder,
                 @NetworkCallback.Flag int callbackFlags,
-                @Nullable String callingAttributionTag) {
+                @Nullable String callingAttributionTag, int declaredMethodsFlags) {
             super();
             ensureAllNetworkRequestsHaveType(r);
             mRequests = initializeRequests(r);
@@ -7659,6 +7662,7 @@
             mCallbackFlags = callbackFlags;
             mCallingAttributionTag = callingAttributionTag;
             mPreferenceOrder = PREFERENCE_ORDER_INVALID;
+            mDeclaredMethodsFlags = declaredMethodsFlags;
             linkDeathRecipient();
         }
 
@@ -7699,6 +7703,7 @@
             mCallingAttributionTag = nri.mCallingAttributionTag;
             mUidTrackedForBlockedStatus = nri.mUidTrackedForBlockedStatus;
             mPreferenceOrder = PREFERENCE_ORDER_INVALID;
+            mDeclaredMethodsFlags = nri.mDeclaredMethodsFlags;
             linkDeathRecipient();
         }
 
@@ -7786,7 +7791,8 @@
                     + (mPendingIntent == null ? "" : " to trigger " + mPendingIntent)
                     + " callback flags: " + mCallbackFlags
                     + " order: " + mPreferenceOrder
-                    + " isUidTracked: " + mUidTrackedForBlockedStatus;
+                    + " isUidTracked: " + mUidTrackedForBlockedStatus
+                    + " declaredMethods: 0x" + Integer.toHexString(mDeclaredMethodsFlags);
         }
     }
 
@@ -7924,7 +7930,21 @@
     public NetworkRequest requestNetwork(int asUid, NetworkCapabilities networkCapabilities,
             int reqTypeInt, Messenger messenger, int timeoutMs, final IBinder binder,
             int legacyType, int callbackFlags, @NonNull String callingPackageName,
-            @Nullable String callingAttributionTag) {
+            @Nullable String callingAttributionTag, int declaredMethodsFlag) {
+        if (declaredMethodsFlag == 0) {
+            // This could happen if raw binder calls are used to call the previous overload of
+            // requestNetwork, as missing int arguments in a binder call end up as 0
+            // (Parcel.readInt returns 0 at the end of a parcel). Such raw calls this would be
+            // really unexpected bad behavior from the caller though.
+            // TODO: remove after verifying this does not happen. This could allow enabling the
+            // optimization for callbacks that do not override any method (right now they use
+            // DECLARED_METHODS_ALL), if it is OK to break NetworkCallbacks created using
+            // dexmaker-mockito-inline and either spy() or MockSettings.useConstructor (see
+            // comment in ConnectivityManager which sets the flag to DECLARED_METHODS_ALL).
+            Log.wtf(TAG, "requestNetwork called without declaredMethodsFlag from "
+                    + callingPackageName);
+            declaredMethodsFlag = DECLARED_METHODS_ALL;
+        }
         if (legacyType != TYPE_NONE && !hasNetworkStackPermission()) {
             if (isTargetSdkAtleast(Build.VERSION_CODES.M, mDeps.getCallingUid(),
                     callingPackageName)) {
@@ -8016,7 +8036,7 @@
                 nextNetworkRequestId(), reqType);
         final NetworkRequestInfo nri = getNriToRegister(
                 asUid, networkRequest, messenger, binder, callbackFlags,
-                callingAttributionTag);
+                callingAttributionTag, declaredMethodsFlag);
         if (DBG) log("requestNetwork for " + nri);
         trackUidAndRegisterNetworkRequest(EVENT_REGISTER_NETWORK_REQUEST, nri);
         if (timeoutMs > 0) {
@@ -8042,7 +8062,7 @@
     private NetworkRequestInfo getNriToRegister(final int asUid, @NonNull final NetworkRequest nr,
             @Nullable final Messenger msgr, @Nullable final IBinder binder,
             @NetworkCallback.Flag int callbackFlags,
-            @Nullable String callingAttributionTag) {
+            @Nullable String callingAttributionTag, int declaredMethodsFlags) {
         final List<NetworkRequest> requests;
         if (NetworkRequest.Type.TRACK_DEFAULT == nr.type) {
             requests = copyDefaultNetworkRequestsForUid(
@@ -8051,7 +8071,8 @@
             requests = Collections.singletonList(nr);
         }
         return new NetworkRequestInfo(
-                asUid, requests, nr, msgr, binder, callbackFlags, callingAttributionTag);
+                asUid, requests, nr, msgr, binder, callbackFlags, callingAttributionTag,
+                declaredMethodsFlags);
     }
 
     private boolean shouldCheckCapabilitiesDeclaration(
@@ -8341,7 +8362,13 @@
     public NetworkRequest listenForNetwork(NetworkCapabilities networkCapabilities,
             Messenger messenger, IBinder binder,
             @NetworkCallback.Flag int callbackFlags,
-            @NonNull String callingPackageName, @NonNull String callingAttributionTag) {
+            @NonNull String callingPackageName, @NonNull String callingAttributionTag,
+            int declaredMethodsFlag) {
+        if (declaredMethodsFlag == 0) {
+            Log.wtf(TAG, "listenForNetwork called without declaredMethodsFlag from "
+                    + callingPackageName);
+            declaredMethodsFlag = DECLARED_METHODS_ALL;
+        }
         final int callingUid = mDeps.getCallingUid();
         if (!hasWifiNetworkListenPermission(networkCapabilities)) {
             enforceAccessPermission();
@@ -8363,7 +8390,7 @@
                 NetworkRequest.Type.LISTEN);
         NetworkRequestInfo nri =
                 new NetworkRequestInfo(callingUid, networkRequest, messenger, binder, callbackFlags,
-                        callingAttributionTag);
+                        callingAttributionTag, declaredMethodsFlag);
         if (VDBG) log("listenForNetwork for " + nri);
 
         trackUidAndRegisterNetworkRequest(EVENT_REGISTER_NETWORK_LISTENER, nri);
@@ -9805,7 +9832,8 @@
             configBuilder.setUpstreamSelector(nr);
             final NetworkRequestInfo nri = new NetworkRequestInfo(
                     nai.creatorUid, nr, null /* messenger */, null /* binder */,
-                    0 /* callbackFlags */, null /* attributionTag */);
+                    0 /* callbackFlags */, null /* attributionTag */,
+                    DECLARED_METHODS_NONE);
             if (null != oldSatisfier) {
                 // Set the old satisfier in the new NRI so that the rematch will see any changes
                 nri.setSatisfier(oldSatisfier, nr);
@@ -10176,6 +10204,11 @@
             // are Type.LISTEN, but should not have NetworkCallbacks invoked.
             return;
         }
+        if (mUseDeclaredMethodsForCallbacksEnabled
+                && (nri.mDeclaredMethodsFlags & (1 << notificationType)) == 0) {
+            // No need to send the notification as the recipient method is not overridden
+            return;
+        }
         final Bundle bundle = new Bundle();
         // TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects.
         // TODO: check if defensive copies of data is needed.
@@ -11800,6 +11833,10 @@
                         return 0;
                     }
                     case "get-package-networking-enabled": {
+                        if (!mDeps.isAtLeastT()) {
+                            throw new UnsupportedOperationException(
+                                    "This command is not supported on T-");
+                        }
                         final String packageName = getNextArg();
                         final int rule = getPackageFirewallRule(
                                 ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_3, packageName);
@@ -11829,6 +11866,10 @@
                         return 0;
                     }
                     case "get-background-networking-enabled-for-uid": {
+                        if (!mDeps.isAtLeastT()) {
+                            throw new UnsupportedOperationException(
+                                    "This command is not supported on T-");
+                        }
                         final Integer uid = parseIntegerArgument(getNextArg());
                         if (null == uid) {
                             onHelp();
@@ -13877,6 +13918,7 @@
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private int getPackageFirewallRule(final int chain, final String packageName)
             throws PackageManager.NameNotFoundException {
         final PackageManager pm = mContext.getPackageManager();
@@ -13884,6 +13926,7 @@
         return getUidFirewallRule(chain, appId);
     }
 
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     @Override
     public int getUidFirewallRule(final int chain, final int uid) {
         enforceNetworkStackOrSettingsPermission();
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 71f388d..34ea9ab 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -282,7 +282,7 @@
         "//apex_available:platform",
     ],
     lint: {
-        baseline_filename: "lint-baseline.xml",
+        strict_updatability_linting: true,
         error_checks: ["NewApi"],
     },
 }
diff --git a/staticlibs/device/com/android/net/module/util/SocketUtils.java b/staticlibs/device/com/android/net/module/util/SocketUtils.java
index 5e6a6c6..51671a6 100644
--- a/staticlibs/device/com/android/net/module/util/SocketUtils.java
+++ b/staticlibs/device/com/android/net/module/util/SocketUtils.java
@@ -19,8 +19,7 @@
 import static android.net.util.SocketUtils.closeSocket;
 
 import android.annotation.NonNull;
-import android.annotation.RequiresApi;
-import android.os.Build;
+import android.annotation.SuppressLint;
 import android.system.NetlinkSocketAddress;
 
 import java.io.FileDescriptor;
@@ -41,7 +40,11 @@
     /**
      * Make a socket address to communicate with netlink.
      */
-    @NonNull @RequiresApi(Build.VERSION_CODES.S)
+    // NetlinkSocketAddress was CorePlatformApi on R and linter warns this is available on S+.
+    // android.net.util.SocketUtils.makeNetlinkSocketAddress can be used instead, but this method
+    // has been used on R, so suppress the linter and keep as it is.
+    @SuppressLint("NewApi")
+    @NonNull
     public static SocketAddress makeNetlinkSocketAddress(int portId, int groupsMask) {
         return new NetlinkSocketAddress(portId, groupsMask);
     }
diff --git a/staticlibs/lint-baseline.xml b/staticlibs/lint-baseline.xml
deleted file mode 100644
index 2ee3a43..0000000
--- a/staticlibs/lint-baseline.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.4.0-alpha04" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha04">
-
-    <issue
-        id="NewApi"
-        message="Call requires API level 31 (current min is 30): `makeNetlinkSocketAddress`"
-        errorLine1="            Os.bind(fd, makeNetlinkSocketAddress(0, mBindGroups));"
-        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="packages/modules/Connectivity/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java"
-            line="111"
-            column="25"/>
-    </issue>
-
-</issues>
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index fa466f8..cf67a82 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -39,6 +39,7 @@
         "//packages/modules/NetworkStack/tests/integration",
     ],
     lint: {
+        strict_updatability_linting: true,
         test: true,
     },
 }
@@ -56,4 +57,7 @@
     ],
     jarjar_rules: "jarjar-rules.txt",
     test_suites: ["device-tests"],
+    lint: {
+        strict_updatability_linting: true,
+    },
 }
diff --git a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/MdnsTestUtils.kt
similarity index 99%
rename from tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
rename to staticlibs/testutils/devicetests/com/android/testutils/MdnsTestUtils.kt
index 93cec9c..8b88224 100644
--- a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/MdnsTestUtils.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.net.cts
+package com.android.testutils
 
 import android.net.DnsResolver
 import android.net.Network
diff --git a/tests/cts/multidevices/connectivity_multi_devices_test.py b/tests/cts/multidevices/connectivity_multi_devices_test.py
index 7a326cd..abd6fe2 100644
--- a/tests/cts/multidevices/connectivity_multi_devices_test.py
+++ b/tests/cts/multidevices/connectivity_multi_devices_test.py
@@ -5,6 +5,7 @@
 from mobly import test_runner
 from mobly import utils
 from mobly.controllers import android_device
+from utils import mdns_utils
 from utils import tether_utils
 from utils.tether_utils import UpstreamType
 
@@ -61,6 +62,25 @@
           self.serverDevice, UpstreamType.CELLULAR
       )
 
+  def test_mdns_via_hotspot(self):
+    tether_utils.assume_hotspot_test_preconditions(
+        self.serverDevice, self.clientDevice, UpstreamType.NONE
+    )
+    try:
+      # Connectivity of the client verified by asserting the validated capability.
+      tether_utils.setup_hotspot_and_client_for_upstream_type(
+        self.serverDevice, self.clientDevice, UpstreamType.NONE
+      )
+      mdns_utils.register_mdns_service_and_discover_resolve(
+        self.clientDevice, self.serverDevice
+      )
+    finally:
+      mdns_utils.cleanup_mdns_service(
+        self.clientDevice, self.serverDevice
+      )
+      tether_utils.cleanup_tethering_for_upstream_type(
+        self.serverDevice, UpstreamType.NONE
+      )
 
 if __name__ == "__main__":
   # Take test args
diff --git a/tests/cts/multidevices/snippet/Android.bp b/tests/cts/multidevices/snippet/Android.bp
index 5940cbb..b0b32c2 100644
--- a/tests/cts/multidevices/snippet/Android.bp
+++ b/tests/cts/multidevices/snippet/Android.bp
@@ -25,6 +25,7 @@
     ],
     srcs: [
         "ConnectivityMultiDevicesSnippet.kt",
+        "MdnsMultiDevicesSnippet.kt",
     ],
     manifest: "AndroidManifest.xml",
     static_libs: [
diff --git a/tests/cts/multidevices/snippet/AndroidManifest.xml b/tests/cts/multidevices/snippet/AndroidManifest.xml
index 9ed8146..967e581 100644
--- a/tests/cts/multidevices/snippet/AndroidManifest.xml
+++ b/tests/cts/multidevices/snippet/AndroidManifest.xml
@@ -27,7 +27,8 @@
          of a snippet class -->
     <meta-data
         android:name="mobly-snippets"
-        android:value="com.google.snippet.connectivity.ConnectivityMultiDevicesSnippet" />
+        android:value="com.google.snippet.connectivity.ConnectivityMultiDevicesSnippet,
+                       com.google.snippet.connectivity.MdnsMultiDevicesSnippet" />
   </application>
   <!-- Add an instrumentation tag so that the app can be launched through an
        instrument command. The runner `com.google.android.mobly.snippet.SnippetRunner`
diff --git a/tests/cts/multidevices/snippet/MdnsMultiDevicesSnippet.kt b/tests/cts/multidevices/snippet/MdnsMultiDevicesSnippet.kt
new file mode 100644
index 0000000..1b288df
--- /dev/null
+++ b/tests/cts/multidevices/snippet/MdnsMultiDevicesSnippet.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.snippet.connectivity
+
+import android.net.nsd.NsdManager
+import android.net.nsd.NsdServiceInfo
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.NsdDiscoveryRecord
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
+import com.android.testutils.NsdRegistrationRecord
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
+import com.android.testutils.NsdResolveRecord
+import com.android.testutils.NsdResolveRecord.ResolveEvent.ServiceResolved
+import com.google.android.mobly.snippet.Snippet
+import com.google.android.mobly.snippet.rpc.Rpc
+import kotlin.test.assertEquals
+import org.junit.Assert.assertArrayEquals
+
+private const val SERVICE_NAME = "MultiDevicesTest"
+private const val SERVICE_TYPE = "_multi_devices._tcp"
+private const val SERVICE_ATTRIBUTES_KEY = "key"
+private const val SERVICE_ATTRIBUTES_VALUE = "value"
+private const val SERVICE_PORT = 12345
+private const val REGISTRATION_TIMEOUT_MS = 10_000L
+
+class MdnsMultiDevicesSnippet : Snippet {
+    private val context = InstrumentationRegistry.getInstrumentation().getTargetContext()
+    private val nsdManager = context.getSystemService(NsdManager::class.java)!!
+    private val registrationRecord = NsdRegistrationRecord()
+    private val discoveryRecord = NsdDiscoveryRecord()
+    private val resolveRecord = NsdResolveRecord()
+
+    @Rpc(description = "Register a mDns service")
+    fun registerMDnsService() {
+        val info = NsdServiceInfo()
+        info.setServiceName(SERVICE_NAME)
+        info.setServiceType(SERVICE_TYPE)
+        info.setPort(SERVICE_PORT)
+        info.setAttribute(SERVICE_ATTRIBUTES_KEY, SERVICE_ATTRIBUTES_VALUE)
+        nsdManager.registerService(info, NsdManager.PROTOCOL_DNS_SD, registrationRecord)
+        registrationRecord.expectCallback<ServiceRegistered>(REGISTRATION_TIMEOUT_MS)
+    }
+
+    @Rpc(description = "Unregister a mDns service")
+    fun unregisterMDnsService() {
+        nsdManager.unregisterService(registrationRecord)
+        registrationRecord.expectCallback<ServiceUnregistered>()
+    }
+
+    @Rpc(description = "Ensure the discovery and resolution of the mDNS service")
+    // Suppress the warning, as the NsdManager#resolveService() method is deprecated.
+    @Suppress("DEPRECATION")
+    fun ensureMDnsServiceDiscoveryAndResolution() {
+        // Discover a mDns service that matches the test service
+        nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryRecord)
+        val info = discoveryRecord.waitForServiceDiscovered(SERVICE_NAME, SERVICE_TYPE)
+        // Resolve the retrieved mDns service.
+        nsdManager.resolveService(info, resolveRecord)
+        val serviceResolved = resolveRecord.expectCallbackEventually<ServiceResolved>()
+        serviceResolved.serviceInfo.let {
+            assertEquals(SERVICE_NAME, it.serviceName)
+            assertEquals(".$SERVICE_TYPE", it.serviceType)
+            assertEquals(SERVICE_PORT, it.port)
+            assertEquals(1, it.attributes.size)
+            assertArrayEquals(
+                    SERVICE_ATTRIBUTES_VALUE.encodeToByteArray(),
+                    it.attributes[SERVICE_ATTRIBUTES_KEY]
+            )
+        }
+    }
+
+    @Rpc(description = "Stop discovery")
+    fun stopMDnsServiceDiscovery() {
+        nsdManager.stopServiceDiscovery(discoveryRecord)
+        discoveryRecord.expectCallbackEventually<DiscoveryStopped>()
+    }
+}
diff --git a/tests/cts/multidevices/utils/mdns_utils.py b/tests/cts/multidevices/utils/mdns_utils.py
new file mode 100644
index 0000000..ec1fea0
--- /dev/null
+++ b/tests/cts/multidevices/utils/mdns_utils.py
@@ -0,0 +1,42 @@
+#  Copyright (C) 2024 The Android Open Source Project
+#
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+from mobly.controllers import android_device
+
+
+def register_mdns_service_and_discover_resolve(
+    advertising_device: android_device, discovery_device: android_device
+) -> None:
+  """Test mdns advertising, discovery and resolution
+
+  One device registers an mDNS service, and another device discovers and
+  resolves that service.
+  """
+  advertising = advertising_device.connectivity_multi_devices_snippet
+  discovery = discovery_device.connectivity_multi_devices_snippet
+
+  # Register a mDns service
+  advertising.registerMDnsService()
+
+  # Ensure the discovery and resolution of the mDNS service
+  discovery.ensureMDnsServiceDiscoveryAndResolution()
+
+
+def cleanup_mdns_service(
+    advertising_device: android_device, discovery_device: android_device
+) -> None:
+  # Unregister the mDns service
+  advertising_device.connectivity_multi_devices_snippet.unregisterMDnsService()
+  # Stop discovery
+  discovery_device.connectivity_multi_devices_snippet.stopMDnsServiceDiscovery()
diff --git a/tests/cts/multidevices/utils/tether_utils.py b/tests/cts/multidevices/utils/tether_utils.py
index b37ed76..702b596 100644
--- a/tests/cts/multidevices/utils/tether_utils.py
+++ b/tests/cts/multidevices/utils/tether_utils.py
@@ -20,6 +20,7 @@
 
 
 class UpstreamType:
+  NONE = 0
   CELLULAR = 1
   WIFI = 2
 
@@ -56,6 +57,8 @@
         not server.isStaApConcurrencySupported(),
         "Server requires Wifi AP + STA concurrency",
     )
+  elif upstream_type == UpstreamType.NONE:
+    pass
   else:
     raise ValueError(f"Invalid upstream type: {upstream_type}")
 
@@ -78,6 +81,8 @@
     server.requestCellularAndEnsureDefault()
   elif upstream_type == UpstreamType.WIFI:
     server.ensureWifiIsDefault()
+  elif upstream_type == UpstreamType.NONE:
+    pass
   else:
     raise ValueError(f"Invalid upstream type: {upstream_type}")
 
diff --git a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
index 3563f2c..f6cbeeb 100644
--- a/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
+++ b/tests/cts/net/src/android/net/cts/ApfIntegrationTest.kt
@@ -29,7 +29,11 @@
 import android.net.apf.ApfConstants.ETH_ETHERTYPE_OFFSET
 import android.net.apf.ApfConstants.ICMP6_TYPE_OFFSET
 import android.net.apf.ApfConstants.IPV6_NEXT_HEADER_OFFSET
+import android.net.apf.ApfCounterTracker
+import android.net.apf.ApfCounterTracker.Counter.FILTER_AGE_16384THS
 import android.net.apf.ApfV4Generator
+import android.net.apf.ApfV4GeneratorBase
+import android.net.apf.ApfV6Generator
 import android.net.apf.BaseApfGenerator
 import android.net.apf.BaseApfGenerator.MemorySlot
 import android.net.apf.BaseApfGenerator.Register.R0
@@ -73,7 +77,6 @@
 import com.google.common.truth.Truth.assertWithMessage
 import com.google.common.truth.TruthJUnit.assume
 import java.io.FileDescriptor
-import java.lang.Thread
 import java.net.InetSocketAddress
 import java.nio.ByteBuffer
 import java.util.concurrent.CompletableFuture
@@ -394,7 +397,7 @@
         }
     }
 
-    fun ApfV4Generator.addPassIfNotIcmpv6EchoReply() {
+    fun ApfV4GeneratorBase<*>.addPassIfNotIcmpv6EchoReply() {
         // If not IPv6 -> PASS
         addLoad16(R0, ETH_ETHERTYPE_OFFSET)
         addJumpIfR0NotEquals(ETH_P_IPV6.toLong(), BaseApfGenerator.PASS_LABEL)
@@ -573,4 +576,50 @@
         val timeDiff = filterAgeSeconds - filterAgeSecondsOrig
         assertThat(timeDiff).isAnyOf(5, 6)
     }
+
+    @VsrTest(requirements = ["VSR-5.3.12-002", "VSR-5.3.12-005"])
+    @Test
+    fun testFilterAge16384thsIncreasesBetweenPackets() {
+        assumeApfVersionSupportAtLeast(6000)
+        clearApfMemory()
+        val gen = ApfV6Generator(
+                caps.apfVersionSupported,
+                caps.maximumApfProgramSize,
+                caps.maximumApfProgramSize
+        )
+
+        // If not ICMPv6 Echo Reply -> PASS
+        gen.addPassIfNotIcmpv6EchoReply()
+
+        // Store all prefilled memory slots in counter region [500, 520)
+        gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_16384THS)
+        gen.addStoreCounter(FILTER_AGE_16384THS, R0)
+
+        installProgram(gen.generate())
+        readProgram() // wait for install completion
+
+        val payloadSize = 56
+        val data = ByteArray(payloadSize).also { Random.nextBytes(it) }
+        packetReader.sendPing(data, payloadSize)
+        packetReader.expectPingReply()
+
+        var apfRam = readProgram()
+        val filterAge16384thSecondsOrig =
+                ApfCounterTracker.getCounterValue(apfRam, FILTER_AGE_16384THS)
+
+        Thread.sleep(5000)
+
+        packetReader.sendPing(data, payloadSize)
+        packetReader.expectPingReply()
+
+        apfRam = readProgram()
+        val filterAge16384thSeconds = ApfCounterTracker.getCounterValue(apfRam, FILTER_AGE_16384THS)
+        val timeDiff = (filterAge16384thSeconds - filterAge16384thSecondsOrig)
+        // Expect the HAL plus ping latency to be less than 800ms.
+        val timeDiffLowerBound = (4.99 * 16384).toInt()
+        val timeDiffUpperBound = (5.81 * 16384).toInt()
+        // Assert that filter age has increased, but not too much.
+        assertThat(timeDiff).isGreaterThan(timeDiffLowerBound)
+        assertThat(timeDiff).isLessThan(timeDiffUpperBound)
+    }
 }
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
index a3c3f45..284fcae 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -28,7 +28,9 @@
 import com.android.testutils.ConnectivityModuleTest
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.NsdDiscoveryRecord
 import com.android.testutils.TapPacketReader
+import com.android.testutils.pollForQuery
 import com.android.testutils.tryTest
 import java.util.Random
 import kotlin.test.assertEquals
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 6394599..be80787 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -39,19 +39,6 @@
 import android.net.TestNetworkManager
 import android.net.TestNetworkSpecifier
 import android.net.connectivity.ConnectivityCompatChanges
-import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted
-import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
-import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
-import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
-import android.net.cts.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed
-import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
-import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
-import android.net.cts.NsdResolveRecord.ResolveEvent.ResolutionStopped
-import android.net.cts.NsdResolveRecord.ResolveEvent.ServiceResolved
-import android.net.cts.NsdResolveRecord.ResolveEvent.StopResolutionFailed
-import android.net.cts.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdated
-import android.net.cts.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdatedLost
-import android.net.cts.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded
 import android.net.cts.util.CtsNetUtils
 import android.net.nsd.DiscoveryRequest
 import android.net.nsd.NsdManager
@@ -92,9 +79,29 @@
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.DeviceConfigRule
 import com.android.testutils.NSResponder
+import com.android.testutils.NsdDiscoveryRecord
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStarted
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
+import com.android.testutils.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
+import com.android.testutils.NsdEvent
+import com.android.testutils.NsdRecord
+import com.android.testutils.NsdRegistrationRecord
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
+import com.android.testutils.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
+import com.android.testutils.NsdResolveRecord
+import com.android.testutils.NsdResolveRecord.ResolveEvent.ResolutionStopped
+import com.android.testutils.NsdResolveRecord.ResolveEvent.ServiceResolved
+import com.android.testutils.NsdResolveRecord.ResolveEvent.StopResolutionFailed
+import com.android.testutils.NsdServiceInfoCallbackRecord
+import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdated
+import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdatedLost
+import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded
 import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
 import com.android.testutils.TapPacketReader
+import com.android.testutils.TestDnsPacket
 import com.android.testutils.TestableNetworkAgent
 import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
 import com.android.testutils.TestableNetworkCallback
@@ -102,6 +109,11 @@
 import com.android.testutils.assertEmpty
 import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
 import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk33
+import com.android.testutils.pollForAdvertisement
+import com.android.testutils.pollForMdnsPacket
+import com.android.testutils.pollForProbe
+import com.android.testutils.pollForQuery
+import com.android.testutils.pollForReply
 import com.android.testutils.runAsShell
 import com.android.testutils.tryTest
 import com.android.testutils.waitForIdle
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index b71a46f..9a77c89 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -52,6 +52,7 @@
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -72,6 +73,7 @@
 import android.os.Messenger;
 import android.os.Process;
 
+import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.util.test.BroadcastInterceptingContext;
@@ -83,6 +85,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -240,7 +243,7 @@
 
         // register callback
         when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(),
-                anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(request);
+                anyInt(), anyInt(), any(), nullable(String.class), anyInt())).thenReturn(request);
         manager.requestNetwork(request, callback, handler);
 
         // callback triggers
@@ -269,7 +272,7 @@
 
         // register callback
         when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(),
-                anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(req1);
+                anyInt(), anyInt(), any(), nullable(String.class), anyInt())).thenReturn(req1);
         manager.requestNetwork(req1, callback, handler);
 
         // callback triggers
@@ -287,7 +290,7 @@
 
         // callback can be registered again
         when(mService.requestNetwork(anyInt(), any(), anyInt(), captor.capture(), anyInt(), any(),
-                anyInt(), anyInt(), any(), nullable(String.class))).thenReturn(req2);
+                anyInt(), anyInt(), any(), nullable(String.class), anyInt())).thenReturn(req2);
         manager.requestNetwork(req2, callback, handler);
 
         // callback triggers
@@ -311,7 +314,7 @@
 
         when(mCtx.getApplicationInfo()).thenReturn(info);
         when(mService.requestNetwork(anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(),
-                anyInt(), any(), nullable(String.class))).thenReturn(request);
+                anyInt(), any(), nullable(String.class), anyInt())).thenReturn(request);
 
         Handler handler = new Handler(Looper.getMainLooper());
         manager.requestNetwork(request, callback, handler);
@@ -403,15 +406,15 @@
         manager.requestNetwork(request, callback);
         verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(request.networkCapabilities),
                 eq(REQUEST.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
-                eq(testPkgName), eq(testAttributionTag));
+                eq(testPkgName), eq(testAttributionTag), anyInt());
         reset(mService);
 
         // Verify that register network callback does not calls requestNetwork at all.
         manager.registerNetworkCallback(request, callback);
         verify(mService, never()).requestNetwork(anyInt(), any(), anyInt(), any(), anyInt(), any(),
-                anyInt(), anyInt(), any(), any());
+                anyInt(), anyInt(), any(), any(), anyInt());
         verify(mService).listenForNetwork(eq(request.networkCapabilities), any(), any(), anyInt(),
-                eq(testPkgName), eq(testAttributionTag));
+                eq(testPkgName), eq(testAttributionTag), anyInt());
         reset(mService);
 
         Handler handler = new Handler(ConnectivityThread.getInstanceLooper());
@@ -419,24 +422,24 @@
         manager.registerDefaultNetworkCallback(callback);
         verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(null),
                 eq(TRACK_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
-                eq(testPkgName), eq(testAttributionTag));
+                eq(testPkgName), eq(testAttributionTag), anyInt());
         reset(mService);
 
         manager.registerDefaultNetworkCallbackForUid(42, callback, handler);
         verify(mService).requestNetwork(eq(42), eq(null),
                 eq(TRACK_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
-                eq(testPkgName), eq(testAttributionTag));
+                eq(testPkgName), eq(testAttributionTag), anyInt());
 
         manager.requestBackgroundNetwork(request, callback, handler);
         verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(request.networkCapabilities),
                 eq(BACKGROUND_REQUEST.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
-                eq(testPkgName), eq(testAttributionTag));
+                eq(testPkgName), eq(testAttributionTag), anyInt());
         reset(mService);
 
         manager.registerSystemDefaultNetworkCallback(callback, handler);
         verify(mService).requestNetwork(eq(Process.INVALID_UID), eq(null),
                 eq(TRACK_SYSTEM_DEFAULT.ordinal()), any(), anyInt(), any(), eq(TYPE_NONE), anyInt(),
-                eq(testPkgName), eq(testAttributionTag));
+                eq(testPkgName), eq(testAttributionTag), anyInt());
         reset(mService);
     }
 
@@ -516,16 +519,154 @@
                     + " attempts", ref.get());
     }
 
-    private <T> void mockService(Class<T> clazz, String name, T service) {
-        doReturn(service).when(mCtx).getSystemService(name);
-        doReturn(name).when(mCtx).getSystemServiceName(clazz);
+    @Test
+    public void testDeclaredMethodsFlag_requestWithMixedMethods_RegistrationFlagsMatch()
+            throws Exception {
+        doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+                .when(mService).getEnabledConnectivityManagerFeatures();
+        final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
 
-        // If the test suite uses the inline mock maker library, such as for coverage tests,
-        // then the final version of getSystemService must also be mocked, as the real
-        // method will not be called by the test and null object is returned since no mock.
-        // Otherwise, mocking a final method will fail the test.
-        if (mCtx.getSystemService(clazz) == null) {
-            doReturn(service).when(mCtx).getSystemService(clazz);
-        }
+        final NetworkRequest request = new NetworkRequest.Builder().build();
+        final NetworkCallback callback1 = new ConnectivityManager.NetworkCallback() {
+            @Override
+            public void onPreCheck(@NonNull Network network) {}
+            @Override
+            public void onAvailable(@NonNull Network network) {}
+            @Override
+            public void onLost(@NonNull Network network) {}
+            @Override
+            public void onCapabilitiesChanged(@NonNull Network network,
+                    @NonNull NetworkCapabilities networkCapabilities) {}
+            @Override
+            public void onLocalNetworkInfoChanged(@NonNull Network network,
+                    @NonNull LocalNetworkInfo localNetworkInfo) {}
+            @Override
+            public void onNetworkResumed(@NonNull Network network) {}
+            @Override
+            public void onBlockedStatusChanged(@NonNull Network network, int blocked) {}
+        };
+        manager.requestNetwork(request, callback1);
+
+        final InOrder inOrder = inOrder(mService);
+        inOrder.verify(mService).requestNetwork(
+                anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(), anyInt(), any(), any(),
+                eq(1 << ConnectivityManager.CALLBACK_PRECHECK
+                        | 1 << ConnectivityManager.CALLBACK_AVAILABLE
+                        | 1 << ConnectivityManager.CALLBACK_LOST
+                        | 1 << ConnectivityManager.CALLBACK_CAP_CHANGED
+                        | 1 << ConnectivityManager.CALLBACK_LOCAL_NETWORK_INFO_CHANGED
+                        | 1 << ConnectivityManager.CALLBACK_RESUMED
+                        | 1 << ConnectivityManager.CALLBACK_BLK_CHANGED));
+    }
+
+    @Test
+    public void testDeclaredMethodsFlag_listenWithMixedMethods_RegistrationFlagsMatch()
+            throws Exception {
+        final NetworkRequest request = new NetworkRequest.Builder().build();
+        doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+                .when(mService).getEnabledConnectivityManagerFeatures();
+        final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+
+        final NetworkCallback callback2 = new ConnectivityManager.NetworkCallback() {
+            @Override
+            public void onLosing(@NonNull Network network, int maxMsToLive) {}
+            @Override
+            public void onUnavailable() {}
+            @Override
+            public void onLinkPropertiesChanged(@NonNull Network network,
+                    @NonNull LinkProperties linkProperties) {}
+            @Override
+            public void onNetworkSuspended(@NonNull Network network) {}
+        };
+        manager.registerNetworkCallback(request, callback2);
+        // Call a second time with the same callback to exercise caching
+        manager.registerNetworkCallback(request, callback2);
+
+        verify(mService, times(2)).listenForNetwork(
+                any(), any(), any(), anyInt(), any(), any(),
+                eq(1 << ConnectivityManager.CALLBACK_LOSING
+                        // AVAILABLE calls IP_CHANGED and SUSPENDED so it gets added
+                        | 1 << ConnectivityManager.CALLBACK_AVAILABLE
+                        | 1 << ConnectivityManager.CALLBACK_UNAVAIL
+                        | 1 << ConnectivityManager.CALLBACK_IP_CHANGED
+                        | 1 << ConnectivityManager.CALLBACK_SUSPENDED));
+    }
+
+    @Test
+    public void testDeclaredMethodsFlag_requestWithHiddenAvailableCallback_RegistrationFlagsMatch()
+            throws Exception {
+        doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+                .when(mService).getEnabledConnectivityManagerFeatures();
+        final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+
+        final NetworkCallback hiddenOnAvailableCb = new ConnectivityManager.NetworkCallback() {
+            // This overload is @hide but might still be used by (bad) apps
+            @Override
+            public void onAvailable(@NonNull Network network,
+                    @NonNull NetworkCapabilities networkCapabilities,
+                    @NonNull LinkProperties linkProperties, boolean blocked) {}
+        };
+        manager.registerDefaultNetworkCallback(hiddenOnAvailableCb);
+
+        verify(mService).requestNetwork(
+                anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(), anyInt(), any(), any(),
+                eq(1 << ConnectivityManager.CALLBACK_AVAILABLE));
+    }
+
+    public static class NetworkCallbackWithOnLostOnly extends NetworkCallback {
+        @Override
+        public void onLost(@NonNull Network network) {}
+    }
+
+    @Test
+    public void testDeclaredMethodsFlag_requestWithoutAvailableCallback_RegistrationFlagsMatch()
+            throws Exception {
+        doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+                .when(mService).getEnabledConnectivityManagerFeatures();
+        final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+        final Handler handler = new Handler(Looper.getMainLooper());
+
+        final NetworkCallback noOnAvailableCb = new NetworkCallbackWithOnLostOnly();
+        manager.registerSystemDefaultNetworkCallback(noOnAvailableCb, handler);
+
+        verify(mService).requestNetwork(
+                anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(), anyInt(), any(), any(),
+                eq(1 << ConnectivityManager.CALLBACK_LOST));
+    }
+
+    @Test
+    public void testDeclaredMethodsFlag_listenWithMock_OptimizationDisabled()
+            throws Exception {
+        doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+                .when(mService).getEnabledConnectivityManagerFeatures();
+        final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+        final Handler handler = new Handler(Looper.getMainLooper());
+
+        final NetworkRequest request = new NetworkRequest.Builder().build();
+        manager.registerNetworkCallback(request, mock(NetworkCallbackWithOnLostOnly.class),
+                handler);
+
+        verify(mService).listenForNetwork(
+                any(), any(), any(), anyInt(), any(), any(),
+                // Mock that does not call the constructor -> do not use the optimization
+                eq(~0));
+    }
+
+    @Test
+    public void testDeclaredMethodsFlag_requestWitNoCallback_OptimizationDisabled()
+            throws Exception {
+        doReturn(ConnectivityManager.FEATURE_USE_DECLARED_METHODS_FOR_CALLBACKS)
+                .when(mService).getEnabledConnectivityManagerFeatures();
+        final ConnectivityManager manager = new ConnectivityManager(mCtx, mService);
+        final Handler handler = new Handler(Looper.getMainLooper());
+
+        final NetworkRequest request = new NetworkRequest.Builder().build();
+        final NetworkCallback noCallbackAtAll = new ConnectivityManager.NetworkCallback() {};
+        manager.requestBackgroundNetwork(request, noCallbackAtAll, handler);
+
+        verify(mService).requestNetwork(
+                anyInt(), any(), anyInt(), any(), anyInt(), any(), anyInt(), anyInt(), any(), any(),
+                // No callbacks overridden -> do not use the optimization
+                eq(~0));
     }
 }
diff --git a/tests/unit/java/android/net/NetworkCallbackFlagsTest.kt b/tests/unit/java/android/net/NetworkCallbackFlagsTest.kt
new file mode 100644
index 0000000..af06a64
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkCallbackFlagsTest.kt
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net
+
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.ConnectivityManager.NetworkCallbackMethodsHolder
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import java.lang.reflect.Method
+import java.lang.reflect.Modifier
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.doCallRealMethod
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.mockingDetails
+
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@RunWith(DevSdkIgnoreRunner::class)
+class NetworkCallbackFlagsTest {
+
+    // To avoid developers forgetting to update NETWORK_CB_METHODS when modifying NetworkCallbacks,
+    // or using wrong values, calculate it from annotations here and verify that it matches.
+    // This avoids the runtime cost of reflection, but still ensures that the list is correct.
+    @Test
+    fun testNetworkCallbackMethods_calculateFromAnnotations_matchesHardcodedList() {
+        val calculatedMethods = getNetworkCallbackMethodsFromAnnotations()
+        assertEquals(
+            calculatedMethods.toSet(),
+            NetworkCallbackMethodsHolder.NETWORK_CB_METHODS.map {
+                NetworkCallbackMethodWithEquals(
+                    it.mName,
+                    it.mParameterTypes.toList(),
+                    callbacksCallingThisMethod = it.mCallbacksCallingThisMethod
+                )
+            }.toSet()
+        )
+    }
+
+    data class NetworkCallbackMethodWithEquals(
+        val name: String,
+        val parameterTypes: List<Class<*>>,
+        val callbacksCallingThisMethod: Int
+    )
+
+    data class NetworkCallbackMethodBuilder(
+        val name: String,
+        val parameterTypes: List<Class<*>>,
+        val isFinal: Boolean,
+        val methodId: Int,
+        val mayCall: Set<Int>?,
+        var callbacksCallingThisMethod: Int
+    ) {
+        fun build() = NetworkCallbackMethodWithEquals(
+            name,
+            parameterTypes,
+            callbacksCallingThisMethod
+        )
+    }
+
+    /**
+     * Build [NetworkCallbackMethodsHolder.NETWORK_CB_METHODS] from [NetworkCallback] annotations.
+     */
+    private fun getNetworkCallbackMethodsFromAnnotations(): List<NetworkCallbackMethodWithEquals> {
+        val parsedMethods = mutableListOf<NetworkCallbackMethodBuilder>()
+        val methods = NetworkCallback::class.java.declaredMethods
+        methods.forEach { method ->
+            val cb = method.getAnnotation(
+                NetworkCallback.FilteredCallback::class.java
+            ) ?: return@forEach
+            val callbacksCallingThisMethod = if (cb.calledByCallbackId == 0) {
+                0
+            } else {
+                1 shl cb.calledByCallbackId
+            }
+            parsedMethods.add(
+                NetworkCallbackMethodBuilder(
+                    method.name,
+                    method.parameterTypes.toList(),
+                    Modifier.isFinal(method.modifiers),
+                    cb.methodId,
+                    cb.mayCall.toSet(),
+                    callbacksCallingThisMethod
+                )
+            )
+        }
+
+        // Propagate callbacksCallingThisMethod for transitive calls
+        do {
+            var hadChange = false
+            parsedMethods.forEach { caller ->
+                parsedMethods.forEach { callee ->
+                    if (caller.mayCall?.contains(callee.methodId) == true) {
+                        // Callbacks that call the caller also cause calls to the callee. So
+                        // callbacksCallingThisMethod for the callee should include
+                        // callbacksCallingThisMethod from the caller.
+                        val newValue =
+                            caller.callbacksCallingThisMethod or callee.callbacksCallingThisMethod
+                        hadChange = hadChange || callee.callbacksCallingThisMethod != newValue
+                        callee.callbacksCallingThisMethod = newValue
+                    }
+                }
+            }
+        } while (hadChange)
+
+        // Final methods may affect the flags for transitive calls, but cannot be overridden, so do
+        // not need to be in the list (no overridden method in NetworkCallback will match them).
+        return parsedMethods.filter { !it.isFinal }.map { it.build() }
+    }
+
+    @Test
+    fun testMethodsAreAnnotated() {
+        val annotations = NetworkCallback::class.java.declaredMethods.mapNotNull { method ->
+            if (!Modifier.isPublic(method.modifiers) && !Modifier.isProtected(method.modifiers)) {
+                return@mapNotNull null
+            }
+            val annotation = method.getAnnotation(NetworkCallback.FilteredCallback::class.java)
+            assertNotNull(annotation, "$method is missing the @FilteredCallback annotation")
+            return@mapNotNull annotation
+        }
+
+        annotations.groupingBy { it.methodId }.eachCount().forEach { (methodId, cnt) ->
+            assertEquals(1, cnt, "Method ID $methodId is used more than once in @FilteredCallback")
+        }
+    }
+
+    @Test
+    fun testObviousCalleesAreInAnnotation() {
+        NetworkCallback::class.java.declaredMethods.forEach { method ->
+            val annotation = method.getAnnotation(NetworkCallback.FilteredCallback::class.java)
+                ?: return@forEach
+            val missingFlags = getObviousCallees(method).toMutableSet().apply {
+                removeAll(annotation.mayCall.toSet())
+            }
+            val msg = "@FilteredCallback on $method is missing flags " +
+                    "$missingFlags in mayCall. There may be other " +
+                    "calls that are not detected if they are done conditionally."
+            assertEquals(emptySet(), missingFlags, msg)
+        }
+    }
+
+    /**
+     * Invoke the specified NetworkCallback method with mock arguments, return a set of transitively
+     * called methods.
+     *
+     * This provides an idea of which methods are transitively called by the specified method. It's
+     * not perfect as some callees could be called or not depending on the exact values of the mock
+     * arguments that are passed in (for example, onAvailable calls onNetworkSuspended only if the
+     * capabilities lack the NOT_SUSPENDED capability), but it should catch obvious forgotten calls.
+     */
+    private fun getObviousCallees(method: Method): Set<Int> {
+        // Create a mock NetworkCallback that mocks all methods except the one specified by the
+        // caller.
+        val mockCallback = mock(NetworkCallback::class.java)
+
+        if (!Modifier.isFinal(method.modifiers) ||
+            // The mock class will be NetworkCallback (not a subclass) if using mockito-inline,
+            // which mocks final methods too
+            mockCallback.javaClass == NetworkCallback::class.java) {
+            doCallRealMethod().`when`(mockCallback).let { mockObj ->
+                val anyArgs = method.parameterTypes.map { any(it) }
+                method.invoke(mockObj, *anyArgs.toTypedArray())
+            }
+        }
+
+        // Invoke the target method with mock parameters
+        val mockParameters = method.parameterTypes.map { getMockFor(method, it) }
+        method.invoke(mockCallback, *mockParameters.toTypedArray())
+
+        // Aggregate callees
+        val mockingDetails = mockingDetails(mockCallback)
+        return mockingDetails.invocations.mapNotNull { inv ->
+            if (inv.method == method) {
+                null
+            } else {
+                inv.method.getAnnotation(NetworkCallback.FilteredCallback::class.java)?.methodId
+            }
+        }.toSet()
+    }
+
+    private fun getMockFor(method: Method, c: Class<*>): Any {
+        if (!c.isPrimitive && !Modifier.isFinal(c.modifiers)) {
+            return mock(c)
+        }
+        return when (c) {
+            NetworkCapabilities::class.java -> NetworkCapabilities()
+            LinkProperties::class.java -> LinkProperties()
+            LocalNetworkInfo::class.java -> LocalNetworkInfo(null)
+            Boolean::class.java -> false
+            Int::class.java -> 0
+            else -> fail("No mock set for parameter type $c used in $method")
+        }
+    }
+}
diff --git a/tests/unit/java/com/android/server/BpfNetMapsTest.java b/tests/unit/java/com/android/server/BpfNetMapsTest.java
index cbc060a..859c54a 100644
--- a/tests/unit/java/com/android/server/BpfNetMapsTest.java
+++ b/tests/unit/java/com/android/server/BpfNetMapsTest.java
@@ -87,7 +87,9 @@
 import android.net.InetAddresses;
 import android.net.UidOwnerValue;
 import android.os.Build;
+import android.os.Process;
 import android.os.ServiceSpecificException;
+import android.os.UserHandle;
 import android.system.ErrnoException;
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
@@ -1249,6 +1251,32 @@
         );
     }
 
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testIsUidNetworkingBlockedForCoreUids() throws Exception {
+        final long allowlistMatch = BACKGROUND_MATCH;    // Enable any allowlist match.
+        mConfigurationMap.updateEntry(UID_RULES_CONFIGURATION_KEY, new U32(allowlistMatch));
+
+        // Verify that a normal uid that is not on this chain is indeed blocked.
+        assertTrue(BpfNetMapsUtils.isUidNetworkingBlocked(TEST_UID, false, mConfigurationMap,
+                mUidOwnerMap, mDataSaverEnabledMap));
+
+        final int[] coreAids = new int[] {
+                Process.ROOT_UID,
+                Process.SYSTEM_UID,
+                Process.FIRST_APPLICATION_UID - 10,
+                Process.FIRST_APPLICATION_UID - 1,
+        };
+        // Core appIds are not on the chain but should still be allowed on any user.
+        for (int userId = 0; userId < 20; userId++) {
+            for (final int aid : coreAids) {
+                final int uid = UserHandle.getUid(userId, aid);
+                assertFalse(BpfNetMapsUtils.isUidNetworkingBlocked(uid, false, mConfigurationMap,
+                        mUidOwnerMap, mDataSaverEnabledMap));
+            }
+        }
+    }
+
     private void doTestIsUidRestrictedOnMeteredNetworks(
             final long enabledMatches,
             final long uidRules,
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 8526a9a..be7f2a3 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -2871,7 +2871,7 @@
         };
         final NetworkRequest request = mService.listenForNetwork(caps, messenger, binder,
                 NetworkCallback.FLAG_NONE, mContext.getOpPackageName(),
-                mContext.getAttributionTag());
+                mContext.getAttributionTag(), ~0 /* declaredMethodsFlag */);
         mService.releaseNetworkRequest(request);
         deathRecipient.get().binderDied();
         // Wait for the release message to be processed.
@@ -5407,7 +5407,7 @@
             mService.requestNetwork(Process.INVALID_UID, networkCapabilities,
                     NetworkRequest.Type.REQUEST.ordinal(), null, 0, null,
                     ConnectivityManager.TYPE_WIFI, NetworkCallback.FLAG_NONE,
-                    mContext.getPackageName(), getAttributionTag());
+                    mContext.getPackageName(), getAttributionTag(), ~0 /* declaredMethodsFlag */);
         });
 
         final NetworkRequest.Builder builder =
@@ -13655,7 +13655,8 @@
                     IllegalArgumentException.class,
                     () -> mService.requestNetwork(Process.INVALID_UID, nc, reqTypeInt, null, 0,
                             null, ConnectivityManager.TYPE_NONE, NetworkCallback.FLAG_NONE,
-                            mContext.getPackageName(), getAttributionTag())
+                            mContext.getPackageName(), getAttributionTag(),
+                            ~0 /* declaredMethodsFlag */)
             );
         }
     }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt
new file mode 100644
index 0000000..cf990b1
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSDeclaredMethodsForCallbacksTest.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivityservice
+
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.CALLBACK_CAP_CHANGED
+import android.net.ConnectivityManager.CALLBACK_IP_CHANGED
+import android.net.ConnectivityManager.CALLBACK_LOST
+import android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_ALL
+import android.net.LinkAddress
+import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
+import android.net.NetworkRequest
+import android.os.Build
+import com.android.net.module.util.BitUtils.packBits
+import com.android.server.CSTest
+import com.android.server.ConnectivityService
+import com.android.server.defaultLp
+import com.android.server.defaultNc
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.RecorderCallback.CallbackEntry
+import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.tryTest
+import java.util.concurrent.atomic.AtomicInteger
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.spy
+
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+class CSDeclaredMethodsForCallbacksTest : CSTest() {
+    private val mockedCallbackFlags = AtomicInteger(DECLARED_METHODS_ALL)
+    private lateinit var wrappedService: ConnectivityService
+
+    private val instrumentedCm by lazy { ConnectivityManager(context, wrappedService) }
+
+    @Before
+    fun setUpWrappedService() {
+        // Mock the callback flags set by ConnectivityManager when calling ConnectivityService, to
+        // simulate methods not being overridden
+        wrappedService = spy(service)
+        doAnswer { inv ->
+            service.requestNetwork(
+                inv.getArgument(0),
+                inv.getArgument(1),
+                inv.getArgument(2),
+                inv.getArgument(3),
+                inv.getArgument(4),
+                inv.getArgument(5),
+                inv.getArgument(6),
+                inv.getArgument(7),
+                inv.getArgument(8),
+                inv.getArgument(9),
+                mockedCallbackFlags.get())
+        }.`when`(wrappedService).requestNetwork(
+            anyInt(),
+            any(),
+            anyInt(),
+            any(),
+            anyInt(),
+            any(),
+            anyInt(),
+            anyInt(),
+            any(),
+            any(),
+            anyInt()
+        )
+        doAnswer { inv ->
+            service.listenForNetwork(
+                inv.getArgument(0),
+                inv.getArgument(1),
+                inv.getArgument(2),
+                inv.getArgument(3),
+                inv.getArgument(4),
+                inv.getArgument(5),
+                mockedCallbackFlags.get()
+            )
+        }.`when`(wrappedService)
+            .listenForNetwork(any(), any(), any(), anyInt(), any(), any(), anyInt())
+    }
+
+    @Test
+    fun testCallbacksAreFiltered() {
+        val requestCb = TestableNetworkCallback()
+        val listenCb = TestableNetworkCallback()
+        mockedCallbackFlags.withFlags(CALLBACK_IP_CHANGED, CALLBACK_LOST) {
+            instrumentedCm.requestNetwork(NetworkRequest.Builder().build(), requestCb)
+        }
+        mockedCallbackFlags.withFlags(CALLBACK_CAP_CHANGED) {
+            instrumentedCm.registerNetworkCallback(NetworkRequest.Builder().build(), listenCb)
+        }
+
+        with(Agent()) {
+            connect()
+            sendLinkProperties(defaultLp().apply {
+                addLinkAddress(LinkAddress("fe80:db8::123/64"))
+            })
+            sendNetworkCapabilities(defaultNc().apply {
+                addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+            })
+            disconnect()
+        }
+        waitForIdle()
+
+        // Only callbacks for the corresponding flags are called
+        requestCb.expect<CallbackEntry.LinkPropertiesChanged>()
+        requestCb.expect<CallbackEntry.Lost>()
+        requestCb.assertNoCallback(timeoutMs = 0L)
+
+        listenCb.expect<CallbackEntry.CapabilitiesChanged>()
+        listenCb.assertNoCallback(timeoutMs = 0L)
+    }
+}
+
+private fun AtomicInteger.withFlags(vararg flags: Int, action: () -> Unit) {
+    tryTest {
+        set(packBits(flags).toInt())
+        action()
+    } cleanup {
+        set(DECLARED_METHODS_ALL)
+    }
+}
diff --git a/thread/framework/java/android/net/thread/IConfigurationReceiver.aidl b/thread/framework/java/android/net/thread/IConfigurationReceiver.aidl
new file mode 100644
index 0000000..dcc4545
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IConfigurationReceiver.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+import android.net.thread.ThreadConfiguration;
+
+/** Receives the result of a Thread Configuration change. @hide */
+oneway interface IConfigurationReceiver {
+    void onConfigurationChanged(in ThreadConfiguration configuration);
+}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
index c5ca557..f50de74 100644
--- a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -19,11 +19,13 @@
 import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.ChannelMaxPower;
 import android.net.thread.IActiveOperationalDatasetReceiver;
-import android.net.thread.IOperationalDatasetCallback;
+import android.net.thread.IConfigurationReceiver;
 import android.net.thread.IOperationReceiver;
+import android.net.thread.IOperationalDatasetCallback;
 import android.net.thread.IScheduleMigrationReceiver;
 import android.net.thread.IStateCallback;
 import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
 
 /**
 * Interface for communicating with ThreadNetworkControllerService.
@@ -46,4 +48,7 @@
     void createRandomizedDataset(String networkName, IActiveOperationalDatasetReceiver receiver);
 
     void setEnabled(boolean enabled, in IOperationReceiver receiver);
+    void setConfiguration(in ThreadConfiguration config, in IOperationReceiver receiver);
+    void registerConfigurationCallback(in IConfigurationReceiver receiver);
+    void unregisterConfigurationCallback(in IConfigurationReceiver receiver);
 }
diff --git a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
index 520acbd..cecb4e9 100644
--- a/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
+++ b/thread/framework/java/android/net/thread/OperationalDatasetTimestamp.java
@@ -65,11 +65,32 @@
      */
     @NonNull
     public static OperationalDatasetTimestamp fromInstant(@NonNull Instant instant) {
+        return OperationalDatasetTimestamp.fromInstant(instant, true /* isAuthoritativeSource */);
+    }
+
+    /**
+     * Creates a new {@link OperationalDatasetTimestamp} object from an {@link Instant}.
+     *
+     * <p>The {@code seconds} is set to {@code instant.getEpochSecond()}, {@code ticks} is set to
+     * {@link instant#getNano()} based on frequency of 32768 Hz, and {@code isAuthoritativeSource}
+     * is set to {@code isAuthoritativeSource}.
+     *
+     * <p>Note that this conversion can lose precision and a value returned by {@link #toInstant}
+     * may not equal exactly the {@code instant}.
+     *
+     * @throws IllegalArgumentException if {@code instant.getEpochSecond()} is larger than {@code
+     *     0xffffffffffffL}
+     * @see toInstant
+     * @hide
+     */
+    @NonNull
+    public static OperationalDatasetTimestamp fromInstant(
+            @NonNull Instant instant, boolean isAuthoritativeSource) {
         int ticks = getRoundedTicks(instant.getNano());
         long seconds = instant.getEpochSecond() + ticks / TICKS_UPPER_BOUND;
         // the rounded ticks can be 0x8000 if instant.getNano() >= 999984742
         ticks = ticks % TICKS_UPPER_BOUND;
-        return new OperationalDatasetTimestamp(seconds, ticks, true /* isAuthoritativeSource */);
+        return new OperationalDatasetTimestamp(seconds, ticks, isAuthoritativeSource);
     }
 
     /**
diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.aidl b/thread/framework/java/android/net/thread/ThreadConfiguration.aidl
new file mode 100644
index 0000000..9473411
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.thread;
+
+parcelable ThreadConfiguration;
diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.java b/thread/framework/java/android/net/thread/ThreadConfiguration.java
new file mode 100644
index 0000000..e09b3a6
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.thread;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Data interface for Thread device configuration.
+ *
+ * <p>An example usage of creating a {@link ThreadConfiguration} that turns on NAT64 feature based
+ * on an existing {@link ThreadConfiguration}:
+ *
+ * <pre>{@code
+ * ThreadConfiguration config =
+ *     new ThreadConfiguration.Builder(existingConfig).setNat64Enabled(true).build();
+ * }</pre>
+ *
+ * @see ThreadNetworkController#setConfiguration
+ * @see ThreadNetworkController#registerConfigurationCallback
+ * @see ThreadNetworkController#unregisterConfigurationCallback
+ * @hide
+ */
+// @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+// @SystemApi
+public final class ThreadConfiguration implements Parcelable {
+    private final boolean mNat64Enabled;
+    private final boolean mDhcp6PdEnabled;
+
+    private ThreadConfiguration(Builder builder) {
+        this(builder.mNat64Enabled, builder.mDhcp6PdEnabled);
+    }
+
+    private ThreadConfiguration(boolean nat64Enabled, boolean dhcp6PdEnabled) {
+        this.mNat64Enabled = nat64Enabled;
+        this.mDhcp6PdEnabled = dhcp6PdEnabled;
+    }
+
+    /** Returns {@code true} if NAT64 is enabled. */
+    public boolean isNat64Enabled() {
+        return mNat64Enabled;
+    }
+
+    /** Returns {@code true} if DHCPv6 Prefix Delegation is enabled. */
+    public boolean isDhcp6PdEnabled() {
+        return mDhcp6PdEnabled;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (!(other instanceof ThreadConfiguration)) {
+            return false;
+        } else {
+            ThreadConfiguration otherConfig = (ThreadConfiguration) other;
+            return mNat64Enabled == otherConfig.mNat64Enabled
+                    && mDhcp6PdEnabled == otherConfig.mDhcp6PdEnabled;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mNat64Enabled, mDhcp6PdEnabled);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append('{');
+        sb.append("Nat64Enabled=").append(mNat64Enabled);
+        sb.append(", Dhcp6PdEnabled=").append(mDhcp6PdEnabled);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeBoolean(mNat64Enabled);
+        dest.writeBoolean(mDhcp6PdEnabled);
+    }
+
+    public static final @NonNull Creator<ThreadConfiguration> CREATOR =
+            new Creator<>() {
+                @Override
+                public ThreadConfiguration createFromParcel(Parcel in) {
+                    ThreadConfiguration.Builder builder = new ThreadConfiguration.Builder();
+                    builder.setNat64Enabled(in.readBoolean());
+                    builder.setDhcp6PdEnabled(in.readBoolean());
+                    return builder.build();
+                }
+
+                @Override
+                public ThreadConfiguration[] newArray(int size) {
+                    return new ThreadConfiguration[size];
+                }
+            };
+
+    /** The builder for creating {@link ThreadConfiguration} objects. */
+    public static final class Builder {
+        private boolean mNat64Enabled = false;
+        private boolean mDhcp6PdEnabled = false;
+
+        /** Creates a new {@link Builder} object with all features disabled. */
+        public Builder() {}
+
+        /**
+         * Creates a new {@link Builder} object from a {@link ThreadConfiguration} object.
+         *
+         * @param config the Border Router configurations to be copied
+         */
+        public Builder(@NonNull ThreadConfiguration config) {
+            Objects.requireNonNull(config);
+
+            mNat64Enabled = config.mNat64Enabled;
+            mDhcp6PdEnabled = config.mDhcp6PdEnabled;
+        }
+
+        /**
+         * Enables or disables NAT64 for the device.
+         *
+         * <p>Enabling this feature will allow Thread devices to connect to the internet/cloud over
+         * IPv4.
+         */
+        @NonNull
+        public Builder setNat64Enabled(boolean enabled) {
+            this.mNat64Enabled = enabled;
+            return this;
+        }
+
+        /**
+         * Enables or disables Prefix Delegation for the device.
+         *
+         * <p>Enabling this feature will allow Thread devices to connect to the internet/cloud over
+         * IPv6.
+         */
+        @NonNull
+        public Builder setDhcp6PdEnabled(boolean enabled) {
+            this.mDhcp6PdEnabled = enabled;
+            return this;
+        }
+
+        /** Creates a new {@link ThreadConfiguration} object. */
+        @NonNull
+        public ThreadConfiguration build() {
+            return new ThreadConfiguration(this);
+        }
+    }
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
index 8d6b40a..30b3d6a 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -41,6 +41,7 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 /**
  * Provides the primary APIs for controlling all aspects of a Thread network.
@@ -124,6 +125,12 @@
     private final Map<OperationalDatasetCallback, OperationalDatasetCallbackProxy>
             mOpDatasetCallbackMap = new HashMap<>();
 
+    private final Object mConfigurationCallbackMapLock = new Object();
+
+    @GuardedBy("mConfigurationCallbackMapLock")
+    private final Map<Consumer<ThreadConfiguration>, ConfigurationCallbackProxy>
+            mConfigurationCallbackMap = new HashMap<>();
+
     /** @hide */
     public ThreadNetworkController(@NonNull IThreadNetworkController controllerService) {
         requireNonNull(controllerService, "controllerService cannot be null");
@@ -579,6 +586,97 @@
     }
 
     /**
+     * Configures the Thread features for this device.
+     *
+     * <p>This method sets the {@link ThreadConfiguration} for this device. On success, the {@link
+     * OutcomeReceiver#onResult} will be called, and the {@code configuration} will be applied and
+     * persisted to the device; the configuration changes can be observed by {@link
+     * #registerConfigurationCallback}. On failure, {@link OutcomeReceiver#onError} of {@code
+     * receiver} will be invoked with a specific error.
+     *
+     * @param configuration the configuration to set
+     * @param executor the executor to execute {@code receiver}
+     * @param receiver the receiver to receive result of this operation
+     * @hide
+     */
+    // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+    // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+    public void setConfiguration(
+            @NonNull ThreadConfiguration configuration,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<Void, ThreadNetworkException> receiver) {
+        requireNonNull(configuration, "Configuration cannot be null");
+        requireNonNull(executor, "executor cannot be null");
+        requireNonNull(receiver, "receiver cannot be null");
+        try {
+            mControllerService.setConfiguration(
+                    configuration, new OperationReceiverProxy(executor, receiver));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Registers a callback to be called when the configuration is changed.
+     *
+     * <p>Upon return of this method, {@code callback} will be invoked immediately with the new
+     * {@link ThreadConfiguration}.
+     *
+     * @param executor the executor to execute the {@code callback}
+     * @param callback the callback to receive Thread configuration changes
+     * @throws IllegalArgumentException if {@code callback} has already been registered
+     * @hide
+     */
+    // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+    // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+    public void registerConfigurationCallback(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Consumer<ThreadConfiguration> callback) {
+        requireNonNull(executor, "executor cannot be null");
+        requireNonNull(callback, "callback cannot be null");
+        synchronized (mConfigurationCallbackMapLock) {
+            if (mConfigurationCallbackMap.containsKey(callback)) {
+                throw new IllegalArgumentException("callback has already been registered");
+            }
+            ConfigurationCallbackProxy callbackProxy =
+                    new ConfigurationCallbackProxy(executor, callback);
+            mConfigurationCallbackMap.put(callback, callbackProxy);
+            try {
+                mControllerService.registerConfigurationCallback(callbackProxy);
+            } catch (RemoteException e) {
+                mConfigurationCallbackMap.remove(callback);
+                e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Unregisters the configuration callback.
+     *
+     * @param callback the callback which has been registered with {@link
+     *     #registerConfigurationCallback}
+     * @throws IllegalArgumentException if {@code callback} hasn't been registered
+     * @hide
+     */
+    // @FlaggedApi(ThreadNetworkFlags.FLAG_CONFIGURATION_ENABLED)
+    // @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
+    public void unregisterConfigurationCallback(@NonNull Consumer<ThreadConfiguration> callback) {
+        requireNonNull(callback, "callback cannot be null");
+        synchronized (mConfigurationCallbackMapLock) {
+            ConfigurationCallbackProxy callbackProxy = mConfigurationCallbackMap.get(callback);
+            if (callbackProxy == null) {
+                throw new IllegalArgumentException("callback hasn't been registered");
+            }
+            try {
+                mControllerService.unregisterConfigurationCallback(callbackProxy);
+                mConfigurationCallbackMap.remove(callbackProxy.mConfigurationConsumer);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
      * Sets to use a specified test network as the upstream.
      *
      * @param testNetworkInterfaceName The name of the test network interface. When it's null,
@@ -764,4 +862,26 @@
             propagateError(mExecutor, mResultReceiver, errorCode, errorMessage);
         }
     }
+
+    private static final class ConfigurationCallbackProxy extends IConfigurationReceiver.Stub {
+        final Executor mExecutor;
+        final Consumer<ThreadConfiguration> mConfigurationConsumer;
+
+        ConfigurationCallbackProxy(
+                @CallbackExecutor Executor executor,
+                Consumer<ThreadConfiguration> ConfigurationConsumer) {
+            this.mExecutor = executor;
+            this.mConfigurationConsumer = ConfigurationConsumer;
+        }
+
+        @Override
+        public void onConfigurationChanged(ThreadConfiguration configuration) {
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                mExecutor.execute(() -> mConfigurationConsumer.accept(configuration));
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
 }
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
index e6ab988..691bbf5 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkFlags.java
@@ -27,5 +27,9 @@
     /** @hide */
     public static final String FLAG_THREAD_ENABLED = "com.android.net.thread.flags.thread_enabled";
 
+    /** @hide */
+    public static final String FLAG_CONFIGURATION_ENABLED =
+            "com.android.net.thread.flags.configuration_enabled";
+
     private ThreadNetworkFlags() {}
 }
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 0c77dee..2f60d9a 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -91,12 +91,14 @@
 import android.net.thread.ActiveOperationalDataset.SecurityPolicy;
 import android.net.thread.ChannelMaxPower;
 import android.net.thread.IActiveOperationalDatasetReceiver;
+import android.net.thread.IConfigurationReceiver;
 import android.net.thread.IOperationReceiver;
 import android.net.thread.IOperationalDatasetCallback;
 import android.net.thread.IStateCallback;
 import android.net.thread.IThreadNetworkController;
 import android.net.thread.OperationalDatasetTimestamp;
 import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
 import android.net.thread.ThreadNetworkController;
 import android.net.thread.ThreadNetworkController.DeviceRole;
 import android.net.thread.ThreadNetworkException;
@@ -189,6 +191,8 @@
     private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
     private final ConnectivityResources mResources;
     private final Supplier<String> mCountryCodeSupplier;
+    private final Map<IConfigurationReceiver, IBinder.DeathRecipient> mConfigurationReceivers =
+            new HashMap<>();
 
     // This should not be directly used for calling IOtDaemon APIs because ot-daemon may die and
     // {@code mOtDaemon} will be set to {@code null}. Instead, use {@code getOtDaemon()}
@@ -518,17 +522,86 @@
         }
     }
 
+    @Override
+    public void setConfiguration(
+            @NonNull ThreadConfiguration configuration, @NonNull IOperationReceiver receiver) {
+        enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+        mHandler.post(() -> setConfigurationInternal(configuration, receiver));
+    }
+
+    private void setConfigurationInternal(
+            @NonNull ThreadConfiguration configuration,
+            @NonNull IOperationReceiver operationReceiver) {
+        checkOnHandlerThread();
+
+        Log.i(TAG, "Set Thread configuration: " + configuration);
+
+        final boolean changed = mPersistentSettings.putConfiguration(configuration);
+        try {
+            operationReceiver.onSuccess();
+        } catch (RemoteException e) {
+            // do nothing if the client is dead
+        }
+        if (changed) {
+            for (IConfigurationReceiver configReceiver : mConfigurationReceivers.keySet()) {
+                try {
+                    configReceiver.onConfigurationChanged(configuration);
+                } catch (RemoteException e) {
+                    // do nothing if the client is dead
+                }
+            }
+        }
+    }
+
+    @Override
+    public void registerConfigurationCallback(@NonNull IConfigurationReceiver callback) {
+        enforceAllPermissionsGranted(permission.THREAD_NETWORK_PRIVILEGED);
+        mHandler.post(() -> registerConfigurationCallbackInternal(callback));
+    }
+
+    private void registerConfigurationCallbackInternal(@NonNull IConfigurationReceiver callback) {
+        checkOnHandlerThread();
+        if (mConfigurationReceivers.containsKey(callback)) {
+            throw new IllegalStateException("Registering the same IConfigurationReceiver twice");
+        }
+        IBinder.DeathRecipient deathRecipient =
+                () -> mHandler.post(() -> unregisterConfigurationCallbackInternal(callback));
+        try {
+            callback.asBinder().linkToDeath(deathRecipient, 0);
+        } catch (RemoteException e) {
+            return;
+        }
+        mConfigurationReceivers.put(callback, deathRecipient);
+        try {
+            callback.onConfigurationChanged(mPersistentSettings.getConfiguration());
+        } catch (RemoteException e) {
+            // do nothing if the client is dead
+        }
+    }
+
+    @Override
+    public void unregisterConfigurationCallback(@NonNull IConfigurationReceiver callback) {
+        enforceAllPermissionsGranted(permission.THREAD_NETWORK_PRIVILEGED);
+        mHandler.post(() -> unregisterConfigurationCallbackInternal(callback));
+    }
+
+    private void unregisterConfigurationCallbackInternal(@NonNull IConfigurationReceiver callback) {
+        checkOnHandlerThread();
+        if (!mConfigurationReceivers.containsKey(callback)) {
+            return;
+        }
+        callback.asBinder().unlinkToDeath(mConfigurationReceivers.remove(callback), 0);
+    }
+
     private void registerUserRestrictionsReceiver() {
         mContext.registerReceiver(
                 new BroadcastReceiver() {
                     @Override
                     public void onReceive(Context context, Intent intent) {
-                        onUserRestrictionsChanged(isThreadUserRestricted());
+                        mHandler.post(() -> onUserRestrictionsChanged(isThreadUserRestricted()));
                     }
                 },
-                new IntentFilter(UserManager.ACTION_USER_RESTRICTIONS_CHANGED),
-                null /* broadcastPermission */,
-                mHandler);
+                new IntentFilter(UserManager.ACTION_USER_RESTRICTIONS_CHANGED));
     }
 
     private void onUserRestrictionsChanged(boolean newUserRestrictedState) {
@@ -580,12 +653,10 @@
                 new BroadcastReceiver() {
                     @Override
                     public void onReceive(Context context, Intent intent) {
-                        onAirplaneModeChanged(isAirplaneModeOn());
+                        mHandler.post(() -> onAirplaneModeChanged(isAirplaneModeOn()));
                     }
                 },
-                new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED),
-                null /* broadcastPermission */,
-                mHandler);
+                new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED));
     }
 
     private void onAirplaneModeChanged(boolean newAirplaneModeOn) {
@@ -884,9 +955,7 @@
         final byte[] securityFlags = new byte[] {(byte) 0xff, (byte) 0xf8};
 
         return new ActiveOperationalDataset.Builder()
-                .setActiveTimestamp(
-                        new OperationalDatasetTimestamp(
-                                now.getEpochSecond() & 0xffffffffffffL, 0, authoritative))
+                .setActiveTimestamp(OperationalDatasetTimestamp.fromInstant(now, authoritative))
                 .setExtendedPanId(newRandomBytes(random, LENGTH_EXTENDED_PAN_ID))
                 .setPanId(panId)
                 .setNetworkName(networkName)
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index f18aac9..747cc96 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -18,9 +18,11 @@
 
 import static com.android.net.module.util.DeviceConfigUtils.TETHERING_MODULE_NAME;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.ApexEnvironment;
 import android.content.Context;
+import android.net.thread.ThreadConfiguration;
 import android.os.PersistableBundle;
 import android.util.AtomicFile;
 import android.util.Log;
@@ -74,6 +76,16 @@
     /** Stores the Thread country code, null if no country code is stored. */
     public static final Key<String> THREAD_COUNTRY_CODE = new Key<>("thread_country_code", null);
 
+    /** Stores the Thread NAT64 feature toggle state, true for enabled and false for disabled. */
+    private static final Key<Boolean> CONFIG_NAT64_ENABLED =
+            new Key<>("config_nat64_enabled", false);
+
+    /**
+     * Stores the Thread DHCPv6-PD feature toggle state, true for enabled and false for disabled.
+     */
+    private static final Key<Boolean> CONFIG_DHCP6_PD_ENABLED =
+            new Key<>("config_dhcp6_pd_enabled", false);
+
     /******** Thread persistent setting keys ***************/
 
     @GuardedBy("mLock")
@@ -175,6 +187,30 @@
     }
 
     /**
+     * Store a {@link ThreadConfiguration} to the persistent settings.
+     *
+     * @param configuration {@link ThreadConfiguration} to be stored.
+     * @return {@code true} if the configuration was changed, {@code false} otherwise.
+     */
+    public boolean putConfiguration(@NonNull ThreadConfiguration configuration) {
+        if (getConfiguration().equals(configuration)) {
+            return false;
+        }
+        putObject(CONFIG_NAT64_ENABLED.key, configuration.isNat64Enabled());
+        putObject(CONFIG_DHCP6_PD_ENABLED.key, configuration.isDhcp6PdEnabled());
+        writeToStoreFile();
+        return true;
+    }
+
+    /** Retrieve the {@link ThreadConfiguration} from the persistent settings. */
+    public ThreadConfiguration getConfiguration() {
+        return new ThreadConfiguration.Builder()
+                .setNat64Enabled(get(CONFIG_NAT64_ENABLED))
+                .setDhcp6PdEnabled(get(CONFIG_DHCP6_PD_ENABLED))
+                .build();
+    }
+
+    /**
      * Base class to store string key and its default value.
      *
      * @param <T> Type of the value.
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 0e95703..41f34ff 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -45,6 +45,7 @@
 
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
+
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
 import android.content.Context;
diff --git a/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
index 2244a89..11c78e3 100644
--- a/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
+++ b/thread/tests/unit/src/android/net/thread/OperationalDatasetTimestampTest.java
@@ -41,6 +41,19 @@
     }
 
     @Test
+    public void fromInstant_authoritativeIsSetAsSpecified() {
+        Instant instant = Instant.now();
+
+        OperationalDatasetTimestamp timestampAuthoritativeFalse =
+                OperationalDatasetTimestamp.fromInstant(instant, false);
+        OperationalDatasetTimestamp timestampAuthoritativeTrue =
+                OperationalDatasetTimestamp.fromInstant(instant, true);
+
+        assertThat(timestampAuthoritativeFalse.isAuthoritativeSource()).isEqualTo(false);
+        assertThat(timestampAuthoritativeTrue.isAuthoritativeSource()).isEqualTo(true);
+    }
+
+    @Test
     public void fromTlvValue_goodValue_success() {
         OperationalDatasetTimestamp timestamp =
                 OperationalDatasetTimestamp.fromTlvValue(base16().decode("FFEEDDCCBBAA9989"));
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index 2f58943..6e2369f 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -59,6 +59,7 @@
 import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.IActiveOperationalDatasetReceiver;
 import android.net.thread.IOperationReceiver;
+import android.net.thread.ThreadConfiguration;
 import android.net.thread.ThreadNetworkException;
 import android.os.Handler;
 import android.os.IBinder;
@@ -98,6 +99,8 @@
 import java.nio.charset.StandardCharsets;
 import java.time.Clock;
 import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.ZoneId;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicReference;
@@ -539,9 +542,7 @@
                 .when(mContext)
                 .registerReceiver(
                         any(BroadcastReceiver.class),
-                        argThat(actualIntentFilter -> actualIntentFilter.hasAction(action)),
-                        any(),
-                        any());
+                        argThat(actualIntentFilter -> actualIntentFilter.hasAction(action)));
 
         return receiverRef;
     }
@@ -585,6 +586,55 @@
     }
 
     @Test
+    public void createRandomizedDataset_zeroNanoseconds_returnsZeroTicks() throws Exception {
+        Instant now = Instant.ofEpochSecond(0, 0);
+        Clock clock = Clock.fixed(now, ZoneId.systemDefault());
+        MockitoSession session =
+                ExtendedMockito.mockitoSession().mockStatic(SystemClock.class).startMocking();
+        final IActiveOperationalDatasetReceiver mockReceiver =
+                ExtendedMockito.mock(IActiveOperationalDatasetReceiver.class);
+
+        try {
+            ExtendedMockito.when(SystemClock.currentNetworkTimeClock()).thenReturn(clock);
+            mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+            mTestLooper.dispatchAll();
+        } finally {
+            session.finishMocking();
+        }
+
+        verify(mockReceiver, never()).onError(anyInt(), anyString());
+        verify(mockReceiver, times(1)).onSuccess(mActiveDatasetCaptor.capture());
+        ActiveOperationalDataset activeDataset = mActiveDatasetCaptor.getValue();
+        assertThat(activeDataset.getActiveTimestamp().getTicks()).isEqualTo(0);
+    }
+
+    @Test
+    public void createRandomizedDataset_maxNanoseconds_returnsMaxTicks() throws Exception {
+        // The nanoseconds to ticks conversion is rounded in the current implementation.
+        // 32767.5 / 32768 * 1000000000 = 999984741.2109375, using 999984741 to
+        // produce the maximum ticks.
+        Instant now = Instant.ofEpochSecond(0, 999984741);
+        Clock clock = Clock.fixed(now, ZoneId.systemDefault());
+        MockitoSession session =
+                ExtendedMockito.mockitoSession().mockStatic(SystemClock.class).startMocking();
+        final IActiveOperationalDatasetReceiver mockReceiver =
+                ExtendedMockito.mock(IActiveOperationalDatasetReceiver.class);
+
+        try {
+            ExtendedMockito.when(SystemClock.currentNetworkTimeClock()).thenReturn(clock);
+            mService.createRandomizedDataset(DEFAULT_NETWORK_NAME, mockReceiver);
+            mTestLooper.dispatchAll();
+        } finally {
+            session.finishMocking();
+        }
+
+        verify(mockReceiver, never()).onError(anyInt(), anyString());
+        verify(mockReceiver, times(1)).onSuccess(mActiveDatasetCaptor.capture());
+        ActiveOperationalDataset activeDataset = mActiveDatasetCaptor.getValue();
+        assertThat(activeDataset.getActiveTimestamp().getTicks()).isEqualTo(32767);
+    }
+
+    @Test
     public void createRandomizedDataset_hasNetworkTimeClock_datasetActiveTimestampIsAuthoritative()
             throws Exception {
         MockitoSession session =
@@ -708,4 +758,35 @@
         inOrder.verify(mMockTunIfController, times(1)).setInterfaceUp(false);
         inOrder.verify(mMockTunIfController, times(1)).setInterfaceUp(true);
     }
+
+    @Test
+    public void setConfiguration_configurationUpdated() throws Exception {
+        mService.initialize();
+        final IOperationReceiver mockReceiver1 = mock(IOperationReceiver.class);
+        final IOperationReceiver mockReceiver2 = mock(IOperationReceiver.class);
+        final IOperationReceiver mockReceiver3 = mock(IOperationReceiver.class);
+        ThreadConfiguration config1 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(false)
+                        .setDhcp6PdEnabled(false)
+                        .build();
+        ThreadConfiguration config2 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(true)
+                        .setDhcp6PdEnabled(true)
+                        .build();
+        ThreadConfiguration config3 =
+                new ThreadConfiguration.Builder(config2).build(); // Same as config2
+
+        mService.setConfiguration(config1, mockReceiver1);
+        mService.setConfiguration(config2, mockReceiver2);
+        mService.setConfiguration(config3, mockReceiver3);
+        mTestLooper.dispatchAll();
+
+        assertThat(mPersistentSettings.getConfiguration()).isEqualTo(config3);
+        InOrder inOrder = Mockito.inOrder(mockReceiver1, mockReceiver2, mockReceiver3);
+        inOrder.verify(mockReceiver1).onSuccess();
+        inOrder.verify(mockReceiver2).onSuccess();
+        inOrder.verify(mockReceiver3).onSuccess();
+    }
 }
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
index 7d2fe91..c932ac8 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadPersistentSettingsTest.java
@@ -21,16 +21,12 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.validateMockitoUsage;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.res.Resources;
+import android.net.thread.ThreadConfiguration;
 import android.os.PersistableBundle;
 import android.util.AtomicFile;
 
@@ -42,13 +38,14 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 import java.io.ByteArrayOutputStream;
-import java.io.FileInputStream;
 import java.io.FileOutputStream;
 
 /** Unit tests for {@link ThreadPersistentSettings}. */
@@ -57,12 +54,15 @@
 public class ThreadPersistentSettingsTest {
     private static final String TEST_COUNTRY_CODE = "CN";
 
-    @Mock private AtomicFile mAtomicFile;
     @Mock Resources mResources;
     @Mock ConnectivityResources mConnectivityResources;
 
+    private AtomicFile mAtomicFile;
     private ThreadPersistentSettings mThreadPersistentSettings;
 
+    @Rule(order = 0)
+    public final TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -70,8 +70,7 @@
         when(mConnectivityResources.get()).thenReturn(mResources);
         when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
 
-        FileOutputStream fos = mock(FileOutputStream.class);
-        when(mAtomicFile.startWrite()).thenReturn(fos);
+        mAtomicFile = createAtomicFile();
         mThreadPersistentSettings =
                 new ThreadPersistentSettings(mAtomicFile, mConnectivityResources);
     }
@@ -85,7 +84,7 @@
     @Test
     public void initialize_readsFromFile() throws Exception {
         byte[] data = createXmlForParsing(THREAD_ENABLED.key, false);
-        setupAtomicFileMockForRead(data);
+        setupAtomicFileForRead(data);
 
         mThreadPersistentSettings.initialize();
 
@@ -95,7 +94,7 @@
     @Test
     public void initialize_ThreadDisabledInResources_returnsThreadDisabled() throws Exception {
         when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
-        setupAtomicFileMockForRead(new byte[0]);
+        setupAtomicFileForRead(new byte[0]);
 
         mThreadPersistentSettings.initialize();
 
@@ -107,7 +106,7 @@
             throws Exception {
         when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(false);
         byte[] data = createXmlForParsing(THREAD_ENABLED.key, true);
-        setupAtomicFileMockForRead(data);
+        setupAtomicFileForRead(data);
 
         mThreadPersistentSettings.initialize();
 
@@ -119,9 +118,6 @@
         mThreadPersistentSettings.put(THREAD_ENABLED.key, true);
 
         assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isTrue();
-        // Confirm that file writes have been triggered.
-        verify(mAtomicFile).startWrite();
-        verify(mAtomicFile).finishWrite(any());
     }
 
     @Test
@@ -129,9 +125,8 @@
         mThreadPersistentSettings.put(THREAD_ENABLED.key, false);
 
         assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
-        // Confirm that file writes have been triggered.
-        verify(mAtomicFile).startWrite();
-        verify(mAtomicFile).finishWrite(any());
+        mThreadPersistentSettings.initialize();
+        assertThat(mThreadPersistentSettings.get(THREAD_ENABLED)).isFalse();
     }
 
     @Test
@@ -139,10 +134,8 @@
         mThreadPersistentSettings.put(THREAD_COUNTRY_CODE.key, TEST_COUNTRY_CODE);
 
         assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isEqualTo(TEST_COUNTRY_CODE);
-
-        // Confirm that file writes have been triggered.
-        verify(mAtomicFile).startWrite();
-        verify(mAtomicFile).finishWrite(any());
+        mThreadPersistentSettings.initialize();
+        assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isEqualTo(TEST_COUNTRY_CODE);
     }
 
     @Test
@@ -150,10 +143,63 @@
         mThreadPersistentSettings.put(THREAD_COUNTRY_CODE.key, null);
 
         assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isNull();
+        mThreadPersistentSettings.initialize();
+        assertThat(mThreadPersistentSettings.get(THREAD_COUNTRY_CODE)).isNull();
+    }
 
-        // Confirm that file writes have been triggered.
-        verify(mAtomicFile).startWrite();
-        verify(mAtomicFile).finishWrite(any());
+    @Test
+    public void putConfiguration_sameValues_returnsFalse() {
+        ThreadConfiguration configuration =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(true)
+                        .setDhcp6PdEnabled(true)
+                        .build();
+        mThreadPersistentSettings.putConfiguration(configuration);
+
+        assertThat(mThreadPersistentSettings.putConfiguration(configuration)).isFalse();
+    }
+
+    @Test
+    public void putConfiguration_differentValues_returnsTrue() {
+        ThreadConfiguration configuration1 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(false)
+                        .setDhcp6PdEnabled(false)
+                        .build();
+        mThreadPersistentSettings.putConfiguration(configuration1);
+        ThreadConfiguration configuration2 =
+                new ThreadConfiguration.Builder()
+                        .setNat64Enabled(true)
+                        .setDhcp6PdEnabled(true)
+                        .build();
+
+        assertThat(mThreadPersistentSettings.putConfiguration(configuration2)).isTrue();
+    }
+
+    @Test
+    public void putConfiguration_nat64Enabled_valuesUpdatedAndPersisted() throws Exception {
+        ThreadConfiguration configuration =
+                new ThreadConfiguration.Builder().setNat64Enabled(true).build();
+        mThreadPersistentSettings.putConfiguration(configuration);
+
+        assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
+        mThreadPersistentSettings.initialize();
+        assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
+    }
+
+    @Test
+    public void putConfiguration_dhcp6PdEnabled_valuesUpdatedAndPersisted() throws Exception {
+        ThreadConfiguration configuration =
+                new ThreadConfiguration.Builder().setDhcp6PdEnabled(true).build();
+        mThreadPersistentSettings.putConfiguration(configuration);
+
+        assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
+        mThreadPersistentSettings.initialize();
+        assertThat(mThreadPersistentSettings.getConfiguration()).isEqualTo(configuration);
+    }
+
+    private AtomicFile createAtomicFile() throws Exception {
+        return new AtomicFile(mTemporaryFolder.newFile());
     }
 
     private byte[] createXmlForParsing(String key, Boolean value) throws Exception {
@@ -164,19 +210,9 @@
         return outputStream.toByteArray();
     }
 
-    private void setupAtomicFileMockForRead(byte[] dataToRead) throws Exception {
-        FileInputStream is = mock(FileInputStream.class);
-        when(mAtomicFile.openRead()).thenReturn(is);
-        when(is.available()).thenReturn(dataToRead.length).thenReturn(0);
-        doAnswer(
-                        invocation -> {
-                            byte[] data = invocation.getArgument(0);
-                            int pos = invocation.getArgument(1);
-                            if (pos == dataToRead.length) return 0; // read complete.
-                            System.arraycopy(dataToRead, 0, data, 0, dataToRead.length);
-                            return dataToRead.length;
-                        })
-                .when(is)
-                .read(any(), anyInt(), anyInt());
+    private void setupAtomicFileForRead(byte[] dataToRead) throws Exception {
+        try (FileOutputStream outputStream = new FileOutputStream(mAtomicFile.getBaseFile())) {
+            outputStream.write(dataToRead);
+        }
     }
 }