From 7064ea15bbb7dbc8562b8d73b0f55ad7170b3905 Mon Sep 17 00:00:00 2001 From: Lorenzo Colitti Date: Tue, 12 Nov 2019 20:43:02 +0900 Subject: [PATCH] Add a CTS test for private DNS on VPNs. Bug: 122652057 Test: atest com.android.cts.net.HostsideVpnTests passes with fix, fails without it Change-Id: Idc0690dfe6d000337b272bbd55ea4d773aebd5ed Merged-In: Ifa2710aed7e773a24786cc3e4912f126547dfe0b --- .../com/android/cts/net/hostside/VpnTest.java | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java index bc982cec78..a406d7a453 100755 --- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java +++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/VpnTest.java @@ -18,6 +18,7 @@ package com.android.cts.net.hostside; import static android.system.OsConstants.*; +import android.content.ContentResolver; import android.content.Intent; import android.content.pm.PackageManager; import android.net.ConnectivityManager; @@ -27,6 +28,7 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.net.VpnService; +import android.provider.Settings; import android.os.ParcelFileDescriptor; import android.os.Process; import android.support.test.uiautomator.UiDevice; @@ -62,8 +64,12 @@ import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; +import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; +import java.util.Objects; import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; /** * Tests for the VpnService API. @@ -92,6 +98,13 @@ import java.util.Random; */ public class VpnTest extends InstrumentationTestCase { + // These are neither public nor @TestApi. + // TODO: add them to @TestApi. + private static final String PRIVATE_DNS_MODE_SETTING = "private_dns_mode"; + private static final String PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = "hostname"; + private static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic"; + private static final String PRIVATE_DNS_SPECIFIER_SETTING = "private_dns_specifier"; + public static String TAG = "VpnTest"; public static int TIMEOUT_MS = 3 * 1000; public static int SOCKET_TIMEOUT_MS = 100; @@ -108,6 +121,9 @@ public class VpnTest extends InstrumentationTestCase { final Object mLock = new Object(); final Object mLockShutdown = new Object(); + private String mOldPrivateDnsMode; + private String mOldPrivateDnsSpecifier; + private boolean supportedHardware() { final PackageManager pm = getInstrumentation().getContext().getPackageManager(); return !pm.hasSystemFeature("android.hardware.type.watch"); @@ -119,6 +135,7 @@ public class VpnTest extends InstrumentationTestCase { mNetwork = null; mCallback = null; + storePrivateDnsSetting(); mDevice = UiDevice.getInstance(getInstrumentation()); mActivity = launchActivity(getInstrumentation().getTargetContext().getPackageName(), @@ -132,6 +149,7 @@ public class VpnTest extends InstrumentationTestCase { @Override public void tearDown() throws Exception { + restorePrivateDnsSetting(); mRemoteSocketFactoryClient.unbind(); if (mCallback != null) { mCM.unregisterNetworkCallback(mCallback); @@ -535,6 +553,95 @@ public class VpnTest extends InstrumentationTestCase { } } + private ContentResolver getContentResolver() { + return getInstrumentation().getContext().getContentResolver(); + } + + private boolean isPrivateDnsInStrictMode() { + return PRIVATE_DNS_MODE_PROVIDER_HOSTNAME.equals( + Settings.Global.getString(getContentResolver(), PRIVATE_DNS_MODE_SETTING)); + } + + private void storePrivateDnsSetting() { + mOldPrivateDnsMode = Settings.Global.getString(getContentResolver(), + PRIVATE_DNS_MODE_SETTING); + mOldPrivateDnsSpecifier = Settings.Global.getString(getContentResolver(), + PRIVATE_DNS_SPECIFIER_SETTING); + } + + private void restorePrivateDnsSetting() { + Settings.Global.putString(getContentResolver(), PRIVATE_DNS_MODE_SETTING, + mOldPrivateDnsMode); + Settings.Global.putString(getContentResolver(), PRIVATE_DNS_SPECIFIER_SETTING, + mOldPrivateDnsSpecifier); + } + + // TODO: replace with CtsNetUtils.awaitPrivateDnsSetting in Q or above. + private void expectPrivateDnsHostname(final String hostname) throws Exception { + final NetworkRequest request = new NetworkRequest.Builder() + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + .build(); + final CountDownLatch latch = new CountDownLatch(1); + final NetworkCallback callback = new NetworkCallback() { + @Override + public void onLinkPropertiesChanged(Network network, LinkProperties lp) { + if (network.equals(mNetwork) && + Objects.equals(lp.getPrivateDnsServerName(), hostname)) { + latch.countDown(); + } + } + }; + + mCM.registerNetworkCallback(request, callback); + + try { + assertTrue("Private DNS hostname was not " + hostname + " after " + TIMEOUT_MS + "ms", + latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + } finally { + mCM.unregisterNetworkCallback(callback); + } + } + + private void setAndVerifyPrivateDns(boolean strictMode) throws Exception { + final ContentResolver cr = getInstrumentation().getContext().getContentResolver(); + String privateDnsHostname; + + if (strictMode) { + privateDnsHostname = "vpncts-nx.metric.gstatic.com"; + Settings.Global.putString(cr, PRIVATE_DNS_SPECIFIER_SETTING, privateDnsHostname); + Settings.Global.putString(cr, PRIVATE_DNS_MODE_SETTING, + PRIVATE_DNS_MODE_PROVIDER_HOSTNAME); + } else { + Settings.Global.putString(cr, PRIVATE_DNS_MODE_SETTING, PRIVATE_DNS_MODE_OPPORTUNISTIC); + privateDnsHostname = null; + } + + expectPrivateDnsHostname(privateDnsHostname); + + String randomName = "vpncts-" + new Random().nextInt(1000000000) + "-ds.metric.gstatic.com"; + if (strictMode) { + // Strict mode private DNS is enabled. DNS lookups should fail, because the private DNS + // server name is invalid. + try { + InetAddress.getByName(randomName); + fail("VPN DNS lookup should fail with private DNS enabled"); + } catch (UnknownHostException expected) { + } + } else { + // Strict mode private DNS is disabled. DNS lookup should succeed, because the VPN + // provides no DNS servers, and thus DNS falls through to the default network. + assertNotNull("VPN DNS lookup should succeed with private DNS disabled", + InetAddress.getByName(randomName)); + } + } + + // Tests that strict mode private DNS is used on VPNs. + private void checkStrictModePrivateDns() throws Exception { + final boolean initialMode = isPrivateDnsInStrictMode(); + setAndVerifyPrivateDns(!initialMode); + setAndVerifyPrivateDns(initialMode); + } + public void testDefault() throws Exception { if (!supportedHardware()) return; @@ -547,6 +654,8 @@ public class VpnTest extends InstrumentationTestCase { assertSocketClosed(fd, TEST_HOST); checkTrafficOnVpn(); + + checkStrictModePrivateDns(); } public void testAppAllowed() throws Exception { @@ -562,6 +671,8 @@ public class VpnTest extends InstrumentationTestCase { assertSocketClosed(fd, TEST_HOST); checkTrafficOnVpn(); + + checkStrictModePrivateDns(); } public void testAppDisallowed() throws Exception {