diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java index 5869f2b7db..b3d2ba6cfb 100644 --- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java +++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java @@ -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 addresses) - throws Exception { + private TestNetworkTracker createTestUpstream(final List addresses, + final List 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); diff --git a/Tethering/tests/integration/src/android/net/TetheringTester.java b/Tethering/tests/integration/src/android/net/TetheringTester.java index d24661acbf..458680ac36 100644 --- a/Tethering/tests/integration/src/android/net/TetheringTester.java +++ b/Tethering/tests/integration/src/android/net/TetheringTester.java @@ -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 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 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 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;