diff options
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()) +} |