Merge "[Thread] fix the issue that border routing is not available after ot-daemon restarts" into main
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
index 506fa56..7a6df88 100644
--- a/Tethering/src/android/net/ip/IpServer.java
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -124,6 +124,8 @@
     // TODO: have PanService use some visible version of this constant
     private static final String BLUETOOTH_IFACE_ADDR = "192.168.44.1/24";
 
+    private static final String LEGACY_WIFI_P2P_IFACE_ADDRESS = "192.168.49.1/24";
+
     // TODO: have this configurable
     private static final int DHCP_LEASE_TIME_SECS = 3600;
 
@@ -249,6 +251,7 @@
     private final LinkProperties mLinkProperties;
     private final boolean mUsingLegacyDhcp;
     private final int mP2pLeasesSubnetPrefixLength;
+    private final boolean mIsWifiP2pDedicatedIpEnabled;
 
     private final Dependencies mDeps;
 
@@ -313,6 +316,7 @@
         mLinkProperties = new LinkProperties();
         mUsingLegacyDhcp = config.useLegacyDhcpServer();
         mP2pLeasesSubnetPrefixLength = config.getP2pLeasesSubnetPrefixLength();
+        mIsWifiP2pDedicatedIpEnabled = config.shouldEnableWifiP2pDedicatedIp();
         mPrivateAddressCoordinator = addressCoordinator;
         mDeps = deps;
         mTetheringMetrics = tetheringMetrics;
@@ -698,11 +702,18 @@
         return (mInterfaceType == TetheringManager.TETHERING_BLUETOOTH) && !SdkLevel.isAtLeastT();
     }
 
+    private boolean shouldUseWifiP2pDedicatedIp() {
+        return mIsWifiP2pDedicatedIpEnabled
+                && mInterfaceType == TetheringManager.TETHERING_WIFI_P2P;
+    }
+
     private LinkAddress requestIpv4Address(final int scope, final boolean useLastAddress) {
         if (mStaticIpv4ServerAddr != null) return mStaticIpv4ServerAddr;
 
         if (shouldNotConfigureBluetoothInterface()) return new LinkAddress(BLUETOOTH_IFACE_ADDR);
 
+        if (shouldUseWifiP2pDedicatedIp()) return new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS);
+
         return mPrivateAddressCoordinator.requestDownstreamAddress(this, scope, useLastAddress);
     }
 
diff --git a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
index 1d5df61..50f82cf 100644
--- a/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/PrivateAddressCoordinator.java
@@ -28,6 +28,7 @@
 
 import static java.util.Arrays.asList;
 
+import android.content.Context;
 import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
@@ -42,6 +43,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.DeviceConfigUtils;
 
 import java.net.Inet4Address;
 import java.net.InetAddress;
@@ -67,6 +69,9 @@
     // WARNING: Keep in sync with chooseDownstreamAddress
     public static final int PREFIX_LENGTH = 24;
 
+    public static final String TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION =
+            "tether_force_random_prefix_base_selection";
+
     // Upstream monitor would be stopped when tethering is down. When tethering restart, downstream
     // address may be requested before coordinator get current upstream notification. To ensure
     // coordinator do not select conflict downstream prefix, mUpstreamPrefixMap would not be cleared
@@ -79,20 +84,42 @@
     private final List<IpPrefix> mTetheringPrefixes;
     // A supplier that returns ConnectivityManager#getAllNetworks.
     private final Supplier<Network[]> mGetAllNetworksSupplier;
-    private final boolean mIsRandomPrefixBaseEnabled;
-    private final boolean mShouldEnableWifiP2pDedicatedIp;
+    private final Dependencies mDeps;
     // keyed by downstream type(TetheringManager.TETHERING_*).
     private final ArrayMap<AddressKey, LinkAddress> mCachedAddresses;
     private final Random mRandom;
 
+    /** Capture PrivateAddressCoordinator dependencies for injection. */
+    public static class Dependencies {
+        private final Context mContext;
+
+        Dependencies(Context context) {
+            mContext = context;
+        }
+
+        /**
+         * Check whether or not one specific experimental feature is enabled according to {@link
+         * DeviceConfigUtils}.
+         *
+         * @param featureName The feature's name to look up.
+         * @return true if this feature is enabled, or false if disabled.
+         */
+        public boolean isFeatureEnabled(String featureName) {
+            return DeviceConfigUtils.isTetheringFeatureEnabled(mContext, featureName);
+        }
+    }
+
+    public PrivateAddressCoordinator(Supplier<Network[]> getAllNetworksSupplier, Context context) {
+        this(getAllNetworksSupplier, new Dependencies(context));
+    }
+
+    @VisibleForTesting
     public PrivateAddressCoordinator(Supplier<Network[]> getAllNetworksSupplier,
-            boolean isRandomPrefixBase,
-            boolean shouldEnableWifiP2pDedicatedIp) {
+                                     Dependencies deps) {
         mDownstreams = new ArraySet<>();
         mUpstreamPrefixMap = new ArrayMap<>();
         mGetAllNetworksSupplier = getAllNetworksSupplier;
-        mIsRandomPrefixBaseEnabled = isRandomPrefixBase;
-        mShouldEnableWifiP2pDedicatedIp = shouldEnableWifiP2pDedicatedIp;
+        mDeps = deps;
         mCachedAddresses = new ArrayMap<AddressKey, LinkAddress>();
         // Reserved static addresses for bluetooth and wifi p2p.
         mCachedAddresses.put(new AddressKey(TETHERING_BLUETOOTH, CONNECTIVITY_SCOPE_GLOBAL),
@@ -179,11 +206,6 @@
     @Nullable
     public LinkAddress requestDownstreamAddress(final IpServer ipServer, final int scope,
             boolean useLastAddress) {
-        if (mShouldEnableWifiP2pDedicatedIp
-                && ipServer.interfaceType() == TETHERING_WIFI_P2P) {
-            return new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS);
-        }
-
         final AddressKey addrKey = new AddressKey(ipServer.interfaceType(), scope);
         // This ensures that tethering isn't started on 2 different interfaces with the same type.
         // Once tethering could support multiple interface with the same type,
@@ -212,7 +234,7 @@
     }
 
     private int getRandomPrefixIndex() {
-        if (!mIsRandomPrefixBaseEnabled) return 0;
+        if (!mDeps.isFeatureEnabled(TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION)) return 0;
 
         final int random = getRandomInt() & 0xffffff;
         // This is to select the starting prefix range (/8, /12, or /16) instead of the actual
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 49bc86e..13b8004 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -359,10 +359,7 @@
         // Load tethering configuration.
         updateConfiguration();
         mConfig.readEnableSyncSM(mContext);
-        // It is OK for the configuration to be passed to the PrivateAddressCoordinator at
-        // construction time because the only part of the configuration it uses is
-        // shouldEnableWifiP2pDedicatedIp(), and currently do not support changing that.
-        mPrivateAddressCoordinator = mDeps.makePrivateAddressCoordinator(mContext, mConfig);
+        mPrivateAddressCoordinator = mDeps.makePrivateAddressCoordinator(mContext);
 
         // Must be initialized after tethering configuration is loaded because BpfCoordinator
         // constructor needs to use the configuration.
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index c9817c9..b3e9c1b 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -182,7 +182,6 @@
     private final int mP2pLeasesSubnetPrefixLength;
 
     private final boolean mEnableWearTethering;
-    private final boolean mRandomPrefixBase;
 
     private final int mUsbTetheringFunction;
     protected final ContentResolver mContentResolver;
@@ -300,8 +299,6 @@
 
         mEnableWearTethering = shouldEnableWearTethering(ctx);
 
-        mRandomPrefixBase = mDeps.isFeatureEnabled(ctx, TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION);
-
         configLog.log(toString());
     }
 
@@ -390,10 +387,6 @@
         return mEnableWearTethering;
     }
 
-    public boolean isRandomPrefixBaseEnabled() {
-        return mRandomPrefixBase;
-    }
-
     /**
      * Check whether sync SM is enabled then set it to USE_SYNC_SM. This should be called once
      * when tethering is created. Otherwise if the flag is pushed while tethering is enabled,
@@ -455,9 +448,6 @@
         pw.print("mUsbTetheringFunction: ");
         pw.println(isUsingNcm() ? "NCM" : "RNDIS");
 
-        pw.print("mRandomPrefixBase: ");
-        pw.println(mRandomPrefixBase);
-
         pw.print("USE_SYNC_SM: ");
         pw.println(USE_SYNC_SM);
     }
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 81f057c..cc878d5 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -177,13 +177,9 @@
     /**
      * Make PrivateAddressCoordinator to be used by Tethering.
      */
-    public PrivateAddressCoordinator makePrivateAddressCoordinator(
-            Context ctx, TetheringConfiguration cfg) {
+    public PrivateAddressCoordinator makePrivateAddressCoordinator(Context ctx) {
         final ConnectivityManager cm = ctx.getSystemService(ConnectivityManager.class);
-        return new PrivateAddressCoordinator(
-                cm::getAllNetworks,
-                cfg.isRandomPrefixBaseEnabled(),
-                cfg.shouldEnableWifiP2pDedicatedIp());
+        return new PrivateAddressCoordinator(cm::getAllNetworks, ctx);
     }
 
     /**
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index 6de4062..a744953 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -433,7 +433,6 @@
      * @param reported a NetworkTetheringReported object containing statistics to write
      */
     private void write(@NonNull final NetworkTetheringReported reported) {
-        final byte[] upstreamEvents = reported.getUpstreamEvents().toByteArray();
         mDependencies.write(reported);
         if (DBG) {
             Log.d(
@@ -447,7 +446,7 @@
                     + ", userType: "
                     + reported.getUserType().getNumber()
                     + ", upstreamTypes: "
-                    + Arrays.toString(upstreamEvents)
+                    + Arrays.toString(reported.getUpstreamEvents().toByteArray())
                     + ", durationMillis: "
                     + reported.getDurationMillis());
         }
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index 177296a..f7834a3 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -139,6 +139,7 @@
     private static final boolean DEFAULT_USING_BPF_OFFLOAD = true;
     private static final int DEFAULT_SUBNET_PREFIX_LENGTH = 0;
     private static final int P2P_SUBNET_PREFIX_LENGTH = 25;
+    private static final String LEGACY_WIFI_P2P_IFACE_ADDRESS = "192.168.49.1/24";
 
     private static final InterfaceParams TEST_IFACE_PARAMS = new InterfaceParams(
             IFACE_NAME, 42 /* index */, MacAddress.ALL_ZEROS_ADDRESS, 1500 /* defaultMtu */);
@@ -196,6 +197,12 @@
 
     private void initStateMachine(int interfaceType, boolean usingLegacyDhcp,
             boolean usingBpfOffload) throws Exception {
+        initStateMachine(interfaceType, usingLegacyDhcp, usingBpfOffload,
+                false /* shouldEnableWifiP2pDedicatedIp */);
+    }
+
+    private void initStateMachine(int interfaceType, boolean usingLegacyDhcp,
+            boolean usingBpfOffload, boolean shouldEnableWifiP2pDedicatedIp) throws Exception {
         when(mDependencies.getDadProxy(any(), any())).thenReturn(mDadProxy);
         when(mDependencies.getRouterAdvertisementDaemon(any())).thenReturn(mRaDaemon);
         when(mDependencies.getInterfaceParams(IFACE_NAME)).thenReturn(TEST_IFACE_PARAMS);
@@ -213,6 +220,8 @@
         when(mTetherConfig.isBpfOffloadEnabled()).thenReturn(usingBpfOffload);
         when(mTetherConfig.useLegacyDhcpServer()).thenReturn(usingLegacyDhcp);
         when(mTetherConfig.getP2pLeasesSubnetPrefixLength()).thenReturn(P2P_SUBNET_PREFIX_LENGTH);
+        when(mTetherConfig.shouldEnableWifiP2pDedicatedIp())
+                .thenReturn(shouldEnableWifiP2pDedicatedIp);
         when(mBpfCoordinator.isUsingBpfOffload()).thenReturn(usingBpfOffload);
         mIpServer = createIpServer(interfaceType);
         mIpServer.start();
@@ -409,7 +418,7 @@
     }
 
     @Test
-    public void canBeTetheredAsWifiP2p() throws Exception {
+    public void canBeTetheredAsWifiP2p_NotUsingDedicatedIp() throws Exception {
         initStateMachine(TETHERING_WIFI_P2P);
 
         dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
@@ -431,6 +440,33 @@
     }
 
     @Test
+    public void canBeTetheredAsWifiP2p_UsingDedicatedIp() throws Exception {
+        initStateMachine(TETHERING_WIFI_P2P, false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD,
+                true /* shouldEnableWifiP2pDedicatedIp */);
+
+        dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+        InOrder inOrder = inOrder(mCallback, mNetd, mAddressCoordinator);
+        // When using WiFi P2p dedicated IP, the IpServer just picks the IP address without
+        // requesting for it at PrivateAddressCoordinator.
+        inOrder.verify(mAddressCoordinator, never()).requestDownstreamAddress(any(), anyInt(),
+                anyBoolean());
+        inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg ->
+                IFACE_NAME.equals(cfg.ifName) && assertNotContainsFlag(cfg.flags, IF_STATE_UP)));
+        inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME);
+        inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME);
+        inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME),
+                any(), any());
+        inOrder.verify(mCallback).updateInterfaceState(
+                mIpServer, STATE_LOCAL_ONLY, TETHER_ERROR_NO_ERROR);
+        inOrder.verify(mCallback).updateLinkProperties(
+                eq(mIpServer), mLinkPropertiesCaptor.capture());
+        assertIPv4AddressAndDirectlyConnectedRoute(mLinkPropertiesCaptor.getValue());
+        assertEquals(List.of(new LinkAddress(LEGACY_WIFI_P2P_IFACE_ADDRESS)),
+                mLinkPropertiesCaptor.getValue().getLinkAddresses());
+        verifyNoMoreInteractions(mNetd, mCallback, mAddressCoordinator);
+    }
+
+    @Test
     public void handlesFirstUpstreamChange() throws Exception {
         initTetheredStateMachine(TETHERING_BLUETOOTH, null);
 
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
index a5c06f3..bff1fda 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java
@@ -25,6 +25,7 @@
 import static android.net.TetheringManager.TETHERING_WIFI;
 import static android.net.TetheringManager.TETHERING_WIFI_P2P;
 
+import static com.android.networkstack.tethering.PrivateAddressCoordinator.TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION;
 import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix;
 
 import static org.junit.Assert.assertEquals;
@@ -71,7 +72,7 @@
     @Mock private IpServer mWifiP2pIpServer;
     @Mock private Context mContext;
     @Mock private ConnectivityManager mConnectivityMgr;
-    @Mock private TetheringConfiguration mConfig;
+    @Mock private PrivateAddressCoordinator.Dependencies mDeps;
 
     private PrivateAddressCoordinator mPrivateAddressCoordinator;
     private final LinkAddress mBluetoothAddress = new LinkAddress("192.168.44.1/24");
@@ -106,15 +107,9 @@
         when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(mConnectivityMgr);
         when(mContext.getSystemService(ConnectivityManager.class)).thenReturn(mConnectivityMgr);
         when(mConnectivityMgr.getAllNetworks()).thenReturn(mAllNetworks);
-        when(mConfig.shouldEnableWifiP2pDedicatedIp()).thenReturn(false);
-        when(mConfig.isRandomPrefixBaseEnabled()).thenReturn(false);
         setUpIpServers();
         mPrivateAddressCoordinator =
-                spy(
-                        new PrivateAddressCoordinator(
-                                mConnectivityMgr::getAllNetworks,
-                                mConfig.isRandomPrefixBaseEnabled(),
-                                mConfig.shouldEnableWifiP2pDedicatedIp()));
+                spy(new PrivateAddressCoordinator(mConnectivityMgr::getAllNetworks, mDeps));
     }
 
     private LinkAddress requestDownstreamAddress(final IpServer ipServer, int scope,
@@ -282,24 +277,6 @@
     }
 
     @Test
-    public void testEnableLegacyWifiP2PAddress() throws Exception {
-        when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(
-                getSubAddress(mLegacyWifiP2pAddress.getAddress().getAddress()));
-        // No matter #shouldEnableWifiP2pDedicatedIp() is enabled or not, legacy wifi p2p prefix
-        // is resevered.
-        assertReseveredWifiP2pPrefix();
-
-        when(mConfig.shouldEnableWifiP2pDedicatedIp()).thenReturn(true);
-        assertReseveredWifiP2pPrefix();
-
-        // If #shouldEnableWifiP2pDedicatedIp() is enabled, wifi P2P gets the configured address.
-        LinkAddress address = requestDownstreamAddress(mWifiP2pIpServer,
-                CONNECTIVITY_SCOPE_LOCAL, true /* useLastAddress */);
-        assertEquals(mLegacyWifiP2pAddress, address);
-        mPrivateAddressCoordinator.releaseDownstream(mWifiP2pIpServer);
-    }
-
-    @Test
     public void testEnableSapAndLohsConcurrently() throws Exception {
         final LinkAddress hotspotAddress = requestDownstreamAddress(mHotspotIpServer,
                 CONNECTIVITY_SCOPE_GLOBAL, true /* useLastAddress */);
@@ -317,7 +294,7 @@
 
     @Test
     public void testStartedPrefixRange() throws Exception {
-        when(mConfig.isRandomPrefixBaseEnabled()).thenReturn(true);
+        when(mDeps.isFeatureEnabled(TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION)).thenReturn(true);
 
         startedPrefixBaseTest("192.168.0.0/16", 0);
 
@@ -343,11 +320,7 @@
     private void startedPrefixBaseTest(final String expected, final int randomIntForPrefixBase)
             throws Exception {
         mPrivateAddressCoordinator =
-                spy(
-                        new PrivateAddressCoordinator(
-                                mConnectivityMgr::getAllNetworks,
-                                mConfig.isRandomPrefixBaseEnabled(),
-                                mConfig.shouldEnableWifiP2pDedicatedIp()));
+                spy(new PrivateAddressCoordinator(mConnectivityMgr::getAllNetworks, mDeps));
         when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomIntForPrefixBase);
         final LinkAddress address = requestDownstreamAddress(mHotspotIpServer,
                 CONNECTIVITY_SCOPE_GLOBAL, false /* useLastAddress */);
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 9a4945e..66fe957 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -71,6 +71,7 @@
 import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_HIDL_1_0;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.OFFLOAD_HAL_VERSION_NONE;
+import static com.android.networkstack.tethering.PrivateAddressCoordinator.TETHER_FORCE_RANDOM_PREFIX_BASE_SELECTION;
 import static com.android.networkstack.tethering.TestConnectivityManager.BROADCAST_FIRST;
 import static com.android.networkstack.tethering.TestConnectivityManager.CALLBACKS_FIRST;
 import static com.android.networkstack.tethering.Tethering.UserRestrictionActionListener;
@@ -293,6 +294,7 @@
     @Mock private TetheredInterfaceRequestShim mTetheredInterfaceRequestShim;
     @Mock private TetheringMetrics mTetheringMetrics;
     @Mock private RoutingCoordinatorManager mRoutingCoordinatorManager;
+    @Mock private PrivateAddressCoordinator.Dependencies mPrivateAddressCoordinatorDependencies;
 
     private final MockIpServerDependencies mIpServerDependencies =
             spy(new MockIpServerDependencies());
@@ -535,9 +537,11 @@
         }
 
         @Override
-        public PrivateAddressCoordinator makePrivateAddressCoordinator(Context ctx,
-                TetheringConfiguration cfg) {
-            mPrivateAddressCoordinator = super.makePrivateAddressCoordinator(ctx, cfg);
+        public PrivateAddressCoordinator makePrivateAddressCoordinator(Context ctx) {
+            ConnectivityManager cm = ctx.getSystemService(ConnectivityManager.class);
+            mPrivateAddressCoordinator =
+                    new PrivateAddressCoordinator(
+                            cm::getAllNetworks, mPrivateAddressCoordinatorDependencies);
             return mPrivateAddressCoordinator;
         }
 
@@ -664,6 +668,8 @@
                 .thenReturn(true);
         initOffloadConfiguration(OFFLOAD_HAL_VERSION_HIDL_1_0, 0 /* defaultDisabled */);
         when(mOffloadHardwareInterface.getForwardedStats(any())).thenReturn(mForwardedStats);
+        when(mPrivateAddressCoordinatorDependencies.isFeatureEnabled(anyString()))
+                .thenReturn(false);
 
         mServiceContext = new TestContext(mContext);
         mServiceContext.setUseRegisteredHandlers(true);
diff --git a/common/thread_flags.aconfig b/common/thread_flags.aconfig
index 14b70d0..60120bc 100644
--- a/common/thread_flags.aconfig
+++ b/common/thread_flags.aconfig
@@ -35,3 +35,12 @@
     description: "Controls whether the Android Thread Ephemeral Key feature is enabled"
     bug: "348323500"
 }
+
+flag {
+    name: "set_nat64_configuration_enabled"
+    is_exported: true
+    is_fixed_read_only: true
+    namespace: "thread_network"
+    description: "Controls whether the setConfiguration API of NAT64 feature is enabled"
+    bug: "368456504"
+}
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 09a3681..08129eb 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -506,6 +506,13 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.net.thread.ThreadConfiguration> CREATOR;
   }
 
+  @FlaggedApi("com.android.net.thread.flags.set_nat64_configuration_enabled") public static final class ThreadConfiguration.Builder {
+    ctor @FlaggedApi("com.android.net.thread.flags.set_nat64_configuration_enabled") public ThreadConfiguration.Builder();
+    ctor @FlaggedApi("com.android.net.thread.flags.set_nat64_configuration_enabled") public ThreadConfiguration.Builder(@NonNull android.net.thread.ThreadConfiguration);
+    method @FlaggedApi("com.android.net.thread.flags.set_nat64_configuration_enabled") @NonNull public android.net.thread.ThreadConfiguration build();
+    method @FlaggedApi("com.android.net.thread.flags.set_nat64_configuration_enabled") @NonNull public android.net.thread.ThreadConfiguration.Builder setNat64Enabled(boolean);
+  }
+
   @FlaggedApi("com.android.net.thread.flags.thread_enabled") public final class ThreadNetworkController {
     method @FlaggedApi("com.android.net.thread.flags.epskc_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void activateEphemeralKeyMode(@NonNull java.time.Duration, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
     method public void createRandomizedDataset(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.thread.ActiveOperationalDataset,android.net.thread.ThreadNetworkException>);
@@ -520,6 +527,7 @@
     method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void registerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.net.thread.ThreadNetworkController.StateCallback);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void scheduleMigration(@NonNull android.net.thread.PendingOperationalDataset, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
     method @FlaggedApi("com.android.net.thread.flags.channel_max_powers_enabled") @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void setChannelMaxPowers(@NonNull @Size(min=1) android.util.SparseIntArray, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
+    method @FlaggedApi("com.android.net.thread.flags.set_nat64_configuration_enabled") @RequiresPermission(android.Manifest.permission.THREAD_NETWORK_PRIVILEGED) public void setConfiguration(@NonNull android.net.thread.ThreadConfiguration, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
     method @RequiresPermission("android.permission.THREAD_NETWORK_PRIVILEGED") public void setEnabled(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.net.thread.ThreadNetworkException>);
     method @FlaggedApi("com.android.net.thread.flags.configuration_enabled") @RequiresPermission(android.Manifest.permission.THREAD_NETWORK_PRIVILEGED) public void unregisterConfigurationCallback(@NonNull java.util.function.Consumer<android.net.thread.ThreadConfiguration>);
     method @RequiresPermission(allOf={android.Manifest.permission.ACCESS_NETWORK_STATE, "android.permission.THREAD_NETWORK_PRIVILEGED"}) public void unregisterOperationalDatasetCallback(@NonNull android.net.thread.ThreadNetworkController.OperationalDatasetCallback);
diff --git a/networksecurity/OWNERS b/networksecurity/OWNERS
index 1a4130a..0c838c0 100644
--- a/networksecurity/OWNERS
+++ b/networksecurity/OWNERS
@@ -1,4 +1,5 @@
 # Bug component: 1479456
 
+bessiej@google.com
 sandrom@google.com
 tweek@google.com
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index b2ef345..fd73b29 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.net.ct;
 
+import android.annotation.NonNull;
 import android.annotation.RequiresApi;
 import android.app.DownloadManager;
 import android.content.BroadcastReceiver;
@@ -31,10 +32,13 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
 import java.security.KeyFactory;
+import java.security.PublicKey;
 import java.security.Signature;
 import java.security.spec.X509EncodedKeySpec;
 import java.util.Base64;
+import java.util.Optional;
 
 /** Helper class to download certificate transparency log files. */
 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@@ -42,41 +46,23 @@
 
     private static final String TAG = "CertificateTransparencyDownloader";
 
-    // TODO: move key to a DeviceConfig flag.
-    private static final byte[] PUBLIC_KEY_BYTES =
-            Base64.getDecoder()
-                    .decode(
-                            "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsu0BHGnQ++W2CTdyZyxv"
-                                + "HHRALOZPlnu/VMVgo2m+JZ8MNbAOH2cgXb8mvOj8flsX/qPMuKIaauO+PwROMjiq"
-                                + "fUpcFm80Kl7i97ZQyBDYKm3MkEYYpGN+skAR2OebX9G2DfDqFY8+jUpOOWtBNr3L"
-                                + "rmVcwx+FcFdMjGDlrZ5JRmoJ/SeGKiORkbbu9eY1Wd0uVhz/xI5bQb0OgII7hEj+"
-                                + "i/IPbJqOHgB8xQ5zWAJJ0DmG+FM6o7gk403v6W3S8qRYiR84c50KppGwe4YqSMkF"
-                                + "bLDleGQWLoaDSpEWtESisb4JiLaY4H+Kk0EyAhPSb+49JfUozYl+lf7iFN3qRq/S"
-                                + "IXXTh6z0S7Qa8EYDhKGCrpI03/+qprwy+my6fpWHi6aUIk4holUCmWvFxZDfixox"
-                                + "K0RlqbFDl2JXMBquwlQpm8u5wrsic1ksIv9z8x9zh4PJqNpCah0ciemI3YGRQqSe"
-                                + "/mRRXBiSn9YQBUPcaeqCYan+snGADFwHuXCd9xIAdFBolw9R9HTedHGUfVXPJDiF"
-                                + "4VusfX6BRR/qaadB+bqEArF/TzuDUr6FvOR4o8lUUxgLuZ/7HO+bHnaPFKYHHSm+"
-                                + "+z1lVDhhYuSZ8ax3T0C3FZpb7HMjZtpEorSV5ElKJEJwrhrBCMOD8L01EoSPrGlS"
-                                + "1w22i9uGHMn/uGQKo28u7AsCAwEAAQ==");
-
     private final Context mContext;
     private final DataStore mDataStore;
     private final DownloadHelper mDownloadHelper;
     private final CertificateTransparencyInstaller mInstaller;
-    private final byte[] mPublicKey;
+
+    @NonNull private Optional<PublicKey> mPublicKey = Optional.empty();
 
     @VisibleForTesting
     CertificateTransparencyDownloader(
             Context context,
             DataStore dataStore,
             DownloadHelper downloadHelper,
-            CertificateTransparencyInstaller installer,
-            byte[] publicKey) {
+            CertificateTransparencyInstaller installer) {
         mContext = context;
         mDataStore = dataStore;
         mDownloadHelper = downloadHelper;
         mInstaller = installer;
-        mPublicKey = publicKey;
     }
 
     CertificateTransparencyDownloader(Context context, DataStore dataStore) {
@@ -84,8 +70,7 @@
                 context,
                 dataStore,
                 new DownloadHelper(context),
-                new CertificateTransparencyInstaller(),
-                PUBLIC_KEY_BYTES);
+                new CertificateTransparencyInstaller());
     }
 
     void registerReceiver() {
@@ -98,6 +83,20 @@
         }
     }
 
+    void setPublicKey(String publicKey) throws GeneralSecurityException {
+        mPublicKey =
+                Optional.of(
+                        KeyFactory.getInstance("RSA")
+                                .generatePublic(
+                                        new X509EncodedKeySpec(
+                                                Base64.getDecoder().decode(publicKey))));
+    }
+
+    @VisibleForTesting
+    void resetPublicKey() {
+        mPublicKey = Optional.empty();
+    }
+
     void startMetadataDownload(String metadataUrl) {
         long downloadId = download(metadataUrl);
         if (downloadId == -1) {
@@ -202,9 +201,11 @@
     }
 
     private boolean verify(Uri file, Uri signature) throws IOException, GeneralSecurityException {
+        if (!mPublicKey.isPresent()) {
+            throw new InvalidKeyException("Missing public key for signature verification");
+        }
         Signature verifier = Signature.getInstance("SHA256withRSA");
-        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
-        verifier.initVerify(keyFactory.generatePublic(new X509EncodedKeySpec(mPublicKey)));
+        verifier.initVerify(mPublicKey.get());
         ContentResolver contentResolver = mContext.getContentResolver();
 
         try (InputStream fileStream = contentResolver.openInputStream(file);
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
index a263546..914af06 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
@@ -23,6 +23,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import java.security.GeneralSecurityException;
 import java.util.concurrent.Executors;
 
 /** Listener class for the Certificate Transparency Phenotype flags. */
@@ -57,21 +58,35 @@
             return;
         }
 
+        String newPublicKey =
+                DeviceConfig.getString(
+                        Config.NAMESPACE_NETWORK_SECURITY,
+                        Config.FLAG_PUBLIC_KEY,
+                        /* defaultValue= */ "");
         String newVersion =
-                DeviceConfig.getString(Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_VERSION, "");
+                DeviceConfig.getString(
+                        Config.NAMESPACE_NETWORK_SECURITY,
+                        Config.FLAG_VERSION,
+                        /* defaultValue= */ "");
         String newContentUrl =
                 DeviceConfig.getString(
-                        Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_CONTENT_URL, "");
+                        Config.NAMESPACE_NETWORK_SECURITY,
+                        Config.FLAG_CONTENT_URL,
+                        /* defaultValue= */ "");
         String newMetadataUrl =
                 DeviceConfig.getString(
-                        Config.NAMESPACE_NETWORK_SECURITY, Config.FLAG_METADATA_URL, "");
-        if (TextUtils.isEmpty(newVersion)
+                        Config.NAMESPACE_NETWORK_SECURITY,
+                        Config.FLAG_METADATA_URL,
+                        /* defaultValue= */ "");
+        if (TextUtils.isEmpty(newPublicKey)
+                || TextUtils.isEmpty(newVersion)
                 || TextUtils.isEmpty(newContentUrl)
                 || TextUtils.isEmpty(newMetadataUrl)) {
             return;
         }
 
         if (Config.DEBUG) {
+            Log.d(TAG, "newPublicKey=" + newPublicKey);
             Log.d(TAG, "newVersion=" + newVersion);
             Log.d(TAG, "newContentUrl=" + newContentUrl);
             Log.d(TAG, "newMetadataUrl=" + newMetadataUrl);
@@ -88,6 +103,13 @@
             return;
         }
 
+        try {
+            mCertificateTransparencyDownloader.setPublicKey(newPublicKey);
+        } catch (GeneralSecurityException e) {
+            Log.e(TAG, "Error setting the public Key", e);
+            return;
+        }
+
         // TODO: handle the case where there is already a pending download.
 
         mDataStore.setProperty(Config.VERSION_PENDING, newVersion);
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
index 2a6b8e2..611a5c7 100644
--- a/networksecurity/service/src/com/android/server/net/ct/Config.java
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -40,6 +40,7 @@
     static final String FLAG_CONTENT_URL = FLAGS_PREFIX + "content_url";
     static final String FLAG_METADATA_URL = FLAGS_PREFIX + "metadata_url";
     static final String FLAG_VERSION = FLAGS_PREFIX + "version";
+    static final String FLAG_PUBLIC_KEY = FLAGS_PREFIX + "public_key";
 
     // properties
     static final String VERSION_PENDING = "version_pending";
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index a056c35..1aad028 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -48,9 +48,10 @@
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
-import java.security.NoSuchAlgorithmException;
 import java.security.PrivateKey;
+import java.security.PublicKey;
 import java.security.Signature;
+import java.util.Base64;
 
 /** Tests for the {@link CertificateTransparencyDownloader}. */
 @RunWith(JUnit4.class)
@@ -60,18 +61,20 @@
     @Mock private CertificateTransparencyInstaller mCertificateTransparencyInstaller;
 
     private PrivateKey mPrivateKey;
+    private PublicKey mPublicKey;
     private Context mContext;
     private File mTempFile;
     private DataStore mDataStore;
     private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
 
     @Before
-    public void setUp() throws IOException, NoSuchAlgorithmException {
+    public void setUp() throws IOException, GeneralSecurityException {
         MockitoAnnotations.initMocks(this);
 
         KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
         KeyPair keyPair = instance.generateKeyPair();
         mPrivateKey = keyPair.getPrivate();
+        mPublicKey = keyPair.getPublic();
 
         mContext = InstrumentationRegistry.getInstrumentation().getContext();
         mTempFile = File.createTempFile("datastore-test", ".properties");
@@ -80,16 +83,13 @@
 
         mCertificateTransparencyDownloader =
                 new CertificateTransparencyDownloader(
-                        mContext,
-                        mDataStore,
-                        mDownloadHelper,
-                        mCertificateTransparencyInstaller,
-                        keyPair.getPublic().getEncoded());
+                        mContext, mDataStore, mDownloadHelper, mCertificateTransparencyInstaller);
     }
 
     @After
     public void tearDown() {
         mTempFile.delete();
+        mCertificateTransparencyDownloader.resetPublicKey();
     }
 
     @Test
@@ -155,6 +155,8 @@
         long metadataId = 123;
         File metadataFile = sign(logListFile);
         Uri metadataUri = Uri.fromFile(metadataFile);
+        mCertificateTransparencyDownloader.setPublicKey(
+                Base64.getEncoder().encodeToString(mPublicKey.getEncoded()));
 
         setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
         when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(true);
@@ -212,6 +214,28 @@
         assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
     }
 
+    @Test
+    public void testDownloader_handleContentCompleteMissingVerificationPublicKey()
+            throws Exception {
+        String version = "666";
+        long contentId = 666;
+        File logListFile = File.createTempFile("log_list", "json");
+        Uri contentUri = Uri.fromFile(logListFile);
+        long metadataId = 123;
+        File metadataFile = sign(logListFile);
+        Uri metadataUri = Uri.fromFile(metadataFile);
+
+        setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri);
+
+        mCertificateTransparencyDownloader.onReceive(
+                mContext, makeDownloadCompleteIntent(contentId));
+
+        verify(mCertificateTransparencyInstaller, never()).install(any(), eq(version));
+        assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
+        assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
+        assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
+    }
+
     private Intent makeDownloadCompleteIntent(long downloadId) {
         return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
                 .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
index cfeca5d..e52dd2f 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsQueryScheduler.java
@@ -107,7 +107,7 @@
         final QueryTaskConfig nextRunConfig = currentConfig.getConfigForNextRun(queryMode);
         long timeToRun;
         if (mLastScheduledQueryTaskArgs == null && !forceEnableBackoff) {
-            timeToRun = now + nextRunConfig.delayUntilNextTaskWithoutBackoffMs;
+            timeToRun = now + nextRunConfig.delayBeforeTaskWithoutBackoffMs;
         } else {
             timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
                     nextRunConfig, now, minRemainingTtl, lastSentTime, numOfQueriesBeforeBackoff,
@@ -133,7 +133,7 @@
     private static long calculateTimeToRun(@Nullable ScheduledQueryTaskArgs taskArgs,
             QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime,
             int numOfQueriesBeforeBackoff, boolean forceEnableBackoff) {
-        final long baseDelayInMs = queryTaskConfig.delayUntilNextTaskWithoutBackoffMs;
+        final long baseDelayInMs = queryTaskConfig.delayBeforeTaskWithoutBackoffMs;
         if (!(forceEnableBackoff
                 || queryTaskConfig.shouldUseQueryBackoff(numOfQueriesBeforeBackoff))) {
             return lastSentTime + baseDelayInMs;
diff --git a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
index d2cd463..4e74159 100644
--- a/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
+++ b/service-t/src/com/android/server/connectivity/mdns/QueryTaskConfig.java
@@ -55,22 +55,22 @@
     private final int queriesPerBurst;
     private final int timeBetweenBurstsInMs;
     private final int burstCounter;
-    final long delayUntilNextTaskWithoutBackoffMs;
+    final long delayBeforeTaskWithoutBackoffMs;
     private final boolean isFirstBurst;
-    private final long queryCount;
+    private final long queryIndex;
 
-    QueryTaskConfig(long queryCount, int transactionId,
+    QueryTaskConfig(long queryIndex, int transactionId,
             boolean expectUnicastResponse, boolean isFirstBurst, int burstCounter,
             int queriesPerBurst, int timeBetweenBurstsInMs,
-            long delayUntilNextTaskWithoutBackoffMs) {
+            long delayBeforeTaskWithoutBackoffMs) {
         this.transactionId = transactionId;
         this.expectUnicastResponse = expectUnicastResponse;
         this.queriesPerBurst = queriesPerBurst;
         this.timeBetweenBurstsInMs = timeBetweenBurstsInMs;
         this.burstCounter = burstCounter;
-        this.delayUntilNextTaskWithoutBackoffMs = delayUntilNextTaskWithoutBackoffMs;
+        this.delayBeforeTaskWithoutBackoffMs = delayBeforeTaskWithoutBackoffMs;
         this.isFirstBurst = isFirstBurst;
-        this.queryCount = queryCount;
+        this.queryIndex = queryIndex;
     }
 
     QueryTaskConfig(int queryMode) {
@@ -82,26 +82,26 @@
         // Config the scan frequency based on the scan mode.
         if (queryMode == AGGRESSIVE_QUERY_MODE) {
             this.timeBetweenBurstsInMs = INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS;
-            this.delayUntilNextTaskWithoutBackoffMs =
+            this.delayBeforeTaskWithoutBackoffMs =
                     TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS;
         } else if (queryMode == PASSIVE_QUERY_MODE) {
             // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then
             // in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
             // queries.
             this.timeBetweenBurstsInMs = MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
-            this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
+            this.delayBeforeTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
         } else {
             // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
             // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
             // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
             // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
             this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS;
-            this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
+            this.delayBeforeTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
         }
-        this.queryCount = 0;
+        this.queryIndex = 0;
     }
 
-    long getDelayUntilNextTaskWithoutBackoff(boolean isFirstQueryInBurst,
+    long getDelayBeforeNextTaskWithoutBackoff(boolean isFirstQueryInBurst,
             boolean isLastQueryInBurst, int queryMode) {
         if (isFirstQueryInBurst && queryMode == AGGRESSIVE_QUERY_MODE) {
             return 0;
@@ -137,7 +137,7 @@
      * Get new QueryTaskConfig for next run.
      */
     public QueryTaskConfig getConfigForNextRun(int queryMode) {
-        long newQueryCount = queryCount + 1;
+        long newQueryCount = queryIndex + 1;
         int newTransactionId = transactionId + 1;
         if (newTransactionId > UNSIGNED_SHORT_MAX_VALUE) {
             newTransactionId = 1;
@@ -162,7 +162,7 @@
                 getNextExpectUnicastResponse(isLastQueryInBurst, queryMode), newIsFirstBurst,
                 newBurstCounter, newQueriesPerBurst,
                 getNextTimeBetweenBurstsMs(isLastQueryInBurst, queryMode),
-                getDelayUntilNextTaskWithoutBackoff(
+                getDelayBeforeNextTaskWithoutBackoff(
                         isFirstQueryInBurst, isLastQueryInBurst, queryMode));
     }
 
@@ -174,6 +174,6 @@
         if (burstCounter != 0 || isFirstBurst) {
             return false;
         }
-        return queryCount > numOfQueriesBeforeBackoff;
+        return queryIndex > numOfQueriesBeforeBackoff;
     }
 }
diff --git a/staticlibs/tests/unit/host/python/apf_utils_test.py b/staticlibs/tests/unit/host/python/apf_utils_test.py
index 2885460..419b338 100644
--- a/staticlibs/tests/unit/host/python/apf_utils_test.py
+++ b/staticlibs/tests/unit/host/python/apf_utils_test.py
@@ -29,6 +29,10 @@
     get_ipv6_addresses,
     get_hardware_address,
     is_send_raw_packet_downstream_supported,
+    is_packet_capture_supported,
+    start_capture_packets,
+    stop_capture_packets,
+    get_matched_packet_counts,
     send_raw_packet_downstream,
 )
 from net_tests_utils.host.python.assert_utils import UnexpectedBehaviorError
@@ -208,6 +212,144 @@
         "Send raw packet should not be supported.",
     )
 
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_start_capture_success(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.return_value = "success"  # Successful command output
+      start_capture_packets(
+          self.mock_ad, TEST_IFACE_NAME
+      )
+      mock_adb_shell.assert_called_once_with(
+          self.mock_ad,
+          "cmd network_stack capture start"
+          f" {TEST_IFACE_NAME}"
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_start_capture_failure(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.return_value = (  # Unexpected command output
+          "Any Unexpected Output"
+      )
+      with asserts.assert_raises(UnexpectedBehaviorError):
+          start_capture_packets(
+              self.mock_ad, TEST_IFACE_NAME
+          )
+      asserts.assert_true(
+          is_packet_capture_supported(self.mock_ad),
+          "Start capturing packets should be supported.",
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_start_capture_unsupported(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.side_effect = AdbError(
+          cmd="", stdout="Unknown command", stderr="", ret_code=3
+      )
+      with asserts.assert_raises(UnsupportedOperationException):
+          start_capture_packets(
+              self.mock_ad, TEST_IFACE_NAME
+          )
+      asserts.assert_false(
+          is_packet_capture_supported(self.mock_ad),
+          "Start capturing packets should not be supported.",
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_stop_capture_success(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.return_value = "success"  # Successful command output
+      stop_capture_packets(
+          self.mock_ad, TEST_IFACE_NAME
+      )
+      mock_adb_shell.assert_called_once_with(
+          self.mock_ad,
+          "cmd network_stack capture stop"
+          f" {TEST_IFACE_NAME}"
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_stop_capture_failure(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.return_value = (  # Unexpected command output
+          "Any Unexpected Output"
+      )
+      with asserts.assert_raises(UnexpectedBehaviorError):
+          stop_capture_packets(
+              self.mock_ad, TEST_IFACE_NAME
+          )
+      asserts.assert_true(
+          is_packet_capture_supported(self.mock_ad),
+          "Stop capturing packets should be supported.",
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_stop_capture_unsupported(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.side_effect = AdbError(
+          cmd="", stdout="Unknown command", stderr="", ret_code=3
+      )
+      with asserts.assert_raises(UnsupportedOperationException):
+          stop_capture_packets(
+              self.mock_ad, TEST_IFACE_NAME
+          )
+      asserts.assert_false(
+          is_packet_capture_supported(self.mock_ad),
+          "Stop capturing packets should not be supported.",
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_get_matched_packet_counts_success(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.return_value = "10"  # Successful command output
+      get_matched_packet_counts(
+          self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+      )
+      mock_adb_shell.assert_called_once_with(
+          self.mock_ad,
+          "cmd network_stack capture matched-packet-counts"
+          f" {TEST_IFACE_NAME} {TEST_PACKET_IN_HEX}"
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_get_matched_packet_counts_failure(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.return_value = (  # Unexpected command output
+          "Any Unexpected Output"
+      )
+      with asserts.assert_raises(UnexpectedBehaviorError):
+          get_matched_packet_counts(
+              self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+          )
+      asserts.assert_true(
+          is_packet_capture_supported(self.mock_ad),
+          "Get matched packet counts should be supported.",
+      )
+
+  @patch("net_tests_utils.host.python.adb_utils.adb_shell")
+  def test_get_matched_packet_counts_unsupported(
+          self, mock_adb_shell: MagicMock
+  ) -> None:
+      mock_adb_shell.side_effect = AdbError(
+          cmd="", stdout="Unknown command", stderr="", ret_code=3
+      )
+      with asserts.assert_raises(UnsupportedOperationException):
+          get_matched_packet_counts(
+              self.mock_ad, TEST_IFACE_NAME, TEST_PACKET_IN_HEX
+          )
+      asserts.assert_false(
+          is_packet_capture_supported(self.mock_ad),
+          "Get matched packet counts should not be supported.",
+      )
+
   @parameterized.parameters(
       ("2,2048,1", ApfCapabilities(2, 2048, 1)),  # Valid input
       ("3,1024,0", ApfCapabilities(3, 1024, 0)),  # Valid input
diff --git a/staticlibs/testutils/host/python/apf_test_base.py b/staticlibs/testutils/host/python/apf_test_base.py
index 9a30978..2552aa3 100644
--- a/staticlibs/testutils/host/python/apf_test_base.py
+++ b/staticlibs/testutils/host/python/apf_test_base.py
@@ -15,7 +15,7 @@
 from mobly import asserts
 from net_tests_utils.host.python import adb_utils, apf_utils, assert_utils, multi_devices_test_base, tether_utils
 from net_tests_utils.host.python.tether_utils import UpstreamType
-
+import time
 
 class ApfTestBase(multi_devices_test_base.MultiDevicesTestBase):
 
@@ -39,6 +39,7 @@
     )
 
     # Fetch device properties and storing them locally for later use.
+    # TODO: refactor to separate instances to store client and server device
     self.server_iface_name, client_network = (
         tether_utils.setup_hotspot_and_client_for_upstream_type(
             self.serverDevice, self.clientDevice, UpstreamType.NONE
@@ -50,6 +51,21 @@
     self.server_mac_address = apf_utils.get_hardware_address(
         self.serverDevice, self.server_iface_name
     )
+    self.client_mac_address = apf_utils.get_hardware_address(
+        self.clientDevice, self.client_iface_name
+    )
+    self.server_ipv4_addresses = apf_utils.get_ipv4_addresses(
+        self.serverDevice, self.server_iface_name
+    )
+    self.client_ipv4_addresses = apf_utils.get_ipv4_addresses(
+        self.clientDevice, self.client_iface_name
+    )
+    self.server_ipv6_addresses = apf_utils.get_ipv6_addresses(
+        self.serverDevice, self.server_iface_name
+    )
+    self.client_ipv6_addresses = apf_utils.get_ipv6_addresses(
+        self.clientDevice, self.client_iface_name
+    )
 
     # Enable doze mode to activate APF.
     adb_utils.set_doze_mode(self.clientDevice, True)
@@ -81,4 +97,19 @@
         > count_before_test
     )
 
-    # TODO: Verify the packet is not actually received.
+  def send_packet_and_expect_reply_received(
+      self, send_packet: str, counter_name: str, receive_packet: str
+  ) -> None:
+    try:
+        apf_utils.start_capture_packets(self.serverDevice, self.server_iface_name)
+
+        self.send_packet_and_expect_counter_increased(send_packet, counter_name)
+
+        assert_utils.expect_with_retry(
+            lambda: apf_utils.get_matched_packet_counts(
+                self.serverDevice, self.server_iface_name, receive_packet
+            )
+            == 1
+        )
+    finally:
+        apf_utils.stop_capture_packets(self.serverDevice, self.server_iface_name)
diff --git a/staticlibs/testutils/host/python/apf_utils.py b/staticlibs/testutils/host/python/apf_utils.py
index e84ba3e..7fe60bd 100644
--- a/staticlibs/testutils/host/python/apf_utils.py
+++ b/staticlibs/testutils/host/python/apf_utils.py
@@ -178,6 +178,29 @@
         "Cannot get hardware address for " + iface_name
     )
 
+def is_packet_capture_supported(
+        ad: android_device.AndroidDevice,
+) -> bool:
+
+  # Invoke the shell command with empty argument and see how NetworkStack respond.
+  # If supported, an IllegalArgumentException with help page will be printed.
+  functions_with_args = (
+    # list all functions and args with (func, *args) tuple
+    (start_capture_packets, (ad, "")),
+    (stop_capture_packets, (ad, "")),
+    (get_matched_packet_counts, (ad, "", ""))
+  )
+
+  for func, args in functions_with_args:
+    try:
+      func(*args)
+    except UnsupportedOperationException:
+      return False
+    except Exception:
+      continue
+
+  # If no UnsupportOperationException is thrown, regard it as supported
+  return True
 
 def is_send_raw_packet_downstream_supported(
     ad: android_device.AndroidDevice,
@@ -224,25 +247,92 @@
         representation of a packet starting from L2 header.
   """
 
-  cmd = (
-      "cmd network_stack send-raw-packet-downstream"
-      f" {iface_name} {packet_in_hex}"
-  )
+  cmd = f"cmd network_stack send-raw-packet-downstream {iface_name} {packet_in_hex}"
 
   # Expect no output or Unknown command if NetworkStack is too old. Throw otherwise.
-  try:
-    output = adb_utils.adb_shell(ad, cmd)
-  except AdbError as e:
-    output = str(e.stdout)
-  if output:
-    if "Unknown command" in output:
-      raise UnsupportedOperationException(
-          "send-raw-packet-downstream command is not supported."
-      )
+  adb_output = AdbOutputHandler(ad, cmd).get_output()
+  if adb_output:
     raise assert_utils.UnexpectedBehaviorError(
-        f"Got unexpected output: {output} for command: {cmd}."
+      f"Got unexpected output: {adb_output} for command: {cmd}."
     )
 
+def start_capture_packets(
+        ad: android_device.AndroidDevice,
+        iface_name: str
+) -> None:
+  """Starts packet capturing on a specified network interface.
+
+  This function initiates packet capture on the given network interface of an
+  Android device using an ADB shell command. It handles potential errors
+  related to unsupported commands or unexpected output.
+  This command only supports downstream tethering interface.
+
+  Args:
+    ad: The Android device object.
+    iface_name: The name of the network interface (e.g., "wlan0").
+  """
+  cmd = f"cmd network_stack capture start {iface_name}"
+
+  # Expect no output or Unknown command if NetworkStack is too old. Throw otherwise.
+  adb_output = AdbOutputHandler(ad, cmd).get_output()
+  if adb_output != "success":
+    raise assert_utils.UnexpectedBehaviorError(
+      f"Got unexpected output: {adb_output} for command: {cmd}."
+    )
+
+def stop_capture_packets(
+        ad: android_device.AndroidDevice,
+        iface_name: str
+) -> None:
+  """Stops packet capturing on a specified network interface.
+
+  This function terminates packet capture on the given network interface of an
+  Android device using an ADB shell command. It handles potential errors
+  related to unsupported commands or unexpected output.
+
+  Args:
+    ad: The Android device object.
+    iface_name: The name of the network interface (e.g., "wlan0").
+  """
+  cmd = f"cmd network_stack capture stop {iface_name}"
+
+  # Expect no output or Unknown command if NetworkStack is too old. Throw otherwise.
+  adb_output = AdbOutputHandler(ad, cmd).get_output()
+  if adb_output != "success":
+    raise assert_utils.UnexpectedBehaviorError(
+      f"Got unexpected output: {adb_output} for command: {cmd}."
+    )
+
+def get_matched_packet_counts(
+        ad: android_device.AndroidDevice,
+        iface_name: str,
+        packet_in_hex: str
+) -> int:
+  """Gets the number of captured packets matching a specific hexadecimal pattern.
+
+  This function retrieves the count of captured packets on the specified
+  network interface that match a given hexadecimal pattern. It uses an ADB
+  shell command and handles potential errors related to unsupported commands,
+  unexpected output, or invalid output format.
+
+  Args:
+    ad: The Android device object.
+    iface_name: The name of the network interface (e.g., "wlan0").
+    packet_in_hex: The hexadecimal string representing the packet pattern.
+
+  Returns:
+    The number of matched packets as an integer.
+  """
+  cmd = f"cmd network_stack capture matched-packet-counts {iface_name} {packet_in_hex}"
+
+  # Expect no output or Unknown command if NetworkStack is too old. Throw otherwise.
+  adb_output = AdbOutputHandler(ad, cmd).get_output()
+  try:
+    return int(adb_output)
+  except ValueError as e:
+    raise assert_utils.UnexpectedBehaviorError(
+      f"Got unexpected exception: {e} for command: {cmd}."
+    )
 
 @dataclass
 class ApfCapabilities:
@@ -304,3 +394,19 @@
       f"Supported apf version {caps.apf_version_supported} < expected version"
       f" {expected_version}",
   )
+
+class AdbOutputHandler:
+  def __init__(self, ad, cmd):
+    self._ad = ad
+    self._cmd = cmd
+
+  def get_output(self) -> str:
+    try:
+      return adb_utils.adb_shell(self._ad, self._cmd)
+    except AdbError as e:
+      output = str(e.stdout)
+      if "Unknown command" in output:
+        raise UnsupportedOperationException(
+          f"{self._cmd} is not supported."
+        )
+      return output
\ No newline at end of file
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index b62db04..0e9ea0c 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -3061,6 +3061,7 @@
 
         try {
             final Network cellNetwork = networkCallbackRule.requestCell();
+            ensureCellIsValidatedBeforeMockingValidationUrls();
             final Network wifiNetwork = prepareValidatedNetwork();
 
             final TestableNetworkCallback defaultCb =
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index 1165018..83818be 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -94,14 +94,8 @@
     // Tag this module as a cts test artifact
     test_suites: [
         "cts",
-        "mts-dnsresolver",
-        "mts-networking",
         "mts-tethering",
-        "mts-wifi",
-        "mcts-dnsresolver",
-        "mcts-networking",
         "mcts-tethering",
-        "mcts-wifi",
         "general-tests",
     ],
 
diff --git a/thread/framework/java/android/net/thread/ThreadConfiguration.java b/thread/framework/java/android/net/thread/ThreadConfiguration.java
index 1c25535..e6fa1ef 100644
--- a/thread/framework/java/android/net/thread/ThreadConfiguration.java
+++ b/thread/framework/java/android/net/thread/ThreadConfiguration.java
@@ -126,18 +126,29 @@
      *
      * @hide
      */
+    @FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED)
+    @SystemApi
     public static final class Builder {
         private boolean mNat64Enabled = false;
         private boolean mDhcpv6PdEnabled = false;
 
-        /** Creates a new {@link Builder} object with all features disabled. */
+        /**
+         * Creates a new {@link Builder} object with all features disabled.
+         *
+         * @hide
+         */
+        @FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED)
+        @SystemApi
         public Builder() {}
 
         /**
          * Creates a new {@link Builder} object from a {@link ThreadConfiguration} object.
          *
          * @param config the Border Router configurations to be copied
+         * @hide
          */
+        @FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED)
+        @SystemApi
         public Builder(@NonNull ThreadConfiguration config) {
             Objects.requireNonNull(config);
 
@@ -150,7 +161,11 @@
          *
          * <p>Enabling this feature will allow Thread devices to connect to the internet/cloud over
          * IPv4.
+         *
+         * @hide
          */
+        @FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED)
+        @SystemApi
         @NonNull
         public Builder setNat64Enabled(boolean enabled) {
             this.mNat64Enabled = enabled;
@@ -162,6 +177,8 @@
          *
          * <p>Enabling this feature will allow Thread devices to connect to the internet/cloud over
          * IPv6.
+         *
+         * @hide
          */
         @NonNull
         public Builder setDhcpv6PdEnabled(boolean enabled) {
@@ -169,7 +186,13 @@
             return this;
         }
 
-        /** Creates a new {@link ThreadConfiguration} object. */
+        /**
+         * Creates a new {@link ThreadConfiguration} object.
+         *
+         * @hide
+         */
+        @FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED)
+        @SystemApi
         @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 1222398..6d20e13 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkController.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -368,7 +368,7 @@
          *     0-9 of user-input friendly length (typically 9), or {@code null} if {@code
          *     ephemeralKeyState} is {@link #EPHEMERAL_KEY_DISABLED} or the caller doesn't have the
          *     permission {@link android.permission.THREAD_NETWORK_PRIVILEGED}
-         * @param expiry a timestamp of when the ephemeral key will expireor {@code null} if {@code
+         * @param expiry a timestamp of when the ephemeral key will expire or {@code null} if {@code
          *     ephemeralKeyState} is {@link #EPHEMERAL_KEY_DISABLED}
          */
         @FlaggedApi(Flags.FLAG_EPSKC_ENABLED)
@@ -748,15 +748,19 @@
      * 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.
+     * receiver} will be invoked with a specific error:
+     *
+     * <ul>
+     *   <li>{@link ThreadNetworkException#ERROR_UNSUPPORTED_FEATURE} the configuration enables a
+     *       feature which is not supported by the platform.
+     * </ul>
      *
      * @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)
+    @FlaggedApi(Flags.FLAG_SET_NAT64_CONFIGURATION_ENABLED)
+    @RequiresPermission(permission.THREAD_NETWORK_PRIVILEGED)
     public void setConfiguration(
             @NonNull ThreadConfiguration configuration,
             @NonNull @CallbackExecutor Executor executor,
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index fb541e6..99fa7c9 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -220,7 +220,6 @@
     private boolean mUserRestricted;
     private boolean mForceStopOtDaemonEnabled;
 
-    private OtDaemonConfiguration mOtDaemonConfig;
     private InfraLinkState mInfraLinkState;
 
     @VisibleForTesting
@@ -249,7 +248,6 @@
         // TODO: networkToLinkProperties should be shared with NsdPublisher, add a test/assert to
         // verify they are the same.
         mNetworkToLinkProperties = networkToLinkProperties;
-        mOtDaemonConfig = new OtDaemonConfiguration.Builder().build();
         mInfraLinkState = new InfraLinkState.Builder().build();
         mPersistentSettings = persistentSettings;
         mNsdPublisher = nsdPublisher;
@@ -346,6 +344,7 @@
         otDaemon.initialize(
                 mTunIfController.getTunFd(),
                 shouldEnableThread(),
+                newOtDaemonConfig(mPersistentSettings.getConfiguration()),
                 mNsdPublisher,
                 getMeshcopTxtAttributes(mResources.get()),
                 mOtDaemonCallbackProxy,
@@ -556,22 +555,21 @@
     public void setConfiguration(
             @NonNull ThreadConfiguration configuration, @NonNull IOperationReceiver receiver) {
         enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
-        mHandler.post(() -> setConfigurationInternal(configuration, receiver));
+        mHandler.post(
+                () ->
+                        setConfigurationInternal(
+                                configuration, new OperationReceiverWrapper(receiver)));
     }
 
     private void setConfigurationInternal(
             @NonNull ThreadConfiguration configuration,
-            @NonNull IOperationReceiver operationReceiver) {
+            @NonNull OperationReceiverWrapper receiver) {
         checkOnHandlerThread();
 
         LOG.i("Set Thread configuration: " + configuration);
 
         final boolean changed = mPersistentSettings.putConfiguration(configuration);
-        try {
-            operationReceiver.onSuccess();
-        } catch (RemoteException e) {
-            // do nothing if the client is dead
-        }
+        receiver.onSuccess();
         if (changed) {
             for (IConfigurationReceiver configReceiver : mConfigurationReceivers.keySet()) {
                 try {
@@ -581,7 +579,22 @@
                 }
             }
         }
-        // TODO: set the configuration at ot-daemon
+        try {
+            getOtDaemon()
+                    .setConfiguration(
+                            newOtDaemonConfig(configuration),
+                            new LoggingOtStatusReceiver("setConfiguration"));
+        } catch (RemoteException | ThreadNetworkException e) {
+            LOG.e("otDaemon.setConfiguration failed. Config: " + configuration, e);
+        }
+    }
+
+    private static OtDaemonConfiguration newOtDaemonConfig(
+            @NonNull ThreadConfiguration threadConfig) {
+        return new OtDaemonConfiguration.Builder()
+                .setNat64Enabled(threadConfig.isNat64Enabled())
+                .setDhcpv6PdEnabled(threadConfig.isDhcpv6PdEnabled())
+                .build();
     }
 
     @Override
@@ -1467,11 +1480,6 @@
         return builder.build();
     }
 
-    private static OtDaemonConfiguration.Builder newOtDaemonConfigBuilder(
-            OtDaemonConfiguration config) {
-        return new OtDaemonConfiguration.Builder();
-    }
-
     private static InfraLinkState.Builder newInfraLinkStateBuilder() {
         return new InfraLinkState.Builder().setInterfaceName("");
     }
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 1792bfb..07438e9 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -182,6 +182,7 @@
     @After
     public void tearDown() throws Exception {
         dropAllPermissions();
+        setEnabledAndWait(mController, true);
         leaveAndWait(mController);
         tearDownTestNetwork();
         setConfigurationAndWait(mController, DEFAULT_CONFIG);
@@ -1148,15 +1149,9 @@
         CompletableFuture<Void> setFuture2 = new CompletableFuture<>();
         ConfigurationListener listener = new ConfigurationListener(mController);
         ThreadConfiguration config1 =
-                new ThreadConfiguration.Builder()
-                        .setNat64Enabled(true)
-                        .setDhcpv6PdEnabled(true)
-                        .build();
+                new ThreadConfiguration.Builder().setNat64Enabled(true).build();
         ThreadConfiguration config2 =
-                new ThreadConfiguration.Builder()
-                        .setNat64Enabled(false)
-                        .setDhcpv6PdEnabled(true)
-                        .build();
+                new ThreadConfiguration.Builder().setNat64Enabled(false).build();
 
         try {
             runAsShell(
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 b97e2b7..7ac404f 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -741,10 +741,7 @@
                         .setDhcpv6PdEnabled(false)
                         .build();
         ThreadConfiguration config2 =
-                new ThreadConfiguration.Builder()
-                        .setNat64Enabled(true)
-                        .setDhcpv6PdEnabled(true)
-                        .build();
+                new ThreadConfiguration.Builder().setNat64Enabled(true).build();
         ThreadConfiguration config3 =
                 new ThreadConfiguration.Builder(config2).build(); // Same as config2