Merge "Add packet filters for IPv6 UDP"
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 1fe6c2c..ff65228 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -36,7 +36,6 @@
srcs: [
"device/com/android/net/module/util/DeviceConfigUtils.java",
"device/com/android/net/module/util/FdEventsReader.java",
- "device/com/android/net/module/util/HexDump.java",
"device/com/android/net/module/util/NetworkMonitorUtils.java",
"device/com/android/net/module/util/PacketReader.java",
"device/com/android/net/module/util/SharedLog.java",
@@ -121,11 +120,11 @@
"device/com/android/net/module/util/BpfDump.java",
"device/com/android/net/module/util/BpfMap.java",
"device/com/android/net/module/util/BpfUtils.java",
- "device/com/android/net/module/util/HexDump.java",
"device/com/android/net/module/util/IBpfMap.java",
"device/com/android/net/module/util/JniUtil.java",
"device/com/android/net/module/util/Struct.java",
"device/com/android/net/module/util/TcUtils.java",
+ "framework/com/android/net/module/util/HexDump.java",
],
sdk_version: "module_current",
min_sdk_version: "29",
@@ -149,7 +148,6 @@
java_library {
name: "net-utils-device-common-struct",
srcs: [
- "device/com/android/net/module/util/HexDump.java",
"device/com/android/net/module/util/Ipv6Utils.java",
"device/com/android/net/module/util/PacketBuilder.java",
"device/com/android/net/module/util/Struct.java",
@@ -267,6 +265,14 @@
"//packages/apps/Settings",
],
lint: { strict_updatability_linting: true },
+ errorprone: {
+ enabled: true,
+ // Error-prone checking only warns of problems when building. To make the build fail with
+ // these errors, list the specific error-prone problems below.
+ javacflags: [
+ "-Xep:NullablePrimitive:ERROR",
+ ],
+ },
}
java_library {
@@ -328,6 +334,63 @@
lint: { strict_updatability_linting: true },
}
+java_library {
+ name: "net-utils-device-common-wear",
+ srcs: [
+ "device/com/android/net/module/util/wear/*.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__",
+ ],
+ libs: [
+ "framework-annotations-lib",
+ ],
+ static_libs: [
+ "net-utils-device-common-async",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ "//apex_available:platform",
+ ],
+ lint: { strict_updatability_linting: true },
+}
+
+// Limited set of utilities for use by service-connectivity-mdns-standalone-build-test, to make sure
+// the mDNS code can build with only system APIs.
+// The mDNS code is platform code so it should use framework-annotations-lib, contrary to apps that
+// should use sdk_version: "system_current" and only androidx.annotation_annotation. But this build
+// rule verifies that the mDNS code can be built into apps, if code transformations are applied to
+// the annotations.
+// When using "system_current", framework annotations are not available; they would appear as
+// package-private as they are marked as such in the system_current stubs. So build against
+// core_platform and add the stubs manually in "libs". See http://b/147773144#comment7.
+java_library {
+ name: "net-utils-device-common-mdns-standalone-build-test",
+ // Build against core_platform and add the stub libraries manually in "libs", as annotations
+ // are already included in android_system_stubs_current but package-private, so
+ // "framework-annotations-lib" needs to be manually included before
+ // "android_system_stubs_current" (b/272392042)
+ sdk_version: "core_platform",
+ srcs: [
+ "device/com/android/net/module/util/FdEventsReader.java",
+ "device/com/android/net/module/util/SharedLog.java",
+ "framework/com/android/net/module/util/ByteUtils.java",
+ "framework/com/android/net/module/util/CollectionUtils.java",
+ "framework/com/android/net/module/util/HexDump.java",
+ "framework/com/android/net/module/util/LinkPropertiesUtils.java",
+ ],
+ libs: [
+ "framework-annotations-lib",
+ "android_system_stubs_current",
+ "androidx.annotation_annotation",
+ ],
+ visibility: ["//packages/modules/Connectivity/service-t"],
+}
+
// Use a filegroup and not a library for telephony sources, as framework-annotations cannot be
// included either (some annotations would be duplicated on the bootclasspath).
filegroup {
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
index 9042085..9df2b03 100644
--- a/staticlibs/device/com/android/net/module/util/BpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -70,8 +70,11 @@
private static ParcelFileDescriptor cachedBpfFdGet(String path, int mode,
int keySize, int valueSize)
throws ErrnoException, NullPointerException {
- // TODO: key should include keySize & valueSize, but really we should match specific types
- Pair<String, Integer> key = Pair.create(path, mode);
+ // Supports up to 1023 byte key and 65535 byte values
+ // Creating a BpfMap with larger keys/values seems like a bad idea any way...
+ keySize &= 1023; // 10-bits
+ valueSize &= 65535; // 16-bits
+ var key = Pair.create(path, (mode << 26) ^ (keySize << 16) ^ valueSize);
// unlocked fetch is safe: map is concurrent read capable, and only inserted into
ParcelFileDescriptor fd = sFdCache.get(key);
if (fd != null) return fd;
diff --git a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
index 1225aa7..dae4eb9 100644
--- a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
+++ b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -16,9 +16,12 @@
package com.android.net.module.util;
+import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+
import android.content.Context;
-import android.content.pm.ModuleInfo;
+import android.content.Intent;
import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.provider.DeviceConfig;
import android.util.Log;
@@ -28,6 +31,9 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Utilities for modules to query {@link DeviceConfig} and flags.
*/
@@ -43,6 +49,11 @@
public static final String TETHERING_MODULE_NAME = "com.android.tethering";
@VisibleForTesting
+ public static final String RESOURCES_APK_INTENT =
+ "com.android.server.connectivity.intent.action.SERVICE_CONNECTIVITY_RESOURCES_APK";
+ private static final String CONNECTIVITY_RES_PKG_DIR = "/apex/" + TETHERING_MODULE_NAME + "/";
+
+ @VisibleForTesting
public static void resetPackageVersionCacheForTest() {
sPackageVersion = -1;
sModuleVersion = -1;
@@ -189,23 +200,20 @@
*/
public static boolean isFeatureEnabled(@NonNull Context context, @NonNull String namespace,
@NonNull String name, @NonNull String moduleName, boolean defaultEnabled) {
+ // TODO: migrate callers to a non-generic isTetheringFeatureEnabled method.
+ if (!TETHERING_MODULE_NAME.equals(moduleName)) {
+ throw new IllegalArgumentException(
+ "This method is only usable by the tethering module");
+ }
try {
- final long packageVersion = getModuleVersion(context, moduleName);
+ final long packageVersion = getTetheringModuleVersion(context);
return isFeatureEnabled(context, packageVersion, namespace, name, defaultEnabled);
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Could not find the module name", e);
- return defaultEnabled;
+ return false;
}
}
- private static boolean maybeUseFixedPackageVersion(@NonNull Context context) {
- final String packageName = context.getPackageName();
- if (packageName == null) return false;
-
- return packageName.equals("com.android.networkstack.tethering")
- || packageName.equals("com.android.networkstack.tethering.inprocess");
- }
-
private static boolean isFeatureEnabled(@NonNull Context context, long packageVersion,
@NonNull String namespace, String name, boolean defaultEnabled)
throws PackageManager.NameNotFoundException {
@@ -215,36 +223,40 @@
|| (propertyVersion != 0 && packageVersion >= (long) propertyVersion);
}
+ // Guess the tethering module name based on the package prefix of the connectivity resources
+ // Take the resource package name, cut it before "connectivity" and append "tethering".
+ // Then resolve that package version number with packageManager.
+ // If that fails retry by appending "go.tethering" instead
+ private static long resolveTetheringModuleVersion(@NonNull Context context)
+ throws PackageManager.NameNotFoundException {
+ final String connResourcesPackage = getConnectivityResourcesPackageName(context);
+ final int pkgPrefixLen = connResourcesPackage.indexOf("connectivity");
+ if (pkgPrefixLen < 0) {
+ throw new IllegalStateException(
+ "Invalid connectivity resources package: " + connResourcesPackage);
+ }
+
+ final String pkgPrefix = connResourcesPackage.substring(0, pkgPrefixLen);
+ final PackageManager packageManager = context.getPackageManager();
+ try {
+ return packageManager.getPackageInfo(pkgPrefix + "tethering",
+ PackageManager.MATCH_APEX).getLongVersionCode();
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.d(TAG, "Device is using go modules");
+ // fall through
+ }
+
+ return packageManager.getPackageInfo(pkgPrefix + "go.tethering",
+ PackageManager.MATCH_APEX).getLongVersionCode();
+ }
+
private static volatile long sModuleVersion = -1;
- @VisibleForTesting public static long FIXED_PACKAGE_VERSION = 10;
- private static long getModuleVersion(@NonNull Context context, @NonNull String moduleName)
+ private static long getTetheringModuleVersion(@NonNull Context context)
throws PackageManager.NameNotFoundException {
if (sModuleVersion >= 0) return sModuleVersion;
- final PackageManager packageManager = context.getPackageManager();
- ModuleInfo module;
- try {
- module = packageManager.getModuleInfo(
- moduleName, PackageManager.MODULE_APEX_NAME);
- } catch (PackageManager.NameNotFoundException e) {
- // The error may happen if mainline module meta data is not installed e.g. there are
- // no meta data configuration in AOSP build. To be able to enable a feature in AOSP
- // by setting a flag via ADB for example. set a small non-zero fixed number for
- // comparing.
- if (maybeUseFixedPackageVersion(context)) {
- sModuleVersion = FIXED_PACKAGE_VERSION;
- return FIXED_PACKAGE_VERSION;
- } else {
- throw e;
- }
- }
- String modulePackageName = module.getPackageName();
- if (modulePackageName == null) throw new PackageManager.NameNotFoundException(moduleName);
- final long version = packageManager.getPackageInfo(modulePackageName,
- PackageManager.MATCH_APEX).getLongVersionCode();
- sModuleVersion = version;
-
- return version;
+ sModuleVersion = resolveTetheringModuleVersion(context);
+ return sModuleVersion;
}
/**
@@ -272,4 +284,24 @@
return defaultValue;
}
}
+
+ /**
+ * Get the package name of the ServiceConnectivityResources package, used to provide resources
+ * for service-connectivity.
+ */
+ @NonNull
+ public static String getConnectivityResourcesPackageName(@NonNull Context context) {
+ final List<ResolveInfo> pkgs = new ArrayList<>(context.getPackageManager()
+ .queryIntentActivities(new Intent(RESOURCES_APK_INTENT), MATCH_SYSTEM_ONLY));
+ pkgs.removeIf(pkg -> !pkg.activityInfo.applicationInfo.sourceDir.startsWith(
+ CONNECTIVITY_RES_PKG_DIR));
+ if (pkgs.size() > 1) {
+ Log.wtf(TAG, "More than one connectivity resources package found: " + pkgs);
+ }
+ if (pkgs.isEmpty()) {
+ throw new IllegalStateException("No connectivity resource package found");
+ }
+
+ return pkgs.get(0).activityInfo.applicationInfo.packageName;
+ }
}
diff --git a/staticlibs/device/com/android/net/module/util/async/BufferedFile.java b/staticlibs/device/com/android/net/module/util/async/BufferedFile.java
new file mode 100644
index 0000000..bb5736b
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/BufferedFile.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.async;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Buffers inbound and outbound file data within given strict limits.
+ *
+ * Automatically manages all readability and writeability events in EventManager:
+ * - When read buffer has more space - asks EventManager to notify on more data
+ * - When write buffer has more space - asks the user to provide more data
+ * - When underlying file cannot accept more data - registers EventManager callback
+ *
+ * @hide
+ */
+public final class BufferedFile implements AsyncFile.Listener {
+ /**
+ * Receives notifications when new data or output space is available.
+ * @hide
+ */
+ public interface Listener {
+ /** Invoked after the underlying file has been closed. */
+ void onBufferedFileClosed();
+
+ /** Invoked when there's new data in the inbound buffer. */
+ void onBufferedFileInboundData(int readByteCount);
+
+ /** Notifies on data being flushed from output buffer. */
+ void onBufferedFileOutboundSpace();
+
+ /** Notifies on unrecoverable error in file access. */
+ void onBufferedFileIoError(String message);
+ }
+
+ private final Listener mListener;
+ private final EventManager mEventManager;
+ private AsyncFile mFile;
+
+ private final CircularByteBuffer mInboundBuffer;
+ private final AtomicLong mTotalBytesRead = new AtomicLong();
+ private boolean mIsReadingShutdown;
+
+ private final CircularByteBuffer mOutboundBuffer;
+ private final AtomicLong mTotalBytesWritten = new AtomicLong();
+
+ /** Creates BufferedFile based on the given file descriptor. */
+ public static BufferedFile create(
+ EventManager eventManager,
+ FileHandle fileHandle,
+ Listener listener,
+ int inboundBufferSize,
+ int outboundBufferSize) throws IOException {
+ if (fileHandle == null) {
+ throw new NullPointerException();
+ }
+ BufferedFile file = new BufferedFile(
+ eventManager, listener, inboundBufferSize, outboundBufferSize);
+ file.mFile = eventManager.registerFile(fileHandle, file);
+ return file;
+ }
+
+ private BufferedFile(
+ EventManager eventManager,
+ Listener listener,
+ int inboundBufferSize,
+ int outboundBufferSize) {
+ if (eventManager == null || listener == null) {
+ throw new NullPointerException();
+ }
+ mEventManager = eventManager;
+ mListener = listener;
+
+ mInboundBuffer = new CircularByteBuffer(inboundBufferSize);
+ mOutboundBuffer = new CircularByteBuffer(outboundBufferSize);
+ }
+
+ /** Requests this file to be closed. */
+ public void close() {
+ mFile.close();
+ }
+
+ @Override
+ public void onClosed(AsyncFile file) {
+ mListener.onBufferedFileClosed();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // READ PATH
+ ///////////////////////////////////////////////////////////////////////////
+
+ /** Returns buffer that is automatically filled with inbound data. */
+ public ReadableByteBuffer getInboundBuffer() {
+ return mInboundBuffer;
+ }
+
+ public int getInboundBufferFreeSizeForTest() {
+ return mInboundBuffer.freeSize();
+ }
+
+ /** Permanently disables reading of this file, and clears all buffered data. */
+ public void shutdownReading() {
+ mIsReadingShutdown = true;
+ mInboundBuffer.clear();
+ mFile.enableReadEvents(false);
+ }
+
+ /** Returns true after shutdownReading() has been called. */
+ public boolean isReadingShutdown() {
+ return mIsReadingShutdown;
+ }
+
+ /** Starts or resumes async read operations on this file. */
+ public void continueReading() {
+ if (!mIsReadingShutdown && mInboundBuffer.freeSize() > 0) {
+ mFile.enableReadEvents(true);
+ }
+ }
+
+ @Override
+ public void onReadReady(AsyncFile file) {
+ if (mIsReadingShutdown) {
+ return;
+ }
+
+ int readByteCount;
+ try {
+ readByteCount = bufferInputData();
+ } catch (IOException e) {
+ mListener.onBufferedFileIoError("IOException while reading: " + e.toString());
+ return;
+ }
+
+ if (readByteCount > 0) {
+ mListener.onBufferedFileInboundData(readByteCount);
+ }
+
+ continueReading();
+ }
+
+ private int bufferInputData() throws IOException {
+ int totalReadCount = 0;
+ while (true) {
+ final int maxReadCount = mInboundBuffer.getDirectWriteSize();
+ if (maxReadCount == 0) {
+ mFile.enableReadEvents(false);
+ break;
+ }
+
+ final int bufferOffset = mInboundBuffer.getDirectWritePos();
+ final byte[] buffer = mInboundBuffer.getDirectWriteBuffer();
+
+ final int readCount = mFile.read(buffer, bufferOffset, maxReadCount);
+ if (readCount <= 0) {
+ break;
+ }
+
+ mInboundBuffer.accountForDirectWrite(readCount);
+ totalReadCount += readCount;
+ }
+
+ mTotalBytesRead.addAndGet(totalReadCount);
+ return totalReadCount;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // WRITE PATH
+ ///////////////////////////////////////////////////////////////////////////
+
+ /** Returns the number of bytes currently buffered for output. */
+ public int getOutboundBufferSize() {
+ return mOutboundBuffer.size();
+ }
+
+ /** Returns the number of bytes currently available for buffering for output. */
+ public int getOutboundBufferFreeSize() {
+ return mOutboundBuffer.freeSize();
+ }
+
+ /**
+ * Queues the given data for output.
+ * Throws runtime exception if there is not enough space.
+ */
+ public boolean enqueueOutboundData(byte[] data, int pos, int len) {
+ return enqueueOutboundData(data, pos, len, null, 0, 0);
+ }
+
+ /**
+ * Queues data1, then data2 for output.
+ * Throws runtime exception if there is not enough space.
+ */
+ public boolean enqueueOutboundData(
+ byte[] data1, int pos1, int len1,
+ byte[] buffer2, int pos2, int len2) {
+ Assertions.throwsIfOutOfBounds(data1, pos1, len1);
+ Assertions.throwsIfOutOfBounds(buffer2, pos2, len2);
+
+ final int totalLen = len1 + len2;
+
+ if (totalLen > mOutboundBuffer.freeSize()) {
+ flushOutboundBuffer();
+
+ if (totalLen > mOutboundBuffer.freeSize()) {
+ return false;
+ }
+ }
+
+ mOutboundBuffer.writeBytes(data1, pos1, len1);
+
+ if (buffer2 != null) {
+ mOutboundBuffer.writeBytes(buffer2, pos2, len2);
+ }
+
+ flushOutboundBuffer();
+
+ return true;
+ }
+
+ private void flushOutboundBuffer() {
+ try {
+ while (mOutboundBuffer.getDirectReadSize() > 0) {
+ final int maxReadSize = mOutboundBuffer.getDirectReadSize();
+ final int writeCount = mFile.write(
+ mOutboundBuffer.getDirectReadBuffer(),
+ mOutboundBuffer.getDirectReadPos(),
+ maxReadSize);
+
+ if (writeCount == 0) {
+ mFile.enableWriteEvents(true);
+ break;
+ }
+
+ if (writeCount > maxReadSize) {
+ throw new IllegalArgumentException(
+ "Write count " + writeCount + " above max " + maxReadSize);
+ }
+
+ mOutboundBuffer.accountForDirectRead(writeCount);
+ }
+ } catch (IOException e) {
+ scheduleOnIoError("IOException while writing: " + e.toString());
+ }
+ }
+
+ private void scheduleOnIoError(String message) {
+ mEventManager.execute(() -> {
+ mListener.onBufferedFileIoError(message);
+ });
+ }
+
+ @Override
+ public void onWriteReady(AsyncFile file) {
+ mFile.enableWriteEvents(false);
+ flushOutboundBuffer();
+ mListener.onBufferedFileOutboundSpace();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("file={");
+ sb.append(mFile);
+ sb.append("}");
+ if (mIsReadingShutdown) {
+ sb.append(", readingShutdown");
+ }
+ sb.append("}, inboundBuffer={");
+ sb.append(mInboundBuffer);
+ sb.append("}, outboundBuffer={");
+ sb.append(mOutboundBuffer);
+ sb.append("}, totalBytesRead=");
+ sb.append(mTotalBytesRead);
+ sb.append(", totalBytesWritten=");
+ sb.append(mTotalBytesWritten);
+ return sb.toString();
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/async/OsAccess.java b/staticlibs/device/com/android/net/module/util/async/OsAccess.java
new file mode 100644
index 0000000..df0ded2
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/async/OsAccess.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.async;
+
+import android.os.ParcelFileDescriptor;
+import android.system.StructPollfd;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * Provides access to all relevant OS functions..
+ *
+ * @hide
+ */
+public abstract class OsAccess {
+ /** Closes the given file, suppressing IO exceptions. */
+ public abstract void close(ParcelFileDescriptor fd);
+
+ /** Returns file name for debugging purposes. */
+ public abstract String getFileDebugName(ParcelFileDescriptor fd);
+
+ /** Returns inner FileDescriptor instance. */
+ public abstract FileDescriptor getInnerFileDescriptor(ParcelFileDescriptor fd);
+
+ /**
+ * Reads available data from the given non-blocking file descriptor.
+ *
+ * Returns zero if there's no data to read at this moment.
+ * Returns -1 if the file has reached its end or the input stream has been closed.
+ * Otherwise returns the number of bytes read.
+ */
+ public abstract int read(FileDescriptor fd, byte[] buffer, int pos, int len)
+ throws IOException;
+
+ /**
+ * Writes data into the given non-blocking file descriptor.
+ *
+ * Returns zero if there's no buffer space to write to at this moment.
+ * Otherwise returns the number of bytes written.
+ */
+ public abstract int write(FileDescriptor fd, byte[] buffer, int pos, int len)
+ throws IOException;
+
+ public abstract long monotonicTimeMillis();
+ public abstract void setNonBlocking(FileDescriptor fd) throws IOException;
+ public abstract ParcelFileDescriptor[] pipe() throws IOException;
+
+ public abstract int poll(StructPollfd[] fds, int timeoutMs) throws IOException;
+ public abstract short getPollInMask();
+ public abstract short getPollOutMask();
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
index 5a180e7..d462c53 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/InetDiagMessage.java
@@ -19,32 +19,48 @@
import static android.os.Process.INVALID_UID;
import static android.system.OsConstants.AF_INET;
import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.ENOENT;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
import static android.system.OsConstants.NETLINK_INET_DIAG;
+import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DESTROY;
import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+import static com.android.net.module.util.netlink.NetlinkConstants.stringForAddressFamily;
+import static com.android.net.module.util.netlink.NetlinkConstants.stringForProtocol;
import static com.android.net.module.util.netlink.NetlinkUtils.DEFAULT_RECV_BUFSIZE;
+import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
+import static com.android.net.module.util.netlink.NetlinkUtils.TCP_ALIVE_STATE_FILTER;
+import static com.android.net.module.util.netlink.NetlinkUtils.connectSocketToNetlink;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
import android.net.util.SocketUtils;
+import android.os.Process;
import android.system.ErrnoException;
import android.util.Log;
+import android.util.Range;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
+import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
/**
* A NetlinkMessage subclass for netlink inet_diag messages.
@@ -58,8 +74,8 @@
private static final int TIMEOUT_MS = 500;
/**
- * Construct an inet_diag_req_v2 message. This method will throw {@code NullPointerException}
- * if local and remote are not both null or both non-null.
+ * Construct an inet_diag_req_v2 message. This method will throw
+ * {@link IllegalArgumentException} if local and remote are not both null or both non-null.
*/
public static byte[] inetDiagReqV2(int protocol, InetSocketAddress local,
InetSocketAddress remote, int family, short flags) {
@@ -68,16 +84,16 @@
}
/**
- * Construct an inet_diag_req_v2 message. This method will throw {@code NullPointerException}
- * if local and remote are not both null or both non-null.
+ * Construct an inet_diag_req_v2 message. This method will throw
+ * {@code IllegalArgumentException} if local and remote are not both null or both non-null.
*
* @param protocol the request protocol type. This should be set to one of IPPROTO_TCP,
* IPPROTO_UDP, or IPPROTO_UDPLITE.
* @param local local socket address of the target socket. This will be packed into a
- * {@Code StructInetDiagSockId}. Request to diagnose for all sockets if both of
+ * {@link StructInetDiagSockId}. Request to diagnose for all sockets if both of
* local or remote address is null.
* @param remote remote socket address of the target socket. This will be packed into a
- * {@Code StructInetDiagSockId}. Request to diagnose for all sockets if both of
+ * {@link StructInetDiagSockId}. Request to diagnose for all sockets if both of
* local or remote address is null.
* @param family the ip family of the request message. This should be set to either AF_INET or
* AF_INET6 for IPv4 or IPv6 sockets respectively.
@@ -90,18 +106,47 @@
*/
public static byte[] inetDiagReqV2(int protocol, @Nullable InetSocketAddress local,
@Nullable InetSocketAddress remote, int family, short flags, int pad, int idiagExt,
- int state) throws NullPointerException {
+ int state) throws IllegalArgumentException {
+ // Request for all sockets if no specific socket is requested. Specify the local and remote
+ // socket address information for target request socket.
+ if ((local == null) != (remote == null)) {
+ throw new IllegalArgumentException(
+ "Local and remote must be both null or both non-null");
+ }
+ final StructInetDiagSockId id = ((local != null && remote != null)
+ ? new StructInetDiagSockId(local, remote) : null);
+ return inetDiagReqV2(protocol, id, family,
+ SOCK_DIAG_BY_FAMILY, flags, pad, idiagExt, state);
+ }
+
+ /**
+ * Construct an inet_diag_req_v2 message.
+ *
+ * @param protocol the request protocol type. This should be set to one of IPPROTO_TCP,
+ * IPPROTO_UDP, or IPPROTO_UDPLITE.
+ * @param id inet_diag_sockid. See {@link StructInetDiagSockId}
+ * @param family the ip family of the request message. This should be set to either AF_INET or
+ * AF_INET6 for IPv4 or IPv6 sockets respectively.
+ * @param type message types.
+ * @param flags message flags. See <linux_src>/include/uapi/linux/netlink.h.
+ * @param pad for raw socket protocol specification.
+ * @param idiagExt a set of flags defining what kind of extended information to report.
+ * @param state a bit mask that defines a filter of socket states.
+ * @return bytes array representation of the message
+ */
+ public static byte[] inetDiagReqV2(int protocol, @Nullable StructInetDiagSockId id, int family,
+ short type, short flags, int pad, int idiagExt, int state) {
final byte[] bytes = new byte[StructNlMsgHdr.STRUCT_SIZE + StructInetDiagReqV2.STRUCT_SIZE];
final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
byteBuffer.order(ByteOrder.nativeOrder());
final StructNlMsgHdr nlMsgHdr = new StructNlMsgHdr();
nlMsgHdr.nlmsg_len = bytes.length;
- nlMsgHdr.nlmsg_type = SOCK_DIAG_BY_FAMILY;
+ nlMsgHdr.nlmsg_type = type;
nlMsgHdr.nlmsg_flags = flags;
nlMsgHdr.pack(byteBuffer);
final StructInetDiagReqV2 inetDiagReqV2 =
- new StructInetDiagReqV2(protocol, local, remote, family, pad, idiagExt, state);
+ new StructInetDiagReqV2(protocol, id, family, pad, idiagExt, state);
inetDiagReqV2.pack(byteBuffer);
return bytes;
@@ -109,7 +154,8 @@
public StructInetDiagMsg inetDiagMsg;
- private InetDiagMessage(@NonNull StructNlMsgHdr header) {
+ @VisibleForTesting
+ public InetDiagMessage(@NonNull StructNlMsgHdr header) {
super(header);
inetDiagMsg = new StructInetDiagMsg();
}
@@ -128,6 +174,13 @@
return msg;
}
+ private static void closeSocketQuietly(final FileDescriptor fd) {
+ try {
+ SocketUtils.closeSocket(fd);
+ } catch (IOException ignored) {
+ }
+ }
+
private static int lookupUidByFamily(int protocol, InetSocketAddress local,
InetSocketAddress remote, int family, short flags,
FileDescriptor fd)
@@ -218,13 +271,7 @@
| InterruptedIOException e) {
Log.e(TAG, e.toString());
} finally {
- if (fd != null) {
- try {
- SocketUtils.closeSocket(fd);
- } catch (IOException e) {
- Log.e(TAG, e.toString());
- }
- }
+ closeSocketQuietly(fd);
}
return uid;
}
@@ -240,7 +287,185 @@
(short) (StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_DUMP) /* flag */,
0 /* pad */,
1 << NetlinkConstants.INET_DIAG_MEMINFO /* idiagExt */,
- NetlinkUtils.TCP_MONITOR_STATE_FILTER);
+ TCP_ALIVE_STATE_FILTER);
+ }
+
+ private static void sendNetlinkDestroyRequest(FileDescriptor fd, int proto,
+ InetDiagMessage diagMsg) throws InterruptedIOException, ErrnoException {
+ final byte[] destroyMsg = InetDiagMessage.inetDiagReqV2(
+ proto,
+ diagMsg.inetDiagMsg.id,
+ diagMsg.inetDiagMsg.idiag_family,
+ SOCK_DESTROY,
+ (short) (StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_ACK),
+ 0 /* pad */,
+ 0 /* idiagExt */,
+ 1 << diagMsg.inetDiagMsg.idiag_state
+ );
+ NetlinkUtils.sendMessage(fd, destroyMsg, 0, destroyMsg.length, IO_TIMEOUT_MS);
+ NetlinkUtils.receiveNetlinkAck(fd);
+ }
+
+ private static void sendNetlinkDumpRequest(FileDescriptor fd, int proto, int states, int family)
+ throws InterruptedIOException, ErrnoException {
+ final byte[] dumpMsg = InetDiagMessage.inetDiagReqV2(
+ proto,
+ null /* id */,
+ family,
+ SOCK_DIAG_BY_FAMILY,
+ (short) (StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_DUMP),
+ 0 /* pad */,
+ 0 /* idiagExt */,
+ states);
+ NetlinkUtils.sendMessage(fd, dumpMsg, 0, dumpMsg.length, IO_TIMEOUT_MS);
+ }
+
+ private static int processNetlinkDumpAndDestroySockets(FileDescriptor dumpFd,
+ FileDescriptor destroyFd, int proto, Predicate<InetDiagMessage> filter)
+ throws InterruptedIOException, ErrnoException {
+ int destroyedSockets = 0;
+
+ while (true) {
+ final ByteBuffer buf = NetlinkUtils.recvMessage(
+ dumpFd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
+
+ while (buf.remaining() > 0) {
+ final int position = buf.position();
+ final NetlinkMessage nlMsg = NetlinkMessage.parse(buf, NETLINK_INET_DIAG);
+ if (nlMsg == null) {
+ // Move to the position where parse started for error log.
+ buf.position(position);
+ Log.e(TAG, "Failed to parse netlink message: " + hexify(buf));
+ break;
+ }
+
+ if (nlMsg.getHeader().nlmsg_type == NLMSG_DONE) {
+ return destroyedSockets;
+ }
+
+ if (!(nlMsg instanceof InetDiagMessage)) {
+ Log.wtf(TAG, "Received unexpected netlink message: " + nlMsg);
+ continue;
+ }
+
+ final InetDiagMessage diagMsg = (InetDiagMessage) nlMsg;
+ if (filter.test(diagMsg)) {
+ try {
+ sendNetlinkDestroyRequest(destroyFd, proto, diagMsg);
+ destroyedSockets++;
+ } catch (InterruptedIOException | ErrnoException e) {
+ if (!(e instanceof ErrnoException
+ && ((ErrnoException) e).errno == ENOENT)) {
+ Log.e(TAG, "Failed to destroy socket: diagMsg=" + diagMsg + ", " + e);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns whether the InetDiagMessage is for adb socket or not
+ */
+ @VisibleForTesting
+ public static boolean isAdbSocket(final InetDiagMessage msg) {
+ // This is inaccurate since adb could run with ROOT_UID or other services can run with
+ // SHELL_UID. But this check covers most cases and enough.
+ // Note that getting service.adb.tcp.port system property is prohibited by sepolicy
+ // TODO: skip the socket only if there is a listen socket owned by SHELL_UID with the same
+ // source port as this socket
+ return msg.inetDiagMsg.idiag_uid == Process.SHELL_UID;
+ }
+
+ /**
+ * Returns whether the range contains the uid in the InetDiagMessage or not
+ */
+ @VisibleForTesting
+ public static boolean containsUid(InetDiagMessage msg, Set<Range<Integer>> ranges) {
+ for (final Range<Integer> range: ranges) {
+ if (range.contains(msg.inetDiagMsg.idiag_uid)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isLoopbackAddress(InetAddress addr) {
+ if (addr.isLoopbackAddress()) return true;
+ if (!(addr instanceof Inet6Address)) return false;
+
+ // Following check is for v4-mapped v6 address. StructInetDiagSockId contains v4-mapped v6
+ // address as Inet6Address, See StructInetDiagSockId#parse
+ final byte[] addrBytes = addr.getAddress();
+ for (int i = 0; i < 10; i++) {
+ if (addrBytes[i] != 0) return false;
+ }
+ return addrBytes[10] == (byte) 0xff
+ && addrBytes[11] == (byte) 0xff
+ && addrBytes[12] == 127;
+ }
+
+ /**
+ * Returns whether the socket address in the InetDiagMessage is loopback or not
+ */
+ @VisibleForTesting
+ public static boolean isLoopback(InetDiagMessage msg) {
+ final InetAddress srcAddr = msg.inetDiagMsg.id.locSocketAddress.getAddress();
+ final InetAddress dstAddr = msg.inetDiagMsg.id.remSocketAddress.getAddress();
+ return isLoopbackAddress(srcAddr)
+ || isLoopbackAddress(dstAddr)
+ || srcAddr.equals(dstAddr);
+ }
+
+ private static void destroySockets(int proto, int states, Predicate<InetDiagMessage> filter)
+ throws ErrnoException, SocketException, InterruptedIOException {
+ FileDescriptor dumpFd = null;
+ FileDescriptor destroyFd = null;
+
+ try {
+ dumpFd = NetlinkUtils.createNetLinkInetDiagSocket();
+ destroyFd = NetlinkUtils.createNetLinkInetDiagSocket();
+ connectSocketToNetlink(dumpFd);
+ connectSocketToNetlink(destroyFd);
+
+ for (int family : List.of(AF_INET, AF_INET6)) {
+ try {
+ sendNetlinkDumpRequest(dumpFd, proto, states, family);
+ } catch (InterruptedIOException | ErrnoException e) {
+ Log.e(TAG, "Failed to send netlink dump request: " + e);
+ continue;
+ }
+ final int destroyedSockets = processNetlinkDumpAndDestroySockets(
+ dumpFd, destroyFd, proto, filter);
+ Log.d(TAG, "Destroyed " + destroyedSockets + " sockets"
+ + ", proto=" + stringForProtocol(proto)
+ + ", family=" + stringForAddressFamily(family)
+ + ", states=" + states);
+ }
+ } finally {
+ closeSocketQuietly(dumpFd);
+ closeSocketQuietly(destroyFd);
+ }
+ }
+
+ /**
+ * Close tcp sockets that match the following condition
+ * 1. TCP status is one of TCP_ESTABLISHED, TCP_SYN_SENT, and TCP_SYN_RECV
+ * 2. Owner uid of socket is not in the exemptUids
+ * 3. Owner uid of socket is in the ranges
+ * 4. Socket is not loopback
+ * 5. Socket is not adb socket
+ *
+ * @param ranges target uid ranges
+ * @param exemptUids uids to skip close socket
+ */
+ public static void destroyLiveTcpSockets(Set<Range<Integer>> ranges, Set<Integer> exemptUids)
+ throws SocketException, InterruptedIOException, ErrnoException {
+ destroySockets(IPPROTO_TCP, TCP_ALIVE_STATE_FILTER,
+ (diagMsg) -> !exemptUids.contains(diagMsg.inetDiagMsg.idiag_uid)
+ && containsUid(diagMsg, ranges)
+ && !isLoopback(diagMsg)
+ && !isAdbSocket(diagMsg));
}
@Override
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
index c44a5b4..44c51d8 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
@@ -141,6 +141,7 @@
/* see include/uapi/linux/sock_diag.h */
public static final short SOCK_DIAG_BY_FAMILY = 20;
+ public static final short SOCK_DESTROY = 21;
// Netlink groups.
public static final int RTMGRP_LINK = 1;
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index ae16cf8..308ea24 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -36,6 +36,7 @@
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import java.io.FileDescriptor;
import java.io.IOException;
@@ -55,7 +56,7 @@
private static final int TCP_SYN_SENT = 2;
private static final int TCP_SYN_RECV = 3;
- public static final int TCP_MONITOR_STATE_FILTER =
+ public static final int TCP_ALIVE_STATE_FILTER =
(1 << TCP_ESTABLISHED) | (1 << TCP_SYN_SENT) | (1 << TCP_SYN_RECV);
public static final int UNKNOWN_MARK = 0xffffffff;
@@ -81,6 +82,54 @@
}
/**
+ * Parse netlink error message
+ *
+ * @param bytes byteBuffer to parse netlink error message
+ * @return NetlinkErrorMessage if bytes contains valid NetlinkErrorMessage, else {@code null}
+ */
+ @Nullable
+ private static NetlinkErrorMessage parseNetlinkErrorMessage(ByteBuffer bytes) {
+ final StructNlMsgHdr nlmsghdr = StructNlMsgHdr.parse(bytes);
+ if (nlmsghdr == null || nlmsghdr.nlmsg_type != NetlinkConstants.NLMSG_ERROR) {
+ return null;
+ }
+ return NetlinkErrorMessage.parse(nlmsghdr, bytes);
+ }
+
+ /**
+ * Receive netlink ack message and check error
+ *
+ * @param fd fd to read netlink message
+ */
+ public static void receiveNetlinkAck(final FileDescriptor fd)
+ throws InterruptedIOException, ErrnoException {
+ final ByteBuffer bytes = recvMessage(fd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
+ // recvMessage() guaranteed to not return null if it did not throw.
+ final NetlinkErrorMessage response = parseNetlinkErrorMessage(bytes);
+ if (response != null && response.getNlMsgError() != null) {
+ final int errno = response.getNlMsgError().error;
+ if (errno != 0) {
+ // TODO: consider ignoring EINVAL (-22), which appears to be
+ // normal when probing a neighbor for which the kernel does
+ // not already have / no longer has a link layer address.
+ Log.e(TAG, "receiveNetlinkAck, errmsg=" + response.toString());
+ // Note: convert kernel errnos (negative) into userspace errnos (positive).
+ throw new ErrnoException(response.toString(), Math.abs(errno));
+ }
+ } else {
+ final String errmsg;
+ if (response == null) {
+ bytes.position(0);
+ errmsg = "raw bytes: " + NetlinkConstants.hexify(bytes);
+ } else {
+ errmsg = response.toString();
+ }
+ Log.e(TAG, "receiveNetlinkAck, errmsg=" + errmsg);
+ throw new ErrnoException(errmsg, EPROTO);
+ }
+ }
+
+ /**
* Send one netlink message to kernel via netlink socket.
*
* @param nlProto netlink protocol type.
@@ -93,31 +142,7 @@
try {
connectSocketToNetlink(fd);
sendMessage(fd, msg, 0, msg.length, IO_TIMEOUT_MS);
- final ByteBuffer bytes = recvMessage(fd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
- // recvMessage() guaranteed to not return null if it did not throw.
- final NetlinkMessage response = NetlinkMessage.parse(bytes, nlProto);
- if (response != null && response instanceof NetlinkErrorMessage
- && (((NetlinkErrorMessage) response).getNlMsgError() != null)) {
- final int errno = ((NetlinkErrorMessage) response).getNlMsgError().error;
- if (errno != 0) {
- // TODO: consider ignoring EINVAL (-22), which appears to be
- // normal when probing a neighbor for which the kernel does
- // not already have / no longer has a link layer address.
- Log.e(TAG, errPrefix + ", errmsg=" + response.toString());
- // Note: convert kernel errnos (negative) into userspace errnos (positive).
- throw new ErrnoException(response.toString(), Math.abs(errno));
- }
- } else {
- final String errmsg;
- if (response == null) {
- bytes.position(0);
- errmsg = "raw bytes: " + NetlinkConstants.hexify(bytes);
- } else {
- errmsg = response.toString();
- }
- Log.e(TAG, errPrefix + ", errmsg=" + errmsg);
- throw new ErrnoException(errmsg, EPROTO);
- }
+ receiveNetlinkAck(fd);
} catch (InterruptedIOException e) {
Log.e(TAG, errPrefix, e);
throw new ErrnoException(errPrefix, ETIMEDOUT, e);
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
index c07cec0..2829b92 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkAddressMessage.java
@@ -17,6 +17,7 @@
package com.android.net.module.util.netlink;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_ACK;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REPLACE;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
import android.system.OsConstants;
@@ -58,12 +59,20 @@
@Nullable
private StructIfacacheInfo mIfacacheInfo;
- private RtNetlinkAddressMessage(@NonNull StructNlMsgHdr header) {
+ @VisibleForTesting
+ public RtNetlinkAddressMessage(@NonNull final StructNlMsgHdr header,
+ @NonNull final StructIfaddrMsg ifaddrMsg,
+ @NonNull final InetAddress ipAddress,
+ @Nullable final StructIfacacheInfo structIfacacheInfo,
+ int flags) {
super(header);
- mIfaddrmsg = null;
- mIpAddress = null;
- mIfacacheInfo = null;
- mFlags = 0;
+ mIfaddrmsg = ifaddrMsg;
+ mIpAddress = ipAddress;
+ mIfacacheInfo = structIfacacheInfo;
+ mFlags = flags;
+ }
+ private RtNetlinkAddressMessage(@NonNull StructNlMsgHdr header) {
+ this(header, null, null, null, 0);
}
public int getFlags() {
@@ -160,7 +169,7 @@
final StructNlMsgHdr nlmsghdr = new StructNlMsgHdr();
nlmsghdr.nlmsg_type = NetlinkConstants.RTM_NEWADDR;
- nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+ nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_REPLACE | NLM_F_ACK;
nlmsghdr.nlmsg_seq = seqNo;
final RtNetlinkAddressMessage msg = new RtNetlinkAddressMessage(nlmsghdr);
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructIfaddrMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructIfaddrMsg.java
index 9196feb..2802726 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructIfaddrMsg.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructIfaddrMsg.java
@@ -18,6 +18,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.net.module.util.Struct;
import com.android.net.module.util.Struct.Field;
@@ -49,7 +50,8 @@
@Field(order = 4, type = Type.S32)
public final int index;
- StructIfaddrMsg(short family, short prefixLen, short flags, short scope, int index) {
+ @VisibleForTesting
+ public StructIfaddrMsg(short family, short prefixLen, short flags, short scope, int index) {
this.family = family;
this.prefixLen = prefixLen;
this.flags = flags;
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagMsg.java
index ea018cf..cbd895d 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagMsg.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagMsg.java
@@ -43,12 +43,22 @@
*/
public class StructInetDiagMsg {
public static final int STRUCT_SIZE = 4 + StructInetDiagSockId.STRUCT_SIZE + 20;
- private static final int IDIAG_SOCK_ID_OFFSET = StructNlMsgHdr.STRUCT_SIZE + 4;
- private static final int IDIAG_UID_OFFSET = StructNlMsgHdr.STRUCT_SIZE + 4
- + StructInetDiagSockId.STRUCT_SIZE + 12;
- public int idiag_uid;
+ public short idiag_family;
+ public short idiag_state;
+ public short idiag_timer;
+ public short idiag_retrans;
@NonNull
public StructInetDiagSockId id;
+ public long idiag_expires;
+ public long idiag_rqueue;
+ public long idiag_wqueue;
+ // Use int for uid since other code use int for uid and uid fits to int
+ public int idiag_uid;
+ public long idiag_inode;
+
+ private static short unsignedByte(byte b) {
+ return (short) (b & 0xFF);
+ }
/**
* Parse inet diag netlink message from buffer.
@@ -59,21 +69,35 @@
return null;
}
StructInetDiagMsg struct = new StructInetDiagMsg();
- final byte family = byteBuffer.get();
- byteBuffer.position(IDIAG_SOCK_ID_OFFSET);
- struct.id = StructInetDiagSockId.parse(byteBuffer, family);
+ struct.idiag_family = unsignedByte(byteBuffer.get());
+ struct.idiag_state = unsignedByte(byteBuffer.get());
+ struct.idiag_timer = unsignedByte(byteBuffer.get());
+ struct.idiag_retrans = unsignedByte(byteBuffer.get());
+ struct.id = StructInetDiagSockId.parse(byteBuffer, struct.idiag_family);
if (struct.id == null) {
return null;
}
- struct.idiag_uid = byteBuffer.getInt(IDIAG_UID_OFFSET);
+ struct.idiag_expires = Integer.toUnsignedLong(byteBuffer.getInt());
+ struct.idiag_rqueue = Integer.toUnsignedLong(byteBuffer.getInt());
+ struct.idiag_wqueue = Integer.toUnsignedLong(byteBuffer.getInt());
+ struct.idiag_uid = byteBuffer.getInt();
+ struct.idiag_inode = Integer.toUnsignedLong(byteBuffer.getInt());
return struct;
}
@Override
public String toString() {
return "StructInetDiagMsg{ "
- + "idiag_uid{" + idiag_uid + "}, "
+ + "idiag_family{" + idiag_family + "}, "
+ + "idiag_state{" + idiag_state + "}, "
+ + "idiag_timer{" + idiag_timer + "}, "
+ + "idiag_retrans{" + idiag_retrans + "}, "
+ "id{" + id + "}, "
+ + "idiag_expires{" + idiag_expires + "}, "
+ + "idiag_rqueue{" + idiag_rqueue + "}, "
+ + "idiag_wqueue{" + idiag_wqueue + "}, "
+ + "idiag_uid{" + idiag_uid + "}, "
+ + "idiag_inode{" + idiag_inode + "}, "
+ "}";
}
}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagReqV2.java b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagReqV2.java
index 6eef865..3b47008 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagReqV2.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagReqV2.java
@@ -18,7 +18,6 @@
import androidx.annotation.Nullable;
-import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
/**
@@ -48,23 +47,11 @@
private final int mState;
public static final int INET_DIAG_REQ_V2_ALL_STATES = (int) 0xffffffff;
- public StructInetDiagReqV2(int protocol, InetSocketAddress local, InetSocketAddress remote,
- int family) {
- this(protocol, local, remote, family, 0 /* pad */, 0 /* extension */,
- INET_DIAG_REQ_V2_ALL_STATES);
- }
-
- public StructInetDiagReqV2(int protocol, @Nullable InetSocketAddress local,
- @Nullable InetSocketAddress remote, int family, int pad, int extension, int state)
- throws NullPointerException {
+ public StructInetDiagReqV2(int protocol, @Nullable StructInetDiagSockId id, int family, int pad,
+ int extension, int state) {
mSdiagFamily = (byte) family;
mSdiagProtocol = (byte) protocol;
- // Request for all sockets if no specific socket is requested. Specify the local and remote
- // socket address information for target request socket.
- if ((local == null) != (remote == null)) {
- throw new NullPointerException("Local and remote must be both null or both non-null");
- }
- mId = ((local != null && remote != null) ? new StructInetDiagSockId(local, remote) : null);
+ mId = id;
mPad = (byte) pad;
mIdiagExt = (byte) extension;
mState = state;
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagSockId.java b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagSockId.java
index 648a020..dd85934 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagSockId.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructInetDiagSockId.java
@@ -29,6 +29,7 @@
import androidx.annotation.Nullable;
import java.net.Inet4Address;
+import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
@@ -80,7 +81,7 @@
* Parse inet diag socket id from buffer.
*/
@Nullable
- public static StructInetDiagSockId parse(final ByteBuffer byteBuffer, final byte family) {
+ public static StructInetDiagSockId parse(final ByteBuffer byteBuffer, final short family) {
if (byteBuffer.remaining() < STRUCT_SIZE) {
return null;
}
@@ -89,43 +90,53 @@
final int srcPort = Short.toUnsignedInt(byteBuffer.getShort());
final int dstPort = Short.toUnsignedInt(byteBuffer.getShort());
- final byte[] srcAddrByte;
- final byte[] dstAddrByte;
+ final InetAddress srcAddr;
+ final InetAddress dstAddr;
if (family == AF_INET) {
- srcAddrByte = new byte[IPV4_ADDR_LEN];
- dstAddrByte = new byte[IPV4_ADDR_LEN];
+ final byte[] srcAddrByte = new byte[IPV4_ADDR_LEN];
+ final byte[] dstAddrByte = new byte[IPV4_ADDR_LEN];
byteBuffer.get(srcAddrByte);
// Address always uses IPV6_ADDR_LEN in the buffer. So if the address is IPv4, position
// needs to be advanced to the next field.
byteBuffer.position(byteBuffer.position() + (IPV6_ADDR_LEN - IPV4_ADDR_LEN));
byteBuffer.get(dstAddrByte);
byteBuffer.position(byteBuffer.position() + (IPV6_ADDR_LEN - IPV4_ADDR_LEN));
+ try {
+ srcAddr = Inet4Address.getByAddress(srcAddrByte);
+ dstAddr = Inet4Address.getByAddress(dstAddrByte);
+ } catch (UnknownHostException e) {
+ Log.wtf(TAG, "Failed to parse address: " + e);
+ return null;
+ }
} else if (family == AF_INET6) {
- srcAddrByte = new byte[IPV6_ADDR_LEN];
- dstAddrByte = new byte[IPV6_ADDR_LEN];
+ final byte[] srcAddrByte = new byte[IPV6_ADDR_LEN];
+ final byte[] dstAddrByte = new byte[IPV6_ADDR_LEN];
byteBuffer.get(srcAddrByte);
byteBuffer.get(dstAddrByte);
+ try {
+ // Using Inet6Address.getByAddress to be consistent with idiag_family field since
+ // InetAddress.getByAddress returns Inet4Address if the address is v4-mapped v6
+ // address.
+ srcAddr = Inet6Address.getByAddress(
+ null /* host */, srcAddrByte, -1 /* scope_id */);
+ dstAddr = Inet6Address.getByAddress(
+ null /* host */, dstAddrByte, -1 /* scope_id */);
+ } catch (UnknownHostException e) {
+ Log.wtf(TAG, "Failed to parse address: " + e);
+ return null;
+ }
} else {
Log.wtf(TAG, "Invalid address family: " + family);
return null;
}
- final InetSocketAddress srcAddr;
- final InetSocketAddress dstAddr;
- try {
- srcAddr = new InetSocketAddress(InetAddress.getByAddress(srcAddrByte), srcPort);
- dstAddr = new InetSocketAddress(InetAddress.getByAddress(dstAddrByte), dstPort);
- } catch (UnknownHostException e) {
- // Should not happen. UnknownHostException is thrown only if addr byte array is of
- // illegal length.
- Log.wtf(TAG, "Failed to parse address: " + e);
- return null;
- }
+ final InetSocketAddress srcSocketAddr = new InetSocketAddress(srcAddr, srcPort);
+ final InetSocketAddress dstSocketAddr = new InetSocketAddress(dstAddr, dstPort);
byteBuffer.order(ByteOrder.nativeOrder());
final int ifIndex = byteBuffer.getInt();
final long cookie = byteBuffer.getLong();
- return new StructInetDiagSockId(srcAddr, dstAddr, ifIndex, cookie);
+ return new StructInetDiagSockId(srcSocketAddr, dstSocketAddr, ifIndex, cookie);
}
/**
diff --git a/staticlibs/device/com/android/net/module/util/structs/IaPdOption.java b/staticlibs/device/com/android/net/module/util/structs/IaPdOption.java
new file mode 100644
index 0000000..dbf79dc
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/IaPdOption.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.structs;
+
+import static com.android.net.module.util.NetworkStackConstants.DHCP6_OPTION_IA_PD;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * DHCPv6 IA_PD option.
+ * https://tools.ietf.org/html/rfc8415. This does not contain any option.
+ *
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | OPTION_IA_PD | option-len |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | IAID (4 octets) |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | T1 |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | T2 |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * . .
+ * . IA_PD-options .
+ * . .
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ */
+public class IaPdOption extends Struct {
+ public static final int LENGTH = 12; // option length excluding IA_PD options
+
+ @Field(order = 0, type = Type.S16)
+ public final short code;
+ @Field(order = 1, type = Type.S16)
+ public final short length;
+ @Field(order = 2, type = Type.U32)
+ public final long id;
+ @Field(order = 3, type = Type.U32)
+ public final long t1;
+ @Field(order = 4, type = Type.U32)
+ public final long t2;
+
+ IaPdOption(final short code, final short length, final long id, final long t1,
+ final long t2) {
+ this.code = code;
+ this.length = length;
+ this.id = id;
+ this.t1 = t1;
+ this.t2 = t2;
+ }
+
+ /**
+ * Build an IA_PD option from the required specific parameters.
+ */
+ public static ByteBuffer build(final short length, final long id, final long t1,
+ final long t2) {
+ final IaPdOption option = new IaPdOption((short) DHCP6_OPTION_IA_PD,
+ length /* 12 + IA_PD options length */, id, t1, t2);
+ return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN));
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
new file mode 100644
index 0000000..cd974e6
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/structs/IaPrefixOption.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.structs;
+
+import static com.android.net.module.util.NetworkStackConstants.DHCP6_OPTION_IAPREFIX;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * DHCPv6 IA Prefix Option.
+ * https://tools.ietf.org/html/rfc8415. This does not contain any option.
+ *
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | OPTION_IAPREFIX | option-len |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | preferred-lifetime |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | valid-lifetime |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | prefix-length | |
+ * +-+-+-+-+-+-+-+-+ IPv6-prefix |
+ * | (16 octets) |
+ * | |
+ * | |
+ * | |
+ * | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | | .
+ * +-+-+-+-+-+-+-+-+ .
+ * . IAprefix-options .
+ * . .
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+public class IaPrefixOption extends Struct {
+ public static final int LENGTH = 25; // option length excluding IAprefix-options
+
+ @Field(order = 0, type = Type.S16)
+ public final short code;
+ @Field(order = 1, type = Type.S16)
+ public final short length;
+ @Field(order = 2, type = Type.U32)
+ public final long preferred;
+ @Field(order = 3, type = Type.U32)
+ public final long valid;
+ @Field(order = 4, type = Type.U8)
+ public final short prefixLen;
+ @Field(order = 5, type = Type.ByteArray, arraysize = 16)
+ public final byte[] prefix;
+
+ IaPrefixOption(final short code, final short length, final long preferred,
+ final long valid, final short prefixLen, final byte[] prefix) {
+ this.code = code;
+ this.length = length;
+ this.preferred = preferred;
+ this.valid = valid;
+ this.prefixLen = prefixLen;
+ this.prefix = prefix.clone();
+ }
+
+ /**
+ * Build an IA_PD prefix option with given specific parameters.
+ */
+ public static ByteBuffer build(final short length, final long preferred, final long valid,
+ final short prefixLen, final byte[] prefix) {
+ final IaPrefixOption option = new IaPrefixOption((byte) DHCP6_OPTION_IAPREFIX,
+ length /* 25 + IAPrefix options length */, preferred, valid, prefixLen, prefix);
+ return ByteBuffer.wrap(option.writeToBytes(ByteOrder.BIG_ENDIAN));
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/wear/NetPacketHelpers.java b/staticlibs/device/com/android/net/module/util/wear/NetPacketHelpers.java
new file mode 100644
index 0000000..341c44b
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/wear/NetPacketHelpers.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.wear;
+
+import com.android.net.module.util.async.ReadableByteBuffer;
+
+/**
+ * Implements utilities for decoding parts of TCP/UDP/IP headers.
+ *
+ * @hide
+ */
+final class NetPacketHelpers {
+ static void encodeNetworkUnsignedInt16(int value, byte[] dst, final int dstPos) {
+ dst[dstPos] = (byte) ((value >> 8) & 0xFF);
+ dst[dstPos + 1] = (byte) (value & 0xFF);
+ }
+
+ static int decodeNetworkUnsignedInt16(byte[] data, final int pos) {
+ return ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF);
+ }
+
+ static int decodeNetworkUnsignedInt16(ReadableByteBuffer data, final int pos) {
+ return ((data.peek(pos) & 0xFF) << 8) | (data.peek(pos + 1) & 0xFF);
+ }
+
+ private NetPacketHelpers() {}
+}
diff --git a/staticlibs/device/com/android/net/module/util/wear/PacketFile.java b/staticlibs/device/com/android/net/module/util/wear/PacketFile.java
new file mode 100644
index 0000000..7f5ed78
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/wear/PacketFile.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.wear;
+
+/**
+ * Defines bidirectional file where all transmissions are made as complete packets.
+ *
+ * Automatically manages all readability and writeability events in EventManager:
+ * - When read buffer has more space - asks EventManager to notify on more data
+ * - When write buffer has more space - asks the user to provide more data
+ * - When underlying file cannot accept more data - registers EventManager callback
+ *
+ * @hide
+ */
+public interface PacketFile {
+ /** @hide */
+ public enum ErrorCode {
+ UNEXPECTED_ERROR,
+ IO_ERROR,
+ INBOUND_PACKET_TOO_LARGE,
+ OUTBOUND_PACKET_TOO_LARGE,
+ }
+
+ /**
+ * Receives notifications when new data or output space is available.
+ *
+ * @hide
+ */
+ public interface Listener {
+ /**
+ * Handles the initial part of the stream, which on some systems provides lower-level
+ * configuration data.
+ *
+ * Returns the number of bytes consumed, or zero if the preamble has been fully read.
+ */
+ int onPreambleData(byte[] data, int pos, int len);
+
+ /** Handles one extracted packet. */
+ void onInboundPacket(byte[] data, int pos, int len);
+
+ /** Notifies on new data being added to the buffer. */
+ void onInboundBuffered(int newByteCount, int totalBufferedSize);
+
+ /** Notifies on data being flushed from output buffer. */
+ void onOutboundPacketSpace();
+
+ /** Notifies on unrecoverable error in the packet processing. */
+ void onPacketFileError(ErrorCode error, String message);
+ }
+
+ /** Requests this file to be closed. */
+ void close();
+
+ /** Permanently disables reading of this file, and clears all buffered data. */
+ void shutdownReading();
+
+ /** Starts or resumes async read operations on this file. */
+ void continueReading();
+
+ /** Returns the number of bytes currently buffered as input. */
+ int getInboundBufferSize();
+
+ /** Returns the number of bytes currently available for buffering for output. */
+ int getOutboundFreeSize();
+
+ /**
+ * Queues the given data for output.
+ * Throws runtime exception if there is not enough space.
+ */
+ boolean enqueueOutboundPacket(byte[] data, int pos, int len);
+}
diff --git a/staticlibs/device/com/android/net/module/util/wear/StreamingPacketFile.java b/staticlibs/device/com/android/net/module/util/wear/StreamingPacketFile.java
new file mode 100644
index 0000000..52dbee4
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/wear/StreamingPacketFile.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.wear;
+
+import com.android.net.module.util.async.BufferedFile;
+import com.android.net.module.util.async.EventManager;
+import com.android.net.module.util.async.FileHandle;
+import com.android.net.module.util.async.Assertions;
+import com.android.net.module.util.async.ReadableByteBuffer;
+
+import java.io.IOException;
+
+/**
+ * Implements PacketFile based on a streaming file descriptor.
+ *
+ * Packets are delineated using network-order 2-byte length indicators.
+ *
+ * @hide
+ */
+public final class StreamingPacketFile implements PacketFile, BufferedFile.Listener {
+ private static final int HEADER_SIZE = 2;
+
+ private final EventManager mEventManager;
+ private final Listener mListener;
+ private final BufferedFile mFile;
+ private final int mMaxPacketSize;
+ private final ReadableByteBuffer mInboundBuffer;
+ private boolean mIsInPreamble = true;
+
+ private final byte[] mTempPacketReadBuffer;
+ private final byte[] mTempHeaderWriteBuffer;
+
+ public StreamingPacketFile(
+ EventManager eventManager,
+ FileHandle fileHandle,
+ Listener listener,
+ int maxPacketSize,
+ int maxBufferedInboundPackets,
+ int maxBufferedOutboundPackets) throws IOException {
+ if (eventManager == null || fileHandle == null || listener == null) {
+ throw new NullPointerException();
+ }
+
+ mEventManager = eventManager;
+ mListener = listener;
+ mMaxPacketSize = maxPacketSize;
+
+ final int maxTotalLength = HEADER_SIZE + maxPacketSize;
+
+ mFile = BufferedFile.create(eventManager, fileHandle, this,
+ maxTotalLength * maxBufferedInboundPackets,
+ maxTotalLength * maxBufferedOutboundPackets);
+ mInboundBuffer = mFile.getInboundBuffer();
+
+ mTempPacketReadBuffer = new byte[maxTotalLength];
+ mTempHeaderWriteBuffer = new byte[HEADER_SIZE];
+ }
+
+ @Override
+ public void close() {
+ mFile.close();
+ }
+
+ public BufferedFile getUnderlyingFileForTest() {
+ return mFile;
+ }
+
+ @Override
+ public void shutdownReading() {
+ mFile.shutdownReading();
+ }
+
+ @Override
+ public void continueReading() {
+ mFile.continueReading();
+ }
+
+ @Override
+ public int getInboundBufferSize() {
+ return mInboundBuffer.size();
+ }
+
+ @Override
+ public void onBufferedFileClosed() {
+ }
+
+ @Override
+ public void onBufferedFileInboundData(int readByteCount) {
+ if (mFile.isReadingShutdown()) {
+ return;
+ }
+
+ if (readByteCount > 0) {
+ mListener.onInboundBuffered(readByteCount, mInboundBuffer.size());
+ }
+
+ if (extractOnePacket() && !mFile.isReadingShutdown()) {
+ // There could be more packets already buffered, continue parsing next
+ // packet even before another read event comes
+ mEventManager.execute(() -> {
+ onBufferedFileInboundData(0);
+ });
+ } else {
+ continueReading();
+ }
+ }
+
+ private boolean extractOnePacket() {
+ while (mIsInPreamble) {
+ final int directReadSize = Math.min(
+ mInboundBuffer.getDirectReadSize(), mTempPacketReadBuffer.length);
+ if (directReadSize == 0) {
+ return false;
+ }
+
+ // Copy for safety, so higher-level callback cannot modify the data.
+ System.arraycopy(mInboundBuffer.getDirectReadBuffer(),
+ mInboundBuffer.getDirectReadPos(), mTempPacketReadBuffer, 0, directReadSize);
+
+ final int preambleConsumedBytes = mListener.onPreambleData(
+ mTempPacketReadBuffer, 0, directReadSize);
+ if (mFile.isReadingShutdown()) {
+ return false; // The callback has called shutdownReading().
+ }
+
+ if (preambleConsumedBytes == 0) {
+ mIsInPreamble = false;
+ break;
+ }
+
+ mInboundBuffer.accountForDirectRead(preambleConsumedBytes);
+ }
+
+ final int bufferedSize = mInboundBuffer.size();
+ if (bufferedSize < HEADER_SIZE) {
+ return false;
+ }
+
+ final int dataLength = NetPacketHelpers.decodeNetworkUnsignedInt16(mInboundBuffer, 0);
+ if (dataLength > mMaxPacketSize) {
+ mListener.onPacketFileError(
+ PacketFile.ErrorCode.INBOUND_PACKET_TOO_LARGE,
+ "Inbound packet length: " + dataLength);
+ return false;
+ }
+
+ final int totalLength = HEADER_SIZE + dataLength;
+ if (bufferedSize < totalLength) {
+ return false;
+ }
+
+ mInboundBuffer.readBytes(mTempPacketReadBuffer, 0, totalLength);
+
+ mListener.onInboundPacket(mTempPacketReadBuffer, HEADER_SIZE, dataLength);
+ return true;
+ }
+
+ @Override
+ public int getOutboundFreeSize() {
+ final int freeSize = mFile.getOutboundBufferFreeSize();
+ return (freeSize > HEADER_SIZE ? freeSize - HEADER_SIZE : 0);
+ }
+
+ @Override
+ public boolean enqueueOutboundPacket(byte[] buffer, int pos, int len) {
+ Assertions.throwsIfOutOfBounds(buffer, pos, len);
+
+ if (len == 0) {
+ return true;
+ }
+
+ if (len > mMaxPacketSize) {
+ mListener.onPacketFileError(
+ PacketFile.ErrorCode.OUTBOUND_PACKET_TOO_LARGE,
+ "Outbound packet length: " + len);
+ return false;
+ }
+
+ NetPacketHelpers.encodeNetworkUnsignedInt16(len, mTempHeaderWriteBuffer, 0);
+
+ mFile.enqueueOutboundData(
+ mTempHeaderWriteBuffer, 0, mTempHeaderWriteBuffer.length,
+ buffer, pos, len);
+ return true;
+ }
+
+ @Override
+ public void onBufferedFileOutboundSpace() {
+ mListener.onOutboundPacketSpace();
+ }
+
+ @Override
+ public void onBufferedFileIoError(String message) {
+ mListener.onPacketFileError(PacketFile.ErrorCode.IO_ERROR, message);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("maxPacket=");
+ sb.append(mMaxPacketSize);
+ sb.append(", file={");
+ sb.append(mFile);
+ sb.append("}");
+ return sb.toString();
+ }
+}
diff --git a/staticlibs/framework/com/android/net/module/util/BitUtils.java b/staticlibs/framework/com/android/net/module/util/BitUtils.java
index 2b32e86..3062d8c 100644
--- a/staticlibs/framework/com/android/net/module/util/BitUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/BitUtils.java
@@ -17,6 +17,7 @@
package com.android.net.module.util;
import android.annotation.NonNull;
+import android.annotation.Nullable;
/**
* @hide
@@ -107,4 +108,33 @@
++bitPos;
}
}
+
+ /**
+ * Returns a short but human-readable string of updates between an old and a new bit fields.
+ *
+ * @param oldVal the old bit field to diff from
+ * @param newVal the new bit field to diff to
+ * @return a string fit for logging differences, or null if no differences.
+ * this method cannot return the empty string.
+ */
+ @Nullable
+ public static String describeDifferences(final long oldVal, final long newVal,
+ @NonNull final NameOf nameFetcher) {
+ final long changed = oldVal ^ newVal;
+ if (0 == changed) return null;
+ // If the control reaches here, there are changes (additions, removals, or both) so
+ // the code below is guaranteed to add something to the string and can't return "".
+ final long removed = oldVal & changed;
+ final long added = newVal & changed;
+ final StringBuilder sb = new StringBuilder();
+ if (0 != removed) {
+ sb.append("-");
+ appendStringRepresentationOfBitMaskToStringBuilder(sb, removed, nameFetcher, "-");
+ }
+ if (0 != added) {
+ sb.append("+");
+ appendStringRepresentationOfBitMaskToStringBuilder(sb, added, nameFetcher, "+");
+ }
+ return sb.toString();
+ }
}
diff --git a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
index f08880c..39e7ce9 100644
--- a/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/CollectionUtils.java
@@ -101,7 +101,6 @@
/**
* @return The index of the first element that matches the predicate, or -1 if none.
*/
- @Nullable
public static <T> int indexOf(@NonNull final Collection<T> elem,
@NonNull final Predicate<? super T> predicate) {
int idx = 0;
diff --git a/staticlibs/device/com/android/net/module/util/HexDump.java b/staticlibs/framework/com/android/net/module/util/HexDump.java
similarity index 99%
rename from staticlibs/device/com/android/net/module/util/HexDump.java
rename to staticlibs/framework/com/android/net/module/util/HexDump.java
index a27c0a3..a22c258 100644
--- a/staticlibs/device/com/android/net/module/util/HexDump.java
+++ b/staticlibs/framework/com/android/net/module/util/HexDump.java
@@ -20,6 +20,8 @@
/**
* Hex utility functions.
+ *
+ * @hide
*/
public class HexDump {
private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
diff --git a/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java b/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
index 31d0729..40fc59f 100644
--- a/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
@@ -16,7 +16,10 @@
package com.android.net.module.util;
+import android.annotation.NonNull;
import android.os.Parcel;
+import android.util.Log;
+
import java.net.Inet6Address;
import java.net.InetAddress;
@@ -28,6 +31,7 @@
*/
public class InetAddressUtils {
+ private static final String TAG = InetAddressUtils.class.getSimpleName();
private static final int INET6_ADDR_LENGTH = 16;
/**
@@ -71,5 +75,23 @@
}
}
+ /**
+ * Create a Inet6Address with scope id if it is a link local address. Otherwise, returns the
+ * original address.
+ */
+ public static Inet6Address withScopeId(@NonNull final Inet6Address addr, int scopeid) {
+ if (!addr.isLinkLocalAddress()) {
+ return addr;
+ }
+ try {
+ return Inet6Address.getByAddress(null /* host */, addr.getAddress(),
+ scopeid);
+ } catch (UnknownHostException impossible) {
+ Log.wtf(TAG, "Cannot construct scoped Inet6Address with Inet6Address.getAddress("
+ + addr.getHostAddress() + "): ", impossible);
+ return null;
+ }
+ }
+
private InetAddressUtils() {}
}
diff --git a/staticlibs/framework/com/android/net/module/util/IpUtils.java b/staticlibs/framework/com/android/net/module/util/IpUtils.java
index 569733e..18d96f3 100644
--- a/staticlibs/framework/com/android/net/module/util/IpUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/IpUtils.java
@@ -16,6 +16,8 @@
package com.android.net.module.util;
+import com.android.internal.annotations.VisibleForTesting;
+
import static android.system.OsConstants.IPPROTO_ICMPV6;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
@@ -42,8 +44,9 @@
* payload) or ICMP checksum on the specified portion of a ByteBuffer. The seed
* allows the checksum to commence with a specified value.
*/
- private static int checksum(ByteBuffer buf, int seed, int start, int end) {
- int sum = seed;
+ @VisibleForTesting
+ public static int checksum(ByteBuffer buf, int seed, int start, int end) {
+ int sum = seed + 0xFFFF; // to make things work with empty / zero-filled buffer
final int bufPosition = buf.position();
// set position of original ByteBuffer, so that the ShortBuffer
@@ -69,13 +72,12 @@
b += 256;
}
- sum += b * 256;
+ sum += b * 256; // assumes bytebuffer is network order (ie. big endian)
}
- sum = ((sum >> 16) & 0xFFFF) + (sum & 0xFFFF);
- sum = ((sum + ((sum >> 16) & 0xFFFF)) & 0xFFFF);
- int negated = ~sum;
- return intAbs((short) negated);
+ sum = ((sum >> 16) & 0xFFFF) + (sum & 0xFFFF); // max sum is 0x1FFFE
+ sum = ((sum >> 16) & 0xFFFF) + (sum & 0xFFFF); // max sum is 0xFFFF
+ return sum ^ 0xFFFF; // u16 bitwise negation
}
private static int pseudoChecksumIPv4(
diff --git a/staticlibs/framework/com/android/net/module/util/LinkPropertiesUtils.java b/staticlibs/framework/com/android/net/module/util/LinkPropertiesUtils.java
index 1565f2b..e271f64 100644
--- a/staticlibs/framework/com/android/net/module/util/LinkPropertiesUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/LinkPropertiesUtils.java
@@ -146,6 +146,24 @@
right != null ? right.getLinkAddresses() : null);
}
+ /**
+ * Compares {@code left} {@code LinkProperties} allLinkAddresses against the {@code right}.
+ *
+ * @param left A LinkProperties or null
+ * @param right A LinkProperties or null
+ * @return {@code true} if both are identical, {@code false} otherwise.
+ * @see LinkProperties#getAllLinkAddresses()
+ */
+ public static boolean isIdenticalAllLinkAddresses(@Nullable LinkProperties left,
+ @Nullable LinkProperties right) {
+ if (left == right) return true;
+ if (left == null || right == null) return false;
+ final List<LinkAddress> leftAddresses = left.getAllLinkAddresses();
+ final List<LinkAddress> rightAddresses = right.getAllLinkAddresses();
+ if (leftAddresses.size() != rightAddresses.size()) return false;
+ return leftAddresses.containsAll(rightAddresses);
+ }
+
/**
* Compares {@code left} {@code LinkProperties} interface addresses against the {@code right}.
*
diff --git a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
index 7d4ae30..ba0cab8 100644
--- a/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/staticlibs/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -135,7 +135,7 @@
* - https://tools.ietf.org/html/rfc792
*/
public static final int ICMP_CHECKSUM_OFFSET = 2;
-
+ public static final int ICMP_HEADER_LEN = 8;
/**
* ICMPv6 constants.
*
@@ -210,17 +210,23 @@
*/
public static final int INFINITE_LEASE = 0xffffffff;
public static final int DHCP4_CLIENT_PORT = 68;
+ // The maximum length of a DHCP packet that can be constructed.
+ public static final int DHCP_MAX_LENGTH = 1500;
+ public static final int DHCP_MAX_OPTION_LEN = 255;
/**
* DHCPv6 constants.
*
* See also:
* - https://datatracker.ietf.org/doc/html/rfc8415
+ * - https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml
*/
public static final int DHCP6_CLIENT_PORT = 546;
public static final int DHCP6_SERVER_PORT = 547;
public static final Inet6Address ALL_DHCP_RELAY_AGENTS_AND_SERVERS =
(Inet6Address) InetAddresses.parseNumericAddress("ff02::1:2");
+ public static final int DHCP6_OPTION_IA_PD = 25;
+ public static final int DHCP6_OPTION_IAPREFIX = 26;
/**
* IEEE802.11 standard constants.
diff --git a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
index be5b0cd..8315b8f 100644
--- a/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/PermissionUtils.java
@@ -54,6 +54,20 @@
}
/**
+ * Return true if the context has one of give permission that is allowed
+ * for a particular process and user ID running in the system.
+ */
+ public static boolean checkAnyPermissionOf(@NonNull Context context,
+ int pid, int uid, @NonNull String... permissions) {
+ for (String permission : permissions) {
+ if (context.checkPermission(permission, pid, uid) == PERMISSION_GRANTED) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* Enforce permission check on the context that should have one of given permission.
*/
public static void enforceAnyPermissionOf(@NonNull Context context,
diff --git a/staticlibs/native/bpf_headers/BpfRingbufTest.cpp b/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
index 4a45a93..d23afae 100644
--- a/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
+++ b/staticlibs/native/bpf_headers/BpfRingbufTest.cpp
@@ -23,14 +23,20 @@
#include <unistd.h>
#include "BpfSyscallWrappers.h"
+#include "bpf/BpfRingbuf.h"
#include "bpf/BpfUtils.h"
+#define TEST_RINGBUF_MAGIC_NUM 12345
+
namespace android {
namespace bpf {
using ::android::base::testing::HasError;
using ::android::base::testing::HasValue;
-using ::android::base::testing::WithMessage;
+using ::android::base::testing::WithCode;
+using ::testing::AllOf;
+using ::testing::Gt;
using ::testing::HasSubstr;
+using ::testing::Lt;
class BpfRingbufTest : public ::testing::Test {
protected:
@@ -40,8 +46,11 @@
void SetUp() {
if (!android::bpf::isAtLeastKernelVersion(5, 8, 0)) {
- GTEST_SKIP() << "BPF ring buffers not supported";
- return;
+ GTEST_SKIP() << "BPF ring buffers not supported below 5.8";
+ }
+
+ if (sizeof(unsigned long) != 8) {
+ GTEST_SKIP() << "BPF ring buffers not supported on 32 bit arch";
}
errno = 0;
@@ -51,12 +60,82 @@
<< mProgPath << " was either not found or inaccessible.";
}
+ void RunProgram() {
+ char fake_skb[128] = {};
+ EXPECT_EQ(runProgram(mProgram, fake_skb, sizeof(fake_skb)), 0);
+ }
+
+ void RunTestN(int n) {
+ int run_count = 0;
+ uint64_t output = 0;
+ auto callback = [&](const uint64_t& value) {
+ output = value;
+ run_count++;
+ };
+
+ auto result = BpfRingbuf<uint64_t>::Create(mRingbufPath.c_str());
+ ASSERT_RESULT_OK(result);
+
+ for (int i = 0; i < n; i++) {
+ RunProgram();
+ }
+
+ EXPECT_THAT(result.value()->ConsumeAll(callback), HasValue(n));
+ EXPECT_EQ(output, TEST_RINGBUF_MAGIC_NUM);
+ EXPECT_EQ(run_count, n);
+ }
+
std::string mProgPath;
std::string mRingbufPath;
android::base::unique_fd mProgram;
};
-TEST_F(BpfRingbufTest, CheckSetUp) {}
+TEST_F(BpfRingbufTest, ConsumeSingle) { RunTestN(1); }
+TEST_F(BpfRingbufTest, ConsumeMultiple) { RunTestN(3); }
+
+TEST_F(BpfRingbufTest, FillAndWrap) {
+ int run_count = 0;
+ auto callback = [&](const uint64_t&) { run_count++; };
+
+ auto result = BpfRingbuf<uint64_t>::Create(mRingbufPath.c_str());
+ ASSERT_RESULT_OK(result);
+
+ // 4kb buffer with 16 byte payloads (8 byte data, 8 byte header) should fill
+ // after 255 iterations. Exceed that so that some events are dropped.
+ constexpr int iterations = 300;
+ for (int i = 0; i < iterations; i++) {
+ RunProgram();
+ }
+
+ // Some events were dropped, but consume all that succeeded.
+ EXPECT_THAT(result.value()->ConsumeAll(callback),
+ HasValue(AllOf(Gt(250), Lt(260))));
+ EXPECT_THAT(run_count, AllOf(Gt(250), Lt(260)));
+
+ // After consuming everything, we should be able to use the ring buffer again.
+ run_count = 0;
+ RunProgram();
+ EXPECT_THAT(result.value()->ConsumeAll(callback), HasValue(1));
+ EXPECT_EQ(run_count, 1);
+}
+
+TEST_F(BpfRingbufTest, WrongTypeSize) {
+ // The program under test writes 8-byte uint64_t values so a ringbuffer for
+ // 1-byte uint8_t values will fail to read from it. Note that the map_def does
+ // not specify the value size, so we fail on read, not creation.
+ auto result = BpfRingbuf<uint8_t>::Create(mRingbufPath.c_str());
+ ASSERT_RESULT_OK(result);
+
+ RunProgram();
+
+ EXPECT_THAT(result.value()->ConsumeAll([](const uint8_t&) {}),
+ HasError(WithCode(EMSGSIZE)));
+}
+
+TEST_F(BpfRingbufTest, InvalidPath) {
+ EXPECT_THAT(BpfRingbuf<int>::Create("/sys/fs/bpf/bad_path"),
+ HasError(WithCode(ENOENT)));
+}
} // namespace bpf
} // namespace android
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h b/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
new file mode 100644
index 0000000..9b38dee
--- /dev/null
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfClassic.h
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+// Accept the full packet
+#define BPF_ACCEPT BPF_STMT(BPF_RET | BPF_K, 0xFFFFFFFF)
+
+// Reject the packet
+#define BPF_REJECT BPF_STMT(BPF_RET | BPF_K, 0)
+
+// *TWO* instructions: compare and if equal jump over the reject statement
+#define BPF2_REJECT_IF_NOT_EQUAL(v) \
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, (v), 1, 0), \
+ BPF_REJECT
+
+// 8-bit load relative to start of link layer (mac/ethernet) header.
+#define BPF_LOAD_MAC_RELATIVE_U8(ofs) \
+ BPF_STMT(BPF_LD | BPF_B | BPF_ABS, (__u32)SKF_LL_OFF + (ofs))
+
+// Big/Network Endian 16-bit load relative to start of link layer (mac/ethernet) header.
+#define BPF_LOAD_MAC_RELATIVE_BE16(ofs) \
+ BPF_STMT(BPF_LD | BPF_H | BPF_ABS, (__u32)SKF_LL_OFF + (ofs))
+
+// Big/Network Endian 32-bit load relative to start of link layer (mac/ethernet) header.
+#define BPF_LOAD_MAC_RELATIVE_BE32(ofs) \
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (__u32)SKF_LL_OFF + (ofs))
+
+// 8-bit load relative to start of network (IPv4/IPv6) header.
+#define BPF_LOAD_NET_RELATIVE_U8(ofs) \
+ BPF_STMT(BPF_LD | BPF_B | BPF_ABS, (__u32)SKF_NET_OFF + (ofs))
+
+// Big/Network Endian 16-bit load relative to start of network (IPv4/IPv6) header.
+#define BPF_LOAD_NET_RELATIVE_BE16(ofs) \
+ BPF_STMT(BPF_LD | BPF_H | BPF_ABS, (__u32)SKF_NET_OFF + (ofs))
+
+// Big/Network Endian 32-bit load relative to start of network (IPv4/IPv6) header.
+#define BPF_LOAD_NET_RELATIVE_BE32(ofs) \
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (__u32)SKF_NET_OFF + (ofs))
+
+#define field_sizeof(struct_type,field) sizeof(((struct_type *)0)->field)
+
+// 8-bit load from IPv4 header field.
+#define BPF_LOAD_IPV4_U8(field) \
+ BPF_LOAD_NET_RELATIVE_U8(({ \
+ _Static_assert(field_sizeof(struct iphdr, field) == 1, "field of wrong size"); \
+ offsetof(iphdr, field); \
+ }))
+
+// Big/Network Endian 16-bit load from IPv4 header field.
+#define BPF_LOAD_IPV4_BE16(field) \
+ BPF_LOAD_NET_RELATIVE_BE16(({ \
+ _Static_assert(field_sizeof(struct iphdr, field) == 2, "field of wrong size"); \
+ offsetof(iphdr, field); \
+ }))
+
+// Big/Network Endian 32-bit load from IPv4 header field.
+#define BPF_LOAD_IPV4_BE32(field) \
+ BPF_LOAD_NET_RELATIVE_BE32(({ \
+ _Static_assert(field_sizeof(struct iphdr, field) == 4, "field of wrong size"); \
+ offsetof(iphdr, field); \
+ }))
+
+// 8-bit load from IPv6 header field.
+#define BPF_LOAD_IPV6_U8(field) \
+ BPF_LOAD_NET_RELATIVE_U8(({ \
+ _Static_assert(field_sizeof(struct ipv6hdr, field) == 1, "field of wrong size"); \
+ offsetof(ipv6hdr, field); \
+ }))
+
+// Big/Network Endian 16-bit load from IPv6 header field.
+#define BPF_LOAD_IPV6_BE16(field) \
+ BPF_LOAD_NET_RELATIVE_BE16(({ \
+ _Static_assert(field_sizeof(struct ipv6hdr, field) == 2, "field of wrong size"); \
+ offsetof(ipv6hdr, field); \
+ }))
+
+// Big/Network Endian 32-bit load from IPv6 header field.
+#define BPF_LOAD_IPV6_BE32(field) \
+ BPF_LOAD_NET_RELATIVE_BE32(({ \
+ _Static_assert(field_sizeof(struct ipv6hdr, field) == 4, "field of wrong size"); \
+ offsetof(ipv6hdr, field); \
+ }))
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
new file mode 100644
index 0000000..cac1e43
--- /dev/null
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfRingbuf.h
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+#pragma once
+
+#include <android-base/result.h>
+#include <android-base/unique_fd.h>
+#include <linux/bpf.h>
+#include <sys/mman.h>
+#include <utils/Log.h>
+
+#include "bpf/BpfUtils.h"
+
+namespace android {
+namespace bpf {
+
+// BpfRingbufBase contains the non-templated functionality of BPF ring buffers.
+class BpfRingbufBase {
+ public:
+ ~BpfRingbufBase() {
+ if (mConsumerPos) munmap(mConsumerPos, mConsumerSize);
+ if (mProducerPos) munmap(mProducerPos, mProducerSize);
+ mConsumerPos = nullptr;
+ mProducerPos = nullptr;
+ }
+
+ protected:
+ // Non-initializing constructor, used by Create.
+ BpfRingbufBase(size_t value_size) : mValueSize(value_size) {}
+
+ // Full construction that aborts on error (use Create/Init to handle errors).
+ BpfRingbufBase(const char* path, size_t value_size) : mValueSize(value_size) {
+ if (auto status = Init(path); !status.ok()) {
+ ALOGE("BpfRingbuf init failed: %s", status.error().message().c_str());
+ abort();
+ }
+ }
+
+ // Delete copy constructor (class owns raw pointers).
+ BpfRingbufBase(const BpfRingbufBase&) = delete;
+
+ // Initialize the base ringbuffer components. Must be called exactly once.
+ base::Result<void> Init(const char* path);
+
+ // Consumes all messages from the ring buffer, passing them to the callback.
+ base::Result<int> ConsumeAll(
+ const std::function<void(const void*)>& callback);
+
+ // Replicates c-style void* "byte-wise" pointer addition.
+ template <typename Ptr>
+ static Ptr pointerAddBytes(void* base, ssize_t offset_bytes) {
+ return reinterpret_cast<Ptr>(reinterpret_cast<char*>(base) + offset_bytes);
+ }
+
+ // Rounds len by clearing bitmask, adding header, and aligning to 8 bytes.
+ static uint32_t roundLength(uint32_t len) {
+ len &= ~(BPF_RINGBUF_BUSY_BIT | BPF_RINGBUF_DISCARD_BIT);
+ len += BPF_RINGBUF_HDR_SZ;
+ return (len + 7) & ~7;
+ }
+
+ const size_t mValueSize;
+
+ size_t mConsumerSize;
+ size_t mProducerSize;
+ unsigned long mPosMask;
+ android::base::unique_fd mRingFd;
+
+ void* mDataPos = nullptr;
+ unsigned long* mConsumerPos = nullptr;
+ unsigned long* mProducerPos = nullptr;
+};
+
+// This is a class wrapper for eBPF ring buffers. An eBPF ring buffer is a
+// special type of eBPF map used for sending messages from eBPF to userspace.
+// The implementation relies on fast shared memory and atomics for the producer
+// and consumer management. Ring buffers are a faster alternative to eBPF perf
+// buffers.
+//
+// This class is thread compatible, but not thread safe.
+//
+// Note: A kernel eBPF ring buffer may be accessed by both kernel and userspace
+// processes at the same time. However, the userspace consumers of a given ring
+// buffer all share a single read pointer. There is no guarantee which readers
+// will read which messages.
+template <typename Value>
+class BpfRingbuf : public BpfRingbufBase {
+ public:
+ using MessageCallback = std::function<void(const Value&)>;
+
+ // Creates a ringbuffer wrapper from a pinned path. This initialization will
+ // abort on error. To handle errors, initialize with Create instead.
+ BpfRingbuf(const char* path) : BpfRingbufBase(path, sizeof(Value)) {}
+
+ // Creates a ringbuffer wrapper from a pinned path. There are no guarantees
+ // that the ringbuf outputs messaged of type `Value`, only that they are the
+ // same size. Size is only checked in ConsumeAll.
+ static base::Result<std::unique_ptr<BpfRingbuf<Value>>> Create(
+ const char* path);
+
+ // Consumes all messages from the ring buffer, passing them to the callback.
+ // Returns the number of messages consumed or a non-ok result on error. If the
+ // ring buffer has no pending messages an OK result with count 0 is returned.
+ base::Result<int> ConsumeAll(const MessageCallback& callback);
+
+ private:
+ // Empty ctor for use by Create.
+ BpfRingbuf() : BpfRingbufBase(sizeof(Value)) {}
+};
+
+#define ACCESS_ONCE(x) (*(volatile typeof(x)*)&(x))
+
+#if defined(__i386__) || defined(__x86_64__)
+#define smp_sync() asm volatile("" ::: "memory")
+#elif defined(__aarch64__)
+#define smp_sync() asm volatile("dmb ish" ::: "memory")
+#else
+#define smp_sync() __sync_synchronize()
+#endif
+
+#define smp_store_release(p, v) \
+ do { \
+ smp_sync(); \
+ ACCESS_ONCE(*(p)) = (v); \
+ } while (0)
+
+#define smp_load_acquire(p) \
+ ({ \
+ auto ___p = ACCESS_ONCE(*(p)); \
+ smp_sync(); \
+ ___p; \
+ })
+
+inline base::Result<void> BpfRingbufBase::Init(const char* path) {
+ if (sizeof(unsigned long) != 8) {
+ return android::base::Error()
+ << "BpfRingbuf does not support 32 bit architectures";
+ }
+ mRingFd.reset(mapRetrieveRW(path));
+ if (!mRingFd.ok()) {
+ return android::base::ErrnoError()
+ << "failed to retrieve ringbuffer at " << path;
+ }
+
+ int map_type = android::bpf::bpfGetFdMapType(mRingFd);
+ if (map_type != BPF_MAP_TYPE_RINGBUF) {
+ errno = EINVAL;
+ return android::base::ErrnoError()
+ << "bpf map has wrong type: want BPF_MAP_TYPE_RINGBUF ("
+ << BPF_MAP_TYPE_RINGBUF << ") got " << map_type;
+ }
+
+ int max_entries = android::bpf::bpfGetFdMaxEntries(mRingFd);
+ if (max_entries < 0) {
+ return android::base::ErrnoError()
+ << "failed to read max_entries from ringbuf";
+ }
+ if (max_entries == 0) {
+ errno = EINVAL;
+ return android::base::ErrnoError() << "max_entries must be non-zero";
+ }
+
+ mPosMask = max_entries - 1;
+ mConsumerSize = getpagesize();
+ mProducerSize = getpagesize() + 2 * max_entries;
+
+ {
+ void* ptr = mmap(NULL, mConsumerSize, PROT_READ | PROT_WRITE, MAP_SHARED,
+ mRingFd, 0);
+ if (ptr == MAP_FAILED) {
+ return android::base::ErrnoError()
+ << "failed to mmap ringbuf consumer pages";
+ }
+ mConsumerPos = reinterpret_cast<unsigned long*>(ptr);
+ }
+
+ {
+ void* ptr = mmap(NULL, mProducerSize, PROT_READ, MAP_SHARED, mRingFd,
+ mConsumerSize);
+ if (ptr == MAP_FAILED) {
+ return android::base::ErrnoError()
+ << "failed to mmap ringbuf producer page";
+ }
+ mProducerPos = reinterpret_cast<unsigned long*>(ptr);
+ }
+
+ mDataPos = pointerAddBytes<void*>(mProducerPos, getpagesize());
+ return {};
+}
+
+inline base::Result<int> BpfRingbufBase::ConsumeAll(
+ const std::function<void(const void*)>& callback) {
+ int64_t count = 0;
+ unsigned long cons_pos = smp_load_acquire(mConsumerPos);
+ unsigned long prod_pos = smp_load_acquire(mProducerPos);
+ while (cons_pos < prod_pos) {
+ // Find the start of the entry for this read (wrapping is done here).
+ void* start_ptr = pointerAddBytes<void*>(mDataPos, cons_pos & mPosMask);
+
+ // The entry has an 8 byte header containing the sample length.
+ uint32_t length = smp_load_acquire(reinterpret_cast<uint32_t*>(start_ptr));
+
+ // If the sample isn't committed, we're caught up with the producer.
+ if (length & BPF_RINGBUF_BUSY_BIT) return count;
+
+ cons_pos += roundLength(length);
+
+ if ((length & BPF_RINGBUF_DISCARD_BIT) == 0) {
+ if (length != mValueSize) {
+ smp_store_release(mConsumerPos, cons_pos);
+ errno = EMSGSIZE;
+ return android::base::ErrnoError()
+ << "BPF ring buffer message has unexpected size (want "
+ << mValueSize << " bytes, got " << length << " bytes)";
+ }
+ callback(pointerAddBytes<const void*>(start_ptr, BPF_RINGBUF_HDR_SZ));
+ count++;
+ }
+
+ smp_store_release(mConsumerPos, cons_pos);
+ }
+
+ return count;
+}
+
+template <typename Value>
+inline base::Result<std::unique_ptr<BpfRingbuf<Value>>>
+BpfRingbuf<Value>::Create(const char* path) {
+ auto rb = std::unique_ptr<BpfRingbuf>(new BpfRingbuf);
+ if (auto status = rb->Init(path); !status.ok()) return status.error();
+ return rb;
+}
+
+template <typename Value>
+inline base::Result<int> BpfRingbuf<Value>::ConsumeAll(
+ const MessageCallback& callback) {
+ return BpfRingbufBase::ConsumeAll([&](const void* value) {
+ callback(*reinterpret_cast<const Value*>(value));
+ });
+}
+
+#undef ACCESS_ONCE
+#undef smp_sync
+#undef smp_store_release
+#undef smp_load_acquire
+
+} // namespace bpf
+} // namespace android
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h b/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
index e2cb676..99c7a91 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
@@ -33,17 +33,26 @@
namespace android {
namespace bpf {
+// See kernel's net/core/sock_diag.c __sock_gen_cookie()
+// the implementation of which guarantees 0 will never be returned,
+// primarily because 0 is used to mean not yet initialized,
+// and socket cookies are only assigned on first fetch.
constexpr const uint64_t NONEXISTENT_COOKIE = 0;
static inline uint64_t getSocketCookie(int sockFd) {
uint64_t sock_cookie;
socklen_t cookie_len = sizeof(sock_cookie);
- int res = getsockopt(sockFd, SOL_SOCKET, SO_COOKIE, &sock_cookie, &cookie_len);
- if (res < 0) {
- res = -errno;
- ALOGE("Failed to get socket cookie: %s\n", strerror(errno));
- errno = -res;
- // 0 is an invalid cookie. See sock_gen_cookie.
+ if (getsockopt(sockFd, SOL_SOCKET, SO_COOKIE, &sock_cookie, &cookie_len)) {
+ // Failure is almost certainly either EBADF or ENOTSOCK
+ const int err = errno;
+ ALOGE("Failed to get socket cookie: %s\n", strerror(err));
+ errno = err;
+ return NONEXISTENT_COOKIE;
+ }
+ if (cookie_len != sizeof(sock_cookie)) {
+ // This probably cannot actually happen, but...
+ ALOGE("Failed to get socket cookie: len %d != 8\n", cookie_len);
+ errno = 523; // EBADCOOKIE: kernel internal, seems reasonable enough...
return NONEXISTENT_COOKIE;
}
return sock_cookie;
@@ -54,21 +63,22 @@
// 4.9 kernels. The kernel code of socket release on pf_key socket will
// explicitly call synchronize_rcu() which is exactly what we need.
//
- // Linux 4.14/4.19/5.4/5.10/5.15 (and 5.18) still have this same behaviour.
+ // Linux 4.14/4.19/5.4/5.10/5.15/6.1 (and 6.3-rc5) still have this same behaviour.
// see net/key/af_key.c: pfkey_release() -> synchronize_rcu()
- int pfSocket = socket(AF_KEY, SOCK_RAW | SOCK_CLOEXEC, PF_KEY_V2);
+ // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/key/af_key.c?h=v6.3-rc5#n185
+ const int pfSocket = socket(AF_KEY, SOCK_RAW | SOCK_CLOEXEC, PF_KEY_V2);
if (pfSocket < 0) {
- int ret = -errno;
- ALOGE("create PF_KEY socket failed: %s", strerror(errno));
- return ret;
+ const int err = errno;
+ ALOGE("create PF_KEY socket failed: %s", strerror(err));
+ return -err;
}
// When closing socket, synchronize_rcu() gets called in sock_release().
if (close(pfSocket)) {
- int ret = -errno;
- ALOGE("failed to close the PF_KEY socket: %s", strerror(errno));
- return ret;
+ const int err = errno;
+ ALOGE("failed to close the PF_KEY socket: %s", strerror(err));
+ return -err;
}
return 0;
}
@@ -79,10 +89,8 @@
.rlim_cur = 1073741824, // 1 GiB
.rlim_max = 1073741824, // 1 GiB
};
- int res = setrlimit(RLIMIT_MEMLOCK, &limit);
- if (res) {
- ALOGE("Failed to set the default MEMLOCK rlimit: %s", strerror(errno));
- }
+ const int res = setrlimit(RLIMIT_MEMLOCK, &limit);
+ if (res) ALOGE("Failed to set the default MEMLOCK rlimit: %s", strerror(errno));
return res;
}
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index ed1ee51..70c0f89 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -27,13 +27,6 @@
// Android S / 12 (api level 31) - added 'tethering' mainline eBPF support
#define BPFLOADER_S_VERSION 2u
-// Android T / 13 Beta 3 (api level 33) - added support for 'netd_shared'
-#define BPFLOADER_T_BETA3_VERSION 13u
-
-// v0.18 added support for shared and pindir, but still ignores selinux_content
-// v0.19 added support for selinux_content along with the required selinux changes
-// and should be available starting with Android T Beta 4
-//
// Android T / 13 (api level 33) - support for shared/selinux_context/pindir
#define BPFLOADER_T_VERSION 19u
@@ -43,6 +36,9 @@
// Bpfloader v0.33+ supports {map,prog}.ignore_on_{eng,user,userdebug}
#define BPFLOADER_IGNORED_ON_VERSION 33u
+// Android U / 14 (api level 34) - various new program types added
+#define BPFLOADER_U_VERSION 37u
+
/* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
* before #include "bpf_helpers.h" to change which bpfloaders will
* process the resulting .o file.
@@ -225,7 +221,7 @@
#ifdef THIS_BPF_PROGRAM_IS_FOR_TEST_PURPOSES_ONLY
#define BPF_MAP_ASSERT_OK(type, entries, mode)
-#elif BPFLOADER_MIN_VER >= BPFLOADER_T_BETA3_VERSION
+#elif BPFLOADER_MIN_VER >= BPFLOADER_T_VERSION
#define BPF_MAP_ASSERT_OK(type, entries, mode)
#else
#define BPF_MAP_ASSERT_OK(type, entries, mode) \
@@ -235,11 +231,12 @@
/* type safe macro to declare a map and related accessor functions */
#define DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
- selinux, pindir, share) \
+ selinux, pindir, share, min_loader, max_loader, ignore_eng, \
+ ignore_user, ignore_userdebug) \
DEFINE_BPF_MAP_BASE(the_map, TYPE, sizeof(KeyType), sizeof(ValueType), \
num_entries, usr, grp, md, selinux, pindir, share, \
- KVER_NONE, KVER_INF, BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, \
- false, false, false); \
+ KVER_NONE, KVER_INF, min_loader, max_loader, \
+ ignore_eng, ignore_user, ignore_userdebug); \
BPF_MAP_ASSERT_OK(BPF_MAP_TYPE_##TYPE, (num_entries), (md)); \
BPF_ANNOTATE_KV_PAIR(the_map, KeyType, ValueType); \
\
@@ -271,9 +268,11 @@
#error "Bpf Map UID must be left at default of AID_ROOT for BpfLoader prior to v0.28"
#endif
-#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
- DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
- DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, false)
+#define DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md) \
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, KeyType, ValueType, num_entries, usr, grp, md, \
+ DEFAULT_BPF_MAP_SELINUX_CONTEXT, DEFAULT_BPF_MAP_PIN_SUBDIR, false, \
+ BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, /*ignore_on_eng*/false, \
+ /*ignore_on_user*/false, /*ignore_on_userdebug*/false)
#define DEFINE_BPF_MAP(the_map, TYPE, KeyType, ValueType, num_entries) \
DEFINE_BPF_MAP_UGM(the_map, TYPE, KeyType, ValueType, num_entries, \
@@ -302,6 +301,8 @@
static int (*bpf_probe_read)(void* dst, int size, void* unsafe_ptr) = (void*) BPF_FUNC_probe_read;
static int (*bpf_probe_read_str)(void* dst, int size, void* unsafe_ptr) = (void*) BPF_FUNC_probe_read_str;
+static int (*bpf_probe_read_user)(void* dst, int size, const void* unsafe_ptr) = (void*)BPF_FUNC_probe_read_user;
+static int (*bpf_probe_read_user_str)(void* dst, int size, const void* unsafe_ptr) = (void*) BPF_FUNC_probe_read_user_str;
static unsigned long long (*bpf_ktime_get_ns)(void) = (void*) BPF_FUNC_ktime_get_ns;
static unsigned long long (*bpf_ktime_get_boot_ns)(void) = (void*)BPF_FUNC_ktime_get_boot_ns;
static int (*bpf_trace_printk)(const char* fmt, int fmt_size, ...) = (void*) BPF_FUNC_trace_printk;
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
index d286eba..65540e0 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_map_def.h
@@ -48,9 +48,8 @@
#define DEFAULT_SIZEOF_BPF_MAP_DEF 32 // v0.0 struct: enum (uint sized) + 7 uint
#define DEFAULT_SIZEOF_BPF_PROG_DEF 20 // v0.0 struct: 4 uint + bool + 3 byte alignment pad
-// By default, unless otherwise specified, allow the use of features only supported by v0.28,
-// which first added working support for map uid != root
-#define COMPILE_FOR_BPFLOADER_VERSION 28u
+// By default, unless otherwise specified, allow the use of features only supported by v0.37.
+#define COMPILE_FOR_BPFLOADER_VERSION 37u
/*
* The bpf_{map,prog}_def structures are compiled for different architectures.
diff --git a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h b/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
index f7d6a38..8502961 100644
--- a/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
+++ b/staticlibs/native/bpf_syscall_wrappers/include/BpfSyscallWrappers.h
@@ -150,6 +150,18 @@
});
}
+// Available in 4.12 and later kernels.
+inline int runProgram(const BPF_FD_TYPE prog_fd, const void* data,
+ const uint32_t data_size) {
+ return bpf(BPF_PROG_RUN, {
+ .test = {
+ .prog_fd = BPF_FD_TO_U32(prog_fd),
+ .data_in = ptr_to_u64(data),
+ .data_size_in = data_size,
+ },
+ });
+}
+
// BPF_OBJ_GET_INFO_BY_FD requires 4.14+ kernel
//
// Note: some fields are only defined in newer kernels (ie. the map_info struct grows
diff --git a/staticlibs/native/ip_checksum/checksum.c b/staticlibs/native/ip_checksum/checksum.c
index 04217a7..5641fad 100644
--- a/staticlibs/native/ip_checksum/checksum.c
+++ b/staticlibs/native/ip_checksum/checksum.c
@@ -32,20 +32,16 @@
* len - length of data
*/
uint32_t ip_checksum_add(uint32_t current, const void* data, int len) {
- uint32_t checksum = current;
- int left = len;
const uint16_t* data_16 = data;
- while (left > 1) {
- checksum += *data_16;
+ while (len >= 2) {
+ current += *data_16;
data_16++;
- left -= 2;
+ len -= 2;
}
- if (left) {
- checksum += *(uint8_t*)data_16;
- }
+ if (len) current += *(uint8_t*)data_16; // assumes little endian!
- return checksum;
+ return current;
}
/* function: ip_checksum_fold
@@ -54,9 +50,8 @@
* returns: the folded checksum in network byte order
*/
uint16_t ip_checksum_fold(uint32_t temp_sum) {
- while (temp_sum > 0xffff) {
- temp_sum = (temp_sum >> 16) + (temp_sum & 0xFFFF);
- }
+ temp_sum = (temp_sum >> 16) + (temp_sum & 0xFFFF);
+ temp_sum = (temp_sum >> 16) + (temp_sum & 0xFFFF);
return temp_sum;
}
@@ -75,12 +70,7 @@
* len - length of data
*/
uint16_t ip_checksum(const void* data, int len) {
- // TODO: consider starting from 0xffff so the checksum of a buffer entirely consisting of zeros
- // is correctly calculated as 0.
- uint32_t temp_sum;
-
- temp_sum = ip_checksum_add(0, data, len);
- return ip_checksum_finish(temp_sum);
+ return ip_checksum_finish(ip_checksum_add(0xFFFF, data, len));
}
/* function: ipv6_pseudo_header_checksum
@@ -92,7 +82,6 @@
uint32_t ipv6_pseudo_header_checksum(const struct ip6_hdr* ip6, uint32_t len, uint8_t protocol) {
uint32_t checksum_len = htonl(len);
uint32_t checksum_next = htonl(protocol);
-
uint32_t current = 0;
current = ip_checksum_add(current, &(ip6->ip6_src), sizeof(struct in6_addr));
@@ -109,11 +98,8 @@
* len - the transport length (transport header + payload)
*/
uint32_t ipv4_pseudo_header_checksum(const struct iphdr* ip, uint16_t len) {
- uint16_t temp_protocol, temp_length;
-
- temp_protocol = htons(ip->protocol);
- temp_length = htons(len);
-
+ uint16_t temp_protocol = htons(ip->protocol);
+ uint16_t temp_length = htons(len);
uint32_t current = 0;
current = ip_checksum_add(current, &(ip->saddr), sizeof(uint32_t));
@@ -135,7 +121,7 @@
// Algorithm suggested in RFC 1624.
// http://tools.ietf.org/html/rfc1624#section-3
checksum = ~checksum;
- uint16_t folded_sum = ip_checksum_fold(checksum + new_hdr_sum);
+ uint16_t folded_sum = ip_checksum_fold(new_hdr_sum + checksum);
uint16_t folded_old = ip_checksum_fold(old_hdr_sum);
if (folded_sum > folded_old) {
return ~(folded_sum - folded_old);
diff --git a/staticlibs/native/ip_checksum/checksum.h b/staticlibs/native/ip_checksum/checksum.h
index 868217c..87393c9 100644
--- a/staticlibs/native/ip_checksum/checksum.h
+++ b/staticlibs/native/ip_checksum/checksum.h
@@ -12,11 +12,8 @@
* 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.
- *
- * checksum.h - checksum functions
*/
-#ifndef __CHECKSUM_H__
-#define __CHECKSUM_H__
+#pragma once
#include <netinet/ip.h>
#include <netinet/ip6.h>
@@ -30,5 +27,3 @@
uint32_t ipv4_pseudo_header_checksum(const struct iphdr* ip, uint16_t len);
uint16_t ip_checksum_adjust(uint16_t checksum, uint32_t old_hdr_sum, uint32_t new_hdr_sum);
-
-#endif /* __CHECKSUM_H__ */
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 6e223bd..40371e6 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -21,6 +21,7 @@
"net-utils-device-common-async",
"net-utils-device-common-bpf",
"net-utils-device-common-ip",
+ "net-utils-device-common-wear",
],
libs: [
"android.test.runner",
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/BitUtilsTests.kt b/staticlibs/tests/unit/src/com/android/net/module/util/BitUtilsTests.kt
index 0236716..49940ea 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/BitUtilsTests.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/BitUtilsTests.kt
@@ -17,11 +17,13 @@
package com.android.net.module.util
import com.android.net.module.util.BitUtils.appendStringRepresentationOfBitMaskToStringBuilder
+import com.android.net.module.util.BitUtils.describeDifferences
import com.android.net.module.util.BitUtils.packBits
import com.android.net.module.util.BitUtils.unpackBits
-import org.junit.Test
import kotlin.test.assertEquals
+import kotlin.test.assertNull
import kotlin.test.assertTrue
+import org.junit.Test
class BitUtilsTests {
@Test
@@ -58,4 +60,23 @@
assertEquals(expected, it.toString())
}
}
+
+ @Test
+ fun testDescribeDifferences() {
+ fun describe(a: Long, b: Long) = describeDifferences(a, b, Integer::toString)
+ assertNull(describe(0, 0))
+ assertNull(describe(5, 5))
+ assertNull(describe(Long.MAX_VALUE, Long.MAX_VALUE))
+
+ assertEquals("+0", describe(0, 1))
+ assertEquals("-0", describe(1, 0))
+
+ assertEquals("+0+2", describe(0, 5))
+ assertEquals("+2", describe(1, 5))
+ assertEquals("-0+2", describe(1, 4))
+
+ fun makeField(vararg i: Int) = i.sumOf { 1L shl it }
+ assertEquals("-0-4-6-9+1+3+11", describe(makeField(0, 4, 6, 9), makeField(1, 3, 11)))
+ assertEquals("-1-5-9+6+8", describe(makeField(0, 1, 3, 4, 5, 9), makeField(0, 3, 4, 6, 8)))
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
index 302388d..b75939b 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/DeviceConfigUtilsTest.java
@@ -16,25 +16,30 @@
package com.android.net.module.util;
+import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-import static com.android.net.module.util.DeviceConfigUtils.FIXED_PACKAGE_VERSION;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.content.Context;
-import android.content.pm.ModuleInfo;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.provider.DeviceConfig;
@@ -49,6 +54,8 @@
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoSession;
+import java.util.Arrays;
+
/**
* Tests for DeviceConfigUtils.
@@ -66,14 +73,23 @@
private static final int TEST_MIN_FLAG_VALUE = 100;
private static final long TEST_PACKAGE_VERSION = 290000000;
private static final String TEST_PACKAGE_NAME = "test.package.name";
- private static final String TETHERING_AOSP_PACKAGE_NAME = "com.android.networkstack.tethering";
- private static final String TEST_APEX_NAME = "test.apex.name";
+ // The APEX name is the name of the APEX module, as in android.content.pm.ModuleInfo, and is
+ // used for its mount point in /apex. APEX packages are actually APKs with a different
+ // file extension, so they have an AndroidManifest: the APEX package name is the package name in
+ // that manifest, and is reflected in android.content.pm.ApplicationInfo. Contrary to the APEX
+ // (module) name, different package names are typically used to identify the organization that
+ // built and signed the APEX modules.
+ private static final String TEST_APEX_NAME = "com.android.tethering";
+ private static final String TEST_APEX_PACKAGE_NAME = "com.prefix.android.tethering";
+ private static final String TEST_GO_APEX_PACKAGE_NAME = "com.prefix.android.go.tethering";
+ private static final String TEST_CONNRES_PACKAGE_NAME =
+ "com.prefix.android.connectivity.resources";
+ private final PackageInfo mPackageInfo = new PackageInfo();
+ private final PackageInfo mApexPackageInfo = new PackageInfo();
private MockitoSession mSession;
@Mock private Context mContext;
@Mock private PackageManager mPm;
- @Mock private ModuleInfo mMi;
- @Mock private PackageInfo mPi;
@Mock private Resources mResources;
@Before
@@ -81,15 +97,26 @@
MockitoAnnotations.initMocks(this);
mSession = mockitoSession().spyStatic(DeviceConfig.class).startMocking();
- final PackageInfo pi = new PackageInfo();
- pi.setLongVersionCode(TEST_PACKAGE_VERSION);
+ mPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
+ mApexPackageInfo.setLongVersionCode(TEST_PACKAGE_VERSION);
doReturn(mPm).when(mContext).getPackageManager();
doReturn(TEST_PACKAGE_NAME).when(mContext).getPackageName();
- doReturn(mMi).when(mPm).getModuleInfo(eq(TEST_APEX_NAME), anyInt());
- doReturn(TEST_PACKAGE_NAME).when(mMi).getPackageName();
- doReturn(pi).when(mPm).getPackageInfo(anyString(), anyInt());
+ doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(anyString(), anyInt());
+ doReturn(mPackageInfo).when(mPm).getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt());
+ doReturn(mApexPackageInfo).when(mPm).getPackageInfo(eq(TEST_APEX_PACKAGE_NAME), anyInt());
+
doReturn(mResources).when(mContext).getResources();
+
+ final ResolveInfo ri = new ResolveInfo();
+ ri.activityInfo = new ActivityInfo();
+ ri.activityInfo.applicationInfo = new ApplicationInfo();
+ ri.activityInfo.applicationInfo.packageName = TEST_CONNRES_PACKAGE_NAME;
+ ri.activityInfo.applicationInfo.sourceDir =
+ "/apex/com.android.tethering/priv-app/ServiceConnectivityResources@version";
+ doReturn(Arrays.asList(ri)).when(mPm).queryIntentActivities(argThat(
+ intent -> intent.getAction().equals(DeviceConfigUtils.RESOURCES_APK_INTENT)),
+ eq(MATCH_SYSTEM_ONLY));
}
@After
@@ -223,39 +250,32 @@
TEST_EXPERIMENT_FLAG));
assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
- assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
- TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, true /* defaultEnabled */));
- doThrow(NameNotFoundException.class).when(mPm).getModuleInfo(anyString(), anyInt());
- assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
- TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
- assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
- TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, true /* defaultEnabled */));
}
-
@Test
- public void testFeatureIsEnabledUsingFixedVersion() throws Exception {
- doReturn(TETHERING_AOSP_PACKAGE_NAME).when(mContext).getPackageName();
- doThrow(NameNotFoundException.class).when(mPm).getModuleInfo(anyString(), anyInt());
-
- doReturn(Long.toString(FIXED_PACKAGE_VERSION)).when(() -> DeviceConfig.getProperty(
+ public void testFeatureIsEnabledOnGo() throws Exception {
+ doThrow(NameNotFoundException.class).when(mPm).getPackageInfo(
+ eq(TEST_APEX_PACKAGE_NAME), anyInt());
+ doReturn(mApexPackageInfo).when(mPm).getPackageInfo(
+ eq(TEST_GO_APEX_PACKAGE_NAME), anyInt());
+ doReturn("0").when(() -> DeviceConfig.getProperty(
eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
- assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
- TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
- doReturn(Long.toString(FIXED_PACKAGE_VERSION + 1)).when(() -> DeviceConfig.getProperty(
- eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
+ assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
+ TEST_EXPERIMENT_FLAG));
assertFalse(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
+ assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
+ TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, true /* defaultEnabled */));
- doReturn(Long.toString(FIXED_PACKAGE_VERSION - 1)).when(() -> DeviceConfig.getProperty(
- eq(TEST_NAME_SPACE), eq(TEST_EXPERIMENT_FLAG)));
+ doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+ eq(TEST_EXPERIMENT_FLAG)));
assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
}
@Test
- public void testFeatureIsEnabledCaching() throws Exception {
+ public void testFeatureIsEnabledCaching_APK() throws Exception {
doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
eq(TEST_EXPERIMENT_FLAG)));
assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
@@ -267,14 +287,20 @@
verify(mContext, times(1)).getPackageManager();
verify(mContext, times(1)).getPackageName();
verify(mPm, times(1)).getPackageInfo(anyString(), anyInt());
+ }
+ @Test
+ public void testFeatureIsEnabledCaching_APEX() throws Exception {
+ doReturn(TEST_FLAG_VALUE_STRING).when(() -> DeviceConfig.getProperty(eq(TEST_NAME_SPACE),
+ eq(TEST_EXPERIMENT_FLAG)));
assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
assertTrue(DeviceConfigUtils.isFeatureEnabled(mContext, TEST_NAME_SPACE,
TEST_EXPERIMENT_FLAG, TEST_APEX_NAME, false /* defaultEnabled */));
- // Module info is only queried once
- verify(mPm, times(1)).getModuleInfo(anyString(), anyInt());
+ // Package info is only queried once
+ verify(mPm, times(1)).getPackageInfo(anyString(), anyInt());
+ verify(mContext, never()).getPackageName();
}
@Test
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
index 2736c53..bb2b933 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
@@ -18,6 +18,10 @@
import static junit.framework.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
import android.os.Parcel;
import androidx.test.filters.SmallTest;
@@ -67,4 +71,25 @@
assertEquals(ipv6, out);
assertEquals(42, out.getScopeId());
}
+
+ @Test
+ public void testWithScopeId() {
+ final int scopeId = 999;
+
+ final String globalAddrStr = "2401:fa00:49c:484:dc41:e6ff:fefd:f180";
+ final Inet6Address globalAddr = (Inet6Address) InetAddresses
+ .parseNumericAddress(globalAddrStr);
+ final Inet6Address updatedGlobalAddr = InetAddressUtils.withScopeId(globalAddr, scopeId);
+ assertFalse(updatedGlobalAddr.isLinkLocalAddress());
+ assertEquals(globalAddrStr, updatedGlobalAddr.getHostAddress());
+ assertEquals(0, updatedGlobalAddr.getScopeId());
+
+ final String localAddrStr = "fe80::4735:9628:d038:2087";
+ final Inet6Address localAddr = (Inet6Address) InetAddresses
+ .parseNumericAddress(localAddrStr);
+ final Inet6Address updatedLocalAddr = InetAddressUtils.withScopeId(localAddr, scopeId);
+ assertTrue(updatedLocalAddr.isLinkLocalAddress());
+ assertEquals(localAddrStr + "%" + scopeId, updatedLocalAddr.getHostAddress());
+ assertEquals(scopeId, updatedLocalAddr.getScopeId());
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/IpUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/IpUtilsTest.java
index 20555b3..d57023c 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/IpUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/IpUtilsTest.java
@@ -74,6 +74,20 @@
// print JavaPacketDefinition(str(packet))
@Test
+ public void testEmptyAndZeroBufferChecksum() throws Exception {
+ ByteBuffer packet = ByteBuffer.wrap(new byte[] { (byte) 0x00, (byte) 0x00, });
+ // the following should *not* return 0xFFFF
+ assertEquals(0, IpUtils.checksum(packet, 0, 0, 0));
+ assertEquals(0, IpUtils.checksum(packet, 0, 0, 1));
+ assertEquals(0, IpUtils.checksum(packet, 0, 0, 2));
+ assertEquals(0, IpUtils.checksum(packet, 0, 1, 2));
+ assertEquals(0, IpUtils.checksum(packet, 0xFFFF, 0, 0));
+ assertEquals(0, IpUtils.checksum(packet, 0xFFFF, 0, 1));
+ assertEquals(0, IpUtils.checksum(packet, 0xFFFF, 0, 2));
+ assertEquals(0, IpUtils.checksum(packet, 0xFFFF, 1, 2));
+ }
+
+ @Test
public void testIpv6TcpChecksum() throws Exception {
// packet = (scapy.IPv6(src="2001:db8::1", dst="2001:db8::2", tc=0x80) /
// scapy.TCP(sport=12345, dport=7,
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/LinkPropertiesUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/LinkPropertiesUtilsTest.java
index 09f0490..80ab618 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/LinkPropertiesUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/LinkPropertiesUtilsTest.java
@@ -94,6 +94,9 @@
assertTrue(LinkPropertiesUtils.isIdenticalAddresses(source, target));
assertTrue(LinkPropertiesUtils.isIdenticalAddresses(target, source));
+ assertTrue(LinkPropertiesUtils.isIdenticalAllLinkAddresses(source, target));
+ assertTrue(LinkPropertiesUtils.isIdenticalAllLinkAddresses(target, source));
+
assertTrue(LinkPropertiesUtils.isIdenticalDnses(source, target));
assertTrue(LinkPropertiesUtils.isIdenticalDnses(target, source));
@@ -116,12 +119,17 @@
assertFalse(LinkPropertiesUtils.isIdenticalAddresses(source, target));
assertFalse(LinkPropertiesUtils.isIdenticalAddresses(target, source));
+ assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(source, target));
+ assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(target, source));
+
// Currently, target contains V4_LINKADDR, V6_LINKADDR and testLinkAddr.
// Compare addresses.size() equals but contains different address.
target.removeLinkAddress(V4_LINKADDR);
assertEquals(source.getAddresses().size(), target.getAddresses().size());
assertFalse(LinkPropertiesUtils.isIdenticalAddresses(source, target));
assertFalse(LinkPropertiesUtils.isIdenticalAddresses(target, source));
+ assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(source, target));
+ assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(target, source));
// Restore link address
target.addLinkAddress(V4_LINKADDR);
target.removeLinkAddress(testLinkAddr);
@@ -169,6 +177,13 @@
target.setHttpProxy(null);
assertFalse(LinkPropertiesUtils.isIdenticalHttpProxy(source, target));
assertFalse(LinkPropertiesUtils.isIdenticalHttpProxy(target, source));
+
+ final LinkProperties stacked = new LinkProperties();
+ stacked.setInterfaceName("v4-" + target.getInterfaceName());
+ stacked.addLinkAddress(testLinkAddr);
+ target.addStackedLink(stacked);
+ assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(source, target));
+ assertFalse(LinkPropertiesUtils.isIdenticalAllLinkAddresses(target, source));
}
private <T> void compareResult(List<T> oldItems, List<T> newItems, List<T> expectRemoved,
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/async/BufferedFileTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/async/BufferedFileTest.java
new file mode 100644
index 0000000..11a74f2
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/async/BufferedFileTest.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.async;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.ignoreStubs;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.os.ParcelFileDescriptor;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.testutils.async.ReadableDataAnswer;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BufferedFileTest {
+ @Mock EventManager mockEventManager;
+ @Mock BufferedFile.Listener mockFileListener;
+ @Mock AsyncFile mockAsyncFile;
+ @Mock ParcelFileDescriptor mockParcelFileDescriptor;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager));
+ }
+
+ @Test
+ public void onClosed() throws Exception {
+ final int inboundBufferSize = 1024;
+ final int outboundBufferSize = 768;
+
+ final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+ file.onClosed(mockAsyncFile);
+
+ verify(mockFileListener).onBufferedFileClosed();
+ }
+
+ @Test
+ public void continueReadingAndClose() throws Exception {
+ final int inboundBufferSize = 1024;
+ final int outboundBufferSize = 768;
+
+ final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+ assertEquals(inboundBufferSize, file.getInboundBufferFreeSizeForTest());
+ assertEquals(outboundBufferSize, file.getOutboundBufferFreeSize());
+
+ file.continueReading();
+ verify(mockAsyncFile).enableReadEvents(true);
+
+ file.close();
+ verify(mockAsyncFile).close();
+ }
+
+ @Test
+ public void enqueueOutboundData() throws Exception {
+ final int inboundBufferSize = 10;
+ final int outboundBufferSize = 250;
+
+ final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+ final byte[] data1 = new byte[101];
+ final byte[] data2 = new byte[102];
+ data1[0] = (byte) 1;
+ data2[0] = (byte) 2;
+
+ assertEquals(0, file.getOutboundBufferSize());
+
+ final int totalLen = data1.length + data2.length;
+
+ when(mockAsyncFile.write(any(), anyInt(), anyInt())).thenReturn(0);
+ assertTrue(file.enqueueOutboundData(data1, 0, data1.length, null, 0, 0));
+ verify(mockAsyncFile).enableWriteEvents(true);
+
+ assertEquals(data1.length, file.getOutboundBufferSize());
+
+ checkAndResetMocks();
+
+ final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+ final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+ final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+ when(mockAsyncFile.write(
+ arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture())).thenReturn(totalLen);
+
+ assertTrue(file.enqueueOutboundData(data2, 0, data2.length, null, 0, 0));
+
+ assertEquals(0, file.getInboundBuffer().size());
+ assertEquals(0, file.getOutboundBufferSize());
+
+ assertEquals(0, posCaptor.getValue().intValue());
+ assertEquals(totalLen, lenCaptor.getValue().intValue());
+ assertEquals(data1[0], arrayCaptor.getValue()[0]);
+ assertEquals(data2[0], arrayCaptor.getValue()[data1.length]);
+ }
+
+ @Test
+ public void enqueueOutboundData_combined() throws Exception {
+ final int inboundBufferSize = 10;
+ final int outboundBufferSize = 250;
+
+ final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+ final byte[] data1 = new byte[101];
+ final byte[] data2 = new byte[102];
+ data1[0] = (byte) 1;
+ data2[0] = (byte) 2;
+
+ assertEquals(0, file.getOutboundBufferSize());
+
+ final int totalLen = data1.length + data2.length;
+
+ final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+ final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+ final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+ when(mockAsyncFile.write(
+ arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture())).thenReturn(totalLen);
+
+ assertTrue(file.enqueueOutboundData(data1, 0, data1.length, data2, 0, data2.length));
+
+ assertEquals(0, file.getInboundBuffer().size());
+ assertEquals(0, file.getOutboundBufferSize());
+
+ assertEquals(0, posCaptor.getValue().intValue());
+ assertEquals(totalLen, lenCaptor.getValue().intValue());
+ assertEquals(data1[0], arrayCaptor.getValue()[0]);
+ assertEquals(data2[0], arrayCaptor.getValue()[data1.length]);
+ }
+
+ @Test
+ public void enableWriteEvents() throws Exception {
+ final int inboundBufferSize = 10;
+ final int outboundBufferSize = 250;
+
+ final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+ final byte[] data1 = new byte[101];
+ final byte[] data2 = new byte[102];
+ final byte[] data3 = new byte[103];
+ data1[0] = (byte) 1;
+ data2[0] = (byte) 2;
+ data3[0] = (byte) 3;
+
+ assertEquals(0, file.getOutboundBufferSize());
+
+ // Write first 2 buffers, but fail to flush them, causing async write request.
+ final int data1And2Len = data1.length + data2.length;
+ when(mockAsyncFile.write(any(), eq(0), eq(data1And2Len))).thenReturn(0);
+ assertTrue(file.enqueueOutboundData(data1, 0, data1.length, data2, 0, data2.length));
+ assertEquals(0, file.getInboundBuffer().size());
+ assertEquals(data1And2Len, file.getOutboundBufferSize());
+ verify(mockAsyncFile).enableWriteEvents(true);
+
+ // Try to write 3rd buffers, which won't fit, then fail to flush.
+ when(mockAsyncFile.write(any(), eq(0), eq(data1And2Len))).thenReturn(0);
+ assertFalse(file.enqueueOutboundData(data3, 0, data3.length, null, 0, 0));
+ assertEquals(0, file.getInboundBuffer().size());
+ assertEquals(data1And2Len, file.getOutboundBufferSize());
+ verify(mockAsyncFile, times(2)).enableWriteEvents(true);
+
+ checkAndResetMocks();
+
+ // Simulate writeability event, and successfully flush.
+ final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+ final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+ final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+ when(mockAsyncFile.write(arrayCaptor.capture(),
+ posCaptor.capture(), lenCaptor.capture())).thenReturn(data1And2Len);
+ file.onWriteReady(mockAsyncFile);
+ verify(mockAsyncFile).enableWriteEvents(false);
+ verify(mockFileListener).onBufferedFileOutboundSpace();
+ assertEquals(0, file.getOutboundBufferSize());
+
+ assertEquals(0, posCaptor.getValue().intValue());
+ assertEquals(data1And2Len, lenCaptor.getValue().intValue());
+ assertEquals(data1[0], arrayCaptor.getValue()[0]);
+ assertEquals(data2[0], arrayCaptor.getValue()[data1.length]);
+
+ checkAndResetMocks();
+
+ // Now write, but fail to flush the third buffer.
+ when(mockAsyncFile.write(arrayCaptor.capture(),
+ posCaptor.capture(), lenCaptor.capture())).thenReturn(0);
+ assertTrue(file.enqueueOutboundData(data3, 0, data3.length, null, 0, 0));
+ verify(mockAsyncFile).enableWriteEvents(true);
+ assertEquals(data3.length, file.getOutboundBufferSize());
+
+ assertEquals(data1And2Len, posCaptor.getValue().intValue());
+ assertEquals(outboundBufferSize - data1And2Len, lenCaptor.getValue().intValue());
+ assertEquals(data3[0], arrayCaptor.getValue()[data1And2Len]);
+ }
+
+ @Test
+ public void read() throws Exception {
+ final int inboundBufferSize = 250;
+ final int outboundBufferSize = 10;
+
+ final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+ final byte[] data1 = new byte[101];
+ final byte[] data2 = new byte[102];
+ data1[0] = (byte) 1;
+ data2[0] = (byte) 2;
+
+ final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data1, data2);
+ final ReadableByteBuffer inboundBuffer = file.getInboundBuffer();
+
+ when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+ file.onReadReady(mockAsyncFile);
+ verify(mockAsyncFile).enableReadEvents(true);
+ verify(mockFileListener).onBufferedFileInboundData(eq(data1.length + data2.length));
+
+ assertEquals(0, file.getOutboundBufferSize());
+ assertEquals(data1.length + data2.length, inboundBuffer.size());
+ assertEquals((byte) 1, inboundBuffer.peek(0));
+ assertEquals((byte) 2, inboundBuffer.peek(data1.length));
+ }
+
+ @Test
+ public void enableReadEvents() throws Exception {
+ final int inboundBufferSize = 250;
+ final int outboundBufferSize = 10;
+
+ final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+ final byte[] data1 = new byte[101];
+ final byte[] data2 = new byte[102];
+ final byte[] data3 = new byte[103];
+ data1[0] = (byte) 1;
+ data2[0] = (byte) 2;
+ data3[0] = (byte) 3;
+
+ final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data1, data2, data3);
+ final ReadableByteBuffer inboundBuffer = file.getInboundBuffer();
+
+ when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+ file.onReadReady(mockAsyncFile);
+ verify(mockAsyncFile).enableReadEvents(false);
+ verify(mockFileListener).onBufferedFileInboundData(eq(inboundBufferSize));
+
+ assertEquals(0, file.getOutboundBufferSize());
+ assertEquals(inboundBufferSize, inboundBuffer.size());
+ assertEquals((byte) 1, inboundBuffer.peek(0));
+ assertEquals((byte) 2, inboundBuffer.peek(data1.length));
+ assertEquals((byte) 3, inboundBuffer.peek(data1.length + data2.length));
+
+ checkAndResetMocks();
+
+ // Cannot enable read events since the buffer is full.
+ file.continueReading();
+
+ checkAndResetMocks();
+
+ final byte[] tmp = new byte[inboundBufferSize];
+ inboundBuffer.readBytes(tmp, 0, data1.length);
+ assertEquals(inboundBufferSize - data1.length, inboundBuffer.size());
+
+ file.continueReading();
+
+ inboundBuffer.readBytes(tmp, 0, data2.length);
+ assertEquals(inboundBufferSize - data1.length - data2.length, inboundBuffer.size());
+
+ when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+ file.onReadReady(mockAsyncFile);
+ verify(mockAsyncFile, times(2)).enableReadEvents(true);
+ verify(mockFileListener).onBufferedFileInboundData(
+ eq(data1.length + data2.length + data3.length - inboundBufferSize));
+
+ assertEquals(data3.length, inboundBuffer.size());
+ assertEquals((byte) 3, inboundBuffer.peek(0));
+ }
+
+ @Test
+ public void shutdownReading() throws Exception {
+ final int inboundBufferSize = 250;
+ final int outboundBufferSize = 10;
+
+ final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+ final byte[] data = new byte[100];
+ final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
+ when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+
+ file.shutdownReading();
+ file.onReadReady(mockAsyncFile);
+
+ verify(mockAsyncFile).enableReadEvents(false);
+
+ assertEquals(0, file.getInboundBuffer().size());
+ assertEquals(data.length, dataAnswer.getRemainingSize());
+ }
+
+ @Test
+ public void shutdownReading_inCallback() throws Exception {
+ final int inboundBufferSize = 250;
+ final int outboundBufferSize = 10;
+
+ final BufferedFile file = createFile(inboundBufferSize, outboundBufferSize);
+
+ final byte[] data = new byte[100];
+ final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
+ when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+
+ doAnswer(new Answer() {
+ @Override public Object answer(InvocationOnMock invocation) {
+ file.shutdownReading();
+ return null;
+ }}).when(mockFileListener).onBufferedFileInboundData(anyInt());
+
+ file.onReadReady(mockAsyncFile);
+
+ verify(mockAsyncFile).enableReadEvents(false);
+
+ assertEquals(0, file.getInboundBuffer().size());
+ assertEquals(0, dataAnswer.getRemainingSize());
+ }
+
+ private void checkAndResetMocks() {
+ verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager,
+ mockParcelFileDescriptor));
+ reset(mockFileListener, mockAsyncFile, mockEventManager);
+ }
+
+ private BufferedFile createFile(
+ int inboundBufferSize, int outboundBufferSize) throws Exception {
+ when(mockEventManager.registerFile(any(), any())).thenReturn(mockAsyncFile);
+ return BufferedFile.create(
+ mockEventManager,
+ FileHandle.fromFileDescriptor(mockParcelFileDescriptor),
+ mockFileListener,
+ inboundBufferSize,
+ outboundBufferSize);
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
index c7e2a4d..65e99f8 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java
@@ -16,12 +16,16 @@
package com.android.net.module.util.netlink;
+import static android.os.Process.ROOT_UID;
+import static android.os.Process.SHELL_UID;
import static android.system.OsConstants.AF_INET;
import static android.system.OsConstants.AF_INET6;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
import static android.system.OsConstants.NETLINK_INET_DIAG;
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DESTROY;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_ACK;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
@@ -32,6 +36,8 @@
import static org.junit.Assert.fail;
import android.net.InetAddresses;
+import android.util.ArraySet;
+import android.util.Range;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -41,14 +47,33 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.List;
+import java.util.Set;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class InetDiagSocketTest {
+ // ::FFFF:192.0.2.1
+ private static final byte[] SRC_V4_MAPPED_V6_ADDRESS_BYTES = {
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+ (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+ };
+ // ::FFFF:192.0.2.2
+ private static final byte[] DST_V4_MAPPED_V6_ADDRESS_BYTES = {
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+ (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x02,
+ };
+
// Hexadecimal representation of InetDiagReqV2 request.
private static final String INET_DIAG_REQ_V2_UDP_INET4_HEX =
// struct nlmsghdr
@@ -205,14 +230,14 @@
msg = InetDiagMessage.inetDiagReqV2(IPPROTO_TCP, null, remote, AF_INET6,
NLM_F_REQUEST);
fail("Both remote and local should be null, expected UnknownHostException");
- } catch (NullPointerException e) {
+ } catch (IllegalArgumentException e) {
}
try {
msg = InetDiagMessage.inetDiagReqV2(IPPROTO_TCP, local, null, AF_INET6,
NLM_F_REQUEST, 0 /* pad */, 0 /* idiagExt */, TCP_ALL_STATES);
fail("Both remote and local should be null, expected UnknownHostException");
- } catch (NullPointerException e) {
+ } catch (IllegalArgumentException e) {
}
msg = InetDiagMessage.inetDiagReqV2(IPPROTO_TCP, null, null, AF_INET6,
@@ -221,19 +246,115 @@
assertArrayEquals(INET_DIAG_REQ_V2_TCP_INET6_NO_ID_SPECIFIED_BYTES, msgExt);
}
- // Hexadecimal representation of InetDiagReqV2 request.
- private static final String INET_DIAG_MSG_HEX =
+ // Hexadecimal representation of InetDiagReqV2 request with v4-mapped v6 address
+ private static final String INET_DIAG_REQ_V2_TCP_INET6_V4_MAPPED_HEX =
+ // struct nlmsghdr
+ "48000000" + // length = 72
+ "1400" + // type = SOCK_DIAG_BY_FAMILY
+ "0100" + // flags = NLM_F_REQUEST
+ "00000000" + // seqno
+ "00000000" + // pid (0 == kernel)
+ // struct inet_diag_req_v2
+ "0a" + // family = AF_INET6
+ "06" + // protcol = IPPROTO_TCP
+ "00" + // idiag_ext
+ "00" + // pad
+ "ffffffff" + // idiag_states
+ // inet_diag_sockid
+ "a817" + // idiag_sport = 43031
+ "960f" + // idiag_dport = 38415
+ "00000000000000000000ffffc0000201" + // idiag_src = ::FFFF:192.0.2.1
+ "00000000000000000000ffffc0000202" + // idiag_dst = ::FFFF:192.0.2.2
+ "00000000" + // idiag_if
+ "ffffffffffffffff"; // idiag_cookie = INET_DIAG_NOCOOKIE
+
+ private static final byte[] INET_DIAG_REQ_V2_TCP_INET6_V4_MAPPED_BYTES =
+ HexEncoding.decode(INET_DIAG_REQ_V2_TCP_INET6_V4_MAPPED_HEX.toCharArray(), false);
+
+ @Test
+ public void testInetDiagReqV2TcpInet6V4Mapped() throws Exception {
+ final Inet6Address srcAddr = Inet6Address.getByAddress(
+ null /* host */, SRC_V4_MAPPED_V6_ADDRESS_BYTES, -1 /* scope_id */);
+ final Inet6Address dstAddr = Inet6Address.getByAddress(
+ null /* host */, DST_V4_MAPPED_V6_ADDRESS_BYTES, -1 /* scope_id */);
+ final byte[] msg = InetDiagMessage.inetDiagReqV2(
+ IPPROTO_TCP,
+ new InetSocketAddress(srcAddr, 43031),
+ new InetSocketAddress(dstAddr, 38415),
+ AF_INET6,
+ NLM_F_REQUEST);
+ assertArrayEquals(INET_DIAG_REQ_V2_TCP_INET6_V4_MAPPED_BYTES, msg);
+ }
+
+ // Hexadecimal representation of InetDiagReqV2 request with SOCK_DESTROY
+ private static final String INET_DIAG_REQ_V2_TCP_INET6_DESTROY_HEX =
+ // struct nlmsghdr
+ "48000000" + // length = 72
+ "1500" + // type = SOCK_DESTROY
+ "0500" + // flags = NLM_F_REQUEST | NLM_F_ACK
+ "00000000" + // seqno
+ "00000000" + // pid (0 == kernel)
+ // struct inet_diag_req_v2
+ "0a" + // family = AF_INET6
+ "06" + // protcol = IPPROTO_TCP
+ "00" + // idiag_ext
+ "00" + // pad
+ "ffffffff" + // idiag_states = TCP_ALL_STATES
+ // inet_diag_sockid
+ "a817" + // idiag_sport = 43031
+ "960f" + // idiag_dport = 38415
+ "20010db8000000000000000000000001" + // idiag_src = 2001:db8::1
+ "20010db8000000000000000000000002" + // idiag_dst = 2001:db8::2
+ "07000000" + // idiag_if = 7
+ "5800000000000000"; // idiag_cookie = 88
+
+ private static final byte[] INET_DIAG_REQ_V2_TCP_INET6_DESTROY_BYTES =
+ HexEncoding.decode(INET_DIAG_REQ_V2_TCP_INET6_DESTROY_HEX.toCharArray(), false);
+
+ @Test
+ public void testInetDiagReqV2TcpInet6Destroy() throws Exception {
+ final StructInetDiagSockId sockId = new StructInetDiagSockId(
+ new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::1"), 43031),
+ new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::2"), 38415),
+ 7 /* ifIndex */,
+ 88 /* cookie */);
+ final byte[] msg = InetDiagMessage.inetDiagReqV2(IPPROTO_TCP, sockId, AF_INET6,
+ SOCK_DESTROY, (short) (NLM_F_REQUEST | NLM_F_ACK), 0 /* pad */, 0 /* idiagExt */,
+ TCP_ALL_STATES);
+
+ assertArrayEquals(INET_DIAG_REQ_V2_TCP_INET6_DESTROY_BYTES, msg);
+ }
+
+ private void assertNlMsgHdr(StructNlMsgHdr hdr, short type, short flags, int seq, int pid) {
+ assertNotNull(hdr);
+ assertEquals(type, hdr.nlmsg_type);
+ assertEquals(flags, hdr.nlmsg_flags);
+ assertEquals(seq, hdr.nlmsg_seq);
+ assertEquals(pid, hdr.nlmsg_pid);
+ }
+
+ private void assertInetDiagSockId(StructInetDiagSockId sockId,
+ InetSocketAddress locSocketAddress, InetSocketAddress remSocketAddress,
+ int ifIndex, long cookie) {
+ assertEquals(locSocketAddress, sockId.locSocketAddress);
+ assertEquals(remSocketAddress, sockId.remSocketAddress);
+ assertEquals(ifIndex, sockId.ifIndex);
+ assertEquals(cookie, sockId.cookie);
+ }
+
+ // Hexadecimal representation of InetDiagMessage
+ private static final String INET_DIAG_MSG_HEX1 =
// struct nlmsghdr
"58000000" + // length = 88
"1400" + // type = SOCK_DIAG_BY_FAMILY
"0200" + // flags = NLM_F_MULTI
"00000000" + // seqno
- "f5220000" + // pid (0 == kernel)
+ "f5220000" + // pid
// struct inet_diag_msg
"0a" + // family = AF_INET6
- "01" + // idiag_state
- "00" + // idiag_timer
- "00" + // idiag_retrans
+ "01" + // idiag_state = 1
+ "02" + // idiag_timer = 2
+ "ff" + // idiag_retrans = 255
// inet_diag_sockid
"a817" + // idiag_sport = 43031
"960f" + // idiag_dport = 38415
@@ -241,39 +362,331 @@
"20010db8000000000000000000000002" + // idiag_dst = 2001:db8::2
"07000000" + // idiag_if = 7
"5800000000000000" + // idiag_cookie = 88
- "00000000" + // idiag_expires
- "00000000" + // idiag_rqueue
- "00000000" + // idiag_wqueue
- "a3270000" + // idiag_uid
- "A57E1900"; // idiag_inode
- private static final byte[] INET_DIAG_MSG_BYTES =
- HexEncoding.decode(INET_DIAG_MSG_HEX.toCharArray(), false);
+ "04000000" + // idiag_expires = 4
+ "05000000" + // idiag_rqueue = 5
+ "06000000" + // idiag_wqueue = 6
+ "a3270000" + // idiag_uid = 10147
+ "a57e19f0"; // idiag_inode = 4028202661
- @Test
- public void testParseInetDiagResponse() throws Exception {
- final ByteBuffer byteBuffer = ByteBuffer.wrap(INET_DIAG_MSG_BYTES);
- byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
- final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG);
+ private void assertInetDiagMsg1(final NetlinkMessage msg) {
assertNotNull(msg);
assertTrue(msg instanceof InetDiagMessage);
final InetDiagMessage inetDiagMsg = (InetDiagMessage) msg;
- assertEquals(10147, inetDiagMsg.inetDiagMsg.idiag_uid);
- final StructInetDiagSockId sockId = inetDiagMsg.inetDiagMsg.id;
- assertEquals(43031, sockId.locSocketAddress.getPort());
- assertEquals(InetAddresses.parseNumericAddress("2001:db8::1"),
- sockId.locSocketAddress.getAddress());
- assertEquals(38415, sockId.remSocketAddress.getPort());
- assertEquals(InetAddresses.parseNumericAddress("2001:db8::2"),
- sockId.remSocketAddress.getAddress());
- assertEquals(7, sockId.ifIndex);
- assertEquals(88, sockId.cookie);
- final StructNlMsgHdr hdr = inetDiagMsg.getHeader();
- assertNotNull(hdr);
- assertEquals(NetlinkConstants.SOCK_DIAG_BY_FAMILY, hdr.nlmsg_type);
- assertEquals(StructNlMsgHdr.NLM_F_MULTI, hdr.nlmsg_flags);
- assertEquals(0, hdr.nlmsg_seq);
- assertEquals(8949, hdr.nlmsg_pid);
+ assertNlMsgHdr(inetDiagMsg.getHeader(),
+ NetlinkConstants.SOCK_DIAG_BY_FAMILY,
+ StructNlMsgHdr.NLM_F_MULTI,
+ 0 /* seq */,
+ 8949 /* pid */);
+
+ assertEquals(AF_INET6, inetDiagMsg.inetDiagMsg.idiag_family);
+ assertEquals(1, inetDiagMsg.inetDiagMsg.idiag_state);
+ assertEquals(2, inetDiagMsg.inetDiagMsg.idiag_timer);
+ assertEquals(255, inetDiagMsg.inetDiagMsg.idiag_retrans);
+ assertInetDiagSockId(inetDiagMsg.inetDiagMsg.id,
+ new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::1"), 43031),
+ new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::2"), 38415),
+ 7 /* ifIndex */,
+ 88 /* cookie */);
+ assertEquals(4, inetDiagMsg.inetDiagMsg.idiag_expires);
+ assertEquals(5, inetDiagMsg.inetDiagMsg.idiag_rqueue);
+ assertEquals(6, inetDiagMsg.inetDiagMsg.idiag_wqueue);
+ assertEquals(10147, inetDiagMsg.inetDiagMsg.idiag_uid);
+ assertEquals(4028202661L, inetDiagMsg.inetDiagMsg.idiag_inode);
+ }
+
+ // Hexadecimal representation of InetDiagMessage
+ private static final String INET_DIAG_MSG_HEX2 =
+ // struct nlmsghdr
+ "58000000" + // length = 88
+ "1400" + // type = SOCK_DIAG_BY_FAMILY
+ "0200" + // flags = NLM_F_MULTI
+ "00000000" + // seqno
+ "f5220000" + // pid
+ // struct inet_diag_msg
+ "0a" + // family = AF_INET6
+ "02" + // idiag_state = 2
+ "10" + // idiag_timer = 16
+ "20" + // idiag_retrans = 32
+ // inet_diag_sockid
+ "a845" + // idiag_sport = 43077
+ "01bb" + // idiag_dport = 443
+ "20010db8000000000000000000000003" + // idiag_src = 2001:db8::3
+ "20010db8000000000000000000000004" + // idiag_dst = 2001:db8::4
+ "08000000" + // idiag_if = 8
+ "6300000000000000" + // idiag_cookie = 99
+ "30000000" + // idiag_expires = 48
+ "40000000" + // idiag_rqueue = 64
+ "50000000" + // idiag_wqueue = 80
+ "39300000" + // idiag_uid = 12345
+ "851a0000"; // idiag_inode = 6789
+
+ private void assertInetDiagMsg2(final NetlinkMessage msg) {
+ assertNotNull(msg);
+
+ assertTrue(msg instanceof InetDiagMessage);
+ final InetDiagMessage inetDiagMsg = (InetDiagMessage) msg;
+
+ assertNlMsgHdr(inetDiagMsg.getHeader(),
+ NetlinkConstants.SOCK_DIAG_BY_FAMILY,
+ StructNlMsgHdr.NLM_F_MULTI,
+ 0 /* seq */,
+ 8949 /* pid */);
+
+ assertEquals(AF_INET6, inetDiagMsg.inetDiagMsg.idiag_family);
+ assertEquals(2, inetDiagMsg.inetDiagMsg.idiag_state);
+ assertEquals(16, inetDiagMsg.inetDiagMsg.idiag_timer);
+ assertEquals(32, inetDiagMsg.inetDiagMsg.idiag_retrans);
+ assertInetDiagSockId(inetDiagMsg.inetDiagMsg.id,
+ new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::3"), 43077),
+ new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::4"), 443),
+ 8 /* ifIndex */,
+ 99 /* cookie */);
+ assertEquals(48, inetDiagMsg.inetDiagMsg.idiag_expires);
+ assertEquals(64, inetDiagMsg.inetDiagMsg.idiag_rqueue);
+ assertEquals(80, inetDiagMsg.inetDiagMsg.idiag_wqueue);
+ assertEquals(12345, inetDiagMsg.inetDiagMsg.idiag_uid);
+ assertEquals(6789, inetDiagMsg.inetDiagMsg.idiag_inode);
+ }
+
+ private static final byte[] INET_DIAG_MSG_BYTES =
+ HexEncoding.decode(INET_DIAG_MSG_HEX1.toCharArray(), false);
+
+ @Test
+ public void testParseInetDiagResponse() throws Exception {
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(INET_DIAG_MSG_BYTES);
+ byteBuffer.order(ByteOrder.nativeOrder());
+ assertInetDiagMsg1(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
+ }
+
+
+ private static final byte[] INET_DIAG_MSG_BYTES_MULTIPLE =
+ HexEncoding.decode((INET_DIAG_MSG_HEX1 + INET_DIAG_MSG_HEX2).toCharArray(), false);
+
+ @Test
+ public void testParseInetDiagResponseMultiple() {
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(INET_DIAG_MSG_BYTES_MULTIPLE);
+ byteBuffer.order(ByteOrder.nativeOrder());
+ assertInetDiagMsg1(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
+ assertInetDiagMsg2(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG));
+ }
+
+ private static final String INET_DIAG_SOCK_ID_V4_MAPPED_V6_HEX =
+ "a845" + // idiag_sport = 43077
+ "01bb" + // idiag_dport = 443
+ "00000000000000000000ffffc0000201" + // idiag_src = ::FFFF:192.0.2.1
+ "00000000000000000000ffffc0000202" + // idiag_dst = ::FFFF:192.0.2.2
+ "08000000" + // idiag_if = 8
+ "6300000000000000"; // idiag_cookie = 99
+
+ private static final byte[] INET_DIAG_SOCK_ID_V4_MAPPED_V6_BYTES =
+ HexEncoding.decode(INET_DIAG_SOCK_ID_V4_MAPPED_V6_HEX.toCharArray(), false);
+
+ @Test
+ public void testParseAndPackInetDiagSockIdV4MappedV6() {
+ final ByteBuffer parseByteBuffer = ByteBuffer.wrap(INET_DIAG_SOCK_ID_V4_MAPPED_V6_BYTES);
+ parseByteBuffer.order(ByteOrder.nativeOrder());
+ final StructInetDiagSockId diagSockId =
+ StructInetDiagSockId.parse(parseByteBuffer, (short) AF_INET6);
+ assertNotNull(diagSockId);
+
+ final ByteBuffer packByteBuffer =
+ ByteBuffer.allocate(INET_DIAG_SOCK_ID_V4_MAPPED_V6_BYTES.length);
+ diagSockId.pack(packByteBuffer);
+
+ // Move position to the head since ByteBuffer#equals compares the values from the current
+ // position.
+ parseByteBuffer.position(0);
+ packByteBuffer.position(0);
+ assertEquals(parseByteBuffer, packByteBuffer);
+ }
+
+ // Hexadecimal representation of InetDiagMessage with v4-mapped v6 address
+ private static final String INET_DIAG_MSG_V4_MAPPED_V6_HEX =
+ // struct nlmsghdr
+ "58000000" + // length = 88
+ "1400" + // type = SOCK_DIAG_BY_FAMILY
+ "0200" + // flags = NLM_F_MULTI
+ "00000000" + // seqno
+ "f5220000" + // pid
+ // struct inet_diag_msg
+ "0a" + // family = AF_INET6
+ "01" + // idiag_state = 1
+ "02" + // idiag_timer = 2
+ "03" + // idiag_retrans = 3
+ // inet_diag_sockid
+ "a817" + // idiag_sport = 43031
+ "960f" + // idiag_dport = 38415
+ "00000000000000000000ffffc0000201" + // idiag_src = ::FFFF:192.0.2.1
+ "00000000000000000000ffffc0000202" + // idiag_dst = ::FFFF:192.0.2.2
+ "07000000" + // idiag_if = 7
+ "5800000000000000" + // idiag_cookie = 88
+ "04000000" + // idiag_expires = 4
+ "05000000" + // idiag_rqueue = 5
+ "06000000" + // idiag_wqueue = 6
+ "a3270000" + // idiag_uid = 10147
+ "A57E1900"; // idiag_inode = 1670821
+
+ private static final byte[] INET_DIAG_MSG_V4_MAPPED_V6_BYTES =
+ HexEncoding.decode(INET_DIAG_MSG_V4_MAPPED_V6_HEX.toCharArray(), false);
+
+ @Test
+ public void testParseInetDiagResponseV4MappedV6() throws Exception {
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(INET_DIAG_MSG_V4_MAPPED_V6_BYTES);
+ byteBuffer.order(ByteOrder.nativeOrder());
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG);
+
+ assertNotNull(msg);
+ assertTrue(msg instanceof InetDiagMessage);
+ final InetDiagMessage inetDiagMsg = (InetDiagMessage) msg;
+ final Inet6Address srcAddr = Inet6Address.getByAddress(
+ null /* host */, SRC_V4_MAPPED_V6_ADDRESS_BYTES, -1 /* scope_id */);
+ final Inet6Address dstAddr = Inet6Address.getByAddress(
+ null /* host */, DST_V4_MAPPED_V6_ADDRESS_BYTES, -1 /* scope_id */);
+ assertInetDiagSockId(inetDiagMsg.inetDiagMsg.id,
+ new InetSocketAddress(srcAddr, 43031),
+ new InetSocketAddress(dstAddr, 38415),
+ 7 /* ifIndex */,
+ 88 /* cookie */);
+ }
+
+ private void doTestIsLoopback(InetAddress srcAddr, InetAddress dstAddr, boolean expected) {
+ final InetDiagMessage inetDiagMsg = new InetDiagMessage(new StructNlMsgHdr());
+ inetDiagMsg.inetDiagMsg.id = new StructInetDiagSockId(
+ new InetSocketAddress(srcAddr, 43031),
+ new InetSocketAddress(dstAddr, 38415)
+ );
+
+ assertEquals(expected, InetDiagMessage.isLoopback(inetDiagMsg));
+ }
+
+ @Test
+ public void testIsLoopback() {
+ doTestIsLoopback(
+ InetAddresses.parseNumericAddress("127.0.0.1"),
+ InetAddresses.parseNumericAddress("192.0.2.1"),
+ true
+ );
+ doTestIsLoopback(
+ InetAddresses.parseNumericAddress("192.0.2.1"),
+ InetAddresses.parseNumericAddress("127.7.7.7"),
+ true
+ );
+ doTestIsLoopback(
+ InetAddresses.parseNumericAddress("::1"),
+ InetAddresses.parseNumericAddress("::1"),
+ true
+ );
+ doTestIsLoopback(
+ InetAddresses.parseNumericAddress("::1"),
+ InetAddresses.parseNumericAddress("2001:db8::1"),
+ true
+ );
+ }
+
+ @Test
+ public void testIsLoopbackSameSrcDstAddress() {
+ doTestIsLoopback(
+ InetAddresses.parseNumericAddress("192.0.2.1"),
+ InetAddresses.parseNumericAddress("192.0.2.1"),
+ true
+ );
+ doTestIsLoopback(
+ InetAddresses.parseNumericAddress("2001:db8::1"),
+ InetAddresses.parseNumericAddress("2001:db8::1"),
+ true
+ );
+ }
+
+ @Test
+ public void testIsLoopbackNonLoopbackSocket() {
+ doTestIsLoopback(
+ InetAddresses.parseNumericAddress("192.0.2.1"),
+ InetAddresses.parseNumericAddress("192.0.2.2"),
+ false
+ );
+ doTestIsLoopback(
+ InetAddresses.parseNumericAddress("2001:db8::1"),
+ InetAddresses.parseNumericAddress("2001:db8::2"),
+ false
+ );
+ }
+
+ @Test
+ public void testIsLoopbackV4MappedV6() throws UnknownHostException {
+ // ::FFFF:127.1.2.3
+ final byte[] addrLoopbackByte = {
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+ (byte) 0x7f, (byte) 0x01, (byte) 0x02, (byte) 0x03,
+ };
+ // ::FFFF:192.0.2.1
+ final byte[] addrNonLoopbackByte1 = {
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+ (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+ };
+ // ::FFFF:192.0.2.2
+ final byte[] addrNonLoopbackByte2 = {
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+ (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+ (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x02,
+ };
+
+ final Inet6Address addrLoopback = Inet6Address.getByAddress(null, addrLoopbackByte, -1);
+ final Inet6Address addrNonLoopback1 =
+ Inet6Address.getByAddress(null, addrNonLoopbackByte1, -1);
+ final Inet6Address addrNonLoopback2 =
+ Inet6Address.getByAddress(null, addrNonLoopbackByte2, -1);
+
+ doTestIsLoopback(addrLoopback, addrNonLoopback1, true);
+ doTestIsLoopback(addrNonLoopback1, addrNonLoopback2, false);
+ doTestIsLoopback(addrNonLoopback1, addrNonLoopback1, true);
+ }
+
+ private void doTestContainsUid(final int uid, final Set<Range<Integer>> ranges,
+ final boolean expected) {
+ final InetDiagMessage inetDiagMsg = new InetDiagMessage(new StructNlMsgHdr());
+ inetDiagMsg.inetDiagMsg.idiag_uid = uid;
+ assertEquals(expected, InetDiagMessage.containsUid(inetDiagMsg, ranges));
+ }
+
+ @Test
+ public void testContainsUid() {
+ doTestContainsUid(77 /* uid */,
+ new ArraySet<>(List.of(new Range<>(0, 100))),
+ true /* expected */);
+ doTestContainsUid(77 /* uid */,
+ new ArraySet<>(List.of(new Range<>(77, 77), new Range<>(100, 200))),
+ true /* expected */);
+
+ doTestContainsUid(77 /* uid */,
+ new ArraySet<>(List.of(new Range<>(100, 200))),
+ false /* expected */);
+ doTestContainsUid(77 /* uid */,
+ new ArraySet<>(List.of(new Range<>(0, 76), new Range<>(78, 100))),
+ false /* expected */);
+ }
+
+ private void doTestIsAdbSocket(final int uid, final boolean expected) {
+ final InetDiagMessage inetDiagMsg = new InetDiagMessage(new StructNlMsgHdr());
+ inetDiagMsg.inetDiagMsg.idiag_uid = uid;
+ inetDiagMsg.inetDiagMsg.id = new StructInetDiagSockId(
+ new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::1"), 38417),
+ new InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::2"), 38415)
+ );
+ assertEquals(expected, InetDiagMessage.isAdbSocket(inetDiagMsg));
+ }
+
+ @Test
+ public void testIsAdbSocket() {
+ final int appUid = 10108;
+ doTestIsAdbSocket(SHELL_UID, true /* expected */);
+ doTestIsAdbSocket(ROOT_UID, false /* expected */);
+ doTestIsAdbSocket(appUid, false /* expected */);
}
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
index 7a1639a..6fbfbf9 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
@@ -30,6 +30,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
import android.content.Context;
import android.system.ErrnoException;
@@ -51,11 +52,13 @@
import java.io.FileDescriptor;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Paths;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class NetlinkUtilsTest {
- private static final String TAG = "NetlinkUitlsTest";
+ private static final String TAG = "NetlinkUtilsTest";
private static final int TEST_SEQNO = 5;
private static final int TEST_TIMEOUT_MS = 500;
@@ -82,6 +85,8 @@
// Apps targeting an SDK version > S are not allowed to send RTM_GETNEIGH{TBL} messages
if (SdkLevel.isAtLeastT() && targetSdk > 31) {
+ var ctxt = new String(Files.readAllBytes(Paths.get("/proc/thread-self/attr/current")));
+ assumeFalse("must not be platform app", ctxt.startsWith("u:r:platform_app:s0:"));
try {
NetlinkUtils.sendMessage(fd, req, 0, req.length, TEST_TIMEOUT_MS);
fail("RTM_GETNEIGH is not allowed for apps targeting SDK > 31 on T+ platforms,"
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java
index f845eb4..99d96b5 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkAddressMessageTest.java
@@ -144,7 +144,7 @@
// struct nlmsghdr
"48000000" + // length = 72
"1400" + // type = 20 (RTM_NEWADDR)
- "0500" + // flags = NLM_F_ACK | NLM_F_REQUEST
+ "0501" + // flags = NLM_F_ACK | NLM_F_REQUEST | NLM_F_REPLACE
"01000000" + // seqno = 1
"00000000" + // pid = 0 (send to kernel)
// struct IfaddrMsg
@@ -195,7 +195,7 @@
// struct nlmsghdr
"48000000" + // length = 72
"1400" + // type = 20 (RTM_NEWADDR)
- "0500" + // flags = NLM_F_ACK | NLM_F_REQUEST
+ "0501" + // flags = NLM_F_ACK | NLM_F_REQUEST | NLM_F_REPLACE
"01000000" + // seqno = 1
"00000000" + // pid = 0 (send to kernel)
// struct IfaddrMsg
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/wear/NetPacketHelpersTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/wear/NetPacketHelpersTest.java
new file mode 100644
index 0000000..23e7b15
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/wear/NetPacketHelpersTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.wear;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import com.android.net.module.util.async.CircularByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetPacketHelpersTest {
+ @Test
+ public void decodeNetworkUnsignedInt16() {
+ final byte[] data = new byte[4];
+ data[0] = (byte) 0xFF;
+ data[1] = (byte) 1;
+ data[2] = (byte) 2;
+ data[3] = (byte) 0xFF;
+
+ assertEquals(0x0102, NetPacketHelpers.decodeNetworkUnsignedInt16(data, 1));
+
+ CircularByteBuffer buffer = new CircularByteBuffer(100);
+ buffer.writeBytes(data, 0, data.length);
+
+ assertEquals(0x0102, NetPacketHelpers.decodeNetworkUnsignedInt16(buffer, 1));
+ }
+
+ @Test
+ public void encodeNetworkUnsignedInt16() {
+ final byte[] data = new byte[4];
+ data[0] = (byte) 0xFF;
+ data[3] = (byte) 0xFF;
+ NetPacketHelpers.encodeNetworkUnsignedInt16(0x0102, data, 1);
+
+ assertEquals((byte) 0xFF, data[0]);
+ assertEquals((byte) 1, data[1]);
+ assertEquals((byte) 2, data[2]);
+ assertEquals((byte) 0xFF, data[3]);
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/wear/StreamingPacketFileTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/wear/StreamingPacketFileTest.java
new file mode 100644
index 0000000..1fcca70
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/wear/StreamingPacketFileTest.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.net.module.util.wear;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.ignoreStubs;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.os.ParcelFileDescriptor;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.async.AsyncFile;
+import com.android.net.module.util.async.BufferedFile;
+import com.android.net.module.util.async.EventManager;
+import com.android.net.module.util.async.FileHandle;
+import com.android.net.module.util.async.ReadableByteBuffer;
+import com.android.testutils.async.ReadableDataAnswer;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StreamingPacketFileTest {
+ private static final int MAX_PACKET_SIZE = 100;
+
+ @Mock EventManager mockEventManager;
+ @Mock PacketFile.Listener mockFileListener;
+ @Mock AsyncFile mockAsyncFile;
+ @Mock ParcelFileDescriptor mockParcelFileDescriptor;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager));
+ }
+
+ @Test
+ public void continueReadingAndClose() throws Exception {
+ final int maxBufferedInboundPackets = 3;
+ final int maxBufferedOutboundPackets = 5;
+
+ final StreamingPacketFile file =
+ createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
+ final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
+
+ assertEquals(maxBufferedInboundPackets * (MAX_PACKET_SIZE + 2),
+ bufferedFile.getInboundBufferFreeSizeForTest());
+ assertEquals(maxBufferedOutboundPackets * (MAX_PACKET_SIZE + 2),
+ bufferedFile.getOutboundBufferFreeSize());
+ assertEquals(bufferedFile.getOutboundBufferFreeSize() - 2,
+ file.getOutboundFreeSize());
+
+ file.continueReading();
+ verify(mockAsyncFile).enableReadEvents(true);
+
+ file.close();
+ verify(mockAsyncFile).close();
+ }
+
+ @Test
+ public void enqueueOutboundPacket() throws Exception {
+ final int maxBufferedInboundPackets = 10;
+ final int maxBufferedOutboundPackets = 20;
+
+ final StreamingPacketFile file =
+ createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
+ final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
+
+ final byte[] packet1 = new byte[11];
+ final byte[] packet2 = new byte[12];
+ packet1[0] = (byte) 1;
+ packet2[0] = (byte) 2;
+
+ assertEquals(0, bufferedFile.getOutboundBufferSize());
+
+ when(mockAsyncFile.write(any(), anyInt(), anyInt())).thenReturn(0);
+ assertTrue(file.enqueueOutboundPacket(packet1, 0, packet1.length));
+ verify(mockAsyncFile).enableWriteEvents(true);
+
+ assertEquals(packet1.length + 2, bufferedFile.getOutboundBufferSize());
+
+ checkAndResetMocks();
+
+ final int totalLen = packet1.length + packet2.length + 4;
+
+ final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+ final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+ final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+ when(mockAsyncFile.write(
+ arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture())).thenReturn(totalLen);
+
+ assertTrue(file.enqueueOutboundPacket(packet2, 0, packet2.length));
+
+ assertEquals(0, bufferedFile.getInboundBuffer().size());
+ assertEquals(0, bufferedFile.getOutboundBufferSize());
+
+ assertEquals(0, posCaptor.getValue().intValue());
+ assertEquals(totalLen, lenCaptor.getValue().intValue());
+
+ final byte[] capturedData = arrayCaptor.getValue();
+ assertEquals(packet1.length, NetPacketHelpers.decodeNetworkUnsignedInt16(capturedData, 0));
+ assertEquals(packet2.length,
+ NetPacketHelpers.decodeNetworkUnsignedInt16(capturedData, packet1.length + 2));
+ assertEquals(packet1[0], capturedData[2]);
+ assertEquals(packet2[0], capturedData[packet1.length + 4]);
+ }
+
+ @Test
+ public void onInboundPacket() throws Exception {
+ final int maxBufferedInboundPackets = 10;
+ final int maxBufferedOutboundPackets = 20;
+
+ final StreamingPacketFile file =
+ createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
+ final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
+ final ReadableByteBuffer inboundBuffer = bufferedFile.getInboundBuffer();
+
+ final int len1 = 11;
+ final int len2 = 12;
+ final byte[] data = new byte[len1 + len2 + 4];
+ NetPacketHelpers.encodeNetworkUnsignedInt16(len1, data, 0);
+ NetPacketHelpers.encodeNetworkUnsignedInt16(len2, data, 11 + 2);
+ data[2] = (byte) 1;
+ data[len1 + 4] = (byte) 2;
+
+ final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
+
+ final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+ final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+ final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+
+ when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+ when(mockFileListener.onPreambleData(any(), eq(0), eq(data.length))).thenReturn(0);
+ bufferedFile.onReadReady(mockAsyncFile);
+ verify(mockAsyncFile).enableReadEvents(true);
+ verify(mockFileListener).onInboundBuffered(data.length, data.length);
+ verify(mockFileListener).onInboundPacket(
+ arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture());
+ verify(mockEventManager).execute(any());
+
+ byte[] capturedData = arrayCaptor.getValue();
+ assertEquals(2, posCaptor.getValue().intValue());
+ assertEquals(len1, lenCaptor.getValue().intValue());
+ assertEquals((byte) 1, capturedData[2]);
+
+ checkAndResetMocks();
+
+ when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+ file.onBufferedFileInboundData(0);
+ verify(mockFileListener).onInboundPacket(
+ arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture());
+ verify(mockEventManager).execute(any());
+
+ capturedData = arrayCaptor.getValue();
+ assertEquals(2, posCaptor.getValue().intValue());
+ assertEquals(len2, lenCaptor.getValue().intValue());
+ assertEquals((byte) 2, capturedData[2]);
+
+ assertEquals(0, bufferedFile.getOutboundBufferSize());
+ assertEquals(0, inboundBuffer.size());
+ }
+
+ @Test
+ public void onReadReady_preambleData() throws Exception {
+ final int maxBufferedInboundPackets = 10;
+ final int maxBufferedOutboundPackets = 20;
+
+ final StreamingPacketFile file =
+ createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
+ final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
+ final ReadableByteBuffer inboundBuffer = bufferedFile.getInboundBuffer();
+
+ final int preambleLen = 23;
+ final int len1 = 11;
+ final byte[] data = new byte[preambleLen + 2 + len1];
+ NetPacketHelpers.encodeNetworkUnsignedInt16(len1, data, preambleLen);
+ data[preambleLen + 2] = (byte) 1;
+
+ final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
+
+ when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+ when(mockFileListener.onPreambleData(any(), eq(0), eq(data.length))).thenReturn(5);
+ when(mockFileListener.onPreambleData(
+ any(), eq(0), eq(data.length - 5))).thenReturn(preambleLen - 5);
+ when(mockFileListener.onPreambleData(
+ any(), eq(0), eq(data.length - preambleLen))).thenReturn(0);
+
+ bufferedFile.onReadReady(mockAsyncFile);
+
+ final ArgumentCaptor<byte[]> arrayCaptor = ArgumentCaptor.forClass(byte[].class);
+ final ArgumentCaptor<Integer> posCaptor = ArgumentCaptor.forClass(Integer.class);
+ final ArgumentCaptor<Integer> lenCaptor = ArgumentCaptor.forClass(Integer.class);
+
+ verify(mockFileListener).onInboundBuffered(data.length, data.length);
+ verify(mockFileListener).onInboundPacket(
+ arrayCaptor.capture(), posCaptor.capture(), lenCaptor.capture());
+ verify(mockEventManager).execute(any());
+ verify(mockAsyncFile).enableReadEvents(true);
+
+ final byte[] capturedData = arrayCaptor.getValue();
+ assertEquals(2, posCaptor.getValue().intValue());
+ assertEquals(len1, lenCaptor.getValue().intValue());
+ assertEquals((byte) 1, capturedData[2]);
+
+ assertEquals(0, bufferedFile.getOutboundBufferSize());
+ assertEquals(0, inboundBuffer.size());
+ }
+
+ @Test
+ public void shutdownReading() throws Exception {
+ final int maxBufferedInboundPackets = 10;
+ final int maxBufferedOutboundPackets = 20;
+
+ final StreamingPacketFile file =
+ createFile(maxBufferedInboundPackets, maxBufferedOutboundPackets);
+ final BufferedFile bufferedFile = file.getUnderlyingFileForTest();
+
+ final byte[] data = new byte[100];
+ final ReadableDataAnswer dataAnswer = new ReadableDataAnswer(data);
+ when(mockAsyncFile.read(any(), anyInt(), anyInt())).thenAnswer(dataAnswer);
+
+ doAnswer(new Answer() {
+ @Override public Object answer(InvocationOnMock invocation) {
+ file.shutdownReading();
+ return Integer.valueOf(-1);
+ }}).when(mockFileListener).onPreambleData(any(), anyInt(), anyInt());
+
+ bufferedFile.onReadReady(mockAsyncFile);
+
+ verify(mockFileListener).onInboundBuffered(data.length, data.length);
+ verify(mockAsyncFile).enableReadEvents(false);
+
+ assertEquals(0, bufferedFile.getInboundBuffer().size());
+ }
+
+ private void checkAndResetMocks() {
+ verifyNoMoreInteractions(ignoreStubs(mockFileListener, mockAsyncFile, mockEventManager,
+ mockParcelFileDescriptor));
+ reset(mockFileListener, mockAsyncFile, mockEventManager);
+ }
+
+ private StreamingPacketFile createFile(
+ int maxBufferedInboundPackets, int maxBufferedOutboundPackets) throws Exception {
+ when(mockEventManager.registerFile(any(), any())).thenReturn(mockAsyncFile);
+ return new StreamingPacketFile(
+ mockEventManager,
+ FileHandle.fromFileDescriptor(mockParcelFileDescriptor),
+ mockFileListener,
+ MAX_PACKET_SIZE,
+ maxBufferedInboundPackets,
+ maxBufferedOutboundPackets);
+ }
+}
diff --git a/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt b/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
index 46a3588..30e0daf 100644
--- a/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
@@ -19,6 +19,7 @@
import android.os.Handler
import android.os.HandlerThread
import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -72,5 +73,9 @@
assertEquals(attempt, x)
handler.post { assertEquals(attempt, x) }
}
+
+ assertFailsWith<IllegalArgumentException> {
+ visibleOnHandlerThread(handler) { throw IllegalArgumentException() }
+ }
}
}
diff --git a/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt b/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt
index eed31e0..4ed881a 100644
--- a/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt
+++ b/staticlibs/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt
@@ -34,6 +34,7 @@
import com.android.testutils.RecorderCallback.CallbackEntry.Companion.RESUMED
import com.android.testutils.RecorderCallback.CallbackEntry.Companion.SUSPENDED
import com.android.testutils.RecorderCallback.CallbackEntry.Companion.UNAVAILABLE
+import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import kotlin.reflect.KClass
import kotlin.test.assertEquals
import kotlin.test.assertFails
@@ -140,20 +141,28 @@
val meteredNc = NetworkCapabilities()
val unmeteredNc = NetworkCapabilities().addCapability(NOT_METERED)
// Check that expecting caps (with or without) fails when no callback has been received.
- assertFails { mCallback.expectCapabilitiesWith(NOT_METERED, matcher, SHORT_TIMEOUT_MS) }
- assertFails { mCallback.expectCapabilitiesWithout(NOT_METERED, matcher, SHORT_TIMEOUT_MS) }
+ assertFails {
+ mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { it.hasCapability(NOT_METERED) }
+ }
+ assertFails {
+ mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { !it.hasCapability(NOT_METERED) }
+ }
// Add NOT_METERED and check that With succeeds and Without fails.
mCallback.onCapabilitiesChanged(net, unmeteredNc)
- mCallback.expectCapabilitiesWith(NOT_METERED, matcher)
+ mCallback.expectCaps(matcher) { it.hasCapability(NOT_METERED) }
mCallback.onCapabilitiesChanged(net, unmeteredNc)
- assertFails { mCallback.expectCapabilitiesWithout(NOT_METERED, matcher, SHORT_TIMEOUT_MS) }
+ assertFails {
+ mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { !it.hasCapability(NOT_METERED) }
+ }
// Don't add NOT_METERED and check that With fails and Without succeeds.
mCallback.onCapabilitiesChanged(net, meteredNc)
- assertFails { mCallback.expectCapabilitiesWith(NOT_METERED, matcher, SHORT_TIMEOUT_MS) }
+ assertFails {
+ mCallback.expectCaps(matcher, SHORT_TIMEOUT_MS) { it.hasCapability(NOT_METERED) }
+ }
mCallback.onCapabilitiesChanged(net, meteredNc)
- mCallback.expectCapabilitiesWithout(NOT_METERED, matcher)
+ mCallback.expectCaps(matcher) { !it.hasCapability(NOT_METERED) }
}
@Test
@@ -179,37 +188,35 @@
}
@Test
- fun testCapabilitiesThat() {
+ fun testExpectCaps() {
val net = Network(101)
val netCaps = NetworkCapabilities().addCapability(NOT_METERED).addTransportType(WIFI)
// Check that expecting capabilitiesThat anything fails when no callback has been received.
- assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { true } }
+ assertFails { mCallback.expectCaps(net, SHORT_TIMEOUT_MS) { true } }
// Basic test for true and false
mCallback.onCapabilitiesChanged(net, netCaps)
- mCallback.expectCapabilitiesThat(net) { true }
+ mCallback.expectCaps(net) { true }
mCallback.onCapabilitiesChanged(net, netCaps)
- assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { false } }
+ assertFails { mCallback.expectCaps(net, SHORT_TIMEOUT_MS) { false } }
// Try a positive and a negative case
mCallback.onCapabilitiesChanged(net, netCaps)
- mCallback.expectCapabilitiesThat(net) { caps ->
- caps.hasCapability(NOT_METERED) &&
- caps.hasTransport(WIFI) &&
- !caps.hasTransport(CELLULAR)
+ mCallback.expectCaps(net) {
+ it.hasCapability(NOT_METERED) && it.hasTransport(WIFI) && !it.hasTransport(CELLULAR)
}
mCallback.onCapabilitiesChanged(net, netCaps)
- assertFails { mCallback.expectCapabilitiesThat(net, SHORT_TIMEOUT_MS) { caps ->
- caps.hasTransport(CELLULAR)
- } }
+ assertFails { mCallback.expectCaps(net, SHORT_TIMEOUT_MS) { it.hasTransport(CELLULAR) } }
// Try a matching callback on the wrong network
mCallback.onCapabilitiesChanged(net, netCaps)
- assertFails { mCallback.expectCapabilitiesThat(Network(100), SHORT_TIMEOUT_MS) { true } }
+ assertFails {
+ mCallback.expectCaps(Network(100), SHORT_TIMEOUT_MS) { true }
+ }
}
@Test
- fun testLinkPropertiesThat() {
+ fun testLinkPropertiesCallbacks() {
val net = Network(112)
val linkAddress = LinkAddress("fe80::ace:d00d/64")
val mtu = 1984
@@ -220,30 +227,30 @@
}
// Check that expecting linkPropsThat anything fails when no callback has been received.
- assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { true } }
+ assertFails { mCallback.expect<LinkPropertiesChanged>(net, SHORT_TIMEOUT_MS) { true } }
// Basic test for true and false
mCallback.onLinkPropertiesChanged(net, linkProps)
- mCallback.expectLinkPropertiesThat(net) { true }
+ mCallback.expect<LinkPropertiesChanged>(net) { true }
mCallback.onLinkPropertiesChanged(net, linkProps)
- assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { false } }
+ assertFails { mCallback.expect<LinkPropertiesChanged>(net, SHORT_TIMEOUT_MS) { false } }
// Try a positive and negative case
mCallback.onLinkPropertiesChanged(net, linkProps)
- mCallback.expectLinkPropertiesThat(net) { lp ->
- lp.interfaceName == TEST_INTERFACE_NAME &&
- lp.linkAddresses.contains(linkAddress) &&
- lp.mtu == mtu
+ mCallback.expect<LinkPropertiesChanged>(net) {
+ it.lp.interfaceName == TEST_INTERFACE_NAME &&
+ it.lp.linkAddresses.contains(linkAddress) &&
+ it.lp.mtu == mtu
}
mCallback.onLinkPropertiesChanged(net, linkProps)
- assertFails { mCallback.expectLinkPropertiesThat(net, SHORT_TIMEOUT_MS) { lp ->
- lp.interfaceName != TEST_INTERFACE_NAME
+ assertFails { mCallback.expect<LinkPropertiesChanged>(net, SHORT_TIMEOUT_MS) {
+ it.lp.interfaceName != TEST_INTERFACE_NAME
} }
// Try a matching callback on the wrong network
mCallback.onLinkPropertiesChanged(net, linkProps)
- assertFails { mCallback.expectLinkPropertiesThat(Network(114), SHORT_TIMEOUT_MS) { lp ->
- lp.interfaceName == TEST_INTERFACE_NAME
+ assertFails { mCallback.expect<LinkPropertiesChanged>(Network(114), SHORT_TIMEOUT_MS) {
+ it.lp.interfaceName == TEST_INTERFACE_NAME
} }
}
@@ -359,18 +366,6 @@
}
@Test
- fun testPollOrThrow() {
- assertFails { mCallback.pollOrThrow(SHORT_TIMEOUT_MS) }
- TNCInterpreter.interpretTestSpec(initial = mCallback, lineShift = 1,
- threadTransform = { cb -> cb.createLinkedCopy() }, spec = """
- sleep; onAvailable(133) | pollOrThrow(2) = Available(133) time 1..4
- | pollOrThrow(1) fails
- onCapabilitiesChanged(108) | pollOrThrow(1) = CapabilitiesChanged(108) time 0..3
- onBlockedStatus(199) | pollOrThrow(1) = BlockedStatus(199) time 0..3
- """)
- }
-
- @Test
fun testEventuallyExpect() {
// TODO: Current test does not verify the inline one. Also verify the behavior after
// aligning two eventuallyExpect()
@@ -448,7 +443,6 @@
}
},
Regex("""poll\((\d+)\)""") to { i, cb, t -> cb.poll(t.timeArg(1)) },
- Regex("""pollOrThrow\((\d+)\)""") to { i, cb, t -> cb.pollOrThrow(t.timeArg(1)) },
// Interpret "eventually(Available(xx), timeout)" as calling eventuallyExpect that expects
// CallbackEntry.AVAILABLE with netId of xx within timeout*INTERPRET_TIME_UNIT timeout, and
// likewise for all callback types.
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index c9b3d07..3382156 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -36,7 +36,9 @@
"libnanohttpd",
"net-tests-utils-host-device-common",
"net-utils-device-common",
+ "net-utils-device-common-async",
"net-utils-device-common-netlink",
+ "net-utils-device-common-wear",
"modules-utils-build_system",
],
lint: { strict_updatability_linting: true },
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
index f34ca22..f72938d 100644
--- a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitychecker/ConnectivityCheckTest.kt
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitychecker/ConnectivityCheckTest.kt
@@ -29,10 +29,10 @@
import com.android.testutils.RecorderCallback
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.tryTest
-import org.junit.Test
-import org.junit.runner.RunWith
import kotlin.test.assertTrue
import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ConnectivityCheckTest {
@@ -76,7 +76,7 @@
.addTransportType(TRANSPORT_CELLULAR)
.addCapability(NET_CAPABILITY_INTERNET).build(), cb)
tryTest {
- cb.eventuallyExpectOrNull<RecorderCallback.CallbackEntry.Available>()
+ cb.poll { it is RecorderCallback.CallbackEntry.Available }
?: fail("The device does not have mobile data available. Check that it is " +
"setup with a SIM card that has a working data plan, and that the " +
"APN configuration is valid.")
@@ -84,4 +84,4 @@
cm.unregisterNetworkCallback(cb)
}
}
-}
\ 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
index 7b5ad01..71f7877 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
@@ -32,6 +32,7 @@
import android.os.SystemClock
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.testutils.RecorderCallback.CallbackEntry
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import kotlin.test.assertNotNull
@@ -72,9 +73,7 @@
val config = getOrCreateWifiConfiguration()
connectToWifiConfig(config)
}
- val cb = callback.eventuallyExpectOrNull<RecorderCallback.CallbackEntry.Available>(
- timeoutMs = WIFI_CONNECT_TIMEOUT_MS)
-
+ val cb = callback.poll(WIFI_CONNECT_TIMEOUT_MS) { it is CallbackEntry.Available }
assertNotNull(cb, "Could not connect to a wifi access point within " +
"$WIFI_CONNECT_TIMEOUT_MS ms. Check that the test device has a wifi network " +
"configured, and that the test access point is functioning properly.")
@@ -201,4 +200,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt
index 6871349..aa252a5 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt
@@ -65,11 +65,13 @@
*/
fun visibleOnHandlerThread(handler: Handler, r: ThrowingRunnable) {
val cv = ConditionVariable()
+ var e: Exception? = null
handler.post {
try {
r.run()
} catch (exception: Exception) {
Log.e(TAG, "visibleOnHandlerThread caught exception", exception)
+ e = exception
}
cv.open()
}
@@ -77,4 +79,5 @@
// and this thread also has seen the change (since cv.open() happens-before cv.block()
// returns).
cv.block()
+ e?.let { throw it }
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt
new file mode 100644
index 0000000..d7961a0
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import java.io.FileDescriptor
+import java.net.InetAddress
+
+/**
+ * A class that forwards packets from the external {@link TestNetworkInterface} to the internal
+ * {@link TestNetworkInterface} with NAT. See {@link NatPacketForwarderBase} for detail.
+ */
+class NatExternalPacketForwarder(
+ srcFd: FileDescriptor,
+ mtu: Int,
+ dstFd: FileDescriptor,
+ extAddr: InetAddress,
+ natMap: PacketBridge.NatMap
+) : NatPacketForwarderBase(srcFd, mtu, dstFd, extAddr, natMap) {
+
+ /**
+ * Rewrite addresses, ports and fix up checksums for packets received on the external
+ * interface.
+ *
+ * Incoming response from external interface which is being forwarded to the internal
+ * interface with translated address, e.g. 1.2.3.4:80 -> 8.8.8.8:1234
+ * will be translated into 8.8.8.8:80 -> 192.168.1.1:5678.
+ *
+ * For packets that are not an incoming response, do not forward them to the
+ * internal interface.
+ */
+ override fun preparePacketForForwarding(buf: ByteArray, len: Int, version: Int, proto: Int) {
+ val (addrPos, addrLen) = getAddressPositionAndLength(version)
+
+ // TODO: support one external address per ip version.
+ val extAddrBuf = mExtAddr.address
+ if (addrLen != extAddrBuf.size) throw IllegalStateException("Packet IP version mismatch")
+
+ // Get internal address by port.
+ val transportOffset =
+ if (version == 4) PacketReflector.IPV4_HEADER_LENGTH
+ else PacketReflector.IPV6_HEADER_LENGTH
+ val dstPort = getPortAt(buf, transportOffset + DESTINATION_PORT_OFFSET)
+ val intAddrInfo = synchronized(mNatMap) { mNatMap.fromExternalPort(dstPort) }
+ // No mapping, skip. This usually happens if the connection is initiated directly on
+ // the external interface, e.g. DNS64 resolution, network validation, etc.
+ if (intAddrInfo == null) return
+
+ val intAddrBuf = intAddrInfo.address.address
+ val intPort = intAddrInfo.port
+
+ // Copy the original destination to into the source address.
+ for (i in 0 until addrLen) {
+ buf[addrPos + i] = buf[addrPos + addrLen + i]
+ }
+
+ // Copy the internal address into the destination address.
+ for (i in 0 until addrLen) {
+ buf[addrPos + addrLen + i] = intAddrBuf[i]
+ }
+
+ // Copy the internal port into the destination port.
+ setPortAt(intPort, buf, transportOffset + DESTINATION_PORT_OFFSET)
+
+ // Fix IP and Transport layer checksum.
+ fixPacketChecksum(buf, len, version, proto.toByte())
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt
new file mode 100644
index 0000000..fa39d19
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import java.io.FileDescriptor
+import java.net.InetAddress
+
+/**
+ * A class that forwards packets from the internal {@link TestNetworkInterface} to the external
+ * {@link TestNetworkInterface} with NAT. See {@link NatPacketForwarderBase} for detail.
+ */
+class NatInternalPacketForwarder(
+ srcFd: FileDescriptor,
+ mtu: Int,
+ dstFd: FileDescriptor,
+ extAddr: InetAddress,
+ natMap: PacketBridge.NatMap
+) : NatPacketForwarderBase(srcFd, mtu, dstFd, extAddr, natMap) {
+
+ /**
+ * Rewrite addresses, ports and fix up checksums for packets received on the internal
+ * interface.
+ *
+ * Outgoing packet from the internal interface which is being forwarded to the
+ * external interface with translated address, e.g. 192.168.1.1:5678 -> 8.8.8.8:80
+ * will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
+ *
+ * The external port, e.g. 1234 in the above example, is the port number assigned by
+ * the forwarder when creating the mapping to identify the source address and port when
+ * the response is coming from the external interface. See {@link PacketBridge.NatMap}
+ * for detail.
+ */
+ override fun preparePacketForForwarding(buf: ByteArray, len: Int, version: Int, proto: Int) {
+ val (addrPos, addrLen) = getAddressPositionAndLength(version)
+
+ // TODO: support one external address per ip version.
+ val extAddrBuf = mExtAddr.address
+ if (addrLen != extAddrBuf.size) throw IllegalStateException("Packet IP version mismatch")
+
+ val srcAddr = getInetAddressAt(buf, addrPos, addrLen)
+
+ // Copy the original destination to into the source address.
+ for (i in 0 until addrLen) {
+ buf[addrPos + i] = buf[addrPos + addrLen + i]
+ }
+
+ // Copy the external address into the destination address.
+ for (i in 0 until addrLen) {
+ buf[addrPos + addrLen + i] = extAddrBuf[i]
+ }
+
+ // Add an entry to NAT mapping table.
+ val transportOffset =
+ if (version == 4) PacketReflector.IPV4_HEADER_LENGTH
+ else PacketReflector.IPV6_HEADER_LENGTH
+ val srcPort = getPortAt(buf, transportOffset)
+ val extPort = synchronized(mNatMap) { mNatMap.toExternalPort(srcAddr, srcPort, proto) }
+ // Copy the external port to into the source port.
+ setPortAt(extPort, buf, transportOffset)
+
+ // Fix IP and Transport layer checksum.
+ fixPacketChecksum(buf, len, version, proto.toByte())
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java b/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
new file mode 100644
index 0000000..85c6493
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import static com.android.testutils.PacketReflector.IPPROTO_TCP;
+import static com.android.testutils.PacketReflector.IPPROTO_UDP;
+import static com.android.testutils.PacketReflector.IPV4_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.IPV6_PROTO_OFFSET;
+import static com.android.testutils.PacketReflector.TCP_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.UDP_HEADER_LENGTH;
+
+import android.annotation.NonNull;
+import android.net.TestNetworkInterface;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.Objects;
+
+/**
+ * A class that forwards packets from a {@link TestNetworkInterface} to another
+ * {@link TestNetworkInterface} with NAT.
+ *
+ * For testing purposes, a {@link TestNetworkInterface} provides a {@link FileDescriptor}
+ * which allows content injection on the test network. However, this could be hard to use
+ * because the callers need to compose IP packets in order to inject content to the
+ * test network.
+ *
+ * In order to remove the need of composing the IP packets, this class forwards IP packets to
+ * the {@link FileDescriptor} of another {@link TestNetworkInterface} instance. Thus,
+ * the TCP/IP headers could be parsed/composed automatically by the protocol stack of this
+ * additional {@link TestNetworkInterface}, while the payload is supplied by the
+ * servers run on the interface.
+ *
+ * To make it work, an internal interface and an external interface are defined, where
+ * the client might send packets from the internal interface which are originated from
+ * multiple addresses to a server that listens on the external address.
+ *
+ * When forwarding the outgoing packet on the internal interface, a simple NAT mechanism
+ * is implemented during forwarding, which will swap the source and destination,
+ * but replacing the source address with the external address,
+ * e.g. 192.168.1.1:1234 -> 8.8.8.8:80 will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
+ *
+ * For the above example, a client who sends http request will have a hallucination that
+ * it is talking to a remote server at 8.8.8.8. Also, the server listens on 1.2.3.4 will
+ * have a different hallucination that the request is sent from a remote client at 8.8.8.8,
+ * to a local address 1.2.3.4.
+ *
+ * And a NAT mapping is created at the time when the outgoing packet is forwarded.
+ * With a different internal source port, the instance learned that when a response with the
+ * destination port 1234, it should forward the packet to the internal address 192.168.1.1.
+ *
+ * For the incoming packet received from external interface, for example a http response sent
+ * from the http server, the same mechanism is applied but in a different direction,
+ * where the source and destination will be swapped, and the source address will be replaced
+ * with the internal address, which is obtained from the NAT mapping described above.
+ */
+public abstract class NatPacketForwarderBase extends Thread {
+ private static final String TAG = "NatPacketForwarder";
+ static final int DESTINATION_PORT_OFFSET = 2;
+
+ // The source fd to read packets from.
+ @NonNull
+ final FileDescriptor mSrcFd;
+ // The buffer to temporarily hold the entire packet after receiving.
+ @NonNull
+ final byte[] mBuf;
+ // The destination fd to write packets to.
+ @NonNull
+ final FileDescriptor mDstFd;
+ // The NAT mapping table shared between two NatPacketForwarder instances to map from
+ // the source port to the associated internal address. The map can be read/write from two
+ // different threads on any given time whenever receiving packets on the
+ // {@link TestNetworkInterface}. Thus, synchronize on the object when reading/writing is needed.
+ @GuardedBy("mNatMap")
+ @NonNull
+ final PacketBridge.NatMap mNatMap;
+ // The address of the external interface. See {@link NatPacketForwarder}.
+ @NonNull
+ final InetAddress mExtAddr;
+
+ /**
+ * Construct a {@link NatPacketForwarderBase}.
+ *
+ * This class reads packets from {@code srcFd} of a {@link TestNetworkInterface}, and
+ * forwards them to the {@code dstFd} of another {@link TestNetworkInterface} with
+ * NAT applied. See {@link NatPacketForwarderBase}.
+ *
+ * To apply NAT, the address of the external interface needs to be supplied through
+ * {@code extAddr} to identify the external interface. And a shared NAT mapping table,
+ * {@code natMap} is needed to be shared between these two instances.
+ *
+ * Note that this class is not useful if the instance is not managed by a
+ * {@link PacketBridge} to set up a two-way communication.
+ *
+ * @param srcFd {@link FileDescriptor} to read packets from.
+ * @param mtu MTU of the test network.
+ * @param dstFd {@link FileDescriptor} to write packets to.
+ * @param extAddr the external address, which is the address of the external interface.
+ * See {@link NatPacketForwarderBase}.
+ * @param natMap the NAT mapping table shared between two {@link NatPacketForwarderBase}
+ * instance.
+ */
+ public NatPacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
+ @NonNull FileDescriptor dstFd, @NonNull InetAddress extAddr,
+ @NonNull PacketBridge.NatMap natMap) {
+ super(TAG);
+ mSrcFd = Objects.requireNonNull(srcFd);
+ mBuf = new byte[mtu];
+ mDstFd = Objects.requireNonNull(dstFd);
+ mExtAddr = Objects.requireNonNull(extAddr);
+ mNatMap = Objects.requireNonNull(natMap);
+ }
+
+ /**
+ * A method to prepare forwarding packets between two instances of {@link TestNetworkInterface},
+ * which includes re-write addresses, ports and fix up checksums.
+ * Subclasses should override this method to implement a simple NAT.
+ */
+ abstract void preparePacketForForwarding(@NonNull byte[] buf, int len, int version, int proto);
+
+ private void forwardPacket(@NonNull byte[] buf, int len) {
+ try {
+ Os.write(mDstFd, buf, 0, len);
+ } catch (ErrnoException | IOException e) {
+ Log.e(TAG, "Error writing packet: " + e.getMessage());
+ }
+ }
+
+ // Reads one packet from mSrcFd, and writes the packet to the mDstFd for supported protocols.
+ private void processPacket() {
+ final int len = PacketReflectorUtil.readPacket(mSrcFd, mBuf);
+ if (len < 1) {
+ throw new IllegalStateException("Unexpected buffer length: " + len);
+ }
+
+ final int version = mBuf[0] >>> 4;
+ final int protoPos, ipHdrLen;
+ switch (version) {
+ case 4:
+ ipHdrLen = IPV4_HEADER_LENGTH;
+ protoPos = PacketReflector.IPV4_PROTO_OFFSET;
+ break;
+ case 6:
+ ipHdrLen = IPV6_HEADER_LENGTH;
+ protoPos = IPV6_PROTO_OFFSET;
+ break;
+ default:
+ throw new IllegalStateException("Unexpected version: " + version);
+ }
+ if (len < ipHdrLen) {
+ throw new IllegalStateException("Unexpected buffer length: " + len);
+ }
+
+ final byte proto = mBuf[protoPos];
+ final int transportHdrLen;
+ switch (proto) {
+ case IPPROTO_TCP:
+ transportHdrLen = TCP_HEADER_LENGTH;
+ break;
+ case IPPROTO_UDP:
+ transportHdrLen = UDP_HEADER_LENGTH;
+ break;
+ // TODO: Support ICMP.
+ default:
+ return; // Unknown protocol, ignored.
+ }
+
+ if (len < ipHdrLen + transportHdrLen) {
+ throw new IllegalStateException("Unexpected buffer length: " + len);
+ }
+ // Re-write addresses, ports and fix up checksums.
+ preparePacketForForwarding(mBuf, len, version, proto);
+ // Send the packet to the destination fd.
+ forwardPacket(mBuf, len);
+ }
+
+ @Override
+ public void run() {
+ Log.i(TAG, "starting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
+ while (!interrupted() && mSrcFd.valid()) {
+ processPacket();
+ }
+ Log.i(TAG, "exiting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
new file mode 100644
index 0000000..da3508d
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.net.TestNetworkSpecifier
+import android.os.Binder
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import java.net.InetAddress
+import libcore.io.IoUtils
+
+private const val MIN_PORT_NUMBER = 1025
+private const val MAX_PORT_NUMBER = 65535
+
+/**
+ * A class that set up two {@link TestNetworkInterface} with NAT, and forward packets between them.
+ *
+ * See {@link NatPacketForwarder} for more detailed information.
+ */
+class PacketBridge(
+ context: Context,
+ internalAddr: LinkAddress,
+ externalAddr: LinkAddress,
+ dnsAddr: InetAddress
+) {
+ private val natMap = NatMap()
+ private val binder = Binder()
+
+ private val cm = context.getSystemService(ConnectivityManager::class.java)
+ private val tnm = context.getSystemService(TestNetworkManager::class.java)
+
+ // Create test networks.
+ private val internalIface = tnm.createTunInterface(listOf(internalAddr))
+ private val externalIface = tnm.createTunInterface(listOf(externalAddr))
+
+ // Register test networks to ConnectivityService.
+ private val internalNetworkCallback: TestableNetworkCallback
+ private val externalNetworkCallback: TestableNetworkCallback
+ val internalNetwork: Network
+ val externalNetwork: Network
+ init {
+ val (inCb, inNet) = createTestNetwork(internalIface, internalAddr, dnsAddr)
+ val (exCb, exNet) = createTestNetwork(externalIface, externalAddr, dnsAddr)
+ internalNetworkCallback = inCb
+ externalNetworkCallback = exCb
+ internalNetwork = inNet
+ externalNetwork = exNet
+ }
+
+ // Setup the packet bridge.
+ private val internalFd = internalIface.fileDescriptor.fileDescriptor
+ private val externalFd = externalIface.fileDescriptor.fileDescriptor
+
+ private val pr1 = NatInternalPacketForwarder(
+ internalFd,
+ 1500,
+ externalFd,
+ externalAddr.address,
+ natMap
+ )
+ private val pr2 = NatExternalPacketForwarder(
+ externalFd,
+ 1500,
+ internalFd,
+ externalAddr.address,
+ natMap
+ )
+
+ fun start() {
+ IoUtils.setBlocking(internalFd, true /* blocking */)
+ IoUtils.setBlocking(externalFd, true /* blocking */)
+ pr1.start()
+ pr2.start()
+ }
+
+ fun stop() {
+ pr1.interrupt()
+ pr2.interrupt()
+ cm.unregisterNetworkCallback(internalNetworkCallback)
+ cm.unregisterNetworkCallback(externalNetworkCallback)
+ }
+
+ /**
+ * Creates a test network with given test TUN interface and addresses.
+ */
+ private fun createTestNetwork(
+ testIface: TestNetworkInterface,
+ addr: LinkAddress,
+ dnsAddr: InetAddress
+ ): Pair<TestableNetworkCallback, Network> {
+ // Make a network request to hold the test network
+ val nr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .setNetworkSpecifier(TestNetworkSpecifier(testIface.interfaceName))
+ .build()
+ val testCb = TestableNetworkCallback()
+ cm.requestNetwork(nr, testCb)
+
+ val lp = LinkProperties().apply {
+ addLinkAddress(addr)
+ interfaceName = testIface.interfaceName
+ addDnsServer(dnsAddr)
+ }
+ tnm.setupTestNetwork(lp, true /* isMetered */, binder)
+
+ // Wait for available before return.
+ val network = testCb.expect<Available>().network
+ return testCb to network
+ }
+
+ /**
+ * A helper class to maintain the mappings between internal addresses/ports and external
+ * ports.
+ *
+ * This class assigns an unused external port number if the mapping between
+ * srcaddress:srcport:protocol and the external port does not exist yet.
+ *
+ * Note that this class is not thread-safe. The instance of the class needs to be
+ * synchronized in the callers when being used in multiple threads.
+ */
+ class NatMap {
+ data class AddressInfo(val address: InetAddress, val port: Int, val protocol: Int)
+
+ private val mToExternalPort = HashMap<AddressInfo, Int>()
+ private val mFromExternalPort = HashMap<Int, AddressInfo>()
+
+ // Skip well-known port 0~1024.
+ private var nextExternalPort = MIN_PORT_NUMBER
+
+ fun toExternalPort(addr: InetAddress, port: Int, protocol: Int): Int {
+ val info = AddressInfo(addr, port, protocol)
+ val extPort: Int
+ if (!mToExternalPort.containsKey(info)) {
+ extPort = nextExternalPort++
+ if (nextExternalPort > MAX_PORT_NUMBER) {
+ throw IllegalStateException("Available ports are exhausted")
+ }
+ mToExternalPort[info] = extPort
+ mFromExternalPort[extPort] = info
+ } else {
+ extPort = mToExternalPort[info]!!
+ }
+ return extPort
+ }
+
+ fun fromExternalPort(port: Int): AddressInfo? {
+ return mFromExternalPort[port]
+ }
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
new file mode 100644
index 0000000..69392d4
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2014 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 static android.system.OsConstants.ICMP6_ECHO_REPLY;
+import static android.system.OsConstants.ICMP6_ECHO_REQUEST;
+
+import android.annotation.NonNull;
+import android.net.TestNetworkInterface;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * A class that echoes packets received on a {@link TestNetworkInterface} back to itself.
+ *
+ * For testing purposes, sometimes a mocked environment to simulate a simple echo from the
+ * server side is needed. This is particularly useful if the test, e.g. VpnTest, is
+ * heavily relying on the outside world.
+ *
+ * This class reads packets from the {@link FileDescriptor} of a {@link TestNetworkInterface}, and:
+ * 1. For TCP and UDP packets, simply swaps the source address and the destination
+ * address, then send it back to the {@link FileDescriptor}.
+ * 2. For ICMP ping packets, composes a ping reply and sends it back to the sender.
+ * 3. Ignore all other packets.
+ */
+public class PacketReflector extends Thread {
+
+ static final int IPV4_HEADER_LENGTH = 20;
+ static final int IPV6_HEADER_LENGTH = 40;
+
+ static final int IPV4_ADDR_OFFSET = 12;
+ static final int IPV6_ADDR_OFFSET = 8;
+ static final int IPV4_ADDR_LENGTH = 4;
+ static final int IPV6_ADDR_LENGTH = 16;
+
+ static final int IPV4_PROTO_OFFSET = 9;
+ static final int IPV6_PROTO_OFFSET = 6;
+
+ static final byte IPPROTO_ICMP = 1;
+ static final byte IPPROTO_TCP = 6;
+ static final byte IPPROTO_UDP = 17;
+ private static final byte IPPROTO_ICMPV6 = 58;
+
+ private static final int ICMP_HEADER_LENGTH = 8;
+ static final int TCP_HEADER_LENGTH = 20;
+ static final int UDP_HEADER_LENGTH = 8;
+
+ private static final byte ICMP_ECHO = 8;
+ private static final byte ICMP_ECHOREPLY = 0;
+
+ private static String TAG = "PacketReflector";
+
+ @NonNull
+ private final FileDescriptor mFd;
+ @NonNull
+ private final byte[] mBuf;
+
+ /**
+ * Construct a {@link PacketReflector} from the given {@code fd} of
+ * a {@link TestNetworkInterface}.
+ *
+ * @param fd {@link FileDescriptor} to read/write packets.
+ * @param mtu MTU of the test network.
+ */
+ public PacketReflector(@NonNull FileDescriptor fd, int mtu) {
+ super("PacketReflector");
+ mFd = Objects.requireNonNull(fd);
+ mBuf = new byte[mtu];
+ }
+
+ private static void swapBytes(@NonNull byte[] buf, int pos1, int pos2, int len) {
+ for (int i = 0; i < len; i++) {
+ byte b = buf[pos1 + i];
+ buf[pos1 + i] = buf[pos2 + i];
+ buf[pos2 + i] = b;
+ }
+ }
+
+ private static void swapAddresses(@NonNull byte[] buf, int version) {
+ int addrPos, addrLen;
+ switch (version) {
+ case 4:
+ addrPos = IPV4_ADDR_OFFSET;
+ addrLen = IPV4_ADDR_LENGTH;
+ break;
+ case 6:
+ addrPos = IPV6_ADDR_OFFSET;
+ addrLen = IPV6_ADDR_LENGTH;
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ swapBytes(buf, addrPos, addrPos + addrLen, addrLen);
+ }
+
+ // Reflect TCP packets: swap the source and destination addresses, but don't change the ports.
+ // This is used by the test to "connect to itself" through the VPN.
+ private void processTcpPacket(@NonNull byte[] buf, int version, int len, int hdrLen) {
+ if (len < hdrLen + TCP_HEADER_LENGTH) {
+ return;
+ }
+
+ // Swap src and dst IP addresses.
+ swapAddresses(buf, version);
+
+ // Send the packet back.
+ writePacket(buf, len);
+ }
+
+ // Echo UDP packets: swap source and destination addresses, and source and destination ports.
+ // This is used by the test to check that the bytes it sends are echoed back.
+ private void processUdpPacket(@NonNull byte[] buf, int version, int len, int hdrLen) {
+ if (len < hdrLen + UDP_HEADER_LENGTH) {
+ return;
+ }
+
+ // Swap src and dst IP addresses.
+ swapAddresses(buf, version);
+
+ // Swap dst and src ports.
+ int portOffset = hdrLen;
+ swapBytes(buf, portOffset, portOffset + 2, 2);
+
+ // Send the packet back.
+ writePacket(buf, len);
+ }
+
+ private void processIcmpPacket(@NonNull byte[] buf, int version, int len, int hdrLen) {
+ if (len < hdrLen + ICMP_HEADER_LENGTH) {
+ return;
+ }
+
+ byte type = buf[hdrLen];
+ if (!(version == 4 && type == ICMP_ECHO) &&
+ !(version == 6 && type == (byte) ICMP6_ECHO_REQUEST)) {
+ return;
+ }
+
+ // Save the ping packet we received.
+ byte[] request = buf.clone();
+
+ // Swap src and dst IP addresses, and send the packet back.
+ // This effectively pings the device to see if it replies.
+ swapAddresses(buf, version);
+ writePacket(buf, len);
+
+ // The device should have replied, and buf should now contain a ping response.
+ int received = PacketReflectorUtil.readPacket(mFd, buf);
+ if (received != len) {
+ Log.i(TAG, "Reflecting ping did not result in ping response: " +
+ "read=" + received + " expected=" + len);
+ return;
+ }
+
+ byte replyType = buf[hdrLen];
+ if ((type == ICMP_ECHO && replyType != ICMP_ECHOREPLY)
+ || (type == (byte) ICMP6_ECHO_REQUEST && replyType != (byte) ICMP6_ECHO_REPLY)) {
+ Log.i(TAG, "Received unexpected ICMP reply: original " + type
+ + ", reply " + replyType);
+ return;
+ }
+
+ // Compare the response we got with the original packet.
+ // The only thing that should have changed are addresses, type and checksum.
+ // Overwrite them with the received bytes and see if the packet is otherwise identical.
+ request[hdrLen] = buf[hdrLen]; // Type
+ request[hdrLen + 2] = buf[hdrLen + 2]; // Checksum byte 1.
+ request[hdrLen + 3] = buf[hdrLen + 3]; // Checksum byte 2.
+
+ // Since Linux kernel 4.2, net.ipv6.auto_flowlabels is set by default, and therefore
+ // the request and reply may have different IPv6 flow label: ignore that as well.
+ if (version == 6) {
+ request[1] = (byte) (request[1] & 0xf0 | buf[1] & 0x0f);
+ request[2] = buf[2];
+ request[3] = buf[3];
+ }
+
+ for (int i = 0; i < len; i++) {
+ if (buf[i] != request[i]) {
+ Log.i(TAG, "Received non-matching packet when expecting ping response.");
+ return;
+ }
+ }
+
+ // Now swap the addresses again and reflect the packet. This sends a ping reply.
+ swapAddresses(buf, version);
+ writePacket(buf, len);
+ }
+
+ private void writePacket(@NonNull byte[] buf, int len) {
+ try {
+ Os.write(mFd, buf, 0, len);
+ } catch (ErrnoException | IOException e) {
+ Log.e(TAG, "Error writing packet: " + e.getMessage());
+ }
+ }
+
+ // Reads one packet from our mFd, and possibly writes the packet back.
+ private void processPacket() {
+ int len = PacketReflectorUtil.readPacket(mFd, mBuf);
+ if (len < 1) {
+ // Usually happens when socket read is being interrupted, e.g. stopping PacketReflector.
+ return;
+ }
+
+ int version = mBuf[0] >> 4;
+ int protoPos, hdrLen;
+ if (version == 4) {
+ hdrLen = IPV4_HEADER_LENGTH;
+ protoPos = IPV4_PROTO_OFFSET;
+ } else if (version == 6) {
+ hdrLen = IPV6_HEADER_LENGTH;
+ protoPos = IPV6_PROTO_OFFSET;
+ } else {
+ throw new IllegalStateException("Unexpected version: " + version);
+ }
+
+ if (len < hdrLen) {
+ throw new IllegalStateException("Unexpected buffer length: " + len);
+ }
+
+ byte proto = mBuf[protoPos];
+ switch (proto) {
+ case IPPROTO_ICMP:
+ // fall through
+ case IPPROTO_ICMPV6:
+ processIcmpPacket(mBuf, version, len, hdrLen);
+ break;
+ case IPPROTO_TCP:
+ processTcpPacket(mBuf, version, len, hdrLen);
+ break;
+ case IPPROTO_UDP:
+ processUdpPacket(mBuf, version, len, hdrLen);
+ break;
+ }
+ }
+
+ public void run() {
+ Log.i(TAG, "starting fd=" + mFd + " valid=" + mFd.valid());
+ while (!interrupted() && mFd.valid()) {
+ processPacket();
+ }
+ Log.i(TAG, "exiting fd=" + mFd + " valid=" + mFd.valid());
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
new file mode 100644
index 0000000..b028045
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("PacketReflectorUtil")
+
+package com.android.testutils
+
+import android.system.ErrnoException
+import android.system.Os
+import com.android.net.module.util.IpUtils
+import com.android.testutils.PacketReflector.IPV4_HEADER_LENGTH
+import com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH
+import java.io.FileDescriptor
+import java.io.IOException
+import java.net.InetAddress
+import java.nio.ByteBuffer
+
+fun readPacket(fd: FileDescriptor, buf: ByteArray): Int {
+ return try {
+ Os.read(fd, buf, 0, buf.size)
+ } catch (e: ErrnoException) {
+ -1
+ } catch (e: IOException) {
+ -1
+ }
+}
+
+fun getInetAddressAt(buf: ByteArray, pos: Int, len: Int): InetAddress =
+ InetAddress.getByAddress(buf.copyOfRange(pos, pos + len))
+
+/**
+ * Reads a 16-bit unsigned int at pos in big endian, with no alignment requirements.
+ */
+fun getPortAt(buf: ByteArray, pos: Int): Int {
+ return (buf[pos].toInt() and 0xff shl 8) + (buf[pos + 1].toInt() and 0xff)
+}
+
+fun setPortAt(port: Int, buf: ByteArray, pos: Int) {
+ buf[pos] = (port ushr 8).toByte()
+ buf[pos + 1] = (port and 0xff).toByte()
+}
+
+fun getAddressPositionAndLength(version: Int) = when (version) {
+ 4 -> PacketReflector.IPV4_ADDR_OFFSET to PacketReflector.IPV4_ADDR_LENGTH
+ 6 -> PacketReflector.IPV6_ADDR_OFFSET to PacketReflector.IPV6_ADDR_LENGTH
+ else -> throw IllegalArgumentException("Unknown IP version $version")
+}
+
+private const val IPV4_CHKSUM_OFFSET = 10
+private const val UDP_CHECKSUM_OFFSET = 6
+private const val TCP_CHECKSUM_OFFSET = 16
+
+fun fixPacketChecksum(buf: ByteArray, len: Int, version: Int, protocol: Byte) {
+ // Fill Ip checksum for IPv4. IPv6 header doesn't have a checksum field.
+ if (version == 4) {
+ val checksum = IpUtils.ipChecksum(ByteBuffer.wrap(buf), 0)
+ // Place checksum in Big-endian order.
+ buf[IPV4_CHKSUM_OFFSET] = (checksum.toInt() ushr 8).toByte()
+ buf[IPV4_CHKSUM_OFFSET + 1] = (checksum.toInt() and 0xff).toByte()
+ }
+
+ // Fill transport layer checksum.
+ val transportOffset = if (version == 4) IPV4_HEADER_LENGTH else IPV6_HEADER_LENGTH
+ when (protocol) {
+ PacketReflector.IPPROTO_UDP -> {
+ val checksumPos = transportOffset + UDP_CHECKSUM_OFFSET
+ // Clear before calculate.
+ buf[checksumPos + 1] = 0x00
+ buf[checksumPos] = buf[checksumPos + 1]
+ val checksum = IpUtils.udpChecksum(
+ ByteBuffer.wrap(buf), 0,
+ transportOffset
+ )
+ buf[checksumPos] = (checksum.toInt() ushr 8).toByte()
+ buf[checksumPos + 1] = (checksum.toInt() and 0xff).toByte()
+ }
+ PacketReflector.IPPROTO_TCP -> {
+ val checksumPos = transportOffset + TCP_CHECKSUM_OFFSET
+ // Clear before calculate.
+ buf[checksumPos + 1] = 0x00
+ buf[checksumPos] = buf[checksumPos + 1]
+ val transportLen: Int = len - transportOffset
+ val checksum = IpUtils.tcpChecksum(
+ ByteBuffer.wrap(buf), 0, transportOffset,
+ transportLen
+ )
+ buf[checksumPos] = (checksum.toInt() ushr 8).toByte()
+ buf[checksumPos + 1] = (checksum.toInt() and 0xff).toByte()
+ }
+ // TODO: Support ICMP.
+ else -> throw IllegalArgumentException("Unsupported protocol: $protocol")
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
index 39ce487..740bf63 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestHttpServer.kt
@@ -19,6 +19,7 @@
import android.net.Uri
import com.android.net.module.util.ArrayTrackRecord
import fi.iki.elonen.NanoHTTPD
+import java.io.IOException
/**
* A minimal HTTP server running on a random available port.
@@ -82,7 +83,23 @@
val request = Request(session.uri
?: "", session.method, session.queryParameterString ?: "")
requestsRecord.add(request)
+
+ // For PUT and POST, call parseBody to read InputStream before responding.
+ if (Method.PUT == session.method || Method.POST == session.method) {
+ try {
+ session.parseBody(HashMap())
+ } catch (e: Exception) {
+ when (e) {
+ is IOException, is ResponseException -> e.toResponse()
+ else -> throw e
+ }
+ }
+ }
+
// Default response is a 404
return responses[request] ?: super.serve(session)
}
-}
\ No newline at end of file
+
+ fun Exception.toResponse() =
+ newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", this.toString())
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
index 68d5fa9..0e73112 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt
@@ -83,7 +83,7 @@
) : CallbackEntry()
data class BlockedStatusInt(
override val network: Network,
- val blocked: Int
+ val reason: Int
) : CallbackEntry()
// Convenience constants for expecting a type
companion object {
@@ -220,19 +220,8 @@
* Long.MAX_VALUE.
*/
@JvmOverloads
- fun poll(timeoutMs: Long = defaultTimeoutMs): CallbackEntry? = history.poll(timeoutMs)
-
- /**
- * Get the next callback or throw if timeout.
- *
- * With no argument, this method waits out the default timeout. To wait forever, pass
- * Long.MAX_VALUE.
- */
- @JvmOverloads
- fun pollOrThrow(
- timeoutMs: Long = defaultTimeoutMs,
- errorMsg: String = "Did not receive callback after $timeoutMs"
- ): CallbackEntry = poll(timeoutMs) ?: fail(errorMsg)
+ fun poll(timeoutMs: Long = defaultTimeoutMs, predicate: (CallbackEntry) -> Boolean = { true }) =
+ history.poll(timeoutMs, predicate)
/*****
* expect family of methods.
@@ -349,15 +338,16 @@
timeoutMs: Long = defaultTimeoutMs,
errorMsg: String? = null,
test: (T) -> Boolean = { true }
- ) = pollOrThrow(timeoutMs, "Did not receive ${T::class.simpleName} after ${timeoutMs}ms").also {
- if (it !is T) fail("Expected callback ${T::class.simpleName}, got $it")
- if (ANY_NETWORK !== network && it.network != network) {
- fail("Expected network $network for callback : $it")
- }
- if (!test(it)) {
- fail("${errorMsg ?: "Callback doesn't match predicate"} : $it")
- }
- } as T
+ ) = (poll(timeoutMs) ?: fail("Did not receive ${T::class.simpleName} after ${timeoutMs}ms"))
+ .also {
+ if (it !is T) fail("Expected callback ${T::class.simpleName}, got $it")
+ if (ANY_NETWORK !== network && it.network != network) {
+ fail("Expected network $network for callback : $it")
+ }
+ if (!test(it)) {
+ fail("${errorMsg ?: "Callback doesn't match predicate"} : $it")
+ }
+ } as T
inline fun <reified T : CallbackEntry> expect(
network: HasNetwork,
@@ -366,6 +356,12 @@
test: (T) -> Boolean = { true }
) = expect(network.network, timeoutMs, errorMsg, test)
+ /*****
+ * assertNoCallback family of methods.
+ * These methods make sure that no callback that matches the predicate was received.
+ * If no predicate is given, they make sure that no callback at all was received.
+ * These methods run the waiter func given in the constructor if any.
+ */
@JvmOverloads
fun assertNoCallback(
timeoutMs: Long = defaultNoCallbackTimeoutMs,
@@ -378,14 +374,17 @@
fun assertNoCallback(valid: (CallbackEntry) -> Boolean) =
assertNoCallback(defaultNoCallbackTimeoutMs, valid)
- // Expects a callback of the specified type matching the predicate within the timeout.
- // Any callback that doesn't match the predicate will be skipped. Fails only if
- // no matching callback is received within the timeout.
+ /*****
+ * eventuallyExpect family of methods.
+ * These methods make sure a callback that matches the type/predicate is received eventually.
+ * Any callback of the wrong type, or doesn't match the optional predicate, is ignored.
+ * They fail if no callback matching the predicate is received within the timeout.
+ */
inline fun <reified T : CallbackEntry> eventuallyExpect(
timeoutMs: Long = defaultTimeoutMs,
from: Int = mark,
crossinline predicate: (T) -> Boolean = { true }
- ): T = eventuallyExpectOrNull(timeoutMs, from, predicate).also {
+ ): T = history.poll(timeoutMs, from) { it is T && predicate(it) }.also {
assertNotNull(it, "Callback ${T::class} not received within ${timeoutMs}ms")
} as T
@@ -407,27 +406,6 @@
assertNotNull(it, "Callback ${type.java} not received within ${timeoutMs}ms")
} as T
- // TODO (b/157405399) straighten and unify the method names
- inline fun <reified T : CallbackEntry> eventuallyExpectOrNull(
- timeoutMs: Long = defaultTimeoutMs,
- from: Int = mark,
- crossinline predicate: (T) -> Boolean = { true }
- ) = history.poll(timeoutMs, from) { it is T && predicate(it) } as T?
-
- inline fun expectCapabilitiesThat(
- net: Network,
- tmt: Long = defaultTimeoutMs,
- valid: (NetworkCapabilities) -> Boolean
- ): CapabilitiesChanged =
- expect(net, tmt, "Capabilities don't match expectations") { valid(it.caps) }
-
- inline fun expectLinkPropertiesThat(
- net: Network,
- tmt: Long = defaultTimeoutMs,
- valid: (LinkProperties) -> Boolean
- ): LinkPropertiesChanged =
- expect(net, tmt, "LinkProperties don't match expectations") { valid(it.lp) }
-
// Expects onAvailable and the callbacks that follow it. These are:
// - onSuspended, iff the network was suspended when the callbacks fire.
// - onCapabilitiesChanged.
@@ -448,18 +426,18 @@
tmt: Long = defaultTimeoutMs
) {
expectAvailableCallbacksCommon(net, suspended, validated, tmt)
- expectBlockedStatusCallback(blocked, net, tmt)
+ expect<BlockedStatus>(net, tmt) { it.blocked == blocked }
}
fun expectAvailableCallbacks(
net: Network,
suspended: Boolean,
validated: Boolean,
- blockedStatus: Int,
+ blockedReason: Int,
tmt: Long
) {
expectAvailableCallbacksCommon(net, suspended, validated, tmt)
- expectBlockedStatusCallback(blockedStatus, net)
+ expect<BlockedStatusInt>(net) { it.reason == blockedReason }
}
private fun expectAvailableCallbacksCommon(
@@ -472,10 +450,8 @@
if (suspended) {
expect<Suspended>(net, tmt)
}
- expectCapabilitiesThat(net, tmt) {
- validated == null || validated == it.hasCapability(
- NET_CAPABILITY_VALIDATED
- )
+ expect<CapabilitiesChanged>(net, tmt) {
+ validated == null || validated == it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
}
expect<LinkPropertiesChanged>(net, tmt)
}
@@ -488,16 +464,6 @@
tmt: Long = defaultTimeoutMs
) = expectAvailableCallbacks(net, suspended = true, validated = validated, tmt = tmt)
- fun expectBlockedStatusCallback(blocked: Boolean, net: Network, tmt: Long = defaultTimeoutMs) =
- expect<BlockedStatus>(net, tmt, "Unexpected blocked status") {
- it.blocked == blocked
- }
-
- fun expectBlockedStatusCallback(blocked: Int, net: Network, tmt: Long = defaultTimeoutMs) =
- expect<BlockedStatusInt>(net, tmt, "Unexpected blocked status") {
- it.blocked == blocked
- }
-
// Expects the available callbacks (where the onCapabilitiesChanged must contain the
// VALIDATED capability), plus another onCapabilitiesChanged which is identical to the
// one we just sent.
@@ -514,17 +480,17 @@
// when a network connects and satisfies a callback, and then immediately validates.
fun expectAvailableThenValidatedCallbacks(net: Network, tmt: Long = defaultTimeoutMs) {
expectAvailableCallbacks(net, validated = false, tmt = tmt)
- expectCapabilitiesThat(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
+ expectCaps(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
}
fun expectAvailableThenValidatedCallbacks(
net: Network,
- blockedStatus: Int,
+ blockedReason: Int,
tmt: Long = defaultTimeoutMs
) {
expectAvailableCallbacks(net, validated = false, suspended = false,
- blockedStatus = blockedStatus, tmt = tmt)
- expectCapabilitiesThat(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
+ blockedReason = blockedReason, tmt = tmt)
+ expectCaps(net, tmt) { it.hasCapability(NET_CAPABILITY_VALIDATED) }
}
// Temporary Java compat measure : have MockNetworkAgent implement this so that all existing
@@ -571,38 +537,26 @@
}
@JvmOverloads
- fun expectLinkPropertiesThat(
+ fun expectCaps(
n: HasNetwork,
tmt: Long = defaultTimeoutMs,
- valid: (LinkProperties) -> Boolean
- ) = expectLinkPropertiesThat(n.network, tmt, valid)
+ valid: (NetworkCapabilities) -> Boolean = { true }
+ ) = expect<CapabilitiesChanged>(n.network, tmt) { valid(it.caps) }.caps
@JvmOverloads
- fun expectCapabilitiesThat(
- n: HasNetwork,
+ fun expectCaps(
+ n: Network,
tmt: Long = defaultTimeoutMs,
valid: (NetworkCapabilities) -> Boolean
- ) = expectCapabilitiesThat(n.network, tmt, valid)
+ ) = expect<CapabilitiesChanged>(n, tmt) { valid(it.caps) }.caps
- @JvmOverloads
- fun expectCapabilitiesWith(
- capability: Int,
+ fun expectCaps(
n: HasNetwork,
- timeoutMs: Long = defaultTimeoutMs
- ): NetworkCapabilities {
- return expectCapabilitiesThat(n.network, timeoutMs) { it.hasCapability(capability) }.caps
- }
+ valid: (NetworkCapabilities) -> Boolean
+ ) = expect<CapabilitiesChanged>(n.network) { valid(it.caps) }.caps
- @JvmOverloads
- fun expectCapabilitiesWithout(
- capability: Int,
- n: HasNetwork,
- timeoutMs: Long = defaultTimeoutMs
- ): NetworkCapabilities {
- return expectCapabilitiesThat(n.network, timeoutMs) { !it.hasCapability(capability) }.caps
- }
-
- fun expectBlockedStatusCallback(expectBlocked: Boolean, n: HasNetwork) {
- expectBlockedStatusCallback(expectBlocked, n.network, defaultTimeoutMs)
- }
+ fun expectCaps(
+ tmt: Long,
+ valid: (NetworkCapabilities) -> Boolean
+ ) = expect<CapabilitiesChanged>(ANY_NETWORK, tmt) { valid(it.caps) }.caps
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java b/staticlibs/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java
new file mode 100644
index 0000000..48b57d7
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java
@@ -0,0 +1,568 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils.async;
+
+import android.os.ParcelFileDescriptor;
+import android.system.StructPollfd;
+import android.util.Log;
+
+import com.android.net.module.util.async.CircularByteBuffer;
+import com.android.net.module.util.async.OsAccess;
+
+import java.io.FileDescriptor;
+import java.io.InterruptedIOException;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.concurrent.TimeUnit;
+
+public class FakeOsAccess extends OsAccess {
+ public static final boolean ENABLE_FINE_DEBUG = true;
+
+ public static final int DEFAULT_FILE_DATA_QUEUE_SIZE = 8 * 1024;
+
+ private enum FileType { PAIR, PIPE }
+
+ // Common poll() constants:
+ private static final short POLLIN = 0x0001;
+ private static final short POLLOUT = 0x0004;
+ private static final short POLLERR = 0x0008;
+ private static final short POLLHUP = 0x0010;
+
+ private static final Constructor<FileDescriptor> FD_CONSTRUCTOR;
+ private static final Field FD_FIELD_DESCRIPTOR;
+ private static final Field PFD_FIELD_DESCRIPTOR;
+ private static final Field PFD_FIELD_GUARD;
+ private static final Method CLOSE_GUARD_METHOD_CLOSE;
+
+ private final int mReadQueueSize = DEFAULT_FILE_DATA_QUEUE_SIZE;
+ private final int mWriteQueueSize = DEFAULT_FILE_DATA_QUEUE_SIZE;
+ private final HashMap<Integer, File> mFiles = new HashMap<>();
+ private final byte[] mTmpBuffer = new byte[1024];
+ private final long mStartTime;
+ private final String mLogTag;
+ private int mFileNumberGen = 3;
+ private boolean mHasRateLimitedData;
+
+ public FakeOsAccess(String logTag) {
+ mLogTag = logTag;
+ mStartTime = monotonicTimeMillis();
+ }
+
+ @Override
+ public long monotonicTimeMillis() {
+ return System.nanoTime() / 1000000;
+ }
+
+ @Override
+ public FileDescriptor getInnerFileDescriptor(ParcelFileDescriptor fd) {
+ try {
+ return (FileDescriptor) PFD_FIELD_DESCRIPTOR.get(fd);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void close(ParcelFileDescriptor fd) {
+ if (fd != null) {
+ close(getInnerFileDescriptor(fd));
+
+ try {
+ // Reduce CloseGuard warnings.
+ Object guard = PFD_FIELD_GUARD.get(fd);
+ CLOSE_GUARD_METHOD_CLOSE.invoke(guard);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ public synchronized void close(FileDescriptor fd) {
+ if (fd != null) {
+ File file = getFileOrNull(fd);
+ if (file != null) {
+ file.decreaseRefCount();
+ mFiles.remove(getFileDescriptorNumber(fd));
+ setFileDescriptorNumber(fd, -1);
+ notifyAll();
+ }
+ }
+ }
+
+ private File getFile(String func, FileDescriptor fd) throws IOException {
+ File file = getFileOrNull(fd);
+ if (file == null) {
+ throw newIOException(func, "Unknown file descriptor: " + getFileDebugName(fd));
+ }
+ return file;
+ }
+
+ private File getFileOrNull(FileDescriptor fd) {
+ return mFiles.get(getFileDescriptorNumber(fd));
+ }
+
+ @Override
+ public String getFileDebugName(ParcelFileDescriptor fd) {
+ return (fd != null ? getFileDebugName(getInnerFileDescriptor(fd)) : "null");
+ }
+
+ public String getFileDebugName(FileDescriptor fd) {
+ if (fd == null) {
+ return "null";
+ }
+
+ final int fdNumber = getFileDescriptorNumber(fd);
+ File file = mFiles.get(fdNumber);
+
+ StringBuilder sb = new StringBuilder();
+ if (file != null) {
+ if (file.name != null) {
+ sb.append(file.name);
+ sb.append("/");
+ }
+ sb.append(file.type);
+ sb.append("/");
+ } else {
+ sb.append("BADFD/");
+ }
+ sb.append(fdNumber);
+ return sb.toString();
+ }
+
+ public synchronized void setFileName(FileDescriptor fd, String name) {
+ File file = getFileOrNull(fd);
+ if (file != null) {
+ file.name = name;
+ }
+ }
+
+ @Override
+ public synchronized void setNonBlocking(FileDescriptor fd) throws IOException {
+ File file = getFile("fcntl", fd);
+ file.isBlocking = false;
+ }
+
+ @Override
+ public synchronized int read(FileDescriptor fd, byte[] buffer, int pos, int len)
+ throws IOException {
+ checkBoundaries("read", buffer, pos, len);
+
+ File file = getFile("read", fd);
+ if (file.readQueue == null) {
+ throw newIOException("read", "File not readable");
+ }
+ file.checkNonBlocking("read");
+
+ if (len == 0) {
+ return 0;
+ }
+
+ final int availSize = file.readQueue.size();
+ if (availSize == 0) {
+ if (file.isEndOfStream) {
+ // Java convention uses -1 to indicate end of stream.
+ return -1;
+ }
+ return 0; // EAGAIN
+ }
+
+ final int readCount = Math.min(len, availSize);
+ file.readQueue.readBytes(buffer, pos, readCount);
+ maybeTransferData(file);
+ return readCount;
+ }
+
+ @Override
+ public synchronized int write(FileDescriptor fd, byte[] buffer, int pos, int len)
+ throws IOException {
+ checkBoundaries("write", buffer, pos, len);
+
+ File file = getFile("write", fd);
+ if (file.writeQueue == null) {
+ throw newIOException("read", "File not writable");
+ }
+ if (file.type == FileType.PIPE && file.sink.openCount == 0) {
+ throw newIOException("write", "The other end of pipe is closed");
+ }
+ file.checkNonBlocking("write");
+
+ if (len == 0) {
+ return 0;
+ }
+
+ final int originalFreeSize = file.writeQueue.freeSize();
+ if (originalFreeSize == 0) {
+ return 0; // EAGAIN
+ }
+
+ final int writeCount = Math.min(len, originalFreeSize);
+ file.writeQueue.writeBytes(buffer, pos, writeCount);
+ maybeTransferData(file);
+
+ if (file.writeQueue.freeSize() < originalFreeSize) {
+ final int additionalQueuedCount = originalFreeSize - file.writeQueue.freeSize();
+ Log.i(mLogTag, logStr("Delaying transfer of " + additionalQueuedCount
+ + " bytes, queued=" + file.writeQueue.size() + ", type=" + file.type
+ + ", src_red=" + file.outboundLimiter + ", dst_red=" + file.sink.inboundLimiter));
+ }
+
+ return writeCount;
+ }
+
+ private void maybeTransferData(File file) {
+ boolean hasChanges = copyFileBuffers(file, file.sink);
+ hasChanges = copyFileBuffers(file.source, file) || hasChanges;
+
+ if (hasChanges) {
+ // TODO(b/245971639): Avoid notifying if no-one is polling.
+ notifyAll();
+ }
+ }
+
+ private boolean copyFileBuffers(File src, File dst) {
+ if (src.writeQueue == null || dst.readQueue == null) {
+ return false;
+ }
+
+ final int originalCopyCount = Math.min(mTmpBuffer.length,
+ Math.min(src.writeQueue.size(), dst.readQueue.freeSize()));
+
+ final int allowedCopyCount = RateLimiter.limit(
+ src.outboundLimiter, dst.inboundLimiter, originalCopyCount);
+
+ if (allowedCopyCount < originalCopyCount) {
+ if (ENABLE_FINE_DEBUG) {
+ Log.i(mLogTag, logStr("Delaying transfer of "
+ + (originalCopyCount - allowedCopyCount) + " bytes, original="
+ + originalCopyCount + ", allowed=" + allowedCopyCount
+ + ", type=" + src.type));
+ }
+ if (originalCopyCount > 0) {
+ mHasRateLimitedData = true;
+ }
+ if (allowedCopyCount == 0) {
+ return false;
+ }
+ }
+
+ boolean hasChanges = false;
+ if (allowedCopyCount > 0) {
+ if (dst.readQueue.size() == 0 || src.writeQueue.freeSize() == 0) {
+ hasChanges = true; // Read queue had no data, or write queue was full.
+ }
+ src.writeQueue.readBytes(mTmpBuffer, 0, allowedCopyCount);
+ dst.readQueue.writeBytes(mTmpBuffer, 0, allowedCopyCount);
+ }
+
+ if (!dst.isEndOfStream && src.openCount == 0
+ && src.writeQueue.size() == 0 && dst.readQueue.size() == 0) {
+ dst.isEndOfStream = true;
+ hasChanges = true;
+ }
+
+ return hasChanges;
+ }
+
+ public void clearInboundRateLimit(FileDescriptor fd) {
+ setInboundRateLimit(fd, Integer.MAX_VALUE);
+ }
+
+ public void clearOutboundRateLimit(FileDescriptor fd) {
+ setOutboundRateLimit(fd, Integer.MAX_VALUE);
+ }
+
+ public synchronized void setInboundRateLimit(FileDescriptor fd, int bytesPerSecond) {
+ File file = getFileOrNull(fd);
+ if (file != null) {
+ file.inboundLimiter.setBytesPerSecond(bytesPerSecond);
+ maybeTransferData(file);
+ }
+ }
+
+ public synchronized void setOutboundRateLimit(FileDescriptor fd, int bytesPerSecond) {
+ File file = getFileOrNull(fd);
+ if (file != null) {
+ file.outboundLimiter.setBytesPerSecond(bytesPerSecond);
+ maybeTransferData(file);
+ }
+ }
+
+ public synchronized ParcelFileDescriptor[] socketpair() throws IOException {
+ int fdNumber1 = getNextFd("socketpair");
+ int fdNumber2 = getNextFd("socketpair");
+
+ File file1 = new File(FileType.PAIR, mReadQueueSize, mWriteQueueSize);
+ File file2 = new File(FileType.PAIR, mReadQueueSize, mWriteQueueSize);
+
+ return registerFilePair(fdNumber1, file1, fdNumber2, file2);
+ }
+
+ @Override
+ public synchronized ParcelFileDescriptor[] pipe() throws IOException {
+ int fdNumber1 = getNextFd("pipe");
+ int fdNumber2 = getNextFd("pipe");
+
+ File file1 = new File(FileType.PIPE, mReadQueueSize, 0);
+ File file2 = new File(FileType.PIPE, 0, mWriteQueueSize);
+
+ return registerFilePair(fdNumber1, file1, fdNumber2, file2);
+ }
+
+ private ParcelFileDescriptor[] registerFilePair(
+ int fdNumber1, File file1, int fdNumber2, File file2) {
+ file1.sink = file2;
+ file1.source = file2;
+ file2.sink = file1;
+ file2.source = file1;
+
+ mFiles.put(fdNumber1, file1);
+ mFiles.put(fdNumber2, file2);
+ return new ParcelFileDescriptor[] {
+ newParcelFileDescriptor(fdNumber1), newParcelFileDescriptor(fdNumber2)};
+ }
+
+ @Override
+ public short getPollInMask() {
+ return POLLIN;
+ }
+
+ @Override
+ public short getPollOutMask() {
+ return POLLOUT;
+ }
+
+ @Override
+ public synchronized int poll(StructPollfd[] fds, int timeoutMs) throws IOException {
+ if (timeoutMs < 0) {
+ timeoutMs = (int) TimeUnit.HOURS.toMillis(1); // Make "infinite" equal to 1 hour.
+ }
+
+ if (fds == null || fds.length > 1000) {
+ throw newIOException("poll", "Invalid fds param");
+ }
+ for (StructPollfd pollFd : fds) {
+ getFile("poll", pollFd.fd);
+ }
+
+ int waitCallCount = 0;
+ final long deadline = monotonicTimeMillis() + timeoutMs;
+ while (true) {
+ if (mHasRateLimitedData) {
+ mHasRateLimitedData = false;
+ for (File file : mFiles.values()) {
+ if (file.inboundLimiter.getLastRequestReduction() != 0) {
+ copyFileBuffers(file.source, file);
+ }
+ if (file.outboundLimiter.getLastRequestReduction() != 0) {
+ copyFileBuffers(file, file.sink);
+ }
+ }
+ }
+
+ final int readyCount = calculateReadyCount(fds);
+ if (readyCount > 0) {
+ if (ENABLE_FINE_DEBUG) {
+ Log.v(mLogTag, logStr("Poll returns " + readyCount
+ + " after " + waitCallCount + " wait calls"));
+ }
+ return readyCount;
+ }
+
+ long remainingTimeoutMs = deadline - monotonicTimeMillis();
+ if (remainingTimeoutMs <= 0) {
+ if (ENABLE_FINE_DEBUG) {
+ Log.v(mLogTag, logStr("Poll timeout " + timeoutMs
+ + "ms after " + waitCallCount + " wait calls"));
+ }
+ return 0;
+ }
+
+ if (mHasRateLimitedData) {
+ remainingTimeoutMs = Math.min(RateLimiter.BUCKET_DURATION_MS, remainingTimeoutMs);
+ }
+
+ try {
+ wait(remainingTimeoutMs);
+ } catch (InterruptedException e) {
+ // Ignore and retry
+ }
+ waitCallCount++;
+ }
+ }
+
+ private int calculateReadyCount(StructPollfd[] fds) {
+ int fdCount = 0;
+ for (StructPollfd pollFd : fds) {
+ pollFd.revents = 0;
+
+ File file = getFileOrNull(pollFd.fd);
+ if (file == null) {
+ Log.w(mLogTag, logStr("Ignoring FD concurrently closed by a buggy app: "
+ + getFileDebugName(pollFd.fd)));
+ continue;
+ }
+
+ if (ENABLE_FINE_DEBUG) {
+ Log.v(mLogTag, logStr("calculateReadyCount fd=" + getFileDebugName(pollFd.fd)
+ + ", events=" + pollFd.events + ", eof=" + file.isEndOfStream
+ + ", r=" + (file.readQueue != null ? file.readQueue.size() : -1)
+ + ", w=" + (file.writeQueue != null ? file.writeQueue.freeSize() : -1)));
+ }
+
+ if ((pollFd.events & POLLIN) != 0) {
+ if (file.readQueue != null && file.readQueue.size() != 0) {
+ pollFd.revents |= POLLIN;
+ }
+ if (file.isEndOfStream) {
+ pollFd.revents |= POLLHUP;
+ }
+ }
+
+ if ((pollFd.events & POLLOUT) != 0) {
+ if (file.type == FileType.PIPE && file.sink.openCount == 0) {
+ pollFd.revents |= POLLERR;
+ }
+ if (file.writeQueue != null && file.writeQueue.freeSize() != 0) {
+ pollFd.revents |= POLLOUT;
+ }
+ }
+
+ if (pollFd.revents != 0) {
+ fdCount++;
+ }
+ }
+ return fdCount;
+ }
+
+ private int getNextFd(String func) throws IOException {
+ if (mFileNumberGen > 100000) {
+ throw newIOException(func, "Too many files open");
+ }
+
+ return mFileNumberGen++;
+ }
+
+ private static IOException newIOException(String func, String message) {
+ return new IOException(message + ", func=" + func);
+ }
+
+ public static void checkBoundaries(String func, byte[] buffer, int pos, int len)
+ throws IOException {
+ if (((buffer.length | pos | len) < 0 || pos > buffer.length - len)) {
+ throw newIOException(func, "Invalid array bounds");
+ }
+ }
+
+ private ParcelFileDescriptor newParcelFileDescriptor(int fdNumber) {
+ try {
+ return new ParcelFileDescriptor(newFileDescriptor(fdNumber));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private FileDescriptor newFileDescriptor(int fdNumber) {
+ try {
+ return FD_CONSTRUCTOR.newInstance(Integer.valueOf(fdNumber));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public int getFileDescriptorNumber(FileDescriptor fd) {
+ try {
+ return (Integer) FD_FIELD_DESCRIPTOR.get(fd);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void setFileDescriptorNumber(FileDescriptor fd, int fdNumber) {
+ try {
+ FD_FIELD_DESCRIPTOR.set(fd, Integer.valueOf(fdNumber));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String logStr(String message) {
+ return "[FakeOs " + (monotonicTimeMillis() - mStartTime) + "] " + message;
+ }
+
+ private class File {
+ final FileType type;
+ final CircularByteBuffer readQueue;
+ final CircularByteBuffer writeQueue;
+ final RateLimiter inboundLimiter = new RateLimiter(FakeOsAccess.this, Integer.MAX_VALUE);
+ final RateLimiter outboundLimiter = new RateLimiter(FakeOsAccess.this, Integer.MAX_VALUE);
+ String name;
+ int openCount = 1;
+ boolean isBlocking = true;
+ File sink;
+ File source;
+ boolean isEndOfStream;
+
+ File(FileType type, int readQueueSize, int writeQueueSize) {
+ this.type = type;
+ readQueue = (readQueueSize > 0 ? new CircularByteBuffer(readQueueSize) : null);
+ writeQueue = (writeQueueSize > 0 ? new CircularByteBuffer(writeQueueSize) : null);
+ }
+
+ void decreaseRefCount() {
+ if (openCount <= 0) {
+ throw new IllegalStateException();
+ }
+ openCount--;
+ }
+
+ void checkNonBlocking(String func) throws IOException {
+ if (isBlocking) {
+ throw newIOException(func, "File in blocking mode");
+ }
+ }
+ }
+
+ static {
+ try {
+ FD_CONSTRUCTOR = FileDescriptor.class.getDeclaredConstructor(int.class);
+ FD_CONSTRUCTOR.setAccessible(true);
+
+ Field descriptorIntField;
+ try {
+ descriptorIntField = FileDescriptor.class.getDeclaredField("descriptor");
+ } catch (NoSuchFieldException e) {
+ descriptorIntField = FileDescriptor.class.getDeclaredField("fd");
+ }
+ FD_FIELD_DESCRIPTOR = descriptorIntField;
+ FD_FIELD_DESCRIPTOR.setAccessible(true);
+
+ PFD_FIELD_DESCRIPTOR = ParcelFileDescriptor.class.getDeclaredField("mFd");
+ PFD_FIELD_DESCRIPTOR.setAccessible(true);
+
+ PFD_FIELD_GUARD = ParcelFileDescriptor.class.getDeclaredField("mGuard");
+ PFD_FIELD_GUARD.setAccessible(true);
+
+ CLOSE_GUARD_METHOD_CLOSE = Class.forName("dalvik.system.CloseGuard")
+ .getDeclaredMethod("close");
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/async/RateLimiter.java b/staticlibs/testutils/devicetests/com/android/testutils/async/RateLimiter.java
new file mode 100644
index 0000000..d5cca0a
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/async/RateLimiter.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils.async;
+
+import com.android.net.module.util.async.OsAccess;
+
+import java.util.Arrays;
+
+/**
+ * Limits the number of bytes processed to the given maximum of bytes per second.
+ *
+ * The limiter tracks the total for the past second, along with sums for each 10ms
+ * in the past second, allowing the total to be adjusted as the time passes.
+ */
+public final class RateLimiter {
+ private static final int PERIOD_DURATION_MS = 1000;
+ private static final int BUCKET_COUNT = 100;
+
+ public static final int BUCKET_DURATION_MS = PERIOD_DURATION_MS / BUCKET_COUNT;
+
+ private final OsAccess mOsAccess;
+ private final int[] mStatBuckets = new int[BUCKET_COUNT];
+ private int mMaxPerPeriodBytes;
+ private int mMaxPerBucketBytes;
+ private int mRecordedPeriodBytes;
+ private long mLastLimitTimestamp;
+ private int mLastRequestReduction;
+
+ public RateLimiter(OsAccess osAccess, int bytesPerSecond) {
+ mOsAccess = osAccess;
+ setBytesPerSecond(bytesPerSecond);
+ clear();
+ }
+
+ public int getBytesPerSecond() {
+ return mMaxPerPeriodBytes;
+ }
+
+ public void setBytesPerSecond(int bytesPerSecond) {
+ mMaxPerPeriodBytes = bytesPerSecond;
+ mMaxPerBucketBytes = Math.max(1, (mMaxPerPeriodBytes / BUCKET_COUNT) * 2);
+ }
+
+ public void clear() {
+ mLastLimitTimestamp = mOsAccess.monotonicTimeMillis();
+ mRecordedPeriodBytes = 0;
+ Arrays.fill(mStatBuckets, 0);
+ }
+
+ public static int limit(RateLimiter limiter1, RateLimiter limiter2, int requestedBytes) {
+ final long now = limiter1.mOsAccess.monotonicTimeMillis();
+ final int allowedCount = Math.min(limiter1.calculateLimit(now, requestedBytes),
+ limiter2.calculateLimit(now, requestedBytes));
+ limiter1.recordBytes(now, requestedBytes, allowedCount);
+ limiter2.recordBytes(now, requestedBytes, allowedCount);
+ return allowedCount;
+ }
+
+ public int limit(int requestedBytes) {
+ final long now = mOsAccess.monotonicTimeMillis();
+ final int allowedCount = calculateLimit(now, requestedBytes);
+ recordBytes(now, requestedBytes, allowedCount);
+ return allowedCount;
+ }
+
+ public int getLastRequestReduction() {
+ return mLastRequestReduction;
+ }
+
+ public boolean acceptAllOrNone(int requestedBytes) {
+ final long now = mOsAccess.monotonicTimeMillis();
+ final int allowedCount = calculateLimit(now, requestedBytes);
+ if (allowedCount < requestedBytes) {
+ return false;
+ }
+ recordBytes(now, requestedBytes, allowedCount);
+ return true;
+ }
+
+ private int calculateLimit(long now, int requestedBytes) {
+ // First remove all stale bucket data and adjust the total.
+ final long currentBucketAbsIdx = now / BUCKET_DURATION_MS;
+ final long staleCutoffIdx = currentBucketAbsIdx - BUCKET_COUNT;
+ for (long i = mLastLimitTimestamp / BUCKET_DURATION_MS; i < staleCutoffIdx; i++) {
+ final int idx = (int) (i % BUCKET_COUNT);
+ mRecordedPeriodBytes -= mStatBuckets[idx];
+ mStatBuckets[idx] = 0;
+ }
+
+ final int bucketIdx = (int) (currentBucketAbsIdx % BUCKET_COUNT);
+ final int maxAllowed = Math.min(mMaxPerPeriodBytes - mRecordedPeriodBytes,
+ Math.min(mMaxPerBucketBytes - mStatBuckets[bucketIdx], requestedBytes));
+ return Math.max(0, maxAllowed);
+ }
+
+ private void recordBytes(long now, int requestedBytes, int actualBytes) {
+ mStatBuckets[(int) ((now / BUCKET_DURATION_MS) % BUCKET_COUNT)] += actualBytes;
+ mRecordedPeriodBytes += actualBytes;
+ mLastRequestReduction = requestedBytes - actualBytes;
+ mLastLimitTimestamp = now;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{max=");
+ sb.append(mMaxPerPeriodBytes);
+ sb.append(",max_bucket=");
+ sb.append(mMaxPerBucketBytes);
+ sb.append(",total=");
+ sb.append(mRecordedPeriodBytes);
+ sb.append(",last_red=");
+ sb.append(mLastRequestReduction);
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/async/ReadableDataAnswer.java b/staticlibs/testutils/devicetests/com/android/testutils/async/ReadableDataAnswer.java
new file mode 100644
index 0000000..4bf5527
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/async/ReadableDataAnswer.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils.async;
+
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+
+public class ReadableDataAnswer implements Answer {
+ private final ArrayList<byte[]> mBuffers = new ArrayList<>();
+ private int mBufferPos;
+
+ public ReadableDataAnswer(byte[] ... buffers) {
+ for (byte[] buffer : buffers) {
+ addBuffer(buffer);
+ }
+ }
+
+ public void addBuffer(byte[] buffer) {
+ if (buffer.length != 0) {
+ mBuffers.add(buffer);
+ }
+ }
+
+ public int getRemainingSize() {
+ int totalSize = 0;
+ for (byte[] buffer : mBuffers) {
+ totalSize += buffer.length;
+ }
+ return totalSize - mBufferPos;
+ }
+
+ private void cleanupBuffers() {
+ if (!mBuffers.isEmpty() && mBufferPos == mBuffers.get(0).length) {
+ mBuffers.remove(0);
+ mBufferPos = 0;
+ }
+ }
+
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ cleanupBuffers();
+
+ if (mBuffers.isEmpty()) {
+ return Integer.valueOf(0);
+ }
+
+ byte[] src = mBuffers.get(0);
+
+ byte[] dst = invocation.<byte[]>getArgument(0);
+ int dstPos = invocation.<Integer>getArgument(1);
+ int dstLen = invocation.<Integer>getArgument(2);
+
+ int copyLen = Math.min(dstLen, src.length - mBufferPos);
+ System.arraycopy(src, mBufferPos, dst, dstPos, copyLen);
+ mBufferPos += copyLen;
+
+ cleanupBuffers();
+ return Integer.valueOf(copyLen);
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt
new file mode 100644
index 0000000..5af890f
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/filters/CtsNetTestCasesMaxTargetSdk33.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils.filters
+
+/**
+ * Only run this test in the CtsNetTestCasesMaxTargetSdk33 suite.
+ */
+annotation class CtsNetTestCasesMaxTargetSdk33(val reason: String)