Merge changes from topics "remove-napt", "support-multipleaddr" into main

* changes:
  Support multiple addresses in PacketBridge
  Remove NAPT from PacketForwarder
This commit is contained in:
Junyu Lai 2024-01-16 07:47:47 +00:00 • committed by Gerrit Code Review
commit 24b54c2285
6 changed files with 58 additions and 318 deletions

View file

@ -1,81 +0,0 @@
/*
* 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())
}
}

View file

@ -1,78 +0,0 @@
/*
* 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())
}
}

View file

@ -16,6 +16,7 @@
package com.android.testutils
import android.annotation.SuppressLint
import android.content.Context
import android.net.ConnectivityManager
import android.net.LinkAddress
@ -31,29 +32,26 @@ 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.
* A class that set up two {@link TestNetworkInterface}, and forward packets between them.
*
* See {@link NatPacketForwarder} for more detailed information.
* See {@link PacketForwarder} for more detailed information.
*/
class PacketBridge(
context: Context,
internalAddr: LinkAddress,
externalAddr: LinkAddress,
addresses: List<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))
// Create test networks. The needed permissions should be supplied by the callers.
@SuppressLint("MissingPermission")
private val internalIface = tnm.createTunInterface(addresses)
@SuppressLint("MissingPermission")
private val externalIface = tnm.createTunInterface(addresses)
// Register test networks to ConnectivityService.
private val internalNetworkCallback: TestableNetworkCallback
@ -61,32 +59,20 @@ class PacketBridge(
val internalNetwork: Network
val externalNetwork: Network
init {
val (inCb, inNet) = createTestNetwork(internalIface, internalAddr, dnsAddr)
val (exCb, exNet) = createTestNetwork(externalIface, externalAddr, dnsAddr)
val (inCb, inNet) = createTestNetwork(internalIface, addresses, dnsAddr)
val (exCb, exNet) = createTestNetwork(externalIface, addresses, dnsAddr)
internalNetworkCallback = inCb
externalNetworkCallback = exCb
internalNetwork = inNet
externalNetwork = exNet
}
// Setup the packet bridge.
// Set up 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
)
private val pr1 = PacketForwarder(internalFd, 1500, externalFd)
private val pr2 = PacketForwarder(externalFd, 1500, internalFd)
fun start() {
IoUtils.setBlocking(internalFd, true /* blocking */)
@ -107,7 +93,7 @@ class PacketBridge(
*/
private fun createTestNetwork(
testIface: TestNetworkInterface,
addr: LinkAddress,
addresses: List<LinkAddress>,
dnsAddr: InetAddress
): Pair<TestableNetworkCallback, Network> {
// Make a network request to hold the test network
@ -120,7 +106,7 @@ class PacketBridge(
cm.requestNetwork(nr, testCb)
val lp = LinkProperties().apply {
addLinkAddress(addr)
setLinkAddresses(addresses)
interfaceName = testIface.interfaceName
addDnsServer(dnsAddr)
}
@ -130,44 +116,4 @@ class PacketBridge(
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]
}
}
}

View file

@ -0,0 +1,160 @@
/*
* 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 java.io.FileDescriptor;
import java.io.IOException;
import java.util.Objects;
/**
* A class that forwards packets from a {@link TestNetworkInterface} to another
* {@link TestNetworkInterface}.
*
* 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 different port.
*
* 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.
*/
public class PacketForwarder extends Thread {
private static final String TAG = "PacketForwarder";
// 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;
/**
* Construct a {@link PacketForwarder}.
*
* This class reads packets from {@code srcFd} of a {@link TestNetworkInterface}, and
* forwards them to the {@code dstFd} of another {@link TestNetworkInterface}.
*
* 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.
*/
public PacketForwarder(@NonNull FileDescriptor srcFd, int mtu,
@NonNull FileDescriptor dstFd) {
super(TAG);
mSrcFd = Objects.requireNonNull(srcFd);
mBuf = new byte[mtu];
mDstFd = Objects.requireNonNull(dstFd);
}
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) {
// Usually happens when socket read is being interrupted, e.g. stopping PacketForwarder.
return;
}
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);
}
// Swap addresses.
PacketReflectorUtil.swapAddresses(mBuf, version);
// 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());
}
}

View file

@ -87,31 +87,6 @@ public class PacketReflector extends Thread {
mBuf = new byte[mtu];
}
private static void swapBytes(@NonNull byte[] buf, int pos1, int pos2, int len) {
for (int i = 0; i < len; i++) {
byte b = buf[pos1 + i];
buf[pos1 + i] = buf[pos2 + i];
buf[pos2 + i] = b;
}
}
private static void swapAddresses(@NonNull byte[] buf, int version) {
int addrPos, addrLen;
switch (version) {
case 4:
addrPos = IPV4_ADDR_OFFSET;
addrLen = IPV4_ADDR_LENGTH;
break;
case 6:
addrPos = IPV6_ADDR_OFFSET;
addrLen = IPV6_ADDR_LENGTH;
break;
default:
throw new IllegalArgumentException();
}
swapBytes(buf, addrPos, addrPos + addrLen, addrLen);
}
// Reflect TCP packets: swap the source and destination addresses, but don't change the ports.
// This is used by the test to "connect to itself" through the VPN.
private void processTcpPacket(@NonNull byte[] buf, int version, int len, int hdrLen) {
@ -120,7 +95,7 @@ public class PacketReflector extends Thread {
}
// Swap src and dst IP addresses.
swapAddresses(buf, version);
PacketReflectorUtil.swapAddresses(buf, version);
// Send the packet back.
writePacket(buf, len);
@ -134,11 +109,11 @@ public class PacketReflector extends Thread {
}
// Swap src and dst IP addresses.
swapAddresses(buf, version);
PacketReflectorUtil.swapAddresses(buf, version);
// Swap dst and src ports.
int portOffset = hdrLen;
swapBytes(buf, portOffset, portOffset + 2, 2);
PacketReflectorUtil.swapBytes(buf, portOffset, portOffset + 2, 2);
// Send the packet back.
writePacket(buf, len);
@ -160,7 +135,7 @@ public class PacketReflector extends Thread {
// Swap src and dst IP addresses, and send the packet back.
// This effectively pings the device to see if it replies.
swapAddresses(buf, version);
PacketReflectorUtil.swapAddresses(buf, version);
writePacket(buf, len);
// The device should have replied, and buf should now contain a ping response.
@ -202,7 +177,7 @@ public class PacketReflector extends Thread {
}
// Now swap the addresses again and reflect the packet. This sends a ping reply.
swapAddresses(buf, version);
PacketReflectorUtil.swapAddresses(buf, version);
writePacket(buf, len);
}

View file

@ -112,3 +112,28 @@ fun fixPacketChecksum(buf: ByteArray, len: Int, version: Int, protocol: Byte) {
else -> throw IllegalArgumentException("Unsupported protocol: $protocol")
}
}
fun swapBytes(buf: ByteArray, pos1: Int, pos2: Int, len: Int) {
for (i in 0 until len) {
val b = buf[pos1 + i]
buf[pos1 + i] = buf[pos2 + i]
buf[pos2 + i] = b
}
}
fun swapAddresses(buf: ByteArray, version: Int) {
val addrPos: Int
val addrLen: Int
when (version) {
4 -> {
addrPos = PacketReflector.IPV4_ADDR_OFFSET
addrLen = PacketReflector.IPV4_ADDR_LENGTH
}
6 -> {
addrPos = PacketReflector.IPV6_ADDR_OFFSET
addrLen = PacketReflector.IPV6_ADDR_LENGTH
}
else -> throw java.lang.IllegalArgumentException()
}
swapBytes(buf, addrPos, addrPos + addrLen, addrLen)
}