Remove NAPT from PacketForwarder

Consider below scenario:
  Internal address: 1.2.3.4
  External address: 5.6.7.8
  Remote address: 8.8.8.8
When a packet was sent from 1.2.3.4 to 8.8.8.8, the current design
translate it to a packet from 8.8.8.8 to 5.6.7.8 and foward it
to the external interface. However, when the response is received
from the external interface, there is a need to remember what is
the original address to forward to. Thus, an address translation
mechanism was introduced in current design.

This change simplify PacketForwarder by removing the address
translation.
Consider, if the 2 interfaces are using the same address.
  Internal address: 1.2.3.4
  External address: 1.2.3.4
  Remote address: 8.8.8.8
When a packet was sent from 1.2.3.4 to 8.8.8.8, simply swap
source and destination and and foward it to the external
interface. When there is a response from the external interface,
the same process can be done, this removes the need of address
translation.

Test: atest NetworkStackIntegrationTests:android.net.NetworkStatsIntegrationTest
Test: atest CtsHostsideNetworkTests:com.android.cts.net.HostsideVpnTests
Bug: N/A

Change-Id: I86f2e774e9112905e5255b5a28c927562a42ab84
This commit is contained in:
Junyu Lai 2023-03-30 13:27:57 +08:00
commit 74acb88d03
6 changed files with 57 additions and 316 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,27 @@ 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.
*/
// TODO: Support multiple addresses.
class PacketBridge(
context: Context,
internalAddr: LinkAddress,
externalAddr: LinkAddress,
address: 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(listOf(address))
@SuppressLint("MissingPermission")
private val externalIface = tnm.createTunInterface(listOf(address))
// Register test networks to ConnectivityService.
private val internalNetworkCallback: TestableNetworkCallback
@ -61,32 +60,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, address, dnsAddr)
val (exCb, exNet) = createTestNetwork(externalIface, address, 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 */)
@ -130,44 +117,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)
}