From 3b160e23857835e8863c0c814fe89a1b6731340f 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: 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 2fc85f6e3e..fde8335b02 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 @@ -20,6 +20,7 @@ import static android.os.Process.INVALID_UID; import static android.system.OsConstants.*; import android.annotation.Nullable; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -33,6 +34,7 @@ import android.net.Proxy; import android.net.ProxyInfo; import android.net.VpnService; import android.net.wifi.WifiManager; +import android.provider.Settings; import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.SystemProperties; @@ -62,8 +64,11 @@ import java.net.InetAddress; 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.ArrayList; +import java.util.Objects; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -95,6 +100,13 @@ import java.util.concurrent.TimeUnit; */ 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; @@ -112,6 +124,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"); @@ -123,6 +138,7 @@ public class VpnTest extends InstrumentationTestCase { mNetwork = null; mCallback = null; + storePrivateDnsSetting(); mDevice = UiDevice.getInstance(getInstrumentation()); mActivity = launchActivity(getInstrumentation().getTargetContext().getPackageName(), @@ -137,6 +153,7 @@ public class VpnTest extends InstrumentationTestCase { @Override public void tearDown() throws Exception { + restorePrivateDnsSetting(); mRemoteSocketFactoryClient.unbind(); if (mCallback != null) { mCM.unregisterNetworkCallback(mCallback); @@ -567,6 +584,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; // If adb TCP port opened, this test may running by adb over network. @@ -598,6 +704,9 @@ public class VpnTest extends InstrumentationTestCase { assertSocketClosed(fd, TEST_HOST); checkTrafficOnVpn(); + + checkStrictModePrivateDns(); + receiver.unregisterQuietly(); } @@ -615,6 +724,8 @@ public class VpnTest extends InstrumentationTestCase { assertSocketClosed(fd, TEST_HOST); checkTrafficOnVpn(); + + checkStrictModePrivateDns(); } public void testAppDisallowed() throws Exception {