Add tethering IPv4 UDP forwarding test
This is a preparation for testing IPv4 BPF offload. - Add an ARP responder. - Add a basic UDP forwarding test. Test: atest EthernetTetheringTest Change-Id: I720a5a2c4b97493eb6a5570cecd73dfc1eabf5cd
This commit is contained in:
committed by
Nucca Chen
parent
e4ee88f108
commit
bb8e9dae7f
@@ -25,9 +25,14 @@ import static android.net.InetAddresses.parseNumericAddress;
|
||||
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
|
||||
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
|
||||
import static android.net.TetheringManager.TETHERING_ETHERNET;
|
||||
import static android.net.TetheringTester.RemoteResponder;
|
||||
import static android.system.OsConstants.IPPROTO_ICMPV6;
|
||||
import static android.system.OsConstants.IPPROTO_IP;
|
||||
import static android.system.OsConstants.IPPROTO_UDP;
|
||||
|
||||
import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA;
|
||||
import static com.android.net.module.util.HexDump.dumpHexString;
|
||||
import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4;
|
||||
import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
|
||||
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
|
||||
import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
|
||||
@@ -47,20 +52,26 @@ import android.net.EthernetManager.TetheredInterfaceRequest;
|
||||
import android.net.TetheringManager.StartTetheringCallback;
|
||||
import android.net.TetheringManager.TetheringEventCallback;
|
||||
import android.net.TetheringManager.TetheringRequest;
|
||||
import android.net.TetheringTester.TetheredDevice;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.SystemClock;
|
||||
import android.os.SystemProperties;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.filters.MediumTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.net.module.util.PacketBuilder;
|
||||
import com.android.net.module.util.Struct;
|
||||
import com.android.net.module.util.structs.EthernetHeader;
|
||||
import com.android.net.module.util.structs.Icmpv6Header;
|
||||
import com.android.net.module.util.structs.Ipv4Header;
|
||||
import com.android.net.module.util.structs.Ipv6Header;
|
||||
import com.android.net.module.util.structs.UdpHeader;
|
||||
import com.android.testutils.HandlerUtils;
|
||||
import com.android.testutils.TapPacketReader;
|
||||
import com.android.testutils.TestNetworkTracker;
|
||||
@@ -71,6 +82,7 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InterfaceAddress;
|
||||
import java.net.NetworkInterface;
|
||||
@@ -92,10 +104,13 @@ public class EthernetTetheringTest {
|
||||
|
||||
private static final String TAG = EthernetTetheringTest.class.getSimpleName();
|
||||
private static final int TIMEOUT_MS = 5000;
|
||||
private static final int TETHER_REACHABILITY_ATTEMPTS = 20;
|
||||
private static final LinkAddress TEST_IP4_ADDR = new LinkAddress("10.0.0.1/8");
|
||||
private static final LinkAddress TEST_IP6_ADDR = new LinkAddress("2001:db8:1::101/64");
|
||||
private static final InetAddress TEST_IP4_DNS = parseNumericAddress("8.8.8.8");
|
||||
private static final InetAddress TEST_IP6_DNS = parseNumericAddress("2001:db8:1::888");
|
||||
private static final ByteBuffer TEST_REACHABILITY_PAYLOAD =
|
||||
ByteBuffer.wrap(new byte[] { (byte) 0x55, (byte) 0xaa });
|
||||
|
||||
private final Context mContext = InstrumentationRegistry.getContext();
|
||||
private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
|
||||
@@ -105,6 +120,7 @@ public class EthernetTetheringTest {
|
||||
private HandlerThread mHandlerThread;
|
||||
private Handler mHandler;
|
||||
private TapPacketReader mDownstreamReader;
|
||||
private TapPacketReader mUpstreamReader;
|
||||
|
||||
private TetheredInterfaceRequester mTetheredInterfaceRequester;
|
||||
private MyTetheringEventCallback mTetheringEventCallback;
|
||||
@@ -140,6 +156,11 @@ public class EthernetTetheringTest {
|
||||
mUpstreamTracker.teardown();
|
||||
mUpstreamTracker = null;
|
||||
}
|
||||
if (mUpstreamReader != null) {
|
||||
TapPacketReader reader = mUpstreamReader;
|
||||
mHandler.post(() -> reader.stop());
|
||||
mUpstreamReader = null;
|
||||
}
|
||||
|
||||
mTm.stopTethering(TETHERING_ETHERNET);
|
||||
if (mTetheringEventCallback != null) {
|
||||
@@ -706,6 +727,168 @@ public class EthernetTetheringTest {
|
||||
// TODO: do basic forwarding test here.
|
||||
}
|
||||
|
||||
// Test network topology:
|
||||
//
|
||||
// public network (rawip) private network
|
||||
// | UE |
|
||||
// +------------+ V +------------+------------+ V +------------+
|
||||
// | Sever +---------+ Upstream | Downstream +---------+ Client |
|
||||
// +------------+ +------------+------------+ +------------+
|
||||
// remote ip public ip private ip
|
||||
// 8.8.8.8:443 <Upstream ip>:9876 <TetheredDevice ip>:9876
|
||||
//
|
||||
private static final Inet4Address REMOTE_IP4_ADDR =
|
||||
(Inet4Address) parseNumericAddress("8.8.8.8");
|
||||
// Used by public port and private port. Assume port 9876 has not been used yet before the
|
||||
// testing that public port and private port are the same in the testing. Note that NAT port
|
||||
// forwarding could be different between private port and public port.
|
||||
private static final short LOCAL_PORT = 9876;
|
||||
private static final short REMOTE_PORT = 433;
|
||||
private static final byte TYPE_OF_SERVICE = 0;
|
||||
private static final short ID = 27149;
|
||||
private static final short ID2 = 27150;
|
||||
private static final short FLAGS_AND_FRAGMENT_OFFSET = (short) 0x4000; // flags=DF, offset=0
|
||||
private static final byte TIME_TO_LIVE = (byte) 0x40;
|
||||
private static final ByteBuffer PAYLOAD =
|
||||
ByteBuffer.wrap(new byte[] { (byte) 0x12, (byte) 0x34 });
|
||||
private static final ByteBuffer PAYLOAD2 =
|
||||
ByteBuffer.wrap(new byte[] { (byte) 0x56, (byte) 0x78 });
|
||||
|
||||
private boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEther,
|
||||
@NonNull final ByteBuffer payload) {
|
||||
final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
|
||||
|
||||
if (hasEther) {
|
||||
final EthernetHeader etherHeader = Struct.parse(EthernetHeader.class, buf);
|
||||
if (etherHeader == null) return false;
|
||||
}
|
||||
|
||||
final Ipv4Header ipv4Header = Struct.parse(Ipv4Header.class, buf);
|
||||
if (ipv4Header == null) return false;
|
||||
|
||||
final UdpHeader udpHeader = Struct.parse(UdpHeader.class, buf);
|
||||
if (udpHeader == null) return false;
|
||||
|
||||
if (buf.remaining() != payload.limit()) return false;
|
||||
|
||||
return Arrays.equals(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit()),
|
||||
payload.array());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private ByteBuffer buildUdpv4Packet(@Nullable final MacAddress srcMac,
|
||||
@Nullable final MacAddress dstMac, short id,
|
||||
@NonNull final Inet4Address srcIp, @NonNull final Inet4Address dstIp,
|
||||
short srcPort, short dstPort, @Nullable final ByteBuffer payload)
|
||||
throws Exception {
|
||||
final boolean hasEther = (srcMac != null && dstMac != null);
|
||||
final int payloadLen = (payload == null) ? 0 : payload.limit();
|
||||
final ByteBuffer buffer = PacketBuilder.allocate(hasEther, IPPROTO_IP, IPPROTO_UDP,
|
||||
payloadLen);
|
||||
final PacketBuilder packetBuilder = new PacketBuilder(buffer);
|
||||
|
||||
if (hasEther) packetBuilder.writeL2Header(srcMac, dstMac, (short) ETHER_TYPE_IPV4);
|
||||
packetBuilder.writeIpv4Header(TYPE_OF_SERVICE, ID, FLAGS_AND_FRAGMENT_OFFSET,
|
||||
TIME_TO_LIVE, (byte) IPPROTO_UDP, srcIp, dstIp);
|
||||
packetBuilder.writeUdpHeader(srcPort, dstPort);
|
||||
if (payload != null) {
|
||||
buffer.put(payload);
|
||||
// in case data might be reused by caller, restore the position and
|
||||
// limit of bytebuffer.
|
||||
payload.clear();
|
||||
}
|
||||
|
||||
return packetBuilder.finalizePacket();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private ByteBuffer buildUdpv4Packet(short id, @NonNull final Inet4Address srcIp,
|
||||
@NonNull final Inet4Address dstIp, short srcPort, short dstPort,
|
||||
@Nullable final ByteBuffer payload) throws Exception {
|
||||
return buildUdpv4Packet(null /* srcMac */, null /* dstMac */, id, srcIp, dstIp, srcPort,
|
||||
dstPort, payload);
|
||||
}
|
||||
|
||||
// TODO: remove this verification once upstream connected notification race is fixed.
|
||||
// See #runUdp4Test.
|
||||
private boolean isIpv4TetherConnectivityVerified(TetheringTester tester,
|
||||
RemoteResponder remote, TetheredDevice tethered) throws Exception {
|
||||
final ByteBuffer probePacket = buildUdpv4Packet(tethered.macAddr,
|
||||
tethered.routerMacAddr, ID, tethered.ipv4Addr /* srcIp */,
|
||||
REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /*dstPort */,
|
||||
TEST_REACHABILITY_PAYLOAD);
|
||||
|
||||
// Send a UDP packet from client and check the packet can be found on upstream interface.
|
||||
for (int i = 0; i < TETHER_REACHABILITY_ATTEMPTS; i++) {
|
||||
tester.sendPacket(probePacket);
|
||||
byte[] expectedPacket = remote.getNextMatchedPacket(p -> {
|
||||
Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
|
||||
return isExpectedUdpPacket(p, false /* hasEther */, TEST_REACHABILITY_PAYLOAD);
|
||||
});
|
||||
if (expectedPacket != null) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void runUdp4Test(TetheringTester tester, RemoteResponder remote) throws Exception {
|
||||
final TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString(
|
||||
"1:2:3:4:5:6"));
|
||||
|
||||
// TODO: remove the connectivity verification for upstream connected notification race.
|
||||
// Because async upstream connected notification can't guarantee the tethering routing is
|
||||
// ready to use. Need to test tethering connectivity before testing.
|
||||
// For short term plan, consider using IPv6 RA to get MAC address because the prefix comes
|
||||
// from upstream. That can guarantee that the routing is ready. Long term plan is that
|
||||
// refactors upstream connected notification from async to sync.
|
||||
assertTrue(isIpv4TetherConnectivityVerified(tester, remote, tethered));
|
||||
|
||||
// Send a UDP packet in original direction.
|
||||
final ByteBuffer originalPacket = buildUdpv4Packet(tethered.macAddr,
|
||||
tethered.routerMacAddr, ID, tethered.ipv4Addr /* srcIp */,
|
||||
REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /*dstPort */,
|
||||
PAYLOAD /* payload */);
|
||||
tester.verifyUpload(remote, originalPacket, p -> {
|
||||
Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
|
||||
return isExpectedUdpPacket(p, false /* hasEther */, PAYLOAD);
|
||||
});
|
||||
|
||||
// Send a UDP packet in reply direction.
|
||||
final Inet4Address publicIp4Addr = (Inet4Address) TEST_IP4_ADDR.getAddress();
|
||||
final ByteBuffer replyPacket = buildUdpv4Packet(ID2, REMOTE_IP4_ADDR /* srcIp */,
|
||||
publicIp4Addr /* dstIp */, REMOTE_PORT /* srcPort */, LOCAL_PORT /*dstPort */,
|
||||
PAYLOAD2 /* payload */);
|
||||
remote.verifyDownload(tester, replyPacket, p -> {
|
||||
Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
|
||||
return isExpectedUdpPacket(p, true/* hasEther */, PAYLOAD2);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUdpV4() throws Exception {
|
||||
assumeFalse(mEm.isAvailable());
|
||||
|
||||
// MyTetheringEventCallback currently only support await first available upstream. Tethering
|
||||
// may select internet network as upstream if test network is not available and not be
|
||||
// preferred yet. Create test upstream network before enable tethering.
|
||||
mUpstreamTracker = createTestUpstream(toList(TEST_IP4_ADDR));
|
||||
|
||||
mDownstreamIface = createTestInterface();
|
||||
mEm.setIncludeTestInterfaces(true);
|
||||
|
||||
final String iface = mTetheredInterfaceRequester.getInterface();
|
||||
assertEquals("TetheredInterfaceCallback for unexpected interface",
|
||||
mDownstreamIface.getInterfaceName(), iface);
|
||||
|
||||
mTetheringEventCallback = enableEthernetTethering(mDownstreamIface.getInterfaceName());
|
||||
assertEquals("onUpstreamChanged for unexpected network", mUpstreamTracker.getNetwork(),
|
||||
mTetheringEventCallback.awaitFirstUpstreamConnected());
|
||||
|
||||
mDownstreamReader = makePacketReader(mDownstreamIface);
|
||||
mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
|
||||
|
||||
runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader));
|
||||
}
|
||||
|
||||
private <T> List<T> toList(T... array) {
|
||||
return Arrays.asList(array);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
|
||||
package android.net;
|
||||
|
||||
import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
|
||||
import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
|
||||
import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
|
||||
import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.net.dhcp.DhcpAckPacket;
|
||||
@@ -24,6 +30,9 @@ import android.net.dhcp.DhcpPacket;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.networkstack.arp.ArpPacket;
|
||||
import com.android.testutils.TapPacketReader;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
@@ -72,15 +81,17 @@ public final class TetheringTester {
|
||||
}
|
||||
|
||||
public class TetheredDevice {
|
||||
private final MacAddress mMacAddr;
|
||||
|
||||
public final Inet4Address mIpv4Addr;
|
||||
public final MacAddress macAddr;
|
||||
public final MacAddress routerMacAddr;
|
||||
public final Inet4Address ipv4Addr;
|
||||
|
||||
private TetheredDevice(MacAddress mac) throws Exception {
|
||||
mMacAddr = mac;
|
||||
macAddr = mac;
|
||||
|
||||
DhcpResults dhcpResults = runDhcp(mMacAddr.toByteArray());
|
||||
mIpv4Addr = (Inet4Address) dhcpResults.ipAddress.getAddress();
|
||||
DhcpResults dhcpResults = runDhcp(macAddr.toByteArray());
|
||||
ipv4Addr = (Inet4Address) dhcpResults.ipAddress.getAddress();
|
||||
routerMacAddr = getRouterMacAddressFromArp(ipv4Addr, macAddr,
|
||||
dhcpResults.serverAddress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,12 +157,84 @@ public final class TetheringTester {
|
||||
DhcpPacket.decodeFullPacket(packet, packet.length, DhcpPacket.ENCAP_L2);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ArpPacket parseArpPacket(final byte[] packet) {
|
||||
try {
|
||||
return ArpPacket.parseArpPacket(packet, packet.length);
|
||||
} catch (ArpPacket.ParseException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeReplyArp(byte[] packet) {
|
||||
ByteBuffer buf = ByteBuffer.wrap(packet);
|
||||
|
||||
final ArpPacket arpPacket = parseArpPacket(packet);
|
||||
if (arpPacket == null || arpPacket.opCode != ARP_REQUEST) return;
|
||||
|
||||
for (int i = 0; i < mTetheredDevices.size(); i++) {
|
||||
TetheredDevice tethered = mTetheredDevices.valueAt(i);
|
||||
if (!arpPacket.targetIp.equals(tethered.ipv4Addr)) continue;
|
||||
|
||||
final ByteBuffer arpReply = ArpPacket.buildArpPacket(
|
||||
arpPacket.senderHwAddress.toByteArray() /* dst */,
|
||||
tethered.macAddr.toByteArray() /* srcMac */,
|
||||
arpPacket.senderIp.getAddress() /* target IP */,
|
||||
arpPacket.senderHwAddress.toByteArray() /* target HW address */,
|
||||
tethered.ipv4Addr.getAddress() /* sender IP */,
|
||||
(short) ARP_REPLY);
|
||||
try {
|
||||
sendPacket(arpReply);
|
||||
} catch (Exception e) {
|
||||
fail("Failed to reply ARP for " + tethered.ipv4Addr);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private MacAddress getRouterMacAddressFromArp(final Inet4Address tetherIp,
|
||||
final MacAddress tetherMac, final Inet4Address routerIp) throws Exception {
|
||||
final ByteBuffer arpProbe = ArpPacket.buildArpPacket(ETHER_BROADCAST /* dst */,
|
||||
tetherMac.toByteArray() /* srcMac */, routerIp.getAddress() /* target IP */,
|
||||
new byte[ETHER_ADDR_LEN] /* target HW address */,
|
||||
tetherIp.getAddress() /* sender IP */, (short) ARP_REQUEST);
|
||||
sendPacket(arpProbe);
|
||||
|
||||
final byte[] packet = getNextMatchedPacket((p) -> {
|
||||
final ArpPacket arpPacket = parseArpPacket(p);
|
||||
if (arpPacket == null || arpPacket.opCode != ARP_REPLY) return false;
|
||||
return arpPacket.targetIp.equals(tetherIp);
|
||||
});
|
||||
|
||||
if (packet != null) {
|
||||
Log.d(TAG, "Get Mac address from ARP");
|
||||
final ArpPacket arpReply = ArpPacket.parseArpPacket(packet, packet.length);
|
||||
return arpReply.senderHwAddress;
|
||||
}
|
||||
|
||||
fail("Could not get ARP packet");
|
||||
return null;
|
||||
}
|
||||
|
||||
public void sendPacket(ByteBuffer packet) throws Exception {
|
||||
mDownstreamReader.sendResponse(packet);
|
||||
}
|
||||
|
||||
public byte[] getNextMatchedPacket(Predicate<byte[]> filter) {
|
||||
return mDownstreamReader.poll(PACKET_READ_TIMEOUT_MS, filter);
|
||||
byte[] packet;
|
||||
while ((packet = mDownstreamReader.poll(PACKET_READ_TIMEOUT_MS)) != null) {
|
||||
if (filter.test(packet)) return packet;
|
||||
|
||||
maybeReplyArp(packet);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void verifyUpload(final RemoteResponder dst, final ByteBuffer packet,
|
||||
final Predicate<byte[]> filter) throws Exception {
|
||||
sendPacket(packet);
|
||||
assertNotNull("Upload fail", dst.getNextMatchedPacket(filter));
|
||||
}
|
||||
|
||||
public static class RemoteResponder {
|
||||
@@ -167,5 +250,11 @@ public final class TetheringTester {
|
||||
public byte[] getNextMatchedPacket(Predicate<byte[]> filter) throws Exception {
|
||||
return mUpstreamReader.poll(PACKET_READ_TIMEOUT_MS, filter);
|
||||
}
|
||||
|
||||
public void verifyDownload(final TetheringTester dst, final ByteBuffer packet,
|
||||
final Predicate<byte[]> filter) throws Exception {
|
||||
sendPacket(packet);
|
||||
assertNotNull("Download fail", dst.getNextMatchedPacket(filter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user