summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--common/framework/com/android/net/module/util/NetworkStackConstants.java3
-rw-r--r--common/native/bpf_headers/include/bpf/BpfUtils.h3
-rw-r--r--common/native/bpf_headers/include/bpf/bpf_helpers.h1
-rw-r--r--common/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt81
-rw-r--r--common/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt78
-rw-r--r--common/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java206
-rw-r--r--common/testutils/devicetests/com/android/testutils/PacketBridge.kt173
-rw-r--r--common/testutils/devicetests/com/android/testutils/PacketReflector.java82
-rw-r--r--common/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt106
-rw-r--r--common/testutils/devicetests/com/android/testutils/TestHttpServer.kt19
10 files changed, 716 insertions, 36 deletions
diff --git a/common/framework/com/android/net/module/util/NetworkStackConstants.java b/common/framework/com/android/net/module/util/NetworkStackConstants.java
index 1d88d6ef..aa2dd4c1 100644
--- a/common/framework/com/android/net/module/util/NetworkStackConstants.java
+++ b/common/framework/com/android/net/module/util/NetworkStackConstants.java
@@ -208,6 +208,9 @@ public final class NetworkStackConstants {
*/
public static final int INFINITE_LEASE = 0xffffffff;
public static final int DHCP4_CLIENT_PORT = 68;
+ // The maximum length of a DHCP packet that can be constructed.
+ public static final int DHCP_MAX_LENGTH = 1500;
+ public static final int DHCP_MAX_OPTION_LEN = 255;
/**
* DHCPv6 constants.
diff --git a/common/native/bpf_headers/include/bpf/BpfUtils.h b/common/native/bpf_headers/include/bpf/BpfUtils.h
index 206acba9..99c7a91d 100644
--- a/common/native/bpf_headers/include/bpf/BpfUtils.h
+++ b/common/native/bpf_headers/include/bpf/BpfUtils.h
@@ -63,8 +63,9 @@ static inline int synchronizeKernelRCU() {
// 4.9 kernels. The kernel code of socket release on pf_key socket will
// explicitly call synchronize_rcu() which is exactly what we need.
//
- // Linux 4.14/4.19/5.4/5.10/5.15 (and 5.18) still have this same behaviour.
+ // Linux 4.14/4.19/5.4/5.10/5.15/6.1 (and 6.3-rc5) still have this same behaviour.
// see net/key/af_key.c: pfkey_release() -> synchronize_rcu()
+ // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/key/af_key.c?h=v6.3-rc5#n185
const int pfSocket = socket(AF_KEY, SOCK_RAW | SOCK_CLOEXEC, PF_KEY_V2);
if (pfSocket < 0) {
diff --git a/common/native/bpf_headers/include/bpf/bpf_helpers.h b/common/native/bpf_headers/include/bpf/bpf_helpers.h
index 36865f33..0300b5ec 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_str)(void* dst, int size, const void* unsafe_ptr) = (void*) BPF_FUNC_probe_read_user_str;
static unsigned long long (*bpf_ktime_get_ns)(void) = (void*) BPF_FUNC_ktime_get_ns;
static unsigned long long (*bpf_ktime_get_boot_ns)(void) = (void*)BPF_FUNC_ktime_get_boot_ns;
static int (*bpf_trace_printk)(const char* fmt, int fmt_size, ...) = (void*) BPF_FUNC_trace_printk;
diff --git a/common/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt b/common/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt
new file mode 100644
index 00000000..d7961a08
--- /dev/null
+++ b/common/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import java.io.FileDescriptor
+import java.net.InetAddress
+
+/**
+ * A class that forwards packets from the external {@link TestNetworkInterface} to the internal
+ * {@link TestNetworkInterface} with NAT. See {@link NatPacketForwarderBase} for detail.
+ */
+class NatExternalPacketForwarder(
+ srcFd: FileDescriptor,
+ mtu: Int,
+ dstFd: FileDescriptor,
+ extAddr: InetAddress,
+ natMap: PacketBridge.NatMap
+) : NatPacketForwarderBase(srcFd, mtu, dstFd, extAddr, natMap) {
+
+ /**
+ * Rewrite addresses, ports and fix up checksums for packets received on the external
+ * interface.
+ *
+ * Incoming response from external interface which is being forwarded to the internal
+ * interface with translated address, e.g. 1.2.3.4:80 -> 8.8.8.8:1234
+ * will be translated into 8.8.8.8:80 -> 192.168.1.1:5678.
+ *
+ * For packets that are not an incoming response, do not forward them to the
+ * internal interface.
+ */
+ override fun preparePacketForForwarding(buf: ByteArray, len: Int, version: Int, proto: Int) {
+ val (addrPos, addrLen) = getAddressPositionAndLength(version)
+
+ // TODO: support one external address per ip version.
+ val extAddrBuf = mExtAddr.address
+ if (addrLen != extAddrBuf.size) throw IllegalStateException("Packet IP version mismatch")
+
+ // Get internal address by port.
+ val transportOffset =
+ if (version == 4) PacketReflector.IPV4_HEADER_LENGTH
+ else PacketReflector.IPV6_HEADER_LENGTH
+ val dstPort = getPortAt(buf, transportOffset + DESTINATION_PORT_OFFSET)
+ val intAddrInfo = synchronized(mNatMap) { mNatMap.fromExternalPort(dstPort) }
+ // No mapping, skip. This usually happens if the connection is initiated directly on
+ // the external interface, e.g. DNS64 resolution, network validation, etc.
+ if (intAddrInfo == null) return
+
+ val intAddrBuf = intAddrInfo.address.address
+ val intPort = intAddrInfo.port
+
+ // Copy the original destination to into the source address.
+ for (i in 0 until addrLen) {
+ buf[addrPos + i] = buf[addrPos + addrLen + i]
+ }
+
+ // Copy the internal address into the destination address.
+ for (i in 0 until addrLen) {
+ buf[addrPos + addrLen + i] = intAddrBuf[i]
+ }
+
+ // Copy the internal port into the destination port.
+ setPortAt(intPort, buf, transportOffset + DESTINATION_PORT_OFFSET)
+
+ // Fix IP and Transport layer checksum.
+ fixPacketChecksum(buf, len, version, proto.toByte())
+ }
+}
diff --git a/common/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt b/common/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt
new file mode 100644
index 00000000..fa39d19e
--- /dev/null
+++ b/common/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import java.io.FileDescriptor
+import java.net.InetAddress
+
+/**
+ * A class that forwards packets from the internal {@link TestNetworkInterface} to the external
+ * {@link TestNetworkInterface} with NAT. See {@link NatPacketForwarderBase} for detail.
+ */
+class NatInternalPacketForwarder(
+ srcFd: FileDescriptor,
+ mtu: Int,
+ dstFd: FileDescriptor,
+ extAddr: InetAddress,
+ natMap: PacketBridge.NatMap
+) : NatPacketForwarderBase(srcFd, mtu, dstFd, extAddr, natMap) {
+
+ /**
+ * Rewrite addresses, ports and fix up checksums for packets received on the internal
+ * interface.
+ *
+ * Outgoing packet from the internal interface which is being forwarded to the
+ * external interface with translated address, e.g. 192.168.1.1:5678 -> 8.8.8.8:80
+ * will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
+ *
+ * The external port, e.g. 1234 in the above example, is the port number assigned by
+ * the forwarder when creating the mapping to identify the source address and port when
+ * the response is coming from the external interface. See {@link PacketBridge.NatMap}
+ * for detail.
+ */
+ override fun preparePacketForForwarding(buf: ByteArray, len: Int, version: Int, proto: Int) {
+ val (addrPos, addrLen) = getAddressPositionAndLength(version)
+
+ // TODO: support one external address per ip version.
+ val extAddrBuf = mExtAddr.address
+ if (addrLen != extAddrBuf.size) throw IllegalStateException("Packet IP version mismatch")
+
+ val srcAddr = getInetAddressAt(buf, addrPos, addrLen)
+
+ // Copy the original destination to into the source address.
+ for (i in 0 until addrLen) {
+ buf[addrPos + i] = buf[addrPos + addrLen + i]
+ }
+
+ // Copy the external address into the destination address.
+ for (i in 0 until addrLen) {
+ buf[addrPos + addrLen + i] = extAddrBuf[i]
+ }
+
+ // Add an entry to NAT mapping table.
+ val transportOffset =
+ if (version == 4) PacketReflector.IPV4_HEADER_LENGTH
+ else PacketReflector.IPV6_HEADER_LENGTH
+ val srcPort = getPortAt(buf, transportOffset)
+ val extPort = synchronized(mNatMap) { mNatMap.toExternalPort(srcAddr, srcPort, proto) }
+ // Copy the external port to into the source port.
+ setPortAt(extPort, buf, transportOffset)
+
+ // Fix IP and Transport layer checksum.
+ fixPacketChecksum(buf, len, version, proto.toByte())
+ }
+}
diff --git a/common/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java b/common/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
new file mode 100644
index 00000000..85c64930
--- /dev/null
+++ b/common/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import static com.android.testutils.PacketReflector.IPPROTO_TCP;
+import static com.android.testutils.PacketReflector.IPPROTO_UDP;
+import static com.android.testutils.PacketReflector.IPV4_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.IPV6_PROTO_OFFSET;
+import static com.android.testutils.PacketReflector.TCP_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.UDP_HEADER_LENGTH;
+
+import android.annotation.NonNull;
+import android.net.TestNetworkInterface;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.Objects;
+
+/**
+ * A class that forwards packets from a {@link TestNetworkInterface} to another
+ * {@link TestNetworkInterface} with NAT.
+ *
+ * For testing purposes, a {@link TestNetworkInterface} provides a {@link FileDescriptor}
+ * which allows content injection on the test network. However, this could be hard to use
+ * because the callers need to compose IP packets in order to inject content to the
+ * test network.
+ *
+ * In order to remove the need of composing the IP packets, this class forwards IP packets to
+ * the {@link FileDescriptor} of another {@link TestNetworkInterface} instance. Thus,
+ * the TCP/IP headers could be parsed/composed automatically by the protocol stack of this
+ * additional {@link TestNetworkInterface}, while the payload is supplied by the
+ * servers run on the interface.
+ *
+ * To make it work, an internal interface and an external interface are defined, where
+ * the client might send packets from the internal interface which are originated from
+ * multiple addresses to a server that listens on the external address.
+ *
+ * When forwarding the outgoing packet on the internal interface, a simple NAT mechanism
+ * is implemented during forwarding, which will swap the source and destination,
+ * but replacing the source address with the external address,
+ * e.g. 192.168.1.1:1234 -> 8.8.8.8:80 will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
+ *
+ * For the above example, a client who sends http request will have a hallucination that
+ * it is talking to a remote server at 8.8.8.8. Also, the server listens on 1.2.3.4 will
+ * have a different hallucination that the request is sent from a remote client at 8.8.8.8,
+ * to a local address 1.2.3.4.
+ *
+ * And a NAT mapping is created at the time when the outgoing packet is forwarded.
+ * With a different internal source port, the instance learned that when a response with the
+ * destination port 1234, it should forward the packet to the internal address 192.168.1.1.
+ *
+ * For the incoming packet received from external interface, for example a http response sent
+ * from the http server, the same mechanism is applied but in a different direction,
+ * where the source and destination will be swapped, and the source address will be replaced
+ * with the internal address, which is obtained from the NAT mapping described above.
+ */
+public abstract class NatPacketForwarderBase extends Thread {
+ private static final String TAG = "NatPacketForwarder";
+ static final int DESTINATION_PORT_OFFSET = 2;
+
+ // The source fd to read packets from.
+ @NonNull
+ final FileDescriptor mSrcFd;
+ // The buffer to temporarily hold the entire packet after receiving.
+ @NonNull
+ final byte[] mBuf;
+ // The destination fd to write packets to.
+ @NonNull
+ final FileDescriptor mDstFd;
+ // The NAT mapping table shared between two NatPacketForwarder instances to map from
+ // the source port to the associated internal address. The map can be read/write from two
+ // different threads on any given time whenever receiving packets on the
+ // {@link TestNetworkInterface}. Thus, synchronize on the object when reading/writing is needed.
+ @GuardedBy("mNatMap")
+ @NonNull
+ final PacketBridge.NatMap mNatMap;
+ // The address of the external interface. See {@link NatPacketForwarder}.
+ @NonNull
+ final InetAddress mExtAddr;
+
+ /**
+ * Construct a {@link NatPacketForwarderBase}.
+ *
+ * This class reads packets from {@code srcFd} of a {@link TestNetworkInterface}, and
+ * forwards them to the {@code dstFd} of another {@link TestNetworkInterface} with
+ * NAT applied. See {@link NatPacketForwarderBase}.
+ *
+ * To apply NAT, the address of the external interface needs to be supplied through
+ * {@code extAddr} to identify the external interface. And a shared NAT mapping table,
+ * {@code natMap} is needed to be shared between these two instances.
+ *
+ * Note that this class is not useful if the instance is not managed by a
+ * {@link PacketBridge} to set up a two-way communication.
+ *
+ * @param srcFd {@link FileDescriptor} to read packets from.
+ * @param mtu MTU of the test network.
+ * @param dstFd {@link FileDescriptor} to write packets to.
+ * @param extAddr the external address, which is the address of the external interface.
+ * See {@link NatPacketForwarderBase}.
+ * @param natMap the NAT mapping table shared between two {@link NatPacketForwarderBase}
+ * instance.
+ */
+ public NatPacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
+ @NonNull FileDescriptor dstFd, @NonNull InetAddress extAddr,
+ @NonNull PacketBridge.NatMap natMap) {
+ super(TAG);
+ mSrcFd = Objects.requireNonNull(srcFd);
+ mBuf = new byte[mtu];
+ mDstFd = Objects.requireNonNull(dstFd);
+ mExtAddr = Objects.requireNonNull(extAddr);
+ mNatMap = Objects.requireNonNull(natMap);
+ }
+
+ /**
+ * A method to prepare forwarding packets between two instances of {@link TestNetworkInterface},
+ * which includes re-write addresses, ports and fix up checksums.
+ * Subclasses should override this method to implement a simple NAT.
+ */
+ abstract void preparePacketForForwarding(@NonNull byte[] buf, int len, int version, int proto);
+
+ private void forwardPacket(@NonNull byte[] buf, int len) {
+ try {
+ Os.write(mDstFd, buf, 0, len);
+ } catch (ErrnoException | IOException e) {
+ Log.e(TAG, "Error writing packet: " + e.getMessage());
+ }
+ }
+
+ // Reads one packet from mSrcFd, and writes the packet to the mDstFd for supported protocols.
+ private void processPacket() {
+ final int len = PacketReflectorUtil.readPacket(mSrcFd, mBuf);
+ if (len < 1) {
+ throw new IllegalStateException("Unexpected buffer length: " + len);
+ }
+
+ final int version = mBuf[0] >>> 4;
+ final int protoPos, ipHdrLen;
+ switch (version) {
+ case 4:
+ ipHdrLen = IPV4_HEADER_LENGTH;
+ protoPos = PacketReflector.IPV4_PROTO_OFFSET;
+ break;
+ case 6:
+ ipHdrLen = IPV6_HEADER_LENGTH;
+ protoPos = IPV6_PROTO_OFFSET;
+ break;
+ default:
+ throw new IllegalStateException("Unexpected version: " + version);
+ }
+ if (len < ipHdrLen) {
+ throw new IllegalStateException("Unexpected buffer length: " + len);
+ }
+
+ final byte proto = mBuf[protoPos];
+ final int transportHdrLen;
+ switch (proto) {
+ case IPPROTO_TCP:
+ transportHdrLen = TCP_HEADER_LENGTH;
+ break;
+ case IPPROTO_UDP:
+ transportHdrLen = UDP_HEADER_LENGTH;
+ break;
+ // TODO: Support ICMP.
+ default:
+ return; // Unknown protocol, ignored.
+ }
+
+ if (len < ipHdrLen + transportHdrLen) {
+ throw new IllegalStateException("Unexpected buffer length: " + len);
+ }
+ // Re-write addresses, ports and fix up checksums.
+ preparePacketForForwarding(mBuf, len, version, proto);
+ // Send the packet to the destination fd.
+ forwardPacket(mBuf, len);
+ }
+
+ @Override
+ public void run() {
+ Log.i(TAG, "starting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
+ while (!interrupted() && mSrcFd.valid()) {
+ processPacket();
+ }
+ Log.i(TAG, "exiting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
+ }
+}
diff --git a/common/testutils/devicetests/com/android/testutils/PacketBridge.kt b/common/testutils/devicetests/com/android/testutils/PacketBridge.kt
new file mode 100644
index 00000000..da3508de
--- /dev/null
+++ b/common/testutils/devicetests/com/android/testutils/PacketBridge.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.net.TestNetworkSpecifier
+import android.os.Binder
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import java.net.InetAddress
+import libcore.io.IoUtils
+
+private const val MIN_PORT_NUMBER = 1025
+private const val MAX_PORT_NUMBER = 65535
+
+/**
+ * A class that set up two {@link TestNetworkInterface} with NAT, and forward packets between them.
+ *
+ * See {@link NatPacketForwarder} for more detailed information.
+ */
+class PacketBridge(
+ context: Context,
+ internalAddr: LinkAddress,
+ externalAddr: LinkAddress,
+ dnsAddr: InetAddress
+) {
+ private val natMap = NatMap()
+ private val binder = Binder()
+
+ private val cm = context.getSystemService(ConnectivityManager::class.java)
+ private val tnm = context.getSystemService(TestNetworkManager::class.java)
+
+ // Create test networks.
+ private val internalIface = tnm.createTunInterface(listOf(internalAddr))
+ private val externalIface = tnm.createTunInterface(listOf(externalAddr))
+
+ // Register test networks to ConnectivityService.
+ private val internalNetworkCallback: TestableNetworkCallback
+ private val externalNetworkCallback: TestableNetworkCallback
+ val internalNetwork: Network
+ val externalNetwork: Network
+ init {
+ val (inCb, inNet) = createTestNetwork(internalIface, internalAddr, dnsAddr)
+ val (exCb, exNet) = createTestNetwork(externalIface, externalAddr, dnsAddr)
+ internalNetworkCallback = inCb
+ externalNetworkCallback = exCb
+ internalNetwork = inNet
+ externalNetwork = exNet
+ }
+
+ // Setup the packet bridge.
+ private val internalFd = internalIface.fileDescriptor.fileDescriptor
+ private val externalFd = externalIface.fileDescriptor.fileDescriptor
+
+ private val pr1 = NatInternalPacketForwarder(
+ internalFd,
+ 1500,
+ externalFd,
+ externalAddr.address,
+ natMap
+ )
+ private val pr2 = NatExternalPacketForwarder(
+ externalFd,
+ 1500,
+ internalFd,
+ externalAddr.address,
+ natMap
+ )
+
+ fun start() {
+ IoUtils.setBlocking(internalFd, true /* blocking */)
+ IoUtils.setBlocking(externalFd, true /* blocking */)
+ pr1.start()
+ pr2.start()
+ }
+
+ fun stop() {
+ pr1.interrupt()
+ pr2.interrupt()
+ cm.unregisterNetworkCallback(internalNetworkCallback)
+ cm.unregisterNetworkCallback(externalNetworkCallback)
+ }
+
+ /**
+ * Creates a test network with given test TUN interface and addresses.
+ */
+ private fun createTestNetwork(
+ testIface: TestNetworkInterface,
+ addr: LinkAddress,
+ dnsAddr: InetAddress
+ ): Pair<TestableNetworkCallback, Network> {
+ // Make a network request to hold the test network
+ val nr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .setNetworkSpecifier(TestNetworkSpecifier(testIface.interfaceName))
+ .build()
+ val testCb = TestableNetworkCallback()
+ cm.requestNetwork(nr, testCb)
+
+ val lp = LinkProperties().apply {
+ addLinkAddress(addr)
+ interfaceName = testIface.interfaceName
+ addDnsServer(dnsAddr)
+ }
+ tnm.setupTestNetwork(lp, true /* isMetered */, binder)
+
+ // Wait for available before return.
+ val network = testCb.expect<Available>().network
+ return testCb to network
+ }
+
+ /**
+ * A helper class to maintain the mappings between internal addresses/ports and external
+ * ports.
+ *
+ * This class assigns an unused external port number if the mapping between
+ * srcaddress:srcport:protocol and the external port does not exist yet.
+ *
+ * Note that this class is not thread-safe. The instance of the class needs to be
+ * synchronized in the callers when being used in multiple threads.
+ */
+ class NatMap {
+ data class AddressInfo(val address: InetAddress, val port: Int, val protocol: Int)
+
+ private val mToExternalPort = HashMap<AddressInfo, Int>()
+ private val mFromExternalPort = HashMap<Int, AddressInfo>()
+
+ // Skip well-known port 0~1024.
+ private var nextExternalPort = MIN_PORT_NUMBER
+
+ fun toExternalPort(addr: InetAddress, port: Int, protocol: Int): Int {
+ val info = AddressInfo(addr, port, protocol)
+ val extPort: Int
+ if (!mToExternalPort.containsKey(info)) {
+ extPort = nextExternalPort++
+ if (nextExternalPort > MAX_PORT_NUMBER) {
+ throw IllegalStateException("Available ports are exhausted")
+ }
+ mToExternalPort[info] = extPort
+ mFromExternalPort[extPort] = info
+ } else {
+ extPort = mToExternalPort[info]!!
+ }
+ return extPort
+ }
+
+ fun fromExternalPort(port: Int): AddressInfo? {
+ return mFromExternalPort[port]
+ }
+ }
+}
diff --git a/common/testutils/devicetests/com/android/testutils/PacketReflector.java b/common/testutils/devicetests/com/android/testutils/PacketReflector.java
index 96bca624..69392d44 100644
--- a/common/testutils/devicetests/com/android/testutils/PacketReflector.java
+++ b/common/testutils/devicetests/com/android/testutils/PacketReflector.java
@@ -20,46 +20,70 @@ import static android.system.OsConstants.ICMP6_ECHO_REPLY;
import static android.system.OsConstants.ICMP6_ECHO_REQUEST;
import android.annotation.NonNull;
+import android.net.TestNetworkInterface;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
import java.io.FileDescriptor;
import java.io.IOException;
+import java.util.Objects;
+/**
+ * A class that echoes packets received on a {@link TestNetworkInterface} back to itself.
+ *
+ * For testing purposes, sometimes a mocked environment to simulate a simple echo from the
+ * server side is needed. This is particularly useful if the test, e.g. VpnTest, is
+ * heavily relying on the outside world.
+ *
+ * This class reads packets from the {@link FileDescriptor} of a {@link TestNetworkInterface}, and:
+ * 1. For TCP and UDP packets, simply swaps the source address and the destination
+ * address, then send it back to the {@link FileDescriptor}.
+ * 2. For ICMP ping packets, composes a ping reply and sends it back to the sender.
+ * 3. Ignore all other packets.
+ */
public class PacketReflector extends Thread {
- private static final int IPV4_HEADER_LENGTH = 20;
- private static final int IPV6_HEADER_LENGTH = 40;
+ static final int IPV4_HEADER_LENGTH = 20;
+ static final int IPV6_HEADER_LENGTH = 40;
- private static final int IPV4_ADDR_OFFSET = 12;
- private static final int IPV6_ADDR_OFFSET = 8;
- private static final int IPV4_ADDR_LENGTH = 4;
- private static final int IPV6_ADDR_LENGTH = 16;
+ static final int IPV4_ADDR_OFFSET = 12;
+ static final int IPV6_ADDR_OFFSET = 8;
+ static final int IPV4_ADDR_LENGTH = 4;
+ static final int IPV6_ADDR_LENGTH = 16;
- private static final int IPV4_PROTO_OFFSET = 9;
- private static final int IPV6_PROTO_OFFSET = 6;
+ static final int IPV4_PROTO_OFFSET = 9;
+ static final int IPV6_PROTO_OFFSET = 6;
- private static final byte IPPROTO_ICMP = 1;
- private static final byte IPPROTO_TCP = 6;
- private static final byte IPPROTO_UDP = 17;
+ static final byte IPPROTO_ICMP = 1;
+ static final byte IPPROTO_TCP = 6;
+ static final byte IPPROTO_UDP = 17;
private static final byte IPPROTO_ICMPV6 = 58;
private static final int ICMP_HEADER_LENGTH = 8;
- private static final int TCP_HEADER_LENGTH = 20;
- private static final int UDP_HEADER_LENGTH = 8;
+ static final int TCP_HEADER_LENGTH = 20;
+ static final int UDP_HEADER_LENGTH = 8;
private static final byte ICMP_ECHO = 8;
private static final byte ICMP_ECHOREPLY = 0;
private static String TAG = "PacketReflector";
- @NonNull private FileDescriptor mFd;
- @NonNull private byte[] mBuf;
-
+ @NonNull
+ private final FileDescriptor mFd;
+ @NonNull
+ private final byte[] mBuf;
+
+ /**
+ * Construct a {@link PacketReflector} from the given {@code fd} of
+ * a {@link TestNetworkInterface}.
+ *
+ * @param fd {@link FileDescriptor} to read/write packets.
+ * @param mtu MTU of the test network.
+ */
public PacketReflector(@NonNull FileDescriptor fd, int mtu) {
super("PacketReflector");
- mFd = fd;
+ mFd = Objects.requireNonNull(fd);
mBuf = new byte[mtu];
}
@@ -140,7 +164,7 @@ public class PacketReflector extends Thread {
writePacket(buf, len);
// The device should have replied, and buf should now contain a ping response.
- int received = readPacket(buf);
+ int received = PacketReflectorUtil.readPacket(mFd, buf);
if (received != len) {
Log.i(TAG, "Reflecting ping did not result in ping response: " +
"read=" + received + " expected=" + len);
@@ -190,21 +214,11 @@ public class PacketReflector extends Thread {
}
}
- private int readPacket(@NonNull byte[] buf) {
- int len;
- try {
- len = Os.read(mFd, buf, 0, buf.length);
- } catch (ErrnoException | IOException e) {
- Log.e(TAG, "Error reading packet: " + e.getMessage());
- len = -1;
- }
- return len;
- }
-
// Reads one packet from our mFd, and possibly writes the packet back.
private void processPacket() {
- int len = readPacket(mBuf);
+ int len = PacketReflectorUtil.readPacket(mFd, mBuf);
if (len < 1) {
+ // Usually happens when socket read is being interrupted, e.g. stopping PacketReflector.
return;
}
@@ -217,11 +231,11 @@ public class PacketReflector extends Thread {
hdrLen = IPV6_HEADER_LENGTH;
protoPos = IPV6_PROTO_OFFSET;
} else {
- return;
+ throw new IllegalStateException("Unexpected version: " + version);
}
if (len < hdrLen) {
- return;
+ throw new IllegalStateException("Unexpected buffer length: " + len);
}
byte proto = mBuf[protoPos];
@@ -241,10 +255,10 @@ public class PacketReflector extends Thread {
}
public void run() {
- Log.i(TAG, "PacketReflector starting fd=" + mFd + " valid=" + mFd.valid());
+ Log.i(TAG, "starting fd=" + mFd + " valid=" + mFd.valid());
while (!interrupted() && mFd.valid()) {
processPacket();
}
- Log.i(TAG, "PacketReflector exiting fd=" + mFd + " valid=" + mFd.valid());
+ Log.i(TAG, "exiting fd=" + mFd + " valid=" + mFd.valid());
}
}
diff --git a/common/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt b/common/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
new file mode 100644
index 00000000..b0280454
--- /dev/null
+++ b/common/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("PacketReflectorUtil")
+
+package com.android.testutils
+
+import android.system.ErrnoException
+import android.system.Os
+import com.android.net.module.util.IpUtils
+import com.android.testutils.PacketReflector.IPV4_HEADER_LENGTH
+import com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH
+import java.io.FileDescriptor
+import java.io.IOException
+import java.net.InetAddress
+import java.nio.ByteBuffer
+
+fun readPacket(fd: FileDescriptor, buf: ByteArray): Int {
+ return try {
+ Os.read(fd, buf, 0, buf.size)
+ } catch (e: ErrnoException) {
+ -1
+ } catch (e: IOException) {
+ -1
+ }
+}
+
+fun getInetAddressAt(buf: ByteArray, pos: Int, len: Int): InetAddress =
+ InetAddress.getByAddress(buf.copyOfRange(pos, pos + len))
+
+/**
+ * Reads a 16-bit unsigned int at pos in big endian, with no alignment requirements.
+ */
+fun getPortAt(buf: ByteArray, pos: Int): Int {
+ return (buf[pos].toInt() and 0xff shl 8) + (buf[pos + 1].toInt() and 0xff)
+}
+
+fun setPortAt(port: Int, buf: ByteArray, pos: Int) {
+ buf[pos] = (port ushr 8).toByte()
+ buf[pos + 1] = (port and 0xff).toByte()
+}
+
+fun getAddressPositionAndLength(version: Int) = when (version) {
+ 4 -> PacketReflector.IPV4_ADDR_OFFSET to PacketReflector.IPV4_ADDR_LENGTH
+ 6 -> PacketReflector.IPV6_ADDR_OFFSET to PacketReflector.IPV6_ADDR_LENGTH
+ else -> throw IllegalArgumentException("Unknown IP version $version")
+}
+
+private const val IPV4_CHKSUM_OFFSET = 10
+private const val UDP_CHECKSUM_OFFSET = 6
+private const val TCP_CHECKSUM_OFFSET = 16
+
+fun fixPacketChecksum(buf: ByteArray, len: Int, version: Int, protocol: Byte) {
+ // Fill Ip checksum for IPv4. IPv6 header doesn't have a checksum field.
+ if (version == 4) {
+ val checksum = IpUtils.ipChecksum(ByteBuffer.wrap(buf), 0)
+ // Place checksum in Big-endian order.
+ buf[IPV4_CHKSUM_OFFSET] = (checksum.toInt() ushr 8).toByte()
+ buf[IPV4_CHKSUM_OFFSET + 1] = (checksum.toInt() and 0xff).toByte()
+ }
+
+ // Fill transport layer checksum.
+ val transportOffset = if (version == 4) IPV4_HEADER_LENGTH else IPV6_HEADER_LENGTH
+ when (protocol) {
+ PacketReflector.IPPROTO_UDP -> {
+ val checksumPos = transportOffset + UDP_CHECKSUM_OFFSET
+ // Clear before calculate.
+ buf[checksumPos + 1] = 0x00
+ buf[checksumPos] = buf[checksumPos + 1]
+ val checksum = IpUtils.udpChecksum(
+ ByteBuffer.wrap(buf), 0,
+ transportOffset
+ )
+ buf[checksumPos] = (checksum.toInt() ushr 8).toByte()
+ buf[checksumPos + 1] = (checksum.toInt() and 0xff).toByte()
+ }
+ PacketReflector.IPPROTO_TCP -> {
+ val checksumPos = transportOffset + TCP_CHECKSUM_OFFSET
+ // Clear before calculate.
+ buf[checksumPos + 1] = 0x00
+ buf[checksumPos] = buf[checksumPos + 1]
+ val transportLen: Int = len - transportOffset
+ val checksum = IpUtils.tcpChecksum(
+ ByteBuffer.wrap(buf), 0, transportOffset,
+ transportLen
+ )
+ buf[checksumPos] = (checksum.toInt() ushr 8).toByte()
+ buf[checksumPos + 1] = (checksum.toInt() and 0xff).toByte()
+ }
+ // TODO: Support ICMP.
+ else -> throw IllegalArgumentException("Unsupported protocol: $protocol")
+ }
+}
diff --git a/common/testutils/devicetests/com/android/testutils/TestHttpServer.kt b/common/testutils/devicetests/com/android/testutils/TestHttpServer.kt
index 39ce4872..740bf63a 100644
--- a/common/testutils/devicetests/com/android/testutils/TestHttpServer.kt
+++ b/common/testutils/devicetests/com/android/testutils/TestHttpServer.kt
@@ -19,6 +19,7 @@ package com.android.testutils
import android.net.Uri
import com.android.net.module.util.ArrayTrackRecord
import fi.iki.elonen.NanoHTTPD
+import java.io.IOException
/**
* A minimal HTTP server running on a random available port.
@@ -82,7 +83,23 @@ class TestHttpServer(host: String? = null) : NanoHTTPD(host, 0 /* auto-select th
val request = Request(session.uri
?: "", session.method, session.queryParameterString ?: "")
requestsRecord.add(request)
+
+ // For PUT and POST, call parseBody to read InputStream before responding.
+ if (Method.PUT == session.method || Method.POST == session.method) {
+ try {
+ session.parseBody(HashMap())
+ } catch (e: Exception) {
+ when (e) {
+ is IOException, is ResponseException -> e.toResponse()
+ else -> throw e
+ }
+ }
+ }
+
// Default response is a 404
return responses[request] ?: super.serve(session)
}
-} \ No newline at end of file
+
+ fun Exception.toResponse() =
+ newFixedLengthResponse(Response.Status.INTERNAL_ERROR, "text/plain", this.toString())
+}