Merge "Use custom defined DNS/HTTP port" into main

This commit is contained in:
Jimi Chen 2024-02-21 09:03:47 +00:00 • committed by Gerrit Code Review
commit 48f8d1892f
5 changed files with 171 additions and 11 deletions

View file

@ -0,0 +1,44 @@
/*
* Copyright (C) 2024 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
class ExternalPacketForwarder(
srcFd: FileDescriptor,
mtu: Int,
dstFd: FileDescriptor,
forwardMap: Map<Int, Int>
) : PacketForwarderBase(srcFd, mtu, dstFd, forwardMap) {
/**
* Prepares a packet for forwarding by potentially updating the
* source port based on the specified port remapping rules.
*
* @param buf The packet data as a byte array.
* @param version The IP version of the packet (e.g., 4 for IPv4).
*/
override fun remapPort(buf: ByteArray, version: Int) {
val transportOffset = getTransportOffset(version)
val intPort = getRemappedPort(buf, transportOffset)
// Copy remapped source port.
if (intPort != 0) {
setPortAt(intPort, buf, transportOffset)
}
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (C) 2024 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
class InternalPacketForwarder(
srcFd: FileDescriptor,
mtu: Int,
dstFd: FileDescriptor,
forwardMap: Map<Int, Int>
) : PacketForwarderBase(srcFd, mtu, dstFd, forwardMap) {
/**
* Prepares a packet for forwarding by potentially updating the
* destination port based on the specified port remapping rules.
*
* @param buf The packet data as a byte array.
* @param version The IP version of the packet (e.g., 4 for IPv4).
*/
override fun remapPort(buf: ByteArray, version: Int) {
val transportOffset = getTransportOffset(version) + DESTINATION_PORT_OFFSET
val extPort = getRemappedPort(buf, transportOffset)
// Copy remapped destination port.
if (extPort != 0) {
setPortAt(extPort, buf, transportOffset)
}
}
}

View file

@ -40,7 +40,8 @@ import libcore.io.IoUtils
class PacketBridge(
context: Context,
addresses: List<LinkAddress>,
dnsAddr: InetAddress
dnsAddr: InetAddress,
portMapping: List<Pair<Int, Int>>
) {
private val binder = Binder()
@ -56,6 +57,10 @@ class PacketBridge(
// Register test networks to ConnectivityService.
private val internalNetworkCallback: TestableNetworkCallback
private val externalNetworkCallback: TestableNetworkCallback
private val internalForwardMap = HashMap<Int, Int>()
private val externalForwardMap = HashMap<Int, Int>()
val internalNetwork: Network
val externalNetwork: Network
init {
@ -65,14 +70,28 @@ class PacketBridge(
externalNetworkCallback = exCb
internalNetwork = inNet
externalNetwork = exNet
for (mapping in portMapping) {
internalForwardMap[mapping.first] = mapping.second
externalForwardMap[mapping.second] = mapping.first
}
}
// Set up the packet bridge.
private val internalFd = internalIface.fileDescriptor.fileDescriptor
private val externalFd = externalIface.fileDescriptor.fileDescriptor
private val pr1 = PacketForwarder(internalFd, 1500, externalFd)
private val pr2 = PacketForwarder(externalFd, 1500, internalFd)
private val pr1 = InternalPacketForwarder(
internalFd,
1500,
externalFd,
internalForwardMap
)
private val pr2 = ExternalPacketForwarder(
externalFd,
1500,
internalFd,
externalForwardMap
)
fun start() {
IoUtils.setBlocking(internalFd, true /* blocking */)

View file

@ -0,0 +1,212 @@
/*
* 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.Map;
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 abstract class PacketForwarderBase extends Thread {
private static final String TAG = "PacketForwarder";
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;
@NonNull
final Map<Integer, Integer> mPortRemapRules;
/**
* Construct a {@link PacketForwarderBase}.
*
* 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.
* @param portRemapRules port remap rules
*/
public PacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
@NonNull FileDescriptor dstFd,
@NonNull Map<Integer, Integer> portRemapRules) {
super(TAG);
mSrcFd = Objects.requireNonNull(srcFd);
mBuf = new byte[mtu];
mDstFd = Objects.requireNonNull(dstFd);
mPortRemapRules = Objects.requireNonNull(portRemapRules);
}
/**
* A method to prepare forwarding packets between two instances of {@link TestNetworkInterface},
* which includes ports mapping.
* Subclasses should override this method to implement the needed port remapping.
* For internal forwarder will remapped destination port,
* external forwarder will remapped source port.
* Example:
* An outgoing packet from the internal interface with
* source 1.2.3.4:1234 and destination 8.8.8.8:80
* might be translated to 8.8.8.8:1234 -> 1.2.3.4:8080 before forwarding.
* An outgoing packet from the external interface with
* source 1.2.3.4:8080 and destination 8.8.8.8:1234
* might be translated to 8.8.8.8:80 -> 1.2.3.4:1234 before forwarding.
*/
abstract void remapPort(@NonNull byte[] buf, int version);
/**
* Retrieves a potentially remapped port number from a packet.
*
* @param buf The packet data as a byte array.
* @param transportOffset The offset within the packet where the transport layer port begins.
* @return The remapped port if a mapping exists in the internal forwarding map,
* otherwise returns 0 (indicating no remapping).
*/
int getRemappedPort(@NonNull byte[] buf, int transportOffset) {
int port = PacketReflectorUtil.getPortAt(buf, transportOffset);
return mPortRemapRules.getOrDefault(port, 0);
}
int getTransportOffset(int version) {
return version == 4 ? IPV4_HEADER_LENGTH : IPV6_HEADER_LENGTH;
}
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 mDestFd for supported protocols.
* This includes:
* 1.Address Swapping: Swaps source and destination IP addresses.
* 2.Port Remapping: Remap port if necessary.
* 3.Checksum Recalculation: Updates IP and transport layer checksums to reflect changes.
*/
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 source and destination address.
PacketReflectorUtil.swapAddresses(mBuf, version);
// Remapping the port.
remapPort(mBuf, version);
// Fix IP and Transport layer checksum.
PacketReflectorUtil.fixPacketChecksum(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());
}
}

View file

@ -25,8 +25,10 @@ import java.io.IOException
* A minimal HTTP server running on a random available port.
*
* @param host The host to listen to, or null to listen on all hosts
* @param port The port to listen to, or 0 to auto select
*/
class TestHttpServer(host: String? = null) : NanoHTTPD(host, 0 /* auto-select the port */) {
class TestHttpServer
@JvmOverloads constructor(host: String? = null, port: Int = 0) : NanoHTTPD(host, port) {
// Map of URL path -> HTTP response code
private val responses = HashMap<Request, Response>()