Merge "Add data structures to parse netlink route messages."
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index a7a027c..f1e7c85 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -38,12 +38,12 @@
"device/com/android/net/module/util/FdEventsReader.java",
"device/com/android/net/module/util/HexDump.java",
"device/com/android/net/module/util/PacketReader.java",
+ "device/com/android/net/module/util/SharedLog.java",
// This library is used by system modules, for which the system health impact of Kotlin
// has not yet been evaluated. Annotations may need jarjar'ing.
// "src_devicecommon/**/*.kt",
- ":framework-annotations",
],
- sdk_version: "system_current",
+ sdk_version: "module_current",
min_sdk_version: "29",
target_sdk_version: "30",
apex_available: [
@@ -68,6 +68,7 @@
],
libs: [
"androidx.annotation_annotation",
+ "framework-annotations-lib",
],
}
@@ -95,10 +96,11 @@
java_library {
name: "net-utils-device-common-netlink",
- // TODO: Ipv6Utils and Struct stuff could be separated out of th netlink library into
+ // 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.
srcs: [
+ "device/com/android/net/module/util/BpfMap.java",
"device/com/android/net/module/util/HexDump.java",
"device/com/android/net/module/util/Ipv6Utils.java",
"device/com/android/net/module/util/Struct.java",
@@ -125,6 +127,36 @@
}
java_library {
+ // TODO : this target should probably be folded into net-utils-device-common
+ name: "net-utils-device-common-ip",
+ srcs: [
+ "device/com/android/net/module/util/ip/*.java",
+ ],
+ sdk_version: "module_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__",
+ ],
+ libs: [
+ "framework-annotations-lib",
+ "framework-connectivity",
+ ],
+ static_libs: [
+ "net-utils-device-common",
+ "net-utils-device-common-netlink",
+ "net-utils-framework-common",
+ "netd-client",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ "//apex_available:platform",
+ ],
+}
+
+java_library {
name: "net-utils-framework-common",
srcs: [
":net-utils-framework-common-srcs",
@@ -170,13 +202,16 @@
name: "net-utils-services-common",
srcs: [
":net-utils-services-common-srcs",
- ":framework-annotations",
],
- sdk_version: "system_current",
+ sdk_version: "module_current",
min_sdk_version: "30",
static_libs: [
"modules-utils-build_system",
],
+ libs: [
+ "framework-annotations-lib",
+ "framework-connectivity",
+ ],
visibility: [
"//frameworks/base/services/net",
],
@@ -205,7 +240,11 @@
filegroup {
name: "net-utils-framework-wifi-common-srcs",
srcs: [
- "framework/com/android/net/module/util/**/*.java",
+ "framework/com/android/net/module/util/DnsSdTxtRecord.java",
+ "framework/com/android/net/module/util/Inet4AddressUtils.java",
+ "framework/com/android/net/module/util/InetAddressUtils.java",
+ "framework/com/android/net/module/util/MacAddressUtils.java",
+ "framework/com/android/net/module/util/NetUtils.java",
],
path: "framework",
visibility: [
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..aa74152
--- /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("tetherutilsjni");
+ }
+
+ // 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/SharedLog.java b/staticlibs/device/com/android/net/module/util/SharedLog.java
new file mode 100644
index 0000000..37c6f6d
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/SharedLog.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2017 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.annotation.NonNull;
+import android.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.time.LocalDateTime;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.StringJoiner;
+
+
+/**
+ * Class to centralize logging functionality for tethering.
+ *
+ * All access to class methods other than dump() must be on the same thread.
+ *
+ * @hide
+ */
+public class SharedLog {
+ private static final int DEFAULT_MAX_RECORDS = 500;
+ private static final String COMPONENT_DELIMITER = ".";
+
+ private enum Category {
+ NONE,
+ ERROR,
+ MARK,
+ WARN,
+ }
+
+ private final LocalLog mLocalLog;
+ // The tag to use for output to the system log. This is not output to the
+ // LocalLog because that would be redundant.
+ private final String mTag;
+ // The component (or subcomponent) of a system that is sharing this log.
+ // This can grow in depth if components call forSubComponent() to obtain
+ // their SharedLog instance. The tag is not included in the component for
+ // brevity.
+ private final String mComponent;
+
+ public SharedLog(String tag) {
+ this(DEFAULT_MAX_RECORDS, tag);
+ }
+
+ public SharedLog(int maxRecords, String tag) {
+ this(new LocalLog(maxRecords), tag, tag);
+ }
+
+ private SharedLog(LocalLog localLog, String tag, String component) {
+ mLocalLog = localLog;
+ mTag = tag;
+ mComponent = component;
+ }
+
+ public String getTag() {
+ return mTag;
+ }
+
+ /**
+ * Create a SharedLog based on this log with an additional component prefix on each logged line.
+ */
+ public SharedLog forSubComponent(String component) {
+ if (!isRootLogInstance()) {
+ component = mComponent + COMPONENT_DELIMITER + component;
+ }
+ return new SharedLog(mLocalLog, mTag, component);
+ }
+
+ /**
+ * Dump the contents of this log.
+ *
+ * <p>This method may be called on any thread.
+ */
+ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ mLocalLog.dump(writer);
+ }
+
+ //////
+ // Methods that both log an entry and emit it to the system log.
+ //////
+
+ /**
+ * Log an error due to an exception. This does not include the exception stacktrace.
+ *
+ * <p>The log entry will be also added to the system log.
+ * @see #e(String, Throwable)
+ */
+ public void e(Exception e) {
+ Log.e(mTag, record(Category.ERROR, e.toString()));
+ }
+
+ /**
+ * Log an error message.
+ *
+ * <p>The log entry will be also added to the system log.
+ */
+ public void e(String msg) {
+ Log.e(mTag, record(Category.ERROR, msg));
+ }
+
+ /**
+ * Log an error due to an exception, with the exception stacktrace if provided.
+ *
+ * <p>The error and exception message appear in the shared log, but the stacktrace is only
+ * logged in general log output (logcat). The log entry will be also added to the system log.
+ */
+ public void e(@NonNull String msg, @Nullable Throwable exception) {
+ if (exception == null) {
+ e(msg);
+ return;
+ }
+ Log.e(mTag, record(Category.ERROR, msg + ": " + exception.getMessage()), exception);
+ }
+
+ /**
+ * Log an informational message.
+ *
+ * <p>The log entry will be also added to the system log.
+ */
+ public void i(String msg) {
+ Log.i(mTag, record(Category.NONE, msg));
+ }
+
+ /**
+ * Log a warning message.
+ *
+ * <p>The log entry will be also added to the system log.
+ */
+ public void w(String msg) {
+ Log.w(mTag, record(Category.WARN, msg));
+ }
+
+ //////
+ // Methods that only log an entry (and do NOT emit to the system log).
+ //////
+
+ /**
+ * Log a general message to be only included in the in-memory log.
+ *
+ * <p>The log entry will *not* be added to the system log.
+ */
+ public void log(String msg) {
+ record(Category.NONE, msg);
+ }
+
+ /**
+ * Log a general, formatted message to be only included in the in-memory log.
+ *
+ * <p>The log entry will *not* be added to the system log.
+ * @see String#format(String, Object...)
+ */
+ public void logf(String fmt, Object... args) {
+ log(String.format(fmt, args));
+ }
+
+ /**
+ * Log a message with MARK level.
+ *
+ * <p>The log entry will *not* be added to the system log.
+ */
+ public void mark(String msg) {
+ record(Category.MARK, msg);
+ }
+
+ private String record(Category category, String msg) {
+ final String entry = logLine(category, msg);
+ mLocalLog.append(entry);
+ return entry;
+ }
+
+ private String logLine(Category category, String msg) {
+ final StringJoiner sj = new StringJoiner(" ");
+ if (!isRootLogInstance()) sj.add("[" + mComponent + "]");
+ if (category != Category.NONE) sj.add(category.toString());
+ return sj.add(msg).toString();
+ }
+
+ // Check whether this SharedLog instance is nominally the top level in
+ // a potential hierarchy of shared logs (the root of a tree),
+ // or is a subcomponent within the hierarchy.
+ private boolean isRootLogInstance() {
+ return TextUtils.isEmpty(mComponent) || mComponent.equals(mTag);
+ }
+
+ private static final class LocalLog {
+ private final Deque<String> mLog;
+ private final int mMaxLines;
+
+ LocalLog(int maxLines) {
+ mMaxLines = Math.max(0, maxLines);
+ mLog = new ArrayDeque<>(mMaxLines);
+ }
+
+ synchronized void append(String logLine) {
+ if (mMaxLines <= 0) return;
+ while (mLog.size() >= mMaxLines) {
+ mLog.remove();
+ }
+ mLog.add(LocalDateTime.now() + " - " + logLine);
+ }
+
+ /**
+ * Dumps the content of local log to print writer with each log entry
+ *
+ * @param pw printer writer to write into
+ */
+ synchronized void dump(PrintWriter pw) {
+ for (final String s : mLog) {
+ pw.println(s);
+ }
+ }
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ip/ConntrackMonitor.java b/staticlibs/device/com/android/net/module/util/ip/ConntrackMonitor.java
new file mode 100644
index 0000000..420a544
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ip/ConntrackMonitor.java
@@ -0,0 +1,203 @@
+/*
+ * 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.ip;
+
+import static com.android.net.module.util.netlink.ConntrackMessage.DYING_MASK;
+import static com.android.net.module.util.netlink.ConntrackMessage.ESTABLISHED_MASK;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+import android.system.OsConstants;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.netlink.ConntrackMessage;
+import com.android.net.module.util.netlink.NetlinkConstants;
+import com.android.net.module.util.netlink.NetlinkMessage;
+
+import java.util.Objects;
+
+
+/**
+ * ConntrackMonitor.
+ *
+ * Monitors the netfilter conntrack notifications and presents to callers
+ * ConntrackEvents describing each event.
+ *
+ * @hide
+ */
+public class ConntrackMonitor extends NetlinkMonitor {
+ private static final String TAG = ConntrackMonitor.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ // Reference kernel/uapi/linux/netfilter/nfnetlink_compat.h
+ public static final int NF_NETLINK_CONNTRACK_NEW = 1;
+ public static final int NF_NETLINK_CONNTRACK_UPDATE = 2;
+ public static final int NF_NETLINK_CONNTRACK_DESTROY = 4;
+
+ // The socket receive buffer size in bytes. If too many conntrack messages are sent too
+ // quickly, the conntrack messages can overflow the socket receive buffer. This can happen
+ // if too many connections are disconnected by losing network and so on. Use a large-enough
+ // buffer to avoid the error ENOBUFS while listening to the conntrack messages.
+ private static final int SOCKET_RECV_BUFSIZE = 6 * 1024 * 1024;
+
+ /**
+ * A class for describing parsed netfilter conntrack events.
+ */
+ public static class ConntrackEvent {
+ /**
+ * Conntrack event type.
+ */
+ public final short msgType;
+ /**
+ * Original direction conntrack tuple.
+ */
+ public final ConntrackMessage.Tuple tupleOrig;
+ /**
+ * Reply direction conntrack tuple.
+ */
+ public final ConntrackMessage.Tuple tupleReply;
+ /**
+ * Connection status. A bitmask of ip_conntrack_status enum flags.
+ */
+ public final int status;
+ /**
+ * Conntrack timeout.
+ */
+ public final int timeoutSec;
+
+ public ConntrackEvent(ConntrackMessage msg) {
+ this.msgType = msg.getHeader().nlmsg_type;
+ this.tupleOrig = msg.tupleOrig;
+ this.tupleReply = msg.tupleReply;
+ this.status = msg.status;
+ this.timeoutSec = msg.timeoutSec;
+ }
+
+ @VisibleForTesting
+ public ConntrackEvent(short msgType, ConntrackMessage.Tuple tupleOrig,
+ ConntrackMessage.Tuple tupleReply, int status, int timeoutSec) {
+ this.msgType = msgType;
+ this.tupleOrig = tupleOrig;
+ this.tupleReply = tupleReply;
+ this.status = status;
+ this.timeoutSec = timeoutSec;
+ }
+
+ @Override
+ @VisibleForTesting
+ public boolean equals(Object o) {
+ if (!(o instanceof ConntrackEvent)) return false;
+ ConntrackEvent that = (ConntrackEvent) o;
+ return this.msgType == that.msgType
+ && Objects.equals(this.tupleOrig, that.tupleOrig)
+ && Objects.equals(this.tupleReply, that.tupleReply)
+ && this.status == that.status
+ && this.timeoutSec == that.timeoutSec;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(msgType, tupleOrig, tupleReply, status, timeoutSec);
+ }
+
+ @Override
+ public String toString() {
+ return "ConntrackEvent{"
+ + "msg_type{"
+ + NetlinkConstants.stringForNlMsgType(msgType, OsConstants.NETLINK_NETFILTER)
+ + "}, "
+ + "tuple_orig{" + tupleOrig + "}, "
+ + "tuple_reply{" + tupleReply + "}, "
+ + "status{"
+ + status + "(" + ConntrackMessage.stringForIpConntrackStatus(status) + ")"
+ + "}, "
+ + "timeout_sec{" + Integer.toUnsignedLong(timeoutSec) + "}"
+ + "}";
+ }
+
+ /**
+ * Check the established NAT session conntrack message.
+ *
+ * @param msg the conntrack message to check.
+ * @return true if an established NAT message, false if not.
+ */
+ public static boolean isEstablishedNatSession(@NonNull ConntrackMessage msg) {
+ if (msg.getMessageType() != NetlinkConstants.IPCTNL_MSG_CT_NEW) return false;
+ if (msg.tupleOrig == null) return false;
+ if (msg.tupleReply == null) return false;
+ if (msg.timeoutSec == 0) return false;
+ if ((msg.status & ESTABLISHED_MASK) != ESTABLISHED_MASK) return false;
+
+ return true;
+ }
+
+ /**
+ * Check the dying NAT session conntrack message.
+ * Note that IPCTNL_MSG_CT_DELETE event has no CTA_TIMEOUT attribute.
+ *
+ * @param msg the conntrack message to check.
+ * @return true if a dying NAT message, false if not.
+ */
+ public static boolean isDyingNatSession(@NonNull ConntrackMessage msg) {
+ if (msg.getMessageType() != NetlinkConstants.IPCTNL_MSG_CT_DELETE) return false;
+ if (msg.tupleOrig == null) return false;
+ if (msg.tupleReply == null) return false;
+ if (msg.timeoutSec != 0) return false;
+ if ((msg.status & DYING_MASK) != DYING_MASK) return false;
+
+ return true;
+ }
+ }
+
+ /**
+ * A callback to caller for conntrack event.
+ */
+ public interface ConntrackEventConsumer {
+ /**
+ * Every conntrack event received on the netlink socket is passed in
+ * here.
+ */
+ void accept(@NonNull ConntrackEvent event);
+ }
+
+ private final ConntrackEventConsumer mConsumer;
+
+ public ConntrackMonitor(@NonNull Handler h, @NonNull SharedLog log,
+ @NonNull ConntrackEventConsumer cb) {
+ super(h, log, TAG, OsConstants.NETLINK_NETFILTER, NF_NETLINK_CONNTRACK_NEW
+ | NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY, SOCKET_RECV_BUFSIZE);
+ mConsumer = cb;
+ }
+
+ @Override
+ public void processNetlinkMessage(NetlinkMessage nlMsg, final long whenMs) {
+ if (!(nlMsg instanceof ConntrackMessage)) {
+ mLog.e("non-conntrack msg: " + nlMsg);
+ return;
+ }
+
+ final ConntrackMessage conntrackMsg = (ConntrackMessage) nlMsg;
+ if (!(ConntrackEvent.isEstablishedNatSession(conntrackMsg)
+ || ConntrackEvent.isDyingNatSession(conntrackMsg))) {
+ return;
+ }
+
+ mConsumer.accept(new ConntrackEvent(conntrackMsg));
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ip/InterfaceController.java b/staticlibs/device/com/android/net/module/util/ip/InterfaceController.java
new file mode 100644
index 0000000..7277fec
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ip/InterfaceController.java
@@ -0,0 +1,211 @@
+/*
+ * 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 com.android.net.module.util.ip;
+
+import static android.net.INetd.IF_STATE_DOWN;
+import static android.net.INetd.IF_STATE_UP;
+
+import android.net.INetd;
+import android.net.InterfaceConfigurationParcel;
+import android.net.LinkAddress;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.system.OsConstants;
+
+import com.android.net.module.util.SharedLog;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+
+/**
+ * Encapsulates the multiple IP configuration operations performed on an interface.
+ *
+ * TODO: refactor/eliminate the redundant ways to set and clear addresses.
+ *
+ * @hide
+ */
+public class InterfaceController {
+ private static final boolean DBG = false;
+
+ private final String mIfName;
+ private final INetd mNetd;
+ private final SharedLog mLog;
+
+ public InterfaceController(String ifname, INetd netd, SharedLog log) {
+ mIfName = ifname;
+ mNetd = netd;
+ mLog = log;
+ }
+
+ /**
+ * Set the IPv4 address and also optionally bring the interface up or down.
+ */
+ public boolean setInterfaceConfiguration(final LinkAddress ipv4Addr,
+ final Boolean setIfaceUp) {
+ if (!(ipv4Addr.getAddress() instanceof Inet4Address)) {
+ throw new IllegalArgumentException("Invalid or mismatched Inet4Address");
+ }
+ // Note: currently netd only support INetd#IF_STATE_UP and #IF_STATE_DOWN.
+ // Other flags would be ignored.
+
+ final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel();
+ ifConfig.ifName = mIfName;
+ ifConfig.ipv4Addr = ipv4Addr.getAddress().getHostAddress();
+ ifConfig.prefixLength = ipv4Addr.getPrefixLength();
+ // Netd ignores hwaddr in interfaceSetCfg.
+ ifConfig.hwAddr = "";
+ if (setIfaceUp == null) {
+ // Empty array means no change.
+ ifConfig.flags = new String[0];
+ } else {
+ // Netd ignores any flag that's not IF_STATE_UP or IF_STATE_DOWN in interfaceSetCfg.
+ ifConfig.flags = setIfaceUp.booleanValue()
+ ? new String[] {IF_STATE_UP} : new String[] {IF_STATE_DOWN};
+ }
+ try {
+ mNetd.interfaceSetCfg(ifConfig);
+ } catch (RemoteException | ServiceSpecificException e) {
+ logError("Setting IPv4 address to %s/%d failed: %s",
+ ifConfig.ipv4Addr, ifConfig.prefixLength, e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Set the IPv4 address of the interface.
+ */
+ public boolean setIPv4Address(final LinkAddress address) {
+ return setInterfaceConfiguration(address, null);
+ }
+
+ /**
+ * Clear the IPv4Address of the interface.
+ */
+ public boolean clearIPv4Address() {
+ return setIPv4Address(new LinkAddress("0.0.0.0/0"));
+ }
+
+ private boolean setEnableIPv6(boolean enabled) {
+ try {
+ mNetd.interfaceSetEnableIPv6(mIfName, enabled);
+ } catch (RemoteException | ServiceSpecificException e) {
+ logError("%s IPv6 failed: %s", (enabled ? "enabling" : "disabling"), e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Enable IPv6 on the interface.
+ */
+ public boolean enableIPv6() {
+ return setEnableIPv6(true);
+ }
+
+ /**
+ * Disable IPv6 on the interface.
+ */
+ public boolean disableIPv6() {
+ return setEnableIPv6(false);
+ }
+
+ /**
+ * Enable or disable IPv6 privacy extensions on the interface.
+ * @param enabled Whether the extensions should be enabled.
+ */
+ public boolean setIPv6PrivacyExtensions(boolean enabled) {
+ try {
+ mNetd.interfaceSetIPv6PrivacyExtensions(mIfName, enabled);
+ } catch (RemoteException | ServiceSpecificException e) {
+ logError("error %s IPv6 privacy extensions: %s",
+ (enabled ? "enabling" : "disabling"), e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Set IPv6 address generation mode on the interface.
+ *
+ * <p>IPv6 should be disabled before changing the mode.
+ */
+ public boolean setIPv6AddrGenModeIfSupported(int mode) {
+ try {
+ mNetd.setIPv6AddrGenMode(mIfName, mode);
+ } catch (RemoteException e) {
+ logError("Unable to set IPv6 addrgen mode: %s", e);
+ return false;
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode != OsConstants.EOPNOTSUPP) {
+ logError("Unable to set IPv6 addrgen mode: %s", e);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Add an address to the interface.
+ */
+ public boolean addAddress(LinkAddress addr) {
+ return addAddress(addr.getAddress(), addr.getPrefixLength());
+ }
+
+ /**
+ * Add an address to the interface.
+ */
+ public boolean addAddress(InetAddress ip, int prefixLen) {
+ try {
+ mNetd.interfaceAddAddress(mIfName, ip.getHostAddress(), prefixLen);
+ } catch (ServiceSpecificException | RemoteException e) {
+ logError("failed to add %s/%d: %s", ip, prefixLen, e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Remove an address from the interface.
+ */
+ public boolean removeAddress(InetAddress ip, int prefixLen) {
+ try {
+ mNetd.interfaceDelAddress(mIfName, ip.getHostAddress(), prefixLen);
+ } catch (ServiceSpecificException | RemoteException e) {
+ logError("failed to remove %s/%d: %s", ip, prefixLen, e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Remove all addresses from the interface.
+ */
+ public boolean clearAllAddresses() {
+ try {
+ mNetd.interfaceClearAddrs(mIfName);
+ } catch (Exception e) {
+ logError("Failed to clear addresses: %s", e);
+ return false;
+ }
+ return true;
+ }
+
+ private void logError(String fmt, Object... args) {
+ mLog.e(String.format(fmt, args));
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ip/IpNeighborMonitor.java b/staticlibs/device/com/android/net/module/util/ip/IpNeighborMonitor.java
new file mode 100644
index 0000000..88f8c9d
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ip/IpNeighborMonitor.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2017 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.ip;
+
+import static android.system.OsConstants.NETLINK_ROUTE;
+
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELNEIGH;
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+import static com.android.net.module.util.netlink.NetlinkConstants.stringForNlMsgType;
+
+import android.net.MacAddress;
+import android.os.Handler;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.util.Log;
+
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkSocket;
+import com.android.net.module.util.netlink.RtNetlinkNeighborMessage;
+import com.android.net.module.util.netlink.StructNdMsg;
+
+import java.net.InetAddress;
+import java.util.StringJoiner;
+
+/**
+ * IpNeighborMonitor.
+ *
+ * Monitors the kernel rtnetlink neighbor notifications and presents to callers
+ * NeighborEvents describing each event. Callers can provide a consumer instance
+ * to both filter (e.g. by interface index and IP address) and handle the
+ * generated NeighborEvents.
+ *
+ * @hide
+ */
+public class IpNeighborMonitor extends NetlinkMonitor {
+ private static final String TAG = IpNeighborMonitor.class.getSimpleName();
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ /**
+ * Make the kernel perform neighbor reachability detection (IPv4 ARP or IPv6 ND)
+ * for the given IP address on the specified interface index.
+ *
+ * @return 0 if the request was successfully passed to the kernel; otherwise return
+ * a non-zero error code.
+ */
+ public static int startKernelNeighborProbe(int ifIndex, InetAddress ip) {
+ final String msgSnippet = "probing ip=" + ip.getHostAddress() + "%" + ifIndex;
+ if (DBG) Log.d(TAG, msgSnippet);
+
+ final byte[] msg = RtNetlinkNeighborMessage.newNewNeighborMessage(
+ 1, ip, StructNdMsg.NUD_PROBE, ifIndex, null);
+
+ try {
+ NetlinkSocket.sendOneShotKernelMessage(NETLINK_ROUTE, msg);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Error " + msgSnippet + ": " + e);
+ return -e.errno;
+ }
+
+ return 0;
+ }
+
+ /**
+ * An event about a neighbor.
+ */
+ public static class NeighborEvent {
+ final long elapsedMs;
+ final short msgType;
+ final int ifindex;
+ final InetAddress ip;
+ final short nudState;
+ final MacAddress macAddr;
+
+ public NeighborEvent(long elapsedMs, short msgType, int ifindex, InetAddress ip,
+ short nudState, MacAddress macAddr) {
+ this.elapsedMs = elapsedMs;
+ this.msgType = msgType;
+ this.ifindex = ifindex;
+ this.ip = ip;
+ this.nudState = nudState;
+ this.macAddr = macAddr;
+ }
+
+ boolean isConnected() {
+ return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateConnected(nudState);
+ }
+
+ boolean isValid() {
+ return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateValid(nudState);
+ }
+
+ @Override
+ public String toString() {
+ final StringJoiner j = new StringJoiner(",", "NeighborEvent{", "}");
+ return j.add("@" + elapsedMs)
+ .add(stringForNlMsgType(msgType, NETLINK_ROUTE))
+ .add("if=" + ifindex)
+ .add(ip.getHostAddress())
+ .add(StructNdMsg.stringForNudState(nudState))
+ .add("[" + macAddr + "]")
+ .toString();
+ }
+ }
+
+ /**
+ * A client that consumes NeighborEvent instances.
+ * Implement this to listen to neighbor events.
+ */
+ public interface NeighborEventConsumer {
+ // Every neighbor event received on the netlink socket is passed in
+ // here. Subclasses should filter for events of interest.
+ /**
+ * Consume a neighbor event
+ * @param event the event
+ */
+ void accept(NeighborEvent event);
+ }
+
+ private final NeighborEventConsumer mConsumer;
+
+ public IpNeighborMonitor(Handler h, SharedLog log, NeighborEventConsumer cb) {
+ super(h, log, TAG, NETLINK_ROUTE, OsConstants.RTMGRP_NEIGH);
+ mConsumer = (cb != null) ? cb : (event) -> { /* discard */ };
+ }
+
+ @Override
+ public void processNetlinkMessage(NetlinkMessage nlMsg, final long whenMs) {
+ if (!(nlMsg instanceof RtNetlinkNeighborMessage)) {
+ mLog.e("non-rtnetlink neighbor msg: " + nlMsg);
+ return;
+ }
+
+ final RtNetlinkNeighborMessage neighMsg = (RtNetlinkNeighborMessage) nlMsg;
+ final short msgType = neighMsg.getHeader().nlmsg_type;
+ final StructNdMsg ndMsg = neighMsg.getNdHeader();
+ if (ndMsg == null) {
+ mLog.e("RtNetlinkNeighborMessage without ND message header!");
+ return;
+ }
+
+ final int ifindex = ndMsg.ndm_ifindex;
+ final InetAddress destination = neighMsg.getDestination();
+ final short nudState =
+ (msgType == RTM_DELNEIGH)
+ ? StructNdMsg.NUD_NONE
+ : ndMsg.ndm_state;
+
+ final NeighborEvent event = new NeighborEvent(
+ whenMs, msgType, ifindex, destination, nudState,
+ getMacAddress(neighMsg.getLinkLayerAddress()));
+
+ if (VDBG) {
+ Log.d(TAG, neighMsg.toString());
+ }
+ if (DBG) {
+ Log.d(TAG, event.toString());
+ }
+
+ mConsumer.accept(event);
+ }
+
+ private static MacAddress getMacAddress(byte[] linkLayerAddress) {
+ if (linkLayerAddress != null) {
+ try {
+ return MacAddress.fromBytes(linkLayerAddress);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed to parse link-layer address: " + hexify(linkLayerAddress));
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java b/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java
new file mode 100644
index 0000000..8589876
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/ip/NetlinkMonitor.java
@@ -0,0 +1,166 @@
+/*
+ * 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.ip;
+
+import static android.net.util.SocketUtils.makeNetlinkSocketAddress;
+import static android.system.OsConstants.AF_NETLINK;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_RCVBUF;
+
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+
+import android.annotation.NonNull;
+import android.net.util.SocketUtils;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import com.android.net.module.util.PacketReader;
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.netlink.NetlinkErrorMessage;
+import com.android.net.module.util.netlink.NetlinkMessage;
+import com.android.net.module.util.netlink.NetlinkSocket;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * A simple base class to listen for netlink broadcasts.
+ *
+ * Opens a netlink socket of the given family and binds to the specified groups. Polls the socket
+ * from the event loop of the passed-in {@link Handler}, and calls the subclass-defined
+ * {@link #processNetlinkMessage} method on the handler thread for each netlink message that
+ * arrives. Currently ignores all netlink errors.
+ * @hide
+ */
+public class NetlinkMonitor extends PacketReader {
+ protected final SharedLog mLog;
+ protected final String mTag;
+ private final int mFamily;
+ private final int mBindGroups;
+ private final int mSockRcvbufSize;
+
+ private static final boolean DBG = false;
+
+ // Default socket receive buffer size. This means the specific buffer size is not set.
+ private static final int DEFAULT_SOCKET_RECV_BUFSIZE = -1;
+
+ /**
+ * Constructs a new {@code NetlinkMonitor} instance.
+ *
+ * @param h The Handler on which to poll for messages and on which to call
+ * {@link #processNetlinkMessage}.
+ * @param log A SharedLog to log to.
+ * @param tag The log tag to use for log messages.
+ * @param family the Netlink socket family to, e.g., {@code NETLINK_ROUTE}.
+ * @param bindGroups the netlink groups to bind to.
+ * @param sockRcvbufSize the specific socket receive buffer size in bytes. -1 means that don't
+ * set the specific socket receive buffer size in #createFd and use the default value in
+ * /proc/sys/net/core/rmem_default file. See SO_RCVBUF in man-pages/socket.
+ */
+ public NetlinkMonitor(@NonNull Handler h, @NonNull SharedLog log, @NonNull String tag,
+ int family, int bindGroups, int sockRcvbufSize) {
+ super(h, NetlinkSocket.DEFAULT_RECV_BUFSIZE);
+ mLog = log.forSubComponent(tag);
+ mTag = tag;
+ mFamily = family;
+ mBindGroups = bindGroups;
+ mSockRcvbufSize = sockRcvbufSize;
+ }
+
+ public NetlinkMonitor(@NonNull Handler h, @NonNull SharedLog log, @NonNull String tag,
+ int family, int bindGroups) {
+ this(h, log, tag, family, bindGroups, DEFAULT_SOCKET_RECV_BUFSIZE);
+ }
+
+ @Override
+ protected FileDescriptor createFd() {
+ FileDescriptor fd = null;
+
+ try {
+ fd = Os.socket(AF_NETLINK, SOCK_DGRAM | SOCK_NONBLOCK, mFamily);
+ if (mSockRcvbufSize != DEFAULT_SOCKET_RECV_BUFSIZE) {
+ Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, mSockRcvbufSize);
+ }
+ Os.bind(fd, makeNetlinkSocketAddress(0, mBindGroups));
+ NetlinkSocket.connectToKernel(fd);
+
+ if (DBG) {
+ final SocketAddress nlAddr = Os.getsockname(fd);
+ Log.d(mTag, "bound to sockaddr_nl{" + nlAddr.toString() + "}");
+ }
+ } catch (ErrnoException | SocketException e) {
+ logError("Failed to create rtnetlink socket", e);
+ closeSocketQuietly(fd);
+ return null;
+ }
+
+ return fd;
+ }
+
+ @Override
+ protected void handlePacket(byte[] recvbuf, int length) {
+ final long whenMs = SystemClock.elapsedRealtime();
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(recvbuf, 0, length);
+ byteBuffer.order(ByteOrder.nativeOrder());
+
+ while (byteBuffer.remaining() > 0) {
+ try {
+ final int position = byteBuffer.position();
+ final NetlinkMessage nlMsg = NetlinkMessage.parse(byteBuffer, mFamily);
+ if (nlMsg == null || nlMsg.getHeader() == null) {
+ byteBuffer.position(position);
+ mLog.e("unparsable netlink msg: " + hexify(byteBuffer));
+ break;
+ }
+
+ if (nlMsg instanceof NetlinkErrorMessage) {
+ mLog.e("netlink error: " + nlMsg);
+ continue;
+ }
+
+ processNetlinkMessage(nlMsg, whenMs);
+ } catch (Exception e) {
+ mLog.e("Error handling netlink message", e);
+ }
+ }
+ }
+
+ // TODO: move NetworkStackUtils to frameworks/libs/net for NetworkStackUtils#closeSocketQuietly.
+ private void closeSocketQuietly(FileDescriptor fd) {
+ try {
+ SocketUtils.closeSocket(fd);
+ } catch (IOException ignored) {
+ }
+ }
+
+ /**
+ * Processes one netlink message. Must be overridden by subclasses.
+ * @param nlMsg the message to process.
+ * @param whenMs the timestamp, as measured by {@link SystemClock#elapsedRealtime}, when the
+ * message was received.
+ */
+ protected void processNetlinkMessage(NetlinkMessage nlMsg, long whenMs) { }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/BinderUtils.java b/staticlibs/framework/com/android/net/module/util/BinderUtils.java
new file mode 100644
index 0000000..eb695d1
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/BinderUtils.java
@@ -0,0 +1,59 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.os.Binder;
+
+/**
+ * Collection of utilities for {@link Binder} and related classes.
+ * @hide
+ */
+public class BinderUtils {
+ /**
+ * Convenience method for running the provided action enclosed in
+ * {@link Binder#clearCallingIdentity}/{@link Binder#restoreCallingIdentity}
+ *
+ * Any exception thrown by the given action will be caught and rethrown after the call to
+ * {@link Binder#restoreCallingIdentity}
+ *
+ * Note that this is copied from Binder#withCleanCallingIdentity with minor changes
+ * since it is not public.
+ *
+ * @hide
+ */
+ public static final <T extends Exception> void withCleanCallingIdentity(
+ @NonNull ThrowingRunnable<T> action) throws T {
+ final long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ action.run();
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ }
+
+ /**
+ * Like a Runnable, but declared to throw an exception.
+ *
+ * @param <T> The exception class which is declared to be thrown.
+ */
+ @FunctionalInterface
+ public interface ThrowingRunnable<T extends Exception> {
+ /** @see java.lang.Runnable */
+ void run() throws T;
+ }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/InterfaceParams.java b/staticlibs/framework/com/android/net/module/util/InterfaceParams.java
new file mode 100644
index 0000000..30762eb
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/InterfaceParams.java
@@ -0,0 +1,105 @@
+/*
+ * 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 com.android.net.module.util;
+
+import android.net.MacAddress;
+import android.text.TextUtils;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
+
+
+/**
+ * Encapsulate the interface parameters common to IpClient/IpServer components.
+ *
+ * Basically all java.net.NetworkInterface methods throw Exceptions. IpClient
+ * and IpServer (sub)components need most or all of this information at some
+ * point during their lifecycles, so pass only this simplified object around
+ * which can be created once when IpClient/IpServer are told to start.
+ *
+ * @hide
+ */
+public class InterfaceParams {
+ public final String name;
+ public final int index;
+ public final boolean hasMacAddress;
+ public final MacAddress macAddr;
+ public final int defaultMtu;
+
+ // TODO: move the below to NetworkStackConstants when this class is moved to the NetworkStack.
+ private static final int ETHER_MTU = 1500;
+ private static final int IPV6_MIN_MTU = 1280;
+
+
+ /**
+ * Return InterfaceParams corresponding with an interface name
+ * @param name the interface name
+ */
+ public static InterfaceParams getByName(String name) {
+ final NetworkInterface netif = getNetworkInterfaceByName(name);
+ if (netif == null) return null;
+
+ // Not all interfaces have MAC addresses, e.g. rmnet_data0.
+ final MacAddress macAddr = getMacAddress(netif);
+
+ try {
+ return new InterfaceParams(name, netif.getIndex(), macAddr, netif.getMTU());
+ } catch (IllegalArgumentException | SocketException e) {
+ return null;
+ }
+ }
+
+ public InterfaceParams(String name, int index, MacAddress macAddr) {
+ this(name, index, macAddr, ETHER_MTU);
+ }
+
+ public InterfaceParams(String name, int index, MacAddress macAddr, int defaultMtu) {
+ if (TextUtils.isEmpty(name)) {
+ throw new IllegalArgumentException("impossible interface name");
+ }
+
+ if (index <= 0) throw new IllegalArgumentException("invalid interface index");
+
+ this.name = name;
+ this.index = index;
+ this.hasMacAddress = (macAddr != null);
+ this.macAddr = hasMacAddress ? macAddr : MacAddress.fromBytes(new byte[] {
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 });
+ this.defaultMtu = (defaultMtu > IPV6_MIN_MTU) ? defaultMtu : IPV6_MIN_MTU;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s/%d/%s/%d", name, index, macAddr, defaultMtu);
+ }
+
+ private static NetworkInterface getNetworkInterfaceByName(String name) {
+ try {
+ return NetworkInterface.getByName(name);
+ } catch (NullPointerException | SocketException e) {
+ return null;
+ }
+ }
+
+ private static MacAddress getMacAddress(NetworkInterface netif) {
+ try {
+ return MacAddress.fromBytes(netif.getHardwareAddress());
+ } catch (IllegalArgumentException | NullPointerException | SocketException e) {
+ return null;
+ }
+ }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/RouteUtils.java b/staticlibs/framework/com/android/net/module/util/RouteUtils.java
new file mode 100644
index 0000000..c241680
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/RouteUtils.java
@@ -0,0 +1,23 @@
+/*
+ * 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 com.android.net.module.util;
+
+/** @hide */
+// RouteUtils is now empty, because some new methods will be added to it soon and it is less
+// expensive to keep it empty than to remove it now and add it again later.
+public class RouteUtils {
+}
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..edbae7c
--- /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: "libbpfmapjni",
+ 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/Tethering",
+ ],
+}
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 23835fe..a9f9d70 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -13,10 +13,12 @@
defaults: ["framework-connectivity-test-defaults"],
static_libs: [
"net-utils-framework-common",
+ "net-utils-device-common-ip",
"androidx.test.rules",
"mockito-target-extended-minus-junit4",
"net-utils-device-common",
"net-tests-utils",
+ "netd-client",
],
libs: [
"android.test.runner",
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java
new file mode 100644
index 0000000..446e881
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/SharedLogTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2017 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.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SharedLogTest {
+ private static final String TIMESTAMP_PATTERN = "\\d{2}:\\d{2}:\\d{2}";
+ private static final String TIMESTAMP = "HH:MM:SS";
+
+ @Test
+ public void testBasicOperation() {
+ final SharedLog logTop = new SharedLog("top");
+ logTop.mark("first post!");
+
+ final SharedLog logLevel2a = logTop.forSubComponent("twoA");
+ final SharedLog logLevel2b = logTop.forSubComponent("twoB");
+ logLevel2b.e("2b or not 2b");
+ logLevel2b.e("No exception", null);
+ logLevel2b.e("Wait, here's one", new Exception("Test"));
+ logLevel2a.w("second post?");
+
+ final SharedLog logLevel3 = logLevel2a.forSubComponent("three");
+ logTop.log("still logging");
+ logLevel3.log("3 >> 2");
+ logLevel2a.mark("ok: last post");
+
+ final String[] expected = {
+ " - MARK first post!",
+ " - [twoB] ERROR 2b or not 2b",
+ " - [twoB] ERROR No exception",
+ // No stacktrace in shared log, only in logcat
+ " - [twoB] ERROR Wait, here's one: Test",
+ " - [twoA] WARN second post?",
+ " - still logging",
+ " - [twoA.three] 3 >> 2",
+ " - [twoA] MARK ok: last post",
+ };
+ // Verify the logs are all there and in the correct order.
+ verifyLogLines(expected, logTop);
+
+ // In fact, because they all share the same underlying LocalLog,
+ // every subcomponent SharedLog's dump() is identical.
+ verifyLogLines(expected, logLevel2a);
+ verifyLogLines(expected, logLevel2b);
+ verifyLogLines(expected, logLevel3);
+ }
+
+ private static void verifyLogLines(String[] expected, SharedLog log) {
+ final ByteArrayOutputStream ostream = new ByteArrayOutputStream();
+ final PrintWriter pw = new PrintWriter(ostream, true);
+ log.dump(null, pw, null);
+
+ final String dumpOutput = ostream.toString();
+ assertTrue(dumpOutput != null);
+ assertTrue(!"".equals(dumpOutput));
+
+ final String[] lines = dumpOutput.split("\n");
+ assertEquals(expected.length, lines.length);
+
+ for (int i = 0; i < expected.length; i++) {
+ String got = lines[i];
+ String want = expected[i];
+ assertTrue(String.format("'%s' did not contain '%s'", got, want), got.endsWith(want));
+ assertTrue(String.format("'%s' did not contain a %s timestamp", got, TIMESTAMP),
+ got.replaceFirst(TIMESTAMP_PATTERN, TIMESTAMP).contains(TIMESTAMP));
+ }
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/ip/ConntrackMonitorTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/ip/ConntrackMonitorTest.java
new file mode 100644
index 0000000..99d8f07
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/ip/ConntrackMonitorTest.java
@@ -0,0 +1,336 @@
+/*
+ * 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.ip;
+
+import static android.system.OsConstants.AF_UNIX;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import static com.android.net.module.util.netlink.ConntrackMessage.Tuple;
+import static com.android.net.module.util.netlink.ConntrackMessage.TupleIpv4;
+import static com.android.net.module.util.netlink.ConntrackMessage.TupleProto;
+import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_DELETE;
+import static com.android.net.module.util.netlink.NetlinkConstants.IPCTNL_MSG_CT_NEW;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.net.InetAddresses;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.SharedLog;
+import com.android.net.module.util.ip.ConntrackMonitor.ConntrackEvent;
+import com.android.net.module.util.netlink.NetlinkConstants;
+import com.android.net.module.util.netlink.NetlinkSocket;
+
+import libcore.util.HexEncoding;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileDescriptor;
+import java.io.InterruptedIOException;
+import java.net.Inet4Address;
+
+/**
+ * Tests for ConntrackMonitor.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ConntrackMonitorTest {
+ private static final long TIMEOUT_MS = 10_000L;
+
+ @Mock private SharedLog mLog;
+ @Mock private ConntrackMonitor.ConntrackEventConsumer mConsumer;
+
+ private final HandlerThread mHandlerThread = new HandlerThread(
+ ConntrackMonitorTest.class.getSimpleName());
+
+ // Late init since the handler thread has been started.
+ private Handler mHandler;
+ private TestConntrackMonitor mConntrackMonitor;
+
+ // A version of [ConntrackMonitor] that reads packets from the socket pair, and instead
+ // allows the test to write test packets to the socket pair via [sendMessage].
+ private class TestConntrackMonitor extends ConntrackMonitor {
+ TestConntrackMonitor(@NonNull Handler h, @NonNull SharedLog log,
+ @NonNull ConntrackEventConsumer cb) {
+ super(h, log, cb);
+
+ mReadFd = new FileDescriptor();
+ mWriteFd = new FileDescriptor();
+ try {
+ Os.socketpair(AF_UNIX, SOCK_DGRAM, 0, mWriteFd, mReadFd);
+ } catch (ErrnoException e) {
+ fail("Could not create socket pair: " + e);
+ }
+ }
+
+ @Override
+ protected FileDescriptor createFd() {
+ return mReadFd;
+ }
+
+ private void sendMessage(byte[] msg) {
+ mHandler.post(() -> {
+ try {
+ NetlinkSocket.sendMessage(mWriteFd, msg, 0 /* offset */, msg.length,
+ TIMEOUT_MS);
+ } catch (ErrnoException | InterruptedIOException e) {
+ fail("Unable to send netfilter message: " + e);
+ }
+ });
+ }
+
+ private final FileDescriptor mReadFd;
+ private final FileDescriptor mWriteFd;
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+
+ // ConntrackMonitor needs to be started from the handler thread.
+ final ConditionVariable initDone = new ConditionVariable();
+ mHandler.post(() -> {
+ TestConntrackMonitor m = new TestConntrackMonitor(mHandler, mLog, mConsumer);
+ m.start();
+ mConntrackMonitor = m;
+
+ initDone.open();
+ });
+ if (!initDone.block(TIMEOUT_MS)) {
+ fail("... init monitor timed-out after " + TIMEOUT_MS + "ms");
+ }
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mHandlerThread.quitSafely();
+ }
+
+ public static final String CT_V4NEW_TCP_HEX =
+ // CHECKSTYLE:OFF IndentationCheck
+ // struct nlmsghdr
+ "8C000000" + // length = 140
+ "0001" + // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_NEW (0)
+ "0006" + // flags = NLM_F_CREATE (1 << 10) | NLM_F_EXCL (1 << 9)
+ "00000000" + // seqno = 0
+ "00000000" + // pid = 0
+ // struct nfgenmsg
+ "02" + // nfgen_family = AF_INET
+ "00" + // version = NFNETLINK_V0
+ "1234" + // res_id = 0x1234 (big endian)
+ // struct nlattr
+ "3400" + // nla_len = 52
+ "0180" + // nla_type = nested CTA_TUPLE_ORIG
+ // struct nlattr
+ "1400" + // nla_len = 20
+ "0180" + // nla_type = nested CTA_TUPLE_IP
+ "0800 0100 C0A8500C" + // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
+ "0800 0200 8C700874" + // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
+ // struct nlattr
+ "1C00" + // nla_len = 28
+ "0280" + // nla_type = nested CTA_TUPLE_PROTO
+ "0500 0100 06 000000" + // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+ "0600 0200 F3F1 0000" + // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
+ "0600 0300 01BB 0000" + // nla_type=CTA_PROTO_DST_PORT, port=443 (big endian)
+ // struct nlattr
+ "3400" + // nla_len = 52
+ "0280" + // nla_type = nested CTA_TUPLE_REPLY
+ // struct nlattr
+ "1400" + // nla_len = 20
+ "0180" + // nla_type = nested CTA_TUPLE_IP
+ "0800 0100 8C700874" + // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
+ "0800 0200 6451B301" + // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
+ // struct nlattr
+ "1C00" + // nla_len = 28
+ "0280" + // nla_type = nested CTA_TUPLE_PROTO
+ "0500 0100 06 000000" + // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+ "0600 0200 01BB 0000" + // nla_type=CTA_PROTO_SRC_PORT, port=443 (big endian)
+ "0600 0300 F3F1 0000" + // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
+ // struct nlattr
+ "0800" + // nla_len = 8
+ "0300" + // nla_type = CTA_STATUS
+ "0000019e" + // nla_value = 0b110011110 (big endian)
+ // IPS_SEEN_REPLY (1 << 1) | IPS_ASSURED (1 << 2) |
+ // IPS_CONFIRMED (1 << 3) | IPS_SRC_NAT (1 << 4) |
+ // IPS_SRC_NAT_DONE (1 << 7) | IPS_DST_NAT_DONE (1 << 8)
+ // struct nlattr
+ "0800" + // nla_len = 8
+ "0700" + // nla_type = CTA_TIMEOUT
+ "00000078"; // nla_value = 120 (big endian)
+ // CHECKSTYLE:ON IndentationCheck
+ public static final byte[] CT_V4NEW_TCP_BYTES =
+ HexEncoding.decode(CT_V4NEW_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
+
+ @NonNull
+ private ConntrackEvent makeTestConntrackEvent(short msgType, int status, int timeoutSec) {
+ final Inet4Address privateIp =
+ (Inet4Address) InetAddresses.parseNumericAddress("192.168.80.12");
+ final Inet4Address remoteIp =
+ (Inet4Address) InetAddresses.parseNumericAddress("140.112.8.116");
+ final Inet4Address publicIp =
+ (Inet4Address) InetAddresses.parseNumericAddress("100.81.179.1");
+
+ return new ConntrackEvent(
+ (short) (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8 | msgType),
+ new Tuple(new TupleIpv4(privateIp, remoteIp),
+ new TupleProto((byte) IPPROTO_TCP, (short) 62449, (short) 443)),
+ new Tuple(new TupleIpv4(remoteIp, publicIp),
+ new TupleProto((byte) IPPROTO_TCP, (short) 443, (short) 62449)),
+ status,
+ timeoutSec);
+ }
+
+ @Test
+ public void testConntrackEventNew() throws Exception {
+ final ConntrackEvent expectedEvent = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW,
+ 0x19e /* status */, 120 /* timeoutSec */);
+ mConntrackMonitor.sendMessage(CT_V4NEW_TCP_BYTES);
+ verify(mConsumer, timeout(TIMEOUT_MS)).accept(eq(expectedEvent));
+ }
+
+ @Test
+ public void testConntrackEventEquals() {
+ final ConntrackEvent event1 = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+ 5678 /* timeoutSec*/);
+ final ConntrackEvent event2 = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+ 5678 /* timeoutSec*/);
+ assertEquals(event1, event2);
+ }
+
+ @Test
+ public void testConntrackEventNotEquals() {
+ final ConntrackEvent e = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW, 1234 /* status */,
+ 5678 /* timeoutSec*/);
+
+ final ConntrackEvent typeNotEqual = new ConntrackEvent((short) (e.msgType + 1) /* diff */,
+ e.tupleOrig, e.tupleReply, e.status, e.timeoutSec);
+ assertNotEquals(e, typeNotEqual);
+
+ final ConntrackEvent tupleOrigNotEqual = new ConntrackEvent(e.msgType,
+ null /* diff */, e.tupleReply, e.status, e.timeoutSec);
+ assertNotEquals(e, tupleOrigNotEqual);
+
+ final ConntrackEvent tupleReplyNotEqual = new ConntrackEvent(e.msgType,
+ e.tupleOrig, null /* diff */, e.status, e.timeoutSec);
+ assertNotEquals(e, tupleReplyNotEqual);
+
+ final ConntrackEvent statusNotEqual = new ConntrackEvent(e.msgType,
+ e.tupleOrig, e.tupleReply, e.status + 1 /* diff */, e.timeoutSec);
+ assertNotEquals(e, statusNotEqual);
+
+ final ConntrackEvent timeoutSecNotEqual = new ConntrackEvent(e.msgType,
+ e.tupleOrig, e.tupleReply, e.status, e.timeoutSec + 1 /* diff */);
+ assertNotEquals(e, timeoutSecNotEqual);
+ }
+
+ @Test
+ public void testToString() {
+ final ConntrackEvent event = makeTestConntrackEvent(IPCTNL_MSG_CT_NEW,
+ 0x198 /* status */, 120 /* timeoutSec */);
+ final String expected = ""
+ + "ConntrackEvent{"
+ + "msg_type{IPCTNL_MSG_CT_NEW}, "
+ + "tuple_orig{Tuple{IPPROTO_TCP: 192.168.80.12:62449 -> 140.112.8.116:443}}, "
+ + "tuple_reply{Tuple{IPPROTO_TCP: 140.112.8.116:443 -> 100.81.179.1:62449}}, "
+ + "status{408(IPS_CONFIRMED|IPS_SRC_NAT|IPS_SRC_NAT_DONE|IPS_DST_NAT_DONE)}, "
+ + "timeout_sec{120}}";
+ assertEquals(expected, event.toString());
+ }
+
+ public static final String CT_V4DELETE_TCP_HEX =
+ // CHECKSTYLE:OFF IndentationCheck
+ // struct nlmsghdr
+ "84000000" + // length = 132
+ "0201" + // type = NFNL_SUBSYS_CTNETLINK (1) << 8 | IPCTNL_MSG_CT_DELETE (2)
+ "0000" + // flags = 0
+ "00000000" + // seqno = 0
+ "00000000" + // pid = 0
+ // struct nfgenmsg
+ "02" + // nfgen_family = AF_INET
+ "00" + // version = NFNETLINK_V0
+ "1234" + // res_id = 0x1234 (big endian)
+ // struct nlattr
+ "3400" + // nla_len = 52
+ "0180" + // nla_type = nested CTA_TUPLE_ORIG
+ // struct nlattr
+ "1400" + // nla_len = 20
+ "0180" + // nla_type = nested CTA_TUPLE_IP
+ "0800 0100 C0A8500C" + // nla_type=CTA_IP_V4_SRC, ip=192.168.80.12
+ "0800 0200 8C700874" + // nla_type=CTA_IP_V4_DST, ip=140.112.8.116
+ // struct nlattr
+ "1C00" + // nla_len = 28
+ "0280" + // nla_type = nested CTA_TUPLE_PROTO
+ "0500 0100 06 000000" + // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+ "0600 0200 F3F1 0000" + // nla_type=CTA_PROTO_SRC_PORT, port=62449 (big endian)
+ "0600 0300 01BB 0000" + // nla_type=CTA_PROTO_DST_PORT, port=433 (big endian)
+ // struct nlattr
+ "3400" + // nla_len = 52
+ "0280" + // nla_type = nested CTA_TUPLE_REPLY
+ // struct nlattr
+ "1400" + // nla_len = 20
+ "0180" + // nla_type = nested CTA_TUPLE_IP
+ "0800 0100 8C700874" + // nla_type=CTA_IP_V4_SRC, ip=140.112.8.116
+ "0800 0200 6451B301" + // nla_type=CTA_IP_V4_DST, ip=100.81.179.1
+ // struct nlattr
+ "1C00" + // nla_len = 28
+ "0280" + // nla_type = nested CTA_TUPLE_PROTO
+ "0500 0100 06 000000" + // nla_type=CTA_PROTO_NUM, proto=IPPROTO_TCP (6)
+ "0600 0200 01BB 0000" + // nla_type=CTA_PROTO_SRC_PORT, port=433 (big endian)
+ "0600 0300 F3F1 0000" + // nla_type=CTA_PROTO_DST_PORT, port=62449 (big endian)
+ // struct nlattr
+ "0800" + // nla_len = 8
+ "0300" + // nla_type = CTA_STATUS
+ "0000039E"; // nla_value = 0b1110011110 (big endian)
+ // IPS_SEEN_REPLY (1 << 1) | IPS_ASSURED (1 << 2) |
+ // IPS_CONFIRMED (1 << 3) | IPS_SRC_NAT (1 << 4) |
+ // IPS_SRC_NAT_DONE (1 << 7) | IPS_DST_NAT_DONE (1 << 8) |
+ // IPS_DYING (1 << 9)
+ // CHECKSTYLE:ON IndentationCheck
+ public static final byte[] CT_V4DELETE_TCP_BYTES =
+ HexEncoding.decode(CT_V4DELETE_TCP_HEX.replaceAll(" ", "").toCharArray(), false);
+
+ @Test
+ public void testConntrackEventDelete() throws Exception {
+ final ConntrackEvent expectedEvent =
+ makeTestConntrackEvent(IPCTNL_MSG_CT_DELETE, 0x39e /* status */,
+ 0 /* timeoutSec (absent) */);
+ mConntrackMonitor.sendMessage(CT_V4DELETE_TCP_BYTES);
+ verify(mConsumer, timeout(TIMEOUT_MS)).accept(eq(expectedEvent));
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/ip/InterfaceControllerTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/ip/InterfaceControllerTest.java
new file mode 100644
index 0000000..dea667d
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/ip/InterfaceControllerTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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 com.android.net.module.util.ip;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.InterfaceConfigurationParcel;
+import android.net.LinkAddress;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.SharedLog;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class InterfaceControllerTest {
+ private static final String TEST_IFACE = "testif";
+ private static final String TEST_IPV4_ADDR = "192.168.123.28";
+ private static final int TEST_PREFIXLENGTH = 31;
+
+ @Mock private INetd mNetd;
+ @Mock private SharedLog mLog;
+ @Captor private ArgumentCaptor<InterfaceConfigurationParcel> mConfigCaptor;
+
+ private InterfaceController mController;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mController = new InterfaceController(TEST_IFACE, mNetd, mLog);
+
+ doNothing().when(mNetd).interfaceSetCfg(mConfigCaptor.capture());
+ }
+
+ @Test
+ public void testSetIPv4Address() throws Exception {
+ mController.setIPv4Address(
+ new LinkAddress(InetAddresses.parseNumericAddress(TEST_IPV4_ADDR),
+ TEST_PREFIXLENGTH));
+ verify(mNetd, times(1)).interfaceSetCfg(any());
+ final InterfaceConfigurationParcel parcel = mConfigCaptor.getValue();
+ assertEquals(TEST_IFACE, parcel.ifName);
+ assertEquals(TEST_IPV4_ADDR, parcel.ipv4Addr);
+ assertEquals(TEST_PREFIXLENGTH, parcel.prefixLength);
+ assertEquals("", parcel.hwAddr);
+ assertArrayEquals(new String[0], parcel.flags);
+ }
+
+ @Test
+ public void testClearIPv4Address() throws Exception {
+ mController.clearIPv4Address();
+ verify(mNetd, times(1)).interfaceSetCfg(any());
+ final InterfaceConfigurationParcel parcel = mConfigCaptor.getValue();
+ assertEquals(TEST_IFACE, parcel.ifName);
+ assertEquals("0.0.0.0", parcel.ipv4Addr);
+ assertEquals(0, parcel.prefixLength);
+ assertEquals("", parcel.hwAddr);
+ assertArrayEquals(new String[0], parcel.flags);
+ }
+}
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..55b585a
--- /dev/null
+++ b/staticlibs/testutils/app/connectivitychecker/Android.bp
@@ -0,0 +1,29 @@
+// 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.
+
+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"],
+}
\ No newline at end of file
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