/* * Copyright (C) 2022 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 android.net; import static android.Manifest.permission.MANAGE_TEST_NETWORKS; import static android.Manifest.permission.NETWORK_SETTINGS; import static android.Manifest.permission.TETHER_PRIVILEGED; import static android.content.pm.PackageManager.FEATURE_WIFI; 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.buildTcpPacket; import static android.net.TetheringTester.buildUdpPacket; import static android.net.TetheringTester.buildUdpPackets; import static android.net.TetheringTester.isAddressIpv4; import static android.net.TetheringTester.isExpectedFragmentIpPacket; import static android.net.TetheringTester.isExpectedIcmpPacket; import static android.net.TetheringTester.isExpectedTcpPacket; import static android.net.TetheringTester.isExpectedUdpPacket; import static com.android.net.module.util.HexDump.dumpHexString; import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT; import static com.android.net.module.util.NetworkStackConstants.TCPHDR_ACK; import static com.android.net.module.util.NetworkStackConstants.TCPHDR_SYN; import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork; import static com.android.testutils.TestPermissionUtil.runAsShell; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; import android.content.Context; import android.content.pm.PackageManager; import android.net.EthernetManager.TetheredInterfaceCallback; 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.net.cts.util.CtsNetUtils; import android.os.Handler; import android.os.HandlerThread; import android.os.SystemClock; import android.util.ArrayMap; import android.util.Log; import androidx.annotation.NonNull; import androidx.test.platform.app.InstrumentationRegistry; import com.android.net.module.util.Struct; import com.android.net.module.util.structs.FragmentHeader; import com.android.net.module.util.structs.Ipv6Header; import com.android.testutils.HandlerUtils; import com.android.testutils.PollPacketReader; import com.android.testutils.TestNetworkTracker; import org.junit.After; import org.junit.Before; import java.io.FileDescriptor; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** * TODO: Common variables or methods shared between CtsEthernetTetheringTest and * MtsEthernetTetheringTest. */ public abstract class EthernetTetheringTestBase { private static final String TAG = EthernetTetheringTestBase.class.getSimpleName(); protected static final int TIMEOUT_MS = 5000; // Used to check if any tethering interface is available. Choose 200ms to be request timeout // because the average interface requested time on cuttlefish@acloud is around 10ms. // See TetheredInterfaceRequester.getInterface, isInterfaceForTetheringAvailable. private static final int SHORT_TIMEOUT_MS = 1000; private static final int TETHER_REACHABILITY_ATTEMPTS = 20; protected static final long WAIT_RA_TIMEOUT_MS = 2000; // Address and NAT prefix definition. protected static final MacAddress TEST_MAC = MacAddress.fromString("1:2:3:4:5:6"); protected static final LinkAddress TEST_IP4_ADDR = new LinkAddress("10.0.0.1/24"); protected static final LinkAddress TEST_IP6_ADDR = new LinkAddress("2001:db8:1::101/64"); protected static final InetAddress TEST_IP4_DNS = parseNumericAddress("8.8.8.8"); protected static final InetAddress TEST_IP6_DNS = parseNumericAddress("2001:db8:1::888"); protected static final Inet4Address REMOTE_IP4_ADDR = (Inet4Address) parseNumericAddress("8.8.8.8"); protected static final Inet6Address REMOTE_IP6_ADDR = (Inet6Address) parseNumericAddress("2002:db8:1::515:ca"); // The IPv6 network address translation of REMOTE_IP4_ADDR if pref64::/n is 64:ff9b::/96. // For more information, see TetheringTester#PREF64_IPV4ONLY_ADDR, which assumes a prefix // of 64:ff9b::/96. protected static final Inet6Address REMOTE_NAT64_ADDR = (Inet6Address) parseNumericAddress("64:ff9b::808:808"); // LOCAL_PORT is 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. protected static final short LOCAL_PORT = 9876; protected static final short REMOTE_PORT = 433; // Payload definition. protected static final ByteBuffer EMPTY_PAYLOAD = ByteBuffer.wrap(new byte[0]); private static final ByteBuffer TEST_REACHABILITY_PAYLOAD = ByteBuffer.wrap(new byte[] { (byte) 0x55, (byte) 0xaa }); protected static final ByteBuffer RX_PAYLOAD = ByteBuffer.wrap(new byte[] { (byte) 0x12, (byte) 0x34 }); protected static final ByteBuffer TX_PAYLOAD = ByteBuffer.wrap(new byte[] { (byte) 0x56, (byte) 0x78 }); private static final Context sContext = InstrumentationRegistry.getInstrumentation().getContext(); protected static final EthernetManager sEm = sContext.getSystemService(EthernetManager.class); private static final TetheringManager sTm = sContext.getSystemService(TetheringManager.class); private static final PackageManager sPackageManager = sContext.getPackageManager(); private static final CtsNetUtils sCtsNetUtils = new CtsNetUtils(sContext); private static final List sCallbackErrors = Collections.synchronizedList(new ArrayList<>()); // Late initialization in setUp() private boolean mRunTests; private HandlerThread mHandlerThread; private Handler mHandler; protected TetheredInterfaceRequester mTetheredInterfaceRequester; // Late initialization in initTetheringTester(). private PollPacketReader mUpstreamReader; private TestNetworkTracker mUpstreamTracker; private TestNetworkInterface mDownstreamIface; private PollPacketReader mDownstreamReader; private MyTetheringEventCallback mTetheringEventCallback; public Context getContext() { return sContext; } @Before public void setUp() throws Exception { mHandlerThread = new HandlerThread(getClass().getSimpleName()); mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); mRunTests = isEthernetTetheringSupported(); assumeTrue(mRunTests); mTetheredInterfaceRequester = new TetheredInterfaceRequester(); sCallbackErrors.clear(); } private boolean isEthernetTetheringSupported() throws Exception { if (sEm == null) return false; return runAsShell(NETWORK_SETTINGS, TETHER_PRIVILEGED, () -> sTm.isTetheringSupported()); } protected void maybeStopTapPacketReader(final PollPacketReader tapPacketReader) throws Exception { if (tapPacketReader != null) { PollPacketReader reader = tapPacketReader; mHandler.post(() -> reader.stop()); } } protected static void maybeCloseTestInterface(final TestNetworkInterface testInterface) throws Exception { if (testInterface != null) { testInterface.getFileDescriptor().close(); Log.d(TAG, "Deleted test interface " + testInterface.getInterfaceName()); } } protected static void maybeUnregisterTetheringEventCallback( final MyTetheringEventCallback callback) throws Exception { if (callback != null) { callback.awaitInterfaceUntethered(); callback.unregister(); } } protected void stopEthernetTethering(final MyTetheringEventCallback callback) { runAsShell(TETHER_PRIVILEGED, () -> { sTm.stopTethering(TETHERING_ETHERNET); maybeUnregisterTetheringEventCallback(callback); }); } protected void cleanUp() throws Exception { setPreferTestNetworks(false); if (mUpstreamTracker != null) { runAsShell(MANAGE_TEST_NETWORKS, () -> { mUpstreamTracker.teardown(); mUpstreamTracker = null; }); } if (mUpstreamReader != null) { PollPacketReader reader = mUpstreamReader; mHandler.post(() -> reader.stop()); mUpstreamReader = null; } maybeStopTapPacketReader(mDownstreamReader); mDownstreamReader = null; // To avoid flaky which caused by the next test started but the previous interface is not // untracked from EthernetTracker yet. Just delete the test interface without explicitly // calling TetheringManager#stopTethering could let EthernetTracker untrack the test // interface from server mode before tethering stopped. Thus, awaitInterfaceUntethered // could not only make sure tethering is stopped but also guarantee the test interface is // untracked from EthernetTracker. maybeCloseTestInterface(mDownstreamIface); mDownstreamIface = null; maybeUnregisterTetheringEventCallback(mTetheringEventCallback); mTetheringEventCallback = null; setIncludeTestInterfaces(false); } @After public void tearDown() throws Exception { if (mTetheredInterfaceRequester != null) { mTetheredInterfaceRequester.release(); } try { if (mRunTests) cleanUp(); } finally { mHandlerThread.quitSafely(); mHandlerThread.join(); } if (sCallbackErrors.size() > 0) { fail("Some callbacks had errors: " + sCallbackErrors); } } protected boolean isInterfaceForTetheringAvailable() throws Exception { // If previous test case doesn't release tethering interface successfully, the other tests // after that test may be skipped as unexcepted. // TODO: figure out a better way to check default tethering interface existenion. // Use short timeout (200ms) for requesting an existing interface, if any, because // it should reurn faster than requesting a new tethering interface. Using default // timeout (5000ms, TIMEOUT_MS) may make that total testing time is over 1 minute // test module timeout on internal testing. // TODO: if this becomes flaky, consider using default timeout (5000ms) and moving // this check into #setUpOnce. return mTetheredInterfaceRequester.isPhysicalInterfaceAvailable(SHORT_TIMEOUT_MS); } protected static void setIncludeTestInterfaces(boolean include) { runAsShell(NETWORK_SETTINGS, () -> { sEm.setIncludeTestInterfaces(include); }); } protected static void setPreferTestNetworks(boolean prefer) { runAsShell(NETWORK_SETTINGS, () -> { sTm.setPreferTestNetworks(prefer); }); } protected static void waitForRouterAdvertisement(PollPacketReader reader, String iface, long timeoutMs) { final long deadline = SystemClock.uptimeMillis() + timeoutMs; do { byte[] pkt = reader.popPacket(timeoutMs); if (isExpectedIcmpPacket(pkt, true /* hasEth */, false /* isIpv4 */, ICMPV6_ROUTER_ADVERTISEMENT)) { return; } timeoutMs = deadline - SystemClock.uptimeMillis(); } while (timeoutMs > 0); fail("Did not receive router advertisement on " + iface + " after " + timeoutMs + "ms idle"); } protected static final class MyTetheringEventCallback implements TetheringEventCallback { private final CountDownLatch mTetheringStartedLatch = new CountDownLatch(1); private final CountDownLatch mTetheringStoppedLatch = new CountDownLatch(1); private final CountDownLatch mLocalOnlyStartedLatch = new CountDownLatch(1); private final CountDownLatch mLocalOnlyStoppedLatch = new CountDownLatch(1); private final CountDownLatch mClientConnectedLatch = new CountDownLatch(1); private final CountDownLatch mUpstreamLatch = new CountDownLatch(1); private final CountDownLatch mCallbackRegisteredLatch = new CountDownLatch(1); private final TetheringInterface mIface; private final Network mExpectedUpstream; private final boolean mAcceptAnyUpstream; private volatile boolean mInterfaceWasTethered = false; private volatile boolean mInterfaceWasLocalOnly = false; private volatile boolean mUnregistered = false; private volatile Collection mClients = null; private volatile Network mUpstream = null; // The dnsmasq in R might block netd for 20 seconds, which can also block tethering // enable/disable for 20 seconds. To fix this, changing the timeouts from 5 seconds to 30 // seconds. See b/289881008. private static final int EXPANDED_TIMEOUT_MS = 30000; MyTetheringEventCallback(int tetheringType, String iface) { mIface = new TetheringInterface(tetheringType, iface); mExpectedUpstream = null; mAcceptAnyUpstream = true; } MyTetheringEventCallback( int tetheringType, String iface, @NonNull Network expectedUpstream) { Objects.requireNonNull(expectedUpstream); mIface = new TetheringInterface(tetheringType, iface); mExpectedUpstream = expectedUpstream; mAcceptAnyUpstream = false; } public void unregister() { sTm.unregisterTetheringEventCallback(this); mUnregistered = true; } @Override public void onTetheredInterfacesChanged(List interfaces) { addCallbackError("Should only call callback that takes a Set"); } @Override public void onTetheredInterfacesChanged(Set interfaces) { // Ignore stale callbacks registered by previous test cases. if (mUnregistered) return; if (!mInterfaceWasTethered && interfaces.contains(mIface)) { // This interface is being tethered for the first time. Log.d(TAG, "Tethering started: " + interfaces); mInterfaceWasTethered = true; mTetheringStartedLatch.countDown(); } else if (mInterfaceWasTethered && !interfaces.contains(mIface)) { Log.d(TAG, "Tethering stopped: " + interfaces); mTetheringStoppedLatch.countDown(); } } @Override public void onLocalOnlyInterfacesChanged(List interfaces) { addCallbackError("Should only call callback that takes a Set"); } @Override public void onLocalOnlyInterfacesChanged(Set interfaces) { // Ignore stale callbacks registered by previous test cases. if (mUnregistered) return; if (!mInterfaceWasLocalOnly && interfaces.contains(mIface)) { // This interface is being put into local-only mode for the first time. Log.d(TAG, "Local-only started: " + interfaces); mInterfaceWasLocalOnly = true; mLocalOnlyStartedLatch.countDown(); } else if (mInterfaceWasLocalOnly && !interfaces.contains(mIface)) { Log.d(TAG, "Local-only stopped: " + interfaces); mLocalOnlyStoppedLatch.countDown(); } } public void awaitInterfaceTethered() throws Exception { assertTrue("Interface is not tethered after " + EXPANDED_TIMEOUT_MS + "ms", mTetheringStartedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)); } public void awaitInterfaceLocalOnly() throws Exception { assertTrue("Interface is not local-only after " + EXPANDED_TIMEOUT_MS + "ms", mLocalOnlyStartedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)); } // Used to check if the callback has registered. When the callback is registered, // onSupportedTetheringTypes is celled in onCallbackStarted(). After // onSupportedTetheringTypes called, drop the permission for registering callback. // See MyTetheringEventCallback#register, TetheringManager#onCallbackStarted. @Override public void onSupportedTetheringTypes(Set supportedTypes) { // Used to check callback registered. mCallbackRegisteredLatch.countDown(); } public void awaitCallbackRegistered() throws Exception { if (!mCallbackRegisteredLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { fail("Did not receive callback registered signal after " + EXPANDED_TIMEOUT_MS + "ms"); } } public void awaitInterfaceUntethered() throws Exception { // Don't block teardown if the interface was never tethered. // This is racy because the interface might become tethered right after this check, but // that can only happen in tearDown if startTethering timed out, which likely means // the test has already failed. if (!mInterfaceWasTethered && !mInterfaceWasLocalOnly) return; if (mInterfaceWasTethered) { assertTrue(mIface + " not untethered after " + EXPANDED_TIMEOUT_MS + "ms", mTetheringStoppedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)); } else if (mInterfaceWasLocalOnly) { assertTrue(mIface + " not untethered after " + EXPANDED_TIMEOUT_MS + "ms", mLocalOnlyStoppedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)); } else { fail(mIface + " cannot be both tethered and local-only. Update this test class."); } } @Override public void onError(String ifName, int error) { // Ignore stale callbacks registered by previous test cases. if (mUnregistered) return; addCallbackError("TetheringEventCallback got error:" + error + " on iface " + ifName); } @Override public void onClientsChanged(Collection clients) { // Ignore stale callbacks registered by previous test cases. if (mUnregistered) return; Log.d(TAG, "Got clients changed: " + clients); mClients = clients; if (clients.size() > 0) { mClientConnectedLatch.countDown(); } } public Collection awaitClientConnected() throws Exception { assertTrue("Did not receive client connected callback after " + EXPANDED_TIMEOUT_MS + "ms", mClientConnectedLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)); return mClients; } @Override public void onUpstreamChanged(Network network) { // Ignore stale callbacks registered by previous test cases. if (mUnregistered) return; Log.d(TAG, "Got upstream changed: " + network); mUpstream = network; // The callback always updates the current tethering status when it's first registered. // If the caller registers the callback before tethering starts, the null upstream // would be updated. Filtering out the null case because it's not a valid upstream that // we care about. if (mUpstream == null) return; if (mAcceptAnyUpstream || Objects.equals(mUpstream, mExpectedUpstream)) { mUpstreamLatch.countDown(); } } public Network awaitUpstreamChanged(boolean throwTimeoutException) throws Exception { if (!mUpstreamLatch.await(EXPANDED_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { final String errorMessage = "Did not receive upstream " + (mAcceptAnyUpstream ? "any" : mExpectedUpstream) + " callback after " + EXPANDED_TIMEOUT_MS + "ms"; if (throwTimeoutException) { throw new TimeoutException(errorMessage); } else { fail(errorMessage); } } return mUpstream; } } private static void addCallbackError(String error) { Log.e(TAG, error); sCallbackErrors.add(error); } protected static MyTetheringEventCallback enableTethering(String iface, TetheringRequest request, Network expectedUpstream) throws Exception { // Enable tethering with null expectedUpstream means the test accept any upstream after // tethering started. final MyTetheringEventCallback callback; if (expectedUpstream != null) { callback = new MyTetheringEventCallback( request.getTetheringType(), iface, expectedUpstream); } else { callback = new MyTetheringEventCallback(request.getTetheringType(), iface); } runAsShell(NETWORK_SETTINGS, () -> { sTm.registerTetheringEventCallback(c -> c.run() /* executor */, callback); // Need to hold the shell permission until callback is registered. This helps to avoid // the test become flaky. callback.awaitCallbackRegistered(); }); final CountDownLatch tetheringStartedLatch = new CountDownLatch(1); StartTetheringCallback startTetheringCallback = new StartTetheringCallback() { @Override public void onTetheringStarted() { Log.d(TAG, "Tethering started"); tetheringStartedLatch.countDown(); } @Override public void onTetheringFailed(int resultCode) { addCallbackError("Unexpectedly got onTetheringFailed"); } }; Log.d(TAG, "Starting tethering"); runAsShell(TETHER_PRIVILEGED, NETWORK_SETTINGS, () -> { sTm.startTethering(request, c -> c.run() /* executor */, startTetheringCallback); // Binder call is an async call. Need to hold the shell permission until tethering // started. This helps to avoid the test become flaky. if (!tetheringStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) { fail("Did not receive tethering started callback after " + TIMEOUT_MS + "ms"); } }); final int connectivityType = request.getConnectivityScope(); switch (connectivityType) { case CONNECTIVITY_SCOPE_GLOBAL: callback.awaitInterfaceTethered(); break; case CONNECTIVITY_SCOPE_LOCAL: callback.awaitInterfaceLocalOnly(); break; default: fail("Unexpected connectivity type requested: " + connectivityType); } return callback; } protected static MyTetheringEventCallback enableEthernetTethering(String iface, Network expectedUpstream) throws Exception { return enableTethering(iface, new TetheringRequest.Builder(TETHERING_ETHERNET) .setShouldShowEntitlementUi(false).build(), expectedUpstream); } protected int getMTU(TestNetworkInterface iface) throws SocketException { NetworkInterface nif = NetworkInterface.getByName(iface.getInterfaceName()); assertNotNull("Can't get NetworkInterface object for " + iface.getInterfaceName(), nif); return nif.getMTU(); } protected int getIndexByName(String ifaceName) throws SocketException { NetworkInterface nif = NetworkInterface.getByName(ifaceName); assertNotNull("Can't get NetworkInterface object for " + ifaceName, nif); return nif.getIndex(); } protected PollPacketReader makePacketReader(final TestNetworkInterface iface) throws Exception { FileDescriptor fd = iface.getFileDescriptor().getFileDescriptor(); return makePacketReader(fd, getMTU(iface)); } protected PollPacketReader makePacketReader(FileDescriptor fd, int mtu) { final PollPacketReader reader = new PollPacketReader(mHandler, fd, mtu); mHandler.post(() -> reader.start()); HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS); return reader; } protected static final class TetheredInterfaceRequester implements TetheredInterfaceCallback { private TetheredInterfaceRequest mRequest; private final CompletableFuture mFuture = new CompletableFuture<>(); TetheredInterfaceRequester() { mRequest = runAsShell(NETWORK_SETTINGS, () -> sEm.requestTetheredInterface(c -> c.run() /* executor */, this)); } @Override public void onAvailable(String iface) { Log.d(TAG, "Ethernet interface available: " + iface); mFuture.complete(iface); } @Override public void onUnavailable() { mFuture.completeExceptionally(new IllegalStateException("onUnavailable received")); } public boolean isPhysicalInterfaceAvailable(int timeout) { try { final String iface = mFuture.get(timeout, TimeUnit.MILLISECONDS); return !iface.startsWith("testtap"); } catch (Exception e) { return false; } } public String getInterface() throws Exception { return mFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); } public void release() { runAsShell(NETWORK_SETTINGS, () -> mRequest.release()); } } protected static TestNetworkInterface createTestInterface() throws Exception { TestNetworkManager tnm = runAsShell(MANAGE_TEST_NETWORKS, () -> sContext.getSystemService(TestNetworkManager.class)); TestNetworkInterface iface = runAsShell(MANAGE_TEST_NETWORKS, () -> tnm.createTapInterface()); Log.d(TAG, "Created test interface " + iface.getInterfaceName()); return iface; } protected TestNetworkTracker createTestUpstream(final List addresses, final List dnses) throws Exception { setPreferTestNetworks(true); final LinkProperties lp = new LinkProperties(); lp.setLinkAddresses(addresses); lp.setDnsServers(dnses); // TODO: initTestNetwork can take up to 15 seconds on a workstation. Investigate when and // why this is the case. It is unclear whether a 30 second timeout is enough when running // these tests in the much slower test infra. return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(sContext, lp, 30_000)); } protected void sendDownloadPacketUdp(@NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp, @NonNull final TetheringTester tester, boolean is6To4) throws Exception { if (is6To4) { assertFalse("CLAT download test must sends IPv6 packet", isAddressIpv4(srcIp, dstIp)); } // Expected received UDP packet IP protocol. While testing CLAT (is6To4 = true), the packet // on downstream must be IPv4. Otherwise, the IP protocol of test packet is the same on // both downstream and upstream. final boolean isIpv4 = is6To4 ? true : isAddressIpv4(srcIp, dstIp); final ByteBuffer testPacket = buildUdpPacket(srcIp, dstIp, REMOTE_PORT /* srcPort */, LOCAL_PORT /* dstPort */, RX_PAYLOAD); tester.verifyDownload(testPacket, p -> { Log.d(TAG, "Packet in downstream: " + dumpHexString(p)); return isExpectedUdpPacket(p, true /* hasEther */, isIpv4, RX_PAYLOAD); }); } protected void sendUploadPacketUdp(@NonNull final MacAddress srcMac, @NonNull final MacAddress dstMac, @NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp, @NonNull final TetheringTester tester, boolean is4To6) throws Exception { if (is4To6) { assertTrue("CLAT upload test must sends IPv4 packet", isAddressIpv4(srcIp, dstIp)); } // Expected received UDP packet IP protocol. While testing CLAT (is4To6 = true), the packet // on upstream must be IPv6. Otherwise, the IP protocol of test packet is the same on // both downstream and upstream. final boolean isIpv4 = is4To6 ? false : isAddressIpv4(srcIp, dstIp); final ByteBuffer testPacket = buildUdpPacket(srcMac, dstMac, srcIp, dstIp, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */, TX_PAYLOAD); tester.verifyUpload(testPacket, p -> { Log.d(TAG, "Packet in upstream: " + dumpHexString(p)); return isExpectedUdpPacket(p, false /* hasEther */, isIpv4, TX_PAYLOAD); }); } protected void sendDownloadFragmentedUdpPackets(@NonNull final Inet6Address srcIp, @NonNull final Inet6Address dstIp, @NonNull final TetheringTester tester, @NonNull final ByteBuffer payload, int l2mtu) throws Exception { final List testPackets = buildUdpPackets(null /* srcMac */, null /* dstMac */, srcIp, dstIp, REMOTE_PORT, LOCAL_PORT, payload, l2mtu); assertTrue("No packet fragmentation occurs", testPackets.size() > 1); short id = 0; final ArrayMap fragmentPayloads = new ArrayMap<>(); for (ByteBuffer testPacket : testPackets) { Struct.parse(Ipv6Header.class, testPacket); final FragmentHeader fragmentHeader = Struct.parse(FragmentHeader.class, testPacket); // Conversion of IPv6's fragmentOffset field to IPv4's flagsAndFragmentOffset field. // IPv6 Fragment Header: // '13 bits of offset in multiples of 8' + 2 zero bits + more fragment bit // 0 1 2 3 // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | Next Header | Reserved | Fragment Offset |Res|M| // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | Identification | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // IPv4 Header: // zero bit + don't frag bit + more frag bit + '13 bits of offset in multiples of 8' // 0 1 2 3 // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // |Version| IHL |Type of Service| Total Length | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | Identification |Flags| Fragment Offset | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // + . . . + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ short offset = (short) (((fragmentHeader.fragmentOffset & 0x1) << 13) | (fragmentHeader.fragmentOffset >> 3)); // RFC6145: for fragment id, copied from the low-order 16 bits in the identification // field in the Fragment Header. id = (short) (fragmentHeader.identification & 0xffff); final byte[] fragmentPayload = new byte[testPacket.remaining()]; testPacket.get(fragmentPayload); testPacket.flip(); fragmentPayloads.put(offset, ByteBuffer.wrap(fragmentPayload)); } final short fragId = id; tester.verifyDownloadBatch(testPackets, p -> { Log.d(TAG, "Packet in downstream: " + dumpHexString(p)); return isExpectedFragmentIpPacket(p, fragId, fragmentPayloads); }); } protected void sendDownloadPacketTcp(@NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp, short seq, short ack, byte tcpFlags, @NonNull final ByteBuffer payload, @NonNull final TetheringTester tester, boolean is6To4) throws Exception { if (is6To4) { assertFalse("CLAT download test must sends IPv6 packet", isAddressIpv4(srcIp, dstIp)); } // Expected received TCP packet IP protocol. While testing CLAT (is6To4 = true), the packet // on downstream must be IPv4. Otherwise, the IP protocol of test packet is the same on // both downstream and upstream. final boolean isIpv4 = is6To4 ? true : isAddressIpv4(srcIp, dstIp); final ByteBuffer testPacket = buildTcpPacket(null /* srcMac */, null /* dstMac */, srcIp, dstIp, REMOTE_PORT /* srcPort */, LOCAL_PORT /* dstPort */, seq, ack, tcpFlags, payload); tester.verifyDownload(testPacket, p -> { Log.d(TAG, "Packet in downstream: " + dumpHexString(p)); return isExpectedTcpPacket(p, true /* hasEther */, isIpv4, seq, payload); }); } protected void sendUploadPacketTcp(@NonNull final MacAddress srcMac, @NonNull final MacAddress dstMac, @NonNull final InetAddress srcIp, @NonNull final InetAddress dstIp, short seq, short ack, byte tcpFlags, @NonNull final ByteBuffer payload, @NonNull final TetheringTester tester, boolean is4To6) throws Exception { if (is4To6) { assertTrue("CLAT upload test must sends IPv4 packet", isAddressIpv4(srcIp, dstIp)); } // Expected received TCP packet IP protocol. While testing CLAT (is4To6 = true), the packet // on upstream must be IPv6. Otherwise, the IP protocol of test packet is the same on // both downstream and upstream. final boolean isIpv4 = is4To6 ? false : isAddressIpv4(srcIp, dstIp); final ByteBuffer testPacket = buildTcpPacket(srcMac, dstMac, srcIp, dstIp, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */, seq, ack, tcpFlags, payload); tester.verifyUpload(testPacket, p -> { Log.d(TAG, "Packet in upstream: " + dumpHexString(p)); return isExpectedTcpPacket(p, false /* hasEther */, isIpv4, seq, payload); }); } protected void runTcpTest( @NonNull final MacAddress uploadSrcMac, @NonNull final MacAddress uploadDstMac, @NonNull final InetAddress uploadSrcIp, @NonNull final InetAddress uploadDstIp, @NonNull final InetAddress downloadSrcIp, @NonNull final InetAddress downloadDstIp, @NonNull final TetheringTester tester, boolean isClat) throws Exception { // Three way handshake and data transfer. // // Server (base seq = 2000) Client (base seq = 1000) // | | // | [1] [SYN] SEQ = 1000 | // |<---------------------------------------------------------| - // | | ^ // | [2] [SYN + ACK] SEQ = 2000, ACK = 1000+1 | | // |--------------------------------------------------------->| three way handshake // | | | // | [3] [ACK] SEQ = 1001, ACK = 2000+1 | v // |<---------------------------------------------------------| - // | | ^ // | [4] [ACK] SEQ = 1001, ACK = 2001, 2 byte payload | | // |<---------------------------------------------------------| data transfer // | | | // | [5] [ACK] SEQ = 2001, ACK = 1001+2, 2 byte payload | v // |--------------------------------------------------------->| - // | | // // This test can only verify the packets are transferred end to end but TCP state. // TODO: verify TCP state change via /proc/net/nf_conntrack or netlink conntrack event. // [1] [UPLOAD] [SYN]: SEQ = 1000 sendUploadPacketTcp(uploadSrcMac, uploadDstMac, uploadSrcIp, uploadDstIp, (short) 1000 /* seq */, (short) 0 /* ack */, TCPHDR_SYN, EMPTY_PAYLOAD, tester, isClat /* is4To6 */); // [2] [DONWLOAD] [SYN + ACK]: SEQ = 2000, ACK = 1001 sendDownloadPacketTcp(downloadSrcIp, downloadDstIp, (short) 2000 /* seq */, (short) 1001 /* ack */, (byte) ((TCPHDR_SYN | TCPHDR_ACK) & 0xff), EMPTY_PAYLOAD, tester, isClat /* is6To4 */); // [3] [UPLOAD] [ACK]: SEQ = 1001, ACK = 2001 sendUploadPacketTcp(uploadSrcMac, uploadDstMac, uploadSrcIp, uploadDstIp, (short) 1001 /* seq */, (short) 2001 /* ack */, TCPHDR_ACK, EMPTY_PAYLOAD, tester, isClat /* is4To6 */); // [4] [UPLOAD] [ACK]: SEQ = 1001, ACK = 2001, 2 byte payload sendUploadPacketTcp(uploadSrcMac, uploadDstMac, uploadSrcIp, uploadDstIp, (short) 1001 /* seq */, (short) 2001 /* ack */, TCPHDR_ACK, TX_PAYLOAD, tester, isClat /* is4To6 */); // [5] [DONWLOAD] [ACK]: SEQ = 2001, ACK = 1003, 2 byte payload sendDownloadPacketTcp(downloadSrcIp, downloadDstIp, (short) 2001 /* seq */, (short) 1003 /* ack */, TCPHDR_ACK, RX_PAYLOAD, tester, isClat /* is6To4 */); // TODO: test BPF offload maps. } // TODO: remove ipv4 verification (is4To6 = false) once upstream connected notification race is // fixed. See #runUdp4Test. // // This function sends a probe packet to downstream interface and exam the result from upstream // interface to make sure ipv4 tethering is ready. Return the entire packet which received from // upstream interface. @NonNull protected byte[] probeV4TetheringConnectivity(TetheringTester tester, TetheredDevice tethered, boolean is4To6) throws Exception { final ByteBuffer probePacket = buildUdpPacket(tethered.macAddr, tethered.routerMacAddr, 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++) { byte[] expectedPacket = tester.testUpload(probePacket, p -> { Log.d(TAG, "Packet in upstream: " + dumpHexString(p)); // If is4To6 is true, the ipv4 probe packet would be translated to ipv6 by Clat and // would see this translated ipv6 packet in upstream interface. return isExpectedUdpPacket(p, false /* hasEther */, !is4To6 /* isIpv4 */, TEST_REACHABILITY_PAYLOAD); }); if (expectedPacket != null) return expectedPacket; } fail("Can't verify " + (is4To6 ? "ipv4 to ipv6" : "ipv4") + " tethering connectivity after " + TETHER_REACHABILITY_ATTEMPTS + " attempts"); return null; } // TODO: remove triggering upstream reselection once test network can replace selected upstream // network in Tethering module. private void maybeRetryTestedUpstreamChanged(final Network expectedUpstream, final TimeoutException fallbackException) throws Exception { // Fall back original exception because no way to reselect if there is no WIFI feature. assertTrue(fallbackException.toString(), sPackageManager.hasSystemFeature(FEATURE_WIFI)); // Try to toggle wifi network, if any, to reselect upstream network via default network // switching. Because test network has higher priority than internet network, this can // help selecting test network to be upstream network for testing. This tries to avoid // the flaky upstream selection under multinetwork environment. Internet and test network // upstream changed event order is not guaranteed. Once tethering selects non-test // upstream {wifi, ..}, test network won't be selected anymore. If too many test cases // trigger the reselection, the total test time may over test suite 1 minmute timeout. // Probably need to disable/restore all internet networks in a common place of test // process. Currently, EthernetTetheringTest is part of CTS test which needs wifi network // connection if device has wifi feature. // See Tethering#chooseUpstreamType // TODO: toggle cellular network if the device has no WIFI feature. Log.d(TAG, "Toggle WIFI to retry upstream selection"); sCtsNetUtils.disableWifi(); sCtsNetUtils.ensureWifiConnected(); // Wait for expected upstream. final CompletableFuture future = new CompletableFuture<>(); final TetheringEventCallback callback = new TetheringEventCallback() { @Override public void onUpstreamChanged(Network network) { Log.d(TAG, "Got upstream changed: " + network); if (Objects.equals(expectedUpstream, network)) { future.complete(network); } } }; try { sTm.registerTetheringEventCallback(mHandler::post, callback); assertEquals("onUpstreamChanged for unexpected network", expectedUpstream, future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)); } catch (TimeoutException e) { throw new AssertionError("Did not receive upstream " + expectedUpstream + " callback after " + TIMEOUT_MS + "ms"); } finally { sTm.unregisterTetheringEventCallback(callback); } } protected TetheringTester initTetheringTester(List upstreamAddresses, List upstreamDnses) throws Exception { assumeFalse(isInterfaceForTetheringAvailable()); // 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(upstreamAddresses, upstreamDnses); mDownstreamIface = createTestInterface(); setIncludeTestInterfaces(true); // Make sure EtherentTracker use "mDownstreamIface" as server mode interface. assertEquals("TetheredInterfaceCallback for unexpected interface", mDownstreamIface.getInterfaceName(), mTetheredInterfaceRequester.getInterface()); mTetheringEventCallback = enableEthernetTethering(mDownstreamIface.getInterfaceName(), mUpstreamTracker.getNetwork()); try { assertEquals("onUpstreamChanged for test network", mUpstreamTracker.getNetwork(), mTetheringEventCallback.awaitUpstreamChanged( true /* throwTimeoutException */)); } catch (TimeoutException e) { // Due to race condition inside tethering module, test network may not be selected as // tethering upstream. Force tethering retry upstream if possible. If it is not // possible to retry, fail the test with the original timeout exception. maybeRetryTestedUpstreamChanged(mUpstreamTracker.getNetwork(), e); } mDownstreamReader = makePacketReader(mDownstreamIface); mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface()); final ConnectivityManager cm = sContext.getSystemService(ConnectivityManager.class); // Currently tethering don't have API to tell when ipv6 tethering is available. Thus, make // sure tethering already have ipv6 connectivity before testing. if (cm.getLinkProperties(mUpstreamTracker.getNetwork()).hasGlobalIpv6Address()) { waitForRouterAdvertisement(mDownstreamReader, mDownstreamIface.getInterfaceName(), WAIT_RA_TIMEOUT_MS); } return new TetheringTester(mDownstreamReader, mUpstreamReader); } @NonNull protected Inet6Address getClatIpv6Address(TetheringTester tester, TetheredDevice tethered) throws Exception { // Send an IPv4 UDP packet from client and check that a CLAT translated IPv6 UDP packet can // be found on upstream interface. Get CLAT IPv6 address from the CLAT translated IPv6 UDP // packet. byte[] expectedPacket = probeV4TetheringConnectivity(tester, tethered, true /* is4To6 */); // Above has guaranteed that the found packet is an IPv6 packet without ether header. return Struct.parse(Ipv6Header.class, ByteBuffer.wrap(expectedPacket)).srcIp; } protected String getUpstreamInterfaceName() { if (mUpstreamTracker == null) return null; return mUpstreamTracker.getTestIface().getInterfaceName(); } protected List toList(T... array) { return Arrays.asList(array); } }