Snap for 12053972 from 70a9b5806930481969c3ead42d26113bce0372b8 to mainline-media-release

Change-Id: If5bd9f1d30635d7211f570093e853af4668019e2
diff --git a/Android.bp b/Android.bp
index 3712ba6..ab24060 100644
--- a/Android.bp
+++ b/Android.bp
@@ -69,6 +69,7 @@
         "framework-connectivity",
         "framework-connectivity-t",
         "framework-statsd",
+        "framework-tethering",
         "framework-wifi",
     ],
 }
@@ -90,6 +91,7 @@
         "framework-connectivity.stubs.module_lib",
         "framework-connectivity-t",
         "framework-statsd",
+        "framework-tethering",
         "framework-wifi",
     ],
 }
diff --git a/src/android/net/apf/AndroidPacketFilter.java b/src/android/net/apf/AndroidPacketFilter.java
index 720de03..6dd4fad 100644
--- a/src/android/net/apf/AndroidPacketFilter.java
+++ b/src/android/net/apf/AndroidPacketFilter.java
@@ -106,4 +106,11 @@
     default boolean hasDataAccess(int apfVersionSupported) {
         return apfVersionSupported > 2;
     }
+
+    /**
+     * Whether the ApfFilter supports generating ND offload code.
+     */
+    default boolean supportNdOffload() {
+        return false;
+    }
 }
diff --git a/src/android/net/apf/ApfCounterTracker.java b/src/android/net/apf/ApfCounterTracker.java
index f5416c5..6c5b566 100644
--- a/src/android/net/apf/ApfCounterTracker.java
+++ b/src/android/net/apf/ApfCounterTracker.java
@@ -50,6 +50,7 @@
         APF_VERSION,
         APF_PROGRAM_ID,
         // TODO: removing PASSED_ARP after remove LegacyApfFilter.java
+        // The counter sequence should keep the same as ApfSessionInfoMetrics.java
         PASSED_ARP,  // see also MIN_PASS_COUNTER below.
         PASSED_ARP_BROADCAST_REPLY,
         // TODO: removing PASSED_ARP_NON_IPV4 after remove LegacyApfFilter.java
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
index 92c25e7..f6c6ac7 100644
--- a/src/android/net/apf/ApfFilter.java
+++ b/src/android/net/apf/ApfFilter.java
@@ -2250,8 +2250,7 @@
     protected ApfV4GeneratorBase<?> emitPrologueLocked() throws IllegalInstructionException {
         // This is guaranteed to succeed because of the check in maybeCreate.
         ApfV4GeneratorBase<?> gen;
-        if (SdkLevel.isAtLeastV()
-                && ApfV6Generator.supportsVersion(mApfVersionSupported)) {
+        if (shouldUseApfV6Generator()) {
             gen = new ApfV6Generator(mApfVersionSupported, mApfRamSize,
                     mInstallableProgramSizeClamp);
         } else {
@@ -2688,6 +2687,15 @@
         installNewProgramLocked();
     }
 
+    @Override
+    public boolean supportNdOffload() {
+        return shouldUseApfV6Generator() && mShouldHandleNdOffload;
+    }
+
+    private boolean shouldUseApfV6Generator() {
+        return SdkLevel.isAtLeastV() && ApfV6Generator.supportsVersion(mApfVersionSupported);
+    }
+
     /**
      * Add TCP keepalive ack packet filter.
      * This will add a filter to drop acks to the keepalive packet passed as an argument.
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index 7b9cd73..223ed75 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -1023,10 +1023,15 @@
                     public void onClatInterfaceStateUpdate(boolean add) {
                         getHandler().post(() -> {
                             if (mHasSeenClatInterface == add) return;
-                            // Clat interface information is spliced into LinkProperties by
-                            // ConnectivityService, so it cannot be added to the LinkProperties
-                            // here as those propagate back to ConnectivityService.
-                            mCallback.setNeighborDiscoveryOffload(add ? false : true);
+                            // If Apf is not supported or Apf doesn't support ND offload, then
+                            // configure the vendor ND offload feature based on the Clat
+                            // interface state.
+                            if (mApfFilter == null || !mApfFilter.supportNdOffload()) {
+                                // Clat interface information is spliced into LinkProperties by
+                                // ConnectivityService, so it cannot be added to the LinkProperties
+                                // here as those propagate back to ConnectivityService.
+                                mCallback.setNeighborDiscoveryOffload(add ? false : true);
+                            }
                             mHasSeenClatInterface = add;
                             if (mApfFilter != null) {
                                 mApfFilter.updateClatInterfaceState(add);
@@ -3104,6 +3109,10 @@
             // at the beginning.
             mHasSeenClatInterface = false;
             mApfFilter = maybeCreateApfFilter(mCurrentApfCapabilities);
+            // If Apf supports ND offload, then turn off the vendor ND offload feature.
+            if (mApfFilter != null && mApfFilter.supportNdOffload()) {
+                mCallback.setNeighborDiscoveryOffload(false);
+            }
             // TODO: investigate the effects of any multicast filtering racing/interfering with the
             // rest of this IP configuration startup.
             if (mApfFilter == null) {
@@ -3561,6 +3570,10 @@
                     final ApfCapabilities apfCapabilities = (ApfCapabilities) msg.obj;
                     if (handleUpdateApfCapabilities(apfCapabilities)) {
                         mApfFilter = maybeCreateApfFilter(apfCapabilities);
+                        // If Apf supports ND offload, then turn off the vendor ND offload feature.
+                        if (mApfFilter != null && mApfFilter.supportNdOffload()) {
+                            mCallback.setNeighborDiscoveryOffload(false);
+                        }
                     }
                     break;
 
diff --git a/src/android/net/util/RawSocketUtils.java b/src/android/net/util/RawSocketUtils.java
new file mode 100644
index 0000000..a6c8a40
--- /dev/null
+++ b/src/android/net/util/RawSocketUtils.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.util;
+
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.system.OsConstants.AF_PACKET;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+import static android.system.OsConstants.SOCK_RAW;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_DST_ADDR_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_LENGTH;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_OFFSET;
+
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.net.TetheringManager;
+import android.system.Os;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.util.HexDump;
+
+import java.io.FileDescriptor;
+import java.net.NetworkInterface;
+import java.net.SocketAddress;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class RawSocketUtils {
+    // For isTetheredInterface, a quick Tethering event callback is expected
+    // since there's no start/stop Tethering involved. This timeout allows
+    // system messages to be handled, preventing flaky test results.
+    private static final int TETHERING_EVENT_CALLBACK_TIMEOUT_MS = 3000;
+
+    /**
+     * Send a raw packet represents in Hex format to the downstream interface.
+     * <p>
+     * Note that the target interface is limited to tethering downstream
+     * for security considerations.
+     */
+    @RequiresPermission(NETWORK_SETTINGS)
+    public static void sendRawPacketDownStream(@NonNull Context context, @NonNull String ifaceName,
+                                     @NonNull String packetInHex) throws Exception {
+        // 1. Verify Tethering Downstream Interface.
+        enforceTetheredInterface(context, ifaceName);
+
+        // 2. Hex to Byte Array Conversion
+        final byte[] packetData = HexDump.hexStringToByteArray(packetInHex);
+        final byte[] destMac = Arrays.copyOfRange(packetData, ETHER_DST_ADDR_OFFSET,
+                ETHER_DST_ADDR_OFFSET + ETHER_ADDR_LEN);
+        final byte[] etherTypeBytes = Arrays.copyOfRange(packetData, ETHER_TYPE_OFFSET,
+                ETHER_TYPE_OFFSET + ETHER_TYPE_LENGTH);
+        final int etherType = ((etherTypeBytes[0] & 0xFF) << 8) | (etherTypeBytes[1] & 0xFF);
+
+        // 3. Obtain Network Interface
+        final NetworkInterface iface = NetworkInterface.getByName(ifaceName);
+        if (iface == null) {
+            throw new IllegalArgumentException("Invalid network interface: " + ifaceName);
+        }
+
+        // 4. Construct and Send Packet.
+        final SocketAddress addr = SocketUtils.makePacketSocketAddress(
+                etherType,
+                iface.getIndex(),
+                destMac
+        );
+        final FileDescriptor sock = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0);
+        try {
+            Os.sendto(sock, packetData, 0, packetData.length, 0, addr);
+        } finally {
+            SocketUtils.closeSocket(sock);
+        }
+    }
+
+    @RequiresPermission(NETWORK_SETTINGS)
+    private static void enforceTetheredInterface(@NonNull Context context,
+                                               @NonNull String interfaceName)
+            throws ExecutionException, InterruptedException, TimeoutException {
+        final TetheringManager tm = context.getSystemService(TetheringManager.class);
+        final CompletableFuture<List<String>> tetheredInterfaces = new CompletableFuture<>();
+        final TetheringManager.TetheringEventCallback callback =
+                new TetheringManager.TetheringEventCallback() {
+                    @Override
+                    public void onTetheredInterfacesChanged(@NonNull List<String> interfaces) {
+                        tetheredInterfaces.complete(interfaces);
+                    }
+                };
+        tm.registerTetheringEventCallback(c -> c.run() /* executor */, callback);
+        final List<String> tetheredIfaces = tetheredInterfaces.get(
+                TETHERING_EVENT_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        if (!tetheredIfaces.contains(interfaceName)) {
+            throw new SecurityException("Only tethered interfaces " + tetheredIfaces
+                    + " are expected, but got " + interfaceName);
+        }
+    }
+}
diff --git a/src/com/android/networkstack/metrics/ApfSessionInfoMetrics.java b/src/com/android/networkstack/metrics/ApfSessionInfoMetrics.java
index d1776c9..c2c51f6 100644
--- a/src/com/android/networkstack/metrics/ApfSessionInfoMetrics.java
+++ b/src/com/android/networkstack/metrics/ApfSessionInfoMetrics.java
@@ -16,11 +16,115 @@
 
 package com.android.networkstack.metrics;
 
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_802_3_FRAME;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_NON_IPV4;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_OTHER_HOST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_REPLY_SPA_NO_HOST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_REQUEST_ANYHOST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_REQUEST_REPLIED;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_UNKNOWN;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_V6_ONLY;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ETHERTYPE_NOT_ALLOWED;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ETH_BROADCAST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_GARP_REPLY;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_BROADCAST_ADDR;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_BROADCAST_NET;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_KEEPALIVE_ACK;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_L2_BROADCAST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_MULTICAST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_NATT_KEEPALIVE;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_NON_DHCP4;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_TCP_PORT7_UNICAST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_KEEPALIVE_ACK;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MULTICAST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MULTICAST_NA;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MULTICAST_PING;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NON_ICMP_MULTICAST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_INVALID;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_OTHER_HOST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_REPLIED_NON_DAD;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_ROUTER_SOLICITATION;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_MDNS;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_RA;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_ARP;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_BROADCAST_REPLY;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_NON_IPV4;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_REQUEST;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_UNICAST_REPLY;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_UNKNOWN;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_DHCP;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV4;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV4_FROM_DHCPV4_SERVER;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV4_UNICAST;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_ICMP;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NON_ICMP;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_DAD;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_ADDRESS;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_SLLA_OPTION;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_TENTATIVE;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_UNICAST_NON_ICMP;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_MDNS;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_MLD;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_NON_IP_UNICAST;
+import static android.net.apf.ApfCounterTracker.Counter.TOTAL_PACKETS;
+import static android.stats.connectivity.CounterName.CN_DROPPED_802_3_FRAME;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ARP_NON_IPV4;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ARP_OTHER_HOST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ARP_REPLY_SPA_NO_HOST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ARP_REQUEST_ANYHOST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ARP_REQUEST_REPLIED;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ARP_UNKNOWN;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ARP_V6_ONLY;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ETHERTYPE_NOT_ALLOWED;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ETH_BROADCAST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_GARP_REPLY;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV4_BROADCAST_ADDR;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV4_BROADCAST_NET;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV4_KEEPALIVE_ACK;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV4_L2_BROADCAST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV4_MULTICAST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV4_NATT_KEEPALIVE;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV4_NON_DHCP4;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_KEEPALIVE_ACK;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_MULTICAST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_MULTICAST_NA;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_MULTICAST_PING;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_NON_ICMP_MULTICAST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_NS_INVALID;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_NS_OTHER_HOST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_NS_REPLIED_NON_DAD;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_ROUTER_SOLICITATION;
+import static android.stats.connectivity.CounterName.CN_DROPPED_MDNS;
+import static android.stats.connectivity.CounterName.CN_DROPPED_RA;
+import static android.stats.connectivity.CounterName.CN_PASSED_ARP;
+import static android.stats.connectivity.CounterName.CN_PASSED_ARP_BROADCAST_REPLY;
+import static android.stats.connectivity.CounterName.CN_PASSED_ARP_REQUEST;
+import static android.stats.connectivity.CounterName.CN_PASSED_ARP_UNICAST_REPLY;
+import static android.stats.connectivity.CounterName.CN_PASSED_DHCP;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV4;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV4_FROM_DHCPV4_SERVER;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV4_UNICAST;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV6_ICMP;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV6_NON_ICMP;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV6_NS_DAD;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV6_NS_NO_ADDRESS;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV6_NS_NO_SLLA_OPTION;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV6_NS_TENTATIVE;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV6_UNICAST_NON_ICMP;
+import static android.stats.connectivity.CounterName.CN_PASSED_MDNS;
+import static android.stats.connectivity.CounterName.CN_PASSED_MLD;
+import static android.stats.connectivity.CounterName.CN_PASSED_NON_IP_UNICAST;
+import static android.stats.connectivity.CounterName.CN_TOTAL_PACKETS;
+import static android.stats.connectivity.CounterName.CN_UNKNOWN;
+
 import android.net.apf.ApfCounterTracker.Counter;
 import android.stats.connectivity.CounterName;
 
 import androidx.annotation.VisibleForTesting;
 
+import java.util.EnumMap;
+import java.util.Map;
+
 /**
  * Class to record the network stack ApfSessionInfo metrics into statsd.
  *
@@ -31,6 +135,65 @@
 public class ApfSessionInfoMetrics {
     // Define the maximum size of the counter list
     public static final int MAX_NUM_OF_COUNTERS = Counter.class.getEnumConstants().length - 1;
+    private static final EnumMap<Counter, CounterName> apfCounterMetricsMap = new EnumMap<>(
+            Map.ofEntries(
+                Map.entry(TOTAL_PACKETS, CN_TOTAL_PACKETS),
+                // The counter sequence should be keep the same in ApfCounterTracker.java
+                Map.entry(PASSED_ARP, CN_PASSED_ARP),
+                Map.entry(PASSED_ARP_BROADCAST_REPLY, CN_PASSED_ARP_BROADCAST_REPLY),
+                // deprecated in ApfFilter, PASSED_ARP_NON_IPV4 ==> DROPPED_ARP_NON_IPV4
+                Map.entry(PASSED_ARP_NON_IPV4, CN_UNKNOWN),
+                Map.entry(PASSED_ARP_REQUEST, CN_PASSED_ARP_REQUEST),
+                Map.entry(PASSED_ARP_UNICAST_REPLY, CN_PASSED_ARP_UNICAST_REPLY),
+                // deprecated in ApfFilter, PASSED_ARP_UNKNOWN  ==> DROPPED_ARP_UNKNOWN
+                Map.entry(PASSED_ARP_UNKNOWN, CN_UNKNOWN),
+                Map.entry(PASSED_DHCP, CN_PASSED_DHCP),
+                Map.entry(PASSED_IPV4, CN_PASSED_IPV4),
+                Map.entry(PASSED_IPV4_FROM_DHCPV4_SERVER, CN_PASSED_IPV4_FROM_DHCPV4_SERVER),
+                Map.entry(PASSED_IPV4_UNICAST, CN_PASSED_IPV4_UNICAST),
+                Map.entry(PASSED_IPV6_ICMP, CN_PASSED_IPV6_ICMP),
+                Map.entry(PASSED_IPV6_NON_ICMP, CN_PASSED_IPV6_NON_ICMP),
+                Map.entry(PASSED_IPV6_NS_DAD, CN_PASSED_IPV6_NS_DAD),
+                Map.entry(PASSED_IPV6_NS_NO_ADDRESS, CN_PASSED_IPV6_NS_NO_ADDRESS),
+                Map.entry(PASSED_IPV6_NS_NO_SLLA_OPTION, CN_PASSED_IPV6_NS_NO_SLLA_OPTION),
+                Map.entry(PASSED_IPV6_NS_TENTATIVE, CN_PASSED_IPV6_NS_TENTATIVE),
+                Map.entry(PASSED_IPV6_UNICAST_NON_ICMP, CN_PASSED_IPV6_UNICAST_NON_ICMP),
+                Map.entry(PASSED_NON_IP_UNICAST, CN_PASSED_NON_IP_UNICAST),
+                Map.entry(PASSED_MDNS, CN_PASSED_MDNS),
+                Map.entry(PASSED_MLD, CN_PASSED_MLD),
+                Map.entry(DROPPED_ETH_BROADCAST, CN_DROPPED_ETH_BROADCAST),
+                Map.entry(DROPPED_RA, CN_DROPPED_RA),
+                Map.entry(DROPPED_IPV4_L2_BROADCAST, CN_DROPPED_IPV4_L2_BROADCAST),
+                Map.entry(DROPPED_IPV4_BROADCAST_ADDR, CN_DROPPED_IPV4_BROADCAST_ADDR),
+                Map.entry(DROPPED_IPV4_BROADCAST_NET, CN_DROPPED_IPV4_BROADCAST_NET),
+                Map.entry(DROPPED_IPV4_MULTICAST, CN_DROPPED_IPV4_MULTICAST),
+                Map.entry(DROPPED_IPV4_NON_DHCP4, CN_DROPPED_IPV4_NON_DHCP4),
+                Map.entry(DROPPED_IPV6_ROUTER_SOLICITATION, CN_DROPPED_IPV6_ROUTER_SOLICITATION),
+                Map.entry(DROPPED_IPV6_MULTICAST_NA, CN_DROPPED_IPV6_MULTICAST_NA),
+                Map.entry(DROPPED_IPV6_MULTICAST, CN_DROPPED_IPV6_MULTICAST),
+                Map.entry(DROPPED_IPV6_MULTICAST_PING, CN_DROPPED_IPV6_MULTICAST_PING),
+                Map.entry(DROPPED_IPV6_NON_ICMP_MULTICAST, CN_DROPPED_IPV6_NON_ICMP_MULTICAST),
+                Map.entry(DROPPED_IPV6_NS_INVALID, CN_DROPPED_IPV6_NS_INVALID),
+                Map.entry(DROPPED_IPV6_NS_OTHER_HOST, CN_DROPPED_IPV6_NS_OTHER_HOST),
+                Map.entry(DROPPED_IPV6_NS_REPLIED_NON_DAD, CN_DROPPED_IPV6_NS_REPLIED_NON_DAD),
+                Map.entry(DROPPED_802_3_FRAME, CN_DROPPED_802_3_FRAME),
+                Map.entry(DROPPED_ETHERTYPE_NOT_ALLOWED, CN_DROPPED_ETHERTYPE_NOT_ALLOWED),
+                Map.entry(DROPPED_IPV4_KEEPALIVE_ACK, CN_DROPPED_IPV4_KEEPALIVE_ACK),
+                Map.entry(DROPPED_IPV6_KEEPALIVE_ACK, CN_DROPPED_IPV6_KEEPALIVE_ACK),
+                Map.entry(DROPPED_IPV4_NATT_KEEPALIVE, CN_DROPPED_IPV4_NATT_KEEPALIVE),
+                Map.entry(DROPPED_MDNS, CN_DROPPED_MDNS),
+                // TODO: Not supported yet in the metrics backend.
+                Map.entry(DROPPED_IPV4_TCP_PORT7_UNICAST, CN_UNKNOWN),
+                Map.entry(DROPPED_ARP_NON_IPV4, CN_DROPPED_ARP_NON_IPV4),
+                Map.entry(DROPPED_ARP_OTHER_HOST, CN_DROPPED_ARP_OTHER_HOST),
+                Map.entry(DROPPED_ARP_REPLY_SPA_NO_HOST, CN_DROPPED_ARP_REPLY_SPA_NO_HOST),
+                Map.entry(DROPPED_ARP_REQUEST_ANYHOST, CN_DROPPED_ARP_REQUEST_ANYHOST),
+                Map.entry(DROPPED_ARP_REQUEST_REPLIED, CN_DROPPED_ARP_REQUEST_REPLIED),
+                Map.entry(DROPPED_ARP_UNKNOWN, CN_DROPPED_ARP_UNKNOWN),
+                Map.entry(DROPPED_ARP_V6_ONLY, CN_DROPPED_ARP_V6_ONLY),
+                Map.entry(DROPPED_GARP_REPLY, CN_DROPPED_GARP_REPLY)
+            )
+    );
     private final ApfSessionInfoReported.Builder mStatsBuilder =
             ApfSessionInfoReported.newBuilder();
     private final ApfCounterList.Builder mApfCounterListBuilder = ApfCounterList.newBuilder();
@@ -104,85 +267,6 @@
      */
     @VisibleForTesting
     public static CounterName apfFilterCounterToEnum(final Counter counter) {
-        switch(counter) {
-            case TOTAL_PACKETS:
-                return CounterName.CN_TOTAL_PACKETS;
-            case PASSED_ARP:
-                return CounterName.CN_PASSED_ARP;
-            case PASSED_DHCP:
-                return CounterName.CN_PASSED_DHCP;
-            case PASSED_IPV4:
-                return CounterName.CN_PASSED_IPV4;
-            case PASSED_IPV6_NON_ICMP:
-                return CounterName.CN_PASSED_IPV6_NON_ICMP;
-            case PASSED_IPV4_UNICAST:
-                return CounterName.CN_PASSED_IPV4_UNICAST;
-            case PASSED_IPV6_ICMP:
-                return CounterName.CN_PASSED_IPV6_ICMP;
-            case PASSED_IPV6_UNICAST_NON_ICMP:
-                return CounterName.CN_PASSED_IPV6_UNICAST_NON_ICMP;
-            // PASSED_ARP_NON_IPV4 and PASSED_ARP_UNKNOWN were deprecated in ApfFilter:
-            //     PASSED_ARP_NON_IPV4 ==> DROPPED_ARP_NON_IPV4
-            //     PASSED_ARP_UNKNOWN  ==> DROPPED_ARP_UNKNOWN
-            // They are not supported in the metrics.
-            case PASSED_ARP_NON_IPV4:
-            case PASSED_ARP_UNKNOWN:
-                return CounterName.CN_UNKNOWN;
-            case PASSED_ARP_UNICAST_REPLY:
-                return CounterName.CN_PASSED_ARP_UNICAST_REPLY;
-            case PASSED_NON_IP_UNICAST:
-                return CounterName.CN_PASSED_NON_IP_UNICAST;
-            case PASSED_MDNS:
-                return CounterName.CN_PASSED_MDNS;
-            case DROPPED_ETH_BROADCAST:
-                return CounterName.CN_DROPPED_ETH_BROADCAST;
-            case DROPPED_RA:
-                return CounterName.CN_DROPPED_RA;
-            case DROPPED_GARP_REPLY:
-                return CounterName.CN_DROPPED_GARP_REPLY;
-            case DROPPED_ARP_OTHER_HOST:
-                return CounterName.CN_DROPPED_ARP_OTHER_HOST;
-            case DROPPED_IPV4_L2_BROADCAST:
-                return CounterName.CN_DROPPED_IPV4_L2_BROADCAST;
-            case DROPPED_IPV4_BROADCAST_ADDR:
-                return CounterName.CN_DROPPED_IPV4_BROADCAST_ADDR;
-            case DROPPED_IPV4_BROADCAST_NET:
-                return CounterName.CN_DROPPED_IPV4_BROADCAST_NET;
-            case DROPPED_IPV4_MULTICAST:
-                return CounterName.CN_DROPPED_IPV4_MULTICAST;
-            case DROPPED_IPV6_ROUTER_SOLICITATION:
-                return CounterName.CN_DROPPED_IPV6_ROUTER_SOLICITATION;
-            case DROPPED_IPV6_MULTICAST_NA:
-                return CounterName.CN_DROPPED_IPV6_MULTICAST_NA;
-            case DROPPED_IPV6_MULTICAST:
-                return CounterName.CN_DROPPED_IPV6_MULTICAST;
-            case DROPPED_IPV6_MULTICAST_PING:
-                return CounterName.CN_DROPPED_IPV6_MULTICAST_PING;
-            case DROPPED_IPV6_NON_ICMP_MULTICAST:
-                return CounterName.CN_DROPPED_IPV6_NON_ICMP_MULTICAST;
-            case DROPPED_802_3_FRAME:
-                return CounterName.CN_DROPPED_802_3_FRAME;
-            case DROPPED_ETHERTYPE_NOT_ALLOWED:
-                return CounterName.CN_DROPPED_ETHERTYPE_DENYLISTED;
-            case DROPPED_ARP_REPLY_SPA_NO_HOST:
-                return CounterName.CN_DROPPED_ARP_REPLY_SPA_NO_HOST;
-            case DROPPED_IPV4_KEEPALIVE_ACK:
-                return CounterName.CN_DROPPED_IPV4_KEEPALIVE_ACK;
-            case DROPPED_IPV6_KEEPALIVE_ACK:
-                return CounterName.CN_DROPPED_IPV6_KEEPALIVE_ACK;
-            case DROPPED_IPV4_NATT_KEEPALIVE:
-                return CounterName.CN_DROPPED_IPV4_NATT_KEEPALIVE;
-            case DROPPED_MDNS:
-                return CounterName.CN_DROPPED_MDNS;
-            case DROPPED_IPV4_TCP_PORT7_UNICAST:
-                // TODO: Not supported yet in the metrics backend.
-                return CounterName.CN_UNKNOWN;
-            case DROPPED_ARP_NON_IPV4:
-                return CounterName.CN_DROPPED_ARP_NON_IPV4;
-            case DROPPED_ARP_UNKNOWN:
-                return CounterName.CN_DROPPED_ARP_UNKNOWN;
-            default:
-                return CounterName.CN_UNKNOWN;
-        }
+        return apfCounterMetricsMap.getOrDefault(counter, CN_UNKNOWN);
     }
 }
diff --git a/src/com/android/networkstack/util/NetworkStackUtils.java b/src/com/android/networkstack/util/NetworkStackUtils.java
index a7563f4..33a8308 100755
--- a/src/com/android/networkstack/util/NetworkStackUtils.java
+++ b/src/com/android/networkstack/util/NetworkStackUtils.java
@@ -274,6 +274,11 @@
     public static final String IPCLIENT_DHCPV6_PD_PREFERRED_FLAG_VERSION =
             "ipclient_dhcpv6_pd_preferred_flag_version";
 
+    /**
+     * Experiment flag to enable Discovery of Designated Resolvers (DDR).
+     */
+    public static final String DNS_DDR_VERSION = "dns_ddr_version";
+
     /**** BEGIN Feature Kill Switch Flags ****/
 
     /**
diff --git a/src/com/android/server/NetworkStackService.java b/src/com/android/server/NetworkStackService.java
index aa8f3fa..4d10c3b 100644
--- a/src/com/android/server/NetworkStackService.java
+++ b/src/com/android/server/NetworkStackService.java
@@ -19,6 +19,7 @@
 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.net.util.RawSocketUtils.sendRawPacketDownStream;
 
 import static com.android.net.module.util.DeviceConfigUtils.getResBooleanConfig;
 import static com.android.net.module.util.FeatureVersions.FEATURE_IS_UID_NETWORKING_BLOCKED;
@@ -549,6 +550,23 @@
                                 mContext.getSystemService(ConnectivityManager.class);
                         pw.println(cm.isUidNetworkingBlocked(uid, metered /* isNetworkMetered */));
                         return 0;
+                    case "send-raw-packet-downstream": {
+                        // Usage : cmd network_stack send-raw-packet-downstream
+                        //         <interface> <packet-in-hex>
+                        // If no argument, get and display the usage help.
+                        if (getRemainingArgsCount() != 2) {
+                            onHelp();
+                            throw new IllegalArgumentException("Incorrect number of arguments");
+                        }
+                        final String iface = getNextArg();
+                        final String packetInHex = getNextArg();
+                        try {
+                            sendRawPacketDownStream(mContext, iface, packetInHex);
+                        } catch (Exception e) {
+                            throw new RuntimeException(e);
+                        }
+                        return 0;
+                    }
                     case "apf":
                         // Usage: cmd network_stack apf <iface> <cmd>
                         final String iface = getNextArg();
@@ -585,6 +603,12 @@
                 pw.println("    Get whether the networking is blocked for given uid and metered.");
                 pw.println("    <uid>: The target uid.");
                 pw.println("    <metered>: [true|false], Whether the target network is metered.");
+                pw.println("  send-raw-packet-downstream <interface> <packet-in-hex>");
+                pw.println("    Send raw packet for testing purpose.");
+                pw.println("    <interface>: Target interface name, note that this is limited");
+                pw.println("      to tethering downstream for security considerations.");
+                pw.println("    <packet_in_hex>: A valid hexadecimal representation of ");
+                pw.println("      a packet starting from L2 header.");
                 pw.println("  apf <iface> <cmd>");
                 pw.println("    APF utility commands for integration tests.");
                 pw.println("    <iface>: the network interface the provided command operates on.");
diff --git a/src/com/android/server/connectivity/DdrTracker.java b/src/com/android/server/connectivity/DdrTracker.java
new file mode 100644
index 0000000..af86914
--- /dev/null
+++ b/src/com/android/server/connectivity/DdrTracker.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static com.android.net.module.util.ConnectivitySettingsUtils.PRIVATE_DNS_MODE_OFF;
+import static com.android.net.module.util.ConnectivitySettingsUtils.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+import static com.android.net.module.util.ConnectivitySettingsUtils.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.net.shared.PrivateDnsConfig;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A class to perform DDR on a given network (to be implemented).
+ *
+ */
+class DdrTracker {
+    @IntDef(prefix = { "PRIVATE_DNS_MODE_" }, value = {
+        PRIVATE_DNS_MODE_OFF,
+        PRIVATE_DNS_MODE_OPPORTUNISTIC,
+        PRIVATE_DNS_MODE_PROVIDER_HOSTNAME
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    private @interface PrivateDnsMode {}
+
+    // Stores the DNS information that is synced with current DNS configuration.
+    @NonNull
+    private DnsInfo mDnsInfo;
+
+    DdrTracker() {
+        mDnsInfo = new DnsInfo(new PrivateDnsConfig(false /* useTls */));
+    }
+
+    /**
+     * If the private DNS settings on the network has changed, this function updates
+     * the DnsInfo and returns true; otherwise, the DnsInfo remains the same and this function
+     * returns false.
+     */
+    boolean notifyPrivateDnsSettingsChanged(@NonNull PrivateDnsConfig cfg) {
+        if (arePrivateDnsSettingsEquals(cfg, mDnsInfo.cfg)) return false;
+
+        mDnsInfo = new DnsInfo(cfg);
+        return true;
+    }
+
+    @PrivateDnsMode int getPrivateDnsMode() {
+        return mDnsInfo.cfg.mode;
+    }
+
+    // Returns a non-empty string (strict mode) or an empty string (off/opportunistic mode) .
+    @VisibleForTesting
+    @NonNull
+    String getStrictModeHostname() {
+        return mDnsInfo.cfg.hostname;
+    }
+
+    @VisibleForTesting
+    private static boolean arePrivateDnsSettingsEquals(@NonNull PrivateDnsConfig a,
+            @NonNull PrivateDnsConfig b) {
+        return a.mode == b.mode && TextUtils.equals(a.hostname, b.hostname);
+    }
+
+    /**
+     * A class to store current DNS configuration. Only the information relevant to DDR is stored.
+     *   1. Private DNS setting.
+     *   2. A list of Unencrypted DNS servers (to be implemented)
+     */
+    private static class DnsInfo {
+        @NonNull
+        public final PrivateDnsConfig cfg;
+
+        DnsInfo(@NonNull PrivateDnsConfig cfg) {
+            this.cfg = cfg;
+        }
+    }
+}
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index 895fc54..05103d9 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -462,6 +462,9 @@
     @VisibleForTesting
     static final int MAX_PROBE_THREAD_POOL_SIZE = 5;
     private String mPrivateDnsProviderHostname = "";
+    private final boolean mDdrEnabled;
+    @NonNull
+    private final DdrTracker mDdrTracker;
 
     private final Context mContext;
     private final INetworkMonitorCallbacks mCallback;
@@ -679,6 +682,7 @@
                 context, NetworkStackUtils.REEVALUATE_WHEN_RESUME);
         mAsyncPrivdnsResolutionEnabled = deps.isFeatureEnabled(context,
                 NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION);
+        mDdrEnabled = deps.isFeatureEnabled(context, NetworkStackUtils.DNS_DDR_VERSION);
         mUseHttps = getUseHttpsValidation();
         mCaptivePortalUserAgent = getCaptivePortalUserAgent();
         mCaptivePortalFallbackSpecs =
@@ -714,6 +718,8 @@
         mLinkProperties = new LinkProperties();
         mNetworkCapabilities = new NetworkCapabilities(null);
         mNetworkAgentConfig = NetworkAgentConfigShimImpl.newInstance(null);
+
+        mDdrTracker = new DdrTracker();
     }
 
     /**
@@ -1084,6 +1090,9 @@
                 case CMD_PRIVATE_DNS_SETTINGS_CHANGED: {
                     final PrivateDnsConfig cfg = (PrivateDnsConfig) message.obj;
                     final TcpSocketTracker tst = getTcpSocketTracker();
+                    if (mDdrEnabled) {
+                        mDdrTracker.notifyPrivateDnsSettingsChanged(cfg);
+                    }
                     if (!isPrivateDnsValidationRequired() || !cfg.inStrictMode()) {
                         // No DNS resolution required.
                         //
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 91e94a8..e26ea28 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -32,7 +32,7 @@
         "kotlin-reflect",
         "mockito-target-extended-minus-junit4",
         "net-tests-utils",
-        //"net-utils-framework-common",
+        "net-utils-framework-common",
         "testables",
     ],
     libs: [
diff --git a/tests/unit/src/android/net/ip/IpClientTest.java b/tests/unit/src/android/net/ip/IpClientTest.java
index 2a807b5..71f7ebf 100644
--- a/tests/unit/src/android/net/ip/IpClientTest.java
+++ b/tests/unit/src/android/net/ip/IpClientTest.java
@@ -16,14 +16,17 @@
 
 package android.net.ip;
 
+import static android.net.apf.BaseApfGenerator.APF_VERSION_6;
 import static android.net.ip.IpClientLinkObserver.CONFIG_SOCKET_RECV_BUFSIZE;
 import static android.net.ip.IpClientLinkObserver.SOCKET_RECV_BUFSIZE;
+import static android.system.OsConstants.AF_UNSPEC;
 import static android.system.OsConstants.ARPHRD_ETHER;
 import static android.system.OsConstants.IFA_F_PERMANENT;
 import static android.system.OsConstants.IFA_F_TENTATIVE;
 import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
 
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWLINK;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTPROT_KERNEL;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELROUTE;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWADDR;
@@ -47,9 +50,11 @@
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
@@ -91,8 +96,10 @@
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.netlink.NduseroptMessage;
 import com.android.net.module.util.netlink.RtNetlinkAddressMessage;
+import com.android.net.module.util.netlink.RtNetlinkLinkMessage;
 import com.android.net.module.util.netlink.RtNetlinkRouteMessage;
 import com.android.net.module.util.netlink.StructIfaddrMsg;
+import com.android.net.module.util.netlink.StructIfinfoMsg;
 import com.android.net.module.util.netlink.StructNdOptRdnss;
 import com.android.net.module.util.netlink.StructNlMsgHdr;
 import com.android.net.module.util.netlink.StructRtMsg;
@@ -109,6 +116,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -139,6 +147,8 @@
     private static final String INVALID = "INVALID";
     private static final String TEST_IFNAME = "test_wlan0";
     private static final int TEST_IFINDEX = 1001;
+    private static final String TEST_CLAT_IFNAME = "v4-" + TEST_IFNAME;
+    private static final int TEST_CLAT_IFINDEX = 1002;
     // See RFC 7042#section-2.1.2 for EUI-48 documentation values.
     private static final MacAddress TEST_MAC = MacAddress.fromString("00:00:5E:00:53:01");
     private static final int TEST_TIMEOUT_MS = 30_000;
@@ -301,6 +311,21 @@
                 (byte) 0 /* icmp_code */, option, null /* srcaddr */);
     }
 
+    private static RtNetlinkLinkMessage buildRtmLinkMessage(short type, int ifindex,
+            String ifaceName) {
+        final StructNlMsgHdr nlmsghdr =
+                makeNetlinkMessageHeader(type, (short) (NLM_F_REQUEST | NLM_F_ACK));
+        final StructIfinfoMsg ifInfoMsg =
+                new StructIfinfoMsg(
+                        (short) AF_UNSPEC,
+                        ARPHRD_ETHER,
+                        ifindex,
+                        0 /* flags */,
+                        0xffffffffL /* change */);
+
+        return new RtNetlinkLinkMessage(nlmsghdr, 0 /* mtu */,  ifInfoMsg, TEST_MAC, ifaceName);
+    }
+
     private void onInterfaceAddressUpdated(final LinkAddress la, int flags) {
         final RtNetlinkAddressMessage msg =
                 buildRtmAddressMessage(RTM_NEWADDR, la, TEST_IFINDEX, flags);
@@ -322,6 +347,12 @@
         mNetlinkMessageProcessor.processNetlinkMessage(msg, TEST_UNUSED_REAL_TIME /* whenMs */);
     }
 
+    private void onInterfaceAdded(int ifaceIndex, String ifaceName) {
+        final RtNetlinkLinkMessage msg = buildRtmLinkMessage(RTM_NEWLINK, ifaceIndex, ifaceName);
+        mNetlinkMessageProcessor.processNetlinkMessage(msg, TEST_UNUSED_REAL_TIME /* whenMs */);
+    }
+
+
     @Test
     public void testNullInterfaceNameMostDefinitelyThrows() throws Exception {
         setTestInterfaceParams(null);
@@ -947,6 +978,80 @@
     }
 
     @Test
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void testVendorNdOffloadDisabledWhenApfV6Supported() throws Exception {
+        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), anyBoolean()))
+                .thenReturn(mApfFilter);
+        when(mApfFilter.supportNdOffload()).thenReturn(true);
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                .withoutIpReachabilityMonitor()
+                .withApfCapabilities(new ApfCapabilities(APF_VERSION_6,
+                        4096 /* maxProgramSize */, ARPHRD_ETHER))
+                .build();
+        ipc.startProvisioning(config);
+        final InOrder inOrder = inOrder(mCb);
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).setNeighborDiscoveryOffload(true);
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).setNeighborDiscoveryOffload(false);
+
+        // update clat
+        onInterfaceAdded(TEST_CLAT_IFINDEX, TEST_CLAT_IFNAME);
+        verifyShutdown(ipc);
+        inOrder.verify(mCb, never()).setNeighborDiscoveryOffload(anyBoolean());
+        clearInvocations(mApfFilter);
+        clearInvocations(mCb);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void testVendorNdOffloadEnabledWhenApfV6NotSupported() throws Exception {
+        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), anyBoolean()))
+                .thenReturn(mApfFilter);
+        when(mApfFilter.supportNdOffload()).thenReturn(false);
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                .withoutIpReachabilityMonitor()
+                .withApfCapabilities(new ApfCapabilities(APF_VERSION_6,
+                        4096 /* maxProgramSize */, ARPHRD_ETHER))
+                .build();
+        ipc.startProvisioning(config);
+        verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).setNeighborDiscoveryOffload(true);
+
+        // update clat
+        onInterfaceAdded(TEST_CLAT_IFINDEX, TEST_CLAT_IFNAME);
+        verifyShutdown(ipc);
+        verify(mCb, times(1)).setNeighborDiscoveryOffload(true);
+        clearInvocations(mApfFilter);
+        clearInvocations(mCb);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void testVendorNdOffloadDisabledWhenApfCapabilitiesUpdated() throws Exception {
+        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), anyBoolean()))
+                .thenReturn(mApfFilter);
+        when(mApfFilter.supportNdOffload()).thenReturn(true);
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                .withoutIpReachabilityMonitor()
+                .build();
+        ipc.startProvisioning(config);
+        ipc.updateApfCapabilities(
+                new ApfCapabilities(APF_VERSION_6, 4096 /* maxProgramSize */, ARPHRD_ETHER));
+        HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
+        final InOrder inOrder = inOrder(mCb);
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).setNeighborDiscoveryOffload(true);
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).setNeighborDiscoveryOffload(false);
+        verifyShutdown(ipc);
+        inOrder.verify(mCb, never()).setNeighborDiscoveryOffload(anyBoolean());
+        clearInvocations(mApfFilter);
+        clearInvocations(mCb);
+    }
+
+    @Test
     public void testLinkPropertiesUpdate_callSetLinkPropertiesOnApfFilter() throws Exception {
         when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), anyBoolean()))
                 .thenReturn(mApfFilter);
diff --git a/tests/unit/src/android/net/util/RawSocketUtilsTest.kt b/tests/unit/src/android/net/util/RawSocketUtilsTest.kt
new file mode 100644
index 0000000..45bee54
--- /dev/null
+++ b/tests/unit/src/android/net/util/RawSocketUtilsTest.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.util
+
+import android.content.Context
+import android.net.TetheringManager
+import android.system.Os
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.net.module.util.HexDump
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import java.io.FileDescriptor
+import java.net.NetworkInterface
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.framework
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoSession
+import org.mockito.quality.Strictness
+
+@RunWith(DevSdkIgnoreRunner::class)
+class RawSocketUtilsTest {
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
+    companion object {
+        private const val TEST_IFINDEX = 123
+        private const val TEST_IFACENAME = "wlan0"
+        private const val TEST_SRC_MAC = "FFFFFFFFFFFF"
+        private const val TEST_DST_MAC = "1234567890AB"
+        private const val TEST_INVALID_PACKET_IN_HEX = "DEADBEEF"
+        private const val TEST_PACKET_TYPE_IN_HEX = "88A4"
+        private const val TEST_VALID_PACKET_IN_HEX =
+                TEST_DST_MAC + TEST_SRC_MAC + TEST_PACKET_TYPE_IN_HEX
+    }
+    @Mock
+    private lateinit var mockContext: Context
+    @Mock
+    private lateinit var mockTetheringManager: TetheringManager
+    @Mock
+    private lateinit var mockNetworkInterface: NetworkInterface
+
+    // For mocking static methods.
+    private lateinit var mockitoSession: MockitoSession
+
+    @Before
+    fun setup() {
+        mockitoSession = ExtendedMockito.mockitoSession()
+                .mockStatic(Os::class.java)
+                .mockStatic(NetworkInterface::class.java)
+                .mockStatic(SocketUtils::class.java)
+                .initMocks(this)
+                .strictness(Strictness.LENIENT)
+                .startMocking()
+        doReturn(mockTetheringManager).`when`(mockContext)
+                .getSystemService(eq(TetheringManager::class.java))
+        `when`(NetworkInterface.getByName(any())).thenReturn(mockNetworkInterface)
+        doReturn(TEST_IFINDEX).`when`(mockNetworkInterface).index
+    }
+
+    @After
+    fun teardown() {
+        mockitoSession.finishMocking()
+        // Clear mocks to prevent from stubs holding instances and cause memory leaks.
+        framework().clearInlineMocks()
+    }
+
+    @Test
+    fun sendRawPacketDownStream_invalidTetheredInterface() {
+        doAnswer {
+            val callback = it.arguments[1] as TetheringManager.TetheringEventCallback
+            callback.onTetheredInterfacesChanged(listOf("eth0"))
+        }.`when`(mockTetheringManager).registerTetheringEventCallback(any(), any())
+        assertFailsWith<SecurityException> {
+            RawSocketUtils.sendRawPacketDownStream(
+                mockContext,
+                TEST_IFACENAME,
+                TEST_INVALID_PACKET_IN_HEX
+            )
+        }
+    }
+
+    @Test
+    fun sendRawPacketDownStream_invalidPacket() {
+        doAnswer {
+            val callback = it.arguments[1] as TetheringManager.TetheringEventCallback
+            callback.onTetheredInterfacesChanged(listOf(TEST_IFACENAME))
+        }.`when`(mockTetheringManager).registerTetheringEventCallback(any(), any())
+
+        assertFailsWith<ArrayIndexOutOfBoundsException> {
+            RawSocketUtils.sendRawPacketDownStream(
+                    mockContext,
+                    TEST_IFACENAME,
+                    TEST_INVALID_PACKET_IN_HEX
+            )
+        }
+    }
+
+    @Test
+    fun sendRawPacketDownStream_validPacket() {
+        doAnswer {
+            val callback = it.arguments[1] as TetheringManager.TetheringEventCallback
+            callback.onTetheredInterfacesChanged(listOf(TEST_IFACENAME))
+        }.`when`(mockTetheringManager).registerTetheringEventCallback(any(), any())
+
+        RawSocketUtils.sendRawPacketDownStream(
+            mockContext,
+            TEST_IFACENAME,
+            TEST_VALID_PACKET_IN_HEX
+        )
+
+        // Verify interactions with mocked static methods.
+        val fileDescriptorCaptor = ArgumentCaptor.forClass(FileDescriptor::class.java)
+        val packetDataCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        val packetDataLengthCaptor = ArgumentCaptor.forClass(Int::class.java)
+        ExtendedMockito.verify {
+            Os.sendto(
+                fileDescriptorCaptor.capture(),
+                packetDataCaptor.capture(),
+                eq(0),
+                packetDataLengthCaptor.capture(),
+                eq(0),
+                any()
+            )
+        }
+        assertEquals(TEST_VALID_PACKET_IN_HEX, HexDump.toHexString(packetDataCaptor.value))
+        assertEquals(TEST_VALID_PACKET_IN_HEX.length / 2, packetDataLengthCaptor.value)
+        // TODO: Verify ifindex and packetType once the members of PacketSocketAddress
+        //  can be accessed.
+        ExtendedMockito.verify { SocketUtils.closeSocket(eq(fileDescriptorCaptor.value)) }
+    }
+}
diff --git a/tests/unit/src/com/android/networkstack/metrics/ApfSessionInfoMetricsTest.java b/tests/unit/src/com/android/networkstack/metrics/ApfSessionInfoMetricsTest.java
index 8dc3d92..69464cf 100644
--- a/tests/unit/src/com/android/networkstack/metrics/ApfSessionInfoMetricsTest.java
+++ b/tests/unit/src/com/android/networkstack/metrics/ApfSessionInfoMetricsTest.java
@@ -130,7 +130,7 @@
                 CounterName.CN_DROPPED_IPV6_NON_ICMP_MULTICAST);
         verifyCounterName(Counter.DROPPED_802_3_FRAME, CounterName.CN_DROPPED_802_3_FRAME);
         verifyCounterName(Counter.DROPPED_ETHERTYPE_NOT_ALLOWED,
-                CounterName.CN_DROPPED_ETHERTYPE_DENYLISTED);
+                CounterName.CN_DROPPED_ETHERTYPE_NOT_ALLOWED);
         verifyCounterName(Counter.DROPPED_ARP_REPLY_SPA_NO_HOST,
                 CounterName.CN_DROPPED_ARP_REPLY_SPA_NO_HOST);
         verifyCounterName(Counter.DROPPED_IPV4_KEEPALIVE_ACK,
@@ -143,5 +143,29 @@
         verifyCounterName(Counter.DROPPED_IPV4_TCP_PORT7_UNICAST, CounterName.CN_UNKNOWN);
         verifyCounterName(Counter.DROPPED_ARP_NON_IPV4, CounterName.CN_DROPPED_ARP_NON_IPV4);
         verifyCounterName(Counter.DROPPED_ARP_UNKNOWN, CounterName.CN_DROPPED_ARP_UNKNOWN);
+        verifyCounterName(Counter.PASSED_ARP_BROADCAST_REPLY,
+                CounterName.CN_PASSED_ARP_BROADCAST_REPLY);
+        verifyCounterName(Counter.PASSED_ARP_REQUEST, CounterName.CN_PASSED_ARP_REQUEST);
+        verifyCounterName(Counter.PASSED_IPV4_FROM_DHCPV4_SERVER,
+                CounterName.CN_PASSED_IPV4_FROM_DHCPV4_SERVER);
+        verifyCounterName(Counter.PASSED_IPV6_NS_DAD, CounterName.CN_PASSED_IPV6_NS_DAD);
+        verifyCounterName(Counter.PASSED_IPV6_NS_NO_ADDRESS,
+                CounterName.CN_PASSED_IPV6_NS_NO_ADDRESS);
+        verifyCounterName(Counter.PASSED_IPV6_NS_NO_SLLA_OPTION,
+                CounterName.CN_PASSED_IPV6_NS_NO_SLLA_OPTION);
+        verifyCounterName(Counter.PASSED_IPV6_NS_TENTATIVE,
+                CounterName.CN_PASSED_IPV6_NS_TENTATIVE);
+        verifyCounterName(Counter.PASSED_MLD, CounterName.CN_PASSED_MLD);
+        verifyCounterName(Counter.DROPPED_IPV4_NON_DHCP4, CounterName.CN_DROPPED_IPV4_NON_DHCP4);
+        verifyCounterName(Counter.DROPPED_IPV6_NS_INVALID, CounterName.CN_DROPPED_IPV6_NS_INVALID);
+        verifyCounterName(Counter.DROPPED_IPV6_NS_OTHER_HOST,
+                CounterName.CN_DROPPED_IPV6_NS_OTHER_HOST);
+        verifyCounterName(Counter.DROPPED_IPV6_NS_REPLIED_NON_DAD,
+                CounterName.CN_DROPPED_IPV6_NS_REPLIED_NON_DAD);
+        verifyCounterName(Counter.DROPPED_ARP_REQUEST_ANYHOST,
+                CounterName.CN_DROPPED_ARP_REQUEST_ANYHOST);
+        verifyCounterName(Counter.DROPPED_ARP_REQUEST_REPLIED,
+                CounterName.CN_DROPPED_ARP_REQUEST_REPLIED);
+        verifyCounterName(Counter.DROPPED_ARP_V6_ONLY, CounterName.CN_DROPPED_ARP_V6_ONLY);
     }
 }
diff --git a/tests/unit/src/com/android/server/connectivity/DdrTrackerTest.java b/tests/unit/src/com/android/server/connectivity/DdrTrackerTest.java
new file mode 100644
index 0000000..dcc4291
--- /dev/null
+++ b/tests/unit/src/com/android/server/connectivity/DdrTrackerTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.annotation.NonNull;
+import android.net.shared.PrivateDnsConfig;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.MockitoAnnotations;
+
+import java.net.InetAddress;
+
+@RunWith(JUnit4.class)
+public final class DdrTrackerTest {
+    private static final int OFF_MODE = PRIVATE_DNS_MODE_OFF;
+    private static final int OPPORTUNISTIC_MODE = PRIVATE_DNS_MODE_OPPORTUNISTIC;
+    private static final int STRICT_MODE = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+
+    private DdrTracker mDdrTracker;
+
+    private static class PrivateDnsConfigBuilder {
+        private int mMode = OFF_MODE;
+        private String mHostname = null;
+        private final InetAddress[] mIps = null;
+        private final String mDohName = null;
+        private final InetAddress[] mDohIps = null;
+        private final String mDohPath = null;
+        private final int mDohPort = -1;
+
+        PrivateDnsConfigBuilder setMode(int mode) {
+            mMode = mode;
+            return this;
+        }
+        PrivateDnsConfigBuilder setHostname(String value) {
+            mHostname = value;
+            return this;
+        }
+        PrivateDnsConfig build() {
+            return new PrivateDnsConfig(mMode, mHostname, mIps, mDohName, mDohIps, mDohPath,
+                    mDohPort);
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mDdrTracker = new DdrTracker();
+    }
+
+    private void testNotifyPrivateDnsSettingsChangedHelper(int mode, @NonNull String dnsProvider)
+            throws Exception {
+        final PrivateDnsConfig cfg =
+                new PrivateDnsConfigBuilder().setMode(mode).setHostname(dnsProvider).build();
+
+        assertTrue(mDdrTracker.notifyPrivateDnsSettingsChanged(cfg));
+        assertEquals(mode, mDdrTracker.getPrivateDnsMode());
+        assertEquals(dnsProvider, mDdrTracker.getStrictModeHostname());
+        assertFalse(mDdrTracker.notifyPrivateDnsSettingsChanged(cfg));
+    }
+
+    @Test
+    public void testNotifyPrivateDnsSettingsChanged() throws Exception {
+        // Tests the initial private DNS setting in DdrTracker.
+        assertEquals(OFF_MODE, mDdrTracker.getPrivateDnsMode());
+
+        assertEquals("", mDdrTracker.getStrictModeHostname());
+        assertFalse(mDdrTracker.notifyPrivateDnsSettingsChanged(new PrivateDnsConfigBuilder()
+                .setMode(OFF_MODE).build()));
+
+        testNotifyPrivateDnsSettingsChangedHelper(OPPORTUNISTIC_MODE, "");
+        testNotifyPrivateDnsSettingsChangedHelper(STRICT_MODE, "example1.com");
+        testNotifyPrivateDnsSettingsChangedHelper(STRICT_MODE, "example2.com");
+        testNotifyPrivateDnsSettingsChangedHelper(OFF_MODE, "");
+    }
+}