Snap for 11051817 from d023c314fa5f6a2b14c75c9c3e741449cda83470 to mainline-appsearch-release

Change-Id: I55d8edee87e22f8c8718fdcb2d069331598403e6
diff --git a/Android.bp b/Android.bp
index ebe5217..61b4a1e 100644
--- a/Android.bp
+++ b/Android.bp
@@ -193,7 +193,6 @@
         "framework-connectivity",
         "framework-connectivity-t.stubs.module_lib",
         "framework-tethering",
-        "android.net.ipsec.ike.stubs.module_lib",
     ],
     sdk_version: "module_33",
     visibility: ["//visibility:private"],
@@ -215,7 +214,6 @@
         "framework-connectivity",
         "framework-connectivity-t.stubs.module_lib",
         "framework-tethering",
-        "android.net.ipsec.ike.stubs.module_lib"
     ],
     sdk_version: module_34_version,
     visibility: ["//visibility:private"],
@@ -339,7 +337,10 @@
         "src/**/*.java",
         ":statslog-networkstack-java-gen-current"
     ],
-    static_libs: ["NetworkStackApiCurrentShims"],
+    static_libs: [
+        "NetworkStackApiCurrentShims",
+        "net-utils-device-common-struct",
+    ],
     manifest: "AndroidManifestBase.xml",
     visibility: [
         "//frameworks/base/tests/net/integration",
@@ -357,7 +358,10 @@
         "src/**/*.java",
         ":statslog-networkstack-java-gen-stable",
     ],
-    static_libs: ["NetworkStackApiStableShims"],
+    static_libs: [
+        "NetworkStackApiStableShims",
+        "net-utils-device-common-struct",
+    ],
     manifest: "AndroidManifestBase.xml",
     visibility: [
         "//frameworks/base/packages/Connectivity/tests/integration",
diff --git a/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileBuilderShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileBuilderShimImpl.java
deleted file mode 100644
index 2597840..0000000
--- a/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileBuilderShimImpl.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2023 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.networkstack.apishim.api30;
-
-import android.net.Ikev2VpnProfile;
-import android.net.ProxyInfo;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-
-import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileShim;
-import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
-
-import java.security.PrivateKey;
-import java.security.cert.X509Certificate;
-import java.util.List;
-
-/**
- * Implementation of Ikev2VpnProfileBuilderShim for API 30.
- */
-@RequiresApi(Build.VERSION_CODES.R)
-public class Ikev2VpnProfileBuilderShimImpl
-        implements Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> {
-    protected final Ikev2VpnProfile.Builder mBuilder;
-
-    protected Ikev2VpnProfileBuilderShimImpl(@NonNull Ikev2VpnProfile.Builder builder) {
-        mBuilder = builder;
-    }
-
-    protected Ikev2VpnProfileBuilderShimImpl(@NonNull String serverAddr,
-            @NonNull String identity) {
-        mBuilder = new Ikev2VpnProfile.Builder(serverAddr, identity);
-    }
-
-    /**
-     * Returns a new instance of this shim impl.
-     */
-    public static Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> newInstance(
-            @NonNull String serverAddr, @NonNull String identity) {
-        return new Ikev2VpnProfileBuilderShimImpl(serverAddr, identity);
-    }
-
-    @Override
-    public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setAuthPsk(@NonNull byte[] psk) {
-        mBuilder.setAuthPsk(psk);
-        return this;
-    }
-
-    @Override
-    public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setAuthUsernamePassword(
-            @NonNull String user, @NonNull String pass, @Nullable X509Certificate serverRootCa)
-            throws UnsupportedApiLevelException {
-        mBuilder.setAuthUsernamePassword(user, pass, serverRootCa);
-        return this;
-    }
-
-    @Override
-    public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setAuthDigitalSignature(
-            @NonNull X509Certificate userCert, @NonNull PrivateKey key,
-            @Nullable X509Certificate serverRootCa) {
-        mBuilder.setAuthDigitalSignature(userCert, key, serverRootCa);
-        return this;
-    }
-
-    @Override
-    public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setBypassable(boolean isBypassable) {
-        mBuilder.setBypassable(true);
-        return this;
-    }
-
-    @Override
-    public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setProxy(@Nullable ProxyInfo proxy) {
-        mBuilder.setProxy(proxy);
-        return this;
-    }
-
-    @Override
-    public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setMaxMtu(int mtu) {
-        mBuilder.setMaxMtu(mtu);
-        return this;
-    }
-
-    @Override
-    public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setMetered(boolean isMetered) {
-        mBuilder.setMetered(isMetered);
-        return this;
-    }
-
-    @Override
-    public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setAllowedAlgorithms(
-            @NonNull List<String> algorithmNames) {
-        mBuilder.setAllowedAlgorithms(algorithmNames);
-        return this;
-    }
-
-    @Override
-    public Ikev2VpnProfile.Builder getBuilder() {
-        return mBuilder;
-    }
-
-    @Override
-    public Ikev2VpnProfileShim build() {
-        return Ikev2VpnProfileShimImpl.newInstance(mBuilder.build());
-    }
-}
diff --git a/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileShimImpl.java
deleted file mode 100644
index 54a2a35..0000000
--- a/apishim/30/com/android/networkstack/apishim/api30/Ikev2VpnProfileShimImpl.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2023 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.networkstack.apishim.api30;
-
-import android.net.Ikev2VpnProfile;
-
-import androidx.annotation.NonNull;
-
-import com.android.networkstack.apishim.common.Ikev2VpnProfileShim;
-/**
- * Implementation of Ikev2VpnProfileShim for API 30.
- */
-// TODO : when API29 is no longer supported, remove the type argument
-public class Ikev2VpnProfileShimImpl implements Ikev2VpnProfileShim<Ikev2VpnProfile> {
-    protected final Ikev2VpnProfile mProfile;
-
-    protected Ikev2VpnProfileShimImpl(Ikev2VpnProfile profile) {
-        mProfile = profile;
-    }
-
-    /**
-     * Returns a new instance of this shim impl.
-     */
-    public static Ikev2VpnProfileShim<Ikev2VpnProfile> newInstance(
-            @NonNull Ikev2VpnProfile profile) {
-        return new Ikev2VpnProfileShimImpl(profile);
-    }
-
-    public Ikev2VpnProfile getProfile() {
-        return mProfile;
-    }
-}
diff --git a/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileBuilderShimImpl.java b/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileBuilderShimImpl.java
deleted file mode 100644
index 308407b..0000000
--- a/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileBuilderShimImpl.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2023 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.networkstack.apishim.api33;
-
-import android.net.Ikev2VpnProfile;
-import android.net.ipsec.ike.IkeTunnelConnectionParams;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-import com.android.modules.utils.build.SdkLevel;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim;
-import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
-
-/**
- * A shim for Ikev2VpnProfile.Builder
- */
-@RequiresApi(Build.VERSION_CODES.TIRAMISU)
-public class Ikev2VpnProfileBuilderShimImpl
-        extends com.android.networkstack.apishim.api30.Ikev2VpnProfileBuilderShimImpl {
-    protected Ikev2VpnProfileBuilderShimImpl(@NonNull IkeTunnelConnectionParams params) {
-        super(new Ikev2VpnProfile.Builder(params));
-    }
-
-    protected Ikev2VpnProfileBuilderShimImpl(@NonNull String serverAddr,
-            @NonNull String identity) {
-        super(serverAddr, identity);
-    }
-
-    /**
-     * Returns a new instance of this shim impl.
-     */
-    @RequiresApi(Build.VERSION_CODES.R)
-    public static Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> newInstance(
-            @NonNull String serverAddr, @NonNull String identity) {
-        if (SdkLevel.isAtLeastT()) {
-            return new Ikev2VpnProfileBuilderShimImpl(serverAddr, identity);
-        }
-        return com.android.networkstack.apishim.api30.Ikev2VpnProfileBuilderShimImpl
-                .newInstance(serverAddr, identity);
-    }
-
-    /**
-     * Returns a new instance of this shim impl.
-     */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public static Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> newInstance(
-            @NonNull IkeTunnelConnectionParams params) throws UnsupportedApiLevelException {
-        if (SdkLevel.isAtLeastT()) {
-            return new Ikev2VpnProfileBuilderShimImpl(params);
-        } else {
-            throw new UnsupportedApiLevelException("Only supported from API 33");
-        }
-    }
-
-    /**
-     * @see Ikev2VpnProfile.Builder#setRequiresInternetValidation(boolean)
-     */
-    @Override
-    public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setRequiresInternetValidation(
-            boolean requiresInternetValidation) {
-        mBuilder.setRequiresInternetValidation(requiresInternetValidation);
-        return this;
-    }
-
-    /**
-     * @see Ikev2VpnProfile.Builder#setLocalRoutesExcluded(boolean)
-     */
-    @Override
-    public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> setLocalRoutesExcluded(
-            boolean excludeLocalRoutes) {
-        mBuilder.setLocalRoutesExcluded(excludeLocalRoutes);
-        return this;
-    }
-}
diff --git a/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileShimImpl.java b/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileShimImpl.java
deleted file mode 100644
index 8e82218..0000000
--- a/apishim/33/com/android/networkstack/apishim/api33/Ikev2VpnProfileShimImpl.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2023 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.networkstack.apishim.api33;
-
-import android.net.Ikev2VpnProfile;
-
-/**
- * A shim for Ikev2VpnProfile
- */
-public class Ikev2VpnProfileShimImpl
-        extends com.android.networkstack.apishim.api30.Ikev2VpnProfileShimImpl {
-    protected Ikev2VpnProfileShimImpl(Ikev2VpnProfile profile) {
-        super(profile);
-    }
-}
diff --git a/apishim/34/com/android/networkstack/apishim/api34/Ikev2VpnProfileBuilderShimImpl.java b/apishim/34/com/android/networkstack/apishim/api34/Ikev2VpnProfileBuilderShimImpl.java
deleted file mode 100644
index ac74028..0000000
--- a/apishim/34/com/android/networkstack/apishim/api34/Ikev2VpnProfileBuilderShimImpl.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2023 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.networkstack.apishim.api34;
-
-import android.net.Ikev2VpnProfile;
-import android.net.ipsec.ike.IkeTunnelConnectionParams;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-import com.android.modules.utils.build.SdkLevel;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileBuilderShim;
-import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
-
-/**
- * A shim for Ikev2VpnProfile.Builder
- */
-@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-public class Ikev2VpnProfileBuilderShimImpl
-        extends com.android.networkstack.apishim.api33.Ikev2VpnProfileBuilderShimImpl {
-
-    protected Ikev2VpnProfileBuilderShimImpl(@NonNull IkeTunnelConnectionParams params) {
-        super(params);
-    }
-
-    protected Ikev2VpnProfileBuilderShimImpl(@NonNull String serverAddr,
-            @NonNull String identity) {
-        super(serverAddr, identity);
-    }
-
-    /**
-     * Returns a new instance of this shim impl.
-     */
-    @RequiresApi(Build.VERSION_CODES.R)
-    public static Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> newInstance(
-            @NonNull String serverAddr, @NonNull String identity) {
-        if (SdkLevel.isAtLeastU()) {
-            return new Ikev2VpnProfileBuilderShimImpl(serverAddr, identity);
-        }
-        return com.android.networkstack.apishim.api33.Ikev2VpnProfileBuilderShimImpl
-                .newInstance(serverAddr, identity);
-    }
-
-    /**
-     * Returns a new instance of this shim impl.
-     */
-    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public static Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder> newInstance(
-            @NonNull IkeTunnelConnectionParams params) throws UnsupportedApiLevelException {
-        if (SdkLevel.isAtLeastU()) {
-            return new Ikev2VpnProfileBuilderShimImpl(params);
-        } else {
-            return com.android.networkstack.apishim.api33.Ikev2VpnProfileBuilderShimImpl
-                    .newInstance(params);
-        }
-    }
-
-    /**
-     * @see Ikev2VpnProfile.Builder#setAutomaticIpVersionSelectionEnabled(boolean)
-     */
-    @Override
-    public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder>
-            setAutomaticIpVersionSelectionEnabled(boolean isEnabled) {
-        mBuilder.setAutomaticIpVersionSelectionEnabled(isEnabled);
-        return this;
-    }
-
-    /**
-     * @see Ikev2VpnProfile.Builder#setAutomaticNattKeepaliveTimerEnabled(boolean)
-     */
-    @Override
-    public Ikev2VpnProfileBuilderShim<Ikev2VpnProfile.Builder>
-            setAutomaticNattKeepaliveTimerEnabled(boolean isEnabled) {
-        mBuilder.setAutomaticNattKeepaliveTimerEnabled(isEnabled);
-        return this;
-    }
-}
diff --git a/apishim/34/com/android/networkstack/apishim/api34/Ikev2VpnProfileShimImpl.java b/apishim/34/com/android/networkstack/apishim/api34/Ikev2VpnProfileShimImpl.java
deleted file mode 100644
index 848bbd1..0000000
--- a/apishim/34/com/android/networkstack/apishim/api34/Ikev2VpnProfileShimImpl.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2023 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.networkstack.apishim.api34;
-
-import android.net.Ikev2VpnProfile;
-import android.os.Build;
-
-import androidx.annotation.RequiresApi;
-
-import com.android.modules.utils.build.SdkLevel;
-import com.android.networkstack.apishim.common.Ikev2VpnProfileShim;
-
-/**
- * A shim for Ikev2VpnProfile
- */
-@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-public class Ikev2VpnProfileShimImpl
-        extends com.android.networkstack.apishim.api33.Ikev2VpnProfileShimImpl {
-    protected Ikev2VpnProfileShimImpl(Ikev2VpnProfile profile) {
-        super(profile);
-    }
-    /**
-     * Returns a new instance of this shim impl.
-     */
-    @RequiresApi(Build.VERSION_CODES.R)
-    public static Ikev2VpnProfileShim<Ikev2VpnProfile> newInstance(Ikev2VpnProfile profile) {
-        if (SdkLevel.isAtLeastU()) {
-            return new Ikev2VpnProfileShimImpl(profile);
-        } else {
-            return com.android.networkstack.apishim.api33.Ikev2VpnProfileShimImpl
-                    .newInstance(profile);
-        }
-    }
-
-    /**
-     * @see Ikev2VpnProfile#isAutomaticIpVersionSelectionEnabled()
-     */
-    public boolean isAutomaticIpVersionSelectionEnabled() {
-        return mProfile.isAutomaticIpVersionSelectionEnabled();
-    }
-
-    /**
-     * @see Ikev2VpnProfile#isAutomaticNattKeepaliveTimerEnabled()
-     */
-    public boolean isAutomaticNattKeepaliveTimerEnabled() {
-        return mProfile.isAutomaticNattKeepaliveTimerEnabled();
-    }
-}
diff --git a/apishim/35/com/android/networkstack/apishim/Ikev2VpnProfileBuilderShimImpl.java b/apishim/35/com/android/networkstack/apishim/Ikev2VpnProfileBuilderShimImpl.java
deleted file mode 100644
index 486063f..0000000
--- a/apishim/35/com/android/networkstack/apishim/Ikev2VpnProfileBuilderShimImpl.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2023 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.networkstack.apishim;
-
-import android.net.ipsec.ike.IkeTunnelConnectionParams;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-
-/**
- * A shim for Ikev2VpnProfile.Builder
- */
-// TODO: when available in all active branches: @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-@RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
-public class Ikev2VpnProfileBuilderShimImpl
-        extends com.android.networkstack.apishim.api34.Ikev2VpnProfileBuilderShimImpl {
-    // Currently identical to the API 34 shim, so inherit everything
-    protected Ikev2VpnProfileBuilderShimImpl(@NonNull IkeTunnelConnectionParams params) {
-        super(params);
-    }
-}
diff --git a/apishim/35/com/android/networkstack/apishim/Ikev2VpnProfileShimImpl.java b/apishim/35/com/android/networkstack/apishim/Ikev2VpnProfileShimImpl.java
deleted file mode 100644
index bc40e16..0000000
--- a/apishim/35/com/android/networkstack/apishim/Ikev2VpnProfileShimImpl.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2023 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.networkstack.apishim;
-
-import android.net.Ikev2VpnProfile;
-import android.os.Build;
-
-import androidx.annotation.RequiresApi;
-
-/**
- * A shim for Ikev2VpnProfile
- */
-// TODO: when available in all active branches: @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-@RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
-public class Ikev2VpnProfileShimImpl
-        extends com.android.networkstack.apishim.api34.Ikev2VpnProfileShimImpl {
-    // Currently identical to the API 34 shim, so inherit everything
-    protected Ikev2VpnProfileShimImpl(Ikev2VpnProfile profile) {
-        super(profile);
-    }
-}
diff --git a/apishim/common/com/android/networkstack/apishim/common/Ikev2VpnProfileBuilderShim.java b/apishim/common/com/android/networkstack/apishim/common/Ikev2VpnProfileBuilderShim.java
deleted file mode 100644
index 4232885..0000000
--- a/apishim/common/com/android/networkstack/apishim/common/Ikev2VpnProfileBuilderShim.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright (C) 2023 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.networkstack.apishim.common;
-
-import android.net.ProxyInfo;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.security.PrivateKey;
-import java.security.cert.X509Certificate;
-import java.util.List;
-
-/**
- * A shim for Ikev2VpnProfile.Builder.
- *
- * T should extend Ikev2VpnProfile.Builder, but this can't be written here as that class is not
- * available in API29.
- * @param <T> type of builder, typically Ikev2VpnProfile.Builder
- */
-// TODO : when API29 is no longer supported, remove the type argument
-public interface Ikev2VpnProfileBuilderShim<T> {
-    /**
-     * @see Ikev2VpnProfile.Builder#setRequiresInternetValidation(boolean)
-     */
-    default Ikev2VpnProfileBuilderShim<T> setRequiresInternetValidation(
-            boolean requiresInternetValidation) throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API 33");
-    }
-
-    /**
-     * @see Ikev2VpnProfile.Builder#setAuthPsk(byte[])
-     */
-    default Ikev2VpnProfileBuilderShim<T> setAuthPsk(@NonNull byte[] psk)
-            throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API 30");
-    }
-
-    /**
-     * @see Ikev2VpnProfile.Builder#setAuthUsernamePassword(String, String, X509Certificate)
-     */
-    default Ikev2VpnProfileBuilderShim<T> setAuthUsernamePassword(@NonNull String user,
-            @NonNull String pass, @Nullable X509Certificate serverRootCa)
-            throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API 30");
-    }
-
-    /**
-     * @see Ikev2VpnProfile.Builder#setAuthDigitalSignature(X509Certificate, PrivateKey,
-     *      X509Certificate)
-     */
-    default Ikev2VpnProfileBuilderShim<T> setAuthDigitalSignature(@NonNull X509Certificate userCert,
-            @NonNull PrivateKey key, @Nullable X509Certificate serverRootCa)
-            throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API 30");
-    }
-
-    /**
-     * @see Ikev2VpnProfile.Builder#setBypassable(boolean)
-     */
-    default Ikev2VpnProfileBuilderShim<T> setBypassable(boolean isBypassable)
-            throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API 30");
-    }
-
-    /**
-     * @see Ikev2VpnProfile.Builder#setProxy(ProxyInfo)
-     */
-    default Ikev2VpnProfileBuilderShim<T> setProxy(@Nullable ProxyInfo proxy)
-            throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API 30");
-    }
-
-    /**
-     * @see Ikev2VpnProfile.Builder#setMaxMtu(int)
-     */
-    default Ikev2VpnProfileBuilderShim<T> setMaxMtu(int mtu) throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API 30");
-    }
-
-    /**
-     * @see Ikev2VpnProfile.Builder#setMetered(boolean)
-     */
-    default Ikev2VpnProfileBuilderShim<T> setMetered(boolean isMetered)
-            throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API 30");
-    }
-
-    /**
-     * @see Ikev2VpnProfile.Builder#setAllowedAlgorithms(List<String>)
-     */
-    default Ikev2VpnProfileBuilderShim<T> setAllowedAlgorithms(@NonNull List<String> algorithmNames)
-            throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API 30");
-    }
-
-    /**
-     * @see Ikev2VpnProfile.Builder#setLocalRoutesExcluded(boolean)
-     */
-    default Ikev2VpnProfileBuilderShim<T> setLocalRoutesExcluded(boolean excludeLocalRoutes)
-            throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API 33");
-    }
-
-    /**
-     * @see Ikev2VpnProfile.Builder#setAutomaticIpVersionSelectionEnabled(boolean)
-     */
-    default Ikev2VpnProfileBuilderShim<T> setAutomaticIpVersionSelectionEnabled(boolean isEnabled)
-            throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API 34");
-    }
-
-    /**
-     * @see Ikev2VpnProfile.Builder#setAutomaticNattKeepaliveTimerEnabled(boolean)
-     */
-    default Ikev2VpnProfileBuilderShim<T> setAutomaticNattKeepaliveTimerEnabled(boolean isEnabled)
-            throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API 34");
-    }
-
-    /**
-     * Get <T> type of builder, typically Ikev2VpnProfile.Builder
-     */
-    default T getBuilder() throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API 30");
-    }
-
-    /**
-     * Build an Ikev2VpnProfileShim
-     */
-    default Ikev2VpnProfileShim build() throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API 30");
-    }
-}
diff --git a/apishim/common/com/android/networkstack/apishim/common/Ikev2VpnProfileShim.java b/apishim/common/com/android/networkstack/apishim/common/Ikev2VpnProfileShim.java
deleted file mode 100644
index ad0bdcd..0000000
--- a/apishim/common/com/android/networkstack/apishim/common/Ikev2VpnProfileShim.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2023 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.networkstack.apishim.common;
-
-/**
- * A shim for Ikev2VpnProfile.
- *
- * T should extend Ikev2VpnProfile, but this can't be written here as that class is not
- * available in API29.
- * @param <T> type of profile, typically Ikev2VpnProfile
- */
-// TODO : when API29 is no longer supported, remove the type argument
-public interface Ikev2VpnProfileShim<T> {
-    /**
-     * @see Ikev2VpnProfile#isAutomaticNattKeepaliveTimerEnabled()
-     */
-    default boolean isAutomaticNattKeepaliveTimerEnabled() throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API level 34.");
-    }
-
-    /**
-     * @see Ikev2VpnProfile#isAutomaticIpVersionSelectionEnabled()
-     */
-    default boolean isAutomaticIpVersionSelectionEnabled() throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API level 34.");
-    }
-    /**
-     * Return the <T> type of profile.
-     * TODO: remove when Q is no longer supported.
-     */
-    default T getProfile() throws UnsupportedApiLevelException {
-        throw new UnsupportedApiLevelException("Only supported from API level 30.");
-    }
-}
diff --git a/jni/network_stack_utils_jni.cpp b/jni/network_stack_utils_jni.cpp
index b5d97be..6f47d7e 100644
--- a/jni/network_stack_utils_jni.cpp
+++ b/jni/network_stack_utils_jni.cpp
@@ -42,15 +42,6 @@
 constexpr const char NETWORKSTACKUTILS_PKG_NAME[] =
     "com/android/networkstack/util/NetworkStackUtils";
 
-static const uint32_t kEtherTypeOffset = offsetof(ether_header, ether_type);
-static const uint32_t kEtherHeaderLen = sizeof(ether_header);
-static const uint32_t kIPv4Protocol = kEtherHeaderLen + offsetof(iphdr, protocol);
-static const uint32_t kIPv4FlagsOffset = kEtherHeaderLen + offsetof(iphdr, frag_off);
-static const uint32_t kIPv6NextHeader = kEtherHeaderLen + offsetof(ip6_hdr, ip6_nxt);
-static const uint32_t kIPv6PayloadStart = kEtherHeaderLen + sizeof(ip6_hdr);
-static const uint32_t kICMPv6TypeOffset = kIPv6PayloadStart + offsetof(icmp6_hdr, icmp6_type);
-static const uint32_t kUDPSrcPortIndirectOffset = kEtherHeaderLen + offsetof(udphdr, source);
-static const uint32_t kUDPDstPortIndirectOffset = kEtherHeaderLen + offsetof(udphdr, dest);
 static const uint16_t kDhcpClientPort = 68;
 
 static bool checkLenAndCopy(JNIEnv* env, const jbyteArray& addr, int len, void* dst) {
@@ -100,6 +91,8 @@
     }
 }
 
+// fd is a "socket(AF_PACKET, SOCK_RAW, ETH_P_IP)"
+// which guarantees packets already have skb->protocol == htons(ETH_P_IP)
 static void network_stack_utils_attachDhcpFilter(JNIEnv *env, jclass clazz, jobject javaFd) {
     static sock_filter filter_code[] = {
         // Check the protocol is UDP.
@@ -108,14 +101,13 @@
 
         // Check this is not a fragment.
         BPF_LOAD_IPV4_BE16(frag_off),
-        BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K,   IP_MF | IP_OFFMASK, 0, 1),
-        BPF_REJECT,
+        BPF2_REJECT_IF_ANY_MASKED_BITS_SET(IP_MF | IP_OFFMASK),
 
         // Get the IP header length.
-        BPF_STMT(BPF_LDX | BPF_B    | BPF_MSH, kEtherHeaderLen),
+        BPF_LOADX_NET_RELATIVE_IPV4_HLEN,
 
         // Check the destination port.
-        BPF_STMT(BPF_LD  | BPF_H    | BPF_IND, kUDPDstPortIndirectOffset),
+        BPF_LOAD_NETX_RELATIVE_DST_PORT,
         BPF2_REJECT_IF_NOT_EQUAL(kDhcpClientPort),
 
         BPF_ACCEPT,
@@ -131,14 +123,18 @@
     }
 }
 
+// fd is a "socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6)"
+// which guarantees packets already have skb->protocol == htons(ETH_P_IPV6)
 static void network_stack_utils_attachRaFilter(JNIEnv *env, jclass clazz, jobject javaFd) {
     static sock_filter filter_code[] = {
+        BPF_LOADX_CONSTANT_IPV6_HLEN,
+
         // Check IPv6 Next Header is ICMPv6.
         BPF_LOAD_IPV6_U8(nexthdr),
         BPF2_REJECT_IF_NOT_EQUAL(IPPROTO_ICMPV6),
 
         // Check ICMPv6 type is Router Advertisement.
-        BPF_LOAD_NET_RELATIVE_U8(sizeof(ipv6hdr) + offsetof(icmp6_hdr, icmp6_type)),
+        BPF_LOAD_NETX_RELATIVE_ICMP_TYPE,
         BPF2_REJECT_IF_NOT_EQUAL(ND_ROUTER_ADVERT),
 
         BPF_ACCEPT,
@@ -150,20 +146,14 @@
 
     int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
     if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
-        jniThrowExceptionFmt(env, "java/net/SocketException",
-                "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno));
+        jniThrowErrnoException(env, "setsockopt(SO_ATTACH_FILTER)", errno);
     }
 }
 
 // TODO: Move all this filter code into libnetutils.
+// fd is a "socket(AF_PACKET, SOCK_RAW, ETH_P_ALL)"
 static void network_stack_utils_attachControlPacketFilter(
-        JNIEnv *env, jclass clazz, jobject javaFd, jint hardwareAddressType) {
-    if (hardwareAddressType != ARPHRD_ETHER) {
-        jniThrowExceptionFmt(env, "java/net/SocketException",
-                "attachControlPacketFilter only supports ARPHRD_ETHER");
-        return;
-    }
-
+        JNIEnv *env, jclass clazz, jobject javaFd) {
     // Capture all:
     //     - ARPs
     //     - DHCPv4 packets
@@ -175,48 +165,50 @@
     //     '(ip and udp port 68)' or
     //     '(icmp6 and ip6[40] >= 133 and ip6[40] <= 136)'
     static sock_filter filter_code[] = {
-        // Load the link layer next payload field.
-        BPF_STMT(BPF_LD  | BPF_H    | BPF_ABS,  kEtherTypeOffset),
+        // Load the ethertype from skb->protocol
+        BPF_LOAD_SKB_PROTOCOL,
 
         // Accept all ARP.
         // TODO: Figure out how to better filter ARPs on noisy networks.
-        BPF_JUMP(BPF_JMP | BPF_JEQ  | BPF_K,   ETHERTYPE_ARP, 16, 0),
+        BPF2_ACCEPT_IF_EQUAL(ETHERTYPE_ARP),
 
-        // If IPv4:
-        BPF_JUMP(BPF_JMP | BPF_JEQ  | BPF_K,   ETHERTYPE_IP, 0, 9),
+        // If IPv4:  (otherwise jump to the 'IPv6 ...' below)
+        BPF_JUMP_IF_NOT_EQUAL(ETHERTYPE_IP, 14),
 
         // Check the protocol is UDP.
-        BPF_STMT(BPF_LD  | BPF_B    | BPF_ABS, kIPv4Protocol),
-        BPF_JUMP(BPF_JMP | BPF_JEQ  | BPF_K,   IPPROTO_UDP, 0, 14),
+        BPF_LOAD_IPV4_U8(protocol),
+        BPF2_REJECT_IF_NOT_EQUAL(IPPROTO_UDP),
 
         // Check this is not a fragment.
-        BPF_STMT(BPF_LD  | BPF_H    | BPF_ABS, kIPv4FlagsOffset),
-        BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K,   IP_OFFMASK, 12, 0),
+        BPF_LOAD_IPV4_BE16(frag_off),
+        BPF2_REJECT_IF_ANY_MASKED_BITS_SET(IP_MF | IP_OFFMASK),
 
         // Get the IP header length.
-        BPF_STMT(BPF_LDX | BPF_B    | BPF_MSH, kEtherHeaderLen),
+        BPF_LOADX_NET_RELATIVE_IPV4_HLEN,
 
         // Check the source port.
-        BPF_STMT(BPF_LD  | BPF_H    | BPF_IND, kUDPSrcPortIndirectOffset),
-        BPF_JUMP(BPF_JMP | BPF_JEQ  | BPF_K,   kDhcpClientPort, 8, 0),
+        BPF_LOAD_NETX_RELATIVE_SRC_PORT,
+        BPF2_ACCEPT_IF_EQUAL(kDhcpClientPort),
 
         // Check the destination port.
-        BPF_STMT(BPF_LD  | BPF_H    | BPF_IND, kUDPDstPortIndirectOffset),
-        BPF_JUMP(BPF_JMP | BPF_JEQ  | BPF_K,   kDhcpClientPort, 6, 7),
+        BPF_LOAD_NETX_RELATIVE_DST_PORT,
+        BPF2_ACCEPT_IF_EQUAL(kDhcpClientPort),
+
+        // Reject any other UDPv4
+        BPF_REJECT,
 
         // IPv6 ...
-        BPF_JUMP(BPF_JMP | BPF_JEQ  | BPF_K,   ETHERTYPE_IPV6, 0, 6),
+        BPF2_REJECT_IF_NOT_EQUAL(ETHERTYPE_IPV6),
+        // Assume standard, 40-byte, extension header-less ipv6 packet
+        BPF_LOADX_CONSTANT_IPV6_HLEN,
         // ... check IPv6 Next Header is ICMPv6 (ignore fragments), ...
-        BPF_STMT(BPF_LD  | BPF_B    | BPF_ABS, kIPv6NextHeader),
-        BPF_JUMP(BPF_JMP | BPF_JEQ  | BPF_K,   IPPROTO_ICMPV6, 0, 4),
+        BPF_LOAD_IPV6_U8(nexthdr),
+        BPF2_REJECT_IF_NOT_EQUAL(IPPROTO_ICMPV6),
         // ... and check the ICMPv6 type is one of RS/RA/NS/NA.
-        BPF_STMT(BPF_LD  | BPF_B    | BPF_ABS, kICMPv6TypeOffset),
-        BPF_JUMP(BPF_JMP | BPF_JGE  | BPF_K,   ND_ROUTER_SOLICIT, 0, 2),
-        BPF_JUMP(BPF_JMP | BPF_JGT  | BPF_K,   ND_NEIGHBOR_ADVERT, 1, 0),
+        BPF_LOAD_NETX_RELATIVE_ICMP_TYPE,
+        BPF3_REJECT_IF_NOT_IN_RANGE(ND_ROUTER_SOLICIT, ND_NEIGHBOR_ADVERT),
 
         BPF_ACCEPT,
-
-        BPF_REJECT,
     };
     static const sock_fprog filter = {
         sizeof(filter_code) / sizeof(filter_code[0]),
@@ -225,8 +217,7 @@
 
     int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
     if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
-        jniThrowExceptionFmt(env, "java/net/SocketException",
-                "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno));
+        jniThrowErrnoException(env, "setsockopt(SO_ATTACH_FILTER)", errno);
     }
 }
 
@@ -238,7 +229,7 @@
     { "addArpEntry", "([B[BLjava/lang/String;Ljava/io/FileDescriptor;)V", (void*) network_stack_utils_addArpEntry },
     { "attachDhcpFilter", "(Ljava/io/FileDescriptor;)V", (void*) network_stack_utils_attachDhcpFilter },
     { "attachRaFilter", "(Ljava/io/FileDescriptor;)V", (void*) network_stack_utils_attachRaFilter },
-    { "attachControlPacketFilter", "(Ljava/io/FileDescriptor;I)V", (void*) network_stack_utils_attachControlPacketFilter },
+    { "attachControlPacketFilter", "(Ljava/io/FileDescriptor;)V", (void*) network_stack_utils_attachControlPacketFilter },
 };
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index a590ee6..f254610 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -21,6 +21,6 @@
     <string name="notification_channel_name_network_venue_info" msgid="6526543187249265733">"Informacionet për vendin e rrjetit"</string>
     <string name="notification_channel_description_network_venue_info" msgid="5131499595382733605">"Njoftimet e shfaqura për të treguar se rrjeti ka një faqe me informacione për vendin"</string>
     <string name="connected" msgid="4563643884927480998">"U lidh"</string>
-    <string name="tap_for_info" msgid="6849746325626883711">"Lidhur / Trokit për të parë faqen e internetit"</string>
+    <string name="tap_for_info" msgid="6849746325626883711">"Lidhur / Trokit për të parë uebsajtin"</string>
     <string name="application_label" msgid="1322847171305285454">"Menaxheri i rrjetit"</string>
 </resources>
diff --git a/src/android/net/apf/AndroidPacketFilter.java b/src/android/net/apf/AndroidPacketFilter.java
new file mode 100644
index 0000000..18c704e
--- /dev/null
+++ b/src/android/net/apf/AndroidPacketFilter.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2023 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.apf;
+
+import android.net.LinkProperties;
+import android.net.NattKeepalivePacketDataParcelable;
+import android.net.TcpKeepalivePacketDataParcelable;
+
+import com.android.internal.util.IndentingPrintWriter;
+
+/**
+ * The interface for AndroidPacketFilter
+ */
+public interface AndroidPacketFilter {
+    /**
+     * Update the LinkProperties that will be used by APF.
+     */
+    void setLinkProperties(LinkProperties lp);
+
+    /**
+     * Shutdown the APF.
+     */
+    void shutdown();
+
+    /**
+     * Switch for the multicast filter.
+     * @param isEnabled if  the multicast filter should be enabled or not.
+     */
+    void setMulticastFilter(boolean isEnabled);
+
+    /**
+     * Set the APF data snapshot.
+     */
+    void setDataSnapshot(byte[] data);
+
+    /**
+     * Add TCP keepalive ack packet filter.
+     * This will add a filter to drop acks to the keepalive packet passed as an argument.
+     *
+     * @param slot The index used to access the filter.
+     * @param sentKeepalivePacket The attributes of the sent keepalive packet.
+     */
+    void addTcpKeepalivePacketFilter(int slot,
+            TcpKeepalivePacketDataParcelable sentKeepalivePacket);
+
+    /**
+     * Add NAT-T keepalive packet filter.
+     * This will add a filter to drop NAT-T keepalive packet which is passed as an argument.
+     *
+     * @param slot The index used to access the filter.
+     * @param sentKeepalivePacket The attributes of the sent keepalive packet.
+     */
+    void addNattKeepalivePacketFilter(int slot,
+            NattKeepalivePacketDataParcelable sentKeepalivePacket);
+
+    /**
+     * Remove keepalive packet filter.
+     *
+     * @param slot The index used to access the filter.
+     */
+    void removeKeepalivePacketFilter(int slot);
+
+    /**
+     * Dump the status of APF.
+     */
+    void dump(IndentingPrintWriter pw);
+}
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
index c70ac73..41c6726 100644
--- a/src/android/net/apf/ApfFilter.java
+++ b/src/android/net/apf/ApfFilter.java
@@ -25,6 +25,7 @@
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_CLOEXEC;
 import static android.system.OsConstants.SOCK_RAW;
 
 import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
@@ -45,10 +46,6 @@
 import android.net.apf.ApfGenerator.IllegalInstructionException;
 import android.net.apf.ApfGenerator.Register;
 import android.net.ip.IpClient.IpClientCallbacksWrapper;
-import android.net.metrics.ApfProgramEvent;
-import android.net.metrics.ApfStats;
-import android.net.metrics.IpConnectivityLog;
-import android.net.metrics.RaEvent;
 import android.os.PowerManager;
 import android.os.SystemClock;
 import android.system.ErrnoException;
@@ -63,6 +60,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.HexDump;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.TokenBucket;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.ConnectivityUtils;
 import com.android.net.module.util.InterfaceParams;
@@ -104,7 +102,7 @@
  *
  * @hide
  */
-public class ApfFilter {
+public class ApfFilter implements AndroidPacketFilter {
 
     // Helper class for specifying functional filter parameters.
     public static class ApfConfiguration {
@@ -113,16 +111,7 @@
         public boolean ieee802_3Filter;
         public int[] ethTypeBlackList;
         public int minRdnssLifetimeSec;
-    }
-
-    // Enums describing the outcome of receiving an RA packet.
-    private static enum ProcessRaResult {
-        MATCH,          // Received RA matched a known RA
-        DROPPED,        // Received RA ignored due to MAX_RAS
-        PARSE_ERROR,    // Received RA could not be parsed
-        ZERO_LIFETIME,  // Received RA had 0 lifetime
-        UPDATE_NEW_RA,  // APF program updated for new RA
-        UPDATE_EXPIRY   // APF program updated for expiry
+        public int acceptRaMinLft;
     }
 
     /**
@@ -200,14 +189,6 @@
     public class ReceiveThread extends Thread {
         private final byte[] mPacket = new byte[1514];
         private final FileDescriptor mSocket;
-        private final long mStart = SystemClock.elapsedRealtime();
-
-        private int mReceivedRas = 0;
-        private int mMatchingRas = 0;
-        private int mDroppedRas = 0;
-        private int mParseErrors = 0;
-        private int mZeroLifetimeRas = 0;
-        private int mProgramUpdates = 0;
 
         private volatile boolean mStopped;
 
@@ -227,59 +208,13 @@
             while (!mStopped) {
                 try {
                     int length = Os.read(mSocket, mPacket, 0, mPacket.length);
-                    updateStats(processRa(mPacket, length));
+                    processRa(mPacket, length);
                 } catch (IOException|ErrnoException e) {
                     if (!mStopped) {
                         Log.e(TAG, "Read error", e);
                     }
                 }
             }
-            logStats();
-        }
-
-        private void updateStats(ProcessRaResult result) {
-            mReceivedRas++;
-            switch(result) {
-                case MATCH:
-                    mMatchingRas++;
-                    return;
-                case DROPPED:
-                    mDroppedRas++;
-                    return;
-                case PARSE_ERROR:
-                    mParseErrors++;
-                    return;
-                case ZERO_LIFETIME:
-                    mZeroLifetimeRas++;
-                    return;
-                case UPDATE_EXPIRY:
-                    mMatchingRas++;
-                    mProgramUpdates++;
-                    return;
-                case UPDATE_NEW_RA:
-                    mProgramUpdates++;
-                    return;
-            }
-        }
-
-        private void logStats() {
-            final long nowMs = SystemClock.elapsedRealtime();
-            synchronized (this) {
-                final ApfStats stats = new ApfStats.Builder()
-                        .setReceivedRas(mReceivedRas)
-                        .setMatchingRas(mMatchingRas)
-                        .setDroppedRas(mDroppedRas)
-                        .setParseErrors(mParseErrors)
-                        .setZeroLifetimeRas(mZeroLifetimeRas)
-                        .setProgramUpdates(mProgramUpdates)
-                        .setDurationMs(nowMs - mStart)
-                        .setMaxProgramSize(mApfCapabilities.maximumApfProgramSize)
-                        .setProgramUpdatesAll(mNumProgramUpdates)
-                        .setProgramUpdatesAllowingMulticast(mNumProgramUpdatesAllowingMulticast)
-                        .build();
-                mMetricsLog.log(stats);
-                logApfProgramEventLocked(nowMs / DateUtils.SECOND_IN_MILLIS);
-            }
         }
     }
 
@@ -344,8 +279,6 @@
     private static final short ARP_OPCODE_REPLY = 2;
     private static final int ARP_SOURCE_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 14;
     private static final int ARP_TARGET_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 24;
-    // Do not log ApfProgramEvents whose actual lifetimes was less than this.
-    private static final int APF_PROGRAM_EVENT_LIFETIME_THRESHOLD = 2;
     // Limit on the Black List size to cap on program usage for this
     // TODO: Select a proper max length
     private static final int APF_MAX_ETH_TYPE_BLACK_LIST_LEN = 20;
@@ -369,7 +302,7 @@
     private final ApfCapabilities mApfCapabilities;
     private final IpClientCallbacksWrapper mIpClientCallback;
     private final InterfaceParams mInterfaceParams;
-    private final IpConnectivityLog mMetricsLog;
+    private final TokenBucket mTokenBucket;
 
     @VisibleForTesting
     public byte[] mHardwareAddress;
@@ -387,6 +320,10 @@
     // Ignore non-zero RDNSS lifetimes below this value.
     private final int mMinRdnssLifetimeSec;
 
+    // Tracks the value of /proc/sys/ipv6/conf/$iface/accept_ra_min_lft which affects router, RIO,
+    // and PIO valid lifetimes.
+    private final int mAcceptRaMinLft;
+
     // Detects doze mode state transitions.
     private final BroadcastReceiver mDeviceIdleReceiver = new BroadcastReceiver() {
         @Override
@@ -411,13 +348,14 @@
 
     @VisibleForTesting
     public ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams,
-            IpClientCallbacksWrapper ipClientCallback, IpConnectivityLog log) {
+            IpClientCallbacksWrapper ipClientCallback) {
         mApfCapabilities = config.apfCapabilities;
         mIpClientCallback = ipClientCallback;
         mInterfaceParams = ifParams;
         mMulticastFilter = config.multicastFilter;
         mDrop802_3Frames = config.ieee802_3Filter;
         mMinRdnssLifetimeSec = config.minRdnssLifetimeSec;
+        mAcceptRaMinLft = config.acceptRaMinLft;
         mContext = context;
 
         if (mApfCapabilities.hasDataAccess()) {
@@ -433,7 +371,14 @@
         // Now fill the black list from the passed array
         mEthTypeBlackList = filterEthTypeBlackList(config.ethTypeBlackList);
 
-        mMetricsLog = log;
+        // TokenBucket for rate limiting filter installation. APF filtering relies on the filter
+        // always being up-to-date and APF bytecode being in sync with userspace. The TokenBucket
+        // merely prevents illconfigured / abusive networks from impacting the system, so it does
+        // not need to be very restrictive.
+        // The TokenBucket starts with its full capacity of 20 tokens (= 20 filter updates). A new
+        // token is generated every 3 seconds limiting the filter update rate to at most once every
+        // 3 seconds.
+        mTokenBucket = new TokenBucket(3_000 /* deltaMs */, 20 /* capacity */, 20 /* tokens */);
 
         // TODO: ApfFilter should not generate programs until IpClient sends provisioning success.
         maybeStartFilter();
@@ -506,10 +451,10 @@
                 // Install basic filters
                 installNewProgramLocked();
             }
-            socket = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6);
+            socket = Os.socket(AF_PACKET, SOCK_RAW | SOCK_CLOEXEC, 0);
+            NetworkStackUtils.attachRaFilter(socket);
             SocketAddress addr = makePacketSocketAddress(ETH_P_IPV6, mInterfaceParams.index);
             Os.bind(socket, addr);
-            NetworkStackUtils.attachRaFilter(socket);
         } catch(SocketException|ErrnoException e) {
             Log.e(TAG, "Error starting filter", e);
             return;
@@ -520,8 +465,8 @@
 
     // Returns seconds since device boot.
     @VisibleForTesting
-    protected long currentTimeSeconds() {
-        return SystemClock.elapsedRealtime() / DateUtils.SECOND_IN_MILLIS;
+    protected int secondsSinceBoot() {
+        return (int) (SystemClock.elapsedRealtime() / DateUtils.SECOND_IN_MILLIS);
     }
 
     public static class InvalidRaException extends Exception {
@@ -536,8 +481,7 @@
     private static class PacketSection {
         public enum Type {
             MATCH,     // A field that should be matched (e.g., the router IP address).
-            IGNORE,    // An ignored field such as the checksum of the flow label. Not matched.
-            LIFETIME,  // A lifetime. Not matched, and generally counts toward minimum RA lifetime.
+            LIFETIME,  // A lifetime. Not matched, and counts toward minimum RA lifetime if >= min.
         }
 
         /** The type of section. */
@@ -546,22 +490,35 @@
         public final int start;
         /** Length of this section in bytes. */
         public final int length;
-        /** If this is a lifetime, the ICMP option that defined it. 0 for router lifetime. */
-        public final int option;
         /** If this is a lifetime, the lifetime value. */
         public final long lifetime;
+        /** If this is a lifetime, the value below which the lifetime is ignored */
+        public final int min;
 
-        PacketSection(int start, int length, Type type, int option, long lifetime) {
+        PacketSection(int start, int length, Type type, long lifetime, int min) {
             this.start = start;
+
+            if (type == Type.LIFETIME && length != 2 && length != 4) {
+                throw new IllegalArgumentException("LIFETIME section length must be 2 or 4 bytes");
+            }
             this.length = length;
             this.type = type;
-            this.option = option;
+
+            if (type == Type.MATCH && (lifetime != 0 || min != 0)) {
+                throw new IllegalArgumentException("lifetime, min must be 0 for MATCH sections");
+            }
             this.lifetime = lifetime;
+
+            // It has already been asserted that min is 0 for MATCH sections.
+            if (min < 0) {
+                throw new IllegalArgumentException("min must be >= 0 for LIFETIME sections");
+            }
+            this.min = min;
         }
 
         public String toString() {
             if (type == Type.LIFETIME) {
-                return String.format("%s: (%d, %d) %d %d", type, start, length, option, lifetime);
+                return String.format("%s: (%d, %d) %d %d", type, start, length, lifetime, min);
             } else {
                 return String.format("%s: (%d, %d)", type, start, length);
             }
@@ -589,10 +546,18 @@
         private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET = 8;
         private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN = 4;
 
+        // From RFC4861: source link-layer address
+        private static final int ICMP6_SOURCE_LL_ADDRESS_OPTION_TYPE = 1;
+        // From RFC4861: mtu size option
+        private static final int ICMP6_MTU_OPTION_TYPE = 5;
         // From RFC6106: Recursive DNS Server option
         private static final int ICMP6_RDNSS_OPTION_TYPE = 25;
         // From RFC6106: DNS Search List option
         private static final int ICMP6_DNSSL_OPTION_TYPE = 31;
+        // From RFC8910: Captive-Portal option
+        private static final int ICMP6_CAPTIVE_PORTAL_OPTION_TYPE = 37;
+        // From RFC8781: PREF64 option
+        private static final int ICMP6_PREF64_OPTION_TYPE = 38;
 
         // From RFC4191: Route Information option
         private static final int ICMP6_ROUTE_INFO_OPTION_TYPE = 24;
@@ -607,9 +572,9 @@
         private final ArrayList<PacketSection> mPacketSections = new ArrayList<>();
 
         // Minimum lifetime in packet
-        long mMinLifetime;
+        private final int mMinLifetime;
         // When the packet was last captured, in seconds since Unix Epoch
-        long mLastSeen;
+        private final int mLastSeen;
 
         // For debugging only. Offsets into the packet where PIOs are.
         private final ArrayList<Integer> mPrefixOptionOffsets = new ArrayList<>();
@@ -620,9 +585,6 @@
         // For debugging only. Offsets into the packet where RIO options are.
         private final ArrayList<Integer> mRioOptionOffsets = new ArrayList<>();
 
-        // For debugging only. How many times this RA was seen.
-        int seenCount = 0;
-
         // For debugging only. Returns the hex representation of the last matching packet.
         String getLastMatchingPacket() {
             return HexDump.toHexString(mPacket.array(), 0, mPacket.capacity(),
@@ -728,9 +690,25 @@
             // check to prevent doing so in the presence of bugs or malformed or
             // truncated packets.
             if (length == 0) return;
-            mPacketSections.add(
-                    new PacketSection(mPacket.position(), length, PacketSection.Type.MATCH, 0, 0));
-            mPacket.position(mPacket.position() + length);
+
+            // we need to add a MATCH section 'from, length, MATCH, 0, 0'
+            int from = mPacket.position();
+
+            // if possible try to increase the length of the previous match section
+            int lastIdx = mPacketSections.size() - 1;
+            if (lastIdx >= 0) {  // there had to be a previous section
+                PacketSection prev = mPacketSections.get(lastIdx);
+                if (prev.type == PacketSection.Type.MATCH) {  // of type match
+                    if (prev.start + prev.length == from) {  // ending where we start
+                        from -= prev.length;
+                        length += prev.length;
+                        mPacketSections.remove(lastIdx);
+                    }
+                }
+            }
+
+            mPacketSections.add(new PacketSection(from, length, PacketSection.Type.MATCH, 0, 0));
+            mPacket.position(from + length);
         }
 
         /**
@@ -746,51 +724,36 @@
          * @param length the length of the section in bytes
          */
         private void addIgnoreSection(int length) {
-            mPacketSections.add(
-                    new PacketSection(mPacket.position(), length, PacketSection.Type.IGNORE, 0, 0));
             mPacket.position(mPacket.position() + length);
         }
 
         /**
          * Add a packet section that represents a lifetime, starting from the current position.
          * @param length the length of the section in bytes
-         * @param optionType the RA option containing this lifetime, or 0 for router lifetime
          * @param lifetime the lifetime
+         * @param min the minimum acceptable lifetime
          */
-        private void addLifetimeSection(int length, int optionType, long lifetime) {
+        private void addLifetimeSection(int length, long lifetime, int min) {
             mPacketSections.add(
                     new PacketSection(mPacket.position(), length, PacketSection.Type.LIFETIME,
-                            optionType, lifetime));
+                            lifetime, min));
             mPacket.position(mPacket.position() + length);
         }
 
         /**
          * Adds packet sections for an RA option with a 4-byte lifetime 4 bytes into the option
-         * @param optionType the RA option that is being added
          * @param optionLength the length of the option in bytes
+         * @param min the minimum acceptable lifetime
          */
-        private long add4ByteLifetimeOption(int optionType, int optionLength) {
+        private long add4ByteLifetimeOption(int optionLength, int min) {
             addMatchSection(ICMP6_4_BYTE_LIFETIME_OFFSET);
             final long lifetime = getUint32(mPacket, mPacket.position());
-            addLifetimeSection(ICMP6_4_BYTE_LIFETIME_LEN, optionType, lifetime);
+            addLifetimeSection(ICMP6_4_BYTE_LIFETIME_LEN, lifetime, min);
             addMatchSection(optionLength - ICMP6_4_BYTE_LIFETIME_OFFSET
                     - ICMP6_4_BYTE_LIFETIME_LEN);
             return lifetime;
         }
 
-        // http://b/66928272 http://b/65056012
-        // DnsServerRepository ignores RDNSS servers with lifetimes that are too low. Ignore these
-        // lifetimes for the purpose of filter lifetime calculations.
-        private boolean shouldIgnoreLifetime(int optionType, long lifetime) {
-            return optionType == ICMP6_RDNSS_OPTION_TYPE
-                    && lifetime != 0 && lifetime < mMinRdnssLifetimeSec;
-        }
-
-        private boolean isRelevantLifetime(PacketSection section) {
-            return section.type == PacketSection.Type.LIFETIME
-                    && !shouldIgnoreLifetime(section.option, section.lifetime);
-        }
-
         // Note that this parses RA and may throw InvalidRaException (from
         // Buffer.position(int) or due to an invalid-length option) or IndexOutOfBoundsException
         // (from ByteBuffer.get(int) ) if parsing encounters something non-compliant with
@@ -802,7 +765,7 @@
             }
 
             mPacket = ByteBuffer.wrap(Arrays.copyOf(packet, length));
-            mLastSeen = currentTimeSeconds();
+            mLastSeen = secondsSinceBoot();
 
             // Check packet in case a packet arrives before we attach RA filter
             // to our packet socket. b/29586253
@@ -812,13 +775,14 @@
                 throw new InvalidRaException("Not an ICMP6 router advertisement");
             }
 
-
-            RaEvent.Builder builder = new RaEvent.Builder();
-
             // Ignore the flow label and low 4 bits of traffic class.
             addMatchUntil(IPV6_FLOW_LABEL_OFFSET);
             addIgnoreSection(IPV6_FLOW_LABEL_LEN);
 
+            // Ignore IPv6 destination address.
+            addMatchUntil(IPV6_DEST_ADDR_OFFSET);
+            addIgnoreSection(IPV6_ADDR_LEN);
+
             // Ignore checksum.
             addMatchUntil(ICMP6_RA_CHECKSUM_OFFSET);
             addIgnoreSection(ICMP6_RA_CHECKSUM_LEN);
@@ -826,8 +790,7 @@
             // Parse router lifetime
             addMatchUntil(ICMP6_RA_ROUTER_LIFETIME_OFFSET);
             final long routerLifetime = getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET);
-            addLifetimeSection(ICMP6_RA_ROUTER_LIFETIME_LEN, 0, routerLifetime);
-            builder.updateRouterLifetime(routerLifetime);
+            addLifetimeSection(ICMP6_RA_ROUTER_LIFETIME_LEN, routerLifetime, mAcceptRaMinLft);
 
             // Add remaining fields (reachable time and retransmission timer) to match section.
             addMatchUntil(ICMP6_RA_OPTION_OFFSET);
@@ -850,14 +813,14 @@
                         addMatchSection(ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET);
                         lifetime = getUint32(mPacket, mPacket.position());
                         addLifetimeSection(ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN,
-                                ICMP6_PREFIX_OPTION_TYPE, lifetime);
-                        builder.updatePrefixValidLifetime(lifetime);
+                                lifetime, mAcceptRaMinLft);
 
                         // Parse preferred lifetime
                         lifetime = getUint32(mPacket, mPacket.position());
+                        // The PIO preferred lifetime is not affected by accept_ra_min_lft and
+                        // therefore does not have a minimum.
                         addLifetimeSection(ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN,
-                                ICMP6_PREFIX_OPTION_TYPE, lifetime);
-                        builder.updatePrefixPreferredLifetime(lifetime);
+                                lifetime, 0 /* min lifetime */);
 
                         addMatchSection(4);       // Reserved bytes
                         addMatchSection(IPV6_ADDR_LEN);  // The prefix itself
@@ -866,18 +829,19 @@
                     // are processed with the same specialized add4ByteLifetimeOption:
                     case ICMP6_RDNSS_OPTION_TYPE:
                         mRdnssOptionOffsets.add(position);
-                        lifetime = add4ByteLifetimeOption(optionType, optionLength);
-                        builder.updateRdnssLifetime(lifetime);
+                        lifetime = add4ByteLifetimeOption(optionLength, mMinRdnssLifetimeSec);
                         break;
                     case ICMP6_ROUTE_INFO_OPTION_TYPE:
                         mRioOptionOffsets.add(position);
-                        lifetime = add4ByteLifetimeOption(optionType, optionLength);
-                        builder.updateRouteInfoLifetime(lifetime);
+                        lifetime = add4ByteLifetimeOption(optionLength, mAcceptRaMinLft);
                         break;
-                    case ICMP6_DNSSL_OPTION_TYPE:
-                        lifetime = add4ByteLifetimeOption(optionType, optionLength);
-                        builder.updateDnsslLifetime(lifetime);
+                    case ICMP6_SOURCE_LL_ADDRESS_OPTION_TYPE:
+                    case ICMP6_MTU_OPTION_TYPE:
+                    case ICMP6_PREF64_OPTION_TYPE:
+                        addMatchSection(optionLength);
                         break;
+                    case ICMP6_CAPTIVE_PORTAL_OPTION_TYPE: // unlikely to ever change.
+                    case ICMP6_DNSSL_OPTION_TYPE: // currently unsupported in userspace.
                     default:
                         // RFC4861 section 4.2 dictates we ignore unknown options for forwards
                         // compatibility.
@@ -889,73 +853,161 @@
                 }
             }
             mMinLifetime = minLifetime();
-            mMetricsLog.log(builder.build());
+        }
+
+        public enum MatchType {
+            NO_MATCH, // the RAs do not match
+            MATCH_PASS, // the RAS match, and the APF program would pass.
+            MATCH_DROP, // the RAs match, but the APF program would drop.
         }
 
         // Considering only the MATCH sections, does {@code packet} match this RA?
-        boolean matches(byte[] packet, int length) {
-            if (length != mPacket.capacity()) return false;
-            byte[] referencePacket = mPacket.array();
+        MatchType matches(Ra newRa) {
+            // Does their size match?
+            if (newRa.mPacket.capacity() != mPacket.capacity()) return MatchType.NO_MATCH;
+
+            // If the filter has expired, it cannot match the new RA.
+            if (getRemainingFilterLft(secondsSinceBoot()) <= 0) return MatchType.NO_MATCH;
+
+            // Check if all MATCH sections are byte-identical.
+            final byte[] newPacket = newRa.mPacket.array();
+            final byte[] oldPacket = mPacket.array();
             for (PacketSection section : mPacketSections) {
                 if (section.type != PacketSection.Type.MATCH) continue;
                 for (int i = section.start; i < (section.start + section.length); i++) {
-                    if (packet[i] != referencePacket[i]) return false;
+                    if (newPacket[i] != oldPacket[i]) return MatchType.NO_MATCH;
                 }
             }
-            return true;
+
+            // Apply APF lifetime matching to LIFETIME sections and decide whether a packet should
+            // be processed (MATCH_PASS) or ignored (MATCH_DROP). This logic is needed to
+            // consistently process / ignore packets no matter the current state of the APF program.
+            // Note that userspace has no control (or knowledge) over when the APF program is
+            // running.
+            for (PacketSection section : mPacketSections) {
+                if (section.type != PacketSection.Type.LIFETIME) continue;
+
+                // the lifetime of the new RA.
+                long lft = 0;
+                switch (section.length) {
+                    // section.length is guaranteed to be 2 or 4.
+                    case 2: lft = getUint16(newRa.mPacket, section.start); break;
+                    case 4: lft = getUint32(newRa.mPacket, section.start); break;
+                }
+
+                // WARNING: keep this in sync with Ra#generateFilterLocked()!
+                if (section.lifetime == 0) {
+                    // Case 1) old lft == 0
+                    if (section.min > 0) {
+                        // a) in the presence of a min value.
+                        // if lft >= min -> PASS
+                        // gen.addJumpIfR0GreaterThan(section.min - 1, nextFilterLabel);
+                        if (lft >= section.min) return MatchType.MATCH_PASS;
+                    } else {
+                        // b) if min is 0 / there is no min value.
+                        // if lft > 0 -> PASS
+                        // gen.addJumpIfR0GreaterThan(0, nextFilterLabel);
+                        if (lft > 0) return MatchType.MATCH_PASS;
+                    }
+                } else if (section.min == 0) {
+                    // Case 2b) section is not affected by any minimum.
+                    //
+                    // if lft < (oldLft + 2) // 3 -> PASS
+                    // if lft > oldLft            -> PASS
+                    // gen.addJumpIfR0LessThan((int) ((section.lifetime + 2) / 3),
+                    //        nextFilterLabel);
+                    if (lft < (section.lifetime + 2) / 3) return MatchType.MATCH_PASS;
+                    // gen.addJumpIfR0GreaterThan((int) section.lifetime, nextFilterLabel);
+                    if (lft > section.lifetime) return MatchType.MATCH_PASS;
+                } else if (section.lifetime < section.min) {
+                    // Case 2a) 0 < old lft < min
+                    //
+                    // if lft == 0   -> PASS
+                    // if lft >= min -> PASS
+                    // gen.addJumpIfR0Equals(0, nextFilterLabel);
+                    if (lft == 0) return MatchType.MATCH_PASS;
+                    // gen.addJumpIfR0GreaterThan(section.min - 1, nextFilterLabel);
+                    if (lft >= section.min) return MatchType.MATCH_PASS;
+                } else if (section.lifetime <= 3 * (long) section.min) {
+                    // Case 3a) min <= old lft <= 3 * min
+                    // Note that:
+                    // "(old lft + 2) / 3 <= min" is equivalent to "old lft <= 3 * min"
+                    //
+                    // Essentially, in this range there is no "renumbering support", as the
+                    // renumbering constant of 1/3 * old lft is smaller than the minimum
+                    // lifetime accepted by the kernel / userspace.
+                    //
+                    // if lft == 0     -> PASS
+                    // if lft > oldLft -> PASS
+                    // gen.addJumpIfR0Equals(0, nextFilterLabel);
+                    if (lft == 0) return MatchType.MATCH_PASS;
+                    // gen.addJumpIfR0GreaterThan((int) section.lifetime, nextFilterLabel);
+                    if (lft > section.lifetime) return MatchType.MATCH_PASS;
+                } else {
+                    // Case 4a) otherwise
+                    //
+                    // if lft == 0                  -> PASS
+                    // if lft < min                 -> CONTINUE
+                    // if lft < (oldLft + 2) // 3   -> PASS
+                    // if lft > oldLft              -> PASS
+                    // gen.addJumpIfR0Equals(0, nextFilterLabel);
+                    if (lft == 0) return MatchType.MATCH_PASS;
+                    // gen.addJumpIfR0LessThan(section.min, continueLabel);
+                    if (lft < section.min) continue;
+                    // gen.addJumpIfR0LessThan((int) ((section.lifetime + 2) / 3),
+                    //         nextFilterLabel);
+                    if (lft < (section.lifetime + 2) / 3) return MatchType.MATCH_PASS;
+                    // gen.addJumpIfR0GreaterThan((int) section.lifetime, nextFilterLabel);
+                    if (lft > section.lifetime) return MatchType.MATCH_PASS;
+                }
+            }
+
+            return MatchType.MATCH_DROP;
         }
 
         // What is the minimum of all lifetimes within {@code packet} in seconds?
         // Precondition: matches(packet, length) already returned true.
-        long minLifetime() {
-            long minLifetime = Long.MAX_VALUE;
+        private int minLifetime() {
+            // While technically most lifetimes in the RA are u32s, as far as the RA filter is
+            // concerned, INT_MAX is still a *much* longer lifetime than any filter would ever
+            // reasonably be active for.
+            // Clamp minLifetime at INT_MAX.
+            int minLifetime = Integer.MAX_VALUE;
             for (PacketSection section : mPacketSections) {
-                if (isRelevantLifetime(section)) {
-                    minLifetime = Math.min(minLifetime, section.lifetime);
+                if (section.type != PacketSection.Type.LIFETIME) {
+                    continue;
                 }
+                // Ignore lifetimes below section.min and always ignore 0 lifetimes.
+                if (section.lifetime < Math.max(section.min, 1)) {
+                    continue;
+                }
+
+                minLifetime = (int) Math.min(minLifetime, section.lifetime);
             }
             return minLifetime;
         }
 
-        // How many seconds does this RA's have to live, taking into account the fact
-        // that we might have seen it a while ago.
-        long currentLifetime() {
-            return mMinLifetime - (currentTimeSeconds() - mLastSeen);
-        }
-
-        boolean isExpired() {
-            // TODO: We may want to handle 0 lifetime RAs differently, if they are common. We'll
-            // have to calculate the filter lifetime specially as a fraction of 0 is still 0.
-            return currentLifetime() <= 0;
-        }
-
         // Filter for a fraction of the lifetime and adjust for the age of the RA.
-        @GuardedBy("ApfFilter.this")
-        int filterLifetime() {
-            return (int) (mMinLifetime / FRACTION_OF_LIFETIME_TO_FILTER)
-                    - (int) (mProgramBaseTime - mLastSeen);
-        }
-
-        @GuardedBy("ApfFilter.this")
-        boolean shouldFilter() {
-            return filterLifetime() > 0;
+        int getRemainingFilterLft(int currentTimeSeconds) {
+            int filterLifetime = (int) ((mMinLifetime / FRACTION_OF_LIFETIME_TO_FILTER)
+                    - (currentTimeSeconds - mLastSeen));
+            filterLifetime = Math.max(0, filterLifetime);
+            // Clamp filterLifetime to <= 65535, so it fits in 2 bytes.
+            return Math.min(65535, filterLifetime);
         }
 
         // Append a filter for this RA to {@code gen}. Jump to DROP_LABEL if it should be dropped.
         // Jump to the next filter if packet doesn't match this RA.
-        // Return Long.MAX_VALUE if we don't install any filter program for this RA. As the return
-        // value of this function is used to calculate the program min lifetime (which corresponds
-        // to the smallest generated filter lifetime). Returning Long.MAX_VALUE in the case no
-        // filter gets generated makes sure the program lifetime stays unaffected.
         @GuardedBy("ApfFilter.this")
-        long generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+        void generateFilterLocked(ApfGenerator gen, int timeSeconds)
+                throws IllegalInstructionException {
             String nextFilterLabel = "Ra" + getUniqueNumberLocked();
             // Skip if packet is not the right size
             gen.addLoadFromMemory(Register.R0, gen.PACKET_SIZE_MEMORY_SLOT);
             gen.addJumpIfR0NotEquals(mPacket.capacity(), nextFilterLabel);
             // Skip filter if expired
             gen.addLoadFromMemory(Register.R0, gen.FILTER_AGE_MEMORY_SLOT);
-            gen.addJumpIfR0GreaterThan(filterLifetime(), nextFilterLabel);
+            gen.addJumpIfR0GreaterThan(getRemainingFilterLft(timeSeconds), nextFilterLabel);
             for (PacketSection section : mPacketSections) {
                 // Generate code to match the packet bytes.
                 if (section.type == PacketSection.Type.MATCH) {
@@ -964,25 +1016,77 @@
                             Arrays.copyOfRange(mPacket.array(), section.start,
                                     section.start + section.length),
                             nextFilterLabel);
-                }
-
-                // Generate code to test the lifetimes haven't gone down too far.
-                // The packet is accepted if any non-ignored lifetime is lower than filterLifetime.
-                if (isRelevantLifetime(section)) {
+                } else {
                     switch (section.length) {
-                        case 4: gen.addLoad32(Register.R0, section.start); break;
+                        // length asserted to be either 2 or 4 on PacketSection construction
                         case 2: gen.addLoad16(Register.R0, section.start); break;
-                        default:
-                            throw new IllegalStateException(
-                                    "bogus lifetime size " + section.length);
+                        case 4: gen.addLoad32(Register.R0, section.start); break;
                     }
-                    gen.addJumpIfR0LessThan(filterLifetime(), nextFilterLabel);
+
+                    // WARNING: keep this in sync with matches()!
+                    // For more information on lifetime comparisons in the APF bytecode, see
+                    // go/apf-ra-filter.
+                    if (section.lifetime == 0) {
+                        // Case 1) old lft == 0
+                        if (section.min > 0) {
+                            // a) in the presence of a min value.
+                            // if lft >= min -> PASS
+                            gen.addJumpIfR0GreaterThan(section.min - 1, nextFilterLabel);
+                        } else {
+                            // b) if min is 0 / there is no min value.
+                            // if lft > 0 -> PASS
+                            gen.addJumpIfR0GreaterThan(0, nextFilterLabel);
+                        }
+                    } else if (section.min == 0) {
+                        // Case 2b) section is not affected by any minimum.
+                        //
+                        // if lft < (oldLft + 2) // 3 -> PASS
+                        // if lft > oldLft            -> PASS
+                        gen.addJumpIfR0LessThan((int) ((section.lifetime + 2) / 3),
+                                nextFilterLabel);
+                        gen.addJumpIfR0GreaterThan((int) section.lifetime, nextFilterLabel);
+                    } else if (section.lifetime < section.min) {
+                        // Case 2a) 0 < old lft < min
+                        //
+                        // if lft == 0   -> PASS
+                        // if lft >= min -> PASS
+                        gen.addJumpIfR0Equals(0, nextFilterLabel);
+                        gen.addJumpIfR0GreaterThan(section.min - 1, nextFilterLabel);
+                    } else if (section.lifetime <= 3 * (long) section.min) {
+                        // Case 3a) min <= old lft <= 3 * min
+                        // Note that:
+                        // "(old lft + 2) / 3 <= min" is equivalent to "old lft <= 3 * min"
+                        //
+                        // Essentially, in this range there is no "renumbering support", as the
+                        // renumbering constant of 1/3 * old lft is smaller than the minimum
+                        // lifetime accepted by the kernel / userspace.
+                        //
+                        // if lft == 0     -> PASS
+                        // if lft > oldLft -> PASS
+                        gen.addJumpIfR0Equals(0, nextFilterLabel);
+                        gen.addJumpIfR0GreaterThan((int) section.lifetime, nextFilterLabel);
+                    } else {
+                        final String continueLabel = "Continue" + getUniqueNumberLocked();
+                        // Case 4a) otherwise
+                        //
+                        // if lft == 0                  -> PASS
+                        // if lft < min                 -> CONTINUE
+                        // if lft < (oldLft + 2) // 3   -> PASS
+                        // if lft > oldLft              -> PASS
+                        gen.addJumpIfR0Equals(0, nextFilterLabel);
+                        gen.addJumpIfR0LessThan(section.min, continueLabel);
+                        gen.addJumpIfR0LessThan((int) ((section.lifetime + 2) / 3),
+                                nextFilterLabel);
+                        gen.addJumpIfR0GreaterThan((int) section.lifetime, nextFilterLabel);
+
+                        // CONTINUE
+                        gen.defineLabel(continueLabel);
+                    }
                 }
             }
             maybeSetupCounter(gen, Counter.DROPPED_RA);
             gen.addJump(mCountAndDropLabel);
             gen.defineLabel(nextFilterLabel);
-            return filterLifetime();
         }
     }
 
@@ -1212,29 +1316,17 @@
     @GuardedBy("this")
     private final List<String[]> mMdnsAllowList = new ArrayList<>();
 
-    // There is always some marginal benefit to updating the installed APF program when an RA is
-    // seen because we can extend the program's lifetime slightly, but there is some cost to
-    // updating the program, so don't bother unless the program is going to expire soon. This
-    // constant defines "soon" in seconds.
-    private static final long MAX_PROGRAM_LIFETIME_WORTH_REFRESHING = 30;
     // We don't want to filter an RA for it's whole lifetime as it'll be expired by the time we ever
     // see a refresh.  Using half the lifetime might be a good idea except for the fact that
     // packets may be dropped, so let's use 6.
     private static final int FRACTION_OF_LIFETIME_TO_FILTER = 6;
 
-    // The base time for this filter program. In seconds since Unix Epoch.
-    // This is the time when the APF program was generated. All filters in the program should use
-    // this base time as their current time for consistency purposes.
-    @GuardedBy("this")
-    private long mProgramBaseTime;
     // When did we last install a filter program? In seconds since Unix Epoch.
     @GuardedBy("this")
-    private long mLastTimeInstalledProgram;
+    private int mLastTimeInstalledProgram;
     // How long should the last installed filter program live for? In seconds.
     @GuardedBy("this")
-    private long mLastInstalledProgramMinLifetime;
-    @GuardedBy("this")
-    private ApfProgramEvent.Builder mLastInstallEvent;
+    private int mLastInstalledProgramMinLifetime;
 
     // For debugging only. The last program installed.
     @GuardedBy("this")
@@ -1798,17 +1890,17 @@
     @GuardedBy("this")
     @VisibleForTesting
     public void installNewProgramLocked() {
-        purgeExpiredRasLocked();
         ArrayList<Ra> rasToFilter = new ArrayList<>();
         final byte[] program;
-        long programMinLifetime = Long.MAX_VALUE;
-        long maximumApfProgramSize = mApfCapabilities.maximumApfProgramSize;
+        int programMinLft = Integer.MAX_VALUE;
+        int maximumApfProgramSize = mApfCapabilities.maximumApfProgramSize;
         if (mApfCapabilities.hasDataAccess()) {
             // Reserve space for the counters.
             maximumApfProgramSize -= Counter.totalSize();
         }
 
-        mProgramBaseTime = currentTimeSeconds();
+        // Ensure the entire APF program uses the same time base.
+        int timeSeconds = secondsSinceBoot();
         try {
             // Step 1: Determine how many RA filters we can fit in the program.
             ApfGenerator gen = emitPrologueLocked();
@@ -1824,8 +1916,9 @@
             }
 
             for (Ra ra : mRas) {
-                if (!ra.shouldFilter()) continue;
-                ra.generateFilterLocked(gen);
+                // skip filter if it has expired.
+                if (ra.getRemainingFilterLft(timeSeconds) <= 0) continue;
+                ra.generateFilterLocked(gen, timeSeconds);
                 // Stop if we get too big.
                 if (gen.programLengthOverEstimate() > maximumApfProgramSize) {
                     if (VDBG) Log.d(TAG, "Past maximum program size, skipping RAs");
@@ -1838,7 +1931,8 @@
             // Step 2: Actually generate the program
             gen = emitPrologueLocked();
             for (Ra ra : rasToFilter) {
-                programMinLifetime = Math.min(programMinLifetime, ra.generateFilterLocked(gen));
+                ra.generateFilterLocked(gen, timeSeconds);
+                programMinLft = Math.min(programMinLft, ra.getRemainingFilterLft(timeSeconds));
             }
             emitEpilogue(gen);
             program = gen.generate();
@@ -1847,79 +1941,48 @@
             return;
         }
         mIpClientCallback.installPacketFilter(program);
-        mLastTimeInstalledProgram = mProgramBaseTime;
-        mLastInstalledProgramMinLifetime = programMinLifetime;
+        mLastTimeInstalledProgram = timeSeconds;
+        mLastInstalledProgramMinLifetime = programMinLft;
         mLastInstalledProgram = program;
         mNumProgramUpdates++;
 
         if (VDBG) {
             hexDump("Installing filter: ", program, program.length);
         }
-        logApfProgramEventLocked(mProgramBaseTime);
-        mLastInstallEvent = new ApfProgramEvent.Builder()
-                .setLifetime(programMinLifetime)
-                .setFilteredRas(rasToFilter.size())
-                .setCurrentRas(mRas.size())
-                .setProgramLength(program.length)
-                .setFlags(mIPv4Address != null, mMulticastFilter);
-    }
-
-    @GuardedBy("this")
-    private void logApfProgramEventLocked(long now) {
-        if (mLastInstallEvent == null) {
-            return;
-        }
-        ApfProgramEvent.Builder ev = mLastInstallEvent;
-        mLastInstallEvent = null;
-        final long actualLifetime = now - mLastTimeInstalledProgram;
-        ev.setActualLifetime(actualLifetime);
-        if (actualLifetime < APF_PROGRAM_EVENT_LIFETIME_THRESHOLD) {
-            return;
-        }
-        mMetricsLog.log(ev.build());
-    }
-
-    /**
-     * Returns {@code true} if a new program should be installed because the current one dies soon.
-     */
-    private boolean shouldInstallnewProgram() {
-        long expiry = mLastTimeInstalledProgram + mLastInstalledProgramMinLifetime;
-        return expiry < currentTimeSeconds() + MAX_PROGRAM_LIFETIME_WORTH_REFRESHING;
     }
 
     private void hexDump(String msg, byte[] packet, int length) {
         log(msg + HexDump.toHexString(packet, 0, length, false /* lowercase */));
     }
 
-    @GuardedBy("this")
-    private void purgeExpiredRasLocked() {
-        for (int i = 0; i < mRas.size();) {
-            if (mRas.get(i).isExpired()) {
-                log("Expiring " + mRas.get(i));
-                mRas.remove(i);
-            } else {
-                i++;
-            }
-        }
-    }
-
     /**
      * Process an RA packet, updating the list of known RAs and installing a new APF program
      * if the current APF program should be updated.
-     * @return a ProcessRaResult enum describing what action was performed.
      */
     @VisibleForTesting
-    public synchronized ProcessRaResult processRa(byte[] packet, int length) {
+    public synchronized void processRa(byte[] packet, int length) {
         if (VDBG) hexDump("Read packet = ", packet, length);
 
+        final Ra ra;
+        try {
+            ra = new Ra(packet, length);
+        } catch (Exception e) {
+            Log.e(TAG, "Error parsing RA", e);
+            return;
+        }
+
+        // Remove all expired RA filters before trying to match the new RA.
+        // TODO: matches() still checks that the old RA filter has not expired. Consider removing
+        // that check.
+        final int now = secondsSinceBoot();
+        mRas.removeIf(item -> item.getRemainingFilterLft(now) <= 0);
+
         // Have we seen this RA before?
         for (int i = 0; i < mRas.size(); i++) {
-            Ra ra = mRas.get(i);
-            if (ra.matches(packet, length)) {
-                if (VDBG) log("matched RA " + ra);
-                // Update lifetimes.
-                ra.mLastSeen = currentTimeSeconds();
-                ra.seenCount++;
+            final Ra oldRa = mRas.get(i);
+            final Ra.MatchType result = oldRa.matches(ra);
+            if (result == Ra.MatchType.MATCH_PASS) {
+                log("Updating RA from " + oldRa + " to " + ra);
 
                 // Keep mRas in LRU order so as to prioritize generating filters for recently seen
                 // RAs. LRU prioritizes this because RA filters are generated in order from mRas
@@ -1928,36 +1991,33 @@
                 // filter program.
                 // TODO: consider sorting the RAs in order of increasing expiry time as well.
                 // Swap to front of array.
-                mRas.add(0, mRas.remove(i));
+                mRas.remove(i);
+                mRas.add(0, ra);
 
-                // If the current program doesn't expire for a while, don't update.
-                if (shouldInstallnewProgram()) {
+                // Rate limit program installation
+                if (mTokenBucket.get()) {
                     installNewProgramLocked();
-                    return ProcessRaResult.UPDATE_EXPIRY;
+                } else {
+                    Log.e(TAG, "Failed to install prog for tracked RA, too many updates. " + ra);
                 }
-                return ProcessRaResult.MATCH;
+                return;
+            } else if (result == Ra.MatchType.MATCH_DROP) {
+                log("Ignoring RA " + ra + " which matches " + oldRa);
+                return;
             }
         }
-        purgeExpiredRasLocked();
-        // TODO: figure out how to proceed when we've received more then MAX_RAS RAs.
         if (mRas.size() >= MAX_RAS) {
-            return ProcessRaResult.DROPPED;
-        }
-        final Ra ra;
-        try {
-            ra = new Ra(packet, length);
-        } catch (Exception e) {
-            Log.e(TAG, "Error parsing RA", e);
-            return ProcessRaResult.PARSE_ERROR;
-        }
-        // Ignore 0 lifetime RAs.
-        if (ra.isExpired()) {
-            return ProcessRaResult.ZERO_LIFETIME;
+            // Remove the last (i.e. oldest) RA.
+            mRas.remove(mRas.size() - 1);
         }
         log("Adding " + ra);
-        mRas.add(ra);
-        installNewProgramLocked();
-        return ProcessRaResult.UPDATE_NEW_RA;
+        mRas.add(0, ra);
+        // Rate limit program installation
+        if (mTokenBucket.get()) {
+            installNewProgramLocked();
+        } else {
+            Log.e(TAG, "Failed to install prog for new RA, too many updates. " + ra);
+        }
     }
 
     /**
@@ -1975,16 +2035,14 @@
             return null;
         }
         // For now only support generating programs for Ethernet frames. If this restriction is
-        // lifted:
-        //   1. the program generator will need its offsets adjusted.
-        //   2. the packet filter attached to our packet socket will need its offset adjusted.
+        // lifted the program generator will need its offsets adjusted.
         if (apfCapabilities.apfPacketFormat != ARPHRD_ETHER) return null;
         if (!ApfGenerator.supportsVersion(apfCapabilities.apfVersionSupported)) {
             Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported);
             return null;
         }
 
-        return new ApfFilter(context, config, ifParams, ipClientCallback, new IpConnectivityLog());
+        return new ApfFilter(context, config, ifParams, ipClientCallback);
     }
 
     public synchronized void shutdown() {
@@ -2145,7 +2203,7 @@
         pw.println("Program updates: " + mNumProgramUpdates);
         pw.println(String.format(
                 "Last program length %d, installed %ds ago, lifetime %ds",
-                mLastInstalledProgram.length, currentTimeSeconds() - mLastTimeInstalledProgram,
+                mLastInstalledProgram.length, secondsSinceBoot() - mLastTimeInstalledProgram,
                 mLastInstalledProgramMinLifetime));
 
         pw.print("Denylisted Ethertypes:");
@@ -2159,7 +2217,7 @@
             pw.println(ra);
             pw.increaseIndent();
             pw.println(String.format(
-                    "Seen: %d, last %ds ago", ra.seenCount, currentTimeSeconds() - ra.mLastSeen));
+                    "Last seen %ds ago", secondsSinceBoot() - ra.mLastSeen));
             if (DBG) {
                 pw.println("Last match:");
                 pw.increaseIndent();
diff --git a/src/android/net/apf/ApfGenerator.java b/src/android/net/apf/ApfGenerator.java
index 7fc5fd3..0460c83 100644
--- a/src/android/net/apf/ApfGenerator.java
+++ b/src/android/net/apf/ApfGenerator.java
@@ -20,6 +20,7 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 
 /**
  * APF assembler/generator.  A tool for generating an APF program.
@@ -62,7 +63,8 @@
         JNEBS(20), // Compare not equal byte sequence, e.g. "jnebs R0,5,label,0x1122334455"
         EXT(21),   // Followed by immediate indicating ExtendedOpcodes.
         LDDW(22),  // Load 4 bytes from data memory address (register + immediate): "lddw R0, [5]R1"
-        STDW(23);  // Store 4 bytes to data memory address (register + immediate): "stdw R0, [5]R1"
+        STDW(23),  // Store 4 bytes to data memory address (register + immediate): "stdw R0, [5]R1"
+        WRITE(24); // Write 1, 2 or 4 bytes imm to the output buffer, e.g. "WRITE 5"
 
         final int value;
 
@@ -78,7 +80,12 @@
         NOT(32),  // Not, e.g. "not R0"
         NEG(33),  // Negate, e.g. "neg R0"
         SWAP(34), // Swap, e.g. "swap R0,R1"
-        MOVE(35);  // Move, e.g. "move R0,R1"
+        MOVE(35),  // Move, e.g. "move R0,R1"
+        ALLOC(36), // Allocate buffer, "e.g. ALLOC R0"
+        TRANS(37), // Transmit buffer, "e.g. TRANS R0"
+        EWRITE1(38), // Write 1 byte from register to the output buffer, e.g. "EWRITE1 R0"
+        EWRITE2(39), // Write 2 bytes from register to the output buffer, e.g. "EWRITE2 R0"
+        EWRITE4(40); // Write 4 bytes from register to the output buffer, e.g. "EWRITE4 R0"
 
         final int value;
 
@@ -96,13 +103,28 @@
             this.value = value;
         }
     }
+
+    private static class Immediate {
+        public final boolean mSigned;
+        public final byte mImmSize;
+        public final int mValue;
+
+        Immediate(int value, boolean signed) {
+            this(value, signed, calculateImmSize(value, signed));
+        }
+
+        Immediate(int value, boolean signed, byte size) {
+            mValue = value;
+            mSigned = signed;
+            mImmSize = size;
+        }
+    }
+
     private class Instruction {
         private final byte mOpcode;   // A "Opcode" value.
         private final byte mRegister; // A "Register" value.
-        private boolean mHasImm;
-        private byte mImmSize;
-        private boolean mImmSigned;
-        private int mImm;
+        private final int mMaxSupportedImmediates;
+        public final List<Immediate> mImms = new ArrayList<>();
         // When mOpcode is a jump:
         private byte mTargetLabelSize;
         private String mTargetLabel;
@@ -114,27 +136,38 @@
         int offset;
 
         Instruction(Opcodes opcode, Register register) {
-            mOpcode = (byte)opcode.value;
-            mRegister = (byte)register.value;
+            this(opcode, register, 1 /* mMaxSupportedImmediates */);
+        }
+
+        Instruction(Opcodes opcode, Register register, int maxSupportedImm) {
+            mOpcode = (byte) opcode.value;
+            mRegister = (byte) register.value;
+            mMaxSupportedImmediates = maxSupportedImm;
         }
 
         Instruction(Opcodes opcode) {
             this(opcode, Register.R0);
         }
 
-        void setImm(int imm, boolean signed) {
-            mHasImm = true;
-            mImm = imm;
-            mImmSigned = signed;
-            mImmSize = calculateImmSize(imm, signed);
+        void addUnsignedImm(int imm) {
+            addImm(new Immediate(imm, false));
         }
 
-        void setUnsignedImm(int imm) {
-            setImm(imm, false);
+        void addUnsignedImm(int imm, byte size) {
+            addImm(new Immediate(imm, false, size));
         }
 
-        void setSignedImm(int imm) {
-            setImm(imm, true);
+        void addSignedImm(int imm) {
+            addImm(new Immediate(imm, true));
+        }
+
+        void addImm(Immediate imm) {
+            if (mImms.size() == mMaxSupportedImmediates) {
+                throw new IllegalArgumentException(
+                        String.format("Opcode: %d only support at max: %d imms", mOpcode,
+                                mMaxSupportedImmediates));
+            }
+            mImms.add(imm);
         }
 
         void setLabel(String label) throws IllegalInstructionException {
@@ -168,9 +201,7 @@
                 return 0;
             }
             int size = 1;
-            if (mHasImm) {
-                size += generatedImmSize();
-            }
+            size += mImms.size() * generatedImmSize();
             if (mTargetLabel != null) {
                 size += generatedImmSize();
             }
@@ -243,8 +274,8 @@
             if (mTargetLabel != null) {
                 writingOffset = writeValue(calculateTargetLabelOffset(), bytecode, writingOffset);
             }
-            if (mHasImm) {
-                writingOffset = writeValue(mImm, bytecode, writingOffset);
+            for (int i = 0; i < mImms.size(); ++i) {
+                writingOffset = writeValue(mImms.get(i).mValue, bytecode, writingOffset);
             }
             if (mCompareBytes != null) {
                 System.arraycopy(mCompareBytes, 0, bytecode, writingOffset, mCompareBytes.length);
@@ -257,15 +288,19 @@
         }
 
         /**
-         * Calculate the size of either the immediate field or the target label field, if either is
-         * present. Most instructions have either an immediate or a target label field, but for the
+         * Calculate the size of either the immediate fields or the target label field, if either is
+         * present. Most instructions have either immediates or a target label field, but for the
          * instructions that have both, the size of the target label field must be the same as the
-         * size of the immediate field, because there is only one length field in the instruction
-         * byte, hence why this function simply takes the maximum of the two sizes, so neither is
+         * size of the immediate fields, because there is only one length field in the instruction
+         * byte, hence why this function simply takes the maximum of those sizes, so neither is
          * truncated.
          */
         private byte generatedImmSize() {
-            return mImmSize > mTargetLabelSize ? mImmSize : mTargetLabelSize;
+            byte maxSize = mTargetLabelSize;
+            for (int i = 0; i < mImms.size(); ++i) {
+                maxSize = (byte) Math.max(maxSize, mImms.get(i).mImmSize);
+            }
+            return maxSize;
         }
 
         private int calculateTargetLabelOffset() throws IllegalInstructionException {
@@ -284,21 +319,6 @@
             final int targetLabelOffset = targetLabelInstruction.offset - (offset + size());
             return targetLabelOffset;
         }
-
-        private byte calculateImmSize(int imm, boolean signed) {
-            if (imm == 0) {
-                return 0;
-            }
-            if (signed && (imm >= -128 && imm <= 127) ||
-                    !signed && (imm >= 0 && imm <= 255)) {
-                return 1;
-            }
-            if (signed && (imm >= -32768 && imm <= 32767) ||
-                    !signed && (imm >= 0 && imm <= 65535)) {
-                return 2;
-            }
-            return 4;
-        }
     }
 
     /**
@@ -434,7 +454,7 @@
      */
     public ApfGenerator addLoad8(Register register, int offset) {
         Instruction instruction = new Instruction(Opcodes.LDB, register);
-        instruction.setUnsignedImm(offset);
+        instruction.addUnsignedImm(offset);
         addInstruction(instruction);
         return this;
     }
@@ -445,7 +465,7 @@
      */
     public ApfGenerator addLoad16(Register register, int offset) {
         Instruction instruction = new Instruction(Opcodes.LDH, register);
-        instruction.setUnsignedImm(offset);
+        instruction.addUnsignedImm(offset);
         addInstruction(instruction);
         return this;
     }
@@ -456,7 +476,7 @@
      */
     public ApfGenerator addLoad32(Register register, int offset) {
         Instruction instruction = new Instruction(Opcodes.LDW, register);
-        instruction.setUnsignedImm(offset);
+        instruction.addUnsignedImm(offset);
         addInstruction(instruction);
         return this;
     }
@@ -468,7 +488,7 @@
      */
     public ApfGenerator addLoad8Indexed(Register register, int offset) {
         Instruction instruction = new Instruction(Opcodes.LDBX, register);
-        instruction.setUnsignedImm(offset);
+        instruction.addUnsignedImm(offset);
         addInstruction(instruction);
         return this;
     }
@@ -480,7 +500,7 @@
      */
     public ApfGenerator addLoad16Indexed(Register register, int offset) {
         Instruction instruction = new Instruction(Opcodes.LDHX, register);
-        instruction.setUnsignedImm(offset);
+        instruction.addUnsignedImm(offset);
         addInstruction(instruction);
         return this;
     }
@@ -492,7 +512,7 @@
      */
     public ApfGenerator addLoad32Indexed(Register register, int offset) {
         Instruction instruction = new Instruction(Opcodes.LDWX, register);
-        instruction.setUnsignedImm(offset);
+        instruction.addUnsignedImm(offset);
         addInstruction(instruction);
         return this;
     }
@@ -502,7 +522,7 @@
      */
     public ApfGenerator addAdd(int value) {
         Instruction instruction = new Instruction(Opcodes.ADD);
-        instruction.setUnsignedImm(value);
+        instruction.addUnsignedImm(value);
         addInstruction(instruction);
         return this;
     }
@@ -512,7 +532,7 @@
      */
     public ApfGenerator addMul(int value) {
         Instruction instruction = new Instruction(Opcodes.MUL);
-        instruction.setUnsignedImm(value);
+        instruction.addUnsignedImm(value);
         addInstruction(instruction);
         return this;
     }
@@ -522,7 +542,7 @@
      */
     public ApfGenerator addDiv(int value) {
         Instruction instruction = new Instruction(Opcodes.DIV);
-        instruction.setUnsignedImm(value);
+        instruction.addUnsignedImm(value);
         addInstruction(instruction);
         return this;
     }
@@ -532,7 +552,7 @@
      */
     public ApfGenerator addAnd(int value) {
         Instruction instruction = new Instruction(Opcodes.AND);
-        instruction.setUnsignedImm(value);
+        instruction.addUnsignedImm(value);
         addInstruction(instruction);
         return this;
     }
@@ -542,7 +562,7 @@
      */
     public ApfGenerator addOr(int value) {
         Instruction instruction = new Instruction(Opcodes.OR);
-        instruction.setUnsignedImm(value);
+        instruction.addUnsignedImm(value);
         addInstruction(instruction);
         return this;
     }
@@ -552,7 +572,7 @@
      */
     public ApfGenerator addLeftShift(int value) {
         Instruction instruction = new Instruction(Opcodes.SH);
-        instruction.setSignedImm(value);
+        instruction.addSignedImm(value);
         addInstruction(instruction);
         return this;
     }
@@ -563,7 +583,7 @@
      */
     public ApfGenerator addRightShift(int value) {
         Instruction instruction = new Instruction(Opcodes.SH);
-        instruction.setSignedImm(-value);
+        instruction.addSignedImm(-value);
         addInstruction(instruction);
         return this;
     }
@@ -630,7 +650,7 @@
      */
     public ApfGenerator addLoadImmediate(Register register, int value) {
         Instruction instruction = new Instruction(Opcodes.LI, register);
-        instruction.setSignedImm(value);
+        instruction.addSignedImm(value);
         addInstruction(instruction);
         return this;
     }
@@ -641,7 +661,7 @@
      */
     public ApfGenerator addJumpIfR0Equals(int value, String target) {
         Instruction instruction = new Instruction(Opcodes.JEQ);
-        instruction.setUnsignedImm(value);
+        instruction.addUnsignedImm(value);
         instruction.setTargetLabel(target);
         addInstruction(instruction);
         return this;
@@ -653,7 +673,7 @@
      */
     public ApfGenerator addJumpIfR0NotEquals(int value, String target) {
         Instruction instruction = new Instruction(Opcodes.JNE);
-        instruction.setUnsignedImm(value);
+        instruction.addUnsignedImm(value);
         instruction.setTargetLabel(target);
         addInstruction(instruction);
         return this;
@@ -665,7 +685,7 @@
      */
     public ApfGenerator addJumpIfR0GreaterThan(int value, String target) {
         Instruction instruction = new Instruction(Opcodes.JGT);
-        instruction.setUnsignedImm(value);
+        instruction.addUnsignedImm(value);
         instruction.setTargetLabel(target);
         addInstruction(instruction);
         return this;
@@ -677,7 +697,7 @@
      */
     public ApfGenerator addJumpIfR0LessThan(int value, String target) {
         Instruction instruction = new Instruction(Opcodes.JLT);
-        instruction.setUnsignedImm(value);
+        instruction.addUnsignedImm(value);
         instruction.setTargetLabel(target);
         addInstruction(instruction);
         return this;
@@ -689,7 +709,7 @@
      */
     public ApfGenerator addJumpIfR0AnyBitsSet(int value, String target) {
         Instruction instruction = new Instruction(Opcodes.JSET);
-        instruction.setUnsignedImm(value);
+        instruction.addUnsignedImm(value);
         instruction.setTargetLabel(target);
         addInstruction(instruction);
         return this;
@@ -760,7 +780,7 @@
             throw new IllegalInstructionException("JNEBS fails with R1");
         }
         Instruction instruction = new Instruction(Opcodes.JNEBS, register);
-        instruction.setUnsignedImm(bytes.length);
+        instruction.addUnsignedImm(bytes.length);
         instruction.setTargetLabel(target);
         instruction.setCompareBytes(bytes);
         addInstruction(instruction);
@@ -777,7 +797,7 @@
             throw new IllegalInstructionException("illegal memory slot number: " + slot);
         }
         Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.setUnsignedImm(ExtendedOpcodes.LDM.value + slot);
+        instruction.addUnsignedImm(ExtendedOpcodes.LDM.value + slot);
         addInstruction(instruction);
         return this;
     }
@@ -792,7 +812,7 @@
             throw new IllegalInstructionException("illegal memory slot number: " + slot);
         }
         Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.setUnsignedImm(ExtendedOpcodes.STM.value + slot);
+        instruction.addUnsignedImm(ExtendedOpcodes.STM.value + slot);
         addInstruction(instruction);
         return this;
     }
@@ -802,7 +822,7 @@
      */
     public ApfGenerator addNot(Register register) {
         Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.setUnsignedImm(ExtendedOpcodes.NOT.value);
+        instruction.addUnsignedImm(ExtendedOpcodes.NOT.value);
         addInstruction(instruction);
         return this;
     }
@@ -812,7 +832,7 @@
      */
     public ApfGenerator addNeg(Register register) {
         Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.setUnsignedImm(ExtendedOpcodes.NEG.value);
+        instruction.addUnsignedImm(ExtendedOpcodes.NEG.value);
         addInstruction(instruction);
         return this;
     }
@@ -822,7 +842,7 @@
      */
     public ApfGenerator addSwap() {
         Instruction instruction = new Instruction(Opcodes.EXT);
-        instruction.setUnsignedImm(ExtendedOpcodes.SWAP.value);
+        instruction.addUnsignedImm(ExtendedOpcodes.SWAP.value);
         addInstruction(instruction);
         return this;
     }
@@ -833,7 +853,84 @@
      */
     public ApfGenerator addMove(Register register) {
         Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.setUnsignedImm(ExtendedOpcodes.MOVE.value);
+        instruction.addUnsignedImm(ExtendedOpcodes.MOVE.value);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to call the apf_allocate_buffer() function.
+     *
+     * @param register the register value contains the buffer size.
+     */
+    public ApfGenerator addAlloc(Register register) throws IllegalInstructionException {
+        requireApfVersion(5);
+        Instruction instruction = new Instruction(Opcodes.EXT, register);
+        instruction.addUnsignedImm(ExtendedOpcodes.ALLOC.value);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to call the apf_transmit_buffer() function.
+     *
+     * @param register the register value contains the packet type.
+     */
+    public ApfGenerator addTrans(Register register) throws IllegalInstructionException {
+        requireApfVersion(5);
+        Instruction instruction = new Instruction(Opcodes.EXT, register);
+        instruction.addUnsignedImm(ExtendedOpcodes.TRANS.value);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to write 1, 2 or 4 bytes value to output buffer.
+     *
+     * @param value the value to write
+     * @param size the size of the value
+     * @return the ApfGenerator object
+     * @throws IllegalInstructionException throws when size is not 1, 2 or 4
+     */
+    public ApfGenerator addWrite(int value, byte size) throws IllegalInstructionException {
+        requireApfVersion(5);
+        if (!(size == 1 || size == 2 || size == 4)) {
+            throw new IllegalInstructionException("length field must be 1, 2 or 4");
+        }
+        if (size < calculateImmSize(value, false)) {
+            throw new IllegalInstructionException(
+                    String.format("the value %d is unfit into size: %d", value, size));
+        }
+        Instruction instruction = new Instruction(Opcodes.WRITE);
+        instruction.addUnsignedImm(value, size);
+        addInstruction(instruction);
+        return this;
+    }
+
+    /**
+     * Add an instruction to the end of the program to write 1, 2 or 4 bytes value from register
+     * to output buffer.
+     *
+     * @param register the register contains the value to be written
+     * @param size the size of the value
+     * @return the ApfGenerator object
+     * @throws IllegalInstructionException throws when size is not 1, 2 or 4
+     */
+    public ApfGenerator addWrite(Register register, byte size)
+            throws IllegalInstructionException {
+        requireApfVersion(5);
+        if (!(size == 1 || size == 2 || size == 4)) {
+            throw new IllegalInstructionException(
+                    "length field must be 1, 2 or 4");
+        }
+        Instruction instruction = new Instruction(Opcodes.EXT, register);
+        if (size == 1) {
+            instruction.addUnsignedImm(ExtendedOpcodes.EWRITE1.value);
+        } else if (size == 2) {
+            instruction.addUnsignedImm(ExtendedOpcodes.EWRITE2.value);
+        } else {
+            instruction.addUnsignedImm(ExtendedOpcodes.EWRITE4.value);
+        }
         addInstruction(instruction);
         return this;
     }
@@ -848,7 +945,7 @@
             throws IllegalInstructionException {
         requireApfVersion(3);
         Instruction instruction = new Instruction(Opcodes.LDDW, destinationRegister);
-        instruction.setSignedImm(offset);
+        instruction.addSignedImm(offset);
         addInstruction(instruction);
         return this;
     }
@@ -863,7 +960,7 @@
             throws IllegalInstructionException {
         requireApfVersion(3);
         Instruction instruction = new Instruction(Opcodes.STDW, sourceRegister);
-        instruction.setSignedImm(offset);
+        instruction.addSignedImm(offset);
         addInstruction(instruction);
         return this;
     }
@@ -882,6 +979,22 @@
     }
 
     /**
+     * Calculate the size of the imm.
+     */
+    private static byte calculateImmSize(int imm, boolean signed) {
+        if (imm == 0) {
+            return 0;
+        }
+        if (signed && (imm >= -128 && imm <= 127) || !signed && (imm >= 0 && imm <= 255)) {
+            return 1;
+        }
+        if (signed && (imm >= -32768 && imm <= 32767) || !signed && (imm >= 0 && imm <= 65535)) {
+            return 2;
+        }
+        return 4;
+    }
+
+    /**
      * Returns an overestimate of the size of the generated program. {@link #generate} may return
      * a program that is smaller.
      */
diff --git a/src/android/net/apf/LegacyApfFilter.java b/src/android/net/apf/LegacyApfFilter.java
new file mode 100644
index 0000000..63e2bbc
--- /dev/null
+++ b/src/android/net/apf/LegacyApfFilter.java
@@ -0,0 +1,2263 @@
+/*
+ * Copyright (C) 2016 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.apf;
+
+import static android.net.util.SocketUtils.makePacketSocketAddress;
+import static android.system.OsConstants.AF_PACKET;
+import static android.system.OsConstants.ARPHRD_ETHER;
+import static android.system.OsConstants.ETH_P_ARP;
+import static android.system.OsConstants.ETH_P_IP;
+import static android.system.OsConstants.ETH_P_IPV6;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_RAW;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NattKeepalivePacketDataParcelable;
+import android.net.TcpKeepalivePacketDataParcelable;
+import android.net.apf.ApfGenerator.IllegalInstructionException;
+import android.net.apf.ApfGenerator.Register;
+import android.net.ip.IpClient.IpClientCallbacksWrapper;
+import android.net.metrics.ApfProgramEvent;
+import android.net.metrics.ApfStats;
+import android.net.metrics.IpConnectivityLog;
+import android.net.metrics.RaEvent;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.HexDump;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.ConnectivityUtils;
+import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.SocketUtils;
+import com.android.networkstack.util.NetworkStackUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * For networks that support packet filtering via APF programs, {@code ApfFilter}
+ * listens for IPv6 ICMPv6 router advertisements (RAs) and generates APF programs to
+ * filter out redundant duplicate ones.
+ *
+ * Threading model:
+ * A collection of RAs we've received is kept in mRas. Generating APF programs uses mRas to
+ * know what RAs to filter for, thus generating APF programs is dependent on mRas.
+ * mRas can be accessed by multiple threads:
+ * - ReceiveThread, which listens for RAs and adds them to mRas, and generates APF programs.
+ * - callers of:
+ *    - setMulticastFilter(), which can cause an APF program to be generated.
+ *    - dump(), which dumps mRas among other things.
+ *    - shutdown(), which clears mRas.
+ * So access to mRas is synchronized.
+ *
+ * @hide
+ */
+public class LegacyApfFilter implements AndroidPacketFilter {
+
+    // Enums describing the outcome of receiving an RA packet.
+    private static enum ProcessRaResult {
+        MATCH,          // Received RA matched a known RA
+        DROPPED,        // Received RA ignored due to MAX_RAS
+        PARSE_ERROR,    // Received RA could not be parsed
+        ZERO_LIFETIME,  // Received RA had 0 lifetime
+        UPDATE_NEW_RA,  // APF program updated for new RA
+        UPDATE_EXPIRY   // APF program updated for expiry
+    }
+
+    /**
+     * APF packet counters.
+     *
+     * Packet counters are 32bit big-endian values, and allocated near the end of the APF data
+     * buffer, using negative byte offsets, where -4 is equivalent to maximumApfProgramSize - 4,
+     * the last writable 32bit word.
+     */
+    @VisibleForTesting
+    public static enum Counter {
+        RESERVED_OOB,  // Points to offset 0 from the end of the buffer (out-of-bounds)
+        TOTAL_PACKETS,
+        PASSED_ARP,
+        PASSED_DHCP,
+        PASSED_IPV4,
+        PASSED_IPV6_NON_ICMP,
+        PASSED_IPV4_UNICAST,
+        PASSED_IPV6_ICMP,
+        PASSED_IPV6_UNICAST_NON_ICMP,
+        PASSED_ARP_NON_IPV4,
+        PASSED_ARP_UNKNOWN,
+        PASSED_ARP_UNICAST_REPLY,
+        PASSED_NON_IP_UNICAST,
+        PASSED_MDNS,
+        DROPPED_ETH_BROADCAST,
+        DROPPED_RA,
+        DROPPED_GARP_REPLY,
+        DROPPED_ARP_OTHER_HOST,
+        DROPPED_IPV4_L2_BROADCAST,
+        DROPPED_IPV4_BROADCAST_ADDR,
+        DROPPED_IPV4_BROADCAST_NET,
+        DROPPED_IPV4_MULTICAST,
+        DROPPED_IPV6_ROUTER_SOLICITATION,
+        DROPPED_IPV6_MULTICAST_NA,
+        DROPPED_IPV6_MULTICAST,
+        DROPPED_IPV6_MULTICAST_PING,
+        DROPPED_IPV6_NON_ICMP_MULTICAST,
+        DROPPED_802_3_FRAME,
+        DROPPED_ETHERTYPE_BLACKLISTED,
+        DROPPED_ARP_REPLY_SPA_NO_HOST,
+        DROPPED_IPV4_KEEPALIVE_ACK,
+        DROPPED_IPV6_KEEPALIVE_ACK,
+        DROPPED_IPV4_NATT_KEEPALIVE,
+        DROPPED_MDNS;
+
+        // Returns the negative byte offset from the end of the APF data segment for
+        // a given counter.
+        public int offset() {
+            return - this.ordinal() * 4;  // Currently, all counters are 32bit long.
+        }
+
+        // Returns the total size of the data segment in bytes.
+        public static int totalSize() {
+            return (Counter.class.getEnumConstants().length - 1) * 4;
+        }
+    }
+
+    /**
+     * When APFv4 is supported, loads R1 with the offset of the specified counter.
+     */
+    private void maybeSetupCounter(ApfGenerator gen, Counter c) {
+        if (mApfCapabilities.hasDataAccess()) {
+            gen.addLoadImmediate(Register.R1, c.offset());
+        }
+    }
+
+    // When APFv4 is supported, these point to the trampolines generated by emitEpilogue().
+    // Otherwise, they're just aliases for PASS_LABEL and DROP_LABEL.
+    private final String mCountAndPassLabel;
+    private final String mCountAndDropLabel;
+
+    // Thread to listen for RAs.
+    @VisibleForTesting
+    public class ReceiveThread extends Thread {
+        private final byte[] mPacket = new byte[1514];
+        private final FileDescriptor mSocket;
+        private final long mStart = SystemClock.elapsedRealtime();
+
+        private int mReceivedRas = 0;
+        private int mMatchingRas = 0;
+        private int mDroppedRas = 0;
+        private int mParseErrors = 0;
+        private int mZeroLifetimeRas = 0;
+        private int mProgramUpdates = 0;
+
+        private volatile boolean mStopped;
+
+        public ReceiveThread(FileDescriptor socket) {
+            mSocket = socket;
+        }
+
+        public void halt() {
+            mStopped = true;
+            // Interrupts the read() call the thread is blocked in.
+            SocketUtils.closeSocketQuietly(mSocket);
+        }
+
+        @Override
+        public void run() {
+            log("begin monitoring");
+            while (!mStopped) {
+                try {
+                    int length = Os.read(mSocket, mPacket, 0, mPacket.length);
+                    updateStats(processRa(mPacket, length));
+                } catch (IOException|ErrnoException e) {
+                    if (!mStopped) {
+                        Log.e(TAG, "Read error", e);
+                    }
+                }
+            }
+            logStats();
+        }
+
+        private void updateStats(ProcessRaResult result) {
+            mReceivedRas++;
+            switch(result) {
+                case MATCH:
+                    mMatchingRas++;
+                    return;
+                case DROPPED:
+                    mDroppedRas++;
+                    return;
+                case PARSE_ERROR:
+                    mParseErrors++;
+                    return;
+                case ZERO_LIFETIME:
+                    mZeroLifetimeRas++;
+                    return;
+                case UPDATE_EXPIRY:
+                    mMatchingRas++;
+                    mProgramUpdates++;
+                    return;
+                case UPDATE_NEW_RA:
+                    mProgramUpdates++;
+                    return;
+            }
+        }
+
+        private void logStats() {
+            final long nowMs = SystemClock.elapsedRealtime();
+            synchronized (this) {
+                final ApfStats stats = new ApfStats.Builder()
+                        .setReceivedRas(mReceivedRas)
+                        .setMatchingRas(mMatchingRas)
+                        .setDroppedRas(mDroppedRas)
+                        .setParseErrors(mParseErrors)
+                        .setZeroLifetimeRas(mZeroLifetimeRas)
+                        .setProgramUpdates(mProgramUpdates)
+                        .setDurationMs(nowMs - mStart)
+                        .setMaxProgramSize(mApfCapabilities.maximumApfProgramSize)
+                        .setProgramUpdatesAll(mNumProgramUpdates)
+                        .setProgramUpdatesAllowingMulticast(mNumProgramUpdatesAllowingMulticast)
+                        .build();
+                mMetricsLog.log(stats);
+                logApfProgramEventLocked(nowMs / DateUtils.SECOND_IN_MILLIS);
+            }
+        }
+    }
+
+    private static final String TAG = "ApfFilter";
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false;
+
+    private static final int ETH_HEADER_LEN = 14;
+    private static final int ETH_DEST_ADDR_OFFSET = 0;
+    private static final int ETH_ETHERTYPE_OFFSET = 12;
+    private static final int ETH_TYPE_MIN = 0x0600;
+    private static final int ETH_TYPE_MAX = 0xFFFF;
+    // TODO: Make these offsets relative to end of link-layer header; don't include ETH_HEADER_LEN.
+    private static final int IPV4_TOTAL_LENGTH_OFFSET = ETH_HEADER_LEN + 2;
+    private static final int IPV4_FRAGMENT_OFFSET_OFFSET = ETH_HEADER_LEN + 6;
+    // Endianness is not an issue for this constant because the APF interpreter always operates in
+    // network byte order.
+    private static final int IPV4_FRAGMENT_OFFSET_MASK = 0x1fff;
+    private static final int IPV4_PROTOCOL_OFFSET = ETH_HEADER_LEN + 9;
+    private static final int IPV4_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 16;
+    private static final int IPV4_ANY_HOST_ADDRESS = 0;
+    private static final int IPV4_BROADCAST_ADDRESS = -1; // 255.255.255.255
+    private static final int IPV4_HEADER_LEN = 20; // Without options
+
+    // Traffic class and Flow label are not byte aligned. Luckily we
+    // don't care about either value so we'll consider bytes 1-3 of the
+    // IPv6 header as don't care.
+    private static final int IPV6_FLOW_LABEL_OFFSET = ETH_HEADER_LEN + 1;
+    private static final int IPV6_FLOW_LABEL_LEN = 3;
+    private static final int IPV6_NEXT_HEADER_OFFSET = ETH_HEADER_LEN + 6;
+    private static final int IPV6_SRC_ADDR_OFFSET = ETH_HEADER_LEN + 8;
+    private static final int IPV6_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 24;
+    private static final int IPV6_HEADER_LEN = 40;
+    // The IPv6 all nodes address ff02::1
+    private static final byte[] IPV6_ALL_NODES_ADDRESS =
+            { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
+
+    private static final int ICMP6_TYPE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN;
+
+    private static final int IPPROTO_HOPOPTS = 0;
+
+    // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT
+    private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 2;
+    private static final int UDP_HEADER_LEN = 8;
+
+    private static final int TCP_HEADER_SIZE_OFFSET = 12;
+
+    private static final int DHCP_CLIENT_PORT = 68;
+    // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT
+    private static final int DHCP_CLIENT_MAC_OFFSET = ETH_HEADER_LEN + UDP_HEADER_LEN + 28;
+
+    private static final int ARP_HEADER_OFFSET = ETH_HEADER_LEN;
+    private static final byte[] ARP_IPV4_HEADER = {
+            0, 1, // Hardware type: Ethernet (1)
+            8, 0, // Protocol type: IP (0x0800)
+            6,    // Hardware size: 6
+            4,    // Protocol size: 4
+    };
+    private static final int ARP_OPCODE_OFFSET = ARP_HEADER_OFFSET + 6;
+    // Opcode: ARP request (0x0001), ARP reply (0x0002)
+    private static final short ARP_OPCODE_REQUEST = 1;
+    private static final short ARP_OPCODE_REPLY = 2;
+    private static final int ARP_SOURCE_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 14;
+    private static final int ARP_TARGET_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 24;
+    // Do not log ApfProgramEvents whose actual lifetimes was less than this.
+    private static final int APF_PROGRAM_EVENT_LIFETIME_THRESHOLD = 2;
+    // Limit on the Black List size to cap on program usage for this
+    // TODO: Select a proper max length
+    private static final int APF_MAX_ETH_TYPE_BLACK_LIST_LEN = 20;
+
+    private static final byte[] ETH_MULTICAST_MDNS_V4_MAC_ADDRESS =
+            {(byte) 0x01, (byte) 0x00, (byte) 0x5e, (byte) 0x00, (byte) 0x00, (byte) 0xfb};
+    private static final byte[] ETH_MULTICAST_MDNS_V6_MAC_ADDRESS =
+            {(byte) 0x33, (byte) 0x33, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xfb};
+    private static final int MDNS_PORT = 5353;
+    private static final int DNS_HEADER_LEN = 12;
+    private static final int DNS_QDCOUNT_OFFSET = 4;
+    // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT, or the
+    // IPv6 header length.
+    private static final int MDNS_QDCOUNT_OFFSET =
+            ETH_HEADER_LEN + UDP_HEADER_LEN + DNS_QDCOUNT_OFFSET;
+    private static final int MDNS_QNAME_OFFSET =
+            ETH_HEADER_LEN + UDP_HEADER_LEN + DNS_HEADER_LEN;
+
+
+
+    private final ApfCapabilities mApfCapabilities;
+    private final IpClientCallbacksWrapper mIpClientCallback;
+    private final InterfaceParams mInterfaceParams;
+    private final IpConnectivityLog mMetricsLog;
+
+    @VisibleForTesting
+    public byte[] mHardwareAddress;
+    @VisibleForTesting
+    public ReceiveThread mReceiveThread;
+    @GuardedBy("this")
+    private long mUniqueCounter;
+    @GuardedBy("this")
+    private boolean mMulticastFilter;
+    @GuardedBy("this")
+    private boolean mInDozeMode;
+    private final boolean mDrop802_3Frames;
+    private final int[] mEthTypeBlackList;
+
+    // Ignore non-zero RDNSS lifetimes below this value.
+    private final int mMinRdnssLifetimeSec;
+
+    // Detects doze mode state transitions.
+    private final BroadcastReceiver mDeviceIdleReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)) {
+                PowerManager powerManager =
+                        (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+                final boolean deviceIdle = powerManager.isDeviceIdleMode();
+                setDozeMode(deviceIdle);
+            }
+        }
+    };
+    private final Context mContext;
+
+    // Our IPv4 address, if we have just one, otherwise null.
+    @GuardedBy("this")
+    private byte[] mIPv4Address;
+    // The subnet prefix length of our IPv4 network. Only valid if mIPv4Address is not null.
+    @GuardedBy("this")
+    private int mIPv4PrefixLength;
+
+    @VisibleForTesting
+    public LegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
+            InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
+            IpConnectivityLog log) {
+        mApfCapabilities = config.apfCapabilities;
+        mIpClientCallback = ipClientCallback;
+        mInterfaceParams = ifParams;
+        mMulticastFilter = config.multicastFilter;
+        mDrop802_3Frames = config.ieee802_3Filter;
+        mMinRdnssLifetimeSec = config.minRdnssLifetimeSec;
+        mContext = context;
+
+        if (mApfCapabilities.hasDataAccess()) {
+            mCountAndPassLabel = "countAndPass";
+            mCountAndDropLabel = "countAndDrop";
+        } else {
+            // APFv4 unsupported: turn jumps to the counter trampolines to immediately PASS or DROP,
+            // preserving the original pre-APFv4 behavior.
+            mCountAndPassLabel = ApfGenerator.PASS_LABEL;
+            mCountAndDropLabel = ApfGenerator.DROP_LABEL;
+        }
+
+        // Now fill the black list from the passed array
+        mEthTypeBlackList = filterEthTypeBlackList(config.ethTypeBlackList);
+
+        mMetricsLog = log;
+
+        // TODO: ApfFilter should not generate programs until IpClient sends provisioning success.
+        maybeStartFilter();
+
+        // Listen for doze-mode transition changes to enable/disable the IPv6 multicast filter.
+        mContext.registerReceiver(mDeviceIdleReceiver,
+                new IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
+    }
+
+    public synchronized void setDataSnapshot(byte[] data) {
+        mDataSnapshot = data;
+    }
+
+    private void log(String s) {
+        Log.d(TAG, "(" + mInterfaceParams.name + "): " + s);
+    }
+
+    @GuardedBy("this")
+    private long getUniqueNumberLocked() {
+        return mUniqueCounter++;
+    }
+
+    @GuardedBy("this")
+    private static int[] filterEthTypeBlackList(int[] ethTypeBlackList) {
+        ArrayList<Integer> bl = new ArrayList<Integer>();
+
+        for (int p : ethTypeBlackList) {
+            // Check if the protocol is a valid ether type
+            if ((p < ETH_TYPE_MIN) || (p > ETH_TYPE_MAX)) {
+                continue;
+            }
+
+            // Check if the protocol is not repeated in the passed array
+            if (bl.contains(p)) {
+                continue;
+            }
+
+            // Check if list reach its max size
+            if (bl.size() == APF_MAX_ETH_TYPE_BLACK_LIST_LEN) {
+                Log.w(TAG, "Passed EthType Black List size too large (" + bl.size() +
+                        ") using top " + APF_MAX_ETH_TYPE_BLACK_LIST_LEN + " protocols");
+                break;
+            }
+
+            // Now add the protocol to the list
+            bl.add(p);
+        }
+
+        return bl.stream().mapToInt(Integer::intValue).toArray();
+    }
+
+    /**
+     * Attempt to start listening for RAs and, if RAs are received, generating and installing
+     * filters to ignore useless RAs.
+     */
+    @VisibleForTesting
+    public void maybeStartFilter() {
+        FileDescriptor socket;
+        try {
+            mHardwareAddress = mInterfaceParams.macAddr.toByteArray();
+            synchronized(this) {
+                // Clear the APF memory to reset all counters upon connecting to the first AP
+                // in an SSID. This is limited to APFv4 devices because this large write triggers
+                // a crash on some older devices (b/78905546).
+                if (mApfCapabilities.hasDataAccess()) {
+                    byte[] zeroes = new byte[mApfCapabilities.maximumApfProgramSize];
+                    mIpClientCallback.installPacketFilter(zeroes);
+                }
+
+                // Install basic filters
+                installNewProgramLocked();
+            }
+            socket = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6);
+            SocketAddress addr = makePacketSocketAddress(ETH_P_IPV6, mInterfaceParams.index);
+            Os.bind(socket, addr);
+            NetworkStackUtils.attachRaFilter(socket);
+        } catch(SocketException|ErrnoException e) {
+            Log.e(TAG, "Error starting filter", e);
+            return;
+        }
+        mReceiveThread = new ReceiveThread(socket);
+        mReceiveThread.start();
+    }
+
+    // Returns seconds since device boot.
+    @VisibleForTesting
+    protected long currentTimeSeconds() {
+        return SystemClock.elapsedRealtime() / DateUtils.SECOND_IN_MILLIS;
+    }
+
+    public static class InvalidRaException extends Exception {
+        public InvalidRaException(String m) {
+            super(m);
+        }
+    }
+
+    /**
+     *  Class to keep track of a section in a packet.
+     */
+    private static class PacketSection {
+        public enum Type {
+            MATCH,     // A field that should be matched (e.g., the router IP address).
+            IGNORE,    // An ignored field such as the checksum of the flow label. Not matched.
+            LIFETIME,  // A lifetime. Not matched, and generally counts toward minimum RA lifetime.
+        }
+
+        /** The type of section. */
+        public final Type type;
+        /** Offset into the packet at which this section begins. */
+        public final int start;
+        /** Length of this section in bytes. */
+        public final int length;
+        /** If this is a lifetime, the ICMP option that defined it. 0 for router lifetime. */
+        public final int option;
+        /** If this is a lifetime, the lifetime value. */
+        public final long lifetime;
+
+        PacketSection(int start, int length, Type type, int option, long lifetime) {
+            this.start = start;
+            this.length = length;
+            this.type = type;
+            this.option = option;
+            this.lifetime = lifetime;
+        }
+
+        public String toString() {
+            if (type == Type.LIFETIME) {
+                return String.format("%s: (%d, %d) %d %d", type, start, length, option, lifetime);
+            } else {
+                return String.format("%s: (%d, %d)", type, start, length);
+            }
+        }
+    }
+
+    // A class to hold information about an RA.
+    @VisibleForTesting
+    public class Ra {
+        // From RFC4861:
+        private static final int ICMP6_RA_HEADER_LEN = 16;
+        private static final int ICMP6_RA_CHECKSUM_OFFSET =
+                ETH_HEADER_LEN + IPV6_HEADER_LEN + 2;
+        private static final int ICMP6_RA_CHECKSUM_LEN = 2;
+        private static final int ICMP6_RA_OPTION_OFFSET =
+                ETH_HEADER_LEN + IPV6_HEADER_LEN + ICMP6_RA_HEADER_LEN;
+        private static final int ICMP6_RA_ROUTER_LIFETIME_OFFSET =
+                ETH_HEADER_LEN + IPV6_HEADER_LEN + 6;
+        private static final int ICMP6_RA_ROUTER_LIFETIME_LEN = 2;
+        // Prefix information option.
+        private static final int ICMP6_PREFIX_OPTION_TYPE = 3;
+        private static final int ICMP6_PREFIX_OPTION_LEN = 32;
+        private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET = 4;
+        private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN = 4;
+        private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET = 8;
+        private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN = 4;
+
+        // From RFC6106: Recursive DNS Server option
+        private static final int ICMP6_RDNSS_OPTION_TYPE = 25;
+        // From RFC6106: DNS Search List option
+        private static final int ICMP6_DNSSL_OPTION_TYPE = 31;
+
+        // From RFC4191: Route Information option
+        private static final int ICMP6_ROUTE_INFO_OPTION_TYPE = 24;
+        // Above three options all have the same format:
+        private static final int ICMP6_4_BYTE_LIFETIME_OFFSET = 4;
+        private static final int ICMP6_4_BYTE_LIFETIME_LEN = 4;
+
+        // Note: mPacket's position() cannot be assumed to be reset.
+        private final ByteBuffer mPacket;
+
+        // List of sections in the packet.
+        private final ArrayList<PacketSection> mPacketSections = new ArrayList<>();
+
+        // Minimum lifetime in packet
+        long mMinLifetime;
+        // When the packet was last captured, in seconds since Unix Epoch
+        long mLastSeen;
+
+        // For debugging only. Offsets into the packet where PIOs are.
+        private final ArrayList<Integer> mPrefixOptionOffsets = new ArrayList<>();
+
+        // For debugging only. Offsets into the packet where RDNSS options are.
+        private final ArrayList<Integer> mRdnssOptionOffsets = new ArrayList<>();
+
+        // For debugging only. Offsets into the packet where RIO options are.
+        private final ArrayList<Integer> mRioOptionOffsets = new ArrayList<>();
+
+        // For debugging only. How many times this RA was seen.
+        int seenCount = 0;
+
+        // For debugging only. Returns the hex representation of the last matching packet.
+        String getLastMatchingPacket() {
+            return HexDump.toHexString(mPacket.array(), 0, mPacket.capacity(),
+                    false /* lowercase */);
+        }
+
+        // For debugging only. Returns the string representation of the IPv6 address starting at
+        // position pos in the packet.
+        private String IPv6AddresstoString(int pos) {
+            try {
+                byte[] array = mPacket.array();
+                // Can't just call copyOfRange() and see if it throws, because if it reads past the
+                // end it pads with zeros instead of throwing.
+                if (pos < 0 || pos + 16 > array.length || pos + 16 < pos) {
+                    return "???";
+                }
+                byte[] addressBytes = Arrays.copyOfRange(array, pos, pos + 16);
+                InetAddress address = (Inet6Address) InetAddress.getByAddress(addressBytes);
+                return address.getHostAddress();
+            } catch (UnsupportedOperationException e) {
+                // array() failed. Cannot happen, mPacket is array-backed and read-write.
+                return "???";
+            } catch (ClassCastException|UnknownHostException e) {
+                // Cannot happen.
+                return "???";
+            }
+        }
+
+        // Can't be static because it's in a non-static inner class.
+        // TODO: Make this static once RA is its own class.
+        private void prefixOptionToString(StringBuffer sb, int offset) {
+            String prefix = IPv6AddresstoString(offset + 16);
+            int length = getUint8(mPacket, offset + 2);
+            long valid = getUint32(mPacket, offset + 4);
+            long preferred = getUint32(mPacket, offset + 8);
+            sb.append(String.format("%s/%d %ds/%ds ", prefix, length, valid, preferred));
+        }
+
+        private void rdnssOptionToString(StringBuffer sb, int offset) {
+            int optLen = getUint8(mPacket, offset + 1) * 8;
+            if (optLen < 24) return;  // Malformed or empty.
+            long lifetime = getUint32(mPacket, offset + 4);
+            int numServers = (optLen - 8) / 16;
+            sb.append("DNS ").append(lifetime).append("s");
+            for (int server = 0; server < numServers; server++) {
+                sb.append(" ").append(IPv6AddresstoString(offset + 8 + 16 * server));
+            }
+            sb.append(" ");
+        }
+
+        private void rioOptionToString(StringBuffer sb, int offset) {
+            int optLen = getUint8(mPacket, offset + 1) * 8;
+            if (optLen < 8 || optLen > 24) return;  // Malformed or empty.
+            int prefixLen = getUint8(mPacket, offset + 2);
+            long lifetime = getUint32(mPacket, offset + 4);
+
+            // This read is variable length because the prefix can be 0, 8 or 16 bytes long.
+            // We can't use any of the ByteBuffer#get methods here because they all start reading
+            // from the buffer's current position.
+            byte[] prefix = new byte[IPV6_ADDR_LEN];
+            System.arraycopy(mPacket.array(), offset + 8, prefix, 0, optLen - 8);
+            sb.append("RIO ").append(lifetime).append("s ");
+            try {
+                InetAddress address = (Inet6Address) InetAddress.getByAddress(prefix);
+                sb.append(address.getHostAddress());
+            } catch (UnknownHostException impossible) {
+                sb.append("???");
+            }
+            sb.append("/").append(prefixLen).append(" ");
+        }
+
+        public String toString() {
+            try {
+                StringBuffer sb = new StringBuffer();
+                sb.append(String.format("RA %s -> %s %ds ",
+                        IPv6AddresstoString(IPV6_SRC_ADDR_OFFSET),
+                        IPv6AddresstoString(IPV6_DEST_ADDR_OFFSET),
+                        getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET)));
+                for (int i: mPrefixOptionOffsets) {
+                    prefixOptionToString(sb, i);
+                }
+                for (int i: mRdnssOptionOffsets) {
+                    rdnssOptionToString(sb, i);
+                }
+                for (int i: mRioOptionOffsets) {
+                    rioOptionToString(sb, i);
+                }
+                return sb.toString();
+            } catch (BufferUnderflowException|IndexOutOfBoundsException e) {
+                return "<Malformed RA>";
+            }
+        }
+
+        /**
+         * Add a packet section that should be matched, starting from the current position.
+         * @param length the length of the section
+         */
+        private void addMatchSection(int length) {
+            // Don't generate JNEBS instruction for 0 bytes as they will fail the
+            // ASSERT_FORWARD_IN_PROGRAM(pc + cmp_imm - 1) check (where cmp_imm is
+            // the number of bytes to compare) and immediately pass the packet.
+            // The code does not attempt to generate such matches, but add a safety
+            // check to prevent doing so in the presence of bugs or malformed or
+            // truncated packets.
+            if (length == 0) return;
+            mPacketSections.add(
+                    new PacketSection(mPacket.position(), length, PacketSection.Type.MATCH, 0, 0));
+            mPacket.position(mPacket.position() + length);
+        }
+
+        /**
+         * Add a packet section that should be matched, starting from the current position.
+         * @param end the offset in the packet before which the section ends
+         */
+        private void addMatchUntil(int end) {
+            addMatchSection(end - mPacket.position());
+        }
+
+        /**
+         * Add a packet section that should be ignored, starting from the current position.
+         * @param length the length of the section in bytes
+         */
+        private void addIgnoreSection(int length) {
+            mPacketSections.add(
+                    new PacketSection(mPacket.position(), length, PacketSection.Type.IGNORE, 0, 0));
+            mPacket.position(mPacket.position() + length);
+        }
+
+        /**
+         * Add a packet section that represents a lifetime, starting from the current position.
+         * @param length the length of the section in bytes
+         * @param optionType the RA option containing this lifetime, or 0 for router lifetime
+         * @param lifetime the lifetime
+         */
+        private void addLifetimeSection(int length, int optionType, long lifetime) {
+            mPacketSections.add(
+                    new PacketSection(mPacket.position(), length, PacketSection.Type.LIFETIME,
+                            optionType, lifetime));
+            mPacket.position(mPacket.position() + length);
+        }
+
+        /**
+         * Adds packet sections for an RA option with a 4-byte lifetime 4 bytes into the option
+         * @param optionType the RA option that is being added
+         * @param optionLength the length of the option in bytes
+         */
+        private long add4ByteLifetimeOption(int optionType, int optionLength) {
+            addMatchSection(ICMP6_4_BYTE_LIFETIME_OFFSET);
+            final long lifetime = getUint32(mPacket, mPacket.position());
+            addLifetimeSection(ICMP6_4_BYTE_LIFETIME_LEN, optionType, lifetime);
+            addMatchSection(optionLength - ICMP6_4_BYTE_LIFETIME_OFFSET
+                    - ICMP6_4_BYTE_LIFETIME_LEN);
+            return lifetime;
+        }
+
+        // http://b/66928272 http://b/65056012
+        // DnsServerRepository ignores RDNSS servers with lifetimes that are too low. Ignore these
+        // lifetimes for the purpose of filter lifetime calculations.
+        private boolean shouldIgnoreLifetime(int optionType, long lifetime) {
+            return optionType == ICMP6_RDNSS_OPTION_TYPE
+                    && lifetime != 0 && lifetime < mMinRdnssLifetimeSec;
+        }
+
+        private boolean isRelevantLifetime(PacketSection section) {
+            return section.type == PacketSection.Type.LIFETIME
+                    && !shouldIgnoreLifetime(section.option, section.lifetime);
+        }
+
+        // Note that this parses RA and may throw InvalidRaException (from
+        // Buffer.position(int) or due to an invalid-length option) or IndexOutOfBoundsException
+        // (from ByteBuffer.get(int) ) if parsing encounters something non-compliant with
+        // specifications.
+        @VisibleForTesting
+        public Ra(byte[] packet, int length) throws InvalidRaException {
+            if (length < ICMP6_RA_OPTION_OFFSET) {
+                throw new InvalidRaException("Not an ICMP6 router advertisement: too short");
+            }
+
+            mPacket = ByteBuffer.wrap(Arrays.copyOf(packet, length));
+            mLastSeen = currentTimeSeconds();
+
+            // Check packet in case a packet arrives before we attach RA filter
+            // to our packet socket. b/29586253
+            if (getUint16(mPacket, ETH_ETHERTYPE_OFFSET) != ETH_P_IPV6 ||
+                    getUint8(mPacket, IPV6_NEXT_HEADER_OFFSET) != IPPROTO_ICMPV6 ||
+                    getUint8(mPacket, ICMP6_TYPE_OFFSET) != ICMPV6_ROUTER_ADVERTISEMENT) {
+                throw new InvalidRaException("Not an ICMP6 router advertisement");
+            }
+
+
+            RaEvent.Builder builder = new RaEvent.Builder();
+
+            // Ignore the flow label and low 4 bits of traffic class.
+            addMatchUntil(IPV6_FLOW_LABEL_OFFSET);
+            addIgnoreSection(IPV6_FLOW_LABEL_LEN);
+
+            // Ignore checksum.
+            addMatchUntil(ICMP6_RA_CHECKSUM_OFFSET);
+            addIgnoreSection(ICMP6_RA_CHECKSUM_LEN);
+
+            // Parse router lifetime
+            addMatchUntil(ICMP6_RA_ROUTER_LIFETIME_OFFSET);
+            final long routerLifetime = getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET);
+            addLifetimeSection(ICMP6_RA_ROUTER_LIFETIME_LEN, 0, routerLifetime);
+            builder.updateRouterLifetime(routerLifetime);
+
+            // Add remaining fields (reachable time and retransmission timer) to match section.
+            addMatchUntil(ICMP6_RA_OPTION_OFFSET);
+
+            while (mPacket.hasRemaining()) {
+                final int position = mPacket.position();
+                final int optionType = getUint8(mPacket, position);
+                final int optionLength = getUint8(mPacket, position + 1) * 8;
+                long lifetime;
+                switch (optionType) {
+                    case ICMP6_PREFIX_OPTION_TYPE:
+                        mPrefixOptionOffsets.add(position);
+
+                        // Parse valid lifetime
+                        addMatchSection(ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET);
+                        lifetime = getUint32(mPacket, mPacket.position());
+                        addLifetimeSection(ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN,
+                                ICMP6_PREFIX_OPTION_TYPE, lifetime);
+                        builder.updatePrefixValidLifetime(lifetime);
+
+                        // Parse preferred lifetime
+                        lifetime = getUint32(mPacket, mPacket.position());
+                        addLifetimeSection(ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN,
+                                ICMP6_PREFIX_OPTION_TYPE, lifetime);
+                        builder.updatePrefixPreferredLifetime(lifetime);
+
+                        addMatchSection(4);       // Reserved bytes
+                        addMatchSection(IPV6_ADDR_LEN);  // The prefix itself
+                        break;
+                    // These three options have the same lifetime offset and size, and
+                    // are processed with the same specialized add4ByteLifetimeOption:
+                    case ICMP6_RDNSS_OPTION_TYPE:
+                        mRdnssOptionOffsets.add(position);
+                        lifetime = add4ByteLifetimeOption(optionType, optionLength);
+                        builder.updateRdnssLifetime(lifetime);
+                        break;
+                    case ICMP6_ROUTE_INFO_OPTION_TYPE:
+                        mRioOptionOffsets.add(position);
+                        lifetime = add4ByteLifetimeOption(optionType, optionLength);
+                        builder.updateRouteInfoLifetime(lifetime);
+                        break;
+                    case ICMP6_DNSSL_OPTION_TYPE:
+                        lifetime = add4ByteLifetimeOption(optionType, optionLength);
+                        builder.updateDnsslLifetime(lifetime);
+                        break;
+                    default:
+                        // RFC4861 section 4.2 dictates we ignore unknown options for forwards
+                        // compatibility.
+                        mPacket.position(position + optionLength);
+                        break;
+                }
+                if (optionLength <= 0) {
+                    throw new InvalidRaException(String.format(
+                        "Invalid option length opt=%d len=%d", optionType, optionLength));
+                }
+            }
+            mMinLifetime = minLifetime();
+            mMetricsLog.log(builder.build());
+        }
+
+        // Considering only the MATCH sections, does {@code packet} match this RA?
+        boolean matches(byte[] packet, int length) {
+            if (length != mPacket.capacity()) return false;
+            byte[] referencePacket = mPacket.array();
+            for (PacketSection section : mPacketSections) {
+                if (section.type != PacketSection.Type.MATCH) continue;
+                for (int i = section.start; i < (section.start + section.length); i++) {
+                    if (packet[i] != referencePacket[i]) return false;
+                }
+            }
+            return true;
+        }
+
+        // What is the minimum of all lifetimes within {@code packet} in seconds?
+        // Precondition: matches(packet, length) already returned true.
+        long minLifetime() {
+            long minLifetime = Long.MAX_VALUE;
+            for (PacketSection section : mPacketSections) {
+                if (isRelevantLifetime(section)) {
+                    minLifetime = Math.min(minLifetime, section.lifetime);
+                }
+            }
+            return minLifetime;
+        }
+
+        // How many seconds does this RA's have to live, taking into account the fact
+        // that we might have seen it a while ago.
+        long currentLifetime() {
+            return mMinLifetime - (currentTimeSeconds() - mLastSeen);
+        }
+
+        boolean isExpired() {
+            // TODO: We may want to handle 0 lifetime RAs differently, if they are common. We'll
+            // have to calculate the filter lifetime specially as a fraction of 0 is still 0.
+            return currentLifetime() <= 0;
+        }
+
+        // Filter for a fraction of the lifetime and adjust for the age of the RA.
+        @GuardedBy("ApfFilter.this")
+        int filterLifetime() {
+            return (int) (mMinLifetime / FRACTION_OF_LIFETIME_TO_FILTER)
+                    - (int) (mProgramBaseTime - mLastSeen);
+        }
+
+        @GuardedBy("ApfFilter.this")
+        boolean shouldFilter() {
+            return filterLifetime() > 0;
+        }
+
+        // Append a filter for this RA to {@code gen}. Jump to DROP_LABEL if it should be dropped.
+        // Jump to the next filter if packet doesn't match this RA.
+        // Return Long.MAX_VALUE if we don't install any filter program for this RA. As the return
+        // value of this function is used to calculate the program min lifetime (which corresponds
+        // to the smallest generated filter lifetime). Returning Long.MAX_VALUE in the case no
+        // filter gets generated makes sure the program lifetime stays unaffected.
+        @GuardedBy("ApfFilter.this")
+        long generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+            String nextFilterLabel = "Ra" + getUniqueNumberLocked();
+            // Skip if packet is not the right size
+            gen.addLoadFromMemory(Register.R0, gen.PACKET_SIZE_MEMORY_SLOT);
+            gen.addJumpIfR0NotEquals(mPacket.capacity(), nextFilterLabel);
+            // Skip filter if expired
+            gen.addLoadFromMemory(Register.R0, gen.FILTER_AGE_MEMORY_SLOT);
+            gen.addJumpIfR0GreaterThan(filterLifetime(), nextFilterLabel);
+            for (PacketSection section : mPacketSections) {
+                // Generate code to match the packet bytes.
+                if (section.type == PacketSection.Type.MATCH) {
+                    gen.addLoadImmediate(Register.R0, section.start);
+                    gen.addJumpIfBytesNotEqual(Register.R0,
+                            Arrays.copyOfRange(mPacket.array(), section.start,
+                                    section.start + section.length),
+                            nextFilterLabel);
+                }
+
+                // Generate code to test the lifetimes haven't gone down too far.
+                // The packet is accepted if any non-ignored lifetime is lower than filterLifetime.
+                if (isRelevantLifetime(section)) {
+                    switch (section.length) {
+                        case 4: gen.addLoad32(Register.R0, section.start); break;
+                        case 2: gen.addLoad16(Register.R0, section.start); break;
+                        default:
+                            throw new IllegalStateException(
+                                    "bogus lifetime size " + section.length);
+                    }
+                    gen.addJumpIfR0LessThan(filterLifetime(), nextFilterLabel);
+                }
+            }
+            maybeSetupCounter(gen, Counter.DROPPED_RA);
+            gen.addJump(mCountAndDropLabel);
+            gen.defineLabel(nextFilterLabel);
+            return filterLifetime();
+        }
+    }
+
+    // TODO: Refactor these subclasses to avoid so much repetition.
+    private abstract static class KeepalivePacket {
+        // Note that the offset starts from IP header.
+        // These must be added ether header length when generating program.
+        static final int IP_HEADER_OFFSET = 0;
+        static final int IPV4_SRC_ADDR_OFFSET = IP_HEADER_OFFSET + 12;
+
+        // Append a filter for this keepalive ack to {@code gen}.
+        // Jump to drop if it matches the keepalive ack.
+        // Jump to the next filter if packet doesn't match the keepalive ack.
+        abstract void generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException;
+    }
+
+    // A class to hold NAT-T keepalive ack information.
+    private class NattKeepaliveResponse extends KeepalivePacket {
+        static final int UDP_LENGTH_OFFSET = 4;
+        static final int UDP_HEADER_LEN = 8;
+
+        protected class NattKeepaliveResponseData {
+            public final byte[] srcAddress;
+            public final int srcPort;
+            public final byte[] dstAddress;
+            public final int dstPort;
+
+            NattKeepaliveResponseData(final NattKeepalivePacketDataParcelable sentKeepalivePacket) {
+                srcAddress = sentKeepalivePacket.dstAddress;
+                srcPort = sentKeepalivePacket.dstPort;
+                dstAddress = sentKeepalivePacket.srcAddress;
+                dstPort = sentKeepalivePacket.srcPort;
+            }
+        }
+
+        protected final NattKeepaliveResponseData mPacket;
+        protected final byte[] mSrcDstAddr;
+        protected final byte[] mPortFingerprint;
+        // NAT-T keepalive packet
+        protected final byte[] mPayload = {(byte) 0xff};
+
+        NattKeepaliveResponse(final NattKeepalivePacketDataParcelable sentKeepalivePacket) {
+            mPacket = new NattKeepaliveResponseData(sentKeepalivePacket);
+            mSrcDstAddr = concatArrays(mPacket.srcAddress, mPacket.dstAddress);
+            mPortFingerprint = generatePortFingerprint(mPacket.srcPort, mPacket.dstPort);
+        }
+
+        byte[] generatePortFingerprint(int srcPort, int dstPort) {
+            final ByteBuffer fp = ByteBuffer.allocate(4);
+            fp.order(ByteOrder.BIG_ENDIAN);
+            fp.putShort((short) srcPort);
+            fp.putShort((short) dstPort);
+            return fp.array();
+        }
+
+        @Override
+        void generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+            final String nextFilterLabel = "natt_keepalive_filter" + getUniqueNumberLocked();
+
+            gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET);
+            gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel);
+
+            // A NAT-T keepalive packet contains 1 byte payload with the value 0xff
+            // Check payload length is 1
+            gen.addLoadFromMemory(Register.R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
+            gen.addAdd(UDP_HEADER_LEN);
+            gen.addSwap();
+            gen.addLoad16(Register.R0, IPV4_TOTAL_LENGTH_OFFSET);
+            gen.addNeg(Register.R1);
+            gen.addAddR1();
+            gen.addJumpIfR0NotEquals(1, nextFilterLabel);
+
+            // Check that the ports match
+            gen.addLoadFromMemory(Register.R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
+            gen.addAdd(ETH_HEADER_LEN);
+            gen.addJumpIfBytesNotEqual(Register.R0, mPortFingerprint, nextFilterLabel);
+
+            // Payload offset = R0 + UDP header length
+            gen.addAdd(UDP_HEADER_LEN);
+            gen.addJumpIfBytesNotEqual(Register.R0, mPayload, nextFilterLabel);
+
+            maybeSetupCounter(gen, Counter.DROPPED_IPV4_NATT_KEEPALIVE);
+            gen.addJump(mCountAndDropLabel);
+            gen.defineLabel(nextFilterLabel);
+        }
+
+        public String toString() {
+            try {
+                return String.format("%s -> %s",
+                        ConnectivityUtils.addressAndPortToString(
+                                InetAddress.getByAddress(mPacket.srcAddress), mPacket.srcPort),
+                        ConnectivityUtils.addressAndPortToString(
+                                InetAddress.getByAddress(mPacket.dstAddress), mPacket.dstPort));
+            } catch (UnknownHostException e) {
+                return "Unknown host";
+            }
+        }
+    }
+
+    // A class to hold TCP keepalive ack information.
+    private abstract static class TcpKeepaliveAck extends KeepalivePacket {
+        protected static class TcpKeepaliveAckData {
+            public final byte[] srcAddress;
+            public final int srcPort;
+            public final byte[] dstAddress;
+            public final int dstPort;
+            public final int seq;
+            public final int ack;
+
+            // Create the characteristics of the ack packet from the sent keepalive packet.
+            TcpKeepaliveAckData(final TcpKeepalivePacketDataParcelable sentKeepalivePacket) {
+                srcAddress = sentKeepalivePacket.dstAddress;
+                srcPort = sentKeepalivePacket.dstPort;
+                dstAddress = sentKeepalivePacket.srcAddress;
+                dstPort = sentKeepalivePacket.srcPort;
+                seq = sentKeepalivePacket.ack;
+                ack = sentKeepalivePacket.seq + 1;
+            }
+        }
+
+        protected final TcpKeepaliveAckData mPacket;
+        protected final byte[] mSrcDstAddr;
+        protected final byte[] mPortSeqAckFingerprint;
+
+        TcpKeepaliveAck(final TcpKeepaliveAckData packet, final byte[] srcDstAddr) {
+            mPacket = packet;
+            mSrcDstAddr = srcDstAddr;
+            mPortSeqAckFingerprint = generatePortSeqAckFingerprint(mPacket.srcPort,
+                    mPacket.dstPort, mPacket.seq, mPacket.ack);
+        }
+
+        static byte[] generatePortSeqAckFingerprint(int srcPort, int dstPort, int seq, int ack) {
+            final ByteBuffer fp = ByteBuffer.allocate(12);
+            fp.order(ByteOrder.BIG_ENDIAN);
+            fp.putShort((short) srcPort);
+            fp.putShort((short) dstPort);
+            fp.putInt(seq);
+            fp.putInt(ack);
+            return fp.array();
+        }
+
+        public String toString() {
+            try {
+                return String.format("%s -> %s , seq=%d, ack=%d",
+                        ConnectivityUtils.addressAndPortToString(
+                                InetAddress.getByAddress(mPacket.srcAddress), mPacket.srcPort),
+                        ConnectivityUtils.addressAndPortToString(
+                                InetAddress.getByAddress(mPacket.dstAddress), mPacket.dstPort),
+                        Integer.toUnsignedLong(mPacket.seq),
+                        Integer.toUnsignedLong(mPacket.ack));
+            } catch (UnknownHostException e) {
+                return "Unknown host";
+            }
+        }
+
+        // Append a filter for this keepalive ack to {@code gen}.
+        // Jump to drop if it matches the keepalive ack.
+        // Jump to the next filter if packet doesn't match the keepalive ack.
+        abstract void generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException;
+    }
+
+    private class TcpKeepaliveAckV4 extends TcpKeepaliveAck {
+
+        TcpKeepaliveAckV4(final TcpKeepalivePacketDataParcelable sentKeepalivePacket) {
+            this(new TcpKeepaliveAckData(sentKeepalivePacket));
+        }
+        TcpKeepaliveAckV4(final TcpKeepaliveAckData packet) {
+            super(packet, concatArrays(packet.srcAddress, packet.dstAddress) /* srcDstAddr */);
+        }
+
+        @Override
+        void generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+            final String nextFilterLabel = "keepalive_ack" + getUniqueNumberLocked();
+
+            gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET);
+            gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel);
+
+            // Skip to the next filter if it's not zero-sized :
+            // TCP_HEADER_SIZE + IPV4_HEADER_SIZE - ipv4_total_length == 0
+            // Load the IP header size into R1
+            gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
+            // Load the TCP header size into R0 (it's indexed by R1)
+            gen.addLoad8Indexed(Register.R0, ETH_HEADER_LEN + TCP_HEADER_SIZE_OFFSET);
+            // Size offset is in the top nibble, but it must be multiplied by 4, and the two
+            // top bits of the low nibble are guaranteed to be zeroes. Right-shift R0 by 2.
+            gen.addRightShift(2);
+            // R0 += R1 -> R0 contains TCP + IP headers length
+            gen.addAddR1();
+            // Load IPv4 total length
+            gen.addLoad16(Register.R1, IPV4_TOTAL_LENGTH_OFFSET);
+            gen.addNeg(Register.R0);
+            gen.addAddR1();
+            gen.addJumpIfR0NotEquals(0, nextFilterLabel);
+            // Add IPv4 header length
+            gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
+            gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN);
+            gen.addAddR1();
+            gen.addJumpIfBytesNotEqual(Register.R0, mPortSeqAckFingerprint, nextFilterLabel);
+
+            maybeSetupCounter(gen, Counter.DROPPED_IPV4_KEEPALIVE_ACK);
+            gen.addJump(mCountAndDropLabel);
+            gen.defineLabel(nextFilterLabel);
+        }
+    }
+
+    private class TcpKeepaliveAckV6 extends TcpKeepaliveAck {
+        TcpKeepaliveAckV6(final TcpKeepalivePacketDataParcelable sentKeepalivePacket) {
+            this(new TcpKeepaliveAckData(sentKeepalivePacket));
+        }
+        TcpKeepaliveAckV6(final TcpKeepaliveAckData packet) {
+            super(packet, concatArrays(packet.srcAddress, packet.dstAddress) /* srcDstAddr */);
+        }
+
+        @Override
+        void generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+            throw new UnsupportedOperationException("IPv6 TCP Keepalive is not supported yet");
+        }
+    }
+
+    // Maximum number of RAs to filter for.
+    private static final int MAX_RAS = 10;
+
+    @GuardedBy("this")
+    private ArrayList<Ra> mRas = new ArrayList<>();
+    @GuardedBy("this")
+    private SparseArray<KeepalivePacket> mKeepalivePackets = new SparseArray<>();
+    @GuardedBy("this")
+    private final List<String[]> mMdnsAllowList = new ArrayList<>();
+
+    // There is always some marginal benefit to updating the installed APF program when an RA is
+    // seen because we can extend the program's lifetime slightly, but there is some cost to
+    // updating the program, so don't bother unless the program is going to expire soon. This
+    // constant defines "soon" in seconds.
+    private static final long MAX_PROGRAM_LIFETIME_WORTH_REFRESHING = 30;
+    // We don't want to filter an RA for it's whole lifetime as it'll be expired by the time we ever
+    // see a refresh.  Using half the lifetime might be a good idea except for the fact that
+    // packets may be dropped, so let's use 6.
+    private static final int FRACTION_OF_LIFETIME_TO_FILTER = 6;
+
+    // The base time for this filter program. In seconds since Unix Epoch.
+    // This is the time when the APF program was generated. All filters in the program should use
+    // this base time as their current time for consistency purposes.
+    @GuardedBy("this")
+    private long mProgramBaseTime;
+    // When did we last install a filter program? In seconds since Unix Epoch.
+    @GuardedBy("this")
+    private long mLastTimeInstalledProgram;
+    // How long should the last installed filter program live for? In seconds.
+    @GuardedBy("this")
+    private long mLastInstalledProgramMinLifetime;
+    @GuardedBy("this")
+    private ApfProgramEvent.Builder mLastInstallEvent;
+
+    // For debugging only. The last program installed.
+    @GuardedBy("this")
+    private byte[] mLastInstalledProgram;
+
+    /**
+     * For debugging only. Contains the latest APF buffer snapshot captured from the firmware.
+     *
+     * A typical size for this buffer is 4KB. It is present only if the WiFi HAL supports
+     * IWifiStaIface#readApfPacketFilterData(), and the APF interpreter advertised support for
+     * the opcodes to access the data buffer (LDDW and STDW).
+     */
+    @GuardedBy("this") @Nullable
+    private byte[] mDataSnapshot;
+
+    // How many times the program was updated since we started.
+    @GuardedBy("this")
+    private int mNumProgramUpdates = 0;
+    // How many times the program was updated since we started for allowing multicast traffic.
+    @GuardedBy("this")
+    private int mNumProgramUpdatesAllowingMulticast = 0;
+
+    /**
+     * Generate filter code to process ARP packets. Execution of this code ends in either the
+     * DROP_LABEL or PASS_LABEL and does not fall off the end.
+     * Preconditions:
+     *  - Packet being filtered is ARP
+     */
+    @GuardedBy("this")
+    private void generateArpFilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+        // Here's a basic summary of what the ARP filter program does:
+        //
+        // if not ARP IPv4
+        //   pass
+        // if not ARP IPv4 reply or request
+        //   pass
+        // if ARP reply source ip is 0.0.0.0
+        //   drop
+        // if unicast ARP reply
+        //   pass
+        // if interface has no IPv4 address
+        //   if target ip is 0.0.0.0
+        //      drop
+        // else
+        //   if target ip is not the interface ip
+        //      drop
+        // pass
+
+        final String checkTargetIPv4 = "checkTargetIPv4";
+
+        // Pass if not ARP IPv4.
+        gen.addLoadImmediate(Register.R0, ARP_HEADER_OFFSET);
+        maybeSetupCounter(gen, Counter.PASSED_ARP_NON_IPV4);
+        gen.addJumpIfBytesNotEqual(Register.R0, ARP_IPV4_HEADER, mCountAndPassLabel);
+
+        // Pass if unknown ARP opcode.
+        gen.addLoad16(Register.R0, ARP_OPCODE_OFFSET);
+        gen.addJumpIfR0Equals(ARP_OPCODE_REQUEST, checkTargetIPv4); // Skip to unicast check
+        maybeSetupCounter(gen, Counter.PASSED_ARP_UNKNOWN);
+        gen.addJumpIfR0NotEquals(ARP_OPCODE_REPLY, mCountAndPassLabel);
+
+        // Drop if ARP reply source IP is 0.0.0.0
+        gen.addLoad32(Register.R0, ARP_SOURCE_IP_ADDRESS_OFFSET);
+        maybeSetupCounter(gen, Counter.DROPPED_ARP_REPLY_SPA_NO_HOST);
+        gen.addJumpIfR0Equals(IPV4_ANY_HOST_ADDRESS, mCountAndDropLabel);
+
+        // Pass if unicast reply.
+        gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
+        maybeSetupCounter(gen, Counter.PASSED_ARP_UNICAST_REPLY);
+        gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel);
+
+        // Either a unicast request, a unicast reply, or a broadcast reply.
+        gen.defineLabel(checkTargetIPv4);
+        if (mIPv4Address == null) {
+            // When there is no IPv4 address, drop GARP replies (b/29404209).
+            gen.addLoad32(Register.R0, ARP_TARGET_IP_ADDRESS_OFFSET);
+            maybeSetupCounter(gen, Counter.DROPPED_GARP_REPLY);
+            gen.addJumpIfR0Equals(IPV4_ANY_HOST_ADDRESS, mCountAndDropLabel);
+        } else {
+            // When there is an IPv4 address, drop unicast/broadcast requests
+            // and broadcast replies with a different target IPv4 address.
+            gen.addLoadImmediate(Register.R0, ARP_TARGET_IP_ADDRESS_OFFSET);
+            maybeSetupCounter(gen, Counter.DROPPED_ARP_OTHER_HOST);
+            gen.addJumpIfBytesNotEqual(Register.R0, mIPv4Address, mCountAndDropLabel);
+        }
+
+        maybeSetupCounter(gen, Counter.PASSED_ARP);
+        gen.addJump(mCountAndPassLabel);
+    }
+
+    /**
+     * Generate filter code to process IPv4 packets. Execution of this code ends in either the
+     * DROP_LABEL or PASS_LABEL and does not fall off the end.
+     * Preconditions:
+     *  - Packet being filtered is IPv4
+     */
+    @GuardedBy("this")
+    private void generateIPv4FilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+        // Here's a basic summary of what the IPv4 filter program does:
+        //
+        // if filtering multicast (i.e. multicast lock not held):
+        //   if it's DHCP destined to our MAC:
+        //     pass
+        //   if it's L2 broadcast:
+        //     drop
+        //   if it's IPv4 multicast:
+        //     drop
+        //   if it's IPv4 broadcast:
+        //     drop
+        // if keepalive ack
+        //   drop
+        // pass
+
+        if (mMulticastFilter) {
+            final String skipDhcpv4Filter = "skip_dhcp_v4_filter";
+
+            // Pass DHCP addressed to us.
+            // Check it's UDP.
+            gen.addLoad8(Register.R0, IPV4_PROTOCOL_OFFSET);
+            gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipDhcpv4Filter);
+            // Check it's not a fragment. This matches the BPF filter installed by the DHCP client.
+            gen.addLoad16(Register.R0, IPV4_FRAGMENT_OFFSET_OFFSET);
+            gen.addJumpIfR0AnyBitsSet(IPV4_FRAGMENT_OFFSET_MASK, skipDhcpv4Filter);
+            // Check it's addressed to DHCP client port.
+            gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
+            gen.addLoad16Indexed(Register.R0, UDP_DESTINATION_PORT_OFFSET);
+            gen.addJumpIfR0NotEquals(DHCP_CLIENT_PORT, skipDhcpv4Filter);
+            // Check it's DHCP to our MAC address.
+            gen.addLoadImmediate(Register.R0, DHCP_CLIENT_MAC_OFFSET);
+            // NOTE: Relies on R1 containing IPv4 header offset.
+            gen.addAddR1();
+            gen.addJumpIfBytesNotEqual(Register.R0, mHardwareAddress, skipDhcpv4Filter);
+            maybeSetupCounter(gen, Counter.PASSED_DHCP);
+            gen.addJump(mCountAndPassLabel);
+
+            // Drop all multicasts/broadcasts.
+            gen.defineLabel(skipDhcpv4Filter);
+
+            // If IPv4 destination address is in multicast range, drop.
+            gen.addLoad8(Register.R0, IPV4_DEST_ADDR_OFFSET);
+            gen.addAnd(0xf0);
+            maybeSetupCounter(gen, Counter.DROPPED_IPV4_MULTICAST);
+            gen.addJumpIfR0Equals(0xe0, mCountAndDropLabel);
+
+            // If IPv4 broadcast packet, drop regardless of L2 (b/30231088).
+            maybeSetupCounter(gen, Counter.DROPPED_IPV4_BROADCAST_ADDR);
+            gen.addLoad32(Register.R0, IPV4_DEST_ADDR_OFFSET);
+            gen.addJumpIfR0Equals(IPV4_BROADCAST_ADDRESS, mCountAndDropLabel);
+            if (mIPv4Address != null && mIPv4PrefixLength < 31) {
+                maybeSetupCounter(gen, Counter.DROPPED_IPV4_BROADCAST_NET);
+                int broadcastAddr = ipv4BroadcastAddress(mIPv4Address, mIPv4PrefixLength);
+                gen.addJumpIfR0Equals(broadcastAddr, mCountAndDropLabel);
+            }
+
+            // If any TCP keepalive filter matches, drop
+            generateV4KeepaliveFilters(gen);
+
+            // If any NAT-T keepalive filter matches, drop
+            generateV4NattKeepaliveFilters(gen);
+
+            // Otherwise, this is an IPv4 unicast, pass
+            // If L2 broadcast packet, drop.
+            // TODO: can we invert this condition to fall through to the common pass case below?
+            maybeSetupCounter(gen, Counter.PASSED_IPV4_UNICAST);
+            gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
+            gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel);
+            maybeSetupCounter(gen, Counter.DROPPED_IPV4_L2_BROADCAST);
+            gen.addJump(mCountAndDropLabel);
+        } else {
+            generateV4KeepaliveFilters(gen);
+            generateV4NattKeepaliveFilters(gen);
+        }
+
+        // Otherwise, pass
+        maybeSetupCounter(gen, Counter.PASSED_IPV4);
+        gen.addJump(mCountAndPassLabel);
+    }
+
+    private void generateKeepaliveFilters(ApfGenerator gen, Class<?> filterType, int proto,
+            int offset, String label) throws IllegalInstructionException {
+        final boolean haveKeepaliveResponses = CollectionUtils.any(mKeepalivePackets,
+                ack -> filterType.isInstance(ack));
+
+        // If no keepalive packets of this type
+        if (!haveKeepaliveResponses) return;
+
+        // If not the right proto, skip keepalive filters
+        gen.addLoad8(Register.R0, offset);
+        gen.addJumpIfR0NotEquals(proto, label);
+
+        // Drop Keepalive responses
+        for (int i = 0; i < mKeepalivePackets.size(); ++i) {
+            final KeepalivePacket response = mKeepalivePackets.valueAt(i);
+            if (filterType.isInstance(response)) response.generateFilterLocked(gen);
+        }
+
+        gen.defineLabel(label);
+    }
+
+    private void generateV4KeepaliveFilters(ApfGenerator gen) throws IllegalInstructionException {
+        generateKeepaliveFilters(gen, TcpKeepaliveAckV4.class, IPPROTO_TCP, IPV4_PROTOCOL_OFFSET,
+                "skip_v4_keepalive_filter");
+    }
+
+    private void generateV4NattKeepaliveFilters(ApfGenerator gen)
+            throws IllegalInstructionException {
+        generateKeepaliveFilters(gen, NattKeepaliveResponse.class,
+                IPPROTO_UDP, IPV4_PROTOCOL_OFFSET, "skip_v4_nattkeepalive_filter");
+    }
+
+    /**
+     * Generate filter code to process IPv6 packets. Execution of this code ends in either the
+     * DROP_LABEL or PASS_LABEL, or falls off the end for ICMPv6 packets.
+     * Preconditions:
+     *  - Packet being filtered is IPv6
+     */
+    @GuardedBy("this")
+    private void generateIPv6FilterLocked(ApfGenerator gen) throws IllegalInstructionException {
+        // Here's a basic summary of what the IPv6 filter program does:
+        //
+        // if there is a hop-by-hop option present (e.g. MLD query)
+        //   pass
+        // if we're dropping multicast
+        //   if it's not IPCMv6 or it's ICMPv6 but we're in doze mode:
+        //     if it's multicast:
+        //       drop
+        //     pass
+        // if it's ICMPv6 RS to any:
+        //   drop
+        // if it's ICMPv6 NA to anything in ff02::/120
+        //   drop
+        // if keepalive ack
+        //   drop
+
+        gen.addLoad8(Register.R0, IPV6_NEXT_HEADER_OFFSET);
+
+        // MLD packets set the router-alert hop-by-hop option.
+        // TODO: be smarter about not blindly passing every packet with HBH options.
+        gen.addJumpIfR0Equals(IPPROTO_HOPOPTS, mCountAndPassLabel);
+
+        // Drop multicast if the multicast filter is enabled.
+        if (mMulticastFilter) {
+            final String skipIPv6MulticastFilterLabel = "skipIPv6MulticastFilter";
+            final String dropAllIPv6MulticastsLabel = "dropAllIPv6Multicast";
+
+            // While in doze mode, drop ICMPv6 multicast pings, let the others pass.
+            // While awake, let all ICMPv6 multicasts through.
+            if (mInDozeMode) {
+                // Not ICMPv6? -> Proceed to multicast filtering
+                gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, dropAllIPv6MulticastsLabel);
+
+                // ICMPv6 but not ECHO? -> Skip the multicast filter.
+                // (ICMPv6 ECHO requests will go through the multicast filter below).
+                gen.addLoad8(Register.R0, ICMP6_TYPE_OFFSET);
+                gen.addJumpIfR0NotEquals(ICMPV6_ECHO_REQUEST_TYPE, skipIPv6MulticastFilterLabel);
+            } else {
+                gen.addJumpIfR0Equals(IPPROTO_ICMPV6, skipIPv6MulticastFilterLabel);
+            }
+
+            // Drop all other packets sent to ff00::/8 (multicast prefix).
+            gen.defineLabel(dropAllIPv6MulticastsLabel);
+            maybeSetupCounter(gen, Counter.DROPPED_IPV6_NON_ICMP_MULTICAST);
+            gen.addLoad8(Register.R0, IPV6_DEST_ADDR_OFFSET);
+            gen.addJumpIfR0Equals(0xff, mCountAndDropLabel);
+            // If any keepalive filter matches, drop
+            generateV6KeepaliveFilters(gen);
+            // Not multicast. Pass.
+            maybeSetupCounter(gen, Counter.PASSED_IPV6_UNICAST_NON_ICMP);
+            gen.addJump(mCountAndPassLabel);
+            gen.defineLabel(skipIPv6MulticastFilterLabel);
+        } else {
+            generateV6KeepaliveFilters(gen);
+            // If not ICMPv6, pass.
+            maybeSetupCounter(gen, Counter.PASSED_IPV6_NON_ICMP);
+            gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, mCountAndPassLabel);
+        }
+
+        // If we got this far, the packet is ICMPv6.  Drop some specific types.
+
+        // Add unsolicited multicast neighbor announcements filter
+        String skipUnsolicitedMulticastNALabel = "skipUnsolicitedMulticastNA";
+        gen.addLoad8(Register.R0, ICMP6_TYPE_OFFSET);
+        // Drop all router solicitations (b/32833400)
+        maybeSetupCounter(gen, Counter.DROPPED_IPV6_ROUTER_SOLICITATION);
+        gen.addJumpIfR0Equals(ICMPV6_ROUTER_SOLICITATION, mCountAndDropLabel);
+        // If not neighbor announcements, skip filter.
+        gen.addJumpIfR0NotEquals(ICMPV6_NEIGHBOR_ADVERTISEMENT, skipUnsolicitedMulticastNALabel);
+        // Drop all multicast NA to ff02::/120.
+        // This is a way to cover ff02::1 and ff02::2 with a single JNEBS.
+        // TODO: Drop only if they don't contain the address of on-link neighbours.
+        final byte[] unsolicitedNaDropPrefix = Arrays.copyOf(IPV6_ALL_NODES_ADDRESS, 15);
+        gen.addLoadImmediate(Register.R0, IPV6_DEST_ADDR_OFFSET);
+        gen.addJumpIfBytesNotEqual(Register.R0, unsolicitedNaDropPrefix,
+                skipUnsolicitedMulticastNALabel);
+
+        maybeSetupCounter(gen, Counter.DROPPED_IPV6_MULTICAST_NA);
+        gen.addJump(mCountAndDropLabel);
+        gen.defineLabel(skipUnsolicitedMulticastNALabel);
+    }
+
+    /** Encodes qname in TLV pattern. */
+    @VisibleForTesting
+    public static byte[] encodeQname(String[] labels) {
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        for (String label : labels) {
+            byte[] labelBytes = label.getBytes(StandardCharsets.UTF_8);
+            out.write(labelBytes.length);
+            out.write(labelBytes, 0, labelBytes.length);
+        }
+        out.write(0);
+        return out.toByteArray();
+    }
+
+    /**
+     * Generate filter code to process mDNS packets. Execution of this code ends in * DROP_LABEL
+     * or PASS_LABEL if the packet is mDNS packets. Otherwise, skip this check.
+     */
+    @GuardedBy("this")
+    private void generateMdnsFilterLocked(ApfGenerator gen)
+            throws IllegalInstructionException {
+        final String skipMdnsv4Filter = "skip_mdns_v4_filter";
+        final String skipMdnsFilter = "skip_mdns_filter";
+        final String checkMdnsUdpPort = "check_mdns_udp_port";
+        final String mDnsAcceptPacket = "mdns_accept_packet";
+        final String mDnsDropPacket = "mdns_drop_packet";
+
+        // Only turn on the filter if multicast filter is on and the qname allowlist is non-empty.
+        if (!mMulticastFilter || mMdnsAllowList.isEmpty()) {
+            return;
+        }
+
+        // Here's a basic summary of what the mDNS filter program does:
+        //
+        // if it is a multicast mDNS packet
+        //    if QDCOUNT != 1
+        //       pass
+        //    else if the QNAME is in the allowlist
+        //       pass
+        //    else:
+        //       drop
+        //
+        // A packet is considered as a multicast mDNS packet if it matches all the following
+        // conditions
+        //   1. its destination MAC address matches 01:00:5E:00:00:FB or 33:33:00:00:00:FB, for
+        //   v4 and v6 respectively.
+        //   2. it is an IPv4/IPv6 packet
+        //   3. it is a UDP packet with port 5353
+
+        // Check it's L2 mDNS multicast address.
+        gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
+        gen.addJumpIfBytesNotEqual(Register.R0, ETH_MULTICAST_MDNS_V4_MAC_ADDRESS,
+                skipMdnsv4Filter);
+
+        // Checks it's IPv4.
+        gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET);
+        gen.addJumpIfR0NotEquals(ETH_P_IP, skipMdnsFilter);
+
+        // Checks it's UDP.
+        gen.addLoad8(Register.R0, IPV4_PROTOCOL_OFFSET);
+        gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipMdnsFilter);
+        // Set R1 to IPv4 header.
+        gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
+        gen.addJump(checkMdnsUdpPort);
+
+        gen.defineLabel(skipMdnsv4Filter);
+
+        // Checks it's L2 mDNS multicast address.
+        // Relies on R0 containing the ethernet destination mac address offset.
+        gen.addJumpIfBytesNotEqual(Register.R0, ETH_MULTICAST_MDNS_V6_MAC_ADDRESS,
+                skipMdnsFilter);
+
+        // Checks it's IPv6.
+        gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET);
+        gen.addJumpIfR0NotEquals(ETH_P_IPV6, skipMdnsFilter);
+
+        // Checks it's UDP.
+        gen.addLoad8(Register.R0, IPV6_NEXT_HEADER_OFFSET);
+        gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipMdnsFilter);
+
+        // Set R1 to IPv6 header.
+        gen.addLoadImmediate(Register.R1, IPV6_HEADER_LEN);
+
+        // Checks it's mDNS UDP port
+        gen.defineLabel(checkMdnsUdpPort);
+        gen.addLoad16Indexed(Register.R0, UDP_DESTINATION_PORT_OFFSET);
+        gen.addJumpIfR0NotEquals(MDNS_PORT, skipMdnsFilter);
+
+        gen.addLoad16Indexed(Register.R0, MDNS_QDCOUNT_OFFSET);
+        // If QDCOUNT != 1, pass the packet
+        gen.addJumpIfR0NotEquals(1, mDnsAcceptPacket);
+
+        // If QDCOUNT == 1, matches the QNAME with allowlist.
+        // Load offset for the first QNAME.
+        gen.addLoadImmediate(Register.R0, MDNS_QNAME_OFFSET);
+        gen.addAddR1();
+
+        // Check first QNAME against allowlist
+        for (int i = 0; i < mMdnsAllowList.size(); ++i) {
+            final String mDnsNextAllowedQnameCheck = "mdns_next_allowed_qname_check" + i;
+            final byte[] encodedQname = encodeQname(mMdnsAllowList.get(i));
+            gen.addJumpIfBytesNotEqual(Register.R0, encodedQname, mDnsNextAllowedQnameCheck);
+            // QNAME matched
+            gen.addJump(mDnsAcceptPacket);
+            // QNAME not matched
+            gen.defineLabel(mDnsNextAllowedQnameCheck);
+        }
+        // If QNAME doesn't match any entries in allowlist, drop the packet.
+        gen.defineLabel(mDnsDropPacket);
+        maybeSetupCounter(gen, Counter.DROPPED_MDNS);
+        gen.addJump(mCountAndDropLabel);
+
+        gen.defineLabel(mDnsAcceptPacket);
+        maybeSetupCounter(gen, Counter.PASSED_MDNS);
+        gen.addJump(mCountAndPassLabel);
+
+
+        gen.defineLabel(skipMdnsFilter);
+    }
+
+
+    private void generateV6KeepaliveFilters(ApfGenerator gen) throws IllegalInstructionException {
+        generateKeepaliveFilters(gen, TcpKeepaliveAckV6.class, IPPROTO_TCP, IPV6_NEXT_HEADER_OFFSET,
+                "skip_v6_keepalive_filter");
+    }
+
+    /**
+     * Begin generating an APF program to:
+     * <ul>
+     * <li>Drop/Pass 802.3 frames (based on policy)
+     * <li>Drop packets with EtherType within the Black List
+     * <li>Drop ARP requests not for us, if mIPv4Address is set,
+     * <li>Drop IPv4 broadcast packets, except DHCP destined to our MAC,
+     * <li>Drop IPv4 multicast packets, if mMulticastFilter,
+     * <li>Pass all other IPv4 packets,
+     * <li>Drop all broadcast non-IP non-ARP packets.
+     * <li>Pass all non-ICMPv6 IPv6 packets,
+     * <li>Pass all non-IPv4 and non-IPv6 packets,
+     * <li>Drop IPv6 ICMPv6 NAs to anything in ff02::/120.
+     * <li>Drop IPv6 ICMPv6 RSs.
+     * <li>Filter IPv4 packets (see generateIPv4FilterLocked())
+     * <li>Filter IPv6 packets (see generateIPv6FilterLocked())
+     * <li>Let execution continue off the end of the program for IPv6 ICMPv6 packets. This allows
+     *     insertion of RA filters here, or if there aren't any, just passes the packets.
+     * </ul>
+     */
+    @GuardedBy("this")
+    private ApfGenerator emitPrologueLocked() throws IllegalInstructionException {
+        // This is guaranteed to succeed because of the check in maybeCreate.
+        ApfGenerator gen = new ApfGenerator(mApfCapabilities.apfVersionSupported);
+
+        if (mApfCapabilities.hasDataAccess()) {
+            // Increment TOTAL_PACKETS
+            maybeSetupCounter(gen, Counter.TOTAL_PACKETS);
+            gen.addLoadData(Register.R0, 0);  // load counter
+            gen.addAdd(1);
+            gen.addStoreData(Register.R0, 0);  // write-back counter
+        }
+
+        // Here's a basic summary of what the initial program does:
+        //
+        // if it's a 802.3 Frame (ethtype < 0x0600):
+        //    drop or pass based on configurations
+        // if it has a ether-type that belongs to the black list
+        //    drop
+        // if it's ARP:
+        //   insert ARP filter to drop or pass these appropriately
+        // if it's IPv4:
+        //   insert IPv4 filter to drop or pass these appropriately
+        // if it's not IPv6:
+        //   if it's broadcast:
+        //     drop
+        //   pass
+        // insert IPv6 filter to drop, pass, or fall off the end for ICMPv6 packets
+
+        gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET);
+
+        if (mDrop802_3Frames) {
+            // drop 802.3 frames (ethtype < 0x0600)
+            maybeSetupCounter(gen, Counter.DROPPED_802_3_FRAME);
+            gen.addJumpIfR0LessThan(ETH_TYPE_MIN, mCountAndDropLabel);
+        }
+
+        // Handle ether-type black list
+        maybeSetupCounter(gen, Counter.DROPPED_ETHERTYPE_BLACKLISTED);
+        for (int p : mEthTypeBlackList) {
+            gen.addJumpIfR0Equals(p, mCountAndDropLabel);
+        }
+
+        // Add ARP filters:
+        String skipArpFiltersLabel = "skipArpFilters";
+        gen.addJumpIfR0NotEquals(ETH_P_ARP, skipArpFiltersLabel);
+        generateArpFilterLocked(gen);
+        gen.defineLabel(skipArpFiltersLabel);
+
+        // Add mDNS filter:
+        generateMdnsFilterLocked(gen);
+        gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET);
+
+        // Add IPv4 filters:
+        String skipIPv4FiltersLabel = "skipIPv4Filters";
+        gen.addJumpIfR0NotEquals(ETH_P_IP, skipIPv4FiltersLabel);
+        generateIPv4FilterLocked(gen);
+        gen.defineLabel(skipIPv4FiltersLabel);
+
+        // Check for IPv6:
+        // NOTE: Relies on R0 containing ethertype. This is safe because if we got here, we did
+        // not execute the IPv4 filter, since this filter do not fall through, but either drop or
+        // pass.
+        String ipv6FilterLabel = "IPv6Filters";
+        gen.addJumpIfR0Equals(ETH_P_IPV6, ipv6FilterLabel);
+
+        // Drop non-IP non-ARP broadcasts, pass the rest
+        gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
+        maybeSetupCounter(gen, Counter.PASSED_NON_IP_UNICAST);
+        gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel);
+        maybeSetupCounter(gen, Counter.DROPPED_ETH_BROADCAST);
+        gen.addJump(mCountAndDropLabel);
+
+        // Add IPv6 filters:
+        gen.defineLabel(ipv6FilterLabel);
+        generateIPv6FilterLocked(gen);
+        return gen;
+    }
+
+    /**
+     * Append packet counting epilogue to the APF program.
+     *
+     * Currently, the epilogue consists of two trampolines which count passed and dropped packets
+     * before jumping to the actual PASS and DROP labels.
+     */
+    @GuardedBy("this")
+    private void emitEpilogue(ApfGenerator gen) throws IllegalInstructionException {
+        // If APFv4 is unsupported, no epilogue is necessary: if execution reached this far, it
+        // will just fall-through to the PASS label.
+        if (!mApfCapabilities.hasDataAccess()) return;
+
+        // Execution will reach the bottom of the program if none of the filters match,
+        // which will pass the packet to the application processor.
+        maybeSetupCounter(gen, Counter.PASSED_IPV6_ICMP);
+
+        // Append the count & pass trampoline, which increments the counter at the data address
+        // pointed to by R1, then jumps to the pass label. This saves a few bytes over inserting
+        // the entire sequence inline for every counter.
+        gen.defineLabel(mCountAndPassLabel);
+        gen.addLoadData(Register.R0, 0);   // R0 = *(R1 + 0)
+        gen.addAdd(1);                     // R0++
+        gen.addStoreData(Register.R0, 0);  // *(R1 + 0) = R0
+        gen.addJump(gen.PASS_LABEL);
+
+        // Same as above for the count & drop trampoline.
+        gen.defineLabel(mCountAndDropLabel);
+        gen.addLoadData(Register.R0, 0);   // R0 = *(R1 + 0)
+        gen.addAdd(1);                     // R0++
+        gen.addStoreData(Register.R0, 0);  // *(R1 + 0) = R0
+        gen.addJump(gen.DROP_LABEL);
+    }
+
+    /**
+     * Generate and install a new filter program.
+     */
+    @GuardedBy("this")
+    @VisibleForTesting
+    public void installNewProgramLocked() {
+        purgeExpiredRasLocked();
+        ArrayList<Ra> rasToFilter = new ArrayList<>();
+        final byte[] program;
+        long programMinLifetime = Long.MAX_VALUE;
+        long maximumApfProgramSize = mApfCapabilities.maximumApfProgramSize;
+        if (mApfCapabilities.hasDataAccess()) {
+            // Reserve space for the counters.
+            maximumApfProgramSize -= Counter.totalSize();
+        }
+
+        mProgramBaseTime = currentTimeSeconds();
+        try {
+            // Step 1: Determine how many RA filters we can fit in the program.
+            ApfGenerator gen = emitPrologueLocked();
+
+            // The epilogue normally goes after the RA filters, but add it early to include its
+            // length when estimating the total.
+            emitEpilogue(gen);
+
+            // Can't fit the program even without any RA filters?
+            if (gen.programLengthOverEstimate() > maximumApfProgramSize) {
+                Log.e(TAG, "Program exceeds maximum size " + maximumApfProgramSize);
+                return;
+            }
+
+            for (Ra ra : mRas) {
+                if (!ra.shouldFilter()) continue;
+                ra.generateFilterLocked(gen);
+                // Stop if we get too big.
+                if (gen.programLengthOverEstimate() > maximumApfProgramSize) {
+                    if (VDBG) Log.d(TAG, "Past maximum program size, skipping RAs");
+                    break;
+                }
+
+                rasToFilter.add(ra);
+            }
+
+            // Step 2: Actually generate the program
+            gen = emitPrologueLocked();
+            for (Ra ra : rasToFilter) {
+                programMinLifetime = Math.min(programMinLifetime, ra.generateFilterLocked(gen));
+            }
+            emitEpilogue(gen);
+            program = gen.generate();
+        } catch (IllegalInstructionException|IllegalStateException e) {
+            Log.e(TAG, "Failed to generate APF program.", e);
+            return;
+        }
+        mIpClientCallback.installPacketFilter(program);
+        mLastTimeInstalledProgram = mProgramBaseTime;
+        mLastInstalledProgramMinLifetime = programMinLifetime;
+        mLastInstalledProgram = program;
+        mNumProgramUpdates++;
+
+        if (VDBG) {
+            hexDump("Installing filter: ", program, program.length);
+        }
+        logApfProgramEventLocked(mProgramBaseTime);
+        mLastInstallEvent = new ApfProgramEvent.Builder()
+                .setLifetime(programMinLifetime)
+                .setFilteredRas(rasToFilter.size())
+                .setCurrentRas(mRas.size())
+                .setProgramLength(program.length)
+                .setFlags(mIPv4Address != null, mMulticastFilter);
+    }
+
+    @GuardedBy("this")
+    private void logApfProgramEventLocked(long now) {
+        if (mLastInstallEvent == null) {
+            return;
+        }
+        ApfProgramEvent.Builder ev = mLastInstallEvent;
+        mLastInstallEvent = null;
+        final long actualLifetime = now - mLastTimeInstalledProgram;
+        ev.setActualLifetime(actualLifetime);
+        if (actualLifetime < APF_PROGRAM_EVENT_LIFETIME_THRESHOLD) {
+            return;
+        }
+        mMetricsLog.log(ev.build());
+    }
+
+    /**
+     * Returns {@code true} if a new program should be installed because the current one dies soon.
+     */
+    private boolean shouldInstallnewProgram() {
+        long expiry = mLastTimeInstalledProgram + mLastInstalledProgramMinLifetime;
+        return expiry < currentTimeSeconds() + MAX_PROGRAM_LIFETIME_WORTH_REFRESHING;
+    }
+
+    private void hexDump(String msg, byte[] packet, int length) {
+        log(msg + HexDump.toHexString(packet, 0, length, false /* lowercase */));
+    }
+
+    @GuardedBy("this")
+    private void purgeExpiredRasLocked() {
+        for (int i = 0; i < mRas.size();) {
+            if (mRas.get(i).isExpired()) {
+                log("Expiring " + mRas.get(i));
+                mRas.remove(i);
+            } else {
+                i++;
+            }
+        }
+    }
+
+    /**
+     * Process an RA packet, updating the list of known RAs and installing a new APF program
+     * if the current APF program should be updated.
+     * @return a ProcessRaResult enum describing what action was performed.
+     */
+    @VisibleForTesting
+    public synchronized ProcessRaResult processRa(byte[] packet, int length) {
+        if (VDBG) hexDump("Read packet = ", packet, length);
+
+        // Have we seen this RA before?
+        for (int i = 0; i < mRas.size(); i++) {
+            Ra ra = mRas.get(i);
+            if (ra.matches(packet, length)) {
+                if (VDBG) log("matched RA " + ra);
+                // Update lifetimes.
+                ra.mLastSeen = currentTimeSeconds();
+                ra.seenCount++;
+
+                // Keep mRas in LRU order so as to prioritize generating filters for recently seen
+                // RAs. LRU prioritizes this because RA filters are generated in order from mRas
+                // until the filter program exceeds the maximum filter program size allowed by the
+                // chipset, so RAs appearing earlier in mRas are more likely to make it into the
+                // filter program.
+                // TODO: consider sorting the RAs in order of increasing expiry time as well.
+                // Swap to front of array.
+                mRas.add(0, mRas.remove(i));
+
+                // If the current program doesn't expire for a while, don't update.
+                if (shouldInstallnewProgram()) {
+                    installNewProgramLocked();
+                    return ProcessRaResult.UPDATE_EXPIRY;
+                }
+                return ProcessRaResult.MATCH;
+            }
+        }
+        purgeExpiredRasLocked();
+        // TODO: figure out how to proceed when we've received more than MAX_RAS RAs.
+        if (mRas.size() >= MAX_RAS) {
+            return ProcessRaResult.DROPPED;
+        }
+        final Ra ra;
+        try {
+            ra = new Ra(packet, length);
+        } catch (Exception e) {
+            Log.e(TAG, "Error parsing RA", e);
+            return ProcessRaResult.PARSE_ERROR;
+        }
+        // Ignore 0 lifetime RAs.
+        if (ra.isExpired()) {
+            return ProcessRaResult.ZERO_LIFETIME;
+        }
+        log("Adding " + ra);
+        mRas.add(ra);
+        installNewProgramLocked();
+        return ProcessRaResult.UPDATE_NEW_RA;
+    }
+
+    /**
+     * Create an {@link LegacyApfFilter} if {@code apfCapabilities} indicates support for packet
+     * filtering using APF programs.
+     */
+    public static LegacyApfFilter maybeCreate(Context context, ApfFilter.ApfConfiguration config,
+            InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback) {
+        if (context == null || config == null || ifParams == null) return null;
+        ApfCapabilities apfCapabilities =  config.apfCapabilities;
+        if (apfCapabilities == null) return null;
+        if (apfCapabilities.apfVersionSupported == 0) return null;
+        if (apfCapabilities.maximumApfProgramSize < 512) {
+            Log.e(TAG, "Unacceptably small APF limit: " + apfCapabilities.maximumApfProgramSize);
+            return null;
+        }
+        // For now only support generating programs for Ethernet frames. If this restriction is
+        // lifted:
+        //   1. the program generator will need its offsets adjusted.
+        //   2. the packet filter attached to our packet socket will need its offset adjusted.
+        if (apfCapabilities.apfPacketFormat != ARPHRD_ETHER) return null;
+        if (!ApfGenerator.supportsVersion(apfCapabilities.apfVersionSupported)) {
+            Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported);
+            return null;
+        }
+
+        return new LegacyApfFilter(context, config, ifParams, ipClientCallback, new IpConnectivityLog());
+    }
+
+    public synchronized void shutdown() {
+        if (mReceiveThread != null) {
+            log("shutting down");
+            mReceiveThread.halt();  // Also closes socket.
+            mReceiveThread = null;
+        }
+        mRas.clear();
+        mContext.unregisterReceiver(mDeviceIdleReceiver);
+    }
+
+    public synchronized void setMulticastFilter(boolean isEnabled) {
+        if (mMulticastFilter == isEnabled) return;
+        mMulticastFilter = isEnabled;
+        if (!isEnabled) {
+            mNumProgramUpdatesAllowingMulticast++;
+        }
+        installNewProgramLocked();
+    }
+
+    /** Adds qname to the mDNS allowlist */
+    public synchronized void addToMdnsAllowList(String[] labels) {
+        mMdnsAllowList.add(labels);
+        if (mMulticastFilter) {
+            installNewProgramLocked();
+        }
+    }
+
+    /** Removes qname from the mDNS allowlist */
+    public synchronized void removeFromAllowList(String[] labels) {
+        mMdnsAllowList.removeIf(e -> Arrays.equals(labels, e));
+        if (mMulticastFilter) {
+            installNewProgramLocked();
+        }
+    }
+
+    @VisibleForTesting
+    public synchronized void setDozeMode(boolean isEnabled) {
+        if (mInDozeMode == isEnabled) return;
+        mInDozeMode = isEnabled;
+        installNewProgramLocked();
+    }
+
+    /** Find the single IPv4 LinkAddress if there is one, otherwise return null. */
+    private static LinkAddress findIPv4LinkAddress(LinkProperties lp) {
+        LinkAddress ipv4Address = null;
+        for (LinkAddress address : lp.getLinkAddresses()) {
+            if (!(address.getAddress() instanceof Inet4Address)) {
+                continue;
+            }
+            if (ipv4Address != null && !ipv4Address.isSameAddressAs(address)) {
+                // More than one IPv4 address, abort.
+                return null;
+            }
+            ipv4Address = address;
+        }
+        return ipv4Address;
+    }
+
+    public synchronized void setLinkProperties(LinkProperties lp) {
+        // NOTE: Do not keep a copy of LinkProperties as it would further duplicate state.
+        final LinkAddress ipv4Address = findIPv4LinkAddress(lp);
+        final byte[] addr = (ipv4Address != null) ? ipv4Address.getAddress().getAddress() : null;
+        final int prefix = (ipv4Address != null) ? ipv4Address.getPrefixLength() : 0;
+        if ((prefix == mIPv4PrefixLength) && Arrays.equals(addr, mIPv4Address)) {
+            return;
+        }
+        mIPv4Address = addr;
+        mIPv4PrefixLength = prefix;
+        installNewProgramLocked();
+    }
+
+    /**
+     * Add TCP keepalive ack packet filter.
+     * This will add a filter to drop acks to the keepalive packet passed as an argument.
+     *
+     * @param slot The index used to access the filter.
+     * @param sentKeepalivePacket The attributes of the sent keepalive packet.
+     */
+    public synchronized void addTcpKeepalivePacketFilter(final int slot,
+            final TcpKeepalivePacketDataParcelable sentKeepalivePacket) {
+        log("Adding keepalive ack(" + slot + ")");
+        if (null != mKeepalivePackets.get(slot)) {
+            throw new IllegalArgumentException("Keepalive slot " + slot + " is occupied");
+        }
+        final int ipVersion = sentKeepalivePacket.srcAddress.length == 4 ? 4 : 6;
+        mKeepalivePackets.put(slot, (ipVersion == 4)
+                ? new TcpKeepaliveAckV4(sentKeepalivePacket)
+                : new TcpKeepaliveAckV6(sentKeepalivePacket));
+        installNewProgramLocked();
+    }
+
+    /**
+     * Add NAT-T keepalive packet filter.
+     * This will add a filter to drop NAT-T keepalive packet which is passed as an argument.
+     *
+     * @param slot The index used to access the filter.
+     * @param sentKeepalivePacket The attributes of the sent keepalive packet.
+     */
+    public synchronized void addNattKeepalivePacketFilter(final int slot,
+            final NattKeepalivePacketDataParcelable sentKeepalivePacket) {
+        log("Adding NAT-T keepalive packet(" + slot + ")");
+        if (null != mKeepalivePackets.get(slot)) {
+            throw new IllegalArgumentException("NAT-T Keepalive slot " + slot + " is occupied");
+        }
+
+        // TODO : update ApfFilter to support dropping v6 keepalives
+        if (sentKeepalivePacket.srcAddress.length != 4) {
+            return;
+        }
+
+        mKeepalivePackets.put(slot, new NattKeepaliveResponse(sentKeepalivePacket));
+        installNewProgramLocked();
+    }
+
+    /**
+     * Remove keepalive packet filter.
+     *
+     * @param slot The index used to access the filter.
+     */
+    public synchronized void removeKeepalivePacketFilter(int slot) {
+        log("Removing keepalive packet(" + slot + ")");
+        mKeepalivePackets.remove(slot);
+        installNewProgramLocked();
+    }
+
+    static public long counterValue(byte[] data, Counter counter)
+            throws ArrayIndexOutOfBoundsException {
+        // Follow the same wrap-around addressing scheme of the interpreter.
+        int offset = counter.offset();
+        if (offset < 0) {
+            offset = data.length + offset;
+        }
+
+        // Decode 32bit big-endian integer into a long so we can count up beyond 2^31.
+        long value = 0;
+        for (int i = 0; i < 4; i++) {
+            value = value << 8 | (data[offset] & 0xFF);
+            offset++;
+        }
+        return value;
+    }
+
+    public synchronized void dump(IndentingPrintWriter pw) {
+        pw.println("Capabilities: " + mApfCapabilities);
+        pw.println("Receive thread: " + (mReceiveThread != null ? "RUNNING" : "STOPPED"));
+        pw.println("Multicast: " + (mMulticastFilter ? "DROP" : "ALLOW"));
+        pw.println("Minimum RDNSS lifetime: " + mMinRdnssLifetimeSec);
+        try {
+            pw.println("IPv4 address: " + InetAddress.getByAddress(mIPv4Address).getHostAddress());
+        } catch (UnknownHostException|NullPointerException e) {}
+
+        if (mLastTimeInstalledProgram == 0) {
+            pw.println("No program installed.");
+            return;
+        }
+        pw.println("Program updates: " + mNumProgramUpdates);
+        pw.println(String.format(
+                "Last program length %d, installed %ds ago, lifetime %ds",
+                mLastInstalledProgram.length, currentTimeSeconds() - mLastTimeInstalledProgram,
+                mLastInstalledProgramMinLifetime));
+
+        pw.print("Denylisted Ethertypes:");
+        for (int p : mEthTypeBlackList) {
+            pw.print(String.format(" %04x", p));
+        }
+        pw.println();
+        pw.println("RA filters:");
+        pw.increaseIndent();
+        for (Ra ra: mRas) {
+            pw.println(ra);
+            pw.increaseIndent();
+            pw.println(String.format(
+                    "Seen: %d, last %ds ago", ra.seenCount, currentTimeSeconds() - ra.mLastSeen));
+            if (DBG) {
+                pw.println("Last match:");
+                pw.increaseIndent();
+                pw.println(ra.getLastMatchingPacket());
+                pw.decreaseIndent();
+            }
+            pw.decreaseIndent();
+        }
+        pw.decreaseIndent();
+
+        pw.println("TCP Keepalive filters:");
+        pw.increaseIndent();
+        for (int i = 0; i < mKeepalivePackets.size(); ++i) {
+            final KeepalivePacket keepalivePacket = mKeepalivePackets.valueAt(i);
+            if (keepalivePacket instanceof TcpKeepaliveAck) {
+                pw.print("Slot ");
+                pw.print(mKeepalivePackets.keyAt(i));
+                pw.print(": ");
+                pw.println(keepalivePacket);
+            }
+        }
+        pw.decreaseIndent();
+
+        pw.println("NAT-T Keepalive filters:");
+        pw.increaseIndent();
+        for (int i = 0; i < mKeepalivePackets.size(); ++i) {
+            final KeepalivePacket keepalivePacket = mKeepalivePackets.valueAt(i);
+            if (keepalivePacket instanceof NattKeepaliveResponse) {
+                pw.print("Slot ");
+                pw.print(mKeepalivePackets.keyAt(i));
+                pw.print(": ");
+                pw.println(keepalivePacket);
+            }
+        }
+        pw.decreaseIndent();
+
+        if (DBG) {
+            pw.println("Last program:");
+            pw.increaseIndent();
+            pw.println(HexDump.toHexString(mLastInstalledProgram, false /* lowercase */));
+            pw.decreaseIndent();
+        }
+
+        pw.println("APF packet counters: ");
+        pw.increaseIndent();
+        if (!mApfCapabilities.hasDataAccess()) {
+            pw.println("APF counters not supported");
+        } else if (mDataSnapshot == null) {
+            pw.println("No last snapshot.");
+        } else {
+            try {
+                Counter[] counters = Counter.class.getEnumConstants();
+                for (Counter c : Arrays.asList(counters).subList(1, counters.length)) {
+                    long value = counterValue(mDataSnapshot, c);
+                    // Only print non-zero counters
+                    if (value != 0) {
+                        pw.println(c.toString() + ": " + value);
+                    }
+                }
+            } catch (ArrayIndexOutOfBoundsException e) {
+                pw.println("Uh-oh: " + e);
+            }
+            if (VDBG) {
+                pw.println("Raw data dump: ");
+                pw.println(HexDump.dumpHexString(mDataSnapshot));
+            }
+        }
+        pw.decreaseIndent();
+    }
+
+    // TODO: move to android.net.NetworkUtils
+    @VisibleForTesting
+    public static int ipv4BroadcastAddress(byte[] addrBytes, int prefixLength) {
+        return bytesToBEInt(addrBytes) | (int) (Integer.toUnsignedLong(-1) >>> prefixLength);
+    }
+
+    private static int uint8(byte b) {
+        return b & 0xff;
+    }
+
+    private static int getUint16(ByteBuffer buffer, int position) {
+        return buffer.getShort(position) & 0xffff;
+    }
+
+    private static long getUint32(ByteBuffer buffer, int position) {
+        return Integer.toUnsignedLong(buffer.getInt(position));
+    }
+
+    private static int getUint8(ByteBuffer buffer, int position) {
+        return uint8(buffer.get(position));
+    }
+
+    private static int bytesToBEInt(byte[] bytes) {
+        return (uint8(bytes[0]) << 24)
+                + (uint8(bytes[1]) << 16)
+                + (uint8(bytes[2]) << 8)
+                + (uint8(bytes[3]));
+    }
+
+    private static byte[] concatArrays(final byte[]... arr) {
+        int size = 0;
+        for (byte[] a : arr) {
+            size += a.length;
+        }
+        final byte[] result = new byte[size];
+        int offset = 0;
+        for (byte[] a : arr) {
+            System.arraycopy(a, 0, result, offset, a.length);
+            offset += a.length;
+        }
+        return result;
+    }
+}
diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java
index e6339a0..b09a2b1 100644
--- a/src/android/net/dhcp/DhcpClient.java
+++ b/src/android/net/dhcp/DhcpClient.java
@@ -75,7 +75,6 @@
 import android.net.networkstack.aidl.dhcp.DhcpOption;
 import android.net.util.HostnameTransliterator;
 import android.net.util.SocketUtils;
-import android.os.Build;
 import android.os.Handler;
 import android.os.Message;
 import android.os.PowerManager;
@@ -105,7 +104,6 @@
 import com.android.networkstack.R;
 import com.android.networkstack.apishim.CaptivePortalDataShimImpl;
 import com.android.networkstack.apishim.SocketUtilsShimImpl;
-import com.android.networkstack.apishim.common.ShimUtils;
 import com.android.networkstack.metrics.IpProvisioningMetrics;
 import com.android.networkstack.util.NetworkStackUtils;
 
@@ -446,12 +444,18 @@
 
         /**
          * Return whether a feature guarded by a feature flag is enabled.
-         * @see DeviceConfigUtils#isFeatureEnabled(Context, String, String)
+         * @see DeviceConfigUtils#isNetworkStackFeatureEnabled(Context, String)
          */
-        public boolean isFeatureEnabled(final Context context, final String name,
-                boolean defaultEnabled) {
-            return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name,
-                    defaultEnabled);
+        public boolean isFeatureEnabled(final Context context, final String name) {
+            return DeviceConfigUtils.isNetworkStackFeatureEnabled(context, name);
+        }
+
+        /**
+         * Check whether one specific feature is not disabled.
+         * @see DeviceConfigUtils#isNetworkStackFeatureNotChickenedOut(Context, String)
+         */
+        public boolean isFeatureNotChickenedOut(final Context context, final String name) {
+            return DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(context, name);
         }
 
         /**
@@ -543,32 +547,23 @@
 
     /**
      * check whether or not to support caching the last lease info and INIT-REBOOT state.
-     *
-     * INIT-REBOOT state is supported on Android R by default if there is no experiment flag set to
-     * disable this feature explicitly, meanwhile turning this feature on/off by pushing experiment
-     * flag makes it possible to do A/B test and metrics collection on both of Android Q and R, but
-     * it's disabled on Android Q by default.
      */
     public boolean isDhcpLeaseCacheEnabled() {
-        final boolean defaultEnabled =
-                ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q);
-        return mDependencies.isFeatureEnabled(mContext, DHCP_INIT_REBOOT_VERSION, defaultEnabled);
+        return mDependencies.isFeatureNotChickenedOut(mContext, DHCP_INIT_REBOOT_VERSION);
     }
 
     /**
      * check whether or not to support DHCP Rapid Commit option.
      */
     public boolean isDhcpRapidCommitEnabled() {
-        return mDependencies.isFeatureEnabled(mContext, DHCP_RAPID_COMMIT_VERSION,
-                false /* defaultEnabled */);
+        return mDependencies.isFeatureEnabled(mContext, DHCP_RAPID_COMMIT_VERSION);
     }
 
     /**
      * check whether or not to support IP address conflict detection and DHCPDECLINE.
      */
     public boolean isDhcpIpConflictDetectEnabled() {
-        return mDependencies.isFeatureEnabled(mContext, DHCP_IP_CONFLICT_DETECT_VERSION,
-                false /* defaultEnabled */);
+        return mDependencies.isFeatureEnabled(mContext, DHCP_IP_CONFLICT_DETECT_VERSION);
     }
 
     /**
@@ -578,8 +573,7 @@
      * disable this feature explicitly.
      */
     public boolean isIPv6OnlyPreferredModeEnabled() {
-        return mDependencies.isFeatureEnabled(mContext, DHCP_IPV6_ONLY_PREFERRED_VERSION,
-                true /* defaultEnabled */);
+        return mDependencies.isFeatureNotChickenedOut(mContext, DHCP_IPV6_ONLY_PREFERRED_VERSION);
     }
 
     /**
@@ -587,8 +581,7 @@
      * suggested in RFC2131 section 4.4.5.
      */
     public boolean isSlowRetransmissionEnabled() {
-        return mDependencies.isFeatureEnabled(mContext, DHCP_SLOW_RETRANSMISSION_VERSION,
-                false /* defaultEnabled */);
+        return mDependencies.isFeatureEnabled(mContext, DHCP_SLOW_RETRANSMISSION_VERSION);
     }
 
     private void recordMetricEnabledFeatures() {
diff --git a/src/android/net/dhcp/DhcpPacket.java b/src/android/net/dhcp/DhcpPacket.java
index 65145e8..770baac 100644
--- a/src/android/net/dhcp/DhcpPacket.java
+++ b/src/android/net/dhcp/DhcpPacket.java
@@ -1283,8 +1283,14 @@
                             captivePortalUrl = readAsciiString(packet, optionLen, true);
                             break;
                         case DHCP_IPV6_ONLY_PREFERRED:
-                            expectedLen = 4;
-                            ipv6OnlyWaitTime = Integer.valueOf(packet.getInt());
+                            if (optionLen == 4) {
+                                expectedLen = optionLen;
+                                ipv6OnlyWaitTime = Integer.valueOf(packet.getInt());
+                            } else {
+                                // rfc8925#section-3.1: The client MUST ignore the IPv6-Only
+                                // Preferred option if the length field value is not 4.
+                                expectedLen = skipOption(packet, optionLen);
+                            }
                             break;
                         default:
                             expectedLen = skipOption(packet, optionLen);
diff --git a/src/android/net/dhcp/DhcpServer.java b/src/android/net/dhcp/DhcpServer.java
index dac9258..041417d 100644
--- a/src/android/net/dhcp/DhcpServer.java
+++ b/src/android/net/dhcp/DhcpServer.java
@@ -23,7 +23,6 @@
 import static android.net.dhcp.IDhcpServer.STATUS_INVALID_ARGUMENT;
 import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
 import static android.net.dhcp.IDhcpServer.STATUS_UNKNOWN_ERROR;
-import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 import static android.system.OsConstants.AF_INET;
 import static android.system.OsConstants.IPPROTO_UDP;
 import static android.system.OsConstants.SOCK_DGRAM;
@@ -233,7 +232,7 @@
 
         @Override
         public boolean isFeatureEnabled(@NonNull Context context, @NonNull String name) {
-            return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name);
+            return DeviceConfigUtils.isNetworkStackFeatureEnabled(context, name);
         }
     }
 
diff --git a/src/android/net/dhcp6/Dhcp6Client.java b/src/android/net/dhcp6/Dhcp6Client.java
index d16dad4..7359c7c 100644
--- a/src/android/net/dhcp6/Dhcp6Client.java
+++ b/src/android/net/dhcp6/Dhcp6Client.java
@@ -16,6 +16,7 @@
 
 package android.net.dhcp6;
 
+import static android.net.dhcp6.Dhcp6Packet.IAID;
 import static android.net.dhcp6.Dhcp6Packet.PrefixDelegation;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 import static android.system.OsConstants.AF_INET6;
@@ -63,10 +64,10 @@
 import java.io.IOException;
 import java.net.Inet6Address;
 import java.net.SocketException;
-import java.net.UnknownHostException;
 import java.nio.ByteBuffer;
 import java.util.Arrays;
 import java.util.Random;
+import java.util.function.IntSupplier;
 
 /**
  * A DHCPv6 client.
@@ -93,6 +94,7 @@
     public static final int DHCP6_PD_SUCCESS = 1;
     public static final int DHCP6_PD_PREFIX_EXPIRED = 2;
     public static final int DHCP6_PD_PREFIX_CHANGED = 3;
+    public static final int DHCP6_PD_PREFIX_MSG_EXCHANGE_TERMINATED = 4;
 
     // Notification from DHCPv6 state machine before quitting
     public static final int CMD_ON_QUIT = PUBLIC_BASE + 4;
@@ -105,18 +107,20 @@
     private static final int CMD_DHCP6_PD_REBIND = PRIVATE_BASE + 4;
     private static final int CMD_DHCP6_PD_EXPIRE = PRIVATE_BASE + 5;
 
-    // Timers and timeouts.
-    // TODO: comply with RFC8415 section 15(Reliability of Client-Initiated Message Exchanges)
-    private static final int SECONDS           = 1000;
-    private static final int FIRST_TIMEOUT_MS  =   1 * SECONDS;
-    private static final int MAX_TIMEOUT_MS    = 512 * SECONDS;
+    // Transmission and Retransmission parameters in milliseconds.
+    private static final int SECONDS            = 1000;
+    private static final int SOL_TIMEOUT        =    1 * SECONDS;
+    private static final int SOL_MAX_RT         = 3600 * SECONDS;
+    private static final int REQ_TIMEOUT        =    1 * SECONDS;
+    private static final int REQ_MAX_RT         =   30 * SECONDS;
+    private static final int REQ_MAX_RC         =   10;
+    private static final int REN_TIMEOUT        =   10 * SECONDS;
+    private static final int REN_MAX_RT         =  600 * SECONDS;
+    private static final int REB_TIMEOUT        =   10 * SECONDS;
+    private static final int REB_MAX_RT         =  600 * SECONDS;
 
-    // Per rfc8415#section-12, the IAID MUST be consistent across restarts.
-    // Since currently only one IAID is supported, a well-known value can be used (0).
-    private static final int IAID = 0;
+    private int mSolMaxRtMs = SOL_MAX_RT;
 
-    private int mTransId;
-    private long mTransStartMillis;
     @Nullable private PrefixDelegation mAdvertise;
     @Nullable private PrefixDelegation mReply;
     @Nullable private byte[] mServerDuid;
@@ -225,24 +229,70 @@
     }
 
     /**
-     * Retransmits packets using jittered exponential backoff with an optional timeout. Packet
-     * transmission is triggered by CMD_KICK, which is sent by an AlarmManager alarm. Kicks are
-     * cancelled when leaving the state.
+     * Retransmits packets per algorithm defined in RFC8415 section 15. Packet transmission is
+     * triggered by CMD_KICK, which is sent by an AlarmManager alarm. Kicks are cancelled when
+     * leaving the state.
      *
-     * Concrete subclasses must implement sendPacket, which is called when the alarm fires and a
-     * packet needs to be transmitted, and receivePacket, which is triggered by CMD_RECEIVED_PACKET
-     * sent by the receive thread.
-     *
-     * TODO: deduplicate with the similar code in DhcpClient.java
+     * Concrete subclasses must initialize retransmission parameters and implement sendPacket,
+     * which is called when the alarm fires and a packet needs to be transmitted, and receivePacket,
+     * which is triggered by CMD_RECEIVED_PACKET sent by the receive thread.
      */
-    abstract class PacketRetransmittingState extends State {
-        private int mTimer;
+    abstract class MessageExchangeState extends State {
+        private int mTransId = 0;
+        private long mTransStartMs = 0;
+        private long mMaxRetransTimeMs = 0;
+
+        private long mRetransTimeout = -1;
+        private int mRetransCount = 0;
+        private final long mInitialDelayMs;
+        private final long mInitialRetransTimeMs;
+        private final int mMaxRetransCount;
+        private final IntSupplier mMaxRetransTimeSupplier;
+
+        MessageExchangeState(final int delay, final int irt, final int mrc, final IntSupplier mrt) {
+            mInitialDelayMs = delay;
+            mInitialRetransTimeMs = irt;
+            mMaxRetransCount = mrc;
+            mMaxRetransTimeSupplier = mrt;
+        }
 
         @Override
         public void enter() {
             super.enter();
-            mTimer = FIRST_TIMEOUT_MS;
-            sendMessage(CMD_KICK);
+            mMaxRetransTimeMs = mMaxRetransTimeSupplier.getAsInt();
+            // Every message exchange generates a new transaction id.
+            mTransId = mRandom.nextInt() & 0xffffff;
+            sendMessageDelayed(CMD_KICK, mInitialDelayMs);
+        }
+
+        private void handleKick() {
+            // rfc8415#section-21.9: The elapsed time is measured from the time at which the
+            // client sent the first message in the message exchange, and the elapsed-time field
+            // is set to 0 in the first message in the message exchange.
+            final long elapsedTimeMs;
+            if (mRetransCount == 0) {
+                elapsedTimeMs = 0;
+                mTransStartMs = SystemClock.elapsedRealtime();
+            } else {
+                elapsedTimeMs = SystemClock.elapsedRealtime() - mTransStartMs;
+            }
+
+            sendPacket(mTransId, elapsedTimeMs);
+            // Compares retransmission parameters and reschedules alarm accordingly.
+            scheduleKick();
+        }
+
+        private void handleReceivedPacket(Dhcp6Packet packet) {
+            // Technically it is valid for the server to not include a prefix in an IA in certain
+            // scenarios (specifically in a reply to Renew / Rebind, which means: do not extend the
+            // prefix). However, while only supporting a single prefix, this never works well, so if
+            // the server decides to do so, ignore it.
+            // TODO: revisit this when adding multi-prefix support.
+            final boolean validIpo = packet.mPrefixDelegation.ipo != null
+                    && packet.mPrefixDelegation.ipo.isValid();
+            if (packet.isValid(mTransId, mClientDuid) && validIpo) {
+                receivePacket(packet);
+            }
         }
 
         @Override
@@ -253,11 +303,10 @@
 
             switch (message.what) {
                 case CMD_KICK:
-                    sendPacket();
-                    scheduleKick();
+                    handleKick();
                     return HANDLED;
                 case CMD_RECEIVED_PACKET:
-                    receivePacket((Dhcp6Packet) message.obj);
+                    handleReceivedPacket((Dhcp6Packet) message.obj);
                     return HANDLED;
                 default:
                     return NOT_HANDLED;
@@ -268,26 +317,58 @@
         public void exit() {
             super.exit();
             mKickAlarm.cancel();
+            mRetransTimeout = -1;
+            mRetransCount = 0;
+            mMaxRetransTimeMs = 0;
         }
 
-        protected abstract boolean sendPacket();
+        protected abstract boolean sendPacket(int transId, long elapsedTimeMs);
         protected abstract void receivePacket(Dhcp6Packet packet);
+        // If the message exchange is considered to have failed according to the retransmission
+        // mechanism(i.e. client has transmitted the message MRC times or MRD seconds has elapsed
+        // since the first message transmission), this method will be called to roll back to Solicit
+        // state and restart the configuration, and notify IpClient the DHCPv6 message exchange
+        // failure if needed.
+        protected void onMessageExchangeFailed() {}
 
-        protected int jitterTimer(int baseTimer) {
-            int maxJitter = baseTimer / 10;
-            int jitter = mRandom.nextInt(2 * maxJitter) - maxJitter;
-            return baseTimer + jitter;
+        /**
+         * Per RFC8415 section 15, each of the computations of a new RT includes a randomization
+         * factor (RAND), which is a random number chosen with a uniform distribution between -0.1
+         * and +0.1.
+         */
+        private double rand() {
+            return mRandom.nextDouble() / 5 - 0.1;
         }
 
         protected void scheduleKick() {
-            long now = SystemClock.elapsedRealtime();
-            long timeout = jitterTimer(mTimer);
-            long alarmTime = now + timeout;
-            mKickAlarm.schedule(alarmTime);
-            mTimer *= 2;
-            if (mTimer > MAX_TIMEOUT_MS) {
-                mTimer = MAX_TIMEOUT_MS;
+            if (mRetransTimeout == -1) {
+                // RT for the first message transmission is based on IRT.
+                mRetransTimeout = mInitialRetransTimeMs + (long) (rand() * mInitialRetransTimeMs);
+            } else {
+                // RT for each subsequent message transmission is based on the previous value of RT.
+                mRetransTimeout = 2 * mRetransTimeout + (long) (rand() * mRetransTimeout);
             }
+            if (mMaxRetransTimeMs != 0 && mRetransTimeout > mMaxRetransTimeMs) {
+                mRetransTimeout = mMaxRetransTimeMs + (long) (rand() * mMaxRetransTimeMs);
+            }
+            // Per RFC8415 section 18.2.4 and 18.2.5, MRD equals to the remaining time until
+            // earliest T2(RenewState) or valid lifetimes of all leases in all IA have expired
+            // (RebindState), and message exchange is terminated when the earliest time T2 is
+            // reached, at which point client begins the Rebind message exchange, however, section
+            // 15 says the message exchange fails(terminated) once MRD seconds have elapsed since
+            // the client first transmitted the message. So far MRD is being used for Renew, Rebind
+            // and Confirm message retransmission. Given we don't support Confirm message yet, we
+            // can just use rebindTimeout and expirationTimeout on behalf of MRD which have been
+            // scheduled in BoundState to simplify the implementation, therefore, we don't need to
+            // explicitly assign the MRD in the subclasses.
+            if (mMaxRetransCount != 0 && mRetransCount > mMaxRetransCount) {
+                onMessageExchangeFailed();
+                Log.i(TAG, "client has transmitted the message " + mMaxRetransCount
+                        + " times, stopping retransmission");
+                return;
+            }
+            mKickAlarm.schedule(SystemClock.elapsedRealtime() + mRetransTimeout);
+            mRetransCount++;
         }
     }
 
@@ -349,41 +430,33 @@
         mAdvertise = null;
         mReply = null;
         mServerDuid = null;
-    }
-
-    private void startNewTransaction() {
-        mTransId = mRandom.nextInt() & 0xffffff;
-        mTransStartMillis = SystemClock.elapsedRealtime();
-    }
-
-    private long getElapsedTimeMs() {
-        return SystemClock.elapsedRealtime() - mTransStartMillis;
+        mSolMaxRtMs = SOL_MAX_RT;
     }
 
     @SuppressWarnings("ByteBufferBackingArray")
-    private boolean sendSolicitPacket(final ByteBuffer iapd) {
-        final ByteBuffer packet = Dhcp6Packet.buildSolicitPacket(mTransId,
-                getElapsedTimeMs(), iapd.array(), mClientDuid, true /* rapidCommit */);
+    private boolean sendSolicitPacket(int transId, long elapsedTimeMs, final ByteBuffer iapd) {
+        final ByteBuffer packet = Dhcp6Packet.buildSolicitPacket(transId, elapsedTimeMs,
+                iapd.array(), mClientDuid, true /* rapidCommit */);
         return transmitPacket(packet, "solicit");
     }
 
     @SuppressWarnings("ByteBufferBackingArray")
-    private boolean sendRequestPacket(final ByteBuffer iapd) {
-        final ByteBuffer packet = Dhcp6Packet.buildRequestPacket(mTransId, getElapsedTimeMs(),
+    private boolean sendRequestPacket(int transId, long elapsedTimeMs, final ByteBuffer iapd) {
+        final ByteBuffer packet = Dhcp6Packet.buildRequestPacket(transId, elapsedTimeMs,
                 iapd.array(), mClientDuid, mServerDuid);
         return transmitPacket(packet, "request");
     }
 
     @SuppressWarnings("ByteBufferBackingArray")
-    private boolean sendRenewPacket(final ByteBuffer iapd) {
-        final ByteBuffer packet = Dhcp6Packet.buildRenewPacket(mTransId, getElapsedTimeMs(),
+    private boolean sendRenewPacket(int transId, long elapsedTimeMs, final ByteBuffer iapd) {
+        final ByteBuffer packet = Dhcp6Packet.buildRenewPacket(transId, elapsedTimeMs,
                 iapd.array(), mClientDuid, mServerDuid);
         return transmitPacket(packet, "renew");
     }
 
     @SuppressWarnings("ByteBufferBackingArray")
-    private boolean sendRebindPacket(final ByteBuffer iapd) {
-        final ByteBuffer packet = Dhcp6Packet.buildRebindPacket(mTransId, getElapsedTimeMs(),
+    private boolean sendRebindPacket(int transId, long elapsedTimeMs, final ByteBuffer iapd) {
+        final ByteBuffer packet = Dhcp6Packet.buildRebindPacket(transId, elapsedTimeMs,
                 iapd.array(), mClientDuid);
         return transmitPacket(packet, "rebind");
     }
@@ -457,42 +530,45 @@
      *
      * Note: Not implement DHCPv6 server selection, always request the first Advertise we receive.
      */
-    class SolicitState extends PacketRetransmittingState {
-        @Override
-        public void enter() {
-            super.enter();
-            startNewTransaction();
+    class SolicitState extends MessageExchangeState {
+        SolicitState() {
+            // First Solicit message should be delayed by a random amount of time between 0
+            // and SOL_MAX_DELAY(1s).
+            super((int) (new Random().nextDouble() * SECONDS) /* delay */, SOL_TIMEOUT /* IRT */,
+                    0 /* MRC */, () -> mSolMaxRtMs /* MRT */);
         }
 
         @Override
-        protected boolean sendPacket() {
-            return sendSolicitPacket(buildEmptyIaPdOption());
+        public void enter() {
+            super.enter();
+        }
+
+        @Override
+        protected boolean sendPacket(int transId, long elapsedTimeMs) {
+            return sendSolicitPacket(transId, elapsedTimeMs, buildEmptyIaPdOption());
         }
 
         // TODO: support multiple prefixes.
         @Override
         protected void receivePacket(Dhcp6Packet packet) {
-            if (!packet.isValid(mTransId, mClientDuid)) return;
+            final PrefixDelegation pd = packet.mPrefixDelegation;
             if (packet instanceof Dhcp6AdvertisePacket) {
-                mAdvertise = packet.mPrefixDelegation;
-                if (mAdvertise != null && mAdvertise.iaid == IAID) {
-                    Log.d(TAG, "Get prefix delegation option from Advertise: " + mAdvertise);
-                    mServerDuid = packet.mServerDuid;
-                    transitionTo(mRequestState);
-                }
+                Log.d(TAG, "Get prefix delegation option from Advertise: " + pd);
+                mAdvertise = pd;
+                mServerDuid = packet.mServerDuid;
+                mSolMaxRtMs = packet.getSolMaxRtMs().orElse(mSolMaxRtMs);
+                transitionTo(mRequestState);
             } else if (packet instanceof Dhcp6ReplyPacket) {
                 if (!packet.mRapidCommit) {
-                    Log.e(TAG, "Server responded to SOLICIT with REPLY without rapid commit option"
+                    Log.e(TAG, "Server responded to Solicit with Reply without rapid commit option"
                             + ", ignoring");
                     return;
                 }
-                final PrefixDelegation pd = packet.mPrefixDelegation;
-                if (pd != null && pd.iaid == IAID) {
-                    Log.d(TAG, "Get prefix delegation option from RapidCommit Reply: " + pd);
-                    mReply = pd;
-                    mServerDuid = packet.mServerDuid;
-                    transitionTo(mBoundState);
-                }
+                Log.d(TAG, "Get prefix delegation option from RapidCommit Reply: " + pd);
+                mReply = pd;
+                mServerDuid = packet.mServerDuid;
+                mSolMaxRtMs = packet.getSolMaxRtMs().orElse(mSolMaxRtMs);
+                transitionTo(mBoundState);
             }
         }
     }
@@ -501,22 +577,30 @@
      * Client (re)transmits a Request message to request configuration from a specific server and
      * process the Reply message in this state.
      */
-    class RequestState extends PacketRetransmittingState {
+    class RequestState extends MessageExchangeState {
+        RequestState() {
+            super(0 /* delay */, REQ_TIMEOUT /* IRT */, REQ_MAX_RC /* MRC */,
+                    () -> REQ_MAX_RT /* MRT */);
+        }
+
         @Override
-        protected boolean sendPacket() {
-            return sendRequestPacket(buildIaPdOption(mAdvertise));
+        protected boolean sendPacket(int transId, long elapsedTimeMs) {
+            return sendRequestPacket(transId, elapsedTimeMs, buildIaPdOption(mAdvertise));
         }
 
         @Override
         protected void receivePacket(Dhcp6Packet packet) {
             if (!(packet instanceof Dhcp6ReplyPacket)) return;
-            if (!packet.isValid(mTransId, mClientDuid)) return;
             final PrefixDelegation pd = packet.mPrefixDelegation;
-            if (pd != null && pd.iaid == IAID) {
-                Log.d(TAG, "Get prefix delegation option from Reply: " + pd);
-                mReply = pd;
-                transitionTo(mBoundState);
-            }
+            Log.d(TAG, "Get prefix delegation option from Reply: " + pd);
+            mReply = pd;
+            mSolMaxRtMs = packet.getSolMaxRtMs().orElse(mSolMaxRtMs);
+            transitionTo(mBoundState);
+        }
+
+        @Override
+        protected void onMessageExchangeFailed() {
+            transitionTo(mSolicitState);
         }
     }
 
@@ -558,29 +642,16 @@
 
             // TODO: roll back to SOLICIT state after a delay if something wrong happens
             // instead of returning directly.
-            if (!Dhcp6Packet.hasValidPrefixDelegation(mReply)) {
-                Log.e(TAG, "Invalid prefix delegatioin " + mReply);
-                return;
-            }
-            // Configure the IPv6 addresses based on the delegated prefix on the interface.
-            // We've checked that delegated prefix is valid upon receiving the response
-            // from DHCPv6 server, and the server may assign a prefix with length less
-            // than 64. So for SLAAC use case we always set the prefix length to 64 even
-            // if the delegated prefix length is less than 64.
-            final IpPrefix prefix;
-            try {
-                prefix = new IpPrefix(Inet6Address.getByAddress(mReply.ipo.prefix),
-                        RFC7421_PREFIX_LENGTH);
-            } catch (UnknownHostException e) {
-                Log.wtf(TAG, "Invalid delegated prefix "
-                        + HexDump.toHexString(mReply.ipo.prefix));
-                return;
-            }
-            // Create an IPv6 address from the interface mac address with IFA_F_MANAGETEMPADDR
-            // flag, kernel will create another privacy IPv6 address on behalf of user space.
-            // We don't need to remember IPv6 addresses that need to extend the lifetime every
-            // time it enters BoundState.
-            final Inet6Address address = createInet6AddressFromEui64(prefix,
+            // The server may assign a prefix with length less than 64. To support automatic address
+            // generation (with IFA_F_MANAGETEMPADDR), we always set the address prefix length to
+            // 64, even if the delegated prefix length is less than 64. However, the unreachable
+            // route should still use the assigned prefix length.
+            final IpPrefix routePrefix = mReply.ipo.getIpPrefix();
+            final IpPrefix addressPrefix = new IpPrefix(routePrefix.getAddress(),
+                    RFC7421_PREFIX_LENGTH);
+            // Create EUI-64, so we don't need to remember IPv6 addresses that need to extend the
+            // lifetime every time it enters BoundState.
+            final Inet6Address address = createInet6AddressFromEui64(addressPrefix,
                     macAddressToEui64(mIface.macAddr));
             final int flags = IFA_F_NOPREFIXROUTE | IFA_F_MANAGETEMPADDR | IFA_F_NODAD;
             final long now = SystemClock.elapsedRealtime();
@@ -616,39 +687,38 @@
         }
     }
 
-    abstract class ReacquireState extends PacketRetransmittingState {
+    abstract class ReacquireState extends MessageExchangeState {
+        ReacquireState(final int irt, final int mrt) {
+            super(0 /* delay */, irt, 0 /* MRC */, () -> mrt /* MRT */);
+        }
+
         @Override
         public void enter() {
             super.enter();
-            startNewTransaction();
         }
 
         @Override
         protected void receivePacket(Dhcp6Packet packet) {
             if (!(packet instanceof Dhcp6ReplyPacket)) return;
-            if (!packet.isValid(mTransId, mClientDuid)) return;
             final PrefixDelegation pd = packet.mPrefixDelegation;
-            if (pd != null) {
-                if (pd.iaid != IAID
-                        || !(Arrays.equals(pd.ipo.prefix, mReply.ipo.prefix)
-                                && pd.ipo.prefixLen == mReply.ipo.prefixLen)) {
-                    Log.i(TAG, "Renewal prefix " + HexDump.toHexString(pd.ipo.prefix)
-                            + " does not match current prefix "
-                            + HexDump.toHexString(mReply.ipo.prefix));
-                    notifyPrefixDelegation(DHCP6_PD_PREFIX_CHANGED, null);
-                    transitionTo(mSolicitState);
-                    return;
-                }
-                mReply = pd;
-                mServerDuid = packet.mServerDuid;
-                // Once the delegated prefix gets refreshed successfully we have to extend the
-                // preferred lifetime and valid lifetime of global IPv6 addresses, otherwise
-                // these addresses will become depreacated finally and then provisioning failure
-                // happens. So we transit to mBoundState to update the address with refreshed
-                // preferred and valid lifetime via sending RTM_NEWADDR message, going back to
-                // Bound state after a success update.
-                transitionTo(mBoundState);
+            if (!(Arrays.equals(pd.ipo.prefix, mReply.ipo.prefix)
+                    && pd.ipo.prefixLen == mReply.ipo.prefixLen)) {
+                Log.i(TAG, "Renewal prefix " + HexDump.toHexString(pd.ipo.prefix)
+                        + " does not match current prefix "
+                        + HexDump.toHexString(mReply.ipo.prefix));
+                notifyPrefixDelegation(DHCP6_PD_PREFIX_CHANGED, null);
+                transitionTo(mSolicitState);
+                return;
             }
+            mReply = pd;
+            mServerDuid = packet.mServerDuid;
+            // Once the delegated prefix gets refreshed successfully we have to extend the
+            // preferred lifetime and valid lifetime of global IPv6 addresses, otherwise
+            // these addresses will become depreacated finally and then provisioning failure
+            // happens. So we transit to mBoundState to update the address with refreshed
+            // preferred and valid lifetime via sending RTM_NEWADDR message, going back to
+            // Bound state after a success update.
+            transitionTo(mBoundState);
         }
     }
 
@@ -658,6 +728,10 @@
      * extend the lifetimes on the leases assigned to the client.
      */
     class RenewState extends ReacquireState {
+        RenewState() {
+            super(REN_TIMEOUT, REN_MAX_RT);
+        }
+
         @Override
         public boolean processMessage(Message message) {
             if (super.processMessage(message) == HANDLED) {
@@ -673,8 +747,8 @@
         }
 
         @Override
-        protected boolean sendPacket() {
-            return sendRenewPacket(buildIaPdOption(mReply));
+        protected boolean sendPacket(int transId, long elapsedTimeMs) {
+            return sendRenewPacket(transId, elapsedTimeMs, buildIaPdOption(mReply));
         }
     }
 
@@ -684,9 +758,13 @@
      * update other configuration parameters.
      */
     class RebindState extends ReacquireState {
+        RebindState() {
+            super(REB_TIMEOUT, REB_MAX_RT);
+        }
+
         @Override
-        protected boolean sendPacket() {
-            return sendRebindPacket(buildIaPdOption(mReply));
+        protected boolean sendPacket(int transId, long elapsedTimeMs) {
+            return sendRebindPacket(transId, elapsedTimeMs, buildIaPdOption(mReply));
         }
     }
 
@@ -723,16 +801,6 @@
             return mUdpSock;
         }
 
-        @Override
-        protected int readPacket(FileDescriptor fd, byte[] packetBuffer) throws Exception {
-            try {
-                return Os.read(fd, packetBuffer, 0, packetBuffer.length);
-            } catch (IOException | ErrnoException e) {
-                Log.e(TAG, "Fail to read packet");
-                throw e;
-            }
-        }
-
         public int transmitPacket(final ByteBuffer buf) throws ErrnoException, SocketException {
             int ret = Os.sendto(mUdpSock, buf.array(), 0 /* byteOffset */,
                     buf.limit() /* byteCount */, 0 /* flags */, ALL_DHCP_RELAY_AGENTS_AND_SERVERS,
diff --git a/src/android/net/dhcp6/Dhcp6Packet.java b/src/android/net/dhcp6/Dhcp6Packet.java
index 8a11547..540a670 100644
--- a/src/android/net/dhcp6/Dhcp6Packet.java
+++ b/src/android/net/dhcp6/Dhcp6Packet.java
@@ -34,6 +34,7 @@
 import java.nio.ByteOrder;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
+import java.util.OptionalInt;
 
 /**
  * Defines basic data and operations needed to build and use packets for the
@@ -76,6 +77,11 @@
     protected final byte[] mServerDuid;
 
     /**
+     * DHCPv6 Optional Type: Option Request Option.
+     */
+    public static final byte DHCP6_OPTION_REQUEST_OPTION = 6;
+
+    /**
      * DHCPv6 Optional Type: Elapsed time.
      * This time is expressed in hundredths of a second.
      */
@@ -114,6 +120,12 @@
     protected PrefixDelegation mPrefixDelegation;
 
     /**
+     * DHCPv6 Optional Type: SOL_MAX_RT.
+     */
+    public static final byte DHCP6_SOL_MAX_RT = 82;
+    private OptionalInt mSolMaxRt;
+
+    /**
      * The transaction identifier used in this particular DHCPv6 negotiation
      */
     protected final int mTransId;
@@ -122,6 +134,9 @@
      * The unique identifier for IA_NA, IA_TA, IA_PD used in this particular DHCPv6 negotiation
      */
     protected int mIaId;
+    // Per rfc8415#section-12, the IAID MUST be consistent across restarts.
+    // Since currently only one IAID is supported, a well-known value can be used (0).
+    public static final int IAID = 0;
 
     Dhcp6Packet(int transId, int elapsedTime, @NonNull final byte[] clientDuid,
             final byte[] serverDuid, @NonNull final byte[] iapd) {
@@ -162,6 +177,13 @@
     }
 
     /**
+     * Returns the SOL_MAX_RT option value in milliseconds.
+     */
+    public OptionalInt getSolMaxRtMs() {
+        return mSolMaxRt;
+    }
+
+    /**
      * A class to take DHCPv6 IA_PD option allocated from server.
      * https://www.rfc-editor.org/rfc/rfc8415.html#section-21.21
      */
@@ -178,6 +200,29 @@
             this.ipo = ipo;
         }
 
+        /**
+         * Check whether or not the delegated prefix in DHCPv6 packet is valid.
+         *
+         * TODO: ensure that the prefix has a reasonable lifetime, and the timers aren't too short.
+         */
+        public boolean isValid() {
+            if (iaid != IAID) {
+                Log.w(TAG, "IA_ID doesn't match, expected: " + IAID + ", actual: " + iaid);
+                return false;
+            }
+            if (t1 < 0 || t2 < 0) {
+                Log.e(TAG, "IA_PD option with invalid T1 " + t1 + " or T2 " + t2);
+                return false;
+            }
+
+            // Generally, t1 must be smaller or equal to t2 (except when t2 is 0).
+            if (t2 != 0 && t1 > t2) {
+                Log.e(TAG, "IA_PD option with T1 " + t1 + " greater than T2 " + t2);
+                return false;
+            }
+            return true;
+        }
+
         @Override
         public String toString() {
             return "Prefix Delegation: iaid " + iaid + ", t1 " + t1 + ", t2 " + t2
@@ -257,6 +302,7 @@
         short statusCode = STATUS_SUCCESS;
         String statusMsg = null;
         boolean rapidCommit = false;
+        int solMaxRt = 0;
 
         packet.order(ByteOrder.BIG_ENDIAN);
 
@@ -315,6 +361,10 @@
                         statusCode = packet.getShort();
                         statusMsg = readAsciiString(packet, expectedLen - 2, false /* isNullOk */);
                         break;
+                    case DHCP6_SOL_MAX_RT:
+                        expectedLen = 4;
+                        solMaxRt = packet.getInt();
+                        break;
                     default:
                         expectedLen = optionLen;
                         // BufferUnderflowException will be thrown if option is truncated.
@@ -372,6 +422,10 @@
         newPacket.mStatusCode = statusCode;
         newPacket.mStatusMsg = statusMsg;
         newPacket.mRapidCommit = rapidCommit;
+        newPacket.mSolMaxRt =
+                (solMaxRt >= 60 && solMaxRt <= 86400)
+                        ? OptionalInt.of(solMaxRt * 1000)
+                        : OptionalInt.empty();
 
         return newPacket;
     }
@@ -402,49 +456,15 @@
             Log.e(TAG, "Unexpected transaction ID " + mTransId + ", expected " + transId);
             return false;
         }
-        return true;
-    }
-
-    /**
-     * Check whether or not the delegated prefix in DHCPv6 packet is valid.
-     *
-     * TODO: ensure that the prefix has a reasonable lifetime, and the timers aren't too short.
-     */
-    public static boolean hasValidPrefixDelegation(@NonNull final PrefixDelegation pd) {
-        if (pd == null) {
-            Log.e(TAG, "DHCPv6 packet without IA_PD option, ignoring");
+        if (mPrefixDelegation == null) {
+            Log.e(TAG, "DHCPv6 message without IA_PD option, ignoring");
             return false;
         }
-        if (pd.ipo.prefixLen > 64) {
-            Log.e(TAG, "IA_PD option with prefix length " + pd.ipo.prefixLen + " longer than 64");
+        if (!mPrefixDelegation.isValid()) {
+            Log.e(TAG, "DHCPv6 message takes invalid IA_PD option, ignoring");
             return false;
         }
-        final long t1 = pd.t1;
-        final long t2 = pd.t2;
-        if (t1 < 0 || t2 < 0) {
-            Log.e(TAG, "IA_PD option with invalid T1 " + t1 + " or T2 " + t2);
-            return false;
-        }
-        if (t1 > t2) {
-            Log.e(TAG, "IA_PD option with T1 " + t1 + " greater than T2 " + t2);
-            return false;
-        }
-        final long preferred = pd.ipo.preferred;
-        final long valid = pd.ipo.valid;
-        if (preferred < 0 || valid < 0) {
-            Log.e(TAG, "IA_PD option with invalid lifetime, preferred lifetime " + preferred
-                    + ", valid lifetime " + valid);
-            return false;
-        }
-        if (preferred > valid) {
-            Log.e(TAG, "IA_PD option with preferred lifetime " + preferred
-                    + " greater than valid lifetime " + valid);
-            return false;
-        }
-        if (preferred < t2) {
-            Log.e(TAG, "preferred lifetime " + preferred + " is samller than T2 " + t2);
-            return false;
-        }
+        //TODO: check if the status code is success or not.
         return true;
     }
 
diff --git a/src/android/net/dhcp6/Dhcp6RequestPacket.java b/src/android/net/dhcp6/Dhcp6RequestPacket.java
index 0c65f17..6d4dfdf 100644
--- a/src/android/net/dhcp6/Dhcp6RequestPacket.java
+++ b/src/android/net/dhcp6/Dhcp6RequestPacket.java
@@ -49,6 +49,7 @@
         addTlv(packet, DHCP6_CLIENT_IDENTIFIER, mClientDuid);
         addTlv(packet, DHCP6_ELAPSED_TIME, (short) (mElapsedTime & 0xFFFF));
         addTlv(packet, DHCP6_IA_PD, mIaPd);
+        addTlv(packet, DHCP6_OPTION_REQUEST_OPTION, DHCP6_SOL_MAX_RT);
 
         packet.flip();
         return packet;
diff --git a/src/android/net/dhcp6/Dhcp6SolicitPacket.java b/src/android/net/dhcp6/Dhcp6SolicitPacket.java
index 4916c1e..5cf5d01 100644
--- a/src/android/net/dhcp6/Dhcp6SolicitPacket.java
+++ b/src/android/net/dhcp6/Dhcp6SolicitPacket.java
@@ -48,6 +48,7 @@
         addTlv(packet, DHCP6_ELAPSED_TIME, (short) (mElapsedTime & 0xFFFF));
         addTlv(packet, DHCP6_CLIENT_IDENTIFIER, mClientDuid);
         addTlv(packet, DHCP6_IA_PD, mIaPd);
+        addTlv(packet, DHCP6_OPTION_REQUEST_OPTION, DHCP6_SOL_MAX_RT);
         if (mRapidCommit) {
             addTlv(packet, DHCP6_RAPID_COMMIT);
         }
diff --git a/src/android/net/ip/ConnectivityPacketTracker.java b/src/android/net/ip/ConnectivityPacketTracker.java
index 4b92179..51fb428 100644
--- a/src/android/net/ip/ConnectivityPacketTracker.java
+++ b/src/android/net/ip/ConnectivityPacketTracker.java
@@ -18,7 +18,6 @@
 
 import static android.net.util.SocketUtils.makePacketSocketAddress;
 import static android.system.OsConstants.AF_PACKET;
-import static android.system.OsConstants.ARPHRD_ETHER;
 import static android.system.OsConstants.ETH_P_ALL;
 import static android.system.OsConstants.SOCK_NONBLOCK;
 import static android.system.OsConstants.SOCK_RAW;
@@ -110,7 +109,7 @@
             FileDescriptor s = null;
             try {
                 s = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0);
-                NetworkStackUtils.attachControlPacketFilter(s, ARPHRD_ETHER);
+                NetworkStackUtils.attachControlPacketFilter(s);
                 Os.bind(s, makePacketSocketAddress(ETH_P_ALL, mInterface.index));
             } catch (ErrnoException | IOException e) {
                 logError("Failed to create packet tracking socket: ", e);
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index 9fa04ac..11cdd20 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -34,6 +34,7 @@
 import static android.system.OsConstants.ETH_P_IPV6;
 import static android.system.OsConstants.SOCK_NONBLOCK;
 import static android.system.OsConstants.SOCK_RAW;
+
 import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
@@ -42,6 +43,7 @@
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION;
+import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_IGNORE_LOW_RA_LIFETIME_FORCE_DISABLE;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION;
 import static com.android.server.util.PermissionUtil.enforceNetworkStackCallingPermission;
 
@@ -64,8 +66,10 @@
 import android.net.RouteInfo;
 import android.net.TcpKeepalivePacketDataParcelable;
 import android.net.Uri;
+import android.net.apf.AndroidPacketFilter;
 import android.net.apf.ApfCapabilities;
 import android.net.apf.ApfFilter;
+import android.net.apf.LegacyApfFilter;
 import android.net.dhcp.DhcpClient;
 import android.net.dhcp.DhcpPacket;
 import android.net.dhcp6.Dhcp6Client;
@@ -109,6 +113,7 @@
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
 import com.android.internal.util.WakeupMessage;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.DeviceConfigUtils;
 import com.android.net.module.util.InterfaceParams;
@@ -130,6 +135,7 @@
 import com.android.server.NetworkObserverRegistry;
 import com.android.server.NetworkStackService.NetworkStackServiceManager;
 
+import java.io.File;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.net.Inet4Address;
@@ -443,8 +449,14 @@
          * Set maximum acceptable DTIM multiplier to hardware driver.
          */
         public void setMaxDtimMultiplier(int multiplier) {
-            log("setMaxDtimMultiplier(" + multiplier + ")");
             try {
+                // {@link IWifiStaIface#setDtimMultiplier} has been implemented since U, calling
+                // this method on U- platforms does nothing actually.
+                if (!SdkLevel.isAtLeastU()) {
+                    log("SDK level is lower than U, do not call setMaxDtimMultiplier method");
+                    return;
+                }
+                log("setMaxDtimMultiplier(" + multiplier + ")");
                 mCallback.setMaxDtimMultiplier(multiplier);
             } catch (RemoteException e) {
                 log("Failed to call setMaxDtimMultiplier", e);
@@ -475,6 +487,8 @@
     // Sysctl parameter strings.
     private static final String ACCEPT_RA = "accept_ra";
     private static final String ACCEPT_RA_DEFRTR = "accept_ra_defrtr";
+    @VisibleForTesting
+    static final String ACCEPT_RA_MIN_LFT = "accept_ra_min_lft";
     private static final String DAD_TRANSMITS = "dad_transmits";
 
     // Below constants are picked up by MessageUtils and exempt from ProGuard optimization.
@@ -525,6 +539,11 @@
     private static final int DEFAULT_MIN_RDNSS_LIFETIME =
             ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q) ? 120 : 0;
 
+    @VisibleForTesting
+    static final String CONFIG_ACCEPT_RA_MIN_LFT = "ipclient_accept_ra_min_lft";
+    @VisibleForTesting
+    static final int DEFAULT_ACCEPT_RA_MIN_LFT = 180;
+
     // Used to wait for the provisioning to complete eventually and then decide the target
     // network type, which gives the accurate hint to set DTIM multiplier. Per current IPv6
     // provisioning connection latency metrics, the latency of 95% can go up to 16s, so pick
@@ -596,11 +615,11 @@
     // Maps each DHCP option code to a list of IEs, any of which will allow that option.
     private static final Map<Byte, List<byte[]>> DHCP_OPTIONS_ALLOWED = Map.of(
             (byte) 60, Collections.singletonList(
-                    // KT OUI: 00:17:C3, type: 17. See b/170928882.
-                    new byte[]{ (byte) 0x00, (byte) 0x17, (byte) 0xc3, (byte) 0x11 }),
+                    // KT OUI: 00:17:C3, type: 33(0x21). See b/236745261.
+                    new byte[]{ (byte) 0x00, (byte) 0x17, (byte) 0xc3, (byte) 0x21 }),
             (byte) 77, Collections.singletonList(
-                    // KT OUI: 00:17:C3, type: 17. See b/170928882.
-                    new byte[]{ (byte) 0x00, (byte) 0x17, (byte) 0xc3, (byte) 0x11 })
+                    // KT OUI: 00:17:C3, type: 33(0x21). See b/236745261.
+                    new byte[]{ (byte) 0x00, (byte) 0x17, (byte) 0xc3, (byte) 0x21 })
     );
 
     // Initialize configurable particular SSID set supporting DHCP Roaming feature. See
@@ -644,6 +663,9 @@
     // Ignore nonzero RDNSS option lifetimes below this value. 0 = disabled.
     private final int mMinRdnssLifetimeSec;
 
+    // Ignore any nonzero RA section with lifetime below this value.
+    private final int mAcceptRaMinLft;
+
     // Experiment flag read from device config.
     private final boolean mDhcp6PrefixDelegationEnabled;
 
@@ -660,7 +682,7 @@
     private DhcpResults mDhcpResults;
     private String mTcpBufferSizes;
     private ProxyInfo mHttpProxy;
-    private ApfFilter mApfFilter;
+    private AndroidPacketFilter mApfFilter;
     private String mL2Key; // The L2 key for this network, for writing into the memory store
     private String mCluster; // The cluster for this network, for writing into the memory store
     private boolean mMulticastFiltering;
@@ -779,12 +801,18 @@
 
         /**
          * Return whether a feature guarded by a feature flag is enabled.
-         * @see NetworkStackUtils#isFeatureEnabled(Context, String, String)
+         * @see DeviceConfigUtils#isNetworkStackFeatureEnabled(Context, String)
          */
-        public boolean isFeatureEnabled(final Context context, final String name,
-                boolean defaultEnabled) {
-            return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name,
-                    defaultEnabled);
+        public boolean isFeatureEnabled(final Context context, final String name) {
+            return DeviceConfigUtils.isNetworkStackFeatureEnabled(context, name);
+        }
+
+        /**
+         * Check whether one specific feature is not disabled.
+         * @see DeviceConfigUtils#isNetworkStackFeatureNotChickenedOut(Context, String)
+         */
+        public boolean isFeatureNotChickenedOut(final Context context, final String name) {
+            return DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(context, name);
         }
 
         /**
@@ -792,18 +820,23 @@
          * APF programs.
          * @see ApfFilter#maybeCreate
          */
-        public ApfFilter maybeCreateApfFilter(Context context, ApfFilter.ApfConfiguration config,
-                InterfaceParams ifParams, IpClientCallbacksWrapper cb) {
-            return ApfFilter.maybeCreate(context, config, ifParams, cb);
+        public AndroidPacketFilter maybeCreateApfFilter(Context context,
+                ApfFilter.ApfConfiguration config, InterfaceParams ifParams,
+                IpClientCallbacksWrapper cb) {
+            if (isFeatureEnabled(context, NetworkStackUtils.APF_NEW_RA_FILTER_VERSION)) {
+                return ApfFilter.maybeCreate(context, config, ifParams, cb);
+            } else {
+                return LegacyApfFilter.maybeCreate(context, config, ifParams, cb);
+            }
         }
 
         /**
-         * Check whether one specific experimental feature in NetworkStack module from
-         * {@link DeviceConfig} is not disabled.
-         * @see DeviceConfigUtils#isNetworkStackFeatureNotChickenedOut(String)
+         * Check if a specific IPv6 sysctl file exists or not.
          */
-        public boolean isNetworkStackFeatureNotChickenedOut(final String name) {
-            return DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(name);
+        public boolean hasIpv6Sysctl(final String ifname, final String name) {
+            final String path = "/proc/sys/net/ipv6/conf/" + ifname + "/" + name;
+            final File sysctl = new File(path);
+            return sysctl.exists();
         }
     }
 
@@ -845,10 +878,12 @@
         mInterfaceCtrl = new InterfaceController(mInterfaceName, mNetd, mLog);
 
         mDhcp6PrefixDelegationEnabled = mDependencies.isFeatureEnabled(mContext,
-                IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION, false /* defaultEnabled */);
+                IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION);
 
         mMinRdnssLifetimeSec = mDependencies.getDeviceConfigPropertyInt(
                 CONFIG_MIN_RDNSS_LIFETIME, DEFAULT_MIN_RDNSS_LIFETIME);
+        mAcceptRaMinLft = mDependencies.getDeviceConfigPropertyInt(CONFIG_ACCEPT_RA_MIN_LFT,
+                DEFAULT_ACCEPT_RA_MIN_LFT);
 
         IpClientLinkObserver.Configuration config = new IpClientLinkObserver.Configuration(
                 mMinRdnssLifetimeSec);
@@ -1042,18 +1077,15 @@
     }
 
     private boolean isGratuitousNaEnabled() {
-        return mDependencies.isFeatureEnabled(mContext, IPCLIENT_GRATUITOUS_NA_VERSION,
-                false /* defaultEnabled */);
+        return mDependencies.isFeatureEnabled(mContext, IPCLIENT_GRATUITOUS_NA_VERSION);
     }
 
     private boolean isGratuitousArpNaRoamingEnabled() {
-        return mDependencies.isFeatureEnabled(mContext, IPCLIENT_GARP_NA_ROAMING_VERSION,
-                false /* defaultEnabled */);
+        return mDependencies.isFeatureEnabled(mContext, IPCLIENT_GARP_NA_ROAMING_VERSION);
     }
 
     private boolean isMulticastNsEnabled() {
-        return mDependencies.isFeatureEnabled(mContext, IPCLIENT_MULTICAST_NS_VERSION,
-                true /* defaultEnabled */);
+        return mDependencies.isFeatureNotChickenedOut(mContext, IPCLIENT_MULTICAST_NS_VERSION);
     }
 
     @VisibleForTesting
@@ -1253,7 +1285,7 @@
         }
 
         // Thread-unsafe access to mApfFilter but just used for debugging.
-        final ApfFilter apfFilter = mApfFilter;
+        final AndroidPacketFilter apfFilter = mApfFilter;
         final android.net.shared.ProvisioningConfiguration provisioningConfig = mConfiguration;
         final ApfCapabilities apfCapabilities = (provisioningConfig != null)
                 ? provisioningConfig.mApfCapabilities : null;
@@ -1459,19 +1491,20 @@
     // Set "/proc/sys/net/ipv6/conf/${iface}/${name}" with the given specific value.
     private void setIpv6Sysctl(@NonNull final String name, int value) {
         try {
-            mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mInterfaceParams.name,
+            mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mInterfaceName,
                     name, Integer.toString(value));
         } catch (Exception e) {
-            Log.e(mTag, "Failed to set " + name + " to  + " + value + ": " + e);
+            Log.e(mTag, "Failed to set " + name + " to " + value + ": " + e);
         }
     }
 
-    private Integer getIpv6DadTransmits() {
+    // Read "/proc/sys/net/ipv6/conf/${iface}/${name}".
+    private Integer getIpv6Sysctl(@NonNull final String name) {
         try {
-            return Integer.parseUnsignedInt(mNetd.getProcSysNet(INetd.IPV6, INetd.CONF,
-                    mInterfaceName, DAD_TRANSMITS));
+            return Integer.parseInt(mNetd.getProcSysNet(INetd.IPV6, INetd.CONF,
+                    mInterfaceName, name));
         } catch (RemoteException | ServiceSpecificException e) {
-            logError("Couldn't read dad_transmits on " + mInterfaceName, e);
+            logError("Couldn't read " + name + " on " + mInterfaceName, e);
             return null;
         }
     }
@@ -2121,12 +2154,19 @@
                 mConfiguration.mIPv6ProvisioningMode == PROV_IPV6_LINKLOCAL ? 0 : 2);
         setIpv6Sysctl(ACCEPT_RA_DEFRTR, acceptRaDefrtr);
         if (shouldDisableDad()) {
-            final Integer dadTransmits = getIpv6DadTransmits();
+            final Integer dadTransmits = getIpv6Sysctl(DAD_TRANSMITS);
             if (dadTransmits != null) {
                 mDadTransmits = dadTransmits;
                 setIpv6Sysctl(DAD_TRANSMITS, 0 /* dad_transmits */);
             }
         }
+        // Check chickened out flag first before reading IPv6 sysctl, which can prevent from
+        // triggering a potential kernel bug about the sysctl.
+        if (mDependencies.isFeatureNotChickenedOut(mContext,
+                IPCLIENT_IGNORE_LOW_RA_LIFETIME_FORCE_DISABLE)
+                && mDependencies.hasIpv6Sysctl(mInterfaceName, ACCEPT_RA_MIN_LFT)) {
+            setIpv6Sysctl(ACCEPT_RA_MIN_LFT, mAcceptRaMinLft);
+        }
         return mInterfaceCtrl.setIPv6PrivacyExtensions(true)
                 && mInterfaceCtrl.setIPv6AddrGenModeIfSupported(mConfiguration.mIPv6AddrGenMode)
                 && mInterfaceCtrl.enableIPv6();
@@ -2194,9 +2234,21 @@
         //     - disableIpv6() will clear autoconf IPv6 routes as well, and
         //     - we don't get IPv4 routes from netlink
         // so we neither react to nor need to wait for changes in either.
-
         mInterfaceCtrl.disableIPv6();
         mInterfaceCtrl.clearAllAddresses();
+
+        // Reset IPv6 sysctls to their initial state. It's better to restore
+        // sysctls after IPv6 stack is disabled, which prevents a potential
+        // race where receiving an RA between restoring accept_ra and disabling
+        // IPv6 stack, although it's unlikely.
+        setIpv6Sysctl(ACCEPT_RA, 2);
+        setIpv6Sysctl(ACCEPT_RA_DEFRTR, 1);
+        maybeRestoreDadTransmits();
+        if (mDependencies.isFeatureNotChickenedOut(mContext,
+                IPCLIENT_IGNORE_LOW_RA_LIFETIME_FORCE_DISABLE)
+                && mDependencies.hasIpv6Sysctl(mInterfaceName, ACCEPT_RA_MIN_LFT)) {
+            setIpv6Sysctl(ACCEPT_RA_MIN_LFT, 0 /* sysctl default */);
+        }
     }
 
     private void maybeSaveNetworkToIpMemoryStore() {
@@ -2284,7 +2336,7 @@
     }
 
     @Nullable
-    private ApfFilter maybeCreateApfFilter(final ApfCapabilities apfCapabilities) {
+    private AndroidPacketFilter maybeCreateApfFilter(final ApfCapabilities apfCapabilities) {
         ApfFilter.ApfConfiguration apfConfig = new ApfFilter.ApfConfiguration();
         apfConfig.apfCapabilities = apfCapabilities;
         apfConfig.multicastFilter = mMulticastFiltering;
@@ -2300,6 +2352,7 @@
         }
 
         apfConfig.minRdnssLifetimeSec = mMinRdnssLifetimeSec;
+        apfConfig.acceptRaMinLft = mAcceptRaMinLft;
         return mDependencies.maybeCreateApfFilter(mContext, apfConfig, mInterfaceParams,
                 mCallback);
     }
@@ -2415,8 +2468,6 @@
 
             // Restore the interface MTU to initial value if it has changed.
             maybeRestoreInterfaceMtu();
-            // Reset number of dad_transmits to default value if changed.
-            maybeRestoreDadTransmits();
             // Reset DTIM multiplier to default value if changed.
             if (mMaxDtimMultiplier != DTIM_MULTIPLIER_RESET) {
                 mCallback.setMaxDtimMultiplier(DTIM_MULTIPLIER_RESET);
@@ -3024,6 +3075,7 @@
 
                         case Dhcp6Client.DHCP6_PD_PREFIX_EXPIRED:
                         case Dhcp6Client.DHCP6_PD_PREFIX_CHANGED:
+                        case Dhcp6Client.DHCP6_PD_PREFIX_MSG_EXCHANGE_TERMINATED:
                             clearIpv6PrefixDelegationAddresses();
                             mPrefixDelegation = null;
                             handleLinkPropertiesUpdate(SEND_CALLBACKS);
diff --git a/src/android/net/ip/IpClientLinkObserver.java b/src/android/net/ip/IpClientLinkObserver.java
index 761f039..2068caa 100644
--- a/src/android/net/ip/IpClientLinkObserver.java
+++ b/src/android/net/ip/IpClientLinkObserver.java
@@ -19,6 +19,7 @@
 import static android.system.OsConstants.AF_INET6;
 import static android.system.OsConstants.AF_UNSPEC;
 import static android.system.OsConstants.IFF_LOOPBACK;
+
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
 import static com.android.net.module.util.netlink.NetlinkConstants.IFF_LOWER_UP;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_F_CLONED;
@@ -188,7 +189,7 @@
         mDnsServerRepository = new DnsServerRepository(config.minRdnssLifetime);
         mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
         mDependencies = deps;
-        mNetlinkEventParsingEnabled = deps.isNetworkStackFeatureNotChickenedOut(
+        mNetlinkEventParsingEnabled = deps.isFeatureNotChickenedOut(context,
                 IPCLIENT_PARSE_NETLINK_EVENTS_FORCE_DISABLE);
         mNetlinkMonitor = new MyNetlinkMonitor(h, log, mTag);
         mHandler.post(() -> {
@@ -203,8 +204,8 @@
     }
 
     private boolean isIpv6LinkLocalDnsAccepted() {
-        return mDependencies.isFeatureEnabled(mContext,
-                IPCLIENT_ACCEPT_IPV6_LINK_LOCAL_DNS_VERSION, true /* default value */);
+        return mDependencies.isFeatureNotChickenedOut(mContext,
+                IPCLIENT_ACCEPT_IPV6_LINK_LOCAL_DNS_VERSION);
     }
 
     private void maybeLog(String operation, String iface, LinkAddress address) {
diff --git a/src/android/net/ip/IpReachabilityMonitor.java b/src/android/net/ip/IpReachabilityMonitor.java
index 4d40c4f..40a4bb6 100644
--- a/src/android/net/ip/IpReachabilityMonitor.java
+++ b/src/android/net/ip/IpReachabilityMonitor.java
@@ -20,7 +20,6 @@
 import static android.net.metrics.IpReachabilityEvent.NUD_FAILED_ORGANIC;
 import static android.net.metrics.IpReachabilityEvent.PROVISIONING_LOST;
 import static android.net.metrics.IpReachabilityEvent.PROVISIONING_LOST_ORGANIC;
-import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION;
@@ -182,7 +181,8 @@
     public interface Dependencies {
         void acquireWakeLock(long durationMs);
         IpNeighborMonitor makeIpNeighborMonitor(Handler h, SharedLog log, NeighborEventConsumer cb);
-        boolean isFeatureEnabled(Context context, String name, boolean defaultEnabled);
+        boolean isFeatureEnabled(Context context, String name);
+        boolean isFeatureNotChickenedOut(Context context, String name);
         IpReachabilityMonitorMetrics getIpReachabilityMonitorMetrics();
 
         static Dependencies makeDefault(Context context, String iface) {
@@ -200,10 +200,12 @@
                     return new IpNeighborMonitor(h, log, cb);
                 }
 
-                public boolean isFeatureEnabled(final Context context, final String name,
-                        boolean defaultEnabled) {
-                    return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name,
-                            defaultEnabled);
+                public boolean isFeatureEnabled(final Context context, final String name) {
+                    return DeviceConfigUtils.isNetworkStackFeatureEnabled(context, name);
+                }
+
+                public boolean isFeatureNotChickenedOut(final Context context, final String name) {
+                    return DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(context, name);
                 }
 
                 public IpReachabilityMonitorMetrics getIpReachabilityMonitorMetrics() {
@@ -258,14 +260,12 @@
         mUsingMultinetworkPolicyTracker = usingMultinetworkPolicyTracker;
         mCm = context.getSystemService(ConnectivityManager.class);
         mDependencies = dependencies;
-        mMulticastResolicitEnabled = dependencies.isFeatureEnabled(context,
-                IP_REACHABILITY_MCAST_RESOLICIT_VERSION, true /* defaultEnabled */);
-        mIgnoreIncompleteIpv6DnsServerEnabled = dependencies.isFeatureEnabled(context,
-                IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION,
-                true /* defaultEnabled */);
+        mMulticastResolicitEnabled = dependencies.isFeatureNotChickenedOut(context,
+                IP_REACHABILITY_MCAST_RESOLICIT_VERSION);
+        mIgnoreIncompleteIpv6DnsServerEnabled = dependencies.isFeatureNotChickenedOut(context,
+                IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION);
         mIgnoreIncompleteIpv6DefaultRouterEnabled = dependencies.isFeatureEnabled(context,
-                IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION,
-                false /* defaultEnabled */);
+                IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION);
         mMetricsLog = metricsLog;
         mNetd = netd;
         Preconditions.checkNotNull(mNetd);
diff --git a/src/com/android/networkstack/netlink/TcpInfo.java b/src/com/android/networkstack/netlink/TcpInfo.java
index 31a408f..73d206e 100644
--- a/src/com/android/networkstack/netlink/TcpInfo.java
+++ b/src/com/android/networkstack/netlink/TcpInfo.java
@@ -19,8 +19,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-
-import com.android.internal.annotations.VisibleForTesting;
+import androidx.annotation.VisibleForTesting;
 
 import java.nio.BufferOverflowException;
 import java.nio.BufferUnderflowException;
@@ -97,10 +96,35 @@
     static final int SEGS_IN_OFFSET = getFieldOffset(Field.SEGS_IN);
     @VisibleForTesting
     static final int SEGS_OUT_OFFSET = getFieldOffset(Field.SEGS_OUT);
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    static final int TOTAL_RETRANS_OFFSET = getFieldOffset(Field.TOTAL_RETRANS);
+    /**
+     * This counts individual incoming packets that appeared on the wire, including:
+     * SYN, SYN-ACK, pure ACKs, data segments (after segmentation offload into small <=mtu
+     * packets), FIN, FIN-ACK, and any retransmits.
+     *
+     * This field is read from the tcpi_segs_in field from {@code struct tcp_info}
+     * in bionic/libc/kernel/uapi/linux/tcp.h. Also see [tcpEStatsPerfSegsIn] in the RFC4898.
+     */
     final int mSegsIn;
+    /**
+     * This counts individual outgoing packets that have been sent to the network, including:
+     * SYN, SYN-ACK, pure ACKs, data segments (after segmentation offload into small <=mtu
+     * packets), FIN, FIN-ACK, and any retransmits.
+     *
+     * This field is read from the tcpi_segs_out field from {@code struct tcp_info}
+     * in bionic/libc/kernel/uapi/linux/tcp.h. Also see [tcpEStatsPerfSegsOut] in the RFC4898.
+     */
     final int mSegsOut;
-    final int mLost;
-    final int mRetransmits;
+    /**
+     * This counts individual accumulated retransmitted packets that have been sent to the network,
+     * including any retransmits for SYN, SYN-ACK, pure ACKs, data segments (after segmentation
+     * offload into small <=mtu packets), FIN and FIN-ACK.
+     *
+     * This field is read from the tcpi_total_retrans field from {@code struct tcp_info}
+     * in bionic/libc/kernel/uapi/linux/tcp.h.
+     */
+    final int mTotalRetrans;
 
     private static int getFieldOffset(@NonNull final Field needle) {
         int offset = 0;
@@ -120,20 +144,18 @@
         final int start = bytes.position();
         mSegsIn = bytes.getInt(start + SEGS_IN_OFFSET);
         mSegsOut = bytes.getInt(start + SEGS_OUT_OFFSET);
-        mLost = bytes.getInt(start + LOST_OFFSET);
-        mRetransmits = bytes.get(start + RETRANSMITS_OFFSET);
+        mTotalRetrans = bytes.get(start + TOTAL_RETRANS_OFFSET);
         // tcp_info structure grows over time as new fields are added. Jump to the end of the
         // structure, as unknown fields might remain at the end of the structure if the tcp_info
         // struct was expanded.
         bytes.position(Math.min(infolen + start, bytes.limit()));
     }
 
-    @VisibleForTesting
-    TcpInfo(int retransmits, int lost, int segsOut, int segsIn) {
-        mRetransmits = retransmits;
-        mLost = lost;
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    TcpInfo(int segsOut, int segsIn, int totalRetrans) {
         mSegsOut = segsOut;
         mSegsIn = segsIn;
+        mTotalRetrans = totalRetrans;
     }
 
     /** Parse a TcpInfo from a giving ByteBuffer with a specific length. */
@@ -180,17 +202,17 @@
         TcpInfo other = (TcpInfo) obj;
 
         return mSegsIn == other.mSegsIn && mSegsOut == other.mSegsOut
-            && mRetransmits == other.mRetransmits && mLost == other.mLost;
+                && mTotalRetrans == other.mTotalRetrans;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mLost, mRetransmits, mSegsIn, mSegsOut);
+        return Objects.hash(mSegsIn, mSegsOut, mTotalRetrans);
     }
 
     @Override
     public String toString() {
-        return "TcpInfo{lost=" + mLost + ", retransmit=" + mRetransmits + ", received=" + mSegsIn
-                + ", sent=" + mSegsOut + "}";
+        return "TcpInfo{received=" + mSegsIn + ", sent=" + mSegsOut
+                + ", totalRetrans=" + mTotalRetrans + "}";
     }
 }
diff --git a/src/com/android/networkstack/netlink/TcpSocketTracker.java b/src/com/android/networkstack/netlink/TcpSocketTracker.java
index c0a5e64..fea858f 100644
--- a/src/com/android/networkstack/netlink/TcpSocketTracker.java
+++ b/src/com/android/networkstack/netlink/TcpSocketTracker.java
@@ -19,6 +19,8 @@
 import static android.net.util.DataStallUtils.CONFIG_TCP_PACKETS_FAIL_PERCENTAGE;
 import static android.net.util.DataStallUtils.DEFAULT_DATA_STALL_MIN_PACKETS_THRESHOLD;
 import static android.net.util.DataStallUtils.DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE;
+import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED;
+import static android.os.PowerManager.ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 import static android.system.OsConstants.AF_INET;
 import static android.system.OsConstants.AF_INET6;
@@ -31,7 +33,9 @@
 import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
 import static com.android.net.module.util.netlink.NetlinkUtils.DEFAULT_RECV_BUFSIZE;
 import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
+import static com.android.networkstack.util.NetworkStackUtils.SKIP_TCP_POLL_IN_LIGHT_DOZE;
 
+import android.annotation.TargetApi;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -60,15 +64,15 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.DeviceConfigUtils;
 import com.android.net.module.util.SocketUtils;
 import com.android.net.module.util.netlink.InetDiagMessage;
-import com.android.net.module.util.netlink.NetlinkConstants;
 import com.android.net.module.util.netlink.NetlinkUtils;
 import com.android.net.module.util.netlink.StructInetDiagMsg;
+import com.android.net.module.util.netlink.StructNlAttr;
 import com.android.net.module.util.netlink.StructNlMsgHdr;
 import com.android.networkstack.apishim.NetworkShimImpl;
-import com.android.networkstack.apishim.common.ShimUtils;
 import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
 
 import java.io.FileDescriptor;
@@ -136,6 +140,8 @@
     @NonNull
     private LinkProperties mLinkProperties;
 
+    private final boolean mShouldDisableInLightDoze;
+
     @VisibleForTesting
     protected final DeviceConfig.OnPropertiesChangedListener mConfigListener =
             new DeviceConfig.OnPropertiesChangedListener() {
@@ -152,14 +158,30 @@
                 }
             };
 
+    private static boolean isDeviceIdleModeChangedAction(Intent intent) {
+        return ACTION_DEVICE_IDLE_MODE_CHANGED.equals(intent.getAction());
+    }
+
+    @TargetApi(Build.VERSION_CODES.TIRAMISU)
+    private boolean isDeviceLightIdleModeChangedAction(Intent intent) {
+        return mShouldDisableInLightDoze
+                && ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED.equals(intent.getAction());
+    }
+
     final BroadcastReceiver mDeviceIdleReceiver = new BroadcastReceiver() {
         @Override
+        @TargetApi(Build.VERSION_CODES.TIRAMISU)
         public void onReceive(Context context, Intent intent) {
             if (intent == null) return;
 
-            if (PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED.equals(intent.getAction())) {
+            if (isDeviceIdleModeChangedAction(intent)
+                    || isDeviceLightIdleModeChangedAction(intent)) {
                 final PowerManager powerManager = context.getSystemService(PowerManager.class);
-                final boolean deviceIdle = powerManager.isDeviceIdleMode();
+                // For tcp polling mechanism, there is no difference between deep doze mode and
+                // light doze mode. The deep doze mode and light doze mode block networking
+                // for uids in the same way, use single variable to control.
+                final boolean deviceIdle = powerManager.isDeviceIdleMode()
+                        || (mShouldDisableInLightDoze && powerManager.isDeviceLightIdleMode());
                 setDozeMode(deviceIdle);
             }
         }
@@ -169,6 +191,7 @@
         mDependencies = dps;
         mNetwork = network;
         mNetd = mDependencies.getNetd();
+        mShouldDisableInLightDoze = mDependencies.shouldDisableInLightDoze();
 
         // If the parcel is null, nothing should be matched which is achieved by the combination of
         // {@code NetlinkUtils#NULL_MASK} and {@code NetlinkUtils#UNKNOWN_MARK}.
@@ -176,16 +199,13 @@
         mNetworkMark = (parcel != null) ? parcel.mark : NetlinkUtils.UNKNOWN_MARK;
         mNetworkMask = (parcel != null) ? parcel.mask : NetlinkUtils.NULL_MASK;
 
-        // Request tcp info from NetworkStack directly needs extra SELinux permission added after Q
-        // release.
-        if (!mDependencies.isTcpInfoParsingSupported()) return;
         // Build SocketDiag messages.
         for (final int family : ADDRESS_FAMILIES) {
             mSockDiagMsg.put(
                     family, InetDiagMessage.buildInetDiagReqForAliveTcpSockets(family));
         }
         mDependencies.addDeviceConfigChangedListener(mConfigListener);
-        mDependencies.addDeviceIdleReceiver(mDeviceIdleReceiver);
+        mDependencies.addDeviceIdleReceiver(mDeviceIdleReceiver, mShouldDisableInLightDoze);
     }
 
     @Nullable
@@ -208,7 +228,6 @@
      * @Return if this polling request is sent to kernel and executes successfully or not.
      */
     public boolean pollSocketsInfo() {
-        if (!mDependencies.isTcpInfoParsingSupported()) return false;
         // Traffic will be restricted in doze mode. TCP info may not reflect the correct network
         // behavior.
         // TODO: Traffic may be restricted by other reason. Get the restriction info from bpf in T+.
@@ -263,7 +282,7 @@
                     ? (mSentSinceLastRecv + stat.sentCount) : 0;
             mLatestReceivedCount = stat.receivedCount;
             mLatestPacketFailPercentage = ((stat.sentCount != 0)
-                    ? ((stat.retransmitCount + stat.lostCount) * 100 / stat.sentCount) : 0);
+                    ? (stat.retransCount * 100 / stat.sentCount) : 0);
 
             // Remove out-of-date socket info.
             cleanupSocketInfo(time);
@@ -382,18 +401,13 @@
         int mark = NetlinkUtils.INIT_MARK_VALUE;
         // Get a tcp_info.
         while (bytes.position() < remainingDataSize) {
-            final RoutingAttribute rtattr =
-                    new RoutingAttribute(bytes.getShort(), bytes.getShort());
-            final short dataLen = rtattr.getDataLength();
-            if (rtattr.rtaType == NetlinkUtils.INET_DIAG_INFO) {
-                tcpInfo = TcpInfo.parse(bytes, dataLen);
-            } else if (rtattr.rtaType == NetlinkUtils.INET_DIAG_MARK) {
-                mark = bytes.getInt();
-            } else {
-                // Data provided by kernel will include both valid data and padding data. The data
-                // len provided from kernel indicates the valid data size. Readers must deduce the
-                // alignment by themselves.
-                skipRemainingAttributesBytesAligned(bytes, dataLen);
+            final StructNlAttr nlattr = StructNlAttr.parse(bytes);
+            if (nlattr == null) break;
+
+            if (nlattr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
+                mark = nlattr.getValueAsInteger();
+            } else if (nlattr.nla_type == NetlinkUtils.INET_DIAG_INFO) {
+                tcpInfo = TcpInfo.parse(nlattr.getValueAsByteBuffer(), nlattr.getAlignedLength());
             }
         }
         final SocketInfo info = new SocketInfo(tcpInfo, family, mark, time, uid, cookie, dstPort);
@@ -407,8 +421,6 @@
      * statemachine thread of NetworkMonitor.
      */
     public boolean isDataStallSuspected() {
-        if (!mDependencies.isTcpInfoParsingSupported()) return false;
-
         // Skip checking data stall since the traffic will be restricted and it will not be real
         // network stall.
         // TODO: Traffic may be restricted by other reason. Get the restriction info from bpf in T+.
@@ -439,14 +451,12 @@
 
         stat.sentCount = current.tcpInfo.mSegsOut;
         stat.receivedCount = current.tcpInfo.mSegsIn;
-        stat.lostCount = current.tcpInfo.mLost;
-        stat.retransmitCount = current.tcpInfo.mRetransmits;
+        stat.retransCount = current.tcpInfo.mTotalRetrans;
 
         if (previous != null && previous.tcpInfo != null) {
             stat.sentCount -= previous.tcpInfo.mSegsOut;
             stat.receivedCount -= previous.tcpInfo.mSegsIn;
-            stat.lostCount -= previous.tcpInfo.mLost;
-            stat.retransmitCount -= previous.tcpInfo.mRetransmits;
+            stat.retransCount -= previous.tcpInfo.mTotalRetrans;
         }
         log("calculateLatestPacketsStat, stat:" + stat);
         return stat;
@@ -458,7 +468,6 @@
      * @return the latest packet fail percentage. -1 denotes that there is no available data.
      */
     public int getLatestPacketFailPercentage() {
-        if (!mDependencies.isTcpInfoParsingSupported()) return -1;
         // Only return fail rate if device sent enough packets.
         if (getSentSinceLastRecv() < getMinPacketsThreshold()) return -1;
         return mLatestPacketFailPercentage;
@@ -469,13 +478,11 @@
      * between each polling period, not an accurate number.
      */
     public int getSentSinceLastRecv() {
-        if (!mDependencies.isTcpInfoParsingSupported()) return -1;
         return mSentSinceLastRecv;
     }
 
     /** Return the number of the packets received in the latest polling cycle. */
     public int getLatestReceivedCount() {
-        if (!mDependencies.isTcpInfoParsingSupported()) return -1;
         return mLatestReceivedCount;
     }
 
@@ -491,70 +498,17 @@
         return mTcpPacketsFailRateThreshold;
     }
 
-    /**
-     * Method to skip the remaining attributes bytes.
-     * Corresponds to NLMSG_NEXT in bionic/libc/kernel/uapi/linux/netlink.h.
-     *
-     * @param buffer the target ByteBuffer
-     * @param len the remaining length to skip.
-     */
-    private static void skipRemainingAttributesBytesAligned(@NonNull final ByteBuffer buffer,
-            final short len) {
-        // Data in {@Code RoutingAttribute} is followed after header with size {@Code NLA_ALIGNTO}
-        // bytes long for each block. Next attribute will start after the padding bytes if any.
-        // If all remaining bytes after header are valid in a data block, next attr will just start
-        // after valid bytes.
-        //
-        // E.g. With NLA_ALIGNTO(4), an attr struct with length 5 means 1 byte valid data remains
-        // after header and 3(4-1) padding bytes. Next attr with length 8 will start after the
-        // padding bytes and contain 4(8-4) valid bytes of data. The next attr start after the
-        // valid bytes, like:
-        //
-        // [HEADER(L=5)][   4-Bytes DATA      ][ HEADER(L=8) ][4 bytes DATA][Next attr]
-        // [ 5 valid bytes ][3 padding bytes  ][      8 valid bytes        ]   ...
-        final int cur = buffer.position();
-        buffer.position(cur + NetlinkConstants.alignedLengthOf(len));
-    }
-
     private static void log(final String str) {
         if (DBG) Log.d(TAG, str);
     }
 
     /** Stops monitoring and releases resources. */
     public void quit() {
-        // Do not need to unregister receiver and listener since registration is skipped
-        // in the constructor.
-        if (!mDependencies.isTcpInfoParsingSupported()) return;
-
         mDependencies.removeDeviceConfigChangedListener(mConfigListener);
         mDependencies.removeBroadcastReceiver(mDeviceIdleReceiver);
     }
 
     /**
-     * Corresponds to {@code struct rtattr} from bionic/libc/kernel/uapi/linux/rtnetlink.h
-     *
-     * struct rtattr {
-     *    unsigned short rta_len;    // Length of option
-     *    unsigned short rta_type;   // Type of option
-     *    // Data follows
-     * };
-     */
-    static class RoutingAttribute {
-        public static final int HEADER_LENGTH = 4;
-
-        public final short rtaLen;  // The whole valid size of the struct.
-        public final short rtaType;
-
-        RoutingAttribute(final short len, final short type) {
-            rtaLen = len;
-            rtaType = type;
-        }
-        public short getDataLength() {
-            return (short) (rtaLen - HEADER_LENGTH);
-        }
-    }
-
-    /**
      * Data class for keeping the socket info.
      */
     @VisibleForTesting
@@ -609,23 +563,21 @@
      * */
     private class TcpStat {
         public int sentCount;
-        public int lostCount;
-        public int retransmitCount;
         public int receivedCount;
+        public int retransCount;
 
         void accumulate(@Nullable final TcpStat stat) {
             if (stat == null) return;
 
             sentCount += stat.sentCount;
-            lostCount += stat.lostCount;
             receivedCount += stat.receivedCount;
-            retransmitCount += stat.retransmitCount;
+            retransCount += stat.retransCount;
         }
 
         @Override
         public String toString() {
-            return "TcpStat {sent=" + sentCount + ", lost=" + lostCount
-                    + ", retransmit=" + retransmitCount + ", received=" + receivedCount + "}";
+            return "TcpStat {sent=" + sentCount + ", retransCount=" + retransCount
+                    + ", received=" + receivedCount + "}";
         }
     }
 
@@ -699,15 +651,6 @@
         }
 
         /**
-         * Return if request tcp info via netlink socket is supported or not.
-         */
-        public boolean isTcpInfoParsingSupported() {
-            // Request tcp info from NetworkStack directly needs extra SELinux permission added
-            // after Q release.
-            return ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q);
-        }
-
-        /**
          * Receive the request message from kernel via given fd.
          */
         public ByteBuffer recvMessage(@NonNull final FileDescriptor fd)
@@ -741,14 +684,31 @@
         }
 
         /** Add receiver for detecting doze mode change to control TCP detection. */
-        public void addDeviceIdleReceiver(@NonNull final BroadcastReceiver receiver) {
-            mContext.registerReceiver(receiver,
-                    new IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
+        @TargetApi(Build.VERSION_CODES.TIRAMISU)
+        public void addDeviceIdleReceiver(@NonNull final BroadcastReceiver receiver,
+                boolean shouldDisableInLightDoze) {
+            final IntentFilter intentFilter = new IntentFilter(ACTION_DEVICE_IDLE_MODE_CHANGED);
+            if (shouldDisableInLightDoze) {
+                intentFilter.addAction(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED);
+            }
+            mContext.registerReceiver(receiver, intentFilter);
         }
 
         /** Remove broadcast receiver. */
         public void removeBroadcastReceiver(@NonNull final BroadcastReceiver receiver) {
             mContext.unregisterReceiver(receiver);
         }
+
+        /**
+         * Get whether polling should be disabled in light doze mode. This method should
+         * only be called once in the constructor, to ensure that the code does not need
+         * to deal with flag values changing at runtime.
+         */
+        @TargetApi(Build.VERSION_CODES.TIRAMISU)
+        public boolean shouldDisableInLightDoze() {
+            // Light doze mode status checking API is only available at T or later releases.
+            return SdkLevel.isAtLeastT() && DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(
+                    mContext, SKIP_TCP_POLL_IN_LIGHT_DOZE);
+        }
     }
 }
diff --git a/src/com/android/networkstack/util/NetworkStackUtils.java b/src/com/android/networkstack/util/NetworkStackUtils.java
index 05f4a52..c9589e3 100755
--- a/src/com/android/networkstack/util/NetworkStackUtils.java
+++ b/src/com/android/networkstack/util/NetworkStackUtils.java
@@ -267,6 +267,11 @@
     public static final String IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION =
             "ipclient_dhcpv6_prefix_delegation_version";
 
+    /**
+     * Experiment flag to enable new ra filter.
+     */
+    public static final String APF_NEW_RA_FILTER_VERSION = "apf_new_ra_filter_version";
+
     /**** BEGIN Feature Kill Switch Flags ****/
 
     /**
@@ -276,6 +281,19 @@
     public static final String IPCLIENT_PARSE_NETLINK_EVENTS_FORCE_DISABLE =
             "ipclient_parse_netlink_events_force_disable";
 
+    /**
+     * Kill switch flag to disable the feature of ignoring any individual RA section with lifetime
+     * below accept_ra_min_lft sysctl.
+     */
+    public static final String IPCLIENT_IGNORE_LOW_RA_LIFETIME_FORCE_DISABLE =
+            "ipclient_ignore_low_ra_lifetime_force_disable";
+
+    /**
+     * Kill switch flag to disable the feature of skipping Tcp socket info polling when light
+     * doze mode is enabled.
+     */
+    public static final String SKIP_TCP_POLL_IN_LIGHT_DOZE = "skip_tcp_poll_in_light_doze_mode";
+
     static {
         System.loadLibrary("networkstackutilsjni");
     }
@@ -375,8 +393,7 @@
      * Attaches a socket filter that accepts ICMPv6 router advertisements to the given socket.
      * @param fd the socket's {@link FileDescriptor}.
      */
-    public static native void attachRaFilter(FileDescriptor fd)
-            throws SocketException;
+    public static native void attachRaFilter(FileDescriptor fd) throws ErrnoException;
 
     /**
      * Attaches a socket filter that accepts L2-L4 signaling traffic required for IP connectivity.
@@ -384,10 +401,8 @@
      * This includes: all ARP, ICMPv6 RS/RA/NS/NA messages, and DHCPv4 exchanges.
      *
      * @param fd the socket's {@link FileDescriptor}.
-     * @param packetType the hardware address type, one of ARPHRD_*.
      */
-    public static native void attachControlPacketFilter(FileDescriptor fd, int packetType)
-            throws SocketException;
+    public static native void attachControlPacketFilter(FileDescriptor fd) throws ErrnoException;
 
     /**
      * Add an entry into the ARP cache.
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index 3be8979..5b551a0 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -125,7 +125,6 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
-import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.stats.connectivity.ProbeResult;
 import android.stats.connectivity.ProbeType;
@@ -627,8 +626,8 @@
         mTestCaptivePortalHttpUrl = getTestUrl(TEST_CAPTIVE_PORTAL_HTTP_URL, validationLogs, deps);
         mIsCaptivePortalCheckEnabled = getIsCaptivePortalCheckEnabled(context, deps);
         mPrivateIpNoInternetEnabled = getIsPrivateIpNoInternetEnabled();
-        mMetricsEnabled = deps.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY,
-                NetworkStackUtils.VALIDATION_METRICS_VERSION, true /* defaultEnabled */);
+        mMetricsEnabled = deps.isFeatureNotChickenedOut(context,
+                NetworkStackUtils.VALIDATION_METRICS_VERSION);
         mUseHttps = getUseHttpsValidation();
         mCaptivePortalUserAgent = getCaptivePortalUserAgent();
         mCaptivePortalFallbackSpecs =
@@ -3325,27 +3324,19 @@
          * Check whether or not one experimental feature in the connectivity namespace is
          * enabled.
          * @param name Flag name of the experiment in the connectivity namespace.
-         * @see DeviceConfigUtils#isFeatureEnabled(Context, String, String)
+         * @see DeviceConfigUtils#isNetworkStackFeatureEnabled(Context, String)
          */
         public boolean isFeatureEnabled(@NonNull Context context, @NonNull String name) {
-            return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name);
+            return DeviceConfigUtils.isNetworkStackFeatureEnabled(context, name);
         }
 
         /**
-         * Check whether or not one specific experimental feature for a particular namespace from
-         * {@link DeviceConfig} is enabled by comparing NetworkStack module version
-         * {@link NetworkStack} with current version of property. If this property version is valid,
-         * the corresponding experimental feature would be enabled, otherwise disabled.
-         * @param context The global context information about an app environment.
-         * @param namespace The namespace containing the property to look up.
-         * @param name The name of the property to look up.
-         * @param defaultEnabled The value to return if the property does not exist or its value is
-         *                       null.
-         * @return true if this feature is enabled, or false if disabled.
+         * Check whether one specific feature is not disabled.
+         * @param name Flag name of the experiment in the connectivity namespace.
+         * @see DeviceConfigUtils#isNetworkStackFeatureNotChickenedOut(Context, String)
          */
-        public boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
-                @NonNull String name, boolean defaultEnabled) {
-            return DeviceConfigUtils.isFeatureEnabled(context, namespace, name, defaultEnabled);
+        public boolean isFeatureNotChickenedOut(@NonNull Context context, @NonNull String name) {
+            return DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(context, name);
         }
 
         /**
diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
index 83043e5..c54906d 100644
--- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
+++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
@@ -40,6 +40,8 @@
 import static android.net.dhcp.DhcpPacket.MIN_V6ONLY_WAIT_MS;
 import static android.net.ip.IIpClientCallbacks.DTIM_MULTIPLIER_RESET;
 import static android.net.ip.IpClient.CONFIG_IPV6_AUTOCONF_TIMEOUT;
+import static android.net.ip.IpClient.CONFIG_ACCEPT_RA_MIN_LFT;
+import static android.net.ip.IpClient.DEFAULT_ACCEPT_RA_MIN_LFT;
 import static android.net.ip.IpClientLinkObserver.CLAT_PREFIX;
 import static android.net.ip.IpClientLinkObserver.CONFIG_SOCKET_RECV_BUFSIZE;
 import static android.net.ip.IpReachabilityMonitor.NUD_MCAST_RESOLICIT_NUM;
@@ -50,6 +52,7 @@
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 import static android.system.OsConstants.IPPROTO_IPV6;
 import static android.system.OsConstants.IPPROTO_UDP;
+
 import static com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress;
 import static com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address;
 import static com.android.net.module.util.NetworkStackConstants.ALL_DHCP_RELAY_AGENTS_AND_SERVERS;
@@ -78,7 +81,9 @@
 import static com.android.testutils.MiscAsserts.assertThrows;
 import static com.android.testutils.ParcelUtils.parcelingRoundTrip;
 import static com.android.testutils.TestPermissionUtil.runAsShell;
+
 import static junit.framework.Assert.fail;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -174,6 +179,7 @@
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.ArrayMap;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.test.InstrumentationRegistry;
@@ -276,6 +282,7 @@
 @RunWith(Parameterized.class)
 @SmallTest
 public abstract class IpClientIntegrationTestCommon {
+    private static final String TAG = IpClientIntegrationTestCommon.class.getSimpleName();
     private static final int DATA_BUFFER_LEN = 4096;
     private static final int PACKET_TIMEOUT_MS = 5_000;
     private static final String TEST_CLUSTER = "some cluster";
@@ -438,7 +445,9 @@
     private static final byte[] TEST_HOTSPOT_OUI = new byte[] {
             (byte) 0x00, (byte) 0x17, (byte) 0xF2
     };
-    private static final byte TEST_VENDOR_SPECIFIC_TYPE = 0x06;
+    private static final byte LEGACY_TEST_VENDOR_SPECIFIC_IE_TYPE = 0x11;
+    private static final byte TEST_VENDOR_SPECIFIC_IE_TYPE = 0x21;
+    private static final int TEST_VENDOR_SPECIFIC_IE_ID = 0xdd;
 
     private static final String TEST_DEFAULT_SSID = "test_ssid";
     private static final String TEST_DEFAULT_BSSID = "00:11:22:33:44:55";
@@ -522,9 +531,13 @@
         }
 
         @Override
-        public boolean isFeatureEnabled(final Context context, final String name,
-                final boolean defaultEnabled) {
-            return IpClientIntegrationTestCommon.this.isFeatureEnabled(name, defaultEnabled);
+        public boolean isFeatureEnabled(final Context context, final String name) {
+            return IpClientIntegrationTestCommon.this.isFeatureEnabled(name);
+        }
+
+        @Override
+        public boolean isFeatureNotChickenedOut(final Context context, final String name) {
+            return IpClientIntegrationTestCommon.this.isFeatureNotChickenedOut(name);
         }
 
         @Override
@@ -543,9 +556,13 @@
                 NetworkStackIpMemoryStore ipMemoryStore, IpProvisioningMetrics metrics) {
             return new DhcpClient.Dependencies(ipMemoryStore, metrics) {
                 @Override
-                public boolean isFeatureEnabled(final Context context, final String name,
-                        final boolean defaultEnabled) {
-                    return Dependencies.this.isFeatureEnabled(context, name, defaultEnabled);
+                public boolean isFeatureEnabled(final Context context, final String name) {
+                    return Dependencies.this.isFeatureEnabled(context, name);
+                }
+
+                @Override
+                public boolean isFeatureNotChickenedOut(final Context context, final String name) {
+                    return Dependencies.this.isFeatureNotChickenedOut(context, name);
                 }
 
                 @Override
@@ -586,9 +603,12 @@
                     return new IpNeighborMonitor(h, log, cb);
                 }
 
-                public boolean isFeatureEnabled(final Context context, final String name,
-                        boolean defaultEnabled) {
-                    return Dependencies.this.isFeatureEnabled(context, name, defaultEnabled);
+                public boolean isFeatureEnabled(final Context context, final String name) {
+                    return Dependencies.this.isFeatureEnabled(context, name);
+                }
+
+                public boolean isFeatureNotChickenedOut(final Context context, final String name) {
+                    return Dependencies.this.isFeatureNotChickenedOut(context, name);
                 }
 
                 public IpReachabilityMonitorMetrics getIpReachabilityMonitorMetrics() {
@@ -622,7 +642,9 @@
 
     protected abstract void setFeatureEnabled(String name, boolean enabled);
 
-    protected abstract boolean isFeatureEnabled(String name, boolean defaultEnabled);
+    protected abstract boolean isFeatureEnabled(String name);
+
+    protected abstract boolean isFeatureNotChickenedOut(String name);
 
     protected abstract boolean useNetworkStackSignature();
 
@@ -667,7 +689,7 @@
     }
 
     private void setFeatureChickenedOut(String name, boolean chickenedOut) {
-        setDeviceConfigProperty(name, chickenedOut ? "1" : "0");
+        setDeviceConfigProperty(name, chickenedOut ? "-1" : "0");
     }
 
     protected void setDhcpFeatures(final boolean isDhcpLeaseCacheEnabled,
@@ -802,6 +824,10 @@
 
         // Set the timeout to wait IPv6 autoconf to complete.
         mDependencies.setDeviceConfigProperty(CONFIG_IPV6_AUTOCONF_TIMEOUT, 500);
+
+        // Set the minimal RA lifetime value, any RA section with liftime below this value will be
+        // ignored.
+        mDependencies.setDeviceConfigProperty(CONFIG_ACCEPT_RA_MIN_LFT, DEFAULT_ACCEPT_RA_MIN_LFT);
     }
 
     private void awaitIpClientShutdown() throws Exception {
@@ -2891,7 +2917,8 @@
     private ScanResultInfo makeScanResultInfo(final String ssid, final String bssid) {
         byte[] data = new byte[10];
         new Random().nextBytes(data);
-        return makeScanResultInfo(0xdd, ssid, bssid, TEST_AP_OUI, (byte) 0x06, data);
+        return makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, ssid, bssid, TEST_AP_OUI,
+                (byte) 0x06, data);
     }
 
     private void doUpstreamHotspotDetectionTest(final int id, final String displayName,
@@ -2935,7 +2962,7 @@
     public void testUpstreamHotspotDetection() throws Exception {
         byte[] data = new byte[10];
         new Random().nextBytes(data);
-        doUpstreamHotspotDetectionTest(0xdd, "\"ssid\"", "ssid",
+        doUpstreamHotspotDetectionTest(TEST_VENDOR_SPECIFIC_IE_ID, "\"ssid\"", "ssid",
                 new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data,
                 true /* expectMetered */);
     }
@@ -2953,7 +2980,7 @@
     public void testUpstreamHotspotDetection_incorrectOUI() throws Exception {
         byte[] data = new byte[10];
         new Random().nextBytes(data);
-        doUpstreamHotspotDetectionTest(0xdd, "\"ssid\"", "ssid",
+        doUpstreamHotspotDetectionTest(TEST_VENDOR_SPECIFIC_IE_ID, "\"ssid\"", "ssid",
                 new byte[] { (byte) 0x00, (byte) 0x1A, (byte) 0x11 }, (byte) 0x06, data,
                 false /* expectMetered */);
     }
@@ -2962,7 +2989,7 @@
     public void testUpstreamHotspotDetection_incorrectSsid() throws Exception {
         byte[] data = new byte[10];
         new Random().nextBytes(data);
-        doUpstreamHotspotDetectionTest(0xdd, "\"another ssid\"", "ssid",
+        doUpstreamHotspotDetectionTest(TEST_VENDOR_SPECIFIC_IE_ID, "\"another ssid\"", "ssid",
                 new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data,
                 false /* expectMetered */);
     }
@@ -2971,7 +2998,7 @@
     public void testUpstreamHotspotDetection_incorrectType() throws Exception {
         byte[] data = new byte[10];
         new Random().nextBytes(data);
-        doUpstreamHotspotDetectionTest(0xdd, "\"ssid\"", "ssid",
+        doUpstreamHotspotDetectionTest(TEST_VENDOR_SPECIFIC_IE_ID, "\"ssid\"", "ssid",
                 new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x0a, data,
                 false /* expectMetered */);
     }
@@ -2979,7 +3006,7 @@
     @Test
     public void testUpstreamHotspotDetection_zeroLengthData() throws Exception {
         byte[] data = new byte[0];
-        doUpstreamHotspotDetectionTest(0xdd, "\"ssid\"", "ssid",
+        doUpstreamHotspotDetectionTest(TEST_VENDOR_SPECIFIC_IE_ID, "\"ssid\"", "ssid",
                 new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xF2 }, (byte) 0x06, data,
                 true /* expectMetered */);
     }
@@ -3164,7 +3191,7 @@
         return lp;
     }
 
-    private void doDualStackProvisioning() throws Exception {
+    private LinkProperties doDualStackProvisioning() throws Exception {
         final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                 .withoutIpReachabilityMonitor()
                 .build();
@@ -3180,7 +3207,7 @@
             mIIpClient.startProvisioning(config.toStableParcelable());
         }
 
-        performDualStackProvisioning();
+        return performDualStackProvisioning();
     }
 
     private boolean hasRouteTo(@NonNull final LinkProperties lp, @NonNull final String prefix) {
@@ -3207,8 +3234,10 @@
     }
 
     @Test
+    @SignatureRequiredTest(reason = "Out of SLO flakiness")
     public void testIgnoreIpv6ProvisioningLoss_disableAcceptRaDefrtr() throws Exception {
-        doDualStackProvisioning();
+        LinkProperties lp = doDualStackProvisioning();
+        Log.d(TAG, "current LinkProperties: " + lp);
 
         final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
 
@@ -3229,7 +3258,8 @@
                     lpFuture.complete(x);
                     return true;
                 }));
-        final LinkProperties lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        Log.d(TAG, "After receiving RA with 0 router lifetime, LinkProperties: " + lp);
         assertNotNull(lp);
         assertEquals(lp.getAddresses().get(0), CLIENT_ADDR);
         assertEquals(lp.getDnsServers().get(0), SERVER_ADDR);
@@ -3243,7 +3273,7 @@
         // Wait for RS after IPv6 stack has been restarted and reply with a normal RA to verify
         // that device gains the IPv6 provisioning without default route and off-link DNS server.
         sendBasicRouterAdvertisement(true /* waitForRs */);
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
+        verify(mCb, timeout(TEST_TIMEOUT_MS).atLeastOnce()).onLinkPropertiesChange(argThat(
                 x -> x.hasGlobalIpv6Address()
                         // IPv4, IPv6 link local, privacy and stable privacy
                         && x.getLinkAddresses().size() == 4
@@ -3537,8 +3567,8 @@
 
     @Test
     public void testDiscoverCustomizedDhcpOptions() throws Exception {
-        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI,
-                (byte) 0x11 /* vendor-specific IE type */);
+        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
+                TEST_VENDOR_SPECIFIC_IE_TYPE);
         final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info,
                 false /* isDhcpLeaseCacheEnabled */);
 
@@ -3549,8 +3579,8 @@
 
     @Test
     public void testDiscoverCustomizedDhcpOptions_nullDhcpOptions() throws Exception {
-        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI,
-                (byte) 0x11 /* vendor-specific IE type */);
+        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
+                TEST_VENDOR_SPECIFIC_IE_TYPE);
         final DhcpPacket packet = doCustomizedDhcpOptionsTest(null /* options */, info,
                 false /* isDhcpLeaseCacheEnabled */);
 
@@ -3571,8 +3601,8 @@
 
     @Test
     public void testDiscoverCustomizedDhcpOptions_disallowedOui() throws Exception {
-        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */,
-                new byte[]{ 0x00, 0x11, 0x22} /* oui */, (byte) 0x11 /* vendor-specific IE type */);
+        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID,
+                new byte[]{ 0x00, 0x11, 0x22} /* oui */, TEST_VENDOR_SPECIFIC_IE_TYPE);
         final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info,
                 false /* isDhcpLeaseCacheEnabled */);
 
@@ -3584,7 +3614,7 @@
     @Test
     public void testDiscoverCustomizedDhcpOptions_invalidIeId() throws Exception {
         final ScanResultInfo info = makeScanResultInfo(0xde /* vendor-specific IE */, TEST_OEM_OUI,
-                (byte) 0x11 /* vendor-specific IE type */);
+                TEST_VENDOR_SPECIFIC_IE_TYPE);
         final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info,
                 false /* isDhcpLeaseCacheEnabled */);
 
@@ -3595,7 +3625,7 @@
 
     @Test
     public void testDiscoverCustomizedDhcpOptions_invalidVendorSpecificType() throws Exception {
-        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI,
+        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
                 (byte) 0x10 /* vendor-specific IE type */);
         final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info,
                 false /* isDhcpLeaseCacheEnabled */);
@@ -3606,14 +3636,26 @@
     }
 
     @Test
+    public void testDiscoverCustomizedDhcpOptions_legacyVendorSpecificType() throws Exception {
+        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
+                 LEGACY_TEST_VENDOR_SPECIFIC_IE_TYPE);
+        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info,
+                false /* isDhcpLeaseCacheEnabled */);
+
+        assertTrue(packet instanceof DhcpDiscoverPacket);
+        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
+        assertNull(packet.mUserClass);
+    }
+
+    @Test
     public void testDisoverCustomizedDhcpOptions_disallowedOption() throws Exception {
         final List<DhcpOption> options = Arrays.asList(
                 makeDhcpOption((byte) 60, TEST_OEM_VENDOR_ID.getBytes()),
                 makeDhcpOption((byte) 77, TEST_OEM_USER_CLASS_INFO),
                 // Option 26: MTU
                 makeDhcpOption((byte) 26, HexDump.toByteArray(TEST_DEFAULT_MTU)));
-        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI,
-                (byte) 0x11 /* vendor-specific IE type */);
+        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
+                TEST_VENDOR_SPECIFIC_IE_TYPE);
         final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info,
                 false /* isDhcpLeaseCacheEnabled */);
 
@@ -3630,8 +3672,8 @@
                 makeDhcpOption((byte) 77, TEST_OEM_USER_CLASS_INFO),
                 // NTP_SERVER
                 makeDhcpOption((byte) 42, null));
-        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI,
-                (byte) 0x11 /* vendor-specific IE type */);
+        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
+                TEST_VENDOR_SPECIFIC_IE_TYPE);
         final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info,
                 false /* isDhcpLeaseCacheEnabled */);
 
@@ -3646,8 +3688,8 @@
         final List<DhcpOption> options = Arrays.asList(
                 // DHCP_USER_CLASS
                 makeDhcpOption((byte) 77, null));
-        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI,
-                (byte) 0x11 /* vendor-specific IE type */);
+        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
+                TEST_VENDOR_SPECIFIC_IE_TYPE);
         final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info,
                 false /* isDhcpLeaseCacheEnabled */);
 
@@ -3660,8 +3702,8 @@
     public void testRequestCustomizedDhcpOptions() throws Exception {
         setUpRetrievedNetworkAttributesForInitRebootState();
 
-        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI,
-                (byte) 0x11 /* vendor-specific IE type */);
+        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
+                TEST_VENDOR_SPECIFIC_IE_TYPE);
         final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info,
                 true /* isDhcpLeaseCacheEnabled */);
 
@@ -3674,8 +3716,8 @@
     public void testRequestCustomizedDhcpOptions_nullDhcpOptions() throws Exception {
         setUpRetrievedNetworkAttributesForInitRebootState();
 
-        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI,
-                (byte) 0x11 /* vendor-specific IE type */);
+        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
+                TEST_VENDOR_SPECIFIC_IE_TYPE);
         final DhcpPacket packet = doCustomizedDhcpOptionsTest(null /* options */, info,
                 true /* isDhcpLeaseCacheEnabled */);
 
@@ -3700,8 +3742,8 @@
     public void testRequestCustomizedDhcpOptions_disallowedOui() throws Exception {
         setUpRetrievedNetworkAttributesForInitRebootState();
 
-        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */,
-                new byte[]{ 0x00, 0x11, 0x22} /* oui */, (byte) 0x11 /* vendor-specific IE type */);
+        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID,
+                new byte[]{ 0x00, 0x11, 0x22} /* oui */, TEST_VENDOR_SPECIFIC_IE_TYPE);
         final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info,
                 true /* isDhcpLeaseCacheEnabled */);
 
@@ -3715,7 +3757,7 @@
         setUpRetrievedNetworkAttributesForInitRebootState();
 
         final ScanResultInfo info = makeScanResultInfo(0xde /* vendor-specific IE */, TEST_OEM_OUI,
-                (byte) 0x11 /* vendor-specific IE type */);
+                TEST_VENDOR_SPECIFIC_IE_TYPE);
         final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info,
                 true /* isDhcpLeaseCacheEnabled */);
 
@@ -3728,8 +3770,22 @@
     public void testRequestCustomizedDhcpOptions_invalidVendorSpecificType() throws Exception {
         setUpRetrievedNetworkAttributesForInitRebootState();
 
-        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI,
-                (byte) 0x10 /* vendor-specific IE type */);
+        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
+                (byte) 0x20 /* vendor-specific IE type */);
+        final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info,
+                true /* isDhcpLeaseCacheEnabled */);
+
+        assertTrue(packet instanceof DhcpRequestPacket);
+        assertEquals(packet.mVendorId, new String("android-dhcp-" + Build.VERSION.RELEASE));
+        assertNull(packet.mUserClass);
+    }
+
+    @Test
+    public void testRequestCustomizedDhcpOptions_legacyVendorSpecificType() throws Exception {
+        setUpRetrievedNetworkAttributesForInitRebootState();
+
+        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
+                LEGACY_TEST_VENDOR_SPECIFIC_IE_TYPE);
         final DhcpPacket packet = doCustomizedDhcpOptionsTest(TEST_OEM_DHCP_OPTIONS, info,
                 true /* isDhcpLeaseCacheEnabled */);
 
@@ -3747,8 +3803,8 @@
                 makeDhcpOption((byte) 77, TEST_OEM_USER_CLASS_INFO),
                 // Option 26: MTU
                 makeDhcpOption((byte) 26, HexDump.toByteArray(TEST_DEFAULT_MTU)));
-        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI,
-                (byte) 0x11 /* vendor-specific IE type */);
+        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
+                TEST_VENDOR_SPECIFIC_IE_TYPE);
         final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info,
                 true /* isDhcpLeaseCacheEnabled */);
 
@@ -3767,8 +3823,8 @@
                 makeDhcpOption((byte) 77, TEST_OEM_USER_CLASS_INFO),
                 // NTP_SERVER
                 makeDhcpOption((byte) 42, null));
-        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI,
-                (byte) 0x11 /* vendor-specific IE type */);
+        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
+                TEST_VENDOR_SPECIFIC_IE_TYPE);
         final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info,
                 true /* isDhcpLeaseCacheEnabled */);
 
@@ -3785,8 +3841,8 @@
         final List<DhcpOption> options = Arrays.asList(
                 // DHCP_USER_CLASS
                 makeDhcpOption((byte) 77, null));
-        final ScanResultInfo info = makeScanResultInfo(0xdd /* vendor-specific IE */, TEST_OEM_OUI,
-                (byte) 0x11 /* vendor-specific IE type */);
+        final ScanResultInfo info = makeScanResultInfo(TEST_VENDOR_SPECIFIC_IE_ID, TEST_OEM_OUI,
+                TEST_VENDOR_SPECIFIC_IE_TYPE);
         final DhcpPacket packet = doCustomizedDhcpOptionsTest(options, info,
                 true /* isDhcpLeaseCacheEnabled */);
 
@@ -3840,7 +3896,7 @@
 
         setFeatureEnabled(NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION,
                 true /* isGratuitousNaEnabled */);
-        assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION, false));
+        assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION));
         startIpClientProvisioning(config);
 
         doIpv6OnlyProvisioning();
@@ -3872,9 +3928,19 @@
         // not strictly necessary.
         setDhcpFeatures(false /* isDhcpLeaseCacheEnabled */, true /* isRapidCommitEnabled */,
                 false /* isDhcpIpConflictDetectEnabled */, false /* isIPv6OnlyPreferredEnabled */);
+
+        // Disable gratuitious neighbor discovery feature manually, if the feature is enabled on
+        // the DUT during experiment launch, that will send another two duplicate NA packets and
+        // mess up the assert of received NA packets.
+        setFeatureEnabled(NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION,
+                false /* isGratuitousNaEnabled */);
+        assumeFalse(isFeatureEnabled(NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION));
         if (isGratuitousArpNaRoamingEnabled) {
             setFeatureEnabled(NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION, true);
-            assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION, false));
+            assumeTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION));
+        } else {
+            setFeatureEnabled(NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION, false);
+            assumeFalse(isFeatureEnabled(NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION));
         }
         startIpClientProvisioning(prov.build());
     }
@@ -4564,7 +4630,7 @@
 
         setFeatureEnabled(NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION,
                 true /* isUnsolicitedNsEnabled */);
-        assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION, false));
+        assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION));
         startIpClientProvisioning(config);
 
         doIpv6OnlyProvisioning();
@@ -4636,6 +4702,7 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     public void testMaxDtimMultiplier_IPv6OnlyNetwork() throws Exception {
         ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                 .withoutIPv4()
@@ -4653,6 +4720,7 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     public void testMaxDtimMultiplier_IPv6LinkLocalOnlyMode() throws Exception {
         final InOrder inOrder = inOrder(mCb);
         ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
@@ -4670,6 +4738,7 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     public void testMaxDtimMultiplier_IPv4OnlyNetwork() throws Exception {
         performDhcpHandshake(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                 true /* isDhcpLeaseCacheEnabled */, false /* shouldReplyRapidCommitAck */,
@@ -4691,12 +4760,14 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     public void testMaxDtimMultiplier_DualStackNetwork() throws Exception {
         final InOrder inOrder = inOrder(mCb);
         runDualStackNetworkDtimMultiplierSetting(inOrder);
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     public void testMaxDtimMultiplier_MulticastLock() throws Exception {
         final InOrder inOrder = inOrder(mCb);
         runDualStackNetworkDtimMultiplierSetting(inOrder);
@@ -4714,6 +4785,7 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     public void testMaxDtimMultiplier_MulticastLockEnabled_StoppedState() throws Exception {
         // Simulate to hold the multicast lock by disabling the multicast filter at StoppedState,
         // verify no callback to be sent, start dual-stack provisioning and verify the multiplier
@@ -4728,6 +4800,7 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     public void testMaxDtimMultiplier_resetMultiplier() throws Exception {
         final InOrder inOrder = inOrder(mCb);
         runDualStackNetworkDtimMultiplierSetting(inOrder);
@@ -5030,4 +5103,34 @@
         }
         verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(any());
     }
+
+    @Test
+    @SignatureRequiredTest(reason = "requires mocked netd to read/write IPv6 sysctl")
+    public void testIpv6SysctlsRestAfterStoppingIpClient() throws Exception {
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                .build();
+        // dad_transmits has been set to 0 in disableIpv6ProvisioningDelays, re-enable
+        // dad_transmits for testing, production code will restore all IPv6 sysctls at
+        // StoppedState#enter anyway, read this parameter value after IpClient shutdown
+        // to check if that's default value 1.
+        mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "dad_transmits", "1");
+        startIpClientProvisioning(config);
+        verify(mNetd, timeout(TEST_TIMEOUT_MS)).interfaceSetEnableIPv6(mIfaceName, true);
+        doIpv6OnlyProvisioning();
+
+        // Shutdown IpClient and check if the IPv6 sysctls: accept_ra, accept_ra_defrtr and
+        // dad_transmits have been reset to the default values.
+        mIpc.shutdown();
+        awaitIpClientShutdown();
+        final int dadTransmits = Integer.parseUnsignedInt(
+                mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "dad_transmits"));
+        assertEquals(1, dadTransmits);
+        final int acceptRa = Integer.parseUnsignedInt(
+                mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "accept_ra"));
+        assertEquals(2, acceptRa);
+        final int acceptRaDefRtr = Integer.parseUnsignedInt(
+                mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, mIfaceName, "accept_ra_defrtr"));
+        assertEquals(1, acceptRaDefRtr);
+    }
 }
diff --git a/tests/integration/root/android/net/ip/IpClientRootTest.kt b/tests/integration/root/android/net/ip/IpClientRootTest.kt
index 05dc358..77d327f 100644
--- a/tests/integration/root/android/net/ip/IpClientRootTest.kt
+++ b/tests/integration/root/android/net/ip/IpClientRootTest.kt
@@ -27,7 +27,6 @@
 import android.net.ipmemorystore.Status
 import android.net.networkstack.TestNetworkStackServiceClient
 import android.os.Process
-import android.provider.DeviceConfig
 import android.util.Log
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.net.module.util.DeviceConfigUtils
@@ -185,11 +184,19 @@
         setDeviceConfigProperty(feature, if (enabled) "1" else "999999999")
     }
 
-    override fun isFeatureEnabled(name: String, defaultEnabled: Boolean): Boolean {
+    override fun isFeatureEnabled(name: String): Boolean {
         automation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG)
         try {
-            return DeviceConfigUtils.isFeatureEnabled(mContext, DeviceConfig.NAMESPACE_CONNECTIVITY,
-                    name, defaultEnabled)
+            return DeviceConfigUtils.isNetworkStackFeatureEnabled(mContext, name)
+        } finally {
+            automation.dropShellPermissionIdentity()
+        }
+    }
+
+    override fun isFeatureNotChickenedOut(name: String): Boolean {
+        automation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG)
+        try {
+            return DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(mContext, name)
         } finally {
             automation.dropShellPermissionIdentity()
         }
diff --git a/tests/integration/signature/android/net/ip/IpClientSignatureTest.kt b/tests/integration/signature/android/net/ip/IpClientSignatureTest.kt
index b494732..2f91f4f 100644
--- a/tests/integration/signature/android/net/ip/IpClientSignatureTest.kt
+++ b/tests/integration/signature/android/net/ip/IpClientSignatureTest.kt
@@ -44,8 +44,12 @@
 
     override fun useNetworkStackSignature() = true
 
-    override fun isFeatureEnabled(name: String, defaultEnabled: Boolean): Boolean {
-        return mEnabledFeatures.get(name) ?: defaultEnabled
+    override fun isFeatureEnabled(name: String): Boolean {
+        return mEnabledFeatures.get(name) ?: false
+    }
+
+    override fun isFeatureNotChickenedOut(name: String): Boolean {
+        return mEnabledFeatures.get(name) ?: true
     }
 
     override fun setFeatureEnabled(name: String, enabled: Boolean) {
diff --git a/tests/integration/signature/android/net/util/NetworkStackUtilsIntegrationTest.kt b/tests/integration/signature/android/net/util/NetworkStackUtilsIntegrationTest.kt
index 2377ffa..3f01bea 100644
--- a/tests/integration/signature/android/net/util/NetworkStackUtilsIntegrationTest.kt
+++ b/tests/integration/signature/android/net/util/NetworkStackUtilsIntegrationTest.kt
@@ -33,6 +33,7 @@
 import android.system.OsConstants.ARPHRD_ETHER
 import android.system.OsConstants.ETH_P_IPV6
 import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.SOCK_CLOEXEC
 import android.system.OsConstants.SOCK_DGRAM
 import android.system.OsConstants.SOCK_NONBLOCK
 import android.system.OsConstants.SOCK_RAW
@@ -159,9 +160,8 @@
         assertArrayEquals("Sent packet != original packet", originalPacket, sentDhcpPacket)
     }
 
-    @Test
-    fun testAttachRaFilter() {
-        val socket = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6)
+    private fun doTestAttachRaFilter(generic: Boolean) {
+        val socket = Os.socket(AF_PACKET, SOCK_RAW or SOCK_CLOEXEC, 0)
         val ifParams = InterfaceParams.getByName(iface.interfaceName)
                 ?: fail("Could not obtain interface params for ${iface.interfaceName}")
         val socketAddr = SocketUtils.makePacketSocketAddress(ETH_P_IPV6, ifParams.index)
@@ -176,7 +176,11 @@
         echo.rewind()
         assertNextPacketEquals(socket, echo.readAsArray(), "ICMPv6 echo")
 
-        NetworkStackUtils.attachRaFilter(socket)
+        if (generic) {
+            NetworkStackUtils.attachControlPacketFilter(socket)
+        } else {
+            NetworkStackUtils.attachRaFilter(socket)
+        }
         // Send another echo, then an RA. After setting the filter expect only the RA.
         echo.rewind()
         reader.sendResponse(echo)
@@ -192,6 +196,16 @@
         assertNextPacketEquals(socket, ra.readAsArray(), "ICMPv6 RA")
     }
 
+    @Test
+    fun testAttachRaFilter() {
+        doTestAttachRaFilter(false)
+    }
+
+    @Test
+    fun testRaViaAttachControlPacketFilter() {
+        doTestAttachRaFilter(true)
+    }
+
     private fun assertNextPacketEquals(socket: FileDescriptor, expected: ByteArray, descr: String) {
         val buffer = ByteArray(TEST_MTU)
         val readPacket = Os.read(socket, buffer, 0 /* byteOffset */, buffer.size)
@@ -293,12 +307,15 @@
         packet.putShort(checksumOffset, IpUtils.ipChecksum(packet, ETHER_HEADER_LEN))
     }
 
-    @Test
-    fun testDhcpResponseWithMfBitDropped() {
+    private fun doTestDhcpResponseWithMfBitDropped(generic: Boolean) {
         val ifindex = InterfaceParams.getByName(iface.interfaceName).index
         val packetSock = Os.socket(AF_PACKET, SOCK_RAW or SOCK_NONBLOCK, /*protocol=*/0)
         try {
-            NetworkStackUtils.attachDhcpFilter(packetSock)
+            if (generic) {
+                NetworkStackUtils.attachControlPacketFilter(packetSock)
+            } else {
+                NetworkStackUtils.attachDhcpFilter(packetSock)
+            }
             val addr = SocketUtils.makePacketSocketAddress(OsConstants.ETH_P_IP, ifindex)
             Os.bind(packetSock, addr)
             val packet = DhcpPacket.buildNakPacket(DhcpPacket.ENCAP_L2, 42,
@@ -319,6 +336,16 @@
             Os.close(packetSock)
         }
     }
+
+    @Test
+    fun testDhcpResponseWithMfBitDropped() {
+        doTestDhcpResponseWithMfBitDropped(false)
+    }
+
+    @Test
+    fun testGenericDhcpResponseWithMfBitDropped() {
+        doTestDhcpResponseWithMfBitDropped(true)
+    }
 }
 
 private fun ByteBuffer.readAsArray(): ByteArray {
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 1233ae5..464e4a1 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -76,6 +76,7 @@
     min_sdk_version: "30",
     defaults: ["NetworkStackTestsDefaults"],
     static_libs: ["NetworkStackApiStableLib"],
+    lint: { test: true },
     visibility: [
         "//packages/modules/NetworkStack/tests/integration",
         "//packages/modules/Connectivity/tests:__subpackages__",
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index 1dbfbbe..50576cc 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -37,6 +37,8 @@
     ],
     static_libs: [
         "libapf",
+        "libapf_v5",
+        "libapfdisassembler",
         "libpcap",
     ],
     sdk_version: "30",
diff --git a/tests/unit/jni/apf_jni.cpp b/tests/unit/jni/apf_jni.cpp
index ff30bd1..5ae3ada 100644
--- a/tests/unit/jni/apf_jni.cpp
+++ b/tests/unit/jni/apf_jni.cpp
@@ -24,15 +24,32 @@
 #include <vector>
 
 #include "apf_interpreter.h"
+#include "disassembler.h"
 #include "nativehelper/scoped_primitive_array.h"
+#include "v5/apf_interpreter.h"
+#include "v5/test_buf_allocator.h"
 
 #define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
 #define LOG_TAG "NetworkStackUtils-JNI"
 
+static int run_apf_interpreter(int apf_version, uint8_t* program,
+                               uint32_t program_len, uint32_t ram_len,
+                               const uint8_t* packet, uint32_t packet_len,
+                               uint32_t filter_age) {
+  if (apf_version == 4) {
+    return accept_packet(program, program_len, ram_len, packet, packet_len,
+                         filter_age);
+  } else {
+    return apf_run(program, program_len, ram_len, packet, packet_len,
+                         filter_age);
+  }
+}
+
 // JNI function acting as simply call-through to native APF interpreter.
-static jint com_android_server_ApfTest_apfSimulate(
-        JNIEnv* env, jclass, jbyteArray jprogram, jbyteArray jpacket,
-        jbyteArray jdata, jint filter_age) {
+static jint
+com_android_server_ApfTest_apfSimulate(JNIEnv* env, jclass, jint apf_version,
+                                       jbyteArray jprogram, jbyteArray jpacket,
+                                       jbyteArray jdata, jint filter_age) {
 
     ScopedByteArrayRO packet(env, jpacket);
     uint32_t packet_len = (uint32_t)packet.size();
@@ -47,9 +64,10 @@
                                 reinterpret_cast<jbyte*>(buf.data() + program_len));
     }
 
-    jint result =
-        accept_packet(buf.data(), program_len, program_len + data_len,
-                        reinterpret_cast<const uint8_t*>(packet.get()), packet_len, filter_age);
+    jint result = run_apf_interpreter(
+        apf_version, buf.data(), program_len, program_len + data_len,
+        reinterpret_cast<const uint8_t *>(packet.get()), packet_len,
+        filter_age);
 
     if (jdata) {
         env->SetByteArrayRegion(jdata, 0, data_len,
@@ -118,8 +136,9 @@
     return env->NewStringUTF(bpf_string.c_str());
 }
 
-static jboolean com_android_server_ApfTest_compareBpfApf(JNIEnv* env, jclass, jstring jfilter,
-        jstring jpcap_filename, jbyteArray japf_program) {
+static jboolean com_android_server_ApfTest_compareBpfApf(
+    JNIEnv* env, jclass, jint apf_version, jstring jfilter,
+    jstring jpcap_filename, jbyteArray japf_program) {
     ScopedUtfChars filter(env, jfilter);
     ScopedUtfChars pcap_filename(env, jpcap_filename);
     ScopedByteArrayRO apf_program(env, japf_program);
@@ -163,7 +182,7 @@
         const uint8_t* apf_packet;
         do {
             apf_packet = pcap_next(apf_pcap.get(), &apf_header);
-        } while (apf_packet != NULL && !accept_packet(
+        } while (apf_packet != NULL && !run_apf_interpreter(apf_version,
                 reinterpret_cast<uint8_t*>(const_cast<int8_t*>(apf_program.get())),
                 apf_program.size(), 0 /* data_len */,
                 apf_packet, apf_header.len, 0 /* filter_age */));
@@ -182,8 +201,9 @@
     return true;
 }
 
-static jboolean com_android_server_ApfTest_dropsAllPackets(JNIEnv* env, jclass, jbyteArray jprogram,
-        jbyteArray jdata, jstring jpcap_filename) {
+static jboolean com_android_server_ApfTest_dropsAllPackets(
+    JNIEnv* env, jclass, jint apf_version, jbyteArray jprogram,
+    jbyteArray jdata, jstring jpcap_filename) {
     ScopedUtfChars pcap_filename(env, jpcap_filename);
     ScopedByteArrayRO apf_program(env, jprogram);
     uint32_t apf_program_len = (uint32_t)apf_program.size();
@@ -208,8 +228,9 @@
     }
 
     while ((apf_packet = pcap_next(apf_pcap.get(), &apf_header)) != NULL) {
-        int result = accept_packet(buf.data(), apf_program_len,
-                                    apf_program_len + data_len, apf_packet, apf_header.len, 0);
+        int result = run_apf_interpreter(
+            apf_version, buf.data(), apf_program_len,
+            apf_program_len + data_len, apf_packet, apf_header.len, 0);
 
         // Return false once packet passes the filter
         if (result) {
@@ -224,6 +245,52 @@
     return true;
 }
 
+static char output_buffer[512];
+
+static jobjectArray com_android_server_ApfTest_disassembleApf(
+    JNIEnv* env, jclass, jbyteArray jprogram) {
+    uint32_t program_len = env->GetArrayLength(jprogram);
+    std::vector<uint8_t> buf(program_len, 0);
+
+    env->GetByteArrayRegion(jprogram, 0, program_len,
+                            reinterpret_cast<jbyte*>(buf.data()));
+    std::vector<std::string> disassemble_output;
+    for (uint32_t pc = 0; pc < program_len;) {
+         pc = apf_disassemble(buf.data(), program_len, pc, output_buffer,
+                              sizeof(output_buffer) / sizeof(output_buffer[0]));
+         disassemble_output.emplace_back(output_buffer);
+    }
+    jclass stringClass = env->FindClass("java/lang/String");
+    jobjectArray disassembleOutput =
+        env->NewObjectArray(disassemble_output.size(), stringClass, nullptr);
+
+    for (jsize i = 0; i < (jsize) disassemble_output.size(); i++) {
+         jstring j_disassemble_output =
+             env->NewStringUTF(disassemble_output[i].c_str());
+         env->SetObjectArrayElement(disassembleOutput, i, j_disassemble_output);
+         env->DeleteLocalRef(j_disassemble_output);
+    }
+
+    return disassembleOutput;
+}
+
+jbyteArray com_android_server_ApfTest_getTransmittedPacket(JNIEnv* env,
+                                                           jclass) {
+    jbyteArray jdata = env->NewByteArray((jint) apf_test_tx_packet_len);
+    if (jdata == NULL) { return NULL; }
+    if (apf_test_tx_packet_len == 0) { return jdata; }
+
+    env->SetByteArrayRegion(jdata, 0, (jint) apf_test_tx_packet_len,
+                            reinterpret_cast<jbyte*>(apf_test_tx_packet));
+
+    return jdata;
+}
+
+void com_android_server_ApfTest_resetTransmittedPacketMemory(JNIEnv, jclass) {
+    apf_test_tx_packet_len = 0;
+    memset(apf_test_tx_packet, 0, APF_TX_BUFFER_SIZE);
+}
+
 extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
     JNIEnv *env;
     if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
@@ -232,17 +299,23 @@
     }
 
     static JNINativeMethod gMethods[] = {
-            { "apfSimulate", "([B[B[BI)I",
+            { "apfSimulate", "(I[B[B[BI)I",
                     (void*)com_android_server_ApfTest_apfSimulate },
             { "compileToBpf", "(Ljava/lang/String;)Ljava/lang/String;",
                     (void*)com_android_server_ApfTest_compileToBpf },
-            { "compareBpfApf", "(Ljava/lang/String;Ljava/lang/String;[B)Z",
+            { "compareBpfApf", "(ILjava/lang/String;Ljava/lang/String;[B)Z",
                     (void*)com_android_server_ApfTest_compareBpfApf },
-            { "dropsAllPackets", "([B[BLjava/lang/String;)Z",
+            { "dropsAllPackets", "(I[B[BLjava/lang/String;)Z",
                     (void*)com_android_server_ApfTest_dropsAllPackets },
+            { "disassembleApf", "([B)[Ljava/lang/String;",
+              (void*)com_android_server_ApfTest_disassembleApf },
+            { "getTransmittedPacket", "()[B",
+              (void*)com_android_server_ApfTest_getTransmittedPacket },
+            { "resetTransmittedPacketMemory", "()V",
+              (void*)com_android_server_ApfTest_resetTransmittedPacketMemory },
     };
 
-    jniRegisterNativeMethods(env, "android/net/apf/ApfTest",
+    jniRegisterNativeMethods(env, "android/net/apf/ApfJniUtils",
             gMethods, ARRAY_SIZE(gMethods));
 
     return JNI_VERSION_1_6;
diff --git a/tests/unit/src/android/net/apf/ApfJniUtils.java b/tests/unit/src/android/net/apf/ApfJniUtils.java
new file mode 100644
index 0000000..e6a7ad7
--- /dev/null
+++ b/tests/unit/src/android/net/apf/ApfJniUtils.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 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.apf;
+
+/**
+ * The class contains the helper method for interacting with native apf code.
+ */
+public class ApfJniUtils {
+
+    static {
+        // Load up native shared library containing APF interpreter exposed via JNI.
+        System.loadLibrary("networkstacktestsjni");
+    }
+
+    /**
+     * Call the APF interpreter to run {@code program} on {@code packet} with persistent memory
+     * segment {@data} pretending the filter was installed {@code filter_age} seconds ago.
+     */
+    public static native int apfSimulate(int apfVersion, byte[] program, byte[] packet,
+            byte[] data, int filterAge);
+
+    /**
+     * Compile a tcpdump human-readable filter (e.g. "icmp" or "tcp port 54") into a BPF
+     * prorgam and return a human-readable dump of the BPF program identical to "tcpdump -d".
+     */
+    public static native String compileToBpf(String filter);
+
+    /**
+     * Open packet capture file {@code pcap_filename} and filter the packets using tcpdump
+     * human-readable filter (e.g. "icmp" or "tcp port 54") compiled to a BPF program and
+     * at the same time using APF program {@code apf_program}.  Return {@code true} if
+     * both APF and BPF programs filter out exactly the same packets.
+     */
+    public static native boolean compareBpfApf(int apfVersion, String filter,
+            String pcapFilename, byte[] apfProgram);
+
+    /**
+     * Open packet capture file {@code pcapFilename} and run it through APF filter. Then
+     * checks whether all the packets are dropped and populates data[] {@code data} with
+     * the APF counters.
+     */
+    public static native boolean dropsAllPackets(int apfVersion, byte[] program, byte[] data,
+            String pcapFilename);
+
+    /**
+     * Disassemble the Apf program into human-readable text.
+     */
+    public static native String[] disassembleApf(byte[] program);
+
+    /**
+     * Get the transmitted packet.
+     */
+    public static native byte[] getTransmittedPacket();
+
+    /**
+     * Reset the memory region that stored the transmitted packet.
+     */
+    public static native void resetTransmittedPacketMemory();
+}
diff --git a/tests/unit/src/android/net/apf/ApfTest.java b/tests/unit/src/android/net/apf/ApfTest.java
index dd1ccb6..69c8917 100644
--- a/tests/unit/src/android/net/apf/ApfTest.java
+++ b/tests/unit/src/android/net/apf/ApfTest.java
@@ -18,6 +18,13 @@
 
 import static android.net.apf.ApfGenerator.Register.R0;
 import static android.net.apf.ApfGenerator.Register.R1;
+import static android.net.apf.ApfJniUtils.compareBpfApf;
+import static android.net.apf.ApfJniUtils.compileToBpf;
+import static android.net.apf.ApfJniUtils.dropsAllPackets;
+import static android.net.apf.ApfTestUtils.DROP;
+import static android.net.apf.ApfTestUtils.MIN_PKT_SIZE;
+import static android.net.apf.ApfTestUtils.PASS;
+import static android.net.apf.ApfTestUtils.assertProgramEquals;
 import static android.system.OsConstants.AF_UNIX;
 import static android.system.OsConstants.ARPHRD_ETHER;
 import static android.system.OsConstants.ETH_P_ARP;
@@ -30,15 +37,12 @@
 import static android.system.OsConstants.SOCK_STREAM;
 
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
-import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 
 import android.content.Context;
 import android.net.InetAddresses;
@@ -52,10 +56,7 @@
 import android.net.apf.ApfGenerator.IllegalInstructionException;
 import android.net.ip.IIpClientCallbacks;
 import android.net.ip.IpClient.IpClientCallbacksWrapper;
-import android.net.metrics.IpConnectivityLog;
-import android.net.metrics.RaEvent;
 import android.os.ConditionVariable;
-import android.os.Parcelable;
 import android.os.SystemClock;
 import android.system.ErrnoException;
 import android.system.Os;
@@ -66,7 +67,6 @@
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.HexDump;
 import com.android.net.module.util.DnsPacket;
@@ -85,10 +85,11 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
+import org.junit.runners.Parameterized;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileOutputStream;
@@ -109,33 +110,31 @@
 /**
  * Tests for APF program generator and interpreter.
  *
- * Build, install and run with:
- *  runtest frameworks-net -c android.net.apf.ApfTest
+ * The test cases will be executed by both APFv4 and APFv6 interpreter.
  */
-@RunWith(AndroidJUnit4.class)
+@RunWith(Parameterized.class)
 @SmallTest
 public class ApfTest {
     private static final int TIMEOUT_MS = 500;
     private static final int MIN_APF_VERSION = 2;
 
-    @Mock IpConnectivityLog mLog;
-    @Mock Context mContext;
+    // Indicates which apf interpreter to run.
+    @Parameterized.Parameter()
+    public int mApfVersion;
 
+    @Parameterized.Parameters
+    public static Iterable<? extends Object> data() {
+        return Arrays.asList(4, 6);
+    }
+
+    @Mock Context mContext;
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        // Load up native shared library containing APF interpreter exposed via JNI.
-        System.loadLibrary("networkstacktestsjni");
     }
 
     private static final String TAG = "ApfTest";
     // Expected return codes from APF interpreter.
-    private static final int PASS = 1;
-    private static final int DROP = 0;
-    // Interpreter will just accept packets without link layer headers, so pad fake packet to at
-    // least the minimum packet size.
-    private static final int MIN_PKT_SIZE = 15;
-
     private static final ApfCapabilities MOCK_APF_CAPABILITIES =
             new ApfCapabilities(2, 4096, ARPHRD_ETHER);
 
@@ -168,101 +167,53 @@
         return config;
     }
 
-    private static String label(int code) {
-        switch (code) {
-            case PASS: return "PASS";
-            case DROP: return "DROP";
-            default:   return "UNKNOWN";
-        }
+    private void assertPass(ApfGenerator gen) throws ApfGenerator.IllegalInstructionException {
+        ApfTestUtils.assertPass(mApfVersion, gen);
     }
 
-    private static void assertReturnCodesEqual(String msg, int expected, int got) {
-        assertEquals(msg, label(expected), label(got));
-    }
-
-    private static void assertReturnCodesEqual(int expected, int got) {
-        assertEquals(label(expected), label(got));
-    }
-
-    private void assertVerdict(int expected, byte[] program, byte[] packet, int filterAge) {
-        final String msg = "Unexpected APF verdict. To debug:\n"
-                + "  apf_run --program " + HexDump.toHexString(program)
-                + " --packet " + HexDump.toHexString(packet) + " --trace | less\n  ";
-        assertReturnCodesEqual(msg, expected, apfSimulate(program, packet, null, filterAge));
-    }
-
-    private void assertVerdict(String msg, int expected, byte[] program, byte[] packet,
-            int filterAge) {
-        assertReturnCodesEqual(msg, expected, apfSimulate(program, packet, null, filterAge));
-    }
-
-    private void assertVerdict(int expected, byte[] program, byte[] packet) {
-        assertVerdict(expected, program, packet, 0);
-    }
-
-    private void assertPass(byte[] program, byte[] packet, int filterAge) {
-        assertVerdict(PASS, program, packet, filterAge);
+    private void assertDrop(ApfGenerator gen) throws ApfGenerator.IllegalInstructionException {
+        ApfTestUtils.assertDrop(mApfVersion, gen);
     }
 
     private void assertPass(byte[] program, byte[] packet) {
-        assertVerdict(PASS, program, packet);
-    }
-
-    private void assertDrop(byte[] program, byte[] packet, int filterAge) {
-        assertVerdict(DROP, program, packet, filterAge);
+        ApfTestUtils.assertPass(mApfVersion, program, packet);
     }
 
     private void assertDrop(byte[] program, byte[] packet) {
-        assertVerdict(DROP, program, packet);
+        ApfTestUtils.assertDrop(mApfVersion, program, packet);
     }
 
-    private void assertProgramEquals(byte[] expected, byte[] program) throws AssertionError {
-        // assertArrayEquals() would only print one byte, making debugging difficult.
-        if (!Arrays.equals(expected, program)) {
-            throw new AssertionError(
-                    "\nexpected: " + HexDump.toHexString(expected) +
-                    "\nactual:   " + HexDump.toHexString(program));
-        }
+    private void assertPass(byte[] program, byte[] packet, int filterAge) {
+        ApfTestUtils.assertPass(mApfVersion, program, packet, filterAge);
     }
 
-    private void assertDataMemoryContents(
-            int expected, byte[] program, byte[] packet, byte[] data, byte[] expected_data)
-            throws IllegalInstructionException, Exception {
-        assertReturnCodesEqual(expected, apfSimulate(program, packet, data, 0 /* filterAge */));
-
-        // assertArrayEquals() would only print one byte, making debugging difficult.
-        if (!Arrays.equals(expected_data, data)) {
-            throw new Exception(
-                    "\nprogram:     " + HexDump.toHexString(program) +
-                    "\ndata memory: " + HexDump.toHexString(data) +
-                    "\nexpected:    " + HexDump.toHexString(expected_data));
-        }
-    }
-
-    private void assertVerdict(int expected, ApfGenerator gen, byte[] packet, int filterAge)
-            throws IllegalInstructionException {
-        assertReturnCodesEqual(expected, apfSimulate(gen.generate(), packet, null,
-              filterAge));
+    private void assertDrop(byte[] program, byte[] packet, int filterAge) {
+        ApfTestUtils.assertDrop(mApfVersion, program, packet, filterAge);
     }
 
     private void assertPass(ApfGenerator gen, byte[] packet, int filterAge)
-            throws IllegalInstructionException {
-        assertVerdict(PASS, gen, packet, filterAge);
+            throws ApfGenerator.IllegalInstructionException {
+        ApfTestUtils.assertPass(mApfVersion, gen, packet, filterAge);
     }
 
     private void assertDrop(ApfGenerator gen, byte[] packet, int filterAge)
-            throws IllegalInstructionException {
-        assertVerdict(DROP, gen, packet, filterAge);
+            throws ApfGenerator.IllegalInstructionException {
+        ApfTestUtils.assertDrop(mApfVersion, gen, packet, filterAge);
     }
 
-    private void assertPass(ApfGenerator gen)
-            throws IllegalInstructionException {
-        assertVerdict(PASS, gen, new byte[MIN_PKT_SIZE], 0);
+    private void assertDataMemoryContents(int expected, byte[] program, byte[] packet,
+            byte[] data, byte[] expectedData) throws Exception {
+        ApfTestUtils.assertDataMemoryContents(mApfVersion, expected, program, packet, data,
+                expectedData);
     }
 
-    private void assertDrop(ApfGenerator gen)
-            throws IllegalInstructionException {
-        assertVerdict(DROP, gen, new byte[MIN_PKT_SIZE], 0);
+    private void assertVerdict(String msg, int expected, byte[] program,
+            byte[] packet, int filterAge) {
+        ApfTestUtils.assertVerdict(mApfVersion, msg, expected, program, packet, filterAge);
+    }
+
+    private void assertVerdict(int expected, byte[] program, byte[] packet) {
+        ApfTestUtils.assertVerdict(mApfVersion, expected, program, packet);
     }
 
     /**
@@ -928,7 +879,7 @@
         for (String tcpdump_filter : tcpdump_filters) {
             byte[] apf_program = Bpf2Apf.convert(compileToBpf(tcpdump_filter));
             assertTrue("Failed to match for filter: " + tcpdump_filter,
-                    compareBpfApf(tcpdump_filter, pcap_filename, apf_program));
+                    compareBpfApf(mApfVersion, tcpdump_filter, pcap_filename, apf_program));
         }
     }
 
@@ -950,17 +901,18 @@
         config.apfCapabilities = MOCK_APF_PCAP_CAPABILITIES;
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
         apfFilter.setLinkProperties(lp);
-        byte[] program = ipClientCallback.getApfProgram();
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
         byte[] data = new byte[ApfFilter.Counter.totalSize()];
         final boolean result;
 
-        result = dropsAllPackets(program, data, pcapFilename);
+        result = dropsAllPackets(mApfVersion, program, data, pcapFilename);
         Log.i(TAG, "testApfFilterPcapFile(): Data counters: " + HexDump.toHexString(data, false));
 
         assertTrue("Failed to drop all packets by filter. \nAPF counters:" +
             HexDump.toHexString(data, false), result);
+        apfFilter.shutdown();
     }
 
     private class MockIpClientCallback extends IpClientCallbacksWrapper {
@@ -982,7 +934,7 @@
             mGotApfProgram.close();
         }
 
-        public byte[] getApfProgram() {
+        public byte[] assertProgramUpdateAndGet() {
             assertTrue(mGotApfProgram.block(TIMEOUT_MS));
             return mLastApfProgram;
         }
@@ -997,14 +949,17 @@
 
         private FileDescriptor mWriteSocket;
         private long mCurrentTimeMs = SystemClock.elapsedRealtime();
+        private final MockIpClientCallback mMockIpClientCb;
 
         public TestApfFilter(Context context, ApfConfiguration config,
-                IpClientCallbacksWrapper ipClientCallback, IpConnectivityLog log) throws Exception {
-            super(context, config, InterfaceParams.getByName("lo"), ipClientCallback, log);
+                MockIpClientCallback ipClientCallback) throws Exception {
+            super(context, config, InterfaceParams.getByName("lo"), ipClientCallback);
+            mMockIpClientCb = ipClientCallback;
         }
 
         // Pretend an RA packet has been received and show it to ApfFilter.
         public void pretendPacketReceived(byte[] packet) throws IOException, ErrnoException {
+            mMockIpClientCb.resetApfProgramWait();
             // ApfFilter's ReceiveThread will be waiting to read this.
             Os.write(mWriteSocket, packet, 0, packet.length);
         }
@@ -1015,8 +970,8 @@
         }
 
         @Override
-        protected long currentTimeSeconds() {
-            return mCurrentTimeMs / DateUtils.SECOND_IN_MILLIS;
+        protected int secondsSinceBoot() {
+            return (int) (mCurrentTimeMs / DateUtils.SECOND_IN_MILLIS);
         }
 
         @Override
@@ -1042,6 +997,10 @@
         @Override
         public void shutdown() {
             super.shutdown();
+            if (mReceiveThread != null) {
+                mReceiveThread.halt();
+                mReceiveThread = null;
+            }
             IoUtils.closeQuietly(mWriteSocket);
         }
     }
@@ -1177,11 +1136,11 @@
 
     // Helper to initialize a default apfFilter.
     private ApfFilter setupApfFilter(
-            IpClientCallbacksWrapper ipClientCallback, ApfConfiguration config) throws Exception {
+            MockIpClientCallback ipClientCallback, ApfConfiguration config) throws Exception {
         LinkAddress link = new LinkAddress(InetAddress.getByAddress(MOCK_IPV4_ADDR), 19);
         LinkProperties lp = new LinkProperties();
         lp.addLinkAddress(link);
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
         apfFilter.setLinkProperties(lp);
         return apfFilter;
     }
@@ -1219,10 +1178,10 @@
 
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
         apfFilter.setLinkProperties(lp);
 
-        byte[] program = ipClientCallback.getApfProgram();
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify empty packet of 100 zero bytes is passed
         ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
@@ -1271,8 +1230,8 @@
     public void testApfFilterIPv6() throws Exception {
         MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         ApfConfiguration config = getDefaultConfig();
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
-        byte[] program = ipClientCallback.getApfProgram();
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify empty IPv6 packet is passed
         ByteBuffer packet = makeIpv6Packet(IPPROTO_UDP);
@@ -1489,7 +1448,7 @@
 
     /** Adds to the program a no-op instruction that is one byte long. */
     private void addOneByteNoop(ApfGenerator gen) {
-        gen.addOr(0);
+        gen.addLeftShift(0);
     }
 
     @Test
@@ -1526,13 +1485,13 @@
         lp.addLinkAddress(link);
 
         ApfConfiguration config = getDefaultConfig();
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
         apfFilter.setLinkProperties(lp);
 
         // Construct IPv4 mDNS packet
         byte[] mdnsv4packet = makeMdnsV4Packet("test.local");
         byte[] mdnsv6packet = makeMdnsV6Packet("test.local");
-        byte[] program = ipClientCallback.getApfProgram();
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
         // mDNSv4 packet is passed if no mDns filter is turned on
         assertPass(program, mdnsv4packet);
         // mDNSv6 packet is passed if no mDNS filter is turned on
@@ -1542,7 +1501,7 @@
         apfFilter.addToMdnsAllowList(new String[]{"test", "local"});
         apfFilter.addToMdnsAllowList(new String[]{"abcd", "local"});
         apfFilter.setMulticastFilter(true);
-        program = ipClientCallback.getApfProgram();
+        program = ipClientCallback.assertProgramUpdateAndGet();
         assertPass(program, mdnsv4packet);
         assertPass(program, mdnsv6packet);
         // If packet contains more than one qname, pass the packet
@@ -1568,7 +1527,7 @@
         assertDrop(program, mdnsv6packet);
 
         apfFilter.removeFromAllowList(new String[]{"abcd", "local"});
-        program = ipClientCallback.getApfProgram();
+        program = ipClientCallback.assertProgramUpdateAndGet();
         mdnsv4packet = makeMdnsV4Packet("abcd.local");
         mdnsv6packet = makeMdnsV6Packet("abcd.local");
         assertDrop(program, mdnsv4packet);
@@ -1763,10 +1722,10 @@
 
         ApfConfiguration config = getDefaultConfig();
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
         apfFilter.setLinkProperties(lp);
 
-        byte[] program = ipClientCallback.getApfProgram();
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Construct IPv4 and IPv6 multicast packets.
         ByteBuffer mcastv4packet = makeIpv4Packet(IPPROTO_UDP);
@@ -1802,7 +1761,7 @@
         // Turn on multicast filter and verify it works
         ipClientCallback.resetApfProgramWait();
         apfFilter.setMulticastFilter(true);
-        program = ipClientCallback.getApfProgram();
+        program = ipClientCallback.assertProgramUpdateAndGet();
         assertDrop(program, mcastv4packet.array());
         assertDrop(program, mcastv6packet.array());
         assertDrop(program, bcastv4packet1.array());
@@ -1812,7 +1771,7 @@
         // Turn off multicast filter and verify it's off
         ipClientCallback.resetApfProgramWait();
         apfFilter.setMulticastFilter(false);
-        program = ipClientCallback.getApfProgram();
+        program = ipClientCallback.assertProgramUpdateAndGet();
         assertPass(program, mcastv4packet.array());
         assertPass(program, mcastv6packet.array());
         assertPass(program, bcastv4packet1.array());
@@ -1824,9 +1783,9 @@
         apfFilter.shutdown();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
+        apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
         apfFilter.setLinkProperties(lp);
-        program = ipClientCallback.getApfProgram();
+        program = ipClientCallback.assertProgramUpdateAndGet();
         assertDrop(program, mcastv4packet.array());
         assertDrop(program, mcastv6packet.array());
         assertDrop(program, bcastv4packet1.array());
@@ -1851,27 +1810,27 @@
         put(packet, IPV6_DEST_ADDR_OFFSET, multicastIpv6Addr);
 
         // Normally, we let multicast pings alone...
-        assertPass(ipClientCallback.getApfProgram(), packet.array());
+        assertPass(ipClientCallback.assertProgramUpdateAndGet(), packet.array());
 
         // ...and even while dozing...
         apfFilter.setDozeMode(true);
-        assertPass(ipClientCallback.getApfProgram(), packet.array());
+        assertPass(ipClientCallback.assertProgramUpdateAndGet(), packet.array());
 
         // ...but when the multicast filter is also enabled, drop the multicast pings to save power.
         apfFilter.setMulticastFilter(true);
-        assertDrop(ipClientCallback.getApfProgram(), packet.array());
+        assertDrop(ipClientCallback.assertProgramUpdateAndGet(), packet.array());
 
         // However, we should still let through all other ICMPv6 types.
         ByteBuffer raPacket = ByteBuffer.wrap(packet.array().clone());
         setIpv6VersionFields(packet);
         packet.put(IPV6_NEXT_HEADER_OFFSET, (byte) IPPROTO_ICMPV6);
         raPacket.put(ICMP6_TYPE_OFFSET, (byte) NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT);
-        assertPass(ipClientCallback.getApfProgram(), raPacket.array());
+        assertPass(ipClientCallback.assertProgramUpdateAndGet(), raPacket.array());
 
         // Now wake up from doze mode to ensure that we no longer drop the packets.
         // (The multicast filter is still enabled at this point).
         apfFilter.setDozeMode(false);
-        assertPass(ipClientCallback.getApfProgram(), packet.array());
+        assertPass(ipClientCallback.assertProgramUpdateAndGet(), packet.array());
 
         apfFilter.shutdown();
     }
@@ -1881,7 +1840,7 @@
         MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         ApfConfiguration config = getDefaultConfig();
         ApfFilter apfFilter = setupApfFilter(ipClientCallback, config);
-        byte[] program = ipClientCallback.getApfProgram();
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify empty packet of 100 zero bytes is passed
         // Note that eth-type = 0 makes it an IEEE802.3 frame
@@ -1901,7 +1860,7 @@
         apfFilter.shutdown();
         config.ieee802_3Filter = DROP_802_3_FRAMES;
         apfFilter = setupApfFilter(ipClientCallback, config);
-        program = ipClientCallback.getApfProgram();
+        program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify that IEEE802.3 frame is dropped
         // In this case ethtype is used for payload length
@@ -1928,7 +1887,7 @@
         MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         ApfConfiguration config = getDefaultConfig();
         ApfFilter apfFilter = setupApfFilter(ipClientCallback, config);
-        byte[] program = ipClientCallback.getApfProgram();
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify empty packet of 100 zero bytes is passed
         // Note that eth-type = 0 makes it an IEEE802.3 frame
@@ -1948,7 +1907,7 @@
         apfFilter.shutdown();
         config.ethTypeBlackList = ipv4BlackList;
         apfFilter = setupApfFilter(ipClientCallback, config);
-        program = ipClientCallback.getApfProgram();
+        program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify that IPv4 frame will be dropped
         setIpv4VersionFields(packet);
@@ -1963,7 +1922,7 @@
         apfFilter.shutdown();
         config.ethTypeBlackList = ipv4Ipv6BlackList;
         apfFilter = setupApfFilter(ipClientCallback, config);
-        program = ipClientCallback.getApfProgram();
+        program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify that IPv4 frame will be dropped
         setIpv4VersionFields(packet);
@@ -1979,7 +1938,7 @@
     private byte[] getProgram(MockIpClientCallback cb, ApfFilter filter, LinkProperties lp) {
         cb.resetApfProgramWait();
         filter.setLinkProperties(lp);
-        return cb.getApfProgram();
+        return cb.assertProgramUpdateAndGet();
     }
 
     private void verifyArpFilter(byte[] program, int filterResult) {
@@ -2009,10 +1968,10 @@
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
 
         // Verify initially ARP request filter is off, and GARP filter is on.
-        verifyArpFilter(ipClientCallback.getApfProgram(), PASS);
+        verifyArpFilter(ipClientCallback.assertProgramUpdateAndGet(), PASS);
 
         // Inform ApfFilter of our address and verify ARP filtering is on
         LinkAddress linkAddress = new LinkAddress(InetAddress.getByAddress(MOCK_IPV4_ADDR), 24);
@@ -2069,7 +2028,7 @@
         final ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mLog);
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb);
         byte[] program;
         final int srcPort = 12345;
         final int dstPort = 54321;
@@ -2098,7 +2057,7 @@
         parcel.ack = ackNum;
 
         apfFilter.addTcpKeepalivePacketFilter(slot1, parcel);
-        program = cb.getApfProgram();
+        program = cb.assertProgramUpdateAndGet();
 
         // Verify IPv4 keepalive ack packet is dropped
         // src: 10.0.0.6, port: 54321
@@ -2137,7 +2096,7 @@
             ipv6Parcel.ack = ackNum;
 
             apfFilter.addTcpKeepalivePacketFilter(slot1, ipv6Parcel);
-            program = cb.getApfProgram();
+            program = cb.assertProgramUpdateAndGet();
 
             // Verify IPv6 keepalive ack packet is dropped
             // src: 2404:0:0:0:0:0:faf2, port: 54321
@@ -2160,7 +2119,7 @@
             // Verify multiple filters
             apfFilter.addTcpKeepalivePacketFilter(slot1, parcel);
             apfFilter.addTcpKeepalivePacketFilter(slot2, ipv6Parcel);
-            program = cb.getApfProgram();
+            program = cb.assertProgramUpdateAndGet();
 
             // Verify IPv4 keepalive ack packet is dropped
             // src: 10.0.0.6, port: 54321
@@ -2199,7 +2158,7 @@
             // TODO: support V6 packets
         }
 
-        program = cb.getApfProgram();
+        program = cb.assertProgramUpdateAndGet();
 
         // Verify IPv4, IPv6 packets are passed
         assertPass(program,
@@ -2262,7 +2221,7 @@
         final ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mLog);
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb);
         byte[] program;
         final int srcPort = 1024;
         final int dstPort = 4500;
@@ -2283,7 +2242,7 @@
         parcel.dstPort = dstPort;
 
         apfFilter.addNattKeepalivePacketFilter(slot1, parcel);
-        program = cb.getApfProgram();
+        program = cb.assertProgramUpdateAndGet();
 
         // Verify IPv4 keepalive packet is dropped
         // src: 10.0.0.6, port: 4500
@@ -2330,100 +2289,188 @@
         return packet.array();
     }
 
-    private void addRdnssOption(ByteBuffer packet, int lifetime, String... servers)
-            throws Exception {
-        int optionLength = 1 + 2 * servers.length;   // In 8-byte units
-        packet.put((byte) ICMP6_RDNSS_OPTION_TYPE);  // Type
-        packet.put((byte) optionLength);             // Length
-        packet.putShort((short) 0);                  // Reserved
-        packet.putInt(lifetime);                     // Lifetime
-        for (String server : servers) {
-            packet.put(InetAddress.getByName(server).getAddress());
-        }
-    }
+    private static class RaPacketBuilder {
+        final ByteArrayOutputStream mPacket = new ByteArrayOutputStream();
+        int mFlowLabel = 0x12345;
+        int mReachableTime = 30_000;
+        int mRetransmissionTimer = 1000;
 
-    private void addRioOption(ByteBuffer packet, int lifetime, String prefixString)
-            throws Exception {
-        IpPrefix prefix = new IpPrefix(prefixString);
+        public RaPacketBuilder(int routerLft) throws Exception {
+            InetAddress src = InetAddress.getByName("fe80::1234:abcd");
+            ByteBuffer buffer = ByteBuffer.allocate(ICMP6_RA_OPTION_OFFSET);
 
-        int optionLength;
-        if (prefix.getPrefixLength() == 0) {
-            optionLength = 1;
-        } else if (prefix.getPrefixLength() <= 64) {
-            optionLength = 2;
-        } else {
-            optionLength = 3;
+            buffer.putShort(ETH_ETHERTYPE_OFFSET, (short) ETH_P_IPV6);
+            buffer.position(ETH_HEADER_LEN);
+
+            // skip version, tclass, flowlabel; set in build()
+            buffer.position(buffer.position() + 4);
+
+            buffer.putShort((short) 0);                     // Payload length; updated later
+            buffer.put((byte) IPPROTO_ICMPV6);              // Next header
+            buffer.put((byte) 0xff);                        // Hop limit
+            buffer.put(src.getAddress());                   // Source address
+            buffer.put(IPV6_ALL_NODES_ADDRESS);             // Destination address
+
+            buffer.put((byte) ICMP6_ROUTER_ADVERTISEMENT);  // Type
+            buffer.put((byte) 0);                           // Code (0)
+            buffer.putShort((short) 0);                     // Checksum (ignored)
+            buffer.put((byte) 64);                          // Hop limit
+            buffer.put((byte) 0);                           // M/O, reserved
+            buffer.putShort((short) routerLft);             // Router lifetime
+            // skip reachable time; set in build()
+            // skip retransmission timer; set in build();
+
+            mPacket.write(buffer.array(), 0, buffer.capacity());
         }
 
-        packet.put((byte) ICMP6_ROUTE_INFO_OPTION_TYPE);  // Type
-        packet.put((byte) optionLength);                  // Length in 8-byte units
-        packet.put((byte) prefix.getPrefixLength());      // Prefix length
-        packet.put((byte) 0b00011000);                    // Pref = high
-        packet.putInt(lifetime);                          // Lifetime
+        public RaPacketBuilder setFlowLabel(int flowLabel) {
+            mFlowLabel = flowLabel;
+            return this;
+        }
 
-        byte[] prefixBytes = prefix.getRawAddress();
-        packet.put(prefixBytes, 0, (optionLength - 1) * 8);
-    }
+        public RaPacketBuilder setReachableTime(int reachable) {
+            mReachableTime = reachable;
+            return this;
+        }
 
-    private void addPioOption(ByteBuffer packet, int valid, int preferred, String prefixString) {
-        IpPrefix prefix = new IpPrefix(prefixString);
-        packet.put((byte) ICMP6_PREFIX_OPTION_TYPE);  // Type
-        packet.put((byte) 4);                         // Length in 8-byte units
-        packet.put((byte) prefix.getPrefixLength());  // Prefix length
-        packet.put((byte) 0b11000000);                // L = 1, A = 1
-        packet.putInt(valid);
-        packet.putInt(preferred);
-        packet.putInt(0);                             // Reserved
-        packet.put(prefix.getRawAddress());
+        public RaPacketBuilder setRetransmissionTimer(int retrans) {
+            mRetransmissionTimer = retrans;
+            return this;
+        }
+
+        public RaPacketBuilder addPioOption(int valid, int preferred, String prefixString)
+                throws Exception {
+            ByteBuffer buffer = ByteBuffer.allocate(ICMP6_PREFIX_OPTION_LEN);
+
+            IpPrefix prefix = new IpPrefix(prefixString);
+            buffer.put((byte) ICMP6_PREFIX_OPTION_TYPE);  // Type
+            buffer.put((byte) 4);                         // Length in 8-byte units
+            buffer.put((byte) prefix.getPrefixLength());  // Prefix length
+            buffer.put((byte) 0b11000000);                // L = 1, A = 1
+            buffer.putInt(valid);
+            buffer.putInt(preferred);
+            buffer.putInt(0);                             // Reserved
+            buffer.put(prefix.getRawAddress());
+
+            mPacket.write(buffer.array(), 0, buffer.capacity());
+            return this;
+        }
+
+        public RaPacketBuilder addRioOption(int lifetime, String prefixString) throws Exception {
+            IpPrefix prefix = new IpPrefix(prefixString);
+
+            int optionLength;
+            if (prefix.getPrefixLength() == 0) {
+                optionLength = 1;
+            } else if (prefix.getPrefixLength() <= 64) {
+                optionLength = 2;
+            } else {
+                optionLength = 3;
+            }
+
+            ByteBuffer buffer = ByteBuffer.allocate(optionLength * 8);
+
+            buffer.put((byte) ICMP6_ROUTE_INFO_OPTION_TYPE);  // Type
+            buffer.put((byte) optionLength);                  // Length in 8-byte units
+            buffer.put((byte) prefix.getPrefixLength());      // Prefix length
+            buffer.put((byte) 0b00011000);                    // Pref = high
+            buffer.putInt(lifetime);                          // Lifetime
+
+            byte[] prefixBytes = prefix.getRawAddress();
+            buffer.put(prefixBytes, 0, (optionLength - 1) * 8);
+
+            mPacket.write(buffer.array(), 0, buffer.capacity());
+            return this;
+        }
+
+        public RaPacketBuilder addDnsslOption(int lifetime, String... domains) {
+            ByteArrayOutputStream dnssl = new ByteArrayOutputStream();
+            for (String domain : domains) {
+                for (String label : domain.split(".")) {
+                    final byte[] bytes = label.getBytes(StandardCharsets.UTF_8);
+                    dnssl.write((byte) bytes.length);
+                    dnssl.write(bytes, 0, bytes.length);
+                }
+                dnssl.write((byte) 0);
+            }
+
+            // Extend with 0s to make it 8-byte aligned.
+            while (dnssl.size() % 8 != 0) {
+                dnssl.write((byte) 0);
+            }
+
+            final int length = ICMP6_4_BYTE_OPTION_LEN + dnssl.size();
+            ByteBuffer buffer = ByteBuffer.allocate(length);
+
+            buffer.put((byte) ICMP6_DNSSL_OPTION_TYPE);  // Type
+            buffer.put((byte) (length / 8));             // Length
+            // skip past reserved bytes
+            buffer.position(buffer.position() + 2);
+            buffer.putInt(lifetime);                     // Lifetime
+            buffer.put(dnssl.toByteArray());             // Domain names
+
+            mPacket.write(buffer.array(), 0, buffer.capacity());
+            return this;
+        }
+
+        public RaPacketBuilder addRdnssOption(int lifetime, String... servers) throws Exception {
+            int optionLength = 1 + 2 * servers.length;   // In 8-byte units
+            ByteBuffer buffer = ByteBuffer.allocate(optionLength * 8);
+
+            buffer.put((byte) ICMP6_RDNSS_OPTION_TYPE);  // Type
+            buffer.put((byte) optionLength);             // Length
+            buffer.putShort((short) 0);                  // Reserved
+            buffer.putInt(lifetime);                     // Lifetime
+            for (String server : servers) {
+                buffer.put(InetAddress.getByName(server).getAddress());
+            }
+
+            mPacket.write(buffer.array(), 0, buffer.capacity());
+            return this;
+        }
+
+        public RaPacketBuilder addZeroLengthOption() throws Exception {
+            ByteBuffer buffer = ByteBuffer.allocate(ICMP6_4_BYTE_OPTION_LEN);
+            buffer.put((byte) ICMP6_PREFIX_OPTION_TYPE);
+            buffer.put((byte) 0);
+
+            mPacket.write(buffer.array(), 0, buffer.capacity());
+            return this;
+        }
+
+        public byte[] build() {
+            ByteBuffer buffer = ByteBuffer.wrap(mPacket.toByteArray());
+            // IPv6, traffic class = 0, flow label = mFlowLabel
+            buffer.putInt(IP_HEADER_OFFSET, 0x60000000 | (0xFFFFF & mFlowLabel));
+            buffer.putShort(IPV6_PAYLOAD_LENGTH_OFFSET, (short) buffer.capacity());
+
+            buffer.position(ICMP6_RA_REACHABLE_TIME_OFFSET);
+            buffer.putInt(mReachableTime);
+            buffer.putInt(mRetransmissionTimer);
+
+            return buffer.array();
+        }
     }
 
     private byte[] buildLargeRa() throws Exception {
-        InetAddress src = InetAddress.getByName("fe80::1234:abcd");
+        RaPacketBuilder builder = new RaPacketBuilder(1800 /* router lft */);
 
-        ByteBuffer packet = ByteBuffer.wrap(new byte[1514]);
-        packet.putShort(ETH_ETHERTYPE_OFFSET, (short) ETH_P_IPV6);
-        packet.position(ETH_HEADER_LEN);
+        builder.addRioOption(1200, "64:ff9b::/96");
+        builder.addRdnssOption(7200, "2001:db8:1::1", "2001:db8:1::2");
+        builder.addRioOption(2100, "2000::/3");
+        builder.addRioOption(2400, "::/0");
+        builder.addPioOption(600, 300, "2001:db8:a::/64");
+        builder.addRioOption(1500, "2001:db8:c:d::/64");
+        builder.addPioOption(86400, 43200, "fd95:d1e:12::/64");
 
-        packet.putInt(0x60012345);                                  // Version, tclass, flowlabel
-        packet.putShort((short) 0);                                 // Payload length; updated later
-        packet.put((byte) IPPROTO_ICMPV6);                          // Next header
-        packet.put((byte) 0xff);                                    // Hop limit
-        packet.put(src.getAddress());                               // Source address
-        packet.put(IPV6_ALL_NODES_ADDRESS);                         // Destination address
-
-        packet.put((byte) ICMP6_ROUTER_ADVERTISEMENT);              // Type
-        packet.put((byte) 0);                                       // Code (0)
-        packet.putShort((short) 0);                                 // Checksum (ignored)
-        packet.put((byte) 64);                                      // Hop limit
-        packet.put((byte) 0);                                       // M/O, reserved
-        packet.putShort((short) 1800);                              // Router lifetime
-        packet.putInt(30_000);                                      // Reachable time
-        packet.putInt(1000);                                        // Retrans timer
-
-        addRioOption(packet, 1200, "64:ff9b::/96");
-        addRdnssOption(packet, 7200, "2001:db8:1::1", "2001:db8:1::2");
-        addRioOption(packet, 2100, "2000::/3");
-        addRioOption(packet, 2400, "::/0");
-        addPioOption(packet, 600, 300, "2001:db8:a::/64");
-        addRioOption(packet, 1500, "2001:db8:c:d::/64");
-        addPioOption(packet, 86400, 43200, "fd95:d1e:12::/64");
-
-        int length = packet.position();
-        packet.putShort(IPV6_PAYLOAD_LENGTH_OFFSET, (short) length);
-
-        // Don't pass the Ra constructor a packet that is longer than the actual RA.
-        // This relies on the fact that all the relative writes to the byte buffer are at the end.
-        byte[] packetArray = new byte[length];
-        packet.rewind();
-        packet.get(packetArray);
-        return packetArray;
+        return builder.build();
     }
 
     @Test
     public void testRaToString() throws Exception {
         MockIpClientCallback cb = new MockIpClientCallback();
         ApfConfiguration config = getDefaultConfig();
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mLog);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb);
 
         byte[] packet = buildLargeRa();
         ApfFilter.Ra ra = apfFilter.new Ra(packet, packet.length);
@@ -2476,76 +2523,25 @@
     private void verifyRaLifetime(TestApfFilter apfFilter, MockIpClientCallback ipClientCallback,
             ByteBuffer packet, int lifetime) throws IOException, ErrnoException {
         // Verify new program generated if ApfFilter witnesses RA
-        ipClientCallback.resetApfProgramWait();
         apfFilter.pretendPacketReceived(packet.array());
-        byte[] program = ipClientCallback.getApfProgram();
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
         verifyRaLifetime(program, packet, lifetime);
     }
 
-    private void verifyRaEvent(RaEvent expected) {
-        ArgumentCaptor<IpConnectivityLog.Event> captor =
-                ArgumentCaptor.forClass(IpConnectivityLog.Event.class);
-        verify(mLog, atLeastOnce()).log(captor.capture());
-        RaEvent got = lastRaEvent(captor.getAllValues());
-        if (!raEventEquals(expected, got)) {
-            assertEquals(expected, got);  // fail for printing an assertion error message.
-        }
-    }
-
-    private RaEvent lastRaEvent(List<IpConnectivityLog.Event> events) {
-        RaEvent got = null;
-        for (Parcelable ev : events) {
-            if (ev instanceof RaEvent) {
-                got = (RaEvent) ev;
-            }
-        }
-        return got;
-    }
-
-    private boolean raEventEquals(RaEvent ev1, RaEvent ev2) {
-        return (ev1 != null) && (ev2 != null)
-                && (ev1.routerLifetime == ev2.routerLifetime)
-                && (ev1.prefixValidLifetime == ev2.prefixValidLifetime)
-                && (ev1.prefixPreferredLifetime == ev2.prefixPreferredLifetime)
-                && (ev1.routeInfoLifetime == ev2.routeInfoLifetime)
-                && (ev1.rdnssLifetime == ev2.rdnssLifetime)
-                && (ev1.dnsslLifetime == ev2.dnsslLifetime);
-    }
-
     private void assertInvalidRa(TestApfFilter apfFilter, MockIpClientCallback ipClientCallback,
             ByteBuffer packet) throws IOException, ErrnoException {
-        ipClientCallback.resetApfProgramWait();
         apfFilter.pretendPacketReceived(packet.array());
         ipClientCallback.assertNoProgramUpdate();
     }
 
-    private ByteBuffer makeBaseRaPacket() {
-        ByteBuffer basePacket = ByteBuffer.wrap(new byte[ICMP6_RA_OPTION_OFFSET]);
-        final int ROUTER_LIFETIME = 1000;
-        final int VERSION_TRAFFIC_CLASS_FLOW_LABEL_OFFSET = ETH_HEADER_LEN;
-        // IPv6, traffic class = 0, flow label = 0x12345
-        final int VERSION_TRAFFIC_CLASS_FLOW_LABEL = 0x60012345;
-
-        basePacket.putShort(ETH_ETHERTYPE_OFFSET, (short) ETH_P_IPV6);
-        basePacket.putInt(VERSION_TRAFFIC_CLASS_FLOW_LABEL_OFFSET,
-                VERSION_TRAFFIC_CLASS_FLOW_LABEL);
-        basePacket.put(IPV6_NEXT_HEADER_OFFSET, (byte) IPPROTO_ICMPV6);
-        basePacket.put(ICMP6_TYPE_OFFSET, (byte) ICMP6_ROUTER_ADVERTISEMENT);
-        basePacket.putShort(ICMP6_RA_ROUTER_LIFETIME_OFFSET, (short) ROUTER_LIFETIME);
-        basePacket.position(IPV6_DEST_ADDR_OFFSET);
-        basePacket.put(IPV6_ALL_NODES_ADDRESS);
-
-        return basePacket;
-    }
-
     @Test
     public void testApfFilterRa() throws Exception {
         MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
-        byte[] program = ipClientCallback.getApfProgram();
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         final int ROUTER_LIFETIME = 1000;
         final int PREFIX_VALID_LIFETIME = 200;
@@ -2554,102 +2550,69 @@
         final int ROUTE_LIFETIME  = 400;
         // Note that lifetime of 2000 will be ignored in favor of shorter route lifetime of 1000.
         final int DNSSL_LIFETIME  = 2000;
-        final int VERSION_TRAFFIC_CLASS_FLOW_LABEL_OFFSET = ETH_HEADER_LEN;
-        // IPv6, traffic class = 0, flow label = 0x12345
-        final int VERSION_TRAFFIC_CLASS_FLOW_LABEL = 0x60012345;
 
         // Verify RA is passed the first time
-        ByteBuffer basePacket = makeBaseRaPacket();
+        RaPacketBuilder ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        ByteBuffer basePacket = ByteBuffer.wrap(ra.build());
         assertPass(program, basePacket.array());
 
         verifyRaLifetime(apfFilter, ipClientCallback, basePacket, ROUTER_LIFETIME);
-        verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, -1, -1, -1));
 
-        ByteBuffer newFlowLabelPacket = ByteBuffer.wrap(new byte[ICMP6_RA_OPTION_OFFSET]);
-        basePacket.clear();
-        newFlowLabelPacket.put(basePacket);
+        ra = new RaPacketBuilder(ROUTER_LIFETIME);
         // Check that changes are ignored in every byte of the flow label.
-        newFlowLabelPacket.putInt(VERSION_TRAFFIC_CLASS_FLOW_LABEL_OFFSET,
-                VERSION_TRAFFIC_CLASS_FLOW_LABEL + 0x11111);
+        ra.setFlowLabel(0x56789);
+        ByteBuffer newFlowLabelPacket = ByteBuffer.wrap(ra.build());
 
         // Ensure zero-length options cause the packet to be silently skipped.
         // Do this before we test other packets. http://b/29586253
-        ByteBuffer zeroLengthOptionPacket = ByteBuffer.wrap(
-                new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN]);
-        basePacket.clear();
-        zeroLengthOptionPacket.put(basePacket);
-        zeroLengthOptionPacket.put((byte)ICMP6_PREFIX_OPTION_TYPE);
-        zeroLengthOptionPacket.put((byte)0);
+        ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        ra.addZeroLengthOption();
+        ByteBuffer zeroLengthOptionPacket = ByteBuffer.wrap(ra.build());
         assertInvalidRa(apfFilter, ipClientCallback, zeroLengthOptionPacket);
 
         // Generate several RAs with different options and lifetimes, and verify when
         // ApfFilter is shown these packets, it generates programs to filter them for the
         // appropriate lifetime.
-        ByteBuffer prefixOptionPacket = ByteBuffer.wrap(
-                new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_PREFIX_OPTION_LEN]);
-        basePacket.clear();
-        prefixOptionPacket.put(basePacket);
-        addPioOption(prefixOptionPacket, PREFIX_VALID_LIFETIME, PREFIX_PREFERRED_LIFETIME,
-                "2001:db8::/64");
+        ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        ra.addPioOption(PREFIX_VALID_LIFETIME, PREFIX_PREFERRED_LIFETIME, "2001:db8::/64");
+        ByteBuffer prefixOptionPacket = ByteBuffer.wrap(ra.build());
         verifyRaLifetime(
                 apfFilter, ipClientCallback, prefixOptionPacket, PREFIX_PREFERRED_LIFETIME);
-        verifyRaEvent(new RaEvent(
-                ROUTER_LIFETIME, PREFIX_VALID_LIFETIME, PREFIX_PREFERRED_LIFETIME, -1, -1, -1));
 
-        ByteBuffer rdnssOptionPacket = ByteBuffer.wrap(
-                new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN + 2 * IPV6_ADDR_LEN]);
-        basePacket.clear();
-        rdnssOptionPacket.put(basePacket);
-        addRdnssOption(rdnssOptionPacket, RDNSS_LIFETIME,
-                "2001:4860:4860::8888", "2001:4860:4860::8844");
+        ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        ra.addRdnssOption(RDNSS_LIFETIME, "2001:4860:4860::8888", "2001:4860:4860::8844");
+        ByteBuffer rdnssOptionPacket = ByteBuffer.wrap(ra.build());
         verifyRaLifetime(apfFilter, ipClientCallback, rdnssOptionPacket, RDNSS_LIFETIME);
-        verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, -1, RDNSS_LIFETIME, -1));
 
         final int lowLifetime = 60;
-        ByteBuffer lowLifetimeRdnssOptionPacket = ByteBuffer.wrap(
-                new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN + IPV6_ADDR_LEN]);
-        basePacket.clear();
-        lowLifetimeRdnssOptionPacket.put(basePacket);
-        addRdnssOption(lowLifetimeRdnssOptionPacket, lowLifetime, "2620:fe::9");
+        ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        ra.addRdnssOption(lowLifetime, "2620:fe::9");
+        ByteBuffer lowLifetimeRdnssOptionPacket = ByteBuffer.wrap(ra.build());
         verifyRaLifetime(apfFilter, ipClientCallback, lowLifetimeRdnssOptionPacket,
                 ROUTER_LIFETIME);
-        verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, -1, lowLifetime, -1));
 
-        ByteBuffer routeInfoOptionPacket = ByteBuffer.wrap(
-                new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN + IPV6_ADDR_LEN]);
-        basePacket.clear();
-        routeInfoOptionPacket.put(basePacket);
-        addRioOption(routeInfoOptionPacket, ROUTE_LIFETIME, "64:ff9b::/96");
+        ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        ra.addRioOption(ROUTE_LIFETIME, "64:ff9b::/96");
+        ByteBuffer routeInfoOptionPacket = ByteBuffer.wrap(ra.build());
         verifyRaLifetime(apfFilter, ipClientCallback, routeInfoOptionPacket, ROUTE_LIFETIME);
-        verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, ROUTE_LIFETIME, -1, -1));
 
         // Check that RIOs differing only in the first 4 bytes are different.
-        ByteBuffer similarRouteInfoOptionPacket = ByteBuffer.wrap(
-                new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN + IPV6_ADDR_LEN]);
-        basePacket.clear();
-        similarRouteInfoOptionPacket.put(basePacket);
-        addRioOption(similarRouteInfoOptionPacket, ROUTE_LIFETIME, "64:ff9b::/64");
+        ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        ra.addRioOption(ROUTE_LIFETIME, "64:ff9b::/64");
         // Packet should be passed because it is different.
-        program = ipClientCallback.getApfProgram();
-        assertPass(program, similarRouteInfoOptionPacket.array());
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertPass(program, ra.build());
 
-        ByteBuffer dnsslOptionPacket = ByteBuffer.wrap(
-                new byte[ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_OPTION_LEN]);
-        basePacket.clear();
-        dnsslOptionPacket.put(basePacket);
-        dnsslOptionPacket.put((byte)ICMP6_DNSSL_OPTION_TYPE);
-        dnsslOptionPacket.put((byte)(ICMP6_4_BYTE_OPTION_LEN / 8));
-        dnsslOptionPacket.putInt(
-                ICMP6_RA_OPTION_OFFSET + ICMP6_4_BYTE_LIFETIME_OFFSET, DNSSL_LIFETIME);
+        ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        ra.addDnsslOption(DNSSL_LIFETIME, "test.example.com", "one.more.example.com");
+        ByteBuffer dnsslOptionPacket = ByteBuffer.wrap(ra.build());
         verifyRaLifetime(apfFilter, ipClientCallback, dnsslOptionPacket, ROUTER_LIFETIME);
-        verifyRaEvent(new RaEvent(ROUTER_LIFETIME, -1, -1, -1, -1, DNSSL_LIFETIME));
 
         ByteBuffer largeRaPacket = ByteBuffer.wrap(buildLargeRa());
         verifyRaLifetime(apfFilter, ipClientCallback, largeRaPacket, 300);
-        verifyRaEvent(new RaEvent(1800, 600, 300, 1200, 7200, -1));
 
         // Verify that current program filters all the RAs (note: ApfFilter.MAX_RAS == 10).
-        program = ipClientCallback.getApfProgram();
+        program = ipClientCallback.assertProgramUpdateAndGet();
         verifyRaLifetime(program, basePacket, ROUTER_LIFETIME);
         verifyRaLifetime(program, newFlowLabelPacket, ROUTER_LIFETIME);
         verifyRaLifetime(program, prefixOptionPacket, PREFIX_PREFERRED_LIFETIME);
@@ -2668,38 +2631,37 @@
         final ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
-        byte[] program = ipClientCallback.getApfProgram();
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
         final int RA_REACHABLE_TIME = 1800;
         final int RA_RETRANSMISSION_TIMER = 1234;
 
         // Create an Ra packet without options
         // Reachable time = 1800, retransmission timer = 1234
-        ByteBuffer raPacket = makeBaseRaPacket();
-        raPacket.position(ICMP6_RA_REACHABLE_TIME_OFFSET);
-        raPacket.putInt(RA_REACHABLE_TIME);
-        raPacket.putInt(RA_RETRANSMISSION_TIMER);
+        RaPacketBuilder ra = new RaPacketBuilder(1800 /* router lft */);
+        ra.setReachableTime(RA_REACHABLE_TIME);
+        ra.setRetransmissionTimer(RA_RETRANSMISSION_TIMER);
+        byte[] raPacket = ra.build();
         // First RA passes filter
-        assertPass(program, raPacket.array());
+        assertPass(program, raPacket);
 
         // Assume apf is shown the given RA, it generates program to filter it.
-        ipClientCallback.resetApfProgramWait();
-        apfFilter.pretendPacketReceived(raPacket.array());
-        program = ipClientCallback.getApfProgram();
-        assertDrop(program, raPacket.array());
+        apfFilter.pretendPacketReceived(raPacket);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, raPacket);
 
         // A packet with different reachable time should be passed.
         // Reachable time = 2300, retransmission timer = 1234
-        raPacket.clear();
-        raPacket.putInt(ICMP6_RA_REACHABLE_TIME_OFFSET, RA_REACHABLE_TIME + 500);
-        assertPass(program, raPacket.array());
+        ra.setReachableTime(RA_REACHABLE_TIME + 500);
+        raPacket = ra.build();
+        assertPass(program, raPacket);
 
         // A packet with different retransmission timer should be passed.
         // Reachable time = 1800, retransmission timer = 2234
-        raPacket.clear();
-        raPacket.putInt(ICMP6_RA_REACHABLE_TIME_OFFSET, RA_REACHABLE_TIME);
-        raPacket.putInt(ICMP6_RA_RETRANSMISSION_TIMER_OFFSET, RA_RETRANSMISSION_TIMER + 1000);
-        assertPass(program, raPacket.array());
+        ra.setReachableTime(RA_REACHABLE_TIME);
+        ra.setRetransmissionTimer(RA_RETRANSMISSION_TIMER + 1000);
+        raPacket = ra.build();
+        assertPass(program, raPacket);
     }
 
     // The ByteBuffer is always created by ByteBuffer#wrap in the helper functions
@@ -2710,21 +2672,21 @@
         final ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mLog);
-        byte[] program = ipClientCallback.getApfProgram();
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         final int routerLifetime = 1000;
         final int timePassedSeconds = 12;
 
         // Verify that when the program is generated and installed some time after RA is last seen
         // it should be installed with the correct remaining lifetime.
-        ByteBuffer basePacket = makeBaseRaPacket();
+        ByteBuffer basePacket = ByteBuffer.wrap(new RaPacketBuilder(routerLifetime).build());
         verifyRaLifetime(apfFilter, ipClientCallback, basePacket, routerLifetime);
         apfFilter.increaseCurrentTimeSeconds(timePassedSeconds);
         synchronized (apfFilter) {
             apfFilter.installNewProgramLocked();
         }
-        program = ipClientCallback.getApfProgram();
+        program = ipClientCallback.assertProgramUpdateAndGet();
         verifyRaLifetime(program, basePacket, routerLifetime, timePassedSeconds);
 
         // Packet should be passed if the program is installed after 1/6 * lifetime from last seen
@@ -2732,13 +2694,13 @@
         synchronized (apfFilter) {
             apfFilter.installNewProgramLocked();
         }
-        program = ipClientCallback.getApfProgram();
+        program = ipClientCallback.assertProgramUpdateAndGet();
         assertDrop(program, basePacket.array());
         apfFilter.increaseCurrentTimeSeconds(1);
         synchronized (apfFilter) {
             apfFilter.installNewProgramLocked();
         }
-        program = ipClientCallback.getApfProgram();
+        program = ipClientCallback.assertProgramUpdateAndGet();
         assertPass(program, basePacket.array());
 
         apfFilter.shutdown();
@@ -2779,7 +2741,7 @@
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mLog);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb);
         for (int i = 0; i < 1000; i++) {
             byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)];
             r.nextBytes(packet);
@@ -2790,6 +2752,7 @@
                 throw new Exception("bad packet: " + HexDump.toHexString(packet), e);
             }
         }
+        apfFilter.shutdown();
     }
 
     @Test
@@ -2800,7 +2763,7 @@
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mLog);
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb);
         for (int i = 0; i < 1000; i++) {
             byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)];
             r.nextBytes(packet);
@@ -2810,37 +2773,293 @@
                 throw new Exception("bad packet: " + HexDump.toHexString(packet), e);
             }
         }
+        apfFilter.shutdown();
     }
 
-    /**
-     * Call the APF interpreter to run {@code program} on {@code packet} with persistent memory
-     * segment {@data} pretending the filter was installed {@code filter_age} seconds ago.
-     */
-    private native static int apfSimulate(byte[] program, byte[] packet, byte[] data,
-        int filter_age);
+    @Test
+    public void testMatchedRaUpdatesLifetime() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, getDefaultConfig(),
+                ipClientCallback);
 
-    /**
-     * Compile a tcpdump human-readable filter (e.g. "icmp" or "tcp port 54") into a BPF
-     * prorgam and return a human-readable dump of the BPF program identical to "tcpdump -d".
-     */
-    private native static String compileToBpf(String filter);
+        // Create an RA and build an APF program
+        byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build();
+        apfFilter.pretendPacketReceived(ra);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
-    /**
-     * Open packet capture file {@code pcap_filename} and filter the packets using tcpdump
-     * human-readable filter (e.g. "icmp" or "tcp port 54") compiled to a BPF program and
-     * at the same time using APF program {@code apf_program}.  Return {@code true} if
-     * both APF and BPF programs filter out exactly the same packets.
-     */
-    private native static boolean compareBpfApf(String filter, String pcap_filename,
-            byte[] apf_program);
+        // lifetime dropped significantly, assert pass
+        ra = new RaPacketBuilder(200 /* router lifetime */).build();
+        assertPass(program, ra);
+
+        // update program with the new RA
+        apfFilter.pretendPacketReceived(ra);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // assert program was updated and new lifetimes were taken into account.
+        assertDrop(program, ra);
+        apfFilter.shutdown();
+    }
+
+    // Test for go/apf-ra-filter Case 1a.
+    // Old lifetime is 0
+    @Test
+    public void testAcceptRaMinLftCase1a() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        // configure accept_ra_min_lft
+        final ApfConfiguration config = getDefaultConfig();
+        config.acceptRaMinLft = 180;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+
+        // Create an initial RA and build an APF program
+        byte[] ra = new RaPacketBuilder(1800 /* router lifetime */)
+                .addPioOption(1800 /*valid*/, 0 /*preferred*/, "2001:db8::/64")
+                .build();
+
+        apfFilter.pretendPacketReceived(ra);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // repeated RA is dropped
+        assertDrop(program, ra);
+
+        // PIO preferred lifetime increases
+        ra = new RaPacketBuilder(1800 /* router lifetime */)
+                .addPioOption(1800 /*valid*/, 1 /*preferred*/, "2001:db8::/64")
+                .build();
+        assertPass(program, ra);
+        apfFilter.shutdown();
+    }
+
+    // Test for go/apf-ra-filter Case 2a.
+    // Old lifetime is > 0
+    @Test
+    public void testAcceptRaMinLftCase2a() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        // configure accept_ra_min_lft
+        final ApfConfiguration config = getDefaultConfig();
+        config.acceptRaMinLft = 180;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+
+        // Create an initial RA and build an APF program
+        byte[] ra = new RaPacketBuilder(1800 /* router lifetime */)
+                .addPioOption(1800 /*valid*/, 100 /*preferred*/, "2001:db8::/64")
+                .build();
+
+        apfFilter.pretendPacketReceived(ra);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // repeated RA is dropped
+        assertDrop(program, ra);
+
+        // PIO preferred lifetime increases
+        ra = new RaPacketBuilder(1800 /* router lifetime */)
+                .addPioOption(1800 /*valid*/, 101 /*preferred*/, "2001:db8::/64")
+                .build();
+        assertPass(program, ra);
+
+        // PIO preferred lifetime decreases significantly
+        ra = new RaPacketBuilder(1800 /* router lifetime */)
+                .addPioOption(1800 /*valid*/, 33 /*preferred*/, "2001:db8::/64")
+                .build();
+        assertPass(program, ra);
+        apfFilter.shutdown();
+    }
 
 
-    /**
-     * Open packet capture file {@code pcapFilename} and run it through APF filter. Then
-     * checks whether all the packets are dropped and populates data[] {@code data} with
-     * the APF counters.
-     */
-    private native static boolean dropsAllPackets(byte[] program, byte[] data, String pcapFilename);
+    // Test for go/apf-ra-filter Case 1b.
+    // Old lifetime is 0
+    @Test
+    public void testAcceptRaMinLftCase1b() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        // configure accept_ra_min_lft
+        final ApfConfiguration config = getDefaultConfig();
+        config.acceptRaMinLft = 180;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+
+        // Create an initial RA and build an APF program
+        byte[] ra = new RaPacketBuilder(0 /* router lifetime */).build();
+
+        apfFilter.pretendPacketReceived(ra);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // repeated RA is dropped
+        assertDrop(program, ra);
+
+        // lifetime increases below accept_ra_min_lft
+        ra = new RaPacketBuilder(179 /* router lifetime */).build();
+        assertDrop(program, ra);
+
+        // lifetime increases to accept_ra_min_lft
+        ra = new RaPacketBuilder(180 /* router lifetime */).build();
+        assertPass(program, ra);
+        apfFilter.shutdown();
+    }
+
+
+    // Test for go/apf-ra-filter Case 2b.
+    // Old lifetime is < accept_ra_min_lft (but not 0).
+    @Test
+    public void testAcceptRaMinLftCase2b() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        // configure accept_ra_min_lft
+        final ApfConfiguration config = getDefaultConfig();
+        config.acceptRaMinLft = 180;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+
+        // Create an initial RA and build an APF program
+        byte[] ra = new RaPacketBuilder(100 /* router lifetime */).build();
+
+        apfFilter.pretendPacketReceived(ra);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // repeated RA is dropped
+        assertDrop(program, ra);
+
+        // lifetime increases
+        ra = new RaPacketBuilder(101 /* router lifetime */).build();
+        assertDrop(program, ra);
+
+        // lifetime decreases significantly
+        ra = new RaPacketBuilder(1 /* router lifetime */).build();
+        assertDrop(program, ra);
+
+        // equals accept_ra_min_lft
+        ra = new RaPacketBuilder(180 /* router lifetime */).build();
+        assertPass(program, ra);
+
+        // lifetime is 0
+        ra = new RaPacketBuilder(0 /* router lifetime */).build();
+        assertPass(program, ra);
+        apfFilter.shutdown();
+    }
+
+    // Test for go/apf-ra-filter Case 3b.
+    // Old lifetime is >= accept_ra_min_lft and <= 3 * accept_ra_min_lft
+    @Test
+    public void testAcceptRaMinLftCase3b() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        // configure accept_ra_min_lft
+        final ApfConfiguration config = getDefaultConfig();
+        config.acceptRaMinLft = 180;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+
+        // Create an initial RA and build an APF program
+        byte[] ra = new RaPacketBuilder(200 /* router lifetime */).build();
+
+        apfFilter.pretendPacketReceived(ra);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // repeated RA is dropped
+        assertDrop(program, ra);
+
+        // lifetime increases
+        ra = new RaPacketBuilder(201 /* router lifetime */).build();
+        assertPass(program, ra);
+
+        // lifetime is below accept_ra_min_lft (but not 0)
+        ra = new RaPacketBuilder(1 /* router lifetime */).build();
+        assertDrop(program, ra);
+
+        // lifetime is 0
+        ra = new RaPacketBuilder(0 /* router lifetime */).build();
+        assertPass(program, ra);
+        apfFilter.shutdown();
+    }
+
+    // Test for go/apf-ra-filter Case 4b.
+    // Old lifetime is > 3 * accept_ra_min_lft
+    @Test
+    public void testAcceptRaMinLftCase4b() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        // configure accept_ra_min_lft
+        final ApfConfiguration config = getDefaultConfig();
+        config.acceptRaMinLft = 180;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+
+        // Create an initial RA and build an APF program
+        byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build();
+
+        apfFilter.pretendPacketReceived(ra);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // repeated RA is dropped
+        assertDrop(program, ra);
+
+        // lifetime increases
+        ra = new RaPacketBuilder(1801 /* router lifetime */).build();
+        assertPass(program, ra);
+
+        // lifetime is 1/3 of old lft
+        ra = new RaPacketBuilder(600 /* router lifetime */).build();
+        assertDrop(program, ra);
+
+        // lifetime is below 1/3 of old lft
+        ra = new RaPacketBuilder(599 /* router lifetime */).build();
+        assertPass(program, ra);
+
+        // lifetime is below accept_ra_min_lft (but not 0)
+        ra = new RaPacketBuilder(1 /* router lifetime */).build();
+        assertDrop(program, ra);
+
+        // lifetime is 0
+        ra = new RaPacketBuilder(0 /* router lifetime */).build();
+        assertPass(program, ra);
+        apfFilter.shutdown();
+    }
+
+    @Test
+    public void testRaFilterIsUpdated() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        // configure accept_ra_min_lft
+        final ApfConfiguration config = getDefaultConfig();
+        config.acceptRaMinLft = 180;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback);
+
+        // Create an initial RA and build an APF program
+        byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build();
+        apfFilter.pretendPacketReceived(ra);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // repeated RA is dropped.
+        assertDrop(program, ra);
+
+        // updated RA is passed, repeated RA is dropped after program update.
+        ra = new RaPacketBuilder(599 /* router lifetime */).build();
+        assertPass(program, ra);
+        apfFilter.pretendPacketReceived(ra);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, ra);
+
+        ra = new RaPacketBuilder(180 /* router lifetime */).build();
+        assertPass(program, ra);
+        apfFilter.pretendPacketReceived(ra);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, ra);
+
+        ra = new RaPacketBuilder(0 /* router lifetime */).build();
+        assertPass(program, ra);
+        apfFilter.pretendPacketReceived(ra);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, ra);
+
+        ra = new RaPacketBuilder(180 /* router lifetime */).build();
+        assertPass(program, ra);
+        apfFilter.pretendPacketReceived(ra);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, ra);
+
+        ra = new RaPacketBuilder(599 /* router lifetime */).build();
+        assertPass(program, ra);
+        apfFilter.pretendPacketReceived(ra);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, ra);
+
+        ra = new RaPacketBuilder(1800 /* router lifetime */).build();
+        assertPass(program, ra);
+        apfFilter.pretendPacketReceived(ra);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, ra);
+        apfFilter.shutdown();
+    }
 
     @Test
     public void testBroadcastAddress() throws Exception {
diff --git a/tests/unit/src/android/net/apf/ApfTestUtils.java b/tests/unit/src/android/net/apf/ApfTestUtils.java
new file mode 100644
index 0000000..555b92e
--- /dev/null
+++ b/tests/unit/src/android/net/apf/ApfTestUtils.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2023 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.apf;
+
+import static android.net.apf.ApfJniUtils.apfSimulate;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.internal.util.HexDump;
+
+import java.util.Arrays;
+
+/**
+ * The util class for calling the APF interpreter and check the return value
+ */
+public class ApfTestUtils {
+    public static final int PASS = 1;
+    public static final int DROP = 0;
+    // Interpreter will just accept packets without link layer headers, so pad fake packet to at
+    // least the minimum packet size.
+    public static final int MIN_PKT_SIZE = 15;
+
+    private ApfTestUtils() {
+    }
+
+    private static String label(int code) {
+        switch (code) {
+            case PASS:
+                return "PASS";
+            case DROP:
+                return "DROP";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+    private static void assertReturnCodesEqual(String msg, int expected, int got) {
+        assertEquals(msg, label(expected), label(got));
+    }
+
+    private static void assertReturnCodesEqual(int expected, int got) {
+        assertEquals(label(expected), label(got));
+    }
+
+    private static void assertVerdict(int apfVersion, int expected, byte[] program, byte[] packet,
+            int filterAge) {
+        final String msg = "Unexpected APF verdict. To debug:\n" + "  apf_run --program "
+                + HexDump.toHexString(program) + " --packet " + HexDump.toHexString(packet)
+                + " --trace | less\n  ";
+        assertReturnCodesEqual(msg, expected,
+                apfSimulate(apfVersion, program, packet, null, filterAge));
+    }
+
+    /**
+     * Runs the APF program and checks the return code is equals to expected value. If not, the
+     * customized message is printed.
+     */
+    public static void assertVerdict(int apfVersion, String msg, int expected, byte[] program,
+            byte[] packet, int filterAge) {
+        assertReturnCodesEqual(msg, expected,
+                apfSimulate(apfVersion, program, packet, null, filterAge));
+    }
+
+    /**
+     * Runs the APF program and checks the return code is equals to expected value.
+     */
+    public static void assertVerdict(int apfVersion, int expected, byte[] program, byte[] packet) {
+        assertVerdict(apfVersion, expected, program, packet, 0);
+    }
+
+    /**
+     * Runs the APF program and checks the return code is PASS.
+     */
+    public static void assertPass(int apfVersion, byte[] program, byte[] packet, int filterAge) {
+        assertVerdict(apfVersion, PASS, program, packet, filterAge);
+    }
+
+    /**
+     * Runs the APF program and checks the return code is PASS.
+     */
+    public static void assertPass(int apfVersion, byte[] program, byte[] packet) {
+        assertVerdict(apfVersion, PASS, program, packet);
+    }
+
+    /**
+     * Runs the APF program and checks the return code is DROP.
+     */
+    public static void assertDrop(int apfVersion, byte[] program, byte[] packet, int filterAge) {
+        assertVerdict(apfVersion, DROP, program, packet, filterAge);
+    }
+
+    /**
+     * Runs the APF program and checks the return code is DROP.
+     */
+    public static void assertDrop(int apfVersion, byte[] program, byte[] packet) {
+        assertVerdict(apfVersion, DROP, program, packet);
+    }
+
+    /**
+     * Checks the generated APF program equals to the expected value.
+     */
+    public static void assertProgramEquals(byte[] expected, byte[] program) throws AssertionError {
+        // assertArrayEquals() would only print one byte, making debugging difficult.
+        if (!Arrays.equals(expected, program)) {
+            throw new AssertionError("\nexpected: " + HexDump.toHexString(expected) + "\nactual:   "
+                    + HexDump.toHexString(program));
+        }
+    }
+
+    /**
+     * Runs the APF program and checks the return code and data regions equals to expected value.
+     */
+    public static void assertDataMemoryContents(int apfVersion, int expected, byte[] program,
+            byte[] packet, byte[] data, byte[] expectedData)
+            throws ApfGenerator.IllegalInstructionException, Exception {
+        assertReturnCodesEqual(expected,
+                apfSimulate(apfVersion, program, packet, data, 0 /* filterAge */));
+
+        // assertArrayEquals() would only print one byte, making debugging difficult.
+        if (!Arrays.equals(expectedData, data)) {
+            throw new Exception("\nprogram:     " + HexDump.toHexString(program) + "\ndata memory: "
+                    + HexDump.toHexString(data) + "\nexpected:    " + HexDump.toHexString(
+                    expectedData));
+        }
+    }
+
+    private static void assertVerdict(int apfVersion, int expected, ApfGenerator gen, byte[] packet,
+            int filterAge) throws ApfGenerator.IllegalInstructionException {
+        assertReturnCodesEqual(expected,
+                apfSimulate(apfVersion, gen.generate(), packet, null, filterAge));
+    }
+
+    /**
+     * Runs the APF program and checks the return code is PASS.
+     */
+    public static void assertPass(int apfVersion, ApfGenerator gen, byte[] packet, int filterAge)
+            throws ApfGenerator.IllegalInstructionException {
+        assertVerdict(apfVersion, PASS, gen, packet, filterAge);
+    }
+
+    /**
+     * Runs the APF program and checks the return code is DROP.
+     */
+    public static void assertDrop(int apfVersion, ApfGenerator gen, byte[] packet, int filterAge)
+            throws ApfGenerator.IllegalInstructionException {
+        assertVerdict(apfVersion, DROP, gen, packet, filterAge);
+    }
+
+    /**
+     * Runs the APF program and checks the return code is PASS.
+     */
+    public static void assertPass(int apfVersion, ApfGenerator gen)
+            throws ApfGenerator.IllegalInstructionException {
+        assertVerdict(apfVersion, PASS, gen, new byte[MIN_PKT_SIZE], 0);
+    }
+
+    /**
+     * Runs the APF program and checks the return code is DROP.
+     */
+    public static void assertDrop(int apfVersion, ApfGenerator gen)
+            throws ApfGenerator.IllegalInstructionException {
+        assertVerdict(apfVersion, DROP, gen, new byte[MIN_PKT_SIZE], 0);
+    }
+}
diff --git a/tests/unit/src/android/net/apf/ApfV5Test.kt b/tests/unit/src/android/net/apf/ApfV5Test.kt
new file mode 100644
index 0000000..d42396a
--- /dev/null
+++ b/tests/unit/src/android/net/apf/ApfV5Test.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 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.apf
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import kotlin.test.assertContentEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests for APFv6 specific instructions.
+ */
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class ApfV5Test {
+
+    @Test
+    fun testApfInstructionsEncoding() {
+        var gen = ApfGenerator(MIN_APF_VERSION)
+        gen.addAlloc(ApfGenerator.Register.R0)
+        var program = gen.generate()
+        assertContentEquals(byteArrayOf(encodeInstruction(21, 1, 0), 36), program)
+        assertContentEquals(arrayOf("       0: alloc r0"), ApfJniUtils.disassembleApf(program))
+
+        gen = ApfGenerator(MIN_APF_VERSION)
+        gen.addTrans(ApfGenerator.Register.R1)
+        program = gen.generate()
+        assertContentEquals(byteArrayOf(encodeInstruction(21, 1, 1), 37), program)
+        assertContentEquals(arrayOf("       0: trans r1"), ApfJniUtils.disassembleApf(program))
+
+        gen = ApfGenerator(MIN_APF_VERSION)
+        gen.addWrite(0x01, 1)
+        gen.addWrite(0x0102, 2)
+        gen.addWrite(0x01020304, 4)
+        program = gen.generate()
+        assertContentEquals(byteArrayOf(
+                encodeInstruction(24, 1, 0), 0x01,
+                encodeInstruction(24, 2, 0), 0x01, 0x02,
+                encodeInstruction(24, 4, 0), 0x01, 0x02, 0x03, 0x04
+        ), program)
+        assertContentEquals(arrayOf(
+                "       0: write 0x01",
+                "       2: write 0x0102",
+                "       5: write 0x01020304"), ApfJniUtils.disassembleApf(program))
+
+        gen = ApfGenerator(MIN_APF_VERSION)
+        gen.addWrite(ApfGenerator.Register.R0, 1)
+        gen.addWrite(ApfGenerator.Register.R0, 2)
+        gen.addWrite(ApfGenerator.Register.R0, 4)
+        program = gen.generate()
+        assertContentEquals(byteArrayOf(
+                encodeInstruction(21, 1, 0), 38,
+                encodeInstruction(21, 1, 0), 39,
+                encodeInstruction(21, 1, 0), 40
+        ), program)
+        assertContentEquals(arrayOf(
+                "       0: write r0, 1",
+                "       2: write r0, 2",
+                "       4: write r0, 4"), ApfJniUtils.disassembleApf(program))
+    }
+
+    private fun encodeInstruction(opcode: Int, immLength: Int, register: Int): Byte {
+        val immLengthEncoding = if (immLength == 4) 3 else immLength
+        return opcode.shl(3).or(immLengthEncoding.shl(1)).or(register).toByte()
+    }
+
+    companion object {
+        private const val MIN_APF_VERSION = 5
+    }
+}
diff --git a/tests/unit/src/android/net/dhcp/DhcpPacketTest.java b/tests/unit/src/android/net/dhcp/DhcpPacketTest.java
index 1a1f6c3..42ea54b 100644
--- a/tests/unit/src/android/net/dhcp/DhcpPacketTest.java
+++ b/tests/unit/src/android/net/dhcp/DhcpPacketTest.java
@@ -1300,6 +1300,44 @@
         checkBuildOfferPacket(3600, null);
     }
 
+    @Test
+    public void testInvalidLengthIpv6OnlyPreferredOption() throws Exception {
+        // CHECKSTYLE:OFF Generated code
+        final ByteBuffer packet = ByteBuffer.wrap(HexDump.hexStringToByteArray(
+                // IP header.
+                "45100158000040004011B5CEC0A80164C0A80102" +
+                // UDP header
+                "004300440144CE63" +
+                // BOOTP header
+                "02010600B8BF41E60000000000000000C0A80102C0A8016400000000" +
+                // MAC address.
+                "22B3614EE01200000000000000000000" +
+                // Server name and padding.
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                // File.
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                "0000000000000000000000000000000000000000000000000000000000000000" +
+                // Options
+                "638253633501023604C0A80164330400000E103A04000007083B0400000C4E01" +
+                "04FFFFFF001C04C0A801FF0304C0A801640604C0A801640C0C74657374686F73" +
+                "746E616D651A0205DC" +
+                // Option 108 (0x6c, IPv6-Only preferred option), length 8 (0x08)
+                "6C080102030405060708" +
+                // End of options.
+                "FF"));
+        // CHECKSTYLE:ON Generated code
+
+        final DhcpPacket offerPacket = DhcpPacket.decodeFullPacket(packet, ENCAP_L3,
+                TEST_EMPTY_OPTIONS_SKIP_LIST);
+        // rfc8925#section-3.1: The client MUST ignore the IPv6-Only Preferred option if the length
+        // field value is not 4.
+        assertTrue(offerPacket instanceof DhcpOfferPacket);
+        assertEquals(offerPacket.mIpv6OnlyWaitTime, null);
+    }
+
     private static byte[] intToByteArray(int val) {
         return ByteBuffer.allocate(4).putInt(val).array();
     }
diff --git a/tests/unit/src/android/net/ip/IpClientTest.java b/tests/unit/src/android/net/ip/IpClientTest.java
index 7db939b..d8b2cc2 100644
--- a/tests/unit/src/android/net/ip/IpClientTest.java
+++ b/tests/unit/src/android/net/ip/IpClientTest.java
@@ -17,6 +17,7 @@
 package android.net.ip;
 
 import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -36,6 +37,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
+
 import static java.util.Collections.emptySet;
 
 import android.annotation.SuppressLint;
@@ -420,6 +422,8 @@
         verifyNetworkAttributesStored(l2Key, new NetworkAttributes.Builder()
                 .setCluster(cluster)
                 .build());
+
+        verifyShutdown(ipc);
     }
 
     private void verifyShutdown(IpClient ipc) throws Exception {
@@ -479,6 +483,8 @@
                 fail(testcase.errorMessage());
             }
         }
+
+        ipc.shutdown();
     }
 
     static class IsProvisionedTestCase {
@@ -827,6 +833,7 @@
         final MacAddress bssid = ipc.getInitialBssid(layer2Info, scanResultInfo,
                 true /* isAtLeastS */);
         assertEquals(bssid, MacAddress.fromString(TEST_BSSID));
+        ipc.shutdown();
     }
 
     @Test
@@ -837,6 +844,7 @@
         final MacAddress bssid = ipc.getInitialBssid(layer2Info, null /* ScanResultInfo */,
                 true /* isAtLeastS */);
         assertEquals(bssid, MacAddress.fromString(TEST_BSSID));
+        ipc.shutdown();
     }
 
     @Test
@@ -848,6 +856,7 @@
         final MacAddress bssid = ipc.getInitialBssid(layer2Info, scanResultInfo,
                 true /* isAtLeastS */);
         assertNull(bssid);
+        ipc.shutdown();
     }
 
     @Test
@@ -857,6 +866,7 @@
         final MacAddress bssid = ipc.getInitialBssid(null /* layer2Info */, scanResultInfo,
                 true /* isAtLeastS */);
         assertNull(bssid);
+        ipc.shutdown();
     }
 
     @Test
@@ -868,6 +878,7 @@
         final MacAddress bssid = ipc.getInitialBssid(layer2Info, scanResultInfo,
                 false /* isAtLeastS */);
         assertEquals(bssid, MacAddress.fromString(TEST_BSSID));
+        ipc.shutdown();
     }
 
     @Test
@@ -877,6 +888,7 @@
         final MacAddress bssid = ipc.getInitialBssid(null /* layer2Info */, scanResultInfo,
                 false /* isAtLeastS */);
         assertEquals(bssid, MacAddress.fromString(TEST_BSSID));
+        ipc.shutdown();
     }
 
     @Test
@@ -886,6 +898,7 @@
         final MacAddress bssid = ipc.getInitialBssid(null /* layer2Info */, scanResultInfo,
                 false /* isAtLeastS */);
         assertNull(bssid);
+        ipc.shutdown();
     }
 
     @Test
@@ -897,6 +910,7 @@
         final MacAddress bssid = ipc.getInitialBssid(layer2Info, scanResultInfo,
                 false /* isAtLeastS */);
         assertEquals(bssid, MacAddress.fromString(TEST_BSSID));
+        ipc.shutdown();
     }
 
     @Test
@@ -907,6 +921,7 @@
         final MacAddress bssid = ipc.getInitialBssid(layer2Info, null /* scanResultInfo */,
                 false /* isAtLeastS */);
         assertEquals(bssid, MacAddress.fromString(TEST_BSSID));
+        ipc.shutdown();
     }
 
     @Test
@@ -915,6 +930,7 @@
         final MacAddress bssid = ipc.getInitialBssid(null /* layer2Info */,
                 null /* scanResultInfo */, false /* isAtLeastS */);
         assertNull(bssid);
+        ipc.shutdown();
     }
 
     interface Fn<A,B> {
diff --git a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
index 3800752..6471f3a 100644
--- a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
+++ b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
@@ -24,7 +24,6 @@
 import android.net.LinkProperties
 import android.net.RouteInfo
 import android.net.metrics.IpConnectivityLog
-import com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION
 import android.os.Handler
 import android.os.HandlerThread
 import android.os.MessageQueue
@@ -36,12 +35,12 @@
 import android.stats.connectivity.NudEventType.NUD_CONFIRM_FAILED
 import android.stats.connectivity.NudEventType.NUD_CONFIRM_FAILED_CRITICAL
 import android.stats.connectivity.NudEventType.NUD_CONFIRM_MAC_ADDRESS_CHANGED
-import android.stats.connectivity.NudEventType.NUD_POST_ROAMING_FAILED
-import android.stats.connectivity.NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL
-import android.stats.connectivity.NudEventType.NUD_POST_ROAMING_MAC_ADDRESS_CHANGED
 import android.stats.connectivity.NudEventType.NUD_ORGANIC_FAILED
 import android.stats.connectivity.NudEventType.NUD_ORGANIC_FAILED_CRITICAL
 import android.stats.connectivity.NudEventType.NUD_ORGANIC_MAC_ADDRESS_CHANGED
+import android.stats.connectivity.NudEventType.NUD_POST_ROAMING_FAILED
+import android.stats.connectivity.NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL
+import android.stats.connectivity.NudEventType.NUD_POST_ROAMING_MAC_ADDRESS_CHANGED
 import android.stats.connectivity.NudNeighborType
 import android.stats.connectivity.NudNeighborType.NUD_NEIGHBOR_BOTH
 import android.stats.connectivity.NudNeighborType.NUD_NEIGHBOR_DNS
@@ -50,32 +49,16 @@
 import android.system.OsConstants.EAGAIN
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
-import com.android.networkstack.metrics.IpReachabilityMonitorMetrics
 import com.android.net.module.util.InterfaceParams
 import com.android.net.module.util.SharedLog
 import com.android.net.module.util.ip.IpNeighborMonitor
 import com.android.net.module.util.netlink.StructNdMsg.NUD_FAILED
 import com.android.net.module.util.netlink.StructNdMsg.NUD_REACHABLE
 import com.android.net.module.util.netlink.StructNdMsg.NUD_STALE
+import com.android.networkstack.metrics.IpReachabilityMonitorMetrics
+import com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION
 import com.android.testutils.makeNewNeighMessage
 import com.android.testutils.waitForIdle
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.anyObject
-import org.mockito.ArgumentMatchers.anyString
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito.doAnswer
-import org.mockito.Mockito.doReturn
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.timeout
-import org.mockito.Mockito.verify
 import java.io.FileDescriptor
 import java.net.Inet4Address
 import java.net.Inet6Address
@@ -86,6 +69,21 @@
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
 import kotlin.test.fail
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
 
 private const val TEST_TIMEOUT_MS = 10_000L
 
@@ -261,8 +259,8 @@
         }.`when`(dependencies).makeIpNeighborMonitor(any(), any(), any())
         doReturn(mIpReachabilityMonitorMetrics)
                 .`when`(dependencies).getIpReachabilityMonitorMetrics()
-        doReturn(true).`when`(dependencies).isFeatureEnabled(anyObject(),
-                eq(IP_REACHABILITY_MCAST_RESOLICIT_VERSION), anyBoolean())
+        doReturn(true).`when`(dependencies).isFeatureNotChickenedOut(any(),
+                eq(IP_REACHABILITY_MCAST_RESOLICIT_VERSION))
 
         val monitorFuture = CompletableFuture<IpReachabilityMonitor>()
         // IpReachabilityMonitor needs to be started from the handler thread
diff --git a/tests/unit/src/com/android/networkstack/netlink/TcpInfoTest.java b/tests/unit/src/com/android/networkstack/netlink/TcpInfoTest.java
index c362044..380f4ec 100644
--- a/tests/unit/src/com/android/networkstack/netlink/TcpInfoTest.java
+++ b/tests/unit/src/com/android/networkstack/netlink/TcpInfoTest.java
@@ -75,7 +75,7 @@
             "02000000" +          // reordering = 3
             "00000000" +          // rcvrtt = 0
             "30560100" +          // rcvspace = 87600
-            "00000000" +          // totalRetrans = 0
+            "05000000" +          // totalRetrans = 5
             "53AC000000000000" +  // pacingRate = 44115
             "FFFFFFFFFFFFFFFF" +  // maxPacingRate = 18446744073709551615
             "0100000000000001" +  // bytesAcked = 1
@@ -93,7 +93,7 @@
     private static final byte[] TCP_INFO_BYTES =
             HexEncoding.decode(TCP_INFO_HEX.toCharArray(), false);
     private static final TcpInfo TEST_TCPINFO =
-            new TcpInfo(0 /* retransmits */, 0 /* lost */, 2 /* segsOut */, 1 /* segsIn */);
+            new TcpInfo(2 /* segsOut */, 1 /* segsIn */, 5 /* totalRetrans */);
 
     private static final String EXPANDED_TCP_INFO_HEX = TCP_INFO_HEX
             + "00000000"         // tcpi_delivered
@@ -134,6 +134,7 @@
     public void testFieldOffset() {
         assertEquals(TcpInfo.RETRANSMITS_OFFSET, 2);
         assertEquals(TcpInfo.LOST_OFFSET, 32);
+        assertEquals(TcpInfo.TOTAL_RETRANS_OFFSET, 100);
         assertEquals(TcpInfo.SEGS_OUT_OFFSET, 136);
         assertEquals(TcpInfo.SEGS_IN_OFFSET, 140);
     }
diff --git a/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java b/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java
index 1d15719..165deab 100644
--- a/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java
+++ b/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java
@@ -18,6 +18,7 @@
 
 import static android.net.util.DataStallUtils.CONFIG_TCP_PACKETS_FAIL_PERCENTAGE;
 import static android.net.util.DataStallUtils.DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE;
+import static android.os.PowerManager.ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 import static android.system.OsConstants.AF_INET;
 
@@ -27,16 +28,13 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
-import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.content.BroadcastReceiver;
@@ -58,9 +56,7 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.netlink.NetlinkUtils;
 import com.android.net.module.util.netlink.StructNlMsgHdr;
-import com.android.networkstack.apishim.ConstantsShim;
 import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
 
 import libcore.util.HexEncoding;
@@ -95,12 +91,12 @@
     private static final byte[] SOCK_DIAG_MSG_BYTES =
             HexEncoding.decode(DIAG_MSG_HEX.toCharArray(), false);
     // Hexadecimal representation of a SOCK_DIAG response with tcp info.
-    private static final String SOCK_DIAG_TCP_ZERO_LOST_HEX =
-            composeSockDiagTcpHex(0 /* lost */, 10 /* sent */);
-    private static final byte[] SOCK_DIAG_TCP_INET_ZERO_LOST_BYTES =
-            HexEncoding.decode(SOCK_DIAG_TCP_ZERO_LOST_HEX.toCharArray(), false);
+    private static final String SOCK_DIAG_TCP_TEST_HEX =
+            composeSockDiagTcpHex(5 /* retrans */, 10 /* sent */);
+    private static final byte[] SOCK_DIAG_TCP_INET_TEST_BYTES =
+            HexEncoding.decode(SOCK_DIAG_TCP_TEST_HEX.toCharArray(), false);
     private static final TcpInfo TEST_TCPINFO =
-            new TcpInfo(5 /* retransmits */, 0 /* lost */, 10 /* segsOut */, 0 /* segsIn */);
+            new TcpInfo(10 /* segsOut */, 0 /* segsIn */, 5 /* totalRetrans */);
     private static final String NLMSG_DONE_HEX =
             // struct nlmsghdr
             "14000000"     // length = 20
@@ -113,7 +109,7 @@
             + "06"           // state
             + "00"           // timer
             + "00";          // retrans
-    private static final String TEST_RESPONSE_HEX = SOCK_DIAG_TCP_ZERO_LOST_HEX + NLMSG_DONE_HEX;
+    private static final String TEST_RESPONSE_HEX = SOCK_DIAG_TCP_TEST_HEX + NLMSG_DONE_HEX;
     private static final byte[] TEST_RESPONSE_BYTES =
             HexEncoding.decode(TEST_RESPONSE_HEX.toCharArray(), false);
     private static final int TEST_NETID1 = 0xA85;
@@ -145,12 +141,12 @@
         mOldWtfHandler =
                 Log.setWtfHandler((tag, what, system) -> Log.e(tag, what.getMessage(), what));
         when(mDependencies.getNetd()).thenReturn(mNetd);
-        when(mDependencies.isTcpInfoParsingSupported()).thenReturn(true);
         when(mDependencies.connectToKernel()).thenReturn(new FileDescriptor());
         when(mDependencies.getDeviceConfigPropertyInt(
                 eq(NAMESPACE_CONNECTIVITY),
                 eq(CONFIG_TCP_PACKETS_FAIL_PERCENTAGE),
                 anyInt())).thenReturn(DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE);
+        when(mDependencies.shouldDisableInLightDoze()).thenReturn(true);
 
         when(mNetd.getFwmarkForNetwork(eq(TEST_NETID1)))
                 .thenReturn(makeMarkMaskParcel(NETID_MASK, TEST_NETID1_FWMARK));
@@ -182,7 +178,7 @@
 
     @Test
     public void testParseSockInfo() {
-        final ByteBuffer buffer = getByteBuffer(SOCK_DIAG_TCP_INET_ZERO_LOST_BYTES);
+        final ByteBuffer buffer = getByteBuffer(SOCK_DIAG_TCP_INET_TEST_BYTES);
         final ArrayList<TcpSocketTracker.SocketInfo> infoList = new ArrayList<>();
         TcpSocketTracker.parseMessage(buffer, AF_INET, infoList, 100L);
         assertEquals(1, infoList.size());
@@ -211,15 +207,10 @@
         assertFalse(NetlinkUtils.enoughBytesRemainForValidNlMsg(buffer));
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q) // TCP info parsing is not supported on Q
+    @Test
     public void testPollSocketsInfo() throws Exception {
-        // This test requires shims that provide API 30 access
-        assumeTrue(ConstantsShim.VERSION >= Build.VERSION_CODES.R);
-        when(mDependencies.isTcpInfoParsingSupported()).thenReturn(false);
         final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
-        assertFalse(tst.pollSocketsInfo());
 
-        when(mDependencies.isTcpInfoParsingSupported()).thenReturn(true);
         // No enough bytes remain for a valid NlMsg.
         final ByteBuffer invalidBuffer = ByteBuffer.allocate(1);
         invalidBuffer.order(ByteOrder.nativeOrder());
@@ -256,11 +247,11 @@
         final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
         // Simulate 1 message with data stall happened.
         doReturn(getByteBufferFromHexString(
-                        composeSockDiagTcpHex(4, 10) + NLMSG_DONE_HEX))
+                        composeSockDiagTcpHex(9, 10) + NLMSG_DONE_HEX))
                 .when(mDependencies).recvMessage(any());
         assertTrue(tst.pollSocketsInfo());
 
-        // ( Lost 4 + default 5 retransmits in the sample ) / 10 sent = 90 percent.
+        // 9 retrans / 10 sent = 90 percent.
         assertEquals(90, tst.getLatestPacketFailPercentage());
         assertEquals(10, tst.getSentSinceLastRecv());
         assertTrue(tst.isDataStallSuspected());
@@ -271,8 +262,8 @@
         final LinkProperties testLp = new LinkProperties();
         testLp.addDnsServer(TEST_DNS1);
         tst.setLinkProperties(testLp);
-        doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(4, 10)
-                + composeSockDiagTcpHex(5, 10, DNS_OVER_TLS_PORT, TEST_COOKIE2)
+        doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(9, 10)
+                + composeSockDiagTcpHex(9, 10, DNS_OVER_TLS_PORT, TEST_COOKIE2)
                 + NLMSG_DONE_HEX))
                 .when(mDependencies).recvMessage(any());
         assertTrue(tst.pollSocketsInfo());
@@ -289,12 +280,12 @@
         // will be counted.
         testLp.addValidatedPrivateDnsServer(TEST_DNS1);
         tst.setLinkProperties(testLp);
-        doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(5, 12)
-                + composeSockDiagTcpHex(7, 12, DNS_OVER_TLS_PORT, TEST_COOKIE2)
+        doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(10, 12)
+                + composeSockDiagTcpHex(11, 12, DNS_OVER_TLS_PORT, TEST_COOKIE2)
                 + NLMSG_DONE_HEX))
                 .when(mDependencies).recvMessage(any());
         assertTrue(tst.pollSocketsInfo());
-        // Lost ( 1 + 2 ) / ( 2 + 2 ) sent = 75 percent.
+        // Retrans ( 1 + 2 ) / ( 2 + 2 ) sent = 75 percent.
         assertEquals(75, tst.getLatestPacketFailPercentage());
         assertEquals(14, tst.getSentSinceLastRecv());
         assertFalse(tst.isDataStallSuspected());
@@ -303,40 +294,18 @@
         // counted. And the stat is correctly subtracted from the stat ignored in the previous
         // polling cycle.
         tst.setOpportunisticMode(false);
-        doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(6, 14)
-                + composeSockDiagTcpHex(9, 14, DNS_OVER_TLS_PORT, TEST_COOKIE2)
+        doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(11, 14)
+                + composeSockDiagTcpHex(13, 14, DNS_OVER_TLS_PORT, TEST_COOKIE2)
                 + NLMSG_DONE_HEX))
                 .when(mDependencies).recvMessage(any());
         assertTrue(tst.pollSocketsInfo());
-        // Lost ( 1 + 2 ) / ( 2 + 2 ) sent = 75 percent.
+        // Retrans ( 1 + 2 ) / ( 2 + 2 ) sent = 75 percent.
         assertEquals(75, tst.getLatestPacketFailPercentage());
         assertEquals(18, tst.getSentSinceLastRecv());
         assertFalse(tst.isDataStallSuspected());
     }
 
     @Test
-    public void testTcpInfoParsingUnsupported() {
-        doReturn(false).when(mDependencies).isTcpInfoParsingSupported();
-        final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
-        verify(mDependencies).getNetd();
-
-        assertFalse(tst.pollSocketsInfo());
-        assertEquals(-1, tst.getLatestPacketFailPercentage());
-        assertEquals(-1, tst.getLatestReceivedCount());
-        assertEquals(-1, tst.getSentSinceLastRecv());
-        assertFalse(tst.isDataStallSuspected());
-
-        verify(mDependencies, atLeastOnce()).isTcpInfoParsingSupported();
-        verifyNoMoreInteractions(mDependencies);
-
-        // Verify that no un-registration for the device configuration listener and broadcast
-        // receiver if TcpInfo parsing is not supported.
-        tst.quit();
-        verify(mDependencies, never()).removeDeviceConfigChangedListener(any());
-        verify(mDependencies, never()).removeBroadcastReceiver(any());
-    }
-
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
     public void testTcpInfoParsingWithMultipleMsgs() throws Exception {
         final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
 
@@ -345,20 +314,20 @@
         //
         // Mocking 6 return results for different IP families(3 for IPv6; 3 for Ipv4). Use the same
         // message for different IP families to reduce the complexity.
-        doReturn(getByteBufferFromHexString(repeat(composeSockDiagTcpHex(0, 10), 5)),
-                getByteBufferFromHexString(repeat(composeSockDiagTcpHex(0, 10), 2)),
+        doReturn(getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 10), 5)),
+                getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 10), 2)),
                 getByteBufferFromHexString(
-                        repeat(composeSockDiagTcpHex(0, 10), 2) + NLMSG_DONE_HEX),
-                getByteBufferFromHexString(repeat(composeSockDiagTcpHex(0, 10), 5)),
-                getByteBufferFromHexString(repeat(composeSockDiagTcpHex(0, 10), 2)),
+                        repeat(composeSockDiagTcpHex(5, 10), 2) + NLMSG_DONE_HEX),
+                getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 10), 5)),
+                getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 10), 2)),
                 getByteBufferFromHexString(
-                        repeat(composeSockDiagTcpHex(0, 10), 2) + NLMSG_DONE_HEX))
+                        repeat(composeSockDiagTcpHex(5, 10), 2) + NLMSG_DONE_HEX))
                 .when(mDependencies).recvMessage(any());
 
         assertTrue(tst.pollSocketsInfo());
         // Verify that code reads all the messages. (3 times for IPv4, 3 times for IPv6)
         verify(mDependencies, times(6)).recvMessage(any());
-        // Calculated from (retransmits + lost) / segsout.
+        // Calculated from totalRetrans / segsout.
         // Note that the counters cannot be verified given that the cookie of the mocked sockets
         // are the same, the latest SocketInfo would overwrite previous reported ones.
         assertEquals(50, tst.getLatestPacketFailPercentage());
@@ -371,12 +340,12 @@
         //
         // Mocking 6 return results for different IP families(3 for IPv6; 3 for Ipv4). Use the same
         // message for different IP families to reduce the complexity.
-        doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(5, 15)),
-                getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 15), 5)),
-                getByteBufferFromHexString(composeSockDiagTcpHex(5, 15) + NLMSG_DONE_HEX),
-                getByteBufferFromHexString(composeSockDiagTcpHex(5, 15)),
-                getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 15), 5)),
-                getByteBufferFromHexString(composeSockDiagTcpHex(5, 15) + NLMSG_DONE_HEX))
+        doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(10, 15)),
+                getByteBufferFromHexString(repeat(composeSockDiagTcpHex(10, 15), 5)),
+                getByteBufferFromHexString(composeSockDiagTcpHex(10, 15) + NLMSG_DONE_HEX),
+                getByteBufferFromHexString(composeSockDiagTcpHex(10, 15)),
+                getByteBufferFromHexString(repeat(composeSockDiagTcpHex(10, 15), 5)),
+                getByteBufferFromHexString(composeSockDiagTcpHex(10, 15) + NLMSG_DONE_HEX))
                 .when(mDependencies).recvMessage(any());
 
         assertTrue(tst.pollSocketsInfo());
@@ -393,12 +362,12 @@
         //
         // Mocking 4 return results for different IP families(2 for IPv6; 2 for Ipv4). Use the same
         // message for different IP families to reduce the complexity.
-        doReturn(getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 15), 5)),
-                getByteBufferFromHexString(composeSockDiagTcpHex(5, 15)),
-                getByteBufferFromHexString(composeSockDiagTcpHex(5, 15) + NLMSG_DONE_HEX),
-                getByteBufferFromHexString(repeat(composeSockDiagTcpHex(5, 15), 5)),
-                getByteBufferFromHexString(composeSockDiagTcpHex(5, 15)),
-                getByteBufferFromHexString(composeSockDiagTcpHex(5, 15) + NLMSG_DONE_HEX))
+        doReturn(getByteBufferFromHexString(repeat(composeSockDiagTcpHex(10, 15), 5)),
+                getByteBufferFromHexString(composeSockDiagTcpHex(10, 15)),
+                getByteBufferFromHexString(composeSockDiagTcpHex(10, 15) + NLMSG_DONE_HEX),
+                getByteBufferFromHexString(repeat(composeSockDiagTcpHex(10, 15), 5)),
+                getByteBufferFromHexString(composeSockDiagTcpHex(10, 15)),
+                getByteBufferFromHexString(composeSockDiagTcpHex(10, 15) + NLMSG_DONE_HEX))
                 .when(mDependencies).recvMessage(any());
 
         assertTrue(tst.pollSocketsInfo());
@@ -415,9 +384,9 @@
         // Mocking 2 return results for different IP families(1 for IPv6; 1 for Ipv4). Use the same
         // message for different IP families to reduce the complexity.
         doReturn(getByteBufferFromHexString(
-                        repeat(composeSockDiagTcpHex(9, 20), 8) + NLMSG_DONE_HEX),
+                        repeat(composeSockDiagTcpHex(14, 20), 8) + NLMSG_DONE_HEX),
                 getByteBufferFromHexString(
-                        repeat(composeSockDiagTcpHex(9, 20), 8) + NLMSG_DONE_HEX))
+                        repeat(composeSockDiagTcpHex(14, 20), 8) + NLMSG_DONE_HEX))
                 .when(mDependencies).recvMessage(any());
 
         assertTrue(tst.pollSocketsInfo());
@@ -435,9 +404,9 @@
         // Mocking 2 return results for different IP families(1 for IPv6; 1 for Ipv4). Use the same
         // message for different IP families to reduce the complexity.
         doReturn(getByteBufferFromHexString(
-                        NLMSG_DONE_HEX + repeat(composeSockDiagTcpHex(15, 26), 2)),
+                        NLMSG_DONE_HEX + repeat(composeSockDiagTcpHex(20, 26), 2)),
                 getByteBufferFromHexString(
-                        NLMSG_DONE_HEX + repeat(composeSockDiagTcpHex(15, 26), 2)))
+                        NLMSG_DONE_HEX + repeat(composeSockDiagTcpHex(20, 26), 2)))
                 .when(mDependencies).recvMessage(any());
         assertTrue(tst.pollSocketsInfo());
         // Another 1 time for IPv6 and 1 time for IPv4
@@ -480,10 +449,17 @@
         final ByteBuffer bb = ByteBuffer.allocate(size);
         bb.order(order);
         switch (size) {
-            case Short.BYTES -> bb.putShort((short) v);
-            case Integer.BYTES -> bb.putInt((int) v);
-            case Long.BYTES -> bb.putLong(v);
-            default -> throw new IllegalArgumentException("Unsupported size: " + size);
+            case Short.BYTES:
+                bb.putShort((short) v);
+                break;
+            case Integer.BYTES:
+                bb.putInt((int) v);
+                break;
+            case Long.BYTES:
+                bb.putLong(v);
+                break;
+            default:
+                throw new IllegalArgumentException("Unsupported size: " + size);
         }
         String s = "";
         for (byte b : bb.array()) {
@@ -492,100 +468,97 @@
         return s;
     }
 
-    private static String composeSockDiagTcpHex(int lost, int sent) {
-        return composeSockDiagTcpHex(lost, sent, TEST_DST_PORT, TEST_COOKIE1);
+    private static String composeSockDiagTcpHex(int retrans, int sent) {
+        return composeSockDiagTcpHex(retrans, sent, TEST_DST_PORT, TEST_COOKIE1);
     }
 
-    private static String composeSockDiagTcpHex(int lost, int sent, short dstPort, long cookie) {
+    private static String composeSockDiagTcpHex(int retrans, int sent, short dstPort, long cookie) {
         return // struct nlmsghdr.
-                "14010000" +        // length = 276
-                "1400" +            // type = SOCK_DIAG_BY_FAMILY
-                "0301" +            // flags = NLM_F_REQUEST | NLM_F_DUMP
-                "00000000" +        // seqno
-                "00000000" +        // pid (0 == kernel)
+                "14010000"          // length = 276
+                + "1400"            // type = SOCK_DIAG_BY_FAMILY
+                + "0301"            // flags = NLM_F_REQUEST | NLM_F_DUMP
+                + "00000000"        // seqno
+                + "00000000"        // pid (0 == kernel)
                 // struct inet_diag_req_v2
-                "02" +              // family = AF_INET
-                "06" +              // state
-                "00" +              // timer
-                "00" +              // retrans
+                + "02"              // family = AF_INET
+                + "06"              // state
+                + "00"              // timer
+                + "00"              // retrans
                 // inet_diag_sockid: ports and addresses are always in big endian,
                 // see StructInetDiagSockId.
-                "DEA5" +                                               // idiag_sport = 56997
-                getHexStringFromShort(dstPort, ByteOrder.BIG_ENDIAN) + // idiag_dport
-                "0a006402000000000000000000000000" +                   // idiag_src = 10.0.100.2
-                "08080808000000000000000000000000" +                   // idiag_dst = 8.8.8.8
-                "00000000" +                                           // idiag_if
-                getHexStringFromLong(cookie) +                         // idiag_cookie
-                "00000000" +                                           // idiag_expires
-                "00000000" +                                           // idiag_rqueue
-                "00000000" +                                           // idiag_wqueue
-                getHexStringFromInt(TEST_UID1) +                       // idiag_uid
-                "00000000" +                                           // idiag_inode
+                + "DEA5"                                               // idiag_sport = 56997
+                + getHexStringFromShort(dstPort, ByteOrder.BIG_ENDIAN) // idiag_dport
+                + "0a006402000000000000000000000000"                   // idiag_src = 10.0.100.2
+                + "08080808000000000000000000000000"                   // idiag_dst = 8.8.8.8
+                + "00000000"                                           // idiag_if
+                + getHexStringFromLong(cookie)                         // idiag_cookie
+                + "00000000"                                           // idiag_expires
+                + "00000000"                                           // idiag_rqueue
+                + "00000000"                                           // idiag_wqueue
+                + getHexStringFromInt(TEST_UID1)                       // idiag_uid
+                + "00000000"                                           // idiag_inode
                 // rtattr
-                "0500" +            // len = 5
-                "0800" +            // type = 8
-                "00000000" +        // data
-                "0800" +            // len = 8
-                "0F00" +            // type = 15(INET_DIAG_MARK)
-                "850A0C00" +        // data, socket mark=789125
-                "AC00" +            // len = 172
-                "0200" +            // type = 2(INET_DIAG_INFO)
+                + "0500"            // len = 5
+                + "0800"            // type = 8
+                + "00000000"        // data
+                + "0800"            // len = 8
+                + "0F00"            // type = 15(INET_DIAG_MARK)
+                + "850A0C00"        // data, socket mark=789125
+                + "AC00"            // len = 172
+                + "0200"            // type = 2(INET_DIAG_INFO)
                 // tcp_info
-                "01" +              // state = TCP_ESTABLISHED
-                "00" +              // ca_state = TCP_CA_OPEN
-                "05" +              // retransmits = 5
-                "00" +              // probes = 0
-                "00" +              // backoff = 0
-                "07" +              // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS
-                "88" +              // wscale = 8
-                "00" +              // delivery_rate_app_limited = 0
-                "4A911B00" +        // rto = 1806666
-                "00000000" +        // ato = 0
-                "2E050000" +        // sndMss = 1326
-                "18020000" +        // rcvMss = 536
-                "00000000" +        // unsacked = 0
-                "00000000" +        // acked = 0
-                getHexStringFromInt(lost) + // lost
-                "00000000" +        // retrans = 0
-                "00000000" +        // fackets = 0
-                "BB000000" +        // lastDataSent = 187
-                "00000000" +        // lastAckSent = 0
-                "BB000000" +        // lastDataRecv = 187
-                "BB000000" +        // lastDataAckRecv = 187
-                "DC050000" +        // pmtu = 1500
-                "30560100" +        // rcvSsthresh = 87600
-                "3E2C0900" +        // rttt = 601150
-                "1F960400" +        // rttvar = 300575
-                "78050000" +        // sndSsthresh = 1400
-                "0A000000" +        // sndCwnd = 10
-                "A8050000" +        // advmss = 1448
-                "03000000" +        // reordering = 3
-                "00000000" +        // rcvrtt = 0
-                "30560100" +        // rcvspace = 87600
-                "00000000" +        // totalRetrans = 0
-                "53AC000000000000" +    // pacingRate = 44115
-                "FFFFFFFFFFFFFFFF" +    // maxPacingRate = 18446744073709551615
-                "0100000000000000" +    // bytesAcked = 1
-                "0000000000000000" +    // bytesReceived = 0
-                getHexStringFromInt(sent) + // SegsOut
-                "00000000" +        // SegsIn = 0
-                "00000000" +        // NotSentBytes = 0
-                "3E2C0900" +        // minRtt = 601150
-                "00000000" +        // DataSegsIn = 0
-                "00000000" +        // DataSegsOut = 0
-                "0000000000000000"; // deliverRate = 0
+                + "01"              // state = TCP_ESTABLISHED
+                + "00"              // ca_state = TCP_CA_OPEN
+                + "05"              // retransmits = 5
+                + "00"              // probes = 0
+                + "00"              // backoff = 0
+                + "07"              // option = TCPI_OPT_WSCALE|TCPI_OPT_SACK|TCPI_OPT_TIMESTAMPS
+                + "88"              // wscale = 8
+                + "00"              // delivery_rate_app_limited = 0
+                + "4A911B00"        // rto = 1806666
+                + "00000000"        // ato = 0
+                + "2E050000"        // sndMss = 1326
+                + "18020000"        // rcvMss = 536
+                + "00000000"        // unsacked = 0
+                + "00000000"        // acked = 0
+                + "00000000"        // lost
+                + "00000000"        // retrans = 0
+                + "00000000"        // fackets = 0
+                + "BB000000"        // lastDataSent = 187
+                + "00000000"        // lastAckSent = 0
+                + "BB000000"        // lastDataRecv = 187
+                + "BB000000"        // lastDataAckRecv = 187
+                + "DC050000"        // pmtu = 1500
+                + "30560100"        // rcvSsthresh = 87600
+                + "3E2C0900"        // rttt = 601150
+                + "1F960400"        // rttvar = 300575
+                + "78050000"        // sndSsthresh = 1400
+                + "0A000000"        // sndCwnd = 10
+                + "A8050000"        // advmss = 1448
+                + "03000000"        // reordering = 3
+                + "00000000"        // rcvrtt = 0
+                + "30560100"        // rcvspace = 87600
+                + getHexStringFromInt(retrans)   // totalRetrans
+                + "53AC000000000000"    // pacingRate = 44115
+                + "FFFFFFFFFFFFFFFF"    // maxPacingRate = 18446744073709551615
+                + "0100000000000000"    // bytesAcked = 1
+                + "0000000000000000"    // bytesReceived = 0
+                + getHexStringFromInt(sent) // SegsOut
+                + "00000000"        // SegsIn = 0
+                + "00000000"        // NotSentBytes = 0
+                + "3E2C0900"        // minRtt = 601150
+                + "00000000"        // DataSegsIn = 0
+                + "00000000"        // DataSegsOut = 0
+                + "0000000000000000"; // deliverRate = 0
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testTcpInfoParsingWithDozeMode() throws Exception {
-        // This test requires shims that provide API 30 access
-        assumeTrue(ConstantsShim.VERSION >= Build.VERSION_CODES.R);
-
         final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
         final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
                 ArgumentCaptor.forClass(BroadcastReceiver.class);
 
-        verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture());
+        verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(), anyBoolean());
         setupNormalTestTcpInfo();
         assertTrue(tst.pollSocketsInfo());
 
@@ -593,69 +566,110 @@
         when(mDependencies.getDeviceConfigPropertyInt(any(), eq(CONFIG_TCP_PACKETS_FAIL_PERCENTAGE),
                 anyInt())).thenReturn(40);
 
-        // Trigger a config update
+        // Trigger a config update.
         tst.mConfigListener.onPropertiesChanged(null /* properties */);
         assertEquals(10, tst.getSentSinceLastRecv());
         assertEquals(50, tst.getLatestPacketFailPercentage());
         assertTrue(tst.isDataStallSuspected());
 
-        // Enable doze mode
+        // Enable doze mode, verify counters are not updated.
         doReturn(true).when(mPowerManager).isDeviceIdleMode();
         final BroadcastReceiver receiver = receiverCaptor.getValue();
         receiver.onReceive(mContext, new Intent(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
         assertFalse(tst.pollSocketsInfo());
+        assertEquals(10, tst.getSentSinceLastRecv());
+        assertEquals(50, tst.getLatestPacketFailPercentage());
         assertFalse(tst.isDataStallSuspected());
     }
 
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testTcpInfoDisableParsingWithLightDozeMode_enabled() throws Exception {
+        final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
+        final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+
+        // Enable light doze mode with 1 netlink message.
+        verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(), anyBoolean());
+        final BroadcastReceiver receiver = receiverCaptor.getValue();
+        doReturn(true).when(mPowerManager).isDeviceLightIdleMode();
+        receiver.onReceive(mContext, new Intent(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED));
+        doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(9, 10)
+                + NLMSG_DONE_HEX)).when(mDependencies).recvMessage(any());
+
+        // Verify counters are not updated.
+        assertFalse(tst.pollSocketsInfo());
+        assertEquals(0, tst.getSentSinceLastRecv());
+        // -1 if not enough packets.
+        assertEquals(-1, tst.getLatestPacketFailPercentage());
+        assertFalse(tst.isDataStallSuspected());
+
+        // Disable light doze mode, verify polling are processed and counters are updated.
+        doReturn(false).when(mPowerManager).isDeviceLightIdleMode();
+        receiver.onReceive(mContext, new Intent(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED));
+        assertTrue(tst.pollSocketsInfo());
+        assertEquals(10, tst.getSentSinceLastRecv());
+        // Lost 4 + default 5 retrans / 10 sent.
+        assertEquals(90, tst.getLatestPacketFailPercentage());
+        assertTrue(tst.isDataStallSuspected());
+    }
+
+    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    public void testTcpInfoDisableParsingWithLightDozeMode_disabled() throws Exception {
+        when(mDependencies.shouldDisableInLightDoze()).thenReturn(false);
+        final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
+        final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+
+        // Enable light doze mode with 1 netlink message.
+        verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(), anyBoolean());
+        final BroadcastReceiver receiver = receiverCaptor.getValue();
+        doReturn(true).when(mPowerManager).isDeviceLightIdleMode();
+        receiver.onReceive(mContext, new Intent(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED));
+        doReturn(getByteBufferFromHexString(composeSockDiagTcpHex(9, 10)
+                + NLMSG_DONE_HEX)).when(mDependencies).recvMessage(any());
+
+        // Verify TcpInfo is still processed.
+        assertTrue(tst.pollSocketsInfo());
+        assertEquals(10, tst.getSentSinceLastRecv());
+        assertEquals(90, tst.getLatestPacketFailPercentage());
+        assertTrue(tst.isDataStallSuspected());
+    }
+
     private void setupNormalTestTcpInfo() throws Exception {
         final ByteBuffer tcpBufferV6 = getByteBuffer(TEST_RESPONSE_BYTES);
         final ByteBuffer tcpBufferV4 = getByteBuffer(TEST_RESPONSE_BYTES);
         doReturn(tcpBufferV6, tcpBufferV4).when(mDependencies).recvMessage(any());
     }
 
-    @Test @IgnoreAfter(Build.VERSION_CODES.Q)
-    public void testTcpInfoParsingNotSupportedOnQ() {
-        assertFalse(new TcpSocketTracker.Dependencies(mContext)
-                .isTcpInfoParsingSupported());
-    }
-
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
-    public void testTcpInfoParsingSupportedFromR() {
-        assertTrue(new TcpSocketTracker.Dependencies(mContext)
-                .isTcpInfoParsingSupported());
-    }
-
     private static final String BAD_DIAG_MSG_HEX =
         // struct nlmsghdr.
-            "00000058" +      // length = 1476395008
-            "1400" +         // type = SOCK_DIAG_BY_FAMILY
-            "0301" +         // flags = NLM_F_REQUEST | NLM_F_DUMP
-            "00000000" +     // seqno
-            "00000000" +     // pid (0 == kernel)
+            "00000058"      // length = 1476395008
+            + "1400"         // type = SOCK_DIAG_BY_FAMILY
+            + "0301"         // flags = NLM_F_REQUEST | NLM_F_DUMP
+            + "00000000"     // seqno
+            + "00000000"     // pid (0 == kernel)
             // struct inet_diag_req_v2
-            "02" +           // family = AF_INET
-            "06" +           // state
-            "00" +           // timer
-            "00" +           // retrans
+            + "02"           // family = AF_INET
+            + "06"           // state
+            + "00"           // timer
+            + "00"           // retrans
             // inet_diag_sockid
-            "DEA5" +         // idiag_sport = 42462
-            "71B9" +         // idiag_dport = 47473
-            "0a006402000000000000000000000000" + // idiag_src = 10.0.100.2
-            "08080808000000000000000000000000" + // idiag_dst = 8.8.8.8
-            "00000000" +    // idiag_if
-            "34ED000076270000" + // idiag_cookie = 43387759684916
-            "00000000" +    // idiag_expires
-            "00000000" +    // idiag_rqueue
-            "00000000" +    // idiag_wqueue
-            "00000000" +    // idiag_uid
-            "00000000";    // idiag_inode
+            + "DEA5"         // idiag_sport = 42462
+            + "71B9"         // idiag_dport = 47473
+            + "0a006402000000000000000000000000" // idiag_src = 10.0.100.2
+            + "08080808000000000000000000000000" // idiag_dst = 8.8.8.8
+            + "00000000"    // idiag_if
+            + "34ED000076270000" // idiag_cookie = 43387759684916
+            + "00000000"    // idiag_expires
+            + "00000000"    // idiag_rqueue
+            + "00000000"    // idiag_wqueue
+            + "00000000"    // idiag_uid
+            + "00000000";    // idiag_inode
     private static final byte[] BAD_SOCK_DIAG_MSG_BYTES =
         HexEncoding.decode(BAD_DIAG_MSG_HEX.toCharArray(), false);
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q) // TCP info parsing is not supported on Q
+    @Test
     public void testPollSocketsInfo_BadFormat() throws Exception {
-        // This test requires shims that provide API 30 access
-        assumeTrue(ConstantsShim.VERSION >= Build.VERSION_CODES.R);
         final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
         setupNormalTestTcpInfo();
         assertTrue(tst.pollSocketsInfo());
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
index cd948b3..3f2ff31 100644
--- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -553,6 +553,7 @@
         }).when(mCleartextDnsNetwork).openConnection(any());
         initHttpConnection(mHttpConnection);
         initHttpConnection(mHttpsConnection);
+        initHttpConnection(mFallbackConnection);
 
         mFakeDns = new FakeDns();
         mFakeDns.startMocking();
@@ -603,10 +604,6 @@
                 0, mCreatedNetworkMonitors.size());
         assertEquals("BroadcastReceiver still registered after disconnect",
                 0, mRegisteredReceivers.size());
-        if (mTstDependencies.isTcpInfoParsingSupported()) {
-            verify(mTstDependencies, times(networkMonitors.length))
-                    .removeDeviceConfigChangedListener(any());
-        }
     }
 
     private void initHttpConnection(HttpURLConnection connection) {
@@ -614,11 +611,13 @@
         // Explicitly set the HttpURLConnection methods so that these will not interact with real
         // methods to prevent threading issue in the test.
         doReturn(new HashMap<>()).when(connection).getHeaderFields();
+        doReturn(null).when(connection).getHeaderField(eq("location"));
         doNothing().when(connection).setInstanceFollowRedirects(anyBoolean());
         doNothing().when(connection).setConnectTimeout(anyInt());
         doNothing().when(connection).setReadTimeout(anyInt());
         doNothing().when(connection).setRequestProperty(any(), any());
         doNothing().when(connection).setUseCaches(anyBoolean());
+        doNothing().when(connection).disconnect();
     }
 
     private void initCallbacks(int interfaceVersion) throws Exception {
@@ -707,7 +706,6 @@
         setNetworkCapabilities(nm, nc);
         HandlerUtils.waitForIdle(nm.getHandler(), HANDLER_TIMEOUT_MS);
         mCreatedNetworkMonitors.add(nm);
-        doReturn(false).when(mTstDependencies).isTcpInfoParsingSupported();
 
         return nm;
     }
@@ -1584,7 +1582,7 @@
         HandlerUtils.waitForIdle(nm.getHandler(), HANDLER_TIMEOUT_MS);
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     public void testIsCaptivePortal_CapportApiNotSupported() throws Exception {
         // Test that on a R+ device, if NetworkStack was compiled without CaptivePortalData support
         // (built against Q), NetworkMonitor behaves as expected.
@@ -1859,7 +1857,6 @@
     public void testIsDataStall_SkipEvaluateOnValidationNotRequiredNetwork() {
         // Make DNS and TCP stall condition satisfied.
         setDataStallEvaluationType(DATA_STALL_EVALUATION_TYPE_DNS | DATA_STALL_EVALUATION_TYPE_TCP);
-        doReturn(true).when(mTstDependencies).isTcpInfoParsingSupported();
         doReturn(0).when(mTst).getLatestReceivedCount();
         doReturn(true).when(mTst).isDataStallSuspected();
         final WrappedNetworkMonitor nm = makeMonitor(CELL_NO_INTERNET_CAPABILITIES);
@@ -1894,7 +1891,6 @@
 
     @Test
     public void testIsDataStall_EvaluationTcp() throws Exception {
-        doReturn(true).when(mTstDependencies).isTcpInfoParsingSupported();
         // Evaluate TCP only. Expect ignoring DNS signal.
         setDataStallEvaluationType(DATA_STALL_EVALUATION_TYPE_TCP);
         WrappedNetworkMonitor wrappedMonitor = makeMonitor(CELL_METERED_CAPABILITIES);
@@ -2296,7 +2292,6 @@
     @Test
     public void testDataStall_setOpportunisticMode() {
         setDataStallEvaluationType(DATA_STALL_EVALUATION_TYPE_TCP);
-        doReturn(true).when(mTstDependencies).isTcpInfoParsingSupported();
         WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor();
         InOrder inOrder = inOrder(mTst);
         // Initialized with default value.
@@ -2402,7 +2397,6 @@
     }
 
     private void setupTcpDataStall() {
-        doReturn(true).when(mTstDependencies).isTcpInfoParsingSupported();
         doReturn(0).when(mTst).getLatestReceivedCount();
         doReturn(TEST_TCP_FAIL_RATE).when(mTst).getLatestPacketFailPercentage();
         doReturn(TEST_TCP_PACKET_COUNT).when(mTst).getSentSinceLastRecv();