Merge "Add a test for starting the legacy VPN." am: 869d4f597d am: 6926df6314 am: 4136a41652 am: 4d64450516

Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/1372896

Change-Id: I82638711fc39d6d29d618d7a1732351dbbaf780b
This commit is contained in:
Chalard Jean
2020-08-06 16:54:00 +00:00
committed by Automerger Merge Worker
2 changed files with 290 additions and 62 deletions

View File

@@ -123,6 +123,7 @@ import java.math.BigInteger;
import java.net.Inet4Address; import java.net.Inet4Address;
import java.net.Inet6Address; import java.net.Inet6Address;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.ArrayList; import java.util.ArrayList;
@@ -190,6 +191,7 @@ public class Vpn {
// automated reconnection // automated reconnection
private final Context mContext; private final Context mContext;
@VisibleForTesting final Dependencies mDeps;
private final NetworkInfo mNetworkInfo; private final NetworkInfo mNetworkInfo;
@VisibleForTesting protected String mPackage; @VisibleForTesting protected String mPackage;
private int mOwnerUID; private int mOwnerUID;
@@ -252,17 +254,106 @@ public class Vpn {
// Handle of the user initiating VPN. // Handle of the user initiating VPN.
private final int mUserHandle; private final int mUserHandle;
interface RetryScheduler {
void checkInterruptAndDelay(boolean sleepLonger) throws InterruptedException;
}
static class Dependencies {
public void startService(final String serviceName) {
SystemService.start(serviceName);
}
public void stopService(final String serviceName) {
SystemService.stop(serviceName);
}
public boolean isServiceRunning(final String serviceName) {
return SystemService.isRunning(serviceName);
}
public boolean isServiceStopped(final String serviceName) {
return SystemService.isStopped(serviceName);
}
public File getStateFile() {
return new File("/data/misc/vpn/state");
}
public void sendArgumentsToDaemon(
final String daemon, final LocalSocket socket, final String[] arguments,
final RetryScheduler retryScheduler) throws IOException, InterruptedException {
final LocalSocketAddress address = new LocalSocketAddress(
daemon, LocalSocketAddress.Namespace.RESERVED);
// Wait for the socket to connect.
while (true) {
try {
socket.connect(address);
break;
} catch (Exception e) {
// ignore
}
retryScheduler.checkInterruptAndDelay(true /* sleepLonger */);
}
socket.setSoTimeout(500);
final OutputStream out = socket.getOutputStream();
for (String argument : arguments) {
byte[] bytes = argument.getBytes(StandardCharsets.UTF_8);
if (bytes.length >= 0xFFFF) {
throw new IllegalArgumentException("Argument is too large");
}
out.write(bytes.length >> 8);
out.write(bytes.length);
out.write(bytes);
retryScheduler.checkInterruptAndDelay(false /* sleepLonger */);
}
out.write(0xFF);
out.write(0xFF);
// Wait for End-of-File.
final InputStream in = socket.getInputStream();
while (true) {
try {
if (in.read() == -1) {
break;
}
} catch (Exception e) {
// ignore
}
retryScheduler.checkInterruptAndDelay(true /* sleepLonger */);
}
}
// TODO : implement and use this.
@NonNull
public InetAddress resolve(final String endpoint) throws UnknownHostException {
try {
return InetAddress.parseNumericAddress(endpoint);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Endpoint is not numeric");
}
throw new UnknownHostException(endpoint);
}
public boolean checkInterfacePresent(final Vpn vpn, final String iface) {
return vpn.jniCheck(iface) == 0;
}
}
public Vpn(Looper looper, Context context, INetworkManagementService netService, public Vpn(Looper looper, Context context, INetworkManagementService netService,
@UserIdInt int userHandle, @NonNull KeyStore keyStore) { @UserIdInt int userHandle, @NonNull KeyStore keyStore) {
this(looper, context, netService, userHandle, keyStore, this(looper, context, new Dependencies(), netService, userHandle, keyStore,
new SystemServices(context), new Ikev2SessionCreator()); new SystemServices(context), new Ikev2SessionCreator());
} }
@VisibleForTesting @VisibleForTesting
protected Vpn(Looper looper, Context context, INetworkManagementService netService, protected Vpn(Looper looper, Context context, Dependencies deps,
INetworkManagementService netService,
int userHandle, @NonNull KeyStore keyStore, SystemServices systemServices, int userHandle, @NonNull KeyStore keyStore, SystemServices systemServices,
Ikev2SessionCreator ikev2SessionCreator) { Ikev2SessionCreator ikev2SessionCreator) {
mContext = context; mContext = context;
mDeps = deps;
mNetd = netService; mNetd = netService;
mUserHandle = userHandle; mUserHandle = userHandle;
mLooper = looper; mLooper = looper;
@@ -2129,7 +2220,8 @@ public class Vpn {
} }
/** This class represents the common interface for all VPN runners. */ /** This class represents the common interface for all VPN runners. */
private abstract class VpnRunner extends Thread { @VisibleForTesting
abstract class VpnRunner extends Thread {
protected VpnRunner(String name) { protected VpnRunner(String name) {
super(name); super(name);
@@ -2638,7 +2730,7 @@ public class Vpn {
} catch (InterruptedException e) { } catch (InterruptedException e) {
} }
for (String daemon : mDaemons) { for (String daemon : mDaemons) {
SystemService.stop(daemon); mDeps.stopService(daemon);
} }
} }
agentDisconnect(); agentDisconnect();
@@ -2663,13 +2755,13 @@ public class Vpn {
// Wait for the daemons to stop. // Wait for the daemons to stop.
for (String daemon : mDaemons) { for (String daemon : mDaemons) {
while (!SystemService.isStopped(daemon)) { while (!mDeps.isServiceStopped(daemon)) {
checkInterruptAndDelay(true); checkInterruptAndDelay(true);
} }
} }
// Clear the previous state. // Clear the previous state.
File state = new File("/data/misc/vpn/state"); final File state = mDeps.getStateFile();
state.delete(); state.delete();
if (state.exists()) { if (state.exists()) {
throw new IllegalStateException("Cannot delete the state"); throw new IllegalStateException("Cannot delete the state");
@@ -2696,57 +2788,19 @@ public class Vpn {
// Start the daemon. // Start the daemon.
String daemon = mDaemons[i]; String daemon = mDaemons[i];
SystemService.start(daemon); mDeps.startService(daemon);
// Wait for the daemon to start. // Wait for the daemon to start.
while (!SystemService.isRunning(daemon)) { while (!mDeps.isServiceRunning(daemon)) {
checkInterruptAndDelay(true); checkInterruptAndDelay(true);
} }
// Create the control socket. // Create the control socket.
mSockets[i] = new LocalSocket(); mSockets[i] = new LocalSocket();
LocalSocketAddress address = new LocalSocketAddress(
daemon, LocalSocketAddress.Namespace.RESERVED);
// Wait for the socket to connect. // Wait for the socket to connect and send over the arguments.
while (true) { mDeps.sendArgumentsToDaemon(daemon, mSockets[i], arguments,
try { this::checkInterruptAndDelay);
mSockets[i].connect(address);
break;
} catch (Exception e) {
// ignore
}
checkInterruptAndDelay(true);
}
mSockets[i].setSoTimeout(500);
// Send over the arguments.
OutputStream out = mSockets[i].getOutputStream();
for (String argument : arguments) {
byte[] bytes = argument.getBytes(StandardCharsets.UTF_8);
if (bytes.length >= 0xFFFF) {
throw new IllegalArgumentException("Argument is too large");
}
out.write(bytes.length >> 8);
out.write(bytes.length);
out.write(bytes);
checkInterruptAndDelay(false);
}
out.write(0xFF);
out.write(0xFF);
// Wait for End-of-File.
InputStream in = mSockets[i].getInputStream();
while (true) {
try {
if (in.read() == -1) {
break;
}
} catch (Exception e) {
// ignore
}
checkInterruptAndDelay(true);
}
} }
// Wait for the daemons to create the new state. // Wait for the daemons to create the new state.
@@ -2754,7 +2808,7 @@ public class Vpn {
// Check if a running daemon is dead. // Check if a running daemon is dead.
for (int i = 0; i < mDaemons.length; ++i) { for (int i = 0; i < mDaemons.length; ++i) {
String daemon = mDaemons[i]; String daemon = mDaemons[i];
if (mArguments[i] != null && !SystemService.isRunning(daemon)) { if (mArguments[i] != null && !mDeps.isServiceRunning(daemon)) {
throw new IllegalStateException(daemon + " is dead"); throw new IllegalStateException(daemon + " is dead");
} }
} }
@@ -2764,7 +2818,8 @@ public class Vpn {
// Now we are connected. Read and parse the new state. // Now we are connected. Read and parse the new state.
String[] parameters = FileUtils.readTextFile(state, 0, null).split("\n", -1); String[] parameters = FileUtils.readTextFile(state, 0, null).split("\n", -1);
if (parameters.length != 7) { if (parameters.length != 7) {
throw new IllegalStateException("Cannot parse the state"); throw new IllegalStateException("Cannot parse the state: '"
+ String.join("', '", parameters) + "'");
} }
// Set the interface and the addresses in the config. // Set the interface and the addresses in the config.
@@ -2818,7 +2873,7 @@ public class Vpn {
checkInterruptAndDelay(false); checkInterruptAndDelay(false);
// Check if the interface is gone while we are waiting. // Check if the interface is gone while we are waiting.
if (jniCheck(mConfig.interfaze) == 0) { if (mDeps.checkInterfacePresent(Vpn.this, mConfig.interfaze)) {
throw new IllegalStateException(mConfig.interfaze + " is gone"); throw new IllegalStateException(mConfig.interfaze + " is gone");
} }
@@ -2849,7 +2904,7 @@ public class Vpn {
while (true) { while (true) {
Thread.sleep(2000); Thread.sleep(2000);
for (int i = 0; i < mDaemons.length; i++) { for (int i = 0; i < mDaemons.length; i++) {
if (mArguments[i] != null && SystemService.isStopped(mDaemons[i])) { if (mArguments[i] != null && mDeps.isServiceStopped(mDaemons[i])) {
return; return;
} }
} }

View File

@@ -30,6 +30,7 @@ import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_VPN; import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@@ -49,6 +50,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.annotation.NonNull;
import android.annotation.UserIdInt; import android.annotation.UserIdInt;
import android.app.AppOpsManager; import android.app.AppOpsManager;
import android.app.NotificationManager; import android.app.NotificationManager;
@@ -65,6 +67,7 @@ import android.net.InetAddresses;
import android.net.IpPrefix; import android.net.IpPrefix;
import android.net.IpSecManager; import android.net.IpSecManager;
import android.net.LinkProperties; import android.net.LinkProperties;
import android.net.LocalSocket;
import android.net.Network; import android.net.Network;
import android.net.NetworkCapabilities; import android.net.NetworkCapabilities;
import android.net.NetworkInfo.DetailedState; import android.net.NetworkInfo.DetailedState;
@@ -74,6 +77,7 @@ import android.net.VpnManager;
import android.net.VpnService; import android.net.VpnService;
import android.os.Build.VERSION_CODES; import android.os.Build.VERSION_CODES;
import android.os.Bundle; import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.INetworkManagementService; import android.os.INetworkManagementService;
import android.os.Looper; import android.os.Looper;
import android.os.Process; import android.os.Process;
@@ -94,6 +98,7 @@ import com.android.internal.net.VpnProfile;
import com.android.server.IpSecService; import com.android.server.IpSecService;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Answers; import org.mockito.Answers;
@@ -101,13 +106,20 @@ import org.mockito.InOrder;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.Inet4Address; import java.net.Inet4Address;
import java.net.InetAddress;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream; import java.util.stream.Stream;
/** /**
@@ -133,7 +145,8 @@ public class VpnTest {
managedProfileA.profileGroupId = primaryUser.id; managedProfileA.profileGroupId = primaryUser.id;
} }
static final String TEST_VPN_PKG = "com.dummy.vpn"; static final String EGRESS_IFACE = "wlan0";
static final String TEST_VPN_PKG = "com.testvpn.vpn";
private static final String TEST_VPN_SERVER = "1.2.3.4"; private static final String TEST_VPN_SERVER = "1.2.3.4";
private static final String TEST_VPN_IDENTITY = "identity"; private static final String TEST_VPN_IDENTITY = "identity";
private static final byte[] TEST_VPN_PSK = "psk".getBytes(); private static final byte[] TEST_VPN_PSK = "psk".getBytes();
@@ -1012,31 +1025,191 @@ public class VpnTest {
// a subsequent CL. // a subsequent CL.
} }
@Test public Vpn startLegacyVpn(final VpnProfile vpnProfile) throws Exception {
public void testStartLegacyVpn() throws Exception {
final Vpn vpn = createVpn(primaryUser.id); final Vpn vpn = createVpn(primaryUser.id);
setMockedUsers(primaryUser); setMockedUsers(primaryUser);
// Dummy egress interface // Dummy egress interface
final String egressIface = "DUMMY0";
final LinkProperties lp = new LinkProperties(); final LinkProperties lp = new LinkProperties();
lp.setInterfaceName(egressIface); lp.setInterfaceName(EGRESS_IFACE);
final RouteInfo defaultRoute = new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), final RouteInfo defaultRoute = new RouteInfo(new IpPrefix(Inet4Address.ANY, 0),
InetAddresses.parseNumericAddress("192.0.2.0"), egressIface); InetAddresses.parseNumericAddress("192.0.2.0"), EGRESS_IFACE);
lp.addRoute(defaultRoute); lp.addRoute(defaultRoute);
vpn.startLegacyVpn(mVpnProfile, mKeyStore, lp); vpn.startLegacyVpn(vpnProfile, mKeyStore, lp);
return vpn;
}
@Test
public void testStartPlatformVpn() throws Exception {
startLegacyVpn(mVpnProfile);
// TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in
// a subsequent CL. // a subsequent patch.
}
@Test
public void testStartRacoonNumericAddress() throws Exception {
startRacoon("1.2.3.4", "1.2.3.4");
}
@Test
@Ignore("b/158974172") // remove when the bug is fixed
public void testStartRacoonHostname() throws Exception {
startRacoon("hostname", "5.6.7.8"); // address returned by deps.resolve
}
public void startRacoon(final String serverAddr, final String expectedAddr)
throws Exception {
final ConditionVariable legacyRunnerReady = new ConditionVariable();
final VpnProfile profile = new VpnProfile("testProfile" /* key */);
profile.type = VpnProfile.TYPE_L2TP_IPSEC_PSK;
profile.name = "testProfileName";
profile.username = "userName";
profile.password = "thePassword";
profile.server = serverAddr;
profile.ipsecIdentifier = "id";
profile.ipsecSecret = "secret";
profile.l2tpSecret = "l2tpsecret";
when(mConnectivityManager.getAllNetworks())
.thenReturn(new Network[] { new Network(101) });
when(mConnectivityManager.registerNetworkAgent(any(), any(), any(), any(),
anyInt(), any(), anyInt())).thenAnswer(invocation -> {
// The runner has registered an agent and is now ready.
legacyRunnerReady.open();
return new Network(102);
});
final Vpn vpn = startLegacyVpn(profile);
final TestDeps deps = (TestDeps) vpn.mDeps;
try {
// udppsk and 1701 are the values for TYPE_L2TP_IPSEC_PSK
assertArrayEquals(
new String[] { EGRESS_IFACE, expectedAddr, "udppsk",
profile.ipsecIdentifier, profile.ipsecSecret, "1701" },
deps.racoonArgs.get(10, TimeUnit.SECONDS));
// literal values are hardcoded in Vpn.java for mtpd args
assertArrayEquals(
new String[] { EGRESS_IFACE, "l2tp", expectedAddr, "1701", profile.l2tpSecret,
"name", profile.username, "password", profile.password,
"linkname", "vpn", "refuse-eap", "nodefaultroute", "usepeerdns",
"idle", "1800", "mtu", "1400", "mru", "1400" },
deps.mtpdArgs.get(10, TimeUnit.SECONDS));
// Now wait for the runner to be ready before testing for the route.
legacyRunnerReady.block(10_000);
// In this test the expected address is always v4 so /32
final RouteInfo expectedRoute = new RouteInfo(new IpPrefix(expectedAddr + "/32"),
RouteInfo.RTN_THROW);
assertTrue("Routes lack the expected throw route (" + expectedRoute + ") : "
+ vpn.mConfig.routes,
vpn.mConfig.routes.contains(expectedRoute));
} finally {
// Now interrupt the thread, unblock the runner and clean up.
vpn.mVpnRunner.exitVpnRunner();
deps.getStateFile().delete(); // set to delete on exit, but this deletes it earlier
vpn.mVpnRunner.join(10_000); // wait for up to 10s for the runner to die and cleanup
}
}
private static final class TestDeps extends Vpn.Dependencies {
public final CompletableFuture<String[]> racoonArgs = new CompletableFuture();
public final CompletableFuture<String[]> mtpdArgs = new CompletableFuture();
public final File mStateFile;
private final HashMap<String, Boolean> mRunningServices = new HashMap<>();
TestDeps() {
try {
mStateFile = File.createTempFile("vpnTest", ".tmp");
mStateFile.deleteOnExit();
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void startService(final String serviceName) {
mRunningServices.put(serviceName, true);
}
@Override
public void stopService(final String serviceName) {
mRunningServices.put(serviceName, false);
}
@Override
public boolean isServiceRunning(final String serviceName) {
return mRunningServices.getOrDefault(serviceName, false);
}
@Override
public boolean isServiceStopped(final String serviceName) {
return !isServiceRunning(serviceName);
}
@Override
public File getStateFile() {
return mStateFile;
}
@Override
public void sendArgumentsToDaemon(
final String daemon, final LocalSocket socket, final String[] arguments,
final Vpn.RetryScheduler interruptChecker) throws IOException {
if ("racoon".equals(daemon)) {
racoonArgs.complete(arguments);
} else if ("mtpd".equals(daemon)) {
writeStateFile(arguments);
mtpdArgs.complete(arguments);
} else {
throw new UnsupportedOperationException("Unsupported daemon : " + daemon);
}
}
private void writeStateFile(final String[] arguments) throws IOException {
mStateFile.delete();
mStateFile.createNewFile();
mStateFile.deleteOnExit();
final BufferedWriter writer = new BufferedWriter(
new FileWriter(mStateFile, false /* append */));
writer.write(EGRESS_IFACE);
writer.write("\n");
// addresses
writer.write("10.0.0.1/24\n");
// routes
writer.write("192.168.6.0/24\n");
// dns servers
writer.write("192.168.6.1\n");
// search domains
writer.write("vpn.searchdomains.com\n");
// endpoint - intentionally empty
writer.write("\n");
writer.flush();
writer.close();
}
@Override
@NonNull
public InetAddress resolve(final String endpoint) {
try {
// If a numeric IP address, return it.
return InetAddress.parseNumericAddress(endpoint);
} catch (IllegalArgumentException e) {
// Otherwise, return some token IP to test for.
return InetAddress.parseNumericAddress("5.6.7.8");
}
}
@Override
public boolean checkInterfacePresent(final Vpn vpn, final String iface) {
return true;
}
} }
/** /**
* Mock some methods of vpn object. * Mock some methods of vpn object.
*/ */
private Vpn createVpn(@UserIdInt int userId) { private Vpn createVpn(@UserIdInt int userId) {
return new Vpn(Looper.myLooper(), mContext, mNetService, return new Vpn(Looper.myLooper(), mContext, new TestDeps(), mNetService,
userId, mKeyStore, mSystemServices, mIkev2SessionCreator); userId, mKeyStore, mSystemServices, mIkev2SessionCreator);
} }