[ST05.3] Support PacketBridge
For testing purposes, a TestNetworkInterface provides a
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 between two
TestNetworkInterface instances. Thus, the TCP/IP headers
could be parsed/composed automatically by the protocol stack of
this additional TestNetworkInterface, while the payload is
supplied by the servers run on the interface.
Test: 1. atest android.net.NetworkStatsIntegrationTest
2. atest com.android.cts.net.HostsideVpnTests
Bug: 139774492
Change-Id: I8fe5a434d6d727a81d106a967dcdca157424e12e
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt
new file mode 100644
index 0000000..d7961a0
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NatExternalPacketForwarder.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import java.io.FileDescriptor
+import java.net.InetAddress
+
+/**
+ * A class that forwards packets from the external {@link TestNetworkInterface} to the internal
+ * {@link TestNetworkInterface} with NAT. See {@link NatPacketForwarderBase} for detail.
+ */
+class NatExternalPacketForwarder(
+ srcFd: FileDescriptor,
+ mtu: Int,
+ dstFd: FileDescriptor,
+ extAddr: InetAddress,
+ natMap: PacketBridge.NatMap
+) : NatPacketForwarderBase(srcFd, mtu, dstFd, extAddr, natMap) {
+
+ /**
+ * Rewrite addresses, ports and fix up checksums for packets received on the external
+ * interface.
+ *
+ * Incoming response from external interface which is being forwarded to the internal
+ * interface with translated address, e.g. 1.2.3.4:80 -> 8.8.8.8:1234
+ * will be translated into 8.8.8.8:80 -> 192.168.1.1:5678.
+ *
+ * For packets that are not an incoming response, do not forward them to the
+ * internal interface.
+ */
+ override fun preparePacketForForwarding(buf: ByteArray, len: Int, version: Int, proto: Int) {
+ val (addrPos, addrLen) = getAddressPositionAndLength(version)
+
+ // TODO: support one external address per ip version.
+ val extAddrBuf = mExtAddr.address
+ if (addrLen != extAddrBuf.size) throw IllegalStateException("Packet IP version mismatch")
+
+ // Get internal address by port.
+ val transportOffset =
+ if (version == 4) PacketReflector.IPV4_HEADER_LENGTH
+ else PacketReflector.IPV6_HEADER_LENGTH
+ val dstPort = getPortAt(buf, transportOffset + DESTINATION_PORT_OFFSET)
+ val intAddrInfo = synchronized(mNatMap) { mNatMap.fromExternalPort(dstPort) }
+ // No mapping, skip. This usually happens if the connection is initiated directly on
+ // the external interface, e.g. DNS64 resolution, network validation, etc.
+ if (intAddrInfo == null) return
+
+ val intAddrBuf = intAddrInfo.address.address
+ val intPort = intAddrInfo.port
+
+ // Copy the original destination to into the source address.
+ for (i in 0 until addrLen) {
+ buf[addrPos + i] = buf[addrPos + addrLen + i]
+ }
+
+ // Copy the internal address into the destination address.
+ for (i in 0 until addrLen) {
+ buf[addrPos + addrLen + i] = intAddrBuf[i]
+ }
+
+ // Copy the internal port into the destination port.
+ setPortAt(intPort, buf, transportOffset + DESTINATION_PORT_OFFSET)
+
+ // Fix IP and Transport layer checksum.
+ fixPacketChecksum(buf, len, version, proto.toByte())
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt b/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt
new file mode 100644
index 0000000..fa39d19
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NatInternalPacketForwarder.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import java.io.FileDescriptor
+import java.net.InetAddress
+
+/**
+ * A class that forwards packets from the internal {@link TestNetworkInterface} to the external
+ * {@link TestNetworkInterface} with NAT. See {@link NatPacketForwarderBase} for detail.
+ */
+class NatInternalPacketForwarder(
+ srcFd: FileDescriptor,
+ mtu: Int,
+ dstFd: FileDescriptor,
+ extAddr: InetAddress,
+ natMap: PacketBridge.NatMap
+) : NatPacketForwarderBase(srcFd, mtu, dstFd, extAddr, natMap) {
+
+ /**
+ * Rewrite addresses, ports and fix up checksums for packets received on the internal
+ * interface.
+ *
+ * Outgoing packet from the internal interface which is being forwarded to the
+ * external interface with translated address, e.g. 192.168.1.1:5678 -> 8.8.8.8:80
+ * will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
+ *
+ * The external port, e.g. 1234 in the above example, is the port number assigned by
+ * the forwarder when creating the mapping to identify the source address and port when
+ * the response is coming from the external interface. See {@link PacketBridge.NatMap}
+ * for detail.
+ */
+ override fun preparePacketForForwarding(buf: ByteArray, len: Int, version: Int, proto: Int) {
+ val (addrPos, addrLen) = getAddressPositionAndLength(version)
+
+ // TODO: support one external address per ip version.
+ val extAddrBuf = mExtAddr.address
+ if (addrLen != extAddrBuf.size) throw IllegalStateException("Packet IP version mismatch")
+
+ val srcAddr = getInetAddressAt(buf, addrPos, addrLen)
+
+ // Copy the original destination to into the source address.
+ for (i in 0 until addrLen) {
+ buf[addrPos + i] = buf[addrPos + addrLen + i]
+ }
+
+ // Copy the external address into the destination address.
+ for (i in 0 until addrLen) {
+ buf[addrPos + addrLen + i] = extAddrBuf[i]
+ }
+
+ // Add an entry to NAT mapping table.
+ val transportOffset =
+ if (version == 4) PacketReflector.IPV4_HEADER_LENGTH
+ else PacketReflector.IPV6_HEADER_LENGTH
+ val srcPort = getPortAt(buf, transportOffset)
+ val extPort = synchronized(mNatMap) { mNatMap.toExternalPort(srcAddr, srcPort, proto) }
+ // Copy the external port to into the source port.
+ setPortAt(extPort, buf, transportOffset)
+
+ // Fix IP and Transport layer checksum.
+ fixPacketChecksum(buf, len, version, proto.toByte())
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java b/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
new file mode 100644
index 0000000..85c6493
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils;
+
+import static com.android.testutils.PacketReflector.IPPROTO_TCP;
+import static com.android.testutils.PacketReflector.IPPROTO_UDP;
+import static com.android.testutils.PacketReflector.IPV4_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.IPV6_PROTO_OFFSET;
+import static com.android.testutils.PacketReflector.TCP_HEADER_LENGTH;
+import static com.android.testutils.PacketReflector.UDP_HEADER_LENGTH;
+
+import android.annotation.NonNull;
+import android.net.TestNetworkInterface;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.Objects;
+
+/**
+ * A class that forwards packets from a {@link TestNetworkInterface} to another
+ * {@link TestNetworkInterface} with NAT.
+ *
+ * For testing purposes, a {@link TestNetworkInterface} provides a {@link FileDescriptor}
+ * which allows content injection on the test network. However, this could be hard to use
+ * because the callers need to compose IP packets in order to inject content to the
+ * test network.
+ *
+ * In order to remove the need of composing the IP packets, this class forwards IP packets to
+ * the {@link FileDescriptor} of another {@link TestNetworkInterface} instance. Thus,
+ * the TCP/IP headers could be parsed/composed automatically by the protocol stack of this
+ * additional {@link TestNetworkInterface}, while the payload is supplied by the
+ * servers run on the interface.
+ *
+ * To make it work, an internal interface and an external interface are defined, where
+ * the client might send packets from the internal interface which are originated from
+ * multiple addresses to a server that listens on the external address.
+ *
+ * When forwarding the outgoing packet on the internal interface, a simple NAT mechanism
+ * is implemented during forwarding, which will swap the source and destination,
+ * but replacing the source address with the external address,
+ * e.g. 192.168.1.1:1234 -> 8.8.8.8:80 will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
+ *
+ * For the above example, a client who sends http request will have a hallucination that
+ * it is talking to a remote server at 8.8.8.8. Also, the server listens on 1.2.3.4 will
+ * have a different hallucination that the request is sent from a remote client at 8.8.8.8,
+ * to a local address 1.2.3.4.
+ *
+ * And a NAT mapping is created at the time when the outgoing packet is forwarded.
+ * With a different internal source port, the instance learned that when a response with the
+ * destination port 1234, it should forward the packet to the internal address 192.168.1.1.
+ *
+ * For the incoming packet received from external interface, for example a http response sent
+ * from the http server, the same mechanism is applied but in a different direction,
+ * where the source and destination will be swapped, and the source address will be replaced
+ * with the internal address, which is obtained from the NAT mapping described above.
+ */
+public abstract class NatPacketForwarderBase extends Thread {
+ private static final String TAG = "NatPacketForwarder";
+ static final int DESTINATION_PORT_OFFSET = 2;
+
+ // The source fd to read packets from.
+ @NonNull
+ final FileDescriptor mSrcFd;
+ // The buffer to temporarily hold the entire packet after receiving.
+ @NonNull
+ final byte[] mBuf;
+ // The destination fd to write packets to.
+ @NonNull
+ final FileDescriptor mDstFd;
+ // The NAT mapping table shared between two NatPacketForwarder instances to map from
+ // the source port to the associated internal address. The map can be read/write from two
+ // different threads on any given time whenever receiving packets on the
+ // {@link TestNetworkInterface}. Thus, synchronize on the object when reading/writing is needed.
+ @GuardedBy("mNatMap")
+ @NonNull
+ final PacketBridge.NatMap mNatMap;
+ // The address of the external interface. See {@link NatPacketForwarder}.
+ @NonNull
+ final InetAddress mExtAddr;
+
+ /**
+ * Construct a {@link NatPacketForwarderBase}.
+ *
+ * This class reads packets from {@code srcFd} of a {@link TestNetworkInterface}, and
+ * forwards them to the {@code dstFd} of another {@link TestNetworkInterface} with
+ * NAT applied. See {@link NatPacketForwarderBase}.
+ *
+ * To apply NAT, the address of the external interface needs to be supplied through
+ * {@code extAddr} to identify the external interface. And a shared NAT mapping table,
+ * {@code natMap} is needed to be shared between these two instances.
+ *
+ * Note that this class is not useful if the instance is not managed by a
+ * {@link PacketBridge} to set up a two-way communication.
+ *
+ * @param srcFd {@link FileDescriptor} to read packets from.
+ * @param mtu MTU of the test network.
+ * @param dstFd {@link FileDescriptor} to write packets to.
+ * @param extAddr the external address, which is the address of the external interface.
+ * See {@link NatPacketForwarderBase}.
+ * @param natMap the NAT mapping table shared between two {@link NatPacketForwarderBase}
+ * instance.
+ */
+ public NatPacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
+ @NonNull FileDescriptor dstFd, @NonNull InetAddress extAddr,
+ @NonNull PacketBridge.NatMap natMap) {
+ super(TAG);
+ mSrcFd = Objects.requireNonNull(srcFd);
+ mBuf = new byte[mtu];
+ mDstFd = Objects.requireNonNull(dstFd);
+ mExtAddr = Objects.requireNonNull(extAddr);
+ mNatMap = Objects.requireNonNull(natMap);
+ }
+
+ /**
+ * A method to prepare forwarding packets between two instances of {@link TestNetworkInterface},
+ * which includes re-write addresses, ports and fix up checksums.
+ * Subclasses should override this method to implement a simple NAT.
+ */
+ abstract void preparePacketForForwarding(@NonNull byte[] buf, int len, int version, int proto);
+
+ private void forwardPacket(@NonNull byte[] buf, int len) {
+ try {
+ Os.write(mDstFd, buf, 0, len);
+ } catch (ErrnoException | IOException e) {
+ Log.e(TAG, "Error writing packet: " + e.getMessage());
+ }
+ }
+
+ // Reads one packet from mSrcFd, and writes the packet to the mDstFd for supported protocols.
+ private void processPacket() {
+ final int len = PacketReflectorUtil.readPacket(mSrcFd, mBuf);
+ if (len < 1) {
+ throw new IllegalStateException("Unexpected buffer length: " + len);
+ }
+
+ final int version = mBuf[0] >>> 4;
+ final int protoPos, ipHdrLen;
+ switch (version) {
+ case 4:
+ ipHdrLen = IPV4_HEADER_LENGTH;
+ protoPos = PacketReflector.IPV4_PROTO_OFFSET;
+ break;
+ case 6:
+ ipHdrLen = IPV6_HEADER_LENGTH;
+ protoPos = IPV6_PROTO_OFFSET;
+ break;
+ default:
+ throw new IllegalStateException("Unexpected version: " + version);
+ }
+ if (len < ipHdrLen) {
+ throw new IllegalStateException("Unexpected buffer length: " + len);
+ }
+
+ final byte proto = mBuf[protoPos];
+ final int transportHdrLen;
+ switch (proto) {
+ case IPPROTO_TCP:
+ transportHdrLen = TCP_HEADER_LENGTH;
+ break;
+ case IPPROTO_UDP:
+ transportHdrLen = UDP_HEADER_LENGTH;
+ break;
+ // TODO: Support ICMP.
+ default:
+ return; // Unknown protocol, ignored.
+ }
+
+ if (len < ipHdrLen + transportHdrLen) {
+ throw new IllegalStateException("Unexpected buffer length: " + len);
+ }
+ // Re-write addresses, ports and fix up checksums.
+ preparePacketForForwarding(mBuf, len, version, proto);
+ // Send the packet to the destination fd.
+ forwardPacket(mBuf, len);
+ }
+
+ @Override
+ public void run() {
+ Log.i(TAG, "starting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
+ while (!interrupted() && mSrcFd.valid()) {
+ processPacket();
+ }
+ Log.i(TAG, "exiting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
new file mode 100644
index 0000000..da3508d
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketBridge.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.testutils
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import android.net.TestNetworkInterface
+import android.net.TestNetworkManager
+import android.net.TestNetworkSpecifier
+import android.os.Binder
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import java.net.InetAddress
+import libcore.io.IoUtils
+
+private const val MIN_PORT_NUMBER = 1025
+private const val MAX_PORT_NUMBER = 65535
+
+/**
+ * A class that set up two {@link TestNetworkInterface} with NAT, and forward packets between them.
+ *
+ * See {@link NatPacketForwarder} for more detailed information.
+ */
+class PacketBridge(
+ context: Context,
+ internalAddr: LinkAddress,
+ externalAddr: LinkAddress,
+ dnsAddr: InetAddress
+) {
+ private val natMap = NatMap()
+ private val binder = Binder()
+
+ private val cm = context.getSystemService(ConnectivityManager::class.java)
+ private val tnm = context.getSystemService(TestNetworkManager::class.java)
+
+ // Create test networks.
+ private val internalIface = tnm.createTunInterface(listOf(internalAddr))
+ private val externalIface = tnm.createTunInterface(listOf(externalAddr))
+
+ // Register test networks to ConnectivityService.
+ private val internalNetworkCallback: TestableNetworkCallback
+ private val externalNetworkCallback: TestableNetworkCallback
+ val internalNetwork: Network
+ val externalNetwork: Network
+ init {
+ val (inCb, inNet) = createTestNetwork(internalIface, internalAddr, dnsAddr)
+ val (exCb, exNet) = createTestNetwork(externalIface, externalAddr, dnsAddr)
+ internalNetworkCallback = inCb
+ externalNetworkCallback = exCb
+ internalNetwork = inNet
+ externalNetwork = exNet
+ }
+
+ // Setup the packet bridge.
+ private val internalFd = internalIface.fileDescriptor.fileDescriptor
+ private val externalFd = externalIface.fileDescriptor.fileDescriptor
+
+ private val pr1 = NatInternalPacketForwarder(
+ internalFd,
+ 1500,
+ externalFd,
+ externalAddr.address,
+ natMap
+ )
+ private val pr2 = NatExternalPacketForwarder(
+ externalFd,
+ 1500,
+ internalFd,
+ externalAddr.address,
+ natMap
+ )
+
+ fun start() {
+ IoUtils.setBlocking(internalFd, true /* blocking */)
+ IoUtils.setBlocking(externalFd, true /* blocking */)
+ pr1.start()
+ pr2.start()
+ }
+
+ fun stop() {
+ pr1.interrupt()
+ pr2.interrupt()
+ cm.unregisterNetworkCallback(internalNetworkCallback)
+ cm.unregisterNetworkCallback(externalNetworkCallback)
+ }
+
+ /**
+ * Creates a test network with given test TUN interface and addresses.
+ */
+ private fun createTestNetwork(
+ testIface: TestNetworkInterface,
+ addr: LinkAddress,
+ dnsAddr: InetAddress
+ ): Pair<TestableNetworkCallback, Network> {
+ // Make a network request to hold the test network
+ val nr = NetworkRequest.Builder()
+ .clearCapabilities()
+ .addTransportType(NetworkCapabilities.TRANSPORT_TEST)
+ .setNetworkSpecifier(TestNetworkSpecifier(testIface.interfaceName))
+ .build()
+ val testCb = TestableNetworkCallback()
+ cm.requestNetwork(nr, testCb)
+
+ val lp = LinkProperties().apply {
+ addLinkAddress(addr)
+ interfaceName = testIface.interfaceName
+ addDnsServer(dnsAddr)
+ }
+ tnm.setupTestNetwork(lp, true /* isMetered */, binder)
+
+ // Wait for available before return.
+ val network = testCb.expect<Available>().network
+ return testCb to network
+ }
+
+ /**
+ * A helper class to maintain the mappings between internal addresses/ports and external
+ * ports.
+ *
+ * This class assigns an unused external port number if the mapping between
+ * srcaddress:srcport:protocol and the external port does not exist yet.
+ *
+ * Note that this class is not thread-safe. The instance of the class needs to be
+ * synchronized in the callers when being used in multiple threads.
+ */
+ class NatMap {
+ data class AddressInfo(val address: InetAddress, val port: Int, val protocol: Int)
+
+ private val mToExternalPort = HashMap<AddressInfo, Int>()
+ private val mFromExternalPort = HashMap<Int, AddressInfo>()
+
+ // Skip well-known port 0~1024.
+ private var nextExternalPort = MIN_PORT_NUMBER
+
+ fun toExternalPort(addr: InetAddress, port: Int, protocol: Int): Int {
+ val info = AddressInfo(addr, port, protocol)
+ val extPort: Int
+ if (!mToExternalPort.containsKey(info)) {
+ extPort = nextExternalPort++
+ if (nextExternalPort > MAX_PORT_NUMBER) {
+ throw IllegalStateException("Available ports are exhausted")
+ }
+ mToExternalPort[info] = extPort
+ mFromExternalPort[extPort] = info
+ } else {
+ extPort = mToExternalPort[info]!!
+ }
+ return extPort
+ }
+
+ fun fromExternalPort(port: Int): AddressInfo? {
+ return mFromExternalPort[port]
+ }
+ }
+}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
index 96bca62..a59750d 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflector.java
@@ -26,40 +26,43 @@
import java.io.FileDescriptor;
import java.io.IOException;
+import java.util.Objects;
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;
public PacketReflector(@NonNull FileDescriptor fd, int mtu) {
super("PacketReflector");
- mFd = fd;
+ mFd = Objects.requireNonNull(fd);
mBuf = new byte[mtu];
}
@@ -140,7 +143,7 @@
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 +193,11 @@
}
}
- 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 +210,11 @@
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 +234,10 @@
}
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/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
new file mode 100644
index 0000000..b028045
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketReflectorUtil.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("PacketReflectorUtil")
+
+package com.android.testutils
+
+import android.system.ErrnoException
+import android.system.Os
+import com.android.net.module.util.IpUtils
+import com.android.testutils.PacketReflector.IPV4_HEADER_LENGTH
+import com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH
+import java.io.FileDescriptor
+import java.io.IOException
+import java.net.InetAddress
+import java.nio.ByteBuffer
+
+fun readPacket(fd: FileDescriptor, buf: ByteArray): Int {
+ return try {
+ Os.read(fd, buf, 0, buf.size)
+ } catch (e: ErrnoException) {
+ -1
+ } catch (e: IOException) {
+ -1
+ }
+}
+
+fun getInetAddressAt(buf: ByteArray, pos: Int, len: Int): InetAddress =
+ InetAddress.getByAddress(buf.copyOfRange(pos, pos + len))
+
+/**
+ * Reads a 16-bit unsigned int at pos in big endian, with no alignment requirements.
+ */
+fun getPortAt(buf: ByteArray, pos: Int): Int {
+ return (buf[pos].toInt() and 0xff shl 8) + (buf[pos + 1].toInt() and 0xff)
+}
+
+fun setPortAt(port: Int, buf: ByteArray, pos: Int) {
+ buf[pos] = (port ushr 8).toByte()
+ buf[pos + 1] = (port and 0xff).toByte()
+}
+
+fun getAddressPositionAndLength(version: Int) = when (version) {
+ 4 -> PacketReflector.IPV4_ADDR_OFFSET to PacketReflector.IPV4_ADDR_LENGTH
+ 6 -> PacketReflector.IPV6_ADDR_OFFSET to PacketReflector.IPV6_ADDR_LENGTH
+ else -> throw IllegalArgumentException("Unknown IP version $version")
+}
+
+private const val IPV4_CHKSUM_OFFSET = 10
+private const val UDP_CHECKSUM_OFFSET = 6
+private const val TCP_CHECKSUM_OFFSET = 16
+
+fun fixPacketChecksum(buf: ByteArray, len: Int, version: Int, protocol: Byte) {
+ // Fill Ip checksum for IPv4. IPv6 header doesn't have a checksum field.
+ if (version == 4) {
+ val checksum = IpUtils.ipChecksum(ByteBuffer.wrap(buf), 0)
+ // Place checksum in Big-endian order.
+ buf[IPV4_CHKSUM_OFFSET] = (checksum.toInt() ushr 8).toByte()
+ buf[IPV4_CHKSUM_OFFSET + 1] = (checksum.toInt() and 0xff).toByte()
+ }
+
+ // Fill transport layer checksum.
+ val transportOffset = if (version == 4) IPV4_HEADER_LENGTH else IPV6_HEADER_LENGTH
+ when (protocol) {
+ PacketReflector.IPPROTO_UDP -> {
+ val checksumPos = transportOffset + UDP_CHECKSUM_OFFSET
+ // Clear before calculate.
+ buf[checksumPos + 1] = 0x00
+ buf[checksumPos] = buf[checksumPos + 1]
+ val checksum = IpUtils.udpChecksum(
+ ByteBuffer.wrap(buf), 0,
+ transportOffset
+ )
+ buf[checksumPos] = (checksum.toInt() ushr 8).toByte()
+ buf[checksumPos + 1] = (checksum.toInt() and 0xff).toByte()
+ }
+ PacketReflector.IPPROTO_TCP -> {
+ val checksumPos = transportOffset + TCP_CHECKSUM_OFFSET
+ // Clear before calculate.
+ buf[checksumPos + 1] = 0x00
+ buf[checksumPos] = buf[checksumPos + 1]
+ val transportLen: Int = len - transportOffset
+ val checksum = IpUtils.tcpChecksum(
+ ByteBuffer.wrap(buf), 0, transportOffset,
+ transportLen
+ )
+ buf[checksumPos] = (checksum.toInt() ushr 8).toByte()
+ buf[checksumPos + 1] = (checksum.toInt() and 0xff).toByte()
+ }
+ // TODO: Support ICMP.
+ else -> throw IllegalArgumentException("Unsupported protocol: $protocol")
+ }
+}