Merge "Add tethering icmpv6 forwarding test" am: 86a800f911
Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/1817920 Change-Id: I1e50559c47fa1ed32207b72d0be9cad00887381e Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -27,6 +27,7 @@ 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.net.TetheringTester.isIcmpv6Type;
|
||||
import static android.system.OsConstants.IPPROTO_ICMPV6;
|
||||
import static android.system.OsConstants.IPPROTO_IP;
|
||||
import static android.system.OsConstants.IPPROTO_UDP;
|
||||
@@ -35,6 +36,8 @@ 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_ECHO_REPLY_TYPE;
|
||||
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
|
||||
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
|
||||
import static com.android.testutils.DeviceInfoUtils.KVersion;
|
||||
import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
|
||||
@@ -73,6 +76,7 @@ import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.filters.MediumTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.net.module.util.Ipv6Utils;
|
||||
import com.android.net.module.util.PacketBuilder;
|
||||
import com.android.net.module.util.Struct;
|
||||
import com.android.net.module.util.bpf.Tether4Key;
|
||||
@@ -101,6 +105,7 @@ import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InterfaceAddress;
|
||||
import java.net.NetworkInterface;
|
||||
@@ -781,21 +786,26 @@ public class EthernetTetheringTest {
|
||||
}
|
||||
}
|
||||
|
||||
private TestNetworkTracker createTestUpstream(final List<LinkAddress> addresses)
|
||||
throws Exception {
|
||||
private TestNetworkTracker createTestUpstream(final List<LinkAddress> addresses,
|
||||
final List<InetAddress> dnses) throws Exception {
|
||||
mTm.setPreferTestNetworks(true);
|
||||
|
||||
return initTestNetwork(mContext, addresses, TIMEOUT_MS);
|
||||
final LinkProperties lp = new LinkProperties();
|
||||
lp.setLinkAddresses(addresses);
|
||||
lp.setDnsServers(dnses);
|
||||
|
||||
return initTestNetwork(mContext, lp, TIMEOUT_MS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestNetworkUpstream() throws Exception {
|
||||
public void testIcmpv6Echo() 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, TEST_IP6_ADDR));
|
||||
mUpstreamTracker = createTestUpstream(toList(TEST_IP4_ADDR, TEST_IP6_ADDR),
|
||||
toList(TEST_IP4_DNS, TEST_IP6_DNS));
|
||||
|
||||
mDownstreamIface = createTestInterface();
|
||||
mEm.setIncludeTestInterfaces(true);
|
||||
@@ -810,7 +820,39 @@ public class EthernetTetheringTest {
|
||||
mTetheringEventCallback.awaitUpstreamChanged());
|
||||
|
||||
mDownstreamReader = makePacketReader(mDownstreamIface);
|
||||
// TODO: do basic forwarding test here.
|
||||
mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
|
||||
|
||||
runPing6Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader));
|
||||
}
|
||||
|
||||
private void runPing6Test(TetheringTester tester, RemoteResponder remote) throws Exception {
|
||||
// Currently tethering don't have API to tell when ipv6 tethering is available. Thus, let
|
||||
// TetheringTester test ipv6 tethering connectivity before testing ipv6.
|
||||
tester.waitForIpv6TetherConnectivityVerified();
|
||||
|
||||
TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString("1:2:3:4:5:6"),
|
||||
true /* hasIpv6 */);
|
||||
Inet6Address remoteIp6Addr = (Inet6Address) parseNumericAddress("2400:222:222::222");
|
||||
ByteBuffer request = Ipv6Utils.buildEchoRequestPacket(tethered.macAddr,
|
||||
tethered.routerMacAddr, tethered.ipv6Addr, remoteIp6Addr);
|
||||
tester.sendPacket(request);
|
||||
|
||||
final byte[] echoRequest = remote.getNextMatchedPacket((p) -> {
|
||||
Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
|
||||
|
||||
return isIcmpv6Type(p, false /* hasEth */, ICMPV6_ECHO_REQUEST_TYPE);
|
||||
});
|
||||
assertNotNull("No icmpv6 echo request in upstream", echoRequest);
|
||||
|
||||
ByteBuffer reply = Ipv6Utils.buildEchoReplyPacket(remoteIp6Addr, tethered.ipv6Addr);
|
||||
remote.sendPacket(reply);
|
||||
|
||||
final byte[] echoReply = tester.getNextMatchedPacket((p) -> {
|
||||
Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
|
||||
|
||||
return isIcmpv6Type(p, true /* hasEth */, ICMPV6_ECHO_REPLY_TYPE);
|
||||
});
|
||||
assertNotNull("No icmpv6 echo reply in downstream", echoReply);
|
||||
}
|
||||
|
||||
// Test network topology:
|
||||
@@ -922,7 +964,7 @@ public class EthernetTetheringTest {
|
||||
private void runUdp4Test(TetheringTester tester, RemoteResponder remote, boolean usingBpf)
|
||||
throws Exception {
|
||||
final TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString(
|
||||
"1:2:3:4:5:6"));
|
||||
"1:2:3:4:5:6"), false /* hasIpv6 */);
|
||||
|
||||
// TODO: remove the connectivity verification for upstream connected notification race.
|
||||
// Because async upstream connected notification can't guarantee the tethering routing is
|
||||
@@ -1043,7 +1085,7 @@ public class EthernetTetheringTest {
|
||||
// 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));
|
||||
mUpstreamTracker = createTestUpstream(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS));
|
||||
|
||||
mDownstreamIface = createTestInterface();
|
||||
mEm.setIncludeTestInterfaces(true);
|
||||
|
||||
@@ -16,10 +16,22 @@
|
||||
|
||||
package android.net;
|
||||
|
||||
import static android.net.InetAddresses.parseNumericAddress;
|
||||
import static android.system.OsConstants.IPPROTO_ICMPV6;
|
||||
|
||||
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 com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
|
||||
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
|
||||
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
|
||||
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA;
|
||||
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION;
|
||||
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
|
||||
import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
|
||||
import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
|
||||
import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.fail;
|
||||
@@ -32,11 +44,24 @@ import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.net.module.util.Ipv6Utils;
|
||||
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.Ipv6Header;
|
||||
import com.android.net.module.util.structs.LlaOption;
|
||||
import com.android.net.module.util.structs.NsHeader;
|
||||
import com.android.net.module.util.structs.PrefixInformationOption;
|
||||
import com.android.net.module.util.structs.RaHeader;
|
||||
import com.android.networkstack.arp.ArpPacket;
|
||||
import com.android.testutils.TapPacketReader;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Predicate;
|
||||
@@ -50,12 +75,14 @@ public final class TetheringTester {
|
||||
private static final String TAG = TetheringTester.class.getSimpleName();
|
||||
private static final int PACKET_READ_TIMEOUT_MS = 100;
|
||||
private static final int DHCP_DISCOVER_ATTEMPTS = 10;
|
||||
private static final int READ_RA_ATTEMPTS = 10;
|
||||
private static final byte[] DHCP_REQUESTED_PARAMS = new byte[] {
|
||||
DhcpPacket.DHCP_SUBNET_MASK,
|
||||
DhcpPacket.DHCP_ROUTER,
|
||||
DhcpPacket.DHCP_DNS_SERVER,
|
||||
DhcpPacket.DHCP_LEASE_TIME,
|
||||
};
|
||||
private static final InetAddress LINK_LOCAL = parseNumericAddress("fe80::1");
|
||||
|
||||
public static final String DHCP_HOSTNAME = "testhostname";
|
||||
|
||||
@@ -69,12 +96,13 @@ public final class TetheringTester {
|
||||
mTetheredDevices = new ArrayMap<>();
|
||||
}
|
||||
|
||||
public TetheredDevice createTetheredDevice(MacAddress macAddr) throws Exception {
|
||||
public TetheredDevice createTetheredDevice(MacAddress macAddr, boolean hasIpv6)
|
||||
throws Exception {
|
||||
if (mTetheredDevices.get(macAddr) != null) {
|
||||
fail("Tethered device already created");
|
||||
}
|
||||
|
||||
TetheredDevice tethered = new TetheredDevice(macAddr);
|
||||
TetheredDevice tethered = new TetheredDevice(macAddr, hasIpv6);
|
||||
mTetheredDevices.put(macAddr, tethered);
|
||||
|
||||
return tethered;
|
||||
@@ -84,14 +112,15 @@ public final class TetheringTester {
|
||||
public final MacAddress macAddr;
|
||||
public final MacAddress routerMacAddr;
|
||||
public final Inet4Address ipv4Addr;
|
||||
public final Inet6Address ipv6Addr;
|
||||
|
||||
private TetheredDevice(MacAddress mac) throws Exception {
|
||||
private TetheredDevice(MacAddress mac, boolean hasIpv6) throws Exception {
|
||||
macAddr = mac;
|
||||
|
||||
DhcpResults dhcpResults = runDhcp(macAddr.toByteArray());
|
||||
ipv4Addr = (Inet4Address) dhcpResults.ipAddress.getAddress();
|
||||
routerMacAddr = getRouterMacAddressFromArp(ipv4Addr, macAddr,
|
||||
dhcpResults.serverAddress);
|
||||
ipv6Addr = hasIpv6 ? runSlaac(macAddr, routerMacAddr) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +245,141 @@ public final class TetheringTester {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void waitForIpv6TetherConnectivityVerified() throws Exception {
|
||||
Log.d(TAG, "Waiting RA multicast");
|
||||
|
||||
// Wait for RA multicast message from router to confirm that the IPv6 tethering
|
||||
// connectivity is ready. We don't extract the router mac address from RA because
|
||||
// we get the router mac address from IPv4 ARP packet. See #getRouterMacAddressFromArp.
|
||||
for (int i = 0; i < READ_RA_ATTEMPTS; i++) {
|
||||
final byte[] raPacket = getNextMatchedPacket((p) -> {
|
||||
return isIcmpv6Type(p, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT);
|
||||
});
|
||||
if (raPacket != null) return;
|
||||
}
|
||||
|
||||
fail("Could not get RA multicast packet after " + READ_RA_ATTEMPTS + " attempts");
|
||||
}
|
||||
|
||||
private List<PrefixInformationOption> getRaPrefixOptions(byte[] packet) {
|
||||
ByteBuffer buf = ByteBuffer.wrap(packet);
|
||||
if (!isIcmpv6Type(buf, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT)) return null;
|
||||
|
||||
Struct.parse(RaHeader.class, buf);
|
||||
final ArrayList<PrefixInformationOption> pioList = new ArrayList<>();
|
||||
while (buf.position() < packet.length) {
|
||||
final int currentPos = buf.position();
|
||||
final int type = Byte.toUnsignedInt(buf.get());
|
||||
final int length = Byte.toUnsignedInt(buf.get());
|
||||
if (type == ICMPV6_ND_OPTION_PIO) {
|
||||
final ByteBuffer pioBuf = ByteBuffer.wrap(buf.array(), currentPos,
|
||||
Struct.getSize(PrefixInformationOption.class));
|
||||
final PrefixInformationOption pio =
|
||||
Struct.parse(PrefixInformationOption.class, pioBuf);
|
||||
pioList.add(pio);
|
||||
|
||||
// Move ByteBuffer position to the next option.
|
||||
buf.position(currentPos + Struct.getSize(PrefixInformationOption.class));
|
||||
} else {
|
||||
buf.position(currentPos + (length * 8));
|
||||
}
|
||||
}
|
||||
return pioList;
|
||||
}
|
||||
|
||||
private Inet6Address runSlaac(MacAddress srcMac, MacAddress dstMac) throws Exception {
|
||||
sendRsPacket(srcMac, dstMac);
|
||||
|
||||
final byte[] raPacket = getNextMatchedPacket((p) -> {
|
||||
return isIcmpv6Type(p, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT);
|
||||
});
|
||||
|
||||
if (raPacket == null) {
|
||||
fail("Could not get ra for prefix options");
|
||||
}
|
||||
final List<PrefixInformationOption> options = getRaPrefixOptions(raPacket);
|
||||
|
||||
for (PrefixInformationOption pio : options) {
|
||||
if (pio.validLifetime > 0) {
|
||||
final byte[] addressBytes = pio.prefix;
|
||||
// Random the last two bytes as suffix.
|
||||
// TODO: Currently do not implmement DAD in the test. Rely the gateway ipv6 address
|
||||
// genetrated by tethering module always has random the last byte.
|
||||
addressBytes[addressBytes.length - 1] = (byte) (new Random()).nextInt();
|
||||
addressBytes[addressBytes.length - 2] = (byte) (new Random()).nextInt();
|
||||
|
||||
return (Inet6Address) InetAddress.getByAddress(addressBytes);
|
||||
}
|
||||
}
|
||||
|
||||
fail("Could not get ipv6 address");
|
||||
return null;
|
||||
}
|
||||
|
||||
private void sendRsPacket(MacAddress srcMac, MacAddress dstMac) throws Exception {
|
||||
Log.d(TAG, "Sending RS");
|
||||
ByteBuffer slla = LlaOption.build((byte) ICMPV6_ND_OPTION_SLLA, srcMac);
|
||||
ByteBuffer rs = Ipv6Utils.buildRsPacket(srcMac, dstMac, (Inet6Address) LINK_LOCAL,
|
||||
IPV6_ADDR_ALL_NODES_MULTICAST, slla);
|
||||
|
||||
sendPacket(rs);
|
||||
}
|
||||
|
||||
private void maybeReplyNa(byte[] packet) {
|
||||
ByteBuffer buf = ByteBuffer.wrap(packet);
|
||||
final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
|
||||
if (ethHdr.etherType != ETHER_TYPE_IPV6) return;
|
||||
|
||||
final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
|
||||
if (ipv6Hdr.nextHeader != (byte) IPPROTO_ICMPV6) return;
|
||||
|
||||
final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf);
|
||||
if (icmpv6Hdr.type != (short) ICMPV6_NEIGHBOR_SOLICITATION) return;
|
||||
|
||||
final NsHeader nsHdr = Struct.parse(NsHeader.class, buf);
|
||||
for (int i = 0; i < mTetheredDevices.size(); i++) {
|
||||
TetheredDevice tethered = mTetheredDevices.valueAt(i);
|
||||
if (!nsHdr.target.equals(tethered.ipv6Addr)) continue;
|
||||
|
||||
final ByteBuffer tlla = LlaOption.build((byte) ICMPV6_ND_OPTION_TLLA, tethered.macAddr);
|
||||
int flags = NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED
|
||||
| NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
|
||||
ByteBuffer ns = Ipv6Utils.buildNaPacket(tethered.macAddr, tethered.routerMacAddr,
|
||||
nsHdr.target, ipv6Hdr.srcIp, flags, nsHdr.target, tlla);
|
||||
try {
|
||||
sendPacket(ns);
|
||||
} catch (Exception e) {
|
||||
fail("Failed to reply NA for " + tethered.ipv6Addr);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isIcmpv6Type(byte[] packet, boolean hasEth, int type) {
|
||||
final ByteBuffer buf = ByteBuffer.wrap(packet);
|
||||
return isIcmpv6Type(buf, hasEth, type);
|
||||
}
|
||||
|
||||
private static boolean isIcmpv6Type(ByteBuffer buf, boolean hasEth, int type) {
|
||||
try {
|
||||
if (hasEth) {
|
||||
final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
|
||||
if (ethHdr.etherType != ETHER_TYPE_IPV6) return false;
|
||||
}
|
||||
|
||||
final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
|
||||
if (ipv6Hdr.nextHeader != (byte) IPPROTO_ICMPV6) return false;
|
||||
|
||||
final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf);
|
||||
return icmpv6Hdr.type == (short) type;
|
||||
} catch (Exception e) {
|
||||
// Parsing packet fail means it is not icmpv6 packet.
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void sendPacket(ByteBuffer packet) throws Exception {
|
||||
mDownstreamReader.sendResponse(packet);
|
||||
}
|
||||
@@ -226,6 +390,7 @@ public final class TetheringTester {
|
||||
if (filter.test(packet)) return packet;
|
||||
|
||||
maybeReplyArp(packet);
|
||||
maybeReplyNa(packet);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user