Allow callers of startTethering to choose local-only mode.

This is useful for OEMs that want to use RNDIS or NCM as a
local-only link that is directly connected to some other host.
This can be used to implement USB tethering using NCM, which
currently only supports local-only mode.

Bug: 175090447
Test: TetheringIntegrationTests:EthernetTetheringTest#testLocalOnlyTethering
Change-Id: I0ffaa46e4640e5b235340a15d25909106ceb0c07
This commit is contained in:
Lorenzo Colitti
2021-04-13 17:17:44 +09:00
parent f7d188c8c9
commit 8a36c297bc
11 changed files with 277 additions and 29 deletions

View File

@@ -19,7 +19,14 @@ 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.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
import static android.net.TetheringManager.TETHERING_ETHERNET;
import static android.system.OsConstants.IPPROTO_ICMPV6;
import static com.android.net.module.util.ConnectivityUtils.isIPv6ULA;
import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -50,6 +57,10 @@ import androidx.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;
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.testutils.HandlerUtils;
import com.android.testutils.TapPacketReader;
@@ -60,6 +71,7 @@ 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;
import java.net.SocketException;
@@ -229,6 +241,82 @@ public class EthernetTetheringTest {
}
private static boolean isRouterAdvertisement(byte[] pkt) {
if (pkt == null) return false;
ByteBuffer buf = ByteBuffer.wrap(pkt);
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) ICMPV6_ROUTER_ADVERTISEMENT;
}
private static void expectRouterAdvertisement(TapPacketReader reader, String iface,
long timeoutMs) {
final long deadline = SystemClock.uptimeMillis() + timeoutMs;
do {
byte[] pkt = reader.popPacket(timeoutMs);
if (isRouterAdvertisement(pkt)) return;
timeoutMs = deadline - SystemClock.uptimeMillis();
} while (timeoutMs > 0);
fail("Did not receive router advertisement on " + iface + " after "
+ timeoutMs + "ms idle");
}
private static void expectLocalOnlyAddresses(String iface) throws Exception {
final List<InterfaceAddress> interfaceAddresses =
NetworkInterface.getByName(iface).getInterfaceAddresses();
boolean foundIpv6Ula = false;
for (InterfaceAddress ia : interfaceAddresses) {
final InetAddress addr = ia.getAddress();
if (isIPv6ULA(addr)) {
foundIpv6Ula = true;
}
final int prefixlen = ia.getNetworkPrefixLength();
final LinkAddress la = new LinkAddress(addr, prefixlen);
if (la.isIpv6() && la.isGlobalPreferred()) {
fail("Found global IPv6 address on local-only interface: " + interfaceAddresses);
}
}
assertTrue("Did not find IPv6 ULA on local-only interface " + iface,
foundIpv6Ula);
}
@Test
public void testLocalOnlyTethering() throws Exception {
assumeFalse(mEm.isAvailable());
mEm.setIncludeTestInterfaces(true);
mTestIface = createTestInterface();
final String iface = mTetheredInterfaceRequester.getInterface();
assertEquals("TetheredInterfaceCallback for unexpected interface",
mTestIface.getInterfaceName(), iface);
final TetheringRequest request = new TetheringRequest.Builder(TETHERING_ETHERNET)
.setConnectivityScope(CONNECTIVITY_SCOPE_LOCAL).build();
mTetheringEventCallback = enableEthernetTethering(iface, request);
mTetheringEventCallback.awaitInterfaceLocalOnly();
// makePacketReader only works after tethering is started, because until then the interface
// does not have an IP address, and unprivileged apps cannot see interfaces without IP
// addresses. This shouldn't be flaky because the TAP interface will buffer all packets even
// before the reader is started.
FileDescriptor fd = mTestIface.getFileDescriptor().getFileDescriptor();
mTapPacketReader = makePacketReader(fd, getMTU(mTestIface));
expectRouterAdvertisement(mTapPacketReader, iface, 2000 /* timeoutMs */);
expectLocalOnlyAddresses(iface);
}
private boolean isAdbOverNetwork() {
// If adb TCP port opened, this test may running by adb over network.
return (SystemProperties.getInt("persist.adb.tcp.port", -1) > -1)
@@ -257,10 +345,13 @@ public class EthernetTetheringTest {
private final TetheringManager mTm;
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 String mIface;
private volatile boolean mInterfaceWasTethered = false;
private volatile boolean mInterfaceWasLocalOnly = false;
private volatile boolean mUnregistered = false;
private volatile Collection<TetheredClient> mClients = null;
@@ -279,7 +370,6 @@ public class EthernetTetheringTest {
// Ignore stale callbacks registered by previous test cases.
if (mUnregistered) return;
final boolean wasTethered = mTetheringStartedLatch.getCount() == 0;
if (!mInterfaceWasTethered && (mIface == null || interfaces.contains(mIface))) {
// This interface is being tethered for the first time.
Log.d(TAG, "Tethering started: " + interfaces);
@@ -291,20 +381,48 @@ public class EthernetTetheringTest {
}
}
@Override
public void onLocalOnlyInterfacesChanged(List<String> interfaces) {
// Ignore stale callbacks registered by previous test cases.
if (mUnregistered) return;
if (!mInterfaceWasLocalOnly && (mIface == null || 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("Ethernet not tethered after " + TIMEOUT_MS + "ms",
mTetheringStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
public void awaitInterfaceLocalOnly() throws Exception {
assertTrue("Ethernet not local-only after " + TIMEOUT_MS + "ms",
mLocalOnlyStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
}
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) return;
if (!mInterfaceWasTethered && !mInterfaceWasLocalOnly) return;
assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms",
mTetheringStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
if (mInterfaceWasTethered) {
assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms",
mTetheringStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} else if (mInterfaceWasLocalOnly) {
assertTrue(mIface + " not untethered after " + TIMEOUT_MS + "ms",
mLocalOnlyStoppedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} else {
fail(mIface + " cannot be both tethered and local-only. Update this test class.");
}
}
@Override
@@ -347,7 +465,19 @@ public class EthernetTetheringTest {
};
Log.d(TAG, "Starting Ethernet tethering");
mTm.startTethering(request, mHandler::post /* executor */, startTetheringCallback);
callback.awaitInterfaceTethered();
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;
}
@@ -444,7 +574,6 @@ public class EthernetTetheringTest {
}
private static final class TetheredInterfaceRequester implements TetheredInterfaceCallback {
private final CountDownLatch mInterfaceAvailableLatch = new CountDownLatch(1);
private final Handler mHandler;
private final EthernetManager mEm;