Merge "Make SkipPresubmit annotation visible by HostsideVpnTests" into main
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index ff65228..e1b5601 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -35,6 +35,7 @@
   name: "net-utils-device-common",
   srcs: [
       "device/com/android/net/module/util/DeviceConfigUtils.java",
+      "device/com/android/net/module/util/DomainUtils.java",
       "device/com/android/net/module/util/FdEventsReader.java",
       "device/com/android/net/module/util/NetworkMonitorUtils.java",
       "device/com/android/net/module/util/PacketReader.java",
diff --git a/staticlibs/device/com/android/net/module/util/DomainUtils.java b/staticlibs/device/com/android/net/module/util/DomainUtils.java
new file mode 100644
index 0000000..80e0b64
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/DomainUtils.java
@@ -0,0 +1,143 @@
+/*
+ * 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.net.module.util;
+
+import android.util.ArrayMap;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.net.module.util.DnsPacketUtils.DnsRecordParser;
+
+import java.nio.BufferOverflowException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+
+/**
+ * Utilities for encoding/decoding the domain name or domain search list.
+ *
+ * @hide
+ */
+public final class DomainUtils {
+    private static final String TAG = "DomainUtils";
+    private static final int MAX_OPTION_LEN = 255;
+
+    @NonNull
+    private static String getSubstring(@NonNull final String string, @NonNull final String[] labels,
+            int index) {
+        int beginIndex = 0;
+        for (int i = 0; i < index; i++) {
+            beginIndex += labels[i].length() + 1; // include the dot
+        }
+        return string.substring(beginIndex);
+    }
+
+    /**
+     * Encode the given single domain name to byte array, comply with RFC1035 section-3.1.
+     *
+     * @return null if the given domain string is invalid, otherwise, return a byte array
+     *         wrapping the encoded domain, not including any padded octets, caller should
+     *         pad zero octets at the end if needed.
+     */
+    @Nullable
+    public static byte[] encode(@NonNull final String domain) {
+        if (!DnsRecordParser.isHostName(domain)) return null;
+        return encode(new String[]{ domain }, false /* compression */);
+    }
+
+    /**
+     * Encode the given multiple domain names to byte array, comply with RFC1035 section-3.1
+     * and section 4.1.4 (message compression) if enabled.
+     *
+     * @return Null if encode fails due to BufferOverflowException, otherwise, return a byte
+     *         array wrapping the encoded domains, not including any padded octets, caller
+     *         should pad zero octets at the end if needed. The byte array may be empty if
+     *         the given domain strings are invalid.
+     */
+    @Nullable
+    public static byte[] encode(@NonNull final String[] domains, boolean compression) {
+        try {
+            final ByteBuffer buffer = ByteBuffer.allocate(MAX_OPTION_LEN);
+            final ArrayMap<String, Integer> offsetMap = new ArrayMap<>();
+            for (int i = 0; i < domains.length; i++) {
+                if (!DnsRecordParser.isHostName(domains[i])) {
+                    Log.e(TAG, "Skip invalid domain name " + domains[i]);
+                    continue;
+                }
+                final String[] labels = domains[i].split("\\.");
+                for (int j = 0; j < labels.length; j++) {
+                    if (compression) {
+                        final String suffix = getSubstring(domains[i], labels, j);
+                        if (offsetMap.containsKey(suffix)) {
+                            int offsetOfSuffix = offsetMap.get(suffix);
+                            offsetOfSuffix |= 0xC000;
+                            buffer.putShort((short) offsetOfSuffix);
+                            break; // unnecessary to put the compressed string into map
+                        } else {
+                            offsetMap.put(suffix, buffer.position());
+                        }
+                    }
+                    // encode the domain name string without compression when:
+                    // - compression feature isn't enabled,
+                    // - suffix does not match any string in the map.
+                    final byte[] labelBytes = labels[j].getBytes(StandardCharsets.UTF_8);
+                    buffer.put((byte) labelBytes.length);
+                    buffer.put(labelBytes);
+                    if (j == labels.length - 1) {
+                        // Pad terminate label at the end of last label.
+                        buffer.put((byte) 0);
+                    }
+                }
+            }
+            buffer.flip();
+            final byte[] out = new byte[buffer.limit()];
+            buffer.get(out);
+            return out;
+        } catch (BufferOverflowException e) {
+            Log.e(TAG, "Fail to encode domain name and stop encoding", e);
+            return null;
+        }
+    }
+
+    /**
+     * Decode domain name(s) from the given byteBuffer. Decode follows RFC1035 section 3.1 and
+     * section 4.1.4(message compression).
+     *
+     * @return domain name(s) string array with space separated, or empty string if decode fails.
+     */
+    @NonNull
+    public static String[] decode(@NonNull final ByteBuffer buffer, boolean compression) {
+        final ArrayList<String> domainList = new ArrayList<>();
+        while (buffer.remaining() > 0) {
+            try {
+                // TODO: replace the recursion with loop in parseName and don't need to pass in the
+                // maxLabelCount parameter to prevent recursion from overflowing stack.
+                final String domain = DnsRecordParser.parseName(buffer, 0 /* depth */,
+                        15 /* maxLabelCount */, compression);
+                if (!DnsRecordParser.isHostName(domain)) continue;
+                domainList.add(domain);
+            } catch (BufferUnderflowException | DnsPacket.ParseException e) {
+                Log.e(TAG, "Fail to parse domain name and stop parsing", e);
+                break;
+            }
+        }
+        return domainList.toArray(new String[0]);
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index 73c88c7..33bd36d 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -33,6 +33,7 @@
 import android.net.util.SocketUtils;
 import android.system.ErrnoException;
 import android.system.Os;
+import android.system.OsConstants;
 import android.system.StructTimeval;
 import android.util.Log;
 
@@ -291,5 +292,20 @@
         return Os.write(fd, bytes, offset, count);
     }
 
+    private static final long CLOCK_TICKS_PER_SECOND = Os.sysconf(OsConstants._SC_CLK_TCK);
+
+    /**
+     * Convert the system time in clock ticks(clock_t type in times(), not in clock()) to
+     * milliseconds. times() clock_t ticks at the kernel's USER_HZ (100) while clock() clock_t
+     * ticks at CLOCKS_PER_SEC (1000000).
+     *
+     * See the NOTES on https://man7.org/linux/man-pages/man2/times.2.html for the difference
+     * of clock_t used in clock() and times().
+     */
+    public static long ticksToMilliSeconds(int intClockTicks) {
+        final long longClockTicks = intClockTicks & 0xffffffffL;
+        return (longClockTicks * 1000) / CLOCK_TICKS_PER_SECOND;
+    }
+
     private NetlinkUtils() {}
 }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNdaCacheInfo.java b/staticlibs/device/com/android/net/module/util/netlink/StructNdaCacheInfo.java
index 79d5ff4..1f9bb7e 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructNdaCacheInfo.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNdaCacheInfo.java
@@ -16,8 +16,7 @@
 
 package com.android.net.module.util.netlink;
 
-import android.system.Os;
-import android.system.OsConstants;
+import static com.android.net.module.util.netlink.NetlinkUtils.ticksToMilliSeconds;
 
 import java.nio.ByteBuffer;
 
@@ -57,15 +56,6 @@
         return struct;
     }
 
-    // TODO: investigate whether this can change during device runtime and
-    // decide what (if anything) should be done about that.
-    private static final long CLOCK_TICKS_PER_SECOND = Os.sysconf(OsConstants._SC_CLK_TCK);
-
-    private static long ticksToMilliSeconds(int intClockTicks) {
-        final long longClockTicks = (long) intClockTicks & 0xffffffff;
-        return (longClockTicks * 1000) / CLOCK_TICKS_PER_SECOND;
-    }
-
     /**
      * Explanatory notes, for reference.
      *
diff --git a/staticlibs/device/com/android/net/module/util/structs/RouteInformationOption.java b/staticlibs/device/com/android/net/module/util/structs/RouteInformationOption.java
new file mode 100644
index 0000000..49bafed
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/RouteInformationOption.java
@@ -0,0 +1,97 @@
+/*
+ * 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.net.module.util.structs;
+
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_RIO;
+
+import android.net.IpPrefix;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * ICMPv6 route information option, as per https://tools.ietf.org/html/rfc4191.
+ *
+ * 0                   1                   2                   3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |     Type      |    Length     | Prefix Length |Resvd|Prf|Resvd|
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                        Route Lifetime                         |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                   Prefix (Variable Length)                    |
+ * .                                                               .
+ * .                                                               .
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class RouteInformationOption extends Struct {
+    public enum Preference {
+        HIGH((byte) 0x1),
+        MEDIUM((byte) 0x0),
+        LOW((byte) 0x3),
+        RESERVED((byte) 0x2);
+
+        final byte mValue;
+        Preference(byte value) {
+            this.mValue = value;
+        }
+    }
+
+    @Field(order = 0, type = Type.S8)
+    public final byte type;
+    @Field(order = 1, type = Type.S8)
+    public final byte length; // Length in 8-byte octets
+    @Field(order = 2, type = Type.U8)
+    public final short prefixLen;
+    @Field(order = 3, type = Type.S8)
+    public final byte prf;
+    @Field(order = 4, type = Type.U32)
+    public final long routeLifetime;
+    @Field(order = 5, type = Type.ByteArray, arraysize = 16)
+    public final byte[] prefix;
+
+    RouteInformationOption(final byte type, final byte length, final short prefixLen,
+            final byte prf, final long routeLifetime, @NonNull final byte[] prefix) {
+        this.type = type;
+        this.length = length;
+        this.prefixLen = prefixLen;
+        this.prf = prf;
+        this.routeLifetime = routeLifetime;
+        this.prefix = prefix;
+    }
+
+    /**
+     * Build a Route Information option from the required specified parameters.
+     */
+    public static ByteBuffer build(final IpPrefix prefix, final Preference prf,
+            final long routeLifetime) {
+        // The prefix field is always assumed to have 16 bytes, but the number of leading
+        // bits in this prefix depends on IpPrefix#prefixLength, then we can simply set the
+        // option length to 3.
+        final RouteInformationOption option = new RouteInformationOption(
+                (byte) ICMPV6_ND_OPTION_RIO, (byte) 3 /* option length */,
+                (short) prefix.getPrefixLength(), (byte) (prf.mValue << 3), routeLifetime,
+                prefix.getRawAddress());
+        return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN));
+    }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/DnsPacketUtils.java b/staticlibs/framework/com/android/net/module/util/DnsPacketUtils.java
index c47bfa0..105d783 100644
--- a/staticlibs/framework/com/android/net/module/util/DnsPacketUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/DnsPacketUtils.java
@@ -130,16 +130,25 @@
 
         /**
          * Parses the domain / target name of a DNS record.
+         */
+        public static String parseName(final ByteBuffer buf, int depth,
+                boolean isNameCompressionSupported) throws
+                BufferUnderflowException, DnsPacket.ParseException {
+            return parseName(buf, depth, MAXLABELCOUNT, isNameCompressionSupported);
+        }
+
+        /**
+         * Parses the domain / target name of a DNS record.
          *
          * As described in RFC 1035 Section 4.1.3, the NAME field of a DNS Resource Record always
          * supports Name Compression, whereas domain names contained in the RDATA payload of a DNS
          * record may or may not support Name Compression, depending on the record TYPE. Moreover,
          * even if Name Compression is supported, its usage is left to the implementation.
          */
-        public static String parseName(ByteBuffer buf, int depth,
+        public static String parseName(final ByteBuffer buf, int depth, int maxLabelCount,
                 boolean isNameCompressionSupported) throws
                 BufferUnderflowException, DnsPacket.ParseException {
-            if (depth > MAXLABELCOUNT) {
+            if (depth > maxLabelCount) {
                 throw new DnsPacket.ParseException("Failed to parse name, too many labels");
             }
             final int len = Byte.toUnsignedInt(buf.get());
@@ -158,7 +167,8 @@
                             "Parse compression name fail, invalid compression");
                 }
                 buf.position(offset);
-                final String pointed = parseName(buf, depth + 1, isNameCompressionSupported);
+                final String pointed = parseName(buf, depth + 1, maxLabelCount,
+                        isNameCompressionSupported);
                 buf.position(oldPos);
                 return pointed;
             } else {
@@ -168,7 +178,8 @@
                 if (head.length() > MAXLABELSIZE) {
                     throw new DnsPacket.ParseException("Parse name fail, invalid label length");
                 }
-                final String tail = parseName(buf, depth + 1, isNameCompressionSupported);
+                final String tail = parseName(buf, depth + 1, maxLabelCount,
+                        isNameCompressionSupported);
                 return TextUtils.isEmpty(tail) ? head : head + "." + tail;
             }
         }
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 6a6f5e1..24aabbc 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -140,6 +140,7 @@
      * ICMPv6 constants.
      *
      * See also:
+     *     - https://tools.ietf.org/html/rfc4191
      *     - https://tools.ietf.org/html/rfc4443
      *     - https://tools.ietf.org/html/rfc4861
      */
@@ -157,6 +158,7 @@
     public static final int ICMPV6_ND_OPTION_TLLA  = 2;
     public static final int ICMPV6_ND_OPTION_PIO   = 3;
     public static final int ICMPV6_ND_OPTION_MTU   = 5;
+    public static final int ICMPV6_ND_OPTION_RIO   = 24;
     public static final int ICMPV6_ND_OPTION_RDNSS = 25;
     public static final int ICMPV6_ND_OPTION_PREF64 = 38;
 
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h b/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
index 9b38dee..dd0804c 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
@@ -22,11 +22,25 @@
 // Reject the packet
 #define BPF_REJECT BPF_STMT(BPF_RET | BPF_K, 0)
 
+// *TWO* instructions: compare and if not equal jump over the accept statement
+#define BPF2_ACCEPT_IF_EQUAL(v) \
+	BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (v), 0, 1), \
+	BPF_ACCEPT
+
 // *TWO* instructions: compare and if equal jump over the reject statement
 #define BPF2_REJECT_IF_NOT_EQUAL(v) \
 	BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (v), 1, 0), \
 	BPF_REJECT
 
+// *TWO* instructions: compare and if none of the bits are set jump over the reject statement
+#define BPF2_REJECT_IF_ANY_BITS_SET(v) \
+	BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, (v), 0, 1), \
+	BPF_REJECT
+
+// loads skb->protocol
+#define BPF_LOAD_SKB_PROTOCOL \
+	BPF_STMT(BPF_LD | BPF_H | BPF_ABS, (__u32)SKF_AD_OFF + SKF_AD_PROTOCOL)
+
 // 8-bit load relative to start of link layer (mac/ethernet) header.
 #define BPF_LOAD_MAC_RELATIVE_U8(ofs) \
 	BPF_STMT(BPF_LD | BPF_B | BPF_ABS, (__u32)SKF_LL_OFF + (ofs))
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 8c49954..40371e6 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -22,7 +22,6 @@
         "net-utils-device-common-bpf",
         "net-utils-device-common-ip",
         "net-utils-device-common-wear",
-        "truth",
     ],
     libs: [
         "android.test.runner",
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DomainUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DomainUtilsTest.java
new file mode 100644
index 0000000..606ed5f
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DomainUtilsTest.java
@@ -0,0 +1,206 @@
+/*
+ * 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.net.module.util;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import libcore.util.HexEncoding;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DomainUtilsTest {
+    @Test
+    public void testEncodeInvalidDomain() {
+        byte[] buffer = DomainUtils.encode(".google.com");
+        assertNull(buffer);
+
+        buffer = DomainUtils.encode("google.com.");
+        assertNull(buffer);
+
+        buffer = DomainUtils.encode("-google.com");
+        assertNull(buffer);
+
+        buffer = DomainUtils.encode("google.com-");
+        assertNull(buffer);
+
+        buffer = DomainUtils.encode("google..com");
+        assertNull(buffer);
+
+        buffer = DomainUtils.encode("google!.com");
+        assertNull(buffer);
+
+        buffer = DomainUtils.encode("google.o");
+        assertNull(buffer);
+
+        buffer = DomainUtils.encode("google,com");
+        assertNull(buffer);
+    }
+
+    @Test
+    public void testEncodeValidDomainNamesWithoutCompression() {
+        // Single domain: "google.com"
+        String suffix = "06676F6F676C6503636F6D00";
+        byte[] buffer = DomainUtils.encode("google.com");
+        //assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // Single domain: "google-guest.com"
+        suffix = "0C676F6F676C652D677565737403636F6D00";
+        buffer = DomainUtils.encode("google-guest.com");
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // domain search list: "example.corp.google.com", "corp.google.com", "google.com"
+        suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+                + "04636F727006676F6F676C6503636F6D00"                // corp.google.com
+                + "06676F6F676C6503636F6D00";                         // google.com
+        buffer = DomainUtils.encode(new String[] {
+                "example.corp.google.com", "corp.google.com", "google.com"},
+                false /* compression */);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+
+        // domain search list: "example.corp.google.com", "corp..google.com"(invalid domain),
+        // "google.com"
+        suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+                + "06676F6F676C6503636F6D00";                         // google.com
+        buffer = DomainUtils.encode(new String[] {
+                "example.corp.google.com", "corp..google.com", "google.com"},
+                false /* compression */);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // Invalid domain search list: "corp..google.com", "..google.com"
+        buffer = DomainUtils.encode(new String[] {"corp..google.com", "..google.com"},
+                false /* compression */);
+        assertEquals(0, buffer.length);
+    }
+
+    @Test
+    public void testEncodeValidDomainNamesWithCompression() {
+        // domain search list: "example.corp.google.com", "corp.google.com", "google.com"
+        String suffix =
+                "076578616D706C6504636F727006676F6F676C6503636F6D00"  // example.corp.google.com
+                + "C008"                                              // corp.google.com
+                + "C00D";                                             // google.com
+        byte[] buffer = DomainUtils.encode(new String[] {
+                "example.corp.google.com", "corp.google.com", "google.com"}, true);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // domain search list: "example.corp.google.com", "a.example.corp.google.com", "google.com"
+        suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+                + "0161C000"                                          // a.example.corp.google.com
+                + "C00D";                                             // google.com
+        buffer = DomainUtils.encode(new String[] {
+                "example.corp.google.com", "a.example.corp.google.com", "google.com"}, true);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // domain search list: "example.corp.google.com", "google.com", "gle.com"
+        suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+                + "C00D"                                              // google.com
+                + "03676C65C014";                                     // gle.com
+        buffer = DomainUtils.encode(new String[] {
+                "example.corp.google.com", "google.com", "gle.com"}, true);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // domain search list: "example.corp.google.com", "google.com", "google"
+        suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+                + "C00D";                                              // google.com
+        buffer = DomainUtils.encode(new String[] {
+                "example.corp.google.com", "google.com", "google"}, true);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // domain search list: "example.corp.google.com", "..google.com"(invalid domain), "google"
+        suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00"; // example.corp.google.com
+        buffer = DomainUtils.encode(new String[] {
+                "example.corp.google.com", "..google.com", "google"}, true);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // domain search list: "example.corp.google.com", "suffix.example.edu.cn", "edu.cn"
+        suffix = "076578616D706C6504636F727006676F6F676C6503636F6D00" // example.corp.google.com
+                + "06737566666978076578616D706C650365647502636E00"    // suffix.example.edu.cn
+                + "C028";                                             // edu.cn
+        buffer = DomainUtils.encode(new String[] {
+                "example.corp.google.com", "suffix.example.edu.cn", "edu.cn"}, true);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+
+        // domain search list: "google.com", "example.com", "sub.example.com"
+        suffix = "06676F6F676C6503636F6D00"                           // google.com
+                + "076578616D706C65C007"                              // example.com
+                + "03737562C00C";                                     // sub.example.com
+        buffer = DomainUtils.encode(new String[] {
+                "google.com", "example.com", "sub.example.com"}, true);
+        assertNotNull(buffer);
+        assertEquals(suffix, HexEncoding.encodeToString(buffer));
+    }
+
+    @Test
+    public void testDecodeDomainNames() {
+        String suffixes = "06676F6F676C6503636F6D00" // google.com
+                + "076578616D706C6503636F6D00"       // example.com
+                + "06676F6F676C6500";                // google
+        String[] expected = new String[] {"google.com", "example.com"};
+        ByteBuffer buffer = ByteBuffer.wrap(HexEncoding.decode(suffixes));
+        String[] suffixString = DomainUtils.decode(buffer, false /* compression */);
+        assertArrayEquals(expected, suffixString);
+
+        // include suffix with invalid length: 64
+        suffixes = "06676F6F676C6503636F6D00"        // google.com
+                + "406578616D706C6503636F6D00"       // example.com(length=64)
+                + "06676F6F676C6500";                // google
+        expected = new String[] {"google.com"};
+        buffer = ByteBuffer.wrap(HexEncoding.decode(suffixes));
+        suffixString = DomainUtils.decode(buffer, false /* compression */);
+        assertArrayEquals(expected, suffixString);
+
+        // include suffix with invalid length: 0
+        suffixes = "06676F6F676C6503636F6D00"         // google.com
+                + "076578616D706C6503636F6D00"        // example.com
+                + "00676F6F676C6500";                 // google(length=0)
+        expected = new String[] {"google.com", "example.com"};
+        buffer = ByteBuffer.wrap(HexEncoding.decode(suffixes));
+        suffixString = DomainUtils.decode(buffer, false /* compression */);
+        assertArrayEquals(expected, suffixString);
+
+        suffixes =
+                "076578616D706C6504636F727006676F6F676C6503636F6D00"  // example.corp.google.com
+                + "C008"                                              // corp.google.com
+                + "C00D";                                             // google.com
+        expected = new String[] {"example.corp.google.com", "corp.google.com", "google.com"};
+        buffer = ByteBuffer.wrap(HexEncoding.decode(suffixes));
+        suffixString = DomainUtils.decode(buffer, true /* compression */);
+        assertArrayEquals(expected, suffixString);
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
index dac5911..3a72dd1 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
@@ -26,8 +26,6 @@
 import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
 import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
 
-import static com.google.common.truth.Truth.assertThat;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
@@ -193,7 +191,9 @@
             // and IPv6 loopback address: 127.0.0.1 and ::1.
             final StructIfaddrMsg ifaMsg = ((RtNetlinkAddressMessage) msg).getIfaddrHeader();
             final InetAddress ipAddress = ((RtNetlinkAddressMessage) msg).getIpAddress();
-            assertThat((int) ifaMsg.family).isAnyOf(AF_INET, AF_INET6);
+            assertTrue(
+                    "Non-IP address family: " + ifaMsg.family,
+                    ifaMsg.family == AF_INET || ifaMsg.family == AF_INET6);
             assertTrue(ipAddress.isLoopbackAddress());
         }
 
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/DnsResolverModuleTest.kt b/staticlibs/testutils/hostdevice/com/android/testutils/DnsResolverModuleTest.kt
new file mode 100644
index 0000000..9e97d51
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/DnsResolverModuleTest.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.testutils
+
+/**
+ * Indicates that the test covers functionality that was rolled out in a resolv module update.
+ */
+annotation class DnsResolverModuleTest
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/SkipMainlinePresubmit.kt b/staticlibs/testutils/hostdevice/com/android/testutils/SkipMainlinePresubmit.kt
new file mode 100644
index 0000000..5952365
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/SkipMainlinePresubmit.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.testutils
+
+/**
+ * Skip the test in presubmit runs for the reason specified in [reason].
+ *
+ * This annotation is typically used to document limitations that prevent a test from being
+ * executed in presubmit on older builds.
+ */
+annotation class SkipMainlinePresubmit(val reason: String)