Add a test for starting the legacy VPN.

The legacy VPN has, among many parameters, a host to connect to.
This host can be specified as a numeric address, or as a hostname.
When it's a name, resolution is required. Currently, name
resolution is performed by the native VPN daemons racoon and
mtpd. When a hostname is used, the framework does not know the
IP address of the VPN server and does not add a throw route for
the VPN server IP address. On older kernels this does not matter
because the legacy PPP kernel code binds the PPP socket to the
right network, but on newer devices that use the upstream PPP
code, this does not work. See b/133797637.

This patch instruments the legacy VPN code so that it can be
run in tests, and uses this instrumentation to simulate passing
a configuration that contains a host, and verifies that the
arguments passed to the mptd and racoon daemons receive the
expected server address, and that the expected throw route is
correctly installed.
It then adds two tests : one specifying the server as a numeric
address, and one as a hostname. As the resolution is currently
broken, the latter of these tests is added disabled, and the
followup fix to the issue enables it.

This test is basic and very targeted, but it's what we need right
now. Also there are plans to remove this entire code path in S, so
the test being ad-hoc is not much of a problem.

Test: this
Bug: 158974172
Change-Id: I420b63db03a58e8c67a98a85c9f4c2d6ed7ad078
(cherry picked from commit 45846a03051a13d667732288e457eab4336a691e)
This commit is contained in:
Chalard Jean
2020-07-22 16:25:59 +09:00
parent 3b7591e1f5
commit c9077f2a38

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);
} }