diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-05-05 23:01:01 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-05-05 23:01:01 +0000 |
commit | 46114d87a2069b4b3559b4884906ee4c76245dc7 (patch) | |
tree | ecc2ab48b8d86907f891142420b62934f6bbb9a1 | |
parent | 02f87bd1e30a05a60f5c746cfe0af205bc5c800e (diff) | |
parent | 9989ddf8959a187657b051901109524eaae83619 (diff) | |
download | net-46114d87a2069b4b3559b4884906ee4c76245dc7.tar.gz |
Snap for 10078357 from 9989ddf8959a187657b051901109524eaae83619 to mainline-adservices-releaseaml_ads_331920180
Change-Id: I93dd26136b760b94798b01c45f51f82cad59c25b
31 files changed, 2016 insertions, 158 deletions
diff --git a/common/Android.bp b/common/Android.bp index 1fe6c2cc..b1c653d8 100644 --- a/common/Android.bp +++ b/common/Android.bp @@ -328,6 +328,63 @@ java_library { 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/HexDump.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/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/common/device/com/android/net/module/util/async/BufferedFile.java b/common/device/com/android/net/module/util/async/BufferedFile.java new file mode 100644 index 00000000..bb5736b0 --- /dev/null +++ b/common/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/common/device/com/android/net/module/util/netlink/InetDiagMessage.java b/common/device/com/android/net/module/util/netlink/InetDiagMessage.java index 5a180e7b..0a2f50d8 100644 --- a/common/device/com/android/net/module/util/netlink/InetDiagMessage.java +++ b/common/device/com/android/net/module/util/netlink/InetDiagMessage.java @@ -58,8 +58,8 @@ public class InetDiagMessage extends NetlinkMessage { 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 +68,16 @@ public class InetDiagMessage extends NetlinkMessage { } /** - * 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 +90,47 @@ public class InetDiagMessage extends NetlinkMessage { */ 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; diff --git a/common/device/com/android/net/module/util/netlink/NetlinkConstants.java b/common/device/com/android/net/module/util/netlink/NetlinkConstants.java index c44a5b48..44c51d8a 100644 --- a/common/device/com/android/net/module/util/netlink/NetlinkConstants.java +++ b/common/device/com/android/net/module/util/netlink/NetlinkConstants.java @@ -141,6 +141,7 @@ public class NetlinkConstants { /* 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/common/device/com/android/net/module/util/netlink/NetlinkUtils.java b/common/device/com/android/net/module/util/netlink/NetlinkUtils.java index ae16cf8d..d4bf14a4 100644 --- a/common/device/com/android/net/module/util/netlink/NetlinkUtils.java +++ b/common/device/com/android/net/module/util/netlink/NetlinkUtils.java @@ -36,6 +36,7 @@ import android.system.StructTimeval; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.io.FileDescriptor; import java.io.IOException; @@ -81,6 +82,54 @@ public class NetlinkUtils { } /** + * 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 @@ public class NetlinkUtils { 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/common/device/com/android/net/module/util/netlink/StructInetDiagMsg.java b/common/device/com/android/net/module/util/netlink/StructInetDiagMsg.java index e7fc02f6..cbd895d6 100644 --- a/common/device/com/android/net/module/util/netlink/StructInetDiagMsg.java +++ b/common/device/com/android/net/module/util/netlink/StructInetDiagMsg.java @@ -43,14 +43,22 @@ import java.nio.ByteBuffer; */ public class StructInetDiagMsg { public static final int STRUCT_SIZE = 4 + StructInetDiagSockId.STRUCT_SIZE + 20; - // Offset to the id field from the beginning of inet_diag_msg struct - private static final int IDIAG_SOCK_ID_OFFSET = 4; - // Offset to the idiag_uid field from the beginning of inet_diag_msg struct - private static final int IDIAG_UID_OFFSET = - IDIAG_SOCK_ID_OFFSET + 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. @@ -60,26 +68,36 @@ public class StructInetDiagMsg { if (byteBuffer.remaining() < STRUCT_SIZE) { return null; } - final int baseOffset = byteBuffer.position(); StructInetDiagMsg struct = new StructInetDiagMsg(); - final byte family = byteBuffer.get(); - byteBuffer.position(baseOffset + 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(baseOffset + IDIAG_UID_OFFSET); - - // Move position to the end of the inet_diag_msg - byteBuffer.position(baseOffset + STRUCT_SIZE); + 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/common/device/com/android/net/module/util/netlink/StructInetDiagReqV2.java b/common/device/com/android/net/module/util/netlink/StructInetDiagReqV2.java index 6eef8653..3b47008a 100644 --- a/common/device/com/android/net/module/util/netlink/StructInetDiagReqV2.java +++ b/common/device/com/android/net/module/util/netlink/StructInetDiagReqV2.java @@ -18,7 +18,6 @@ package com.android.net.module.util.netlink; import androidx.annotation.Nullable; -import java.net.InetSocketAddress; import java.nio.ByteBuffer; /** @@ -48,23 +47,11 @@ public class StructInetDiagReqV2 { 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/common/device/com/android/net/module/util/netlink/StructInetDiagSockId.java b/common/device/com/android/net/module/util/netlink/StructInetDiagSockId.java index 648a0201..dd85934c 100644 --- a/common/device/com/android/net/module/util/netlink/StructInetDiagSockId.java +++ b/common/device/com/android/net/module/util/netlink/StructInetDiagSockId.java @@ -29,6 +29,7 @@ import android.util.Log; 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 @@ public class StructInetDiagSockId { * 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 @@ public class StructInetDiagSockId { 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/common/device/com/android/net/module/util/wear/NetPacketHelpers.java b/common/device/com/android/net/module/util/wear/NetPacketHelpers.java new file mode 100644 index 00000000..341c44be --- /dev/null +++ b/common/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/common/device/com/android/net/module/util/wear/PacketFile.java b/common/device/com/android/net/module/util/wear/PacketFile.java new file mode 100644 index 00000000..7f5ed785 --- /dev/null +++ b/common/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/common/device/com/android/net/module/util/wear/StreamingPacketFile.java b/common/device/com/android/net/module/util/wear/StreamingPacketFile.java new file mode 100644 index 00000000..52dbee47 --- /dev/null +++ b/common/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/common/framework/com/android/net/module/util/InetAddressUtils.java b/common/framework/com/android/net/module/util/InetAddressUtils.java index 31d07297..40fc59fb 100644 --- a/common/framework/com/android/net/module/util/InetAddressUtils.java +++ b/common/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 @@ import java.net.UnknownHostException; */ public class InetAddressUtils { + private static final String TAG = InetAddressUtils.class.getSimpleName(); private static final int INET6_ADDR_LENGTH = 16; /** @@ -71,5 +75,23 @@ public class InetAddressUtils { } } + /** + * 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/common/framework/com/android/net/module/util/IpUtils.java b/common/framework/com/android/net/module/util/IpUtils.java index 569733ed..18d96f30 100644 --- a/common/framework/com/android/net/module/util/IpUtils.java +++ b/common/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 @@ public class IpUtils { * 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 @@ public class IpUtils { 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/common/framework/com/android/net/module/util/LinkPropertiesUtils.java b/common/framework/com/android/net/module/util/LinkPropertiesUtils.java index 1565f2bc..e271f649 100644 --- a/common/framework/com/android/net/module/util/LinkPropertiesUtils.java +++ b/common/framework/com/android/net/module/util/LinkPropertiesUtils.java @@ -146,6 +146,24 @@ public final class LinkPropertiesUtils { 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/common/framework/com/android/net/module/util/PermissionUtils.java b/common/framework/com/android/net/module/util/PermissionUtils.java index be5b0cde..8315b8f6 100644 --- a/common/framework/com/android/net/module/util/PermissionUtils.java +++ b/common/framework/com/android/net/module/util/PermissionUtils.java @@ -54,6 +54,20 @@ public final class PermissionUtils { } /** + * 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/common/native/bpf_headers/include/bpf/bpf_helpers.h b/common/native/bpf_headers/include/bpf/bpf_helpers.h index 0300b5ec..4939483a 100644 --- a/common/native/bpf_headers/include/bpf/bpf_helpers.h +++ b/common/native/bpf_headers/include/bpf/bpf_helpers.h @@ -305,6 +305,7 @@ unsigned long long load_word(void* skb, unsigned long long off) asm("llvm.bpf.lo 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; diff --git a/common/tests/unit/Android.bp b/common/tests/unit/Android.bp index 6e223bd4..40371e63 100644 --- a/common/tests/unit/Android.bp +++ b/common/tests/unit/Android.bp @@ -21,6 +21,7 @@ android_library { "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/common/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java b/common/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java index 2736c53c..bb2b9332 100644 --- a/common/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java +++ b/common/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java @@ -18,6 +18,10 @@ package com.android.net.module.util; 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 @@ public class InetAddressUtilsTest { 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/common/tests/unit/src/com/android/net/module/util/IpUtilsTest.java b/common/tests/unit/src/com/android/net/module/util/IpUtilsTest.java index 20555b35..d57023ca 100644 --- a/common/tests/unit/src/com/android/net/module/util/IpUtilsTest.java +++ b/common/tests/unit/src/com/android/net/module/util/IpUtilsTest.java @@ -74,6 +74,20 @@ public class IpUtilsTest { // 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/common/tests/unit/src/com/android/net/module/util/LinkPropertiesUtilsTest.java b/common/tests/unit/src/com/android/net/module/util/LinkPropertiesUtilsTest.java index 09f04902..80ab6185 100644 --- a/common/tests/unit/src/com/android/net/module/util/LinkPropertiesUtilsTest.java +++ b/common/tests/unit/src/com/android/net/module/util/LinkPropertiesUtilsTest.java @@ -94,6 +94,9 @@ public final class LinkPropertiesUtilsTest { 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 @@ public final class LinkPropertiesUtilsTest { 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 @@ public final class LinkPropertiesUtilsTest { 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/common/tests/unit/src/com/android/net/module/util/async/BufferedFileTest.java b/common/tests/unit/src/com/android/net/module/util/async/BufferedFileTest.java new file mode 100644 index 00000000..11a74f2c --- /dev/null +++ b/common/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/common/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java b/common/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java index d81422fb..30796d24 100644 --- a/common/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java +++ b/common/tests/unit/src/com/android/net/module/util/netlink/InetDiagSocketTest.java @@ -22,6 +22,8 @@ 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; @@ -41,6 +43,7 @@ import libcore.util.HexEncoding; import org.junit.Test; import org.junit.runner.RunWith; +import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; @@ -49,6 +52,21 @@ import java.nio.ByteOrder; @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 +223,14 @@ public class InetDiagSocketTest { 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,6 +239,85 @@ public class InetDiagSocketTest { assertArrayEquals(INET_DIAG_REQ_V2_TCP_INET6_NO_ID_SPECIFIED_BYTES, msgExt); } + // 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); @@ -248,9 +345,9 @@ public class InetDiagSocketTest { "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 @@ -258,11 +355,11 @@ public class InetDiagSocketTest { "20010db8000000000000000000000002" + // idiag_dst = 2001:db8::2 "07000000" + // idiag_if = 7 "5800000000000000" + // idiag_cookie = 88 - "00000000" + // idiag_expires - "00000000" + // idiag_rqueue - "00000000" + // idiag_wqueue + "04000000" + // idiag_expires = 4 + "05000000" + // idiag_rqueue = 5 + "06000000" + // idiag_wqueue = 6 "a3270000" + // idiag_uid = 10147 - "A57E1900"; // idiag_inode + "a57e19f0"; // idiag_inode = 4028202661 private void assertInetDiagMsg1(final NetlinkMessage msg) { assertNotNull(msg); @@ -276,12 +373,20 @@ public class InetDiagSocketTest { 0 /* seq */, 8949 /* pid */); - assertEquals(10147, inetDiagMsg.inetDiagMsg.idiag_uid); + 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 @@ -294,9 +399,9 @@ public class InetDiagSocketTest { "f5220000" + // pid // struct inet_diag_msg "0a" + // family = AF_INET6 - "01" + // idiag_state - "00" + // idiag_timer - "00" + // idiag_retrans + "02" + // idiag_state = 2 + "10" + // idiag_timer = 16 + "20" + // idiag_retrans = 32 // inet_diag_sockid "a845" + // idiag_sport = 43077 "01bb" + // idiag_dport = 443 @@ -304,11 +409,11 @@ public class InetDiagSocketTest { "20010db8000000000000000000000004" + // idiag_dst = 2001:db8::4 "08000000" + // idiag_if = 8 "6300000000000000" + // idiag_cookie = 99 - "00000000" + // idiag_expires - "00000000" + // idiag_rqueue - "00000000" + // idiag_wqueue + "30000000" + // idiag_expires = 48 + "40000000" + // idiag_rqueue = 64 + "50000000" + // idiag_wqueue = 80 "39300000" + // idiag_uid = 12345 - "A57E1900"; // idiag_inode + "851a0000"; // idiag_inode = 6789 private void assertInetDiagMsg2(final NetlinkMessage msg) { assertNotNull(msg); @@ -322,12 +427,20 @@ public class InetDiagSocketTest { 0 /* seq */, 8949 /* pid */); - assertEquals(12345, inetDiagMsg.inetDiagMsg.idiag_uid); + 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 = @@ -336,7 +449,7 @@ public class InetDiagSocketTest { @Test public void testParseInetDiagResponse() throws Exception { final ByteBuffer byteBuffer = ByteBuffer.wrap(INET_DIAG_MSG_BYTES); - byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.order(ByteOrder.nativeOrder()); assertInetDiagMsg1(NetlinkMessage.parse(byteBuffer, NETLINK_INET_DIAG)); } @@ -347,8 +460,87 @@ public class InetDiagSocketTest { @Test public void testParseInetDiagResponseMultiple() { final ByteBuffer byteBuffer = ByteBuffer.wrap(INET_DIAG_MSG_BYTES_MULTIPLE); - byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + 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 */); + } } diff --git a/common/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java b/common/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java index 7a1639ae..6fbfbf91 100644 --- a/common/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java +++ b/common/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java @@ -30,6 +30,7 @@ import static org.junit.Assert.assertEquals; 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 org.junit.runner.RunWith; 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 @@ public class NetlinkUtilsTest { // 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/common/tests/unit/src/com/android/net/module/util/wear/NetPacketHelpersTest.java b/common/tests/unit/src/com/android/net/module/util/wear/NetPacketHelpersTest.java new file mode 100644 index 00000000..23e7b15e --- /dev/null +++ b/common/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/common/tests/unit/src/com/android/net/module/util/wear/StreamingPacketFileTest.java b/common/tests/unit/src/com/android/net/module/util/wear/StreamingPacketFileTest.java new file mode 100644 index 00000000..1fcca704 --- /dev/null +++ b/common/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/common/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt b/common/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt index ec7cdbdf..4ed881af 100644 --- a/common/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt +++ b/common/tests/unit/src/com/android/testutils/TestableNetworkCallbackTest.kt @@ -366,18 +366,6 @@ class TestableNetworkCallbackTest { } @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() @@ -455,7 +443,6 @@ private val interpretTable = listOf<InterpretMatcher<TestableNetworkCallback>>( } }, 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/common/testutils/Android.bp b/common/testutils/Android.bp index bcf89b34..33821564 100644 --- a/common/testutils/Android.bp +++ b/common/testutils/Android.bp @@ -38,6 +38,7 @@ java_library { "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/common/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt b/common/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt index e7d86e00..0e73112d 100644 --- a/common/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt +++ b/common/testutils/devicetests/com/android/testutils/TestableNetworkCallback.kt @@ -223,18 +223,6 @@ open class TestableNetworkCallback private constructor( fun poll(timeoutMs: Long = defaultTimeoutMs, predicate: (CallbackEntry) -> Boolean = { true }) = history.poll(timeoutMs, predicate) - /** - * 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) - /***** * expect family of methods. * These methods fetch the next callback and assert it matches the conditions : type, @@ -350,15 +338,16 @@ open class TestableNetworkCallback private constructor( 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, @@ -367,6 +356,12 @@ open class TestableNetworkCallback private constructor( 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, @@ -379,9 +374,12 @@ open class TestableNetworkCallback private constructor( 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, @@ -408,13 +406,6 @@ open class TestableNetworkCallback private constructor( assertNotNull(it, "Callback ${type.java} not received within ${timeoutMs}ms") } as T - // TODO (b/157405399) remove this method when there are no longer any uses of it. - 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? - // Expects onAvailable and the callbacks that follow it. These are: // - onSuspended, iff the network was suspended when the callbacks fire. // - onCapabilitiesChanged. diff --git a/common/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java b/common/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java index 1b8e26b3..48b57d70 100644 --- a/common/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java +++ b/common/testutils/devicetests/com/android/testutils/async/FakeOsAccess.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.testutils; +package com.android.testutils.async; import android.os.ParcelFileDescriptor; import android.system.StructPollfd; diff --git a/common/testutils/devicetests/com/android/testutils/async/RateLimiter.java b/common/testutils/devicetests/com/android/testutils/async/RateLimiter.java index 137873db..d5cca0a4 100644 --- a/common/testutils/devicetests/com/android/testutils/async/RateLimiter.java +++ b/common/testutils/devicetests/com/android/testutils/async/RateLimiter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.testutils; +package com.android.testutils.async; import com.android.net.module.util.async.OsAccess; diff --git a/common/testutils/devicetests/com/android/testutils/async/ReadableDataAnswer.java b/common/testutils/devicetests/com/android/testutils/async/ReadableDataAnswer.java new file mode 100644 index 00000000..4bf55270 --- /dev/null +++ b/common/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); + } +} |