Merge "Fix libs/net tests package"
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index fde9d3e..d4c58f6 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -95,15 +95,37 @@
 }
 
 java_library {
-    name: "net-utils-device-common-netlink",
-    // TODO: Ipv6Utils and Struct stuff could be separated out of the netlink library into
-    // an individual Struct library, and remove the net-utils-framework-common lib dependency.
-    // But there is no need doing this at the moment.
+    name: "net-utils-device-common-bpf",
+    srcs: [
+        "device/com/android/net/module/util/BpfMap.java",
+        "device/com/android/net/module/util/JniUtil.java",
+    ],
+    sdk_version: "system_current",
+    min_sdk_version: "29",
+    visibility: [
+        "//frameworks/libs/net/common/tests:__subpackages__",
+        "//frameworks/libs/net/common/testutils:__subpackages__",
+        "//packages/modules/Connectivity:__subpackages__",
+        "//packages/modules/NetworkStack:__subpackages__",
+    ],
+    static_libs: [
+        "net-utils-device-common-struct",
+    ],
+    libs: [
+        "androidx.annotation_annotation",
+    ],
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+}
+
+java_library {
+    name: "net-utils-device-common-struct",
     srcs: [
         "device/com/android/net/module/util/HexDump.java",
         "device/com/android/net/module/util/Ipv6Utils.java",
         "device/com/android/net/module/util/Struct.java",
-        "device/com/android/net/module/util/netlink/*.java",
         "device/com/android/net/module/util/structs/*.java",
     ],
     sdk_version: "system_current",
@@ -126,6 +148,30 @@
 }
 
 java_library {
+    name: "net-utils-device-common-netlink",
+    srcs: [
+        "device/com/android/net/module/util/netlink/*.java",
+    ],
+    sdk_version: "system_current",
+    min_sdk_version: "29",
+    visibility: [
+        "//frameworks/libs/net/common/testutils:__subpackages__",
+        "//packages/modules/Connectivity:__subpackages__",
+        "//packages/modules/NetworkStack:__subpackages__",
+    ],
+    static_libs: [
+        "net-utils-device-common-struct",
+    ],
+    libs: [
+        "androidx.annotation_annotation",
+    ],
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+}
+
+java_library {
     // TODO : this target should probably be folded into net-utils-device-common
     name: "net-utils-device-common-ip",
     srcs: [
@@ -268,3 +314,30 @@
         "//packages/modules/Wifi/service",
     ],
 }
+
+// This file group is deprecated; new users should use net-utils-annotations
+filegroup {
+    name: "net-utils-annotations-srcs",
+    srcs: [
+        "annotations/android/net/annotations/PolicyDirection.java",
+    ],
+    visibility: [
+        "//frameworks/base",
+    ],
+}
+
+
+java_library {
+    name: "net-utils-annotations",
+    srcs: [":net-utils-annotations-srcs"],
+    libs: [
+        "framework-annotations-lib",
+    ],
+    sdk_version: "system_current",
+    min_sdk_version: "30",
+    visibility: ["//visibility:public"],
+    apex_available: [
+        "//apex_available:anyapex",
+        "//apex_available:platform",
+    ],
+}
diff --git a/staticlibs/annotations/android/net/annotations/PolicyDirection.java b/staticlibs/annotations/android/net/annotations/PolicyDirection.java
new file mode 100644
index 0000000..febd9b4
--- /dev/null
+++ b/staticlibs/annotations/android/net/annotations/PolicyDirection.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2019 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.annotations;
+
+import android.annotation.IntDef;
+import android.net.IpSecManager;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * IPsec traffic direction.
+ *
+ * <p>Mainline modules cannot reference hidden @IntDef. Moving this annotation to a separate class
+ * to allow others to statically include it.
+ *
+ * @hide
+ */
+@IntDef(value = {IpSecManager.DIRECTION_IN, IpSecManager.DIRECTION_OUT})
+@Retention(RetentionPolicy.SOURCE)
+public @interface PolicyDirection {}
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
new file mode 100644
index 0000000..5f05c7c
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2020 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 android.system.OsConstants.EEXIST;
+import static android.system.OsConstants.ENOENT;
+
+import android.system.ErrnoException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.net.module.util.Struct;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+
+/**
+ * BpfMap is a key -> value mapping structure that is designed to maintained the bpf map entries.
+ * This is a wrapper class of in-kernel data structure. The in-kernel data can be read/written by
+ * passing syscalls with map file descriptor.
+ *
+ * @param <K> the key of the map.
+ * @param <V> the value of the map.
+ */
+public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable {
+    static {
+        System.loadLibrary(JniUtil.getJniLibraryName(BpfMap.class.getPackage()));
+    }
+
+    // Following definitions from kernel include/uapi/linux/bpf.h
+    public static final int BPF_F_RDWR = 0;
+    public static final int BPF_F_RDONLY = 1 << 3;
+    public static final int BPF_F_WRONLY = 1 << 4;
+
+    public static final int BPF_MAP_TYPE_HASH = 1;
+
+    private static final int BPF_F_NO_PREALLOC = 1;
+
+    private static final int BPF_ANY = 0;
+    private static final int BPF_NOEXIST = 1;
+    private static final int BPF_EXIST = 2;
+
+    private final int mMapFd;
+    private final Class<K> mKeyClass;
+    private final Class<V> mValueClass;
+    private final int mKeySize;
+    private final int mValueSize;
+
+    /**
+     * Create a BpfMap map wrapper with "path" of filesystem.
+     *
+     * @param flag the access mode, one of BPF_F_RDWR, BPF_F_RDONLY, or BPF_F_WRONLY.
+     * @throws ErrnoException if the BPF map associated with {@code path} cannot be retrieved.
+     * @throws NullPointerException if {@code path} is null.
+     */
+    public BpfMap(@NonNull final String path, final int flag, final Class<K> key,
+            final Class<V> value) throws ErrnoException, NullPointerException {
+        mMapFd = bpfFdGet(path, flag);
+
+        mKeyClass = key;
+        mValueClass = value;
+        mKeySize = Struct.getSize(key);
+        mValueSize = Struct.getSize(value);
+    }
+
+     /**
+     * Constructor for testing only.
+     * The derived class implements an internal mocked map. It need to implement all functions
+     * which are related with the native BPF map because the BPF map handler is not initialized.
+     * See BpfCoordinatorTest#TestBpfMap.
+     */
+    @VisibleForTesting
+    protected BpfMap(final Class<K> key, final Class<V> value) {
+        mMapFd = -1;
+        mKeyClass = key;
+        mValueClass = value;
+        mKeySize = Struct.getSize(key);
+        mValueSize = Struct.getSize(value);
+    }
+
+    /**
+     * Update an existing or create a new key -> value entry in an eBbpf map.
+     * (use insertOrReplaceEntry() if you need to know whether insert or replace happened)
+     */
+    public void updateEntry(K key, V value) throws ErrnoException {
+        writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_ANY);
+    }
+
+    /**
+     * If the key does not exist in the map, insert key -> value entry into eBpf map.
+     * Otherwise IllegalStateException will be thrown.
+     */
+    public void insertEntry(K key, V value)
+            throws ErrnoException, IllegalStateException {
+        try {
+            writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST);
+        } catch (ErrnoException e) {
+            if (e.errno == EEXIST) throw new IllegalStateException(key + " already exists");
+
+            throw e;
+        }
+    }
+
+    /**
+     * If the key already exists in the map, replace its value. Otherwise NoSuchElementException
+     * will be thrown.
+     */
+    public void replaceEntry(K key, V value)
+            throws ErrnoException, NoSuchElementException {
+        try {
+            writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_EXIST);
+        } catch (ErrnoException e) {
+            if (e.errno == ENOENT) throw new NoSuchElementException(key + " not found");
+
+            throw e;
+        }
+    }
+
+    /**
+     * Update an existing or create a new key -> value entry in an eBbpf map.
+     * Returns true if inserted, false if replaced.
+     * (use updateEntry() if you don't care whether insert or replace happened)
+     * Note: see inline comment below if running concurrently with delete operations.
+     */
+    public boolean insertOrReplaceEntry(K key, V value)
+            throws ErrnoException {
+        try {
+            writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST);
+            return true;   /* insert succeeded */
+        } catch (ErrnoException e) {
+            if (e.errno != EEXIST) throw e;
+        }
+        try {
+            writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_EXIST);
+            return false;   /* replace succeeded */
+        } catch (ErrnoException e) {
+            if (e.errno != ENOENT) throw e;
+        }
+        /* If we reach here somebody deleted after our insert attempt and before our replace:
+         * this implies a race happened.  The kernel bpf delete interface only takes a key,
+         * and not the value, so we can safely pretend the replace actually succeeded and
+         * was immediately followed by the other thread's delete, since the delete cannot
+         * observe the potential change to the value.
+         */
+        return false;   /* pretend replace succeeded */
+    }
+
+    /** Remove existing key from eBpf map. Return false if map was not modified. */
+    public boolean deleteEntry(K key) throws ErrnoException {
+        return deleteMapEntry(mMapFd, key.writeToBytes());
+    }
+
+    /** Returns {@code true} if this map contains no elements. */
+    public boolean isEmpty() throws ErrnoException {
+        return getFirstKey() == null;
+    }
+
+    private K getNextKeyInternal(@Nullable K key) throws ErrnoException {
+        final byte[] rawKey = getNextRawKey(
+                key == null ? null : key.writeToBytes());
+        if (rawKey == null) return null;
+
+        final ByteBuffer buffer = ByteBuffer.wrap(rawKey);
+        buffer.order(ByteOrder.nativeOrder());
+        return Struct.parse(mKeyClass, buffer);
+    }
+
+    /**
+     * Get the next key of the passed-in key. If the passed-in key is not found, return the first
+     * key. If the passed-in key is the last one, return null.
+     *
+     * TODO: consider allowing null passed-in key.
+     */
+    public K getNextKey(@NonNull K key) throws ErrnoException {
+        Objects.requireNonNull(key);
+        return getNextKeyInternal(key);
+    }
+
+    private byte[] getNextRawKey(@Nullable final byte[] key) throws ErrnoException {
+        byte[] nextKey = new byte[mKeySize];
+        if (getNextMapKey(mMapFd, key, nextKey)) return nextKey;
+
+        return null;
+    }
+
+    /** Get the first key of eBpf map. */
+    public K getFirstKey() throws ErrnoException {
+        return getNextKeyInternal(null);
+    }
+
+    /** Check whether a key exists in the map. */
+    public boolean containsKey(@NonNull K key) throws ErrnoException {
+        Objects.requireNonNull(key);
+
+        final byte[] rawValue = getRawValue(key.writeToBytes());
+        return rawValue != null;
+    }
+
+    /** Retrieve a value from the map. Return null if there is no such key. */
+    public V getValue(@NonNull K key) throws ErrnoException {
+        Objects.requireNonNull(key);
+        final byte[] rawValue = getRawValue(key.writeToBytes());
+
+        if (rawValue == null) return null;
+
+        final ByteBuffer buffer = ByteBuffer.wrap(rawValue);
+        buffer.order(ByteOrder.nativeOrder());
+        return Struct.parse(mValueClass, buffer);
+    }
+
+    private byte[] getRawValue(final byte[] key) throws ErrnoException {
+        byte[] value = new byte[mValueSize];
+        if (findMapEntry(mMapFd, key, value)) return value;
+
+        return null;
+    }
+
+    /**
+     * Iterate through the map and handle each key -> value retrieved base on the given BiConsumer.
+     * The given BiConsumer may to delete the passed-in entry, but is not allowed to perform any
+     * other structural modifications to the map, such as adding entries or deleting other entries.
+     * Otherwise, iteration will result in undefined behaviour.
+     */
+    public void forEach(BiConsumer<K, V> action) throws ErrnoException {
+        @Nullable K nextKey = getFirstKey();
+
+        while (nextKey != null) {
+            @NonNull final K curKey = nextKey;
+            @NonNull final V value = getValue(curKey);
+
+            nextKey = getNextKey(curKey);
+            action.accept(curKey, value);
+        }
+    }
+
+    @Override
+    public void close() throws ErrnoException {
+        closeMap(mMapFd);
+    }
+
+    /**
+     * Clears the map. The map may already be empty.
+     *
+     * @throws ErrnoException if the map is already closed, if an error occurred during iteration,
+     *                        or if a non-ENOENT error occurred when deleting a key.
+     */
+    public void clear() throws ErrnoException {
+        K key = getFirstKey();
+        while (key != null) {
+            deleteEntry(key);  // ignores ENOENT.
+            key = getFirstKey();
+        }
+    }
+
+    private static native int closeMap(int fd) throws ErrnoException;
+
+    private native int bpfFdGet(String path, int mode) throws ErrnoException, NullPointerException;
+
+    private native void writeToMapEntry(int fd, byte[] key, byte[] value, int flags)
+            throws ErrnoException;
+
+    private native boolean deleteMapEntry(int fd, byte[] key) throws ErrnoException;
+
+    // If key is found, the operation returns true and the nextKey would reference to the next
+    // element.  If key is not found, the operation returns true and the nextKey would reference to
+    // the first element.  If key is the last element, false is returned.
+    private native boolean getNextMapKey(int fd, byte[] key, byte[] nextKey) throws ErrnoException;
+
+    private native boolean findMapEntry(int fd, byte[] key, byte[] value) throws ErrnoException;
+}
diff --git a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
index 77b7835..30a1c33 100644
--- a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
+++ b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -35,6 +35,12 @@
     private DeviceConfigUtils() {}
 
     private static final String TAG = DeviceConfigUtils.class.getSimpleName();
+    /**
+     * DO NOT MODIFY: this may be used by multiple modules that will not see the updated value
+     * until they are recompiled, so modifying this constant means that different modules may
+     * be referencing a different tethering module variant, or having a stale reference.
+     */
+    public static final String TETHERING_MODULE_NAME = "com.android.tethering";
 
     @VisibleForTesting
     public static void resetPackageVersionCacheForTest() {
diff --git a/staticlibs/device/com/android/net/module/util/JniUtil.java b/staticlibs/device/com/android/net/module/util/JniUtil.java
new file mode 100644
index 0000000..5210a3e
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/JniUtil.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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;
+
+/**
+ * Utilities for modules to use jni.
+ */
+public final class JniUtil {
+    /**
+     * The method to find jni library accroding to the giving package name.
+     *
+     * The jni library name would be packageName + _jni.so. E.g.
+     * com_android_networkstack_tethering_util_jni for tethering,
+     * com_android_connectivity_util_jni for connectivity.
+     */
+    public static String getJniLibraryName(final Package pkg) {
+        final String libPrefix = pkg.getName().replaceAll("\\.", "_");
+
+        return libPrefix + "_jni";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
index 07b52d8..83a82b7 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
@@ -146,12 +146,26 @@
     public static final int RTMGRP_LINK = 1;
     public static final int RTMGRP_IPV4_IFADDR = 0x10;
     public static final int RTMGRP_IPV6_IFADDR = 0x100;
+    public static final int RTMGRP_IPV6_ROUTE  = 0x400;
     public static final int RTNLGRP_ND_USEROPT = 20;
     public static final int RTMGRP_ND_USEROPT = 1 << (RTNLGRP_ND_USEROPT - 1);
 
     // Device flags.
     public static final int IFF_LOWER_UP = 1 << 16;
 
+    // Known values for struct rtmsg rtm_protocol.
+    public static final short RTPROT_KERNEL     = 2;
+    public static final short RTPROT_RA         = 9;
+
+    // Known values for struct rtmsg rtm_scope.
+    public static final short RT_SCOPE_UNIVERSE = 0;
+
+    // Known values for struct rtmsg rtm_type.
+    public static final short RTN_UNICAST       = 1;
+
+    // Known values for struct rtmsg rtm_flags.
+    public static final int RTM_F_CLONED        = 0x200;
+
     /**
      * Convert a netlink message type to a string for control message.
      */
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
index 708736e..a216752 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
@@ -126,6 +126,9 @@
             case NetlinkConstants.RTM_NEWADDR:
             case NetlinkConstants.RTM_DELADDR:
                 return (NetlinkMessage) RtNetlinkAddressMessage.parse(nlmsghdr, byteBuffer);
+            case NetlinkConstants.RTM_NEWROUTE:
+            case NetlinkConstants.RTM_DELROUTE:
+                return (NetlinkMessage) RtNetlinkRouteMessage.parse(nlmsghdr, byteBuffer);
             case NetlinkConstants.RTM_NEWNEIGH:
             case NetlinkConstants.RTM_DELNEIGH:
             case NetlinkConstants.RTM_GETNEIGH:
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
new file mode 100644
index 0000000..c5efcb2
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2021 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.netlink;
+
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY;
+
+import android.net.IpPrefix;
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
+/**
+ * A NetlinkMessage subclass for rtnetlink route messages.
+ *
+ * RtNetlinkRouteMessage.parse() must be called with a ByteBuffer that contains exactly one
+ * netlink message.
+ *
+ * see also:
+ *
+ *     include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class RtNetlinkRouteMessage extends NetlinkMessage {
+    public static final short RTA_DST           = 1;
+    public static final short RTA_OIF           = 4;
+    public static final short RTA_GATEWAY       = 5;
+
+    private int mIfindex;
+    @NonNull
+    private StructRtMsg mRtmsg;
+    @NonNull
+    private IpPrefix mDestination;
+    @Nullable
+    private InetAddress mGateway;
+
+    private RtNetlinkRouteMessage(StructNlMsgHdr header) {
+        super(header);
+        mRtmsg = null;
+        mDestination = null;
+        mGateway = null;
+        mIfindex = 0;
+    }
+
+    public int getInterfaceIndex() {
+        return mIfindex;
+    }
+
+    @NonNull
+    public StructRtMsg getRtMsgHeader() {
+        return mRtmsg;
+    }
+
+    @NonNull
+    public IpPrefix getDestination() {
+        return mDestination;
+    }
+
+    @Nullable
+    public InetAddress getGateway() {
+        return mGateway;
+    }
+
+    /**
+     * Check whether the address families of destination and gateway match rtm_family in
+     * StructRtmsg.
+     *
+     * For example, IPv4-mapped IPv6 addresses as an IPv6 address will be always converted to IPv4
+     * address, that's incorrect when upper layer creates a new {@link RouteInfo} class instance
+     * for IPv6 route with the converted IPv4 gateway.
+     */
+    private static boolean matchRouteAddressFamily(@NonNull final InetAddress address,
+            int family) {
+        return ((address instanceof Inet4Address) && (family == AF_INET))
+                || ((address instanceof Inet6Address) && (family == AF_INET6));
+    }
+
+    /**
+     * Parse rtnetlink route message from {@link ByteBuffer}. This method must be called with a
+     * ByteBuffer that contains exactly one netlink message.
+     *
+     * @param header netlink message header.
+     * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes.
+     */
+    @Nullable
+    public static RtNetlinkRouteMessage parse(@NonNull final StructNlMsgHdr header,
+            @NonNull final ByteBuffer byteBuffer) {
+        final RtNetlinkRouteMessage routeMsg = new RtNetlinkRouteMessage(header);
+
+        routeMsg.mRtmsg = StructRtMsg.parse(byteBuffer);
+        if (routeMsg.mRtmsg == null) return null;
+        int rtmFamily = routeMsg.mRtmsg.family;
+
+        // RTA_DST
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = StructNlAttr.findNextAttrOfType(RTA_DST, byteBuffer);
+        if (nlAttr != null) {
+            final InetAddress destination = nlAttr.getValueAsInetAddress();
+            // If the RTA_DST attribute is malformed, return null.
+            if (destination == null) return null;
+            // If the address family of destination doesn't match rtm_family, return null.
+            if (!matchRouteAddressFamily(destination, rtmFamily)) return null;
+            routeMsg.mDestination = new IpPrefix(destination, routeMsg.mRtmsg.dstLen);
+        } else if (rtmFamily == AF_INET) {
+            routeMsg.mDestination = new IpPrefix(IPV4_ADDR_ANY, 0);
+        } else if (rtmFamily == AF_INET6) {
+            routeMsg.mDestination = new IpPrefix(IPV6_ADDR_ANY, 0);
+        } else {
+            return null;
+        }
+
+        // RTA_GATEWAY
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(RTA_GATEWAY, byteBuffer);
+        if (nlAttr != null) {
+            routeMsg.mGateway = nlAttr.getValueAsInetAddress();
+            // If the RTA_GATEWAY attribute is malformed, return null.
+            if (routeMsg.mGateway == null) return null;
+            // If the address family of gateway doesn't match rtm_family, return null.
+            if (!matchRouteAddressFamily(routeMsg.mGateway, rtmFamily)) return null;
+        }
+
+        // RTA_OIF
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(RTA_OIF, byteBuffer);
+        if (nlAttr != null) {
+            // Any callers that deal with interface names are responsible for converting
+            // the interface index to a name themselves. This may not succeed or may be
+            // incorrect, because the interface might have been deleted, or even deleted
+            // and re-added with a different index, since the netlink message was sent.
+            routeMsg.mIfindex = nlAttr.getValueAsInt(0 /* 0 isn't a valid ifindex */);
+        }
+
+        return routeMsg;
+    }
+
+    /**
+     * Write a rtnetlink address message to {@link ByteBuffer}.
+     */
+    @VisibleForTesting
+    protected void pack(ByteBuffer byteBuffer) {
+        getHeader().pack(byteBuffer);
+        mRtmsg.pack(byteBuffer);
+
+        final StructNlAttr destination = new StructNlAttr(RTA_DST, mDestination.getAddress());
+        destination.pack(byteBuffer);
+
+        if (mGateway != null) {
+            final StructNlAttr gateway = new StructNlAttr(RTA_GATEWAY, mGateway.getAddress());
+            gateway.pack(byteBuffer);
+        }
+        if (mIfindex != 0) {
+            final StructNlAttr ifindex = new StructNlAttr(RTA_OIF, mIfindex);
+            ifindex.pack(byteBuffer);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "RtNetlinkRouteMessage{ "
+                + "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
+                + "Rtmsg{" + mRtmsg.toString() + "}, "
+                + "destination{" + mDestination.getAddress().getHostAddress() + "}, "
+                + "gateway{" + (mGateway == null ? "" : mGateway.getHostAddress()) + "}, "
+                + "ifindex{" + mIfindex + "} "
+                + "}";
+    }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java
new file mode 100644
index 0000000..3cd7292
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2021 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.netlink;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+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;
+
+/**
+ * struct rtmsg
+ *
+ * see also:
+ *
+ *     include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class StructRtMsg extends Struct {
+    // Already aligned.
+    public static final int STRUCT_SIZE = 12;
+
+    @Field(order = 0, type = Type.U8)
+    public final short family; // Address family of route.
+    @Field(order = 1, type = Type.U8)
+    public final short dstLen; // Length of destination.
+    @Field(order = 2, type = Type.U8)
+    public final short srcLen; // Length of source.
+    @Field(order = 3, type = Type.U8)
+    public final short tos;    // TOS filter.
+    @Field(order = 4, type = Type.U8)
+    public final short table;  // Routing table ID.
+    @Field(order = 5, type = Type.U8)
+    public final short protocol; // Routing protocol.
+    @Field(order = 6, type = Type.U8)
+    public final short scope;  // distance to the destination.
+    @Field(order = 7, type = Type.U8)
+    public final short type;   // route type
+    @Field(order = 8, type = Type.U32)
+    public final long flags;
+
+    StructRtMsg(short family, short dstLen, short srcLen, short tos, short table, short protocol,
+            short scope, short type, long flags) {
+        this.family = family;
+        this.dstLen = dstLen;
+        this.srcLen = srcLen;
+        this.tos = tos;
+        this.table = table;
+        this.protocol = protocol;
+        this.scope = scope;
+        this.type = type;
+        this.flags = flags;
+    }
+
+    /**
+     * Parse a rtmsg struct from a {@link ByteBuffer}.
+     *
+     * @param byteBuffer The buffer from which to parse the rtmsg struct.
+     * @return the parsed rtmsg struct, or {@code null} if the rtmsg struct could not be
+     *         parsed successfully (for example, if it was truncated).
+     */
+    @Nullable
+    public static StructRtMsg parse(@NonNull final ByteBuffer byteBuffer) {
+        if (byteBuffer.remaining() < STRUCT_SIZE) return null;
+
+        // The ByteOrder must already have been set to native order.
+        return Struct.parse(StructRtMsg.class, byteBuffer);
+    }
+
+    /**
+     * Write the rtmsg struct to {@link ByteBuffer}.
+     */
+    public void pack(@NonNull final ByteBuffer byteBuffer) {
+        // The ByteOrder must already have been set to native order.
+        this.writeToByteBuffer(byteBuffer);
+    }
+}
diff --git a/staticlibs/native/OWNERS b/staticlibs/native/OWNERS
deleted file mode 100644
index 7655338..0000000
--- a/staticlibs/native/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-maze@google.com
diff --git a/staticlibs/native/README.md b/staticlibs/native/README.md
new file mode 100644
index 0000000..18d19c4
--- /dev/null
+++ b/staticlibs/native/README.md
@@ -0,0 +1,27 @@
+# JNI
+As a general rule, jarjar every static library dependency used in a mainline module into the
+modules's namespace (especially if it is also used by other modules)
+
+Fully-qualified name of java class needs to be hard-coded into the JNI .so, because JNI_OnLoad
+does not take any parameters. This means that there needs to be a different .so target for each
+post-jarjared package, so for each module.
+
+This is the guideline to provide JNI library shared with modules:
+
+* provide a common java library in frameworks/libs/net with the Java class (e.g. BpfMap.java).
+
+* provide a common native library in frameworks/libs/net with the JNI and provide the native
+  register function with class_name parameter. See register_com_android_net_module_util_BpfMap
+  function in frameworks/libs/net/common/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
+  as an example.
+
+When you want to use JNI library from frameworks/lib/net:
+
+* Each module includes the java library (e.g. net-utils-device-common-bpf) and applies its jarjar
+  rules after build.
+
+* Each module creates a native library in their directory, which statically links against the
+  common native library (e.g. libnet_utils_device_common_bpf), and calls the native registered
+  function by hardcoding the post-jarjar class_name.
+
+
diff --git a/staticlibs/native/bpf_syscall_wrappers/Android.bp b/staticlibs/native/bpf_syscall_wrappers/Android.bp
index 136342c..1416b6b 100644
--- a/staticlibs/native/bpf_syscall_wrappers/Android.bp
+++ b/staticlibs/native/bpf_syscall_wrappers/Android.bp
@@ -33,6 +33,7 @@
         "com.android.tethering",
     ],
     visibility: [
+        "//frameworks/libs/net/common/native/bpfmapjni",
         "//packages/modules/Connectivity/service",
         "//packages/modules/Connectivity/Tethering",
         "//system/bpf/libbpf_android",
diff --git a/staticlibs/native/bpfmapjni/Android.bp b/staticlibs/native/bpfmapjni/Android.bp
new file mode 100644
index 0000000..b7af22d
--- /dev/null
+++ b/staticlibs/native/bpfmapjni/Android.bp
@@ -0,0 +1,44 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_static {
+    name: "libnet_utils_device_common_bpfjni",
+    srcs: ["com_android_net_module_util_BpfMap.cpp"],
+    header_libs: [
+        "bpf_syscall_wrappers",
+        "jni_headers",
+    ],
+    shared_libs: [
+        "liblog",
+        "libnativehelper_compat_libc++",
+    ],
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+    ],
+    sdk_version: "30",
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
+        "//apex_available:platform",
+    ],
+    visibility: [
+        "//packages/modules/Connectivity:__subpackages__",
+    ],
+}
diff --git a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
new file mode 100644
index 0000000..e25e17d
--- /dev/null
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#include <errno.h>
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+
+#include "nativehelper/scoped_primitive_array.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+#define BPF_FD_JUST_USE_INT
+#include "BpfSyscallWrappers.h"
+
+namespace android {
+
+static jint com_android_net_module_util_BpfMap_closeMap(JNIEnv *env, jobject clazz,
+        jint fd) {
+    int ret = close(fd);
+
+    if (ret) jniThrowErrnoException(env, "closeMap", errno);
+
+    return ret;
+}
+
+static jint com_android_net_module_util_BpfMap_bpfFdGet(JNIEnv *env, jobject clazz,
+        jstring path, jint mode) {
+    ScopedUtfChars pathname(env, path);
+
+    jint fd = bpf::bpfFdGet(pathname.c_str(), static_cast<unsigned>(mode));
+
+    if (fd < 0) jniThrowErrnoException(env, "bpfFdGet", errno);
+
+    return fd;
+}
+
+static void com_android_net_module_util_BpfMap_writeToMapEntry(JNIEnv *env, jobject clazz,
+        jint fd, jbyteArray key, jbyteArray value, jint flags) {
+    ScopedByteArrayRO keyRO(env, key);
+    ScopedByteArrayRO valueRO(env, value);
+
+    int ret = bpf::writeToMapEntry(static_cast<int>(fd), keyRO.get(), valueRO.get(),
+            static_cast<int>(flags));
+
+    if (ret) jniThrowErrnoException(env, "writeToMapEntry", errno);
+}
+
+static jboolean throwIfNotEnoent(JNIEnv *env, const char* functionName, int ret, int err) {
+    if (ret == 0) return true;
+
+    if (err != ENOENT) jniThrowErrnoException(env, functionName, err);
+    return false;
+}
+
+static jboolean com_android_net_module_util_BpfMap_deleteMapEntry(JNIEnv *env, jobject clazz,
+        jint fd, jbyteArray key) {
+    ScopedByteArrayRO keyRO(env, key);
+
+    // On success, zero is returned.  If the element is not found, -1 is returned and errno is set
+    // to ENOENT.
+    int ret = bpf::deleteMapEntry(static_cast<int>(fd), keyRO.get());
+
+    return throwIfNotEnoent(env, "deleteMapEntry", ret, errno);
+}
+
+static jboolean com_android_net_module_util_BpfMap_getNextMapKey(JNIEnv *env, jobject clazz,
+        jint fd, jbyteArray key, jbyteArray nextKey) {
+    // If key is found, the operation returns zero and sets the next key pointer to the key of the
+    // next element.  If key is not found, the operation returns zero and sets the next key pointer
+    // to the key of the first element.  If key is the last element, -1 is returned and errno is
+    // set to ENOENT.  Other possible errno values are ENOMEM, EFAULT, EPERM, and EINVAL.
+    ScopedByteArrayRW nextKeyRW(env, nextKey);
+    int ret;
+    if (key == nullptr) {
+        // Called by getFirstKey. Find the first key in the map.
+        ret = bpf::getNextMapKey(static_cast<int>(fd), nullptr, nextKeyRW.get());
+    } else {
+        ScopedByteArrayRO keyRO(env, key);
+        ret = bpf::getNextMapKey(static_cast<int>(fd), keyRO.get(), nextKeyRW.get());
+    }
+
+    return throwIfNotEnoent(env, "getNextMapKey", ret, errno);
+}
+
+static jboolean com_android_net_module_util_BpfMap_findMapEntry(JNIEnv *env, jobject clazz,
+        jint fd, jbyteArray key, jbyteArray value) {
+    ScopedByteArrayRO keyRO(env, key);
+    ScopedByteArrayRW valueRW(env, value);
+
+    // If an element is found, the operation returns zero and stores the element's value into
+    // "value".  If no element is found, the operation returns -1 and sets errno to ENOENT.
+    int ret = bpf::findMapEntry(static_cast<int>(fd), keyRO.get(), valueRW.get());
+
+    return throwIfNotEnoent(env, "findMapEntry", ret, errno);
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+    /* name, signature, funcPtr */
+    { "closeMap", "(I)I",
+        (void*) com_android_net_module_util_BpfMap_closeMap },
+    { "bpfFdGet", "(Ljava/lang/String;I)I",
+        (void*) com_android_net_module_util_BpfMap_bpfFdGet },
+    { "writeToMapEntry", "(I[B[BI)V",
+        (void*) com_android_net_module_util_BpfMap_writeToMapEntry },
+    { "deleteMapEntry", "(I[B)Z",
+        (void*) com_android_net_module_util_BpfMap_deleteMapEntry },
+    { "getNextMapKey", "(I[B[B)Z",
+        (void*) com_android_net_module_util_BpfMap_getNextMapKey },
+    { "findMapEntry", "(I[B[B)Z",
+        (void*) com_android_net_module_util_BpfMap_findMapEntry },
+
+};
+
+int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name) {
+    return jniRegisterNativeMethods(env,
+            class_name,
+            gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index a9f9d70..07a8200 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -17,6 +17,7 @@
         "androidx.test.rules",
         "mockito-target-extended-minus-junit4",
         "net-utils-device-common",
+        "net-utils-device-common-bpf",
         "net-tests-utils",
         "netd-client",
     ],
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/JniUtilTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/JniUtilTest.kt
new file mode 100644
index 0000000..7574087
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/JniUtilTest.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 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 androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+public final class JniUtilTest {
+    private val TEST_JAVA_UTIL_NAME = "java_util_jni"
+    private val TEST_ORG_JUNIT_NAME = "org_junit_jni"
+
+    @Test
+    fun testGetJniLibraryName() {
+        assertEquals(TEST_JAVA_UTIL_NAME,
+                JniUtil.getJniLibraryName(java.util.Set::class.java.getPackage()))
+        assertEquals(TEST_ORG_JUNIT_NAME,
+                JniUtil.getJniLibraryName(org.junit.Before::class.java.getPackage()))
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
new file mode 100644
index 0000000..392314f
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2021 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.netlink;
+
+import static android.system.OsConstants.NETLINK_ROUTE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RtNetlinkRouteMessageTest {
+    private static final IpPrefix TEST_IPV6_GLOBAL_PREFIX = new IpPrefix("2001:db8:1::/64");
+    private static final Inet6Address TEST_IPV6_LINK_LOCAL_GATEWAY =
+            (Inet6Address) InetAddresses.parseNumericAddress("fe80::1");
+
+    // An example of the full RTM_NEWROUTE message.
+    private static final String RTM_NEWROUTE_HEX =
+            "88000000180000060000000000000000"            // struct nlmsghr
+            + "0A400000FC02000100000000"                  // struct rtmsg
+            + "08000F00C7060000"                          // RTA_TABLE
+            + "1400010020010DB8000100000000000000000000"  // RTA_DST
+            + "08000400DF020000"                          // RTA_OIF
+            + "0800060000010000"                          // RTA_PRIORITY
+            + "24000C0000000000000000005EEA000000000000"  // RTA_CACHEINFO
+            + "00000000000000000000000000000000"
+            + "14000500FE800000000000000000000000000001"  // RTA_GATEWAY
+            + "0500140000000000";                         // RTA_PREF
+
+    private ByteBuffer toByteBuffer(final String hexString) {
+        return ByteBuffer.wrap(HexDump.hexStringToByteArray(hexString));
+    }
+
+    @Test
+    public void testParseRtmRouteAddress() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkRouteMessage);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+        final StructNlMsgHdr hdr = routeMsg.getHeader();
+        assertNotNull(hdr);
+        assertEquals(136, hdr.nlmsg_len);
+        assertEquals(NetlinkConstants.RTM_NEWROUTE, hdr.nlmsg_type);
+        assertEquals(0x600, hdr.nlmsg_flags);
+        assertEquals(0, hdr.nlmsg_seq);
+        assertEquals(0, hdr.nlmsg_pid);
+
+        final StructRtMsg rtmsg = routeMsg.getRtMsgHeader();
+        assertNotNull(rtmsg);
+        assertEquals((byte) OsConstants.AF_INET6, rtmsg.family);
+        assertEquals(64, rtmsg.dstLen);
+        assertEquals(0, rtmsg.srcLen);
+        assertEquals(0, rtmsg.tos);
+        assertEquals(0xFC, rtmsg.table);
+        assertEquals(NetlinkConstants.RTPROT_KERNEL, rtmsg.protocol);
+        assertEquals(NetlinkConstants.RT_SCOPE_UNIVERSE, rtmsg.scope);
+        assertEquals(NetlinkConstants.RTN_UNICAST, rtmsg.type);
+        assertEquals(0, rtmsg.flags);
+
+        assertEquals(routeMsg.getDestination(), TEST_IPV6_GLOBAL_PREFIX);
+        assertEquals(735, routeMsg.getInterfaceIndex());
+        assertEquals((Inet6Address) routeMsg.getGateway(), TEST_IPV6_LINK_LOCAL_GATEWAY);
+    }
+
+    private static final String RTM_NEWROUTE_PACK_HEX =
+            "4C000000180000060000000000000000"             // struct nlmsghr
+            + "0A400000FC02000100000000"                   // struct rtmsg
+            + "1400010020010DB8000100000000000000000000"   // RTA_DST
+            + "14000500FE800000000000000000000000000001"   // RTA_GATEWAY
+            + "08000400DF020000";                          // RTA_OIF
+
+    @Test
+    public void testPackRtmNewRoute() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_PACK_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkRouteMessage);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+        final ByteBuffer packBuffer = ByteBuffer.allocate(76);
+        packBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        routeMsg.pack(packBuffer);
+        assertEquals(RTM_NEWROUTE_PACK_HEX, HexDump.toHexString(packBuffer.array()));
+    }
+
+    private static final String RTM_NEWROUTE_TRUNCATED_HEX =
+            "48000000180000060000000000000000"             // struct nlmsghr
+            + "0A400000FC02000100000000"                   // struct rtmsg
+            + "1400010020010DB8000100000000000000000000"   // RTA_DST
+            + "10000500FE8000000000000000000000"           // RTA_GATEWAY(truncated)
+            + "08000400DF020000";                          // RTA_OIF
+
+    @Test
+    public void testTruncatedRtmNewRoute() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_TRUNCATED_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        // Parsing RTM_NEWROUTE with truncated RTA_GATEWAY attribute returns null.
+        assertNull(msg);
+    }
+
+    private static final String RTM_NEWROUTE_IPV4_MAPPED_IPV6_GATEWAY_HEX =
+            "4C000000180000060000000000000000"             // struct nlmsghr
+            + "0A400000FC02000100000000"                   // struct rtmsg
+            + "1400010020010DB8000100000000000000000000"   // RTA_DST(2001:db8:1::/64)
+            + "1400050000000000000000000000FFFF0A010203"   // RTA_GATEWAY(::ffff:10.1.2.3)
+            + "08000400DF020000";                          // RTA_OIF
+
+    @Test
+    public void testParseRtmRouteAddress_IPv4MappedIPv6Gateway() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_IPV4_MAPPED_IPV6_GATEWAY_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        // Parsing RTM_NEWROUTE with IPv4-mapped IPv6 gateway address, which doesn't match
+        // rtm_family after address parsing.
+        assertNull(msg);
+    }
+
+    private static final String RTM_NEWROUTE_IPV4_MAPPED_IPV6_DST_HEX =
+            "4C000000180000060000000000000000"             // struct nlmsghr
+            + "0A780000FC02000100000000"                   // struct rtmsg
+            + "1400010000000000000000000000FFFF0A000000"   // RTA_DST(::ffff:10.0.0.0/120)
+            + "14000500FE800000000000000000000000000001"   // RTA_GATEWAY(fe80::1)
+            + "08000400DF020000";                          // RTA_OIF
+
+    @Test
+    public void testParseRtmRouteAddress_IPv4MappedIPv6Destination() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_IPV4_MAPPED_IPV6_DST_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        // Parsing RTM_NEWROUTE with IPv4-mapped IPv6 destination prefix, which doesn't match
+        // rtm_family after address parsing.
+        assertNull(msg);
+    }
+
+    @Test
+    public void testToString() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkRouteMessage);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+        final String expected = "RtNetlinkRouteMessage{ "
+                + "nlmsghdr{"
+                + "StructNlMsgHdr{ nlmsg_len{136}, nlmsg_type{24(RTM_NEWROUTE)}, "
+                + "nlmsg_flags{1536(NLM_F_MATCH)}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+                + "Rtmsg{"
+                + "family: 10, dstLen: 64, srcLen: 0, tos: 0, table: 252, protocol: 2, "
+                + "scope: 0, type: 1, flags: 0}, "
+                + "destination{2001:db8:1::}, "
+                + "gateway{fe80::1}, "
+                + "ifindex{735} "
+                + "}";
+        assertEquals(expected, routeMsg.toString());
+    }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index b7297bb..9fd30f7 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -61,3 +61,14 @@
       "kotlin-test"
   ]
 }
+
+java_test_host {
+    name: "net-tests-utils-host-common",
+    srcs: [
+        "host/**/*.java",
+        "host/**/*.kt",
+    ],
+    libs: ["tradefed"],
+    test_suites: ["device-tests", "general-tests", "cts", "mts"],
+    data: [":ConnectivityChecker"],
+}
diff --git a/staticlibs/testutils/app/connectivitychecker/Android.bp b/staticlibs/testutils/app/connectivitychecker/Android.bp
new file mode 100644
index 0000000..79a4343
--- /dev/null
+++ b/staticlibs/testutils/app/connectivitychecker/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+    name: "ConnectivityChecker",
+    srcs: ["src/**/*.kt"],
+    sdk_version: "system_current",
+    // Allow running the test on any device with SDK Q+, even when built from a branch that uses
+    // an unstable SDK, by targeting a stable SDK regardless of the build SDK.
+    min_sdk_version: "29",
+    target_sdk_version: "30",
+    static_libs: [
+        "androidx.test.rules",
+        "modules-utils-build_system",
+        "net-tests-utils",
+    ],
+    host_required: ["net-tests-utils-host-common"],
+}
diff --git a/staticlibs/testutils/app/connectivitychecker/AndroidManifest.xml b/staticlibs/testutils/app/connectivitychecker/AndroidManifest.xml
new file mode 100644
index 0000000..8e5958c
--- /dev/null
+++ b/staticlibs/testutils/app/connectivitychecker/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.testutils.connectivitychecker">
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <!-- For wifi scans -->
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.testutils.connectivitychecker"
+                     android:label="Connectivity checker target preparer" />
+</manifest>
diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitychecker/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitychecker/ConnectivityCheckTest.kt
new file mode 100644
index 0000000..43b130b
--- /dev/null
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitychecker/ConnectivityCheckTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 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.connectivitychecker
+
+import android.content.pm.PackageManager.FEATURE_TELEPHONY
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.telephony.TelephonyManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.ConnectUtil
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+@RunWith(AndroidJUnit4::class)
+class ConnectivityCheckTest {
+    val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+    val pm by lazy { context.packageManager }
+
+    @Test
+    fun testCheckDeviceSetup() {
+        checkWifiSetup()
+        checkTelephonySetup()
+    }
+
+    private fun checkWifiSetup() {
+        if (!pm.hasSystemFeature(FEATURE_WIFI)) return
+        ConnectUtil(context).ensureWifiConnected()
+    }
+
+    private fun checkTelephonySetup() {
+        if (!pm.hasSystemFeature(FEATURE_TELEPHONY)) return
+        val tm = context.getSystemService(TelephonyManager::class.java)
+                ?: fail("Could not get telephony service")
+
+        val commonError = "Check the test bench. To run the tests anyway for quick & dirty local " +
+                "testing, you can use atest X -- " +
+                "--test-arg com.android.testutils.ConnectivityCheckTargetPreparer:disable:true"
+        // Do not use assertEquals: it outputs "expected X, was Y", which looks like a test failure
+        if (tm.simState == TelephonyManager.SIM_STATE_ABSENT) {
+            fail("The device has no SIM card inserted. " + commonError)
+        } else if (tm.simState != TelephonyManager.SIM_STATE_READY) {
+            fail("The device is not setup with a usable SIM card. Sim state was ${tm.simState}. " +
+                    commonError)
+        }
+        assertTrue(tm.isDataConnectivityPossible,
+            "The device is not setup with a SIM card that supports data connectivity. " +
+                    commonError)
+    }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
new file mode 100644
index 0000000..fc951d8
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2021 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
+
+import android.Manifest.permission
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.wifi.ScanResult
+import android.net.wifi.WifiConfiguration
+import android.net.wifi.WifiManager
+import android.os.ParcelFileDescriptor
+import android.os.SystemClock
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private const val MAX_WIFI_CONNECT_RETRIES = 10
+private const val WIFI_CONNECT_INTERVAL_MS = 500L
+private const val WIFI_CONNECT_TIMEOUT_MS = 30_000L
+
+// Constants used by WifiManager.ActionListener#onFailure. Although onFailure is SystemApi,
+// the error code constants are not (b/204277752)
+private const val WIFI_ERROR_IN_PROGRESS = 1
+private const val WIFI_ERROR_BUSY = 2
+
+class ConnectUtil(private val context: Context) {
+    private val TAG = ConnectUtil::class.java.simpleName
+
+    private val cm = context.getSystemService(ConnectivityManager::class.java)
+            ?: fail("Could not find ConnectivityManager")
+    private val wifiManager = context.getSystemService(WifiManager::class.java)
+            ?: fail("Could not find WifiManager")
+
+    fun ensureWifiConnected(): Network {
+        val callback = TestableNetworkCallback()
+        cm.registerNetworkCallback(NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .build(), callback)
+
+        try {
+            val connInfo = wifiManager.connectionInfo
+            if (connInfo == null || connInfo.networkId == -1) {
+                clearWifiBlocklist()
+                val pfd = getInstrumentation().uiAutomation.executeShellCommand("svc wifi enable")
+                // Read the output stream to ensure the command has completed
+                ParcelFileDescriptor.AutoCloseInputStream(pfd).use { it.readBytes() }
+                val config = getOrCreateWifiConfiguration()
+                connectToWifiConfig(config)
+            }
+            val cb = callback.eventuallyExpectOrNull<RecorderCallback.CallbackEntry.Available>(
+                    timeoutMs = WIFI_CONNECT_TIMEOUT_MS)
+
+            assertNotNull(cb, "Could not connect to a wifi access point within " +
+                    "$WIFI_CONNECT_INTERVAL_MS ms. Check that the test device has a wifi network " +
+                    "configured, and that the test access point is functioning properly.")
+            return cb.network
+        } finally {
+            cm.unregisterNetworkCallback(callback)
+        }
+    }
+
+    private fun connectToWifiConfig(config: WifiConfiguration) {
+        repeat(MAX_WIFI_CONNECT_RETRIES) {
+            val error = runAsShell(permission.NETWORK_SETTINGS) {
+                val listener = ConnectWifiListener()
+                wifiManager.connect(config, listener)
+                listener.connectFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+            } ?: return // Connect succeeded
+
+            // Only retry for IN_PROGRESS and BUSY
+            if (error != WIFI_ERROR_IN_PROGRESS && error != WIFI_ERROR_BUSY) {
+                fail("Failed to connect to " + config.SSID + ": " + error)
+            }
+            Log.w(TAG, "connect failed with $error; waiting before retry")
+            SystemClock.sleep(WIFI_CONNECT_INTERVAL_MS)
+        }
+        fail("Failed to connect to ${config.SSID} after $MAX_WIFI_CONNECT_RETRIES retries")
+    }
+
+    private class ConnectWifiListener : WifiManager.ActionListener {
+        /**
+         * Future completed when the connect process ends. Provides the error code or null if none.
+         */
+        val connectFuture = CompletableFuture<Int?>()
+        override fun onSuccess() {
+            connectFuture.complete(null)
+        }
+
+        override fun onFailure(reason: Int) {
+            connectFuture.complete(reason)
+        }
+    }
+
+    private fun getOrCreateWifiConfiguration(): WifiConfiguration {
+        val configs = runAsShell(permission.NETWORK_SETTINGS) {
+            wifiManager.getConfiguredNetworks()
+        }
+        // If no network is configured, add a config for virtual access points if applicable
+        if (configs.size == 0) {
+            val scanResults = getWifiScanResults()
+            val virtualConfig = maybeConfigureVirtualNetwork(scanResults)
+            assertNotNull(virtualConfig, "The device has no configured wifi network")
+            return virtualConfig
+        }
+        // No need to add a configuration: there is already one.
+        if (configs.size > 1) {
+            // For convenience in case of local testing on devices with multiple saved configs,
+            // prefer the first configuration that is in range.
+            // In actual tests, there should only be one configuration, and it should be usable as
+            // assumed by WifiManagerTest.testConnect.
+            Log.w(TAG, "Multiple wifi configurations found: " +
+                    configs.joinToString(", ") { it.SSID })
+            val scanResultsList = getWifiScanResults()
+            Log.i(TAG, "Scan results: " + scanResultsList.joinToString(", ") {
+                "${it.SSID} (${it.level})"
+            })
+
+            val scanResults = scanResultsList.map { "\"${it.SSID}\"" }.toSet()
+            return configs.firstOrNull { scanResults.contains(it.SSID) } ?: configs[0]
+        }
+        return configs[0]
+    }
+
+    private fun getWifiScanResults(): List<ScanResult> {
+        val scanResultsFuture = CompletableFuture<List<ScanResult>>()
+        runAsShell(permission.NETWORK_SETTINGS) {
+            val receiver: BroadcastReceiver = object : BroadcastReceiver() {
+                override fun onReceive(context: Context, intent: Intent) {
+                    scanResultsFuture.complete(wifiManager.scanResults)
+                }
+            }
+            context.registerReceiver(receiver,
+                    IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
+            wifiManager.startScan()
+        }
+        return try {
+            scanResultsFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+        } catch (e: Exception) {
+            throw AssertionError("Wifi scan results not received within timeout", e)
+        }
+    }
+
+    /**
+     * If a virtual wifi network is detected, add a configuration for that network.
+     * TODO(b/158150376): have the test infrastructure add virtual wifi networks when appropriate.
+     */
+    private fun maybeConfigureVirtualNetwork(scanResults: List<ScanResult>): WifiConfiguration? {
+        // Virtual wifi networks used on the emulator and cloud testing infrastructure
+        val virtualSsids = listOf("VirtWifi", "AndroidWifi")
+        Log.d(TAG, "Wifi scan results: $scanResults")
+        val virtualScanResult = scanResults.firstOrNull { virtualSsids.contains(it.SSID) }
+                ?: return null
+
+        // Only add the virtual configuration if the virtual AP is detected in scans
+        val virtualConfig = WifiConfiguration()
+        // ASCII SSIDs need to be surrounded by double quotes
+        virtualConfig.SSID = "\"${virtualScanResult.SSID}\""
+        virtualConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
+        runAsShell(permission.NETWORK_SETTINGS) {
+            val networkId = wifiManager.addNetwork(virtualConfig)
+            assertTrue(networkId >= 0)
+            assertTrue(wifiManager.enableNetwork(networkId, false /* attemptConnect */))
+        }
+        return virtualConfig
+    }
+
+    /**
+     * Re-enable wifi networks that were blocked, typically because no internet connection was
+     * detected the last time they were connected. This is necessary to make sure wifi can reconnect
+     * to them.
+     */
+    private fun clearWifiBlocklist() {
+        runAsShell(permission.NETWORK_SETTINGS, permission.ACCESS_WIFI_STATE) {
+            for (cfg in wifiManager.configuredNetworks) {
+                assertTrue(wifiManager.enableNetwork(cfg.networkId, false /* attemptConnect */))
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/host/com/android/testutils/ConnectivityCheckTargetPreparer.kt b/staticlibs/testutils/host/com/android/testutils/ConnectivityCheckTargetPreparer.kt
new file mode 100644
index 0000000..85589ad
--- /dev/null
+++ b/staticlibs/testutils/host/com/android/testutils/ConnectivityCheckTargetPreparer.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2021 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
+
+import com.android.ddmlib.testrunner.TestResult
+import com.android.tradefed.invoker.TestInformation
+import com.android.tradefed.result.CollectingTestListener
+import com.android.tradefed.result.ddmlib.DefaultRemoteAndroidTestRunner
+import com.android.tradefed.targetprep.BaseTargetPreparer
+import com.android.tradefed.targetprep.suite.SuiteApkInstaller
+
+private const val CONNECTIVITY_CHECKER_APK = "ConnectivityChecker.apk"
+private const val CONNECTIVITY_PKG_NAME = "com.android.testutils.connectivitychecker"
+// As per the <instrumentation> defined in the checker manifest
+private const val CONNECTIVITY_CHECK_RUNNER_NAME = "androidx.test.runner.AndroidJUnitRunner"
+
+/**
+ * A target preparer that verifies that the device was setup correctly for connectivity tests.
+ *
+ * For quick and dirty local testing, can be disabled by running tests with
+ * "atest -- --test-arg com.android.testutils.ConnectivityCheckTargetPreparer:disable:true".
+ */
+class ConnectivityCheckTargetPreparer : BaseTargetPreparer() {
+    val installer = SuiteApkInstaller()
+
+    override fun setUp(testInformation: TestInformation) {
+        if (isDisabled) return
+        installer.setCleanApk(true)
+        installer.addTestFileName(CONNECTIVITY_CHECKER_APK)
+        installer.setShouldGrantPermission(true)
+        installer.setUp(testInformation)
+
+        val runner = DefaultRemoteAndroidTestRunner(
+                CONNECTIVITY_PKG_NAME,
+                CONNECTIVITY_CHECK_RUNNER_NAME,
+                testInformation.device.iDevice)
+        runner.runOptions = "--no-hidden-api-checks"
+
+        val receiver = CollectingTestListener()
+        if (!testInformation.device.runInstrumentationTests(runner, receiver)) {
+            throw AssertionError("Device state check failed to complete")
+        }
+
+        val runResult = receiver.currentRunResults
+        if (runResult.isRunFailure) {
+            throw AssertionError("Failed to check device state before the test: " +
+                    runResult.runFailureMessage)
+        }
+
+        if (!runResult.hasFailedTests()) return
+        val errorMsg = runResult.testResults.mapNotNull { (testDescription, testResult) ->
+            if (TestResult.TestStatus.FAILURE != testResult.status) null
+            else "$testDescription: ${testResult.stackTrace}"
+        }.joinToString("\n")
+
+        throw AssertionError("Device setup checks failed. Check the test bench: \n$errorMsg")
+    }
+
+    override fun tearDown(testInformation: TestInformation?, e: Throwable?) {
+        if (isTearDownDisabled) return
+        installer.tearDown(testInformation, e)
+    }
+}
\ No newline at end of file