Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 44027dbc authored by Erik Kline's avatar Erik Kline Committed by android-build-merger
Browse files

Merge "Move the logic of (re)evaluation of Private DNS" am: 04233ef1

am: 23ed88ac

Change-Id: I5dc90ecfe6f6f10967b7501645ad8e030cb38982
parents 54772975 23ed88ac
Loading
Loading
Loading
Loading
+47 −92
Original line number Diff line number Diff line
@@ -943,6 +943,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
        mHandler.sendEmptyMessage(EVENT_CONFIGURE_MOBILE_DATA_ALWAYS_ON);
    }

    // See FakeSettingsProvider comment above.
    @VisibleForTesting
    void updatePrivateDnsSettings() {
        mHandler.sendEmptyMessage(EVENT_PRIVATE_DNS_SETTINGS_CHANGED);
    }

    private void handleMobileDataAlwaysOn() {
        final boolean enable = toBool(Settings.Global.getInt(
                mContext.getContentResolver(), Settings.Global.MOBILE_DATA_ALWAYS_ON, 1));
@@ -972,8 +978,8 @@ public class ConnectivityService extends IConnectivityManager.Stub
    }

    private void registerPrivateDnsSettingsCallbacks() {
        for (Uri u : DnsManager.getPrivateDnsSettingsUris()) {
            mSettingsObserver.observe(u, EVENT_PRIVATE_DNS_SETTINGS_CHANGED);
        for (Uri uri : DnsManager.getPrivateDnsSettingsUris()) {
            mSettingsObserver.observe(uri, EVENT_PRIVATE_DNS_SETTINGS_CHANGED);
        }
    }

@@ -1026,8 +1032,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
        if (network == null) {
            return null;
        }
        return getNetworkAgentInfoForNetId(network.netId);
    }

    private NetworkAgentInfo getNetworkAgentInfoForNetId(int netId) {
        synchronized (mNetworkForNetId) {
            return mNetworkForNetId.get(network.netId);
            return mNetworkForNetId.get(netId);
        }
    }

@@ -1167,9 +1177,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
        }
        NetworkAgentInfo nai;
        if (vpnNetId != NETID_UNSET) {
            synchronized (mNetworkForNetId) {
                nai = mNetworkForNetId.get(vpnNetId);
            }
            nai = getNetworkAgentInfoForNetId(vpnNetId);
            if (nai != null) return nai.network;
        }
        nai = getDefaultNetwork();
@@ -2155,41 +2163,21 @@ public class ConnectivityService extends IConnectivityManager.Stub
                default:
                    return false;
                case NetworkMonitor.EVENT_NETWORK_TESTED: {
                    final NetworkAgentInfo nai;
                    synchronized (mNetworkForNetId) {
                        nai = mNetworkForNetId.get(msg.arg2);
                    }
                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2);
                    if (nai == null) break;

                    final boolean valid = (msg.arg1 == NetworkMonitor.NETWORK_TEST_RESULT_VALID);
                    final boolean wasValidated = nai.lastValidated;
                    final boolean wasDefault = isDefaultNetwork(nai);

                    final PrivateDnsConfig privateDnsCfg = (msg.obj instanceof PrivateDnsConfig)
                            ? (PrivateDnsConfig) msg.obj : null;
                    final String redirectUrl = (msg.obj instanceof String) ? (String) msg.obj : "";

                    final boolean reevaluationRequired;
                    final String logMsg;
                    if (valid) {
                        reevaluationRequired = updatePrivateDns(nai, privateDnsCfg);
                        logMsg = (DBG && (privateDnsCfg != null))
                                 ? " with " + privateDnsCfg.toString() : "";
                    } else {
                        reevaluationRequired = false;
                        logMsg = (DBG && !TextUtils.isEmpty(redirectUrl))
                                 ? " with redirect to " + redirectUrl : "";
                    }
                    if (DBG) {
                        final String logMsg = !TextUtils.isEmpty(redirectUrl)
                                 ? " with redirect to " + redirectUrl
                                 : "";
                        log(nai.name() + " validation " + (valid ? "passed" : "failed") + logMsg);
                    }
                    // If there is a change in Private DNS configuration,
                    // trigger reevaluation of the network to test it.
                    if (reevaluationRequired) {
                        nai.networkMonitor.sendMessage(
                                NetworkMonitor.CMD_FORCE_REEVALUATION, Process.SYSTEM_UID);
                        break;
                    }
                    if (valid != nai.lastValidated) {
                        if (wasDefault) {
                            metricsLogger().defaultNetworkMetrics().logDefaultNetworkValidity(
@@ -2218,10 +2206,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
                case NetworkMonitor.EVENT_PROVISIONING_NOTIFICATION: {
                    final int netId = msg.arg2;
                    final boolean visible = toBool(msg.arg1);
                    final NetworkAgentInfo nai;
                    synchronized (mNetworkForNetId) {
                        nai = mNetworkForNetId.get(netId);
                    }
                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
                    // If captive portal status has changed, update capabilities or disconnect.
                    if (nai != null && (visible != nai.lastCaptivePortalDetected)) {
                        final int oldScore = nai.getCurrentScore();
@@ -2252,18 +2237,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
                    break;
                }
                case NetworkMonitor.EVENT_PRIVATE_DNS_CONFIG_RESOLVED: {
                    final NetworkAgentInfo nai;
                    synchronized (mNetworkForNetId) {
                        nai = mNetworkForNetId.get(msg.arg2);
                    }
                    final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2);
                    if (nai == null) break;

                    final PrivateDnsConfig cfg = (PrivateDnsConfig) msg.obj;
                    final boolean reevaluationRequired = updatePrivateDns(nai, cfg);
                    if (nai.lastValidated && reevaluationRequired) {
                        nai.networkMonitor.sendMessage(
                                NetworkMonitor.CMD_FORCE_REEVALUATION, Process.SYSTEM_UID);
                    }
                    updatePrivateDns(nai, (PrivateDnsConfig) msg.obj);
                    break;
                }
            }
@@ -2301,61 +2278,38 @@ public class ConnectivityService extends IConnectivityManager.Stub
        }
    }

    private boolean networkRequiresValidation(NetworkAgentInfo nai) {
        return NetworkMonitor.isValidationRequired(
                mDefaultRequest.networkCapabilities, nai.networkCapabilities);
    }

    private void handlePrivateDnsSettingsChanged() {
        final PrivateDnsConfig cfg = mDnsManager.getPrivateDnsConfig();

        for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
            handlePerNetworkPrivateDnsConfig(nai, cfg);
        }
    }

    private void handlePerNetworkPrivateDnsConfig(NetworkAgentInfo nai, PrivateDnsConfig cfg) {
        // Private DNS only ever applies to networks that might provide
        // Internet access and therefore also require validation.
            if (!NetworkMonitor.isValidationRequired(
                    mDefaultRequest.networkCapabilities, nai.networkCapabilities)) {
                continue;
            }
        if (!networkRequiresValidation(nai)) return;

        // Notify the NetworkMonitor thread in case it needs to cancel or
        // schedule DNS resolutions. If a DNS resolution is required the
        // result will be sent back to us.
        nai.networkMonitor.notifyPrivateDnsSettingsChanged(cfg);

            if (!cfg.inStrictMode()) {
                // No strict mode hostname DNS resolution needed, so just update
                // DNS settings directly. In opportunistic and "off" modes this
                // just reprograms netd with the network-supplied DNS servers
                // (and of course the boolean of whether or not to attempt TLS).
                //
                // TODO: Consider code flow parity with strict mode, i.e. having
                // NetworkMonitor relay the PrivateDnsConfig back to us and then
                // performing this call at that time.
        // With Private DNS bypass support, we can proceed to update the
        // Private DNS config immediately, even if we're in strict mode
        // and have not yet resolved the provider name into a set of IPs.
        updatePrivateDns(nai, cfg);
    }
        }
    }

    private boolean updatePrivateDns(NetworkAgentInfo nai, PrivateDnsConfig newCfg) {
        final boolean reevaluationRequired = true;
        final boolean dontReevaluate = false;

        final PrivateDnsConfig oldCfg = mDnsManager.updatePrivateDns(nai.network, newCfg);
    private void updatePrivateDns(NetworkAgentInfo nai, PrivateDnsConfig newCfg) {
        mDnsManager.updatePrivateDns(nai.network, newCfg);
        updateDnses(nai.linkProperties, null, nai.network.netId);

        if (newCfg == null) {
            if (oldCfg == null) return dontReevaluate;
            return oldCfg.useTls ? reevaluationRequired : dontReevaluate;
        }

        if (oldCfg == null) {
            return newCfg.useTls ? reevaluationRequired : dontReevaluate;
        }

        if (oldCfg.useTls != newCfg.useTls) {
            return reevaluationRequired;
        }

        if (newCfg.inStrictMode() && !Objects.equals(oldCfg.hostname, newCfg.hostname)) {
            return reevaluationRequired;
        }

        return dontReevaluate;
    }

    private void updateLingerState(NetworkAgentInfo nai, long now) {
@@ -3300,7 +3254,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
        if (isNetworkWithLinkPropertiesBlocked(lp, uid, false)) {
            return;
        }
        nai.networkMonitor.sendMessage(NetworkMonitor.CMD_FORCE_REEVALUATION, uid);
        nai.networkMonitor.forceReevaluation(uid);
    }

    private ProxyInfo getDefaultProxy() {
@@ -4919,7 +4873,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
    }

    public void handleUpdateLinkProperties(NetworkAgentInfo nai, LinkProperties newLp) {
        if (mNetworkForNetId.get(nai.network.netId) != nai) {
        if (getNetworkAgentInfoForNetId(nai.network.netId) != nai) {
            // Ignore updates for disconnected networks
            return;
        }
@@ -5499,6 +5453,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
                Slog.wtf(TAG, networkAgent.name() + " connected with null LinkProperties");
            }

            handlePerNetworkPrivateDnsConfig(networkAgent, mDnsManager.getPrivateDnsConfig());
            updateLinkProperties(networkAgent, null);

            networkAgent.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_CONNECTED);
+45 −0
Original line number Diff line number Diff line
@@ -61,6 +61,51 @@ import java.util.StringJoiner;
 * This class it NOT designed for concurrent access. Furthermore, all non-static
 * methods MUST be called from ConnectivityService's thread.
 *
 * [ Private DNS ]
 * The code handling Private DNS is spread across several components, but this
 * seems like the least bad place to collect all the observations.
 *
 * Private DNS handling and updating occurs in response to several different
 * events. Each is described here with its corresponding intended handling.
 *
 * [A] Event: A new network comes up.
 * Mechanics:
 *     [1] ConnectivityService gets notifications from NetworkAgents.
 *     [2] in updateNetworkInfo(), the first time the NetworkAgent goes into
 *         into CONNECTED state, the Private DNS configuration is retrieved,
 *         programmed, and strict mode hostname resolution (if applicable) is
 *         enqueued in NetworkAgent's NetworkMonitor, via a call to
 *         handlePerNetworkPrivateDnsConfig().
 *     [3] Re-resolution of strict mode hostnames that fail to return any
 *         IP addresses happens inside NetworkMonitor; it sends itself a
 *         delayed CMD_EVALUATE_PRIVATE_DNS message in a simple backoff
 *         schedule.
 *     [4] Successfully resolved hostnames are sent to ConnectivityService
 *         inside an EVENT_PRIVATE_DNS_CONFIG_RESOLVED message. The resolved
 *         IP addresses are programmed into netd via:
 *
 *             updatePrivateDns() -> updateDnses()
 *
 *         both of which make calls into DnsManager.
 *     [5] Upon a successful hostname resolution NetworkMonitor initiates a
 *         validation attempt in the form of a lookup for a one-time hostname
 *         that uses Private DNS.
 *
 * [B] Event: Private DNS settings are changed.
 * Mechanics:
 *     [1] ConnectivityService gets notifications from its SettingsObserver.
 *     [2] handlePrivateDnsSettingsChanged() is called, which calls
 *         handlePerNetworkPrivateDnsConfig() and the process proceeds
 *         as if from A.3 above.
 *
 * [C] Event: An application calls ConnectivityManager#reportBadNetwork().
 * Mechanics:
 *     [1] NetworkMonitor is notified and initiates a reevaluation, which
 *         always bypasses Private DNS.
 *     [2] Once completed, NetworkMonitor checks if strict mode is in operation
 *         and if so enqueues another evaluation of Private DNS, as if from
 *         step A.5 above.
 *
 * @hide
 */
public class DnsManager {
+200 −73

File changed.

Preview size limit exceeded, changes collapsed.

+116 −18
Original line number Diff line number Diff line
@@ -17,6 +17,9 @@
package com.android.server;

import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OFF;
import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
import static android.net.ConnectivityManager.TYPE_ETHERNET;
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA;
@@ -70,6 +73,7 @@ import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;


@@ -181,6 +185,9 @@ public class ConnectivityServiceTest {
    private static final int TIMEOUT_MS = 500;
    private static final int TEST_LINGER_DELAY_MS = 120;

    private static final String MOBILE_IFNAME = "test_rmnet_data0";
    private static final String WIFI_IFNAME = "test_wlan0";

    private MockContext mServiceContext;
    private WrappedConnectivityService mService;
    private WrappedConnectivityManager mCm;
@@ -751,7 +758,7 @@ public class ConnectivityServiceTest {

    // NetworkMonitor implementation allowing overriding of Internet connectivity probe result.
    private class WrappedNetworkMonitor extends NetworkMonitor {
        public Handler connectivityHandler;
        public final Handler connectivityHandler;
        // HTTP response code fed back to NetworkMonitor for Internet connectivity probe.
        public int gen204ProbeResult = 500;
        public String gen204ProbeRedirectUrl = null;
@@ -928,6 +935,7 @@ public class ConnectivityServiceTest {
        // Ensure that the default setting for Captive Portals is used for most tests
        setCaptivePortalMode(Settings.Global.CAPTIVE_PORTAL_MODE_PROMPT);
        setMobileDataAlwaysOn(false);
        setPrivateDnsSettings(PRIVATE_DNS_MODE_OFF, "ignored.example.com");
    }

    @After
@@ -2582,6 +2590,14 @@ public class ConnectivityServiceTest {
        waitForIdle();
    }

    private void setPrivateDnsSettings(String mode, String specifier) {
        final ContentResolver cr = mServiceContext.getContentResolver();
        Settings.Global.putString(cr, Settings.Global.PRIVATE_DNS_MODE, mode);
        Settings.Global.putString(cr, Settings.Global.PRIVATE_DNS_SPECIFIER, specifier);
        mService.updatePrivateDnsSettings();
        waitForIdle();
    }

    private boolean isForegroundNetwork(MockNetworkAgent network) {
        NetworkCapabilities nc = mCm.getNetworkCapabilities(network.getNetwork());
        assertNotNull(nc);
@@ -3583,7 +3599,7 @@ public class ConnectivityServiceTest {
        mCm.registerNetworkCallback(networkRequest, networkCallback);

        LinkProperties lp = new LinkProperties();
        lp.setInterfaceName("wlan0");
        lp.setInterfaceName(WIFI_IFNAME);
        LinkAddress myIpv4Address = new LinkAddress("192.168.12.3/24");
        RouteInfo myIpv4DefaultRoute = new RouteInfo((IpPrefix) null,
                NetworkUtils.numericToInetAddress("192.168.12.1"), lp.getInterfaceName());
@@ -3672,52 +3688,63 @@ public class ConnectivityServiceTest {

    @Test
    public void testBasicDnsConfigurationPushed() throws Exception {
        final String IFNAME = "test_rmnet_data0";
        final String[] EMPTY_TLS_SERVERS = new String[0];
        setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com");
        ArgumentCaptor<String[]> tlsServers = ArgumentCaptor.forClass(String[].class);

        // Clear any interactions that occur as a result of CS starting up.
        reset(mNetworkManagementService);

        final String[] EMPTY_STRING_ARRAY = new String[0];
        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
        waitForIdle();
        verify(mNetworkManagementService, never()).setDnsConfigurationForNetwork(
                anyInt(), any(), any(), any(), anyString(), eq(EMPTY_TLS_SERVERS));
                anyInt(), eq(EMPTY_STRING_ARRAY), any(), any(), eq(""), eq(EMPTY_STRING_ARRAY));
        verifyNoMoreInteractions(mNetworkManagementService);

        final LinkProperties cellLp = new LinkProperties();
        cellLp.setInterfaceName(IFNAME);
        cellLp.setInterfaceName(MOBILE_IFNAME);
        // Add IPv4 and IPv6 default routes, because DNS-over-TLS code does
        // "is-reachable" testing in order to not program netd with unreachable
        // nameservers that it might try repeated to validate.
        cellLp.addLinkAddress(new LinkAddress("192.0.2.4/24"));
        cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("192.0.2.4"), IFNAME));
        cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("192.0.2.4"),
                MOBILE_IFNAME));
        cellLp.addLinkAddress(new LinkAddress("2001:db8:1::1/64"));
        cellLp.addRoute(
                new RouteInfo((IpPrefix) null, InetAddress.getByName("2001:db8:1::1"), IFNAME));
        cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("2001:db8:1::1"),
                MOBILE_IFNAME));
        mCellNetworkAgent.sendLinkProperties(cellLp);
        mCellNetworkAgent.connect(false);
        waitForIdle();
        verify(mNetworkManagementService, times(1)).setDnsConfigurationForNetwork(
                anyInt(), mStringArrayCaptor.capture(), any(), any(),
                anyString(), eq(EMPTY_TLS_SERVERS));
        // CS tells netd about the empty DNS config for this network.
        assertEmpty(mStringArrayCaptor.getValue());
        verify(mNetworkManagementService, atLeastOnce()).setDnsConfigurationForNetwork(
                anyInt(), eq(EMPTY_STRING_ARRAY), any(), any(), eq(""), eq(EMPTY_STRING_ARRAY));
        reset(mNetworkManagementService);

        cellLp.addDnsServer(InetAddress.getByName("2001:db8::1"));
        mCellNetworkAgent.sendLinkProperties(cellLp);
        waitForIdle();
        verify(mNetworkManagementService, times(1)).setDnsConfigurationForNetwork(
        verify(mNetworkManagementService, atLeastOnce()).setDnsConfigurationForNetwork(
                anyInt(), mStringArrayCaptor.capture(), any(), any(),
                anyString(), eq(EMPTY_TLS_SERVERS));
                eq(""), tlsServers.capture());
        assertEquals(1, mStringArrayCaptor.getValue().length);
        assertTrue(ArrayUtils.contains(mStringArrayCaptor.getValue(), "2001:db8::1"));
        // Opportunistic mode.
        assertTrue(ArrayUtils.contains(tlsServers.getValue(), "2001:db8::1"));
        reset(mNetworkManagementService);

        cellLp.addDnsServer(InetAddress.getByName("192.0.2.1"));
        mCellNetworkAgent.sendLinkProperties(cellLp);
        waitForIdle();
        verify(mNetworkManagementService, times(1)).setDnsConfigurationForNetwork(
        verify(mNetworkManagementService, atLeastOnce()).setDnsConfigurationForNetwork(
                anyInt(), mStringArrayCaptor.capture(), any(), any(),
                anyString(), eq(EMPTY_TLS_SERVERS));
                eq(""), tlsServers.capture());
        assertEquals(2, mStringArrayCaptor.getValue().length);
        assertTrue(ArrayUtils.containsAll(mStringArrayCaptor.getValue(),
                new String[]{"2001:db8::1", "192.0.2.1"}));
        // Opportunistic mode.
        assertEquals(2, tlsServers.getValue().length);
        assertTrue(ArrayUtils.containsAll(tlsServers.getValue(),
                new String[]{"2001:db8::1", "192.0.2.1"}));
        reset(mNetworkManagementService);

        final String TLS_SPECIFIER = "tls.example.com";
@@ -3730,7 +3757,7 @@ public class ConnectivityServiceTest {
                mCellNetworkAgent.getNetwork().netId,
                new DnsManager.PrivateDnsConfig(TLS_SPECIFIER, TLS_IPS)));
        waitForIdle();
        verify(mNetworkManagementService, times(1)).setDnsConfigurationForNetwork(
        verify(mNetworkManagementService, atLeastOnce()).setDnsConfigurationForNetwork(
                anyInt(), mStringArrayCaptor.capture(), any(), any(),
                eq(TLS_SPECIFIER), eq(TLS_SERVERS));
        assertEquals(2, mStringArrayCaptor.getValue().length);
@@ -3739,6 +3766,77 @@ public class ConnectivityServiceTest {
        reset(mNetworkManagementService);
    }

    @Test
    public void testPrivateDnsSettingsChange() throws Exception {
        final String[] EMPTY_STRING_ARRAY = new String[0];
        ArgumentCaptor<String[]> tlsServers = ArgumentCaptor.forClass(String[].class);

        // Clear any interactions that occur as a result of CS starting up.
        reset(mNetworkManagementService);

        // The default on Android is opportunistic mode ("Automatic").
        setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com");

        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
        waitForIdle();
        // CS tells netd about the empty DNS config for this network.
        verify(mNetworkManagementService, never()).setDnsConfigurationForNetwork(
                anyInt(), eq(EMPTY_STRING_ARRAY), any(), any(), eq(""), eq(EMPTY_STRING_ARRAY));
        verifyNoMoreInteractions(mNetworkManagementService);

        final LinkProperties cellLp = new LinkProperties();
        cellLp.setInterfaceName(MOBILE_IFNAME);
        // Add IPv4 and IPv6 default routes, because DNS-over-TLS code does
        // "is-reachable" testing in order to not program netd with unreachable
        // nameservers that it might try repeated to validate.
        cellLp.addLinkAddress(new LinkAddress("192.0.2.4/24"));
        cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("192.0.2.4"),
                MOBILE_IFNAME));
        cellLp.addLinkAddress(new LinkAddress("2001:db8:1::1/64"));
        cellLp.addRoute(new RouteInfo((IpPrefix) null, InetAddress.getByName("2001:db8:1::1"),
                MOBILE_IFNAME));
        cellLp.addDnsServer(InetAddress.getByName("2001:db8::1"));
        cellLp.addDnsServer(InetAddress.getByName("192.0.2.1"));

        mCellNetworkAgent.sendLinkProperties(cellLp);
        mCellNetworkAgent.connect(false);
        waitForIdle();
        verify(mNetworkManagementService, atLeastOnce()).setDnsConfigurationForNetwork(
                anyInt(), mStringArrayCaptor.capture(), any(), any(),
                eq(""), tlsServers.capture());
        assertEquals(2, mStringArrayCaptor.getValue().length);
        assertTrue(ArrayUtils.containsAll(mStringArrayCaptor.getValue(),
                new String[]{"2001:db8::1", "192.0.2.1"}));
        // Opportunistic mode.
        assertEquals(2, tlsServers.getValue().length);
        assertTrue(ArrayUtils.containsAll(tlsServers.getValue(),
                new String[]{"2001:db8::1", "192.0.2.1"}));
        reset(mNetworkManagementService);

        setPrivateDnsSettings(PRIVATE_DNS_MODE_OFF, "ignored.example.com");
        verify(mNetworkManagementService, times(1)).setDnsConfigurationForNetwork(
                anyInt(), mStringArrayCaptor.capture(), any(), any(),
                eq(""), eq(EMPTY_STRING_ARRAY));
        assertEquals(2, mStringArrayCaptor.getValue().length);
        assertTrue(ArrayUtils.containsAll(mStringArrayCaptor.getValue(),
                new String[]{"2001:db8::1", "192.0.2.1"}));
        reset(mNetworkManagementService);

        setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com");
        verify(mNetworkManagementService, atLeastOnce()).setDnsConfigurationForNetwork(
                anyInt(), mStringArrayCaptor.capture(), any(), any(),
                eq(""), tlsServers.capture());
        assertEquals(2, mStringArrayCaptor.getValue().length);
        assertTrue(ArrayUtils.containsAll(mStringArrayCaptor.getValue(),
                new String[]{"2001:db8::1", "192.0.2.1"}));
        assertEquals(2, tlsServers.getValue().length);
        assertTrue(ArrayUtils.containsAll(tlsServers.getValue(),
                new String[]{"2001:db8::1", "192.0.2.1"}));
        reset(mNetworkManagementService);

        // Can't test strict mode without properly mocking out the DNS lookups.
    }

    private void checkDirectlyConnectedRoutes(Object callbackObj,
            Collection<LinkAddress> linkAddresses, Collection<RouteInfo> otherRoutes) {
        assertTrue(callbackObj instanceof LinkProperties);