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
diff --git a/hostsidetests/net/app/src/com/android/cts/net/hostside/VpnTest.java b/hostsidetests/net/app/src/com/android/cts/net/hostside/VpnTest.java
index bc982ce..a406d7a 100755
--- a/hostsidetests/net/app/src/com/android/cts/net/hostside/VpnTest.java
+++ b/hostsidetests/net/app/src/com/android/cts/net/hostside/VpnTest.java
@@ -18,6 +18,7 @@
 
 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.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.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 @@
  */
 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 @@
     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 @@
 
         mNetwork = null;
         mCallback = null;
+        storePrivateDnsSetting();
 
         mDevice = UiDevice.getInstance(getInstrumentation());
         mActivity = launchActivity(getInstrumentation().getTargetContext().getPackageName(),
@@ -132,6 +149,7 @@
 
     @Override
     public void tearDown() throws Exception {
+        restorePrivateDnsSetting();
         mRemoteSocketFactoryClient.unbind();
         if (mCallback != null) {
             mCM.unregisterNetworkCallback(mCallback);
@@ -535,6 +553,95 @@
         }
     }
 
+    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 @@
         assertSocketClosed(fd, TEST_HOST);
 
         checkTrafficOnVpn();
+
+        checkStrictModePrivateDns();
     }
 
     public void testAppAllowed() throws Exception {
@@ -562,6 +671,8 @@
         assertSocketClosed(fd, TEST_HOST);
 
         checkTrafficOnVpn();
+
+        checkStrictModePrivateDns();
     }
 
     public void testAppDisallowed() throws Exception {