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

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

Merge "Trigger NetworkCallback events when private DNS usage has changed." am: ec4ded52

am: ed67e235

Change-Id: I1e6c54ba016f6a165a302bd135a29d9332aaa235
parents 4afdd6fd ed67e235
Loading
Loading
Loading
Loading
+65 −0
Original line number Diff line number Diff line
@@ -52,6 +52,8 @@ import android.database.ContentObserver;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.PacketKeepalive;
import android.net.IConnectivityManager;
import android.net.IIpConnectivityMetrics;
import android.net.INetdEventCallback;
import android.net.INetworkManagementEventObserver;
import android.net.INetworkPolicyListener;
import android.net.INetworkPolicyManager;
@@ -140,6 +142,7 @@ import com.android.server.am.BatteryStatsService;
import com.android.server.connectivity.DataConnectionStats;
import com.android.server.connectivity.DnsManager;
import com.android.server.connectivity.DnsManager.PrivateDnsConfig;
import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
import com.android.server.connectivity.IpConnectivityMetrics;
import com.android.server.connectivity.KeepaliveTracker;
import com.android.server.connectivity.LingerMonitor;
@@ -155,6 +158,7 @@ import com.android.server.connectivity.PermissionMonitor;
import com.android.server.connectivity.Tethering;
import com.android.server.connectivity.Vpn;
import com.android.server.connectivity.tethering.TetheringDependencies;
import com.android.server.net.BaseNetdEventCallback;
import com.android.server.net.BaseNetworkObserver;
import com.android.server.net.LockdownVpnTracker;
import com.android.server.net.NetworkPolicyManagerInternal;
@@ -256,6 +260,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
    private INetworkStatsService mStatsService;
    private INetworkPolicyManager mPolicyManager;
    private NetworkPolicyManagerInternal mPolicyManagerInternal;
    private IIpConnectivityMetrics mIpConnectivityMetrics;

    private String mCurrentTcpBufferSizes;

@@ -414,6 +419,9 @@ public class ConnectivityService extends IConnectivityManager.Stub
    // Handle changes in Private DNS settings.
    private static final int EVENT_PRIVATE_DNS_SETTINGS_CHANGED = 37;

    // Handle private DNS validation status updates.
    private static final int EVENT_PRIVATE_DNS_VALIDATION_UPDATE = 38;

    private static String eventName(int what) {
        return sMagicDecoderRing.get(what, Integer.toString(what));
    }
@@ -1553,6 +1561,41 @@ public class ConnectivityService extends IConnectivityManager.Stub
        return true;
    }

    @VisibleForTesting
    protected final INetdEventCallback mNetdEventCallback = new BaseNetdEventCallback() {
        @Override
        public void onPrivateDnsValidationEvent(int netId, String ipAddress,
                String hostname, boolean validated) {
            try {
                mHandler.sendMessage(mHandler.obtainMessage(
                        EVENT_PRIVATE_DNS_VALIDATION_UPDATE,
                        new PrivateDnsValidationUpdate(netId,
                                InetAddress.parseNumericAddress(ipAddress),
                                hostname, validated)));
            } catch (IllegalArgumentException e) {
                loge("Error parsing ip address in validation event");
            }
        }
    };

    @VisibleForTesting
    protected void registerNetdEventCallback() {
        mIpConnectivityMetrics =
                (IIpConnectivityMetrics) IIpConnectivityMetrics.Stub.asInterface(
                ServiceManager.getService(IpConnectivityLog.SERVICE_NAME));
        if (mIpConnectivityMetrics == null) {
            Slog.wtf(TAG, "Missing IIpConnectivityMetrics");
        }

        try {
            mIpConnectivityMetrics.addNetdEventCallback(
                    INetdEventCallback.CALLBACK_CALLER_CONNECTIVITY_SERVICE,
                    mNetdEventCallback);
        } catch (Exception e) {
            loge("Error registering netd callback: " + e);
        }
    }

    private final INetworkPolicyListener mPolicyListener = new NetworkPolicyManager.Listener() {
        @Override
        public void onUidRulesChanged(int uid, int uidRules) {
@@ -1738,6 +1781,7 @@ public class ConnectivityService extends IConnectivityManager.Stub

    void systemReady() {
        loadGlobalProxy();
        registerNetdEventCallback();

        synchronized (this) {
            mSystemReady = true;
@@ -2288,6 +2332,9 @@ public class ConnectivityService extends IConnectivityManager.Stub

        for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
            handlePerNetworkPrivateDnsConfig(nai, cfg);
            if (networkRequiresValidation(nai)) {
                handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties));
            }
        }
    }

@@ -2312,6 +2359,15 @@ public class ConnectivityService extends IConnectivityManager.Stub
        updateDnses(nai.linkProperties, null, nai.network.netId);
    }

    private void handlePrivateDnsValidationUpdate(PrivateDnsValidationUpdate update) {
        NetworkAgentInfo nai = getNetworkAgentInfoForNetId(update.netId);
        if (nai == null) {
            return;
        }
        mDnsManager.updatePrivateDnsValidation(update);
        handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties));
    }

    private void updateLingerState(NetworkAgentInfo nai, long now) {
        // 1. Update the linger timer. If it's changed, reschedule or cancel the alarm.
        // 2. If the network was lingering and there are now requests, unlinger it.
@@ -3002,6 +3058,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
                case EVENT_PRIVATE_DNS_SETTINGS_CHANGED:
                    handlePrivateDnsSettingsChanged();
                    break;
                case EVENT_PRIVATE_DNS_VALIDATION_UPDATE:
                    handlePrivateDnsValidationUpdate(
                            (PrivateDnsValidationUpdate) msg.obj);
                    break;
            }
        }
    }
@@ -4575,6 +4635,11 @@ public class ConnectivityService extends IConnectivityManager.Stub

        updateRoutes(newLp, oldLp, netId);
        updateDnses(newLp, oldLp, netId);
        // Make sure LinkProperties represents the latest private DNS status.
        // This does not need to be done before updateDnses because the
        // LinkProperties are not the source of the private DNS configuration.
        // updateDnses will fetch the private DNS configuration from DnsManager.
        mDnsManager.updatePrivateDnsStatus(netId, newLp);

        // Start or stop clat accordingly to network state.
        networkAgent.updateClat(mNetd);
+135 −4
Original line number Diff line number Diff line
@@ -37,10 +37,10 @@ import android.net.Uri;
import android.net.dns.ResolvUtil;
import android.os.Binder;
import android.os.INetworkManagementService;
import android.os.Handler;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Pair;
import android.util.Slog;

import com.android.server.connectivity.MockableSystemProperties;
@@ -50,8 +50,12 @@ import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.Set;
import java.util.StringJoiner;


@@ -110,6 +114,7 @@ import java.util.StringJoiner;
 */
public class DnsManager {
    private static final String TAG = DnsManager.class.getSimpleName();
    private static final PrivateDnsConfig PRIVATE_DNS_OFF = new PrivateDnsConfig();

    /* Defaults for resolver parameters. */
    private static final int DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS = 1800;
@@ -183,11 +188,89 @@ public class DnsManager {
        };
    }

    public static class PrivateDnsValidationUpdate {
        final public int netId;
        final public InetAddress ipAddress;
        final public String hostname;
        final public boolean validated;

        public PrivateDnsValidationUpdate(int netId, InetAddress ipAddress,
                String hostname, boolean validated) {
            this.netId = netId;
            this.ipAddress = ipAddress;
            this.hostname = hostname;
            this.validated = validated;
        }
    }

    private static class PrivateDnsValidationStatuses {
        enum ValidationStatus {
            IN_PROGRESS,
            FAILED,
            SUCCEEDED
        }

        // Validation statuses of <hostname, ipAddress> pairs for a single netId
        private Map<Pair<String, InetAddress>, ValidationStatus> mValidationMap;

        private PrivateDnsValidationStatuses() {
            mValidationMap = new HashMap<>();
        }

        private boolean hasValidatedServer() {
            for (ValidationStatus status : mValidationMap.values()) {
                if (status == ValidationStatus.SUCCEEDED) {
                    return true;
                }
            }
            return false;
        }

        private void updateTrackedDnses(String[] ipAddresses, String hostname) {
            Set<Pair<String, InetAddress>> latestDnses = new HashSet<>();
            for (String ipAddress : ipAddresses) {
                try {
                    latestDnses.add(new Pair(hostname,
                            InetAddress.parseNumericAddress(ipAddress)));
                } catch (IllegalArgumentException e) {}
            }
            // Remove <hostname, ipAddress> pairs that should not be tracked.
            for (Iterator<Map.Entry<Pair<String, InetAddress>, ValidationStatus>> it =
                    mValidationMap.entrySet().iterator(); it.hasNext(); ) {
                Map.Entry<Pair<String, InetAddress>, ValidationStatus> entry = it.next();
                if (!latestDnses.contains(entry.getKey())) {
                    it.remove();
                }
            }
            // Add new <hostname, ipAddress> pairs that should be tracked.
            for (Pair<String, InetAddress> p : latestDnses) {
                if (!mValidationMap.containsKey(p)) {
                    mValidationMap.put(p, ValidationStatus.IN_PROGRESS);
                }
            }
        }

        private void updateStatus(PrivateDnsValidationUpdate update) {
            Pair<String, InetAddress> p = new Pair(update.hostname,
                    update.ipAddress);
            if (!mValidationMap.containsKey(p)) {
                return;
            }
            if (update.validated) {
                mValidationMap.put(p, ValidationStatus.SUCCEEDED);
            } else {
                mValidationMap.put(p, ValidationStatus.FAILED);
            }
        }
    }

    private final Context mContext;
    private final ContentResolver mContentResolver;
    private final INetworkManagementService mNMS;
    private final MockableSystemProperties mSystemProperties;
    // TODO: Replace these Maps with SparseArrays.
    private final Map<Integer, PrivateDnsConfig> mPrivateDnsMap;
    private final Map<Integer, PrivateDnsValidationStatuses> mPrivateDnsValidationMap;

    private int mNumDnsEntries;
    private int mSampleValidity;
@@ -203,6 +286,7 @@ public class DnsManager {
        mNMS = nms;
        mSystemProperties = sp;
        mPrivateDnsMap = new HashMap<>();
        mPrivateDnsValidationMap = new HashMap<>();

        // TODO: Create and register ContentObservers to track every setting
        // used herein, posting messages to respond to changes.
@@ -214,6 +298,7 @@ public class DnsManager {

    public void removeNetwork(Network network) {
        mPrivateDnsMap.remove(network.netId);
        mPrivateDnsValidationMap.remove(network.netId);
    }

    public PrivateDnsConfig updatePrivateDns(Network network, PrivateDnsConfig cfg) {
@@ -223,6 +308,40 @@ public class DnsManager {
                : mPrivateDnsMap.remove(network.netId);
    }

    public void updatePrivateDnsStatus(int netId, LinkProperties lp) {
        // Use the PrivateDnsConfig data pushed to this class instance
        // from ConnectivityService.
        final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId,
                PRIVATE_DNS_OFF);

        final boolean useTls = privateDnsCfg.useTls;
        final boolean strictMode = privateDnsCfg.inStrictMode();
        final String tlsHostname = strictMode ? privateDnsCfg.hostname : "";

        if (strictMode) {
            lp.setUsePrivateDns(true);
            lp.setPrivateDnsServerName(tlsHostname);
        } else if (useTls) {
            // We are in opportunistic mode. Private DNS should be used if there
            // is a known DNS-over-TLS validated server.
            boolean validated = mPrivateDnsValidationMap.containsKey(netId) &&
                    mPrivateDnsValidationMap.get(netId).hasValidatedServer();
            lp.setUsePrivateDns(validated);
            lp.setPrivateDnsServerName(null);
        } else {
            // Private DNS is disabled.
            lp.setUsePrivateDns(false);
            lp.setPrivateDnsServerName(null);
        }
    }

    public void updatePrivateDnsValidation(PrivateDnsValidationUpdate update) {
        final PrivateDnsValidationStatuses statuses =
                mPrivateDnsValidationMap.get(update.netId);
        if (statuses == null) return;
        statuses.updateStatus(update);
    }

    public void setDnsConfigurationForNetwork(
            int netId, LinkProperties lp, boolean isDefaultNetwork) {
        final String[] assignedServers = NetworkUtils.makeStrings(lp.getDnsServers());
@@ -238,10 +357,11 @@ public class DnsManager {
        //
        // At this time we do not attempt to enable Private DNS on non-Internet
        // networks like IMS.
        final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.get(netId);
        final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId,
                PRIVATE_DNS_OFF);

        final boolean useTls = (privateDnsCfg != null) && privateDnsCfg.useTls;
        final boolean strictMode = (privateDnsCfg != null) && privateDnsCfg.inStrictMode();
        final boolean useTls = privateDnsCfg.useTls;
        final boolean strictMode = privateDnsCfg.inStrictMode();
        final String tlsHostname = strictMode ? privateDnsCfg.hostname : "";
        final String[] tlsServers =
                strictMode ? NetworkUtils.makeStrings(
@@ -251,6 +371,17 @@ public class DnsManager {
                : useTls ? assignedServers  // Opportunistic
                : new String[0];            // Off

        // Prepare to track the validation status of the DNS servers in the
        // resolver config when private DNS is in opportunistic or strict mode.
        if (useTls) {
            if (!mPrivateDnsValidationMap.containsKey(netId)) {
                mPrivateDnsValidationMap.put(netId, new PrivateDnsValidationStatuses());
            }
            mPrivateDnsValidationMap.get(netId).updateTrackedDnses(tlsServers, tlsHostname);
        } else {
            mPrivateDnsValidationMap.remove(netId);
        }

        Slog.d(TAG, String.format("setDnsConfigurationForNetwork(%d, %s, %s, %s, %s, %s)",
                netId, Arrays.toString(assignedServers), Arrays.toString(domainStrs),
                Arrays.toString(params), tlsHostname, Arrays.toString(tlsServers)));
+131 −1
Original line number Diff line number Diff line
@@ -161,6 +161,7 @@ import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -879,6 +880,10 @@ public class ConnectivityServiceTest {
            return mMetricsService;
        }

        @Override
        protected void registerNetdEventCallback() {
        }

        public WrappedNetworkMonitor getLastCreatedWrappedNetworkMonitor() {
            return mLastCreatedNetworkMonitor;
        }
@@ -3777,6 +3782,11 @@ public class ConnectivityServiceTest {
        // The default on Android is opportunistic mode ("Automatic").
        setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com");

        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
        final NetworkRequest cellRequest = new NetworkRequest.Builder()
                .addTransportType(TRANSPORT_CELLULAR).build();
        mCm.requestNetwork(cellRequest, cellNetworkCallback);

        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
        waitForIdle();
        // CS tells netd about the empty DNS config for this network.
@@ -3812,6 +3822,14 @@ public class ConnectivityServiceTest {
        assertTrue(ArrayUtils.containsAll(tlsServers.getValue(),
                new String[]{"2001:db8::1", "192.0.2.1"}));
        reset(mNetworkManagementService);
        cellNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
        cellNetworkCallback.expectCallback(CallbackState.NETWORK_CAPABILITIES,
                mCellNetworkAgent);
        CallbackInfo cbi = cellNetworkCallback.expectCallback(
                CallbackState.LINK_PROPERTIES, mCellNetworkAgent);
        cellNetworkCallback.assertNoCallback();
        assertFalse(((LinkProperties)cbi.arg).isPrivateDnsActive());
        assertNull(((LinkProperties)cbi.arg).getPrivateDnsServerName());

        setPrivateDnsSettings(PRIVATE_DNS_MODE_OFF, "ignored.example.com");
        verify(mNetworkManagementService, times(1)).setDnsConfigurationForNetwork(
@@ -3821,6 +3839,7 @@ public class ConnectivityServiceTest {
        assertTrue(ArrayUtils.containsAll(mStringArrayCaptor.getValue(),
                new String[]{"2001:db8::1", "192.0.2.1"}));
        reset(mNetworkManagementService);
        cellNetworkCallback.assertNoCallback();

        setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com");
        verify(mNetworkManagementService, atLeastOnce()).setDnsConfigurationForNetwork(
@@ -3833,8 +3852,112 @@ public class ConnectivityServiceTest {
        assertTrue(ArrayUtils.containsAll(tlsServers.getValue(),
                new String[]{"2001:db8::1", "192.0.2.1"}));
        reset(mNetworkManagementService);
        cellNetworkCallback.assertNoCallback();

        // Can't test strict mode without properly mocking out the DNS lookups.
        setPrivateDnsSettings(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, "strict.example.com");
        // Can't test dns configuration for strict mode without properly mocking
        // out the DNS lookups, but can test that LinkProperties is updated.
        cbi = cellNetworkCallback.expectCallback(CallbackState.LINK_PROPERTIES,
                mCellNetworkAgent);
        cellNetworkCallback.assertNoCallback();
        assertTrue(((LinkProperties)cbi.arg).isPrivateDnsActive());
        assertEquals("strict.example.com", ((LinkProperties)cbi.arg).getPrivateDnsServerName());
    }

    @Test
    public void testLinkPropertiesWithPrivateDnsValidationEvents() throws Exception {
        // The default on Android is opportunistic mode ("Automatic").
        setPrivateDnsSettings(PRIVATE_DNS_MODE_OPPORTUNISTIC, "ignored.example.com");

        final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
        final NetworkRequest cellRequest = new NetworkRequest.Builder()
                .addTransportType(TRANSPORT_CELLULAR).build();
        mCm.requestNetwork(cellRequest, cellNetworkCallback);

        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
        waitForIdle();
        LinkProperties lp = new LinkProperties();
        mCellNetworkAgent.sendLinkProperties(lp);
        mCellNetworkAgent.connect(false);
        waitForIdle();
        cellNetworkCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
        cellNetworkCallback.expectCallback(CallbackState.NETWORK_CAPABILITIES,
                mCellNetworkAgent);
        CallbackInfo cbi = cellNetworkCallback.expectCallback(
                CallbackState.LINK_PROPERTIES, mCellNetworkAgent);
        cellNetworkCallback.assertNoCallback();
        assertFalse(((LinkProperties)cbi.arg).isPrivateDnsActive());
        assertNull(((LinkProperties)cbi.arg).getPrivateDnsServerName());
        Set<InetAddress> dnsServers = new HashSet<>();
        checkDnsServers(cbi.arg, dnsServers);

        // Send a validation event for a server that is not part of the current
        // resolver config. The validation event should be ignored.
        mService.mNetdEventCallback.onPrivateDnsValidationEvent(
                mCellNetworkAgent.getNetwork().netId, "", "145.100.185.18", true);
        cellNetworkCallback.assertNoCallback();

        // Add a dns server to the LinkProperties.
        LinkProperties lp2 = new LinkProperties(lp);
        lp2.addDnsServer(InetAddress.getByName("145.100.185.16"));
        mCellNetworkAgent.sendLinkProperties(lp2);
        cbi = cellNetworkCallback.expectCallback(CallbackState.LINK_PROPERTIES,
                mCellNetworkAgent);
        cellNetworkCallback.assertNoCallback();
        assertFalse(((LinkProperties)cbi.arg).isPrivateDnsActive());
        assertNull(((LinkProperties)cbi.arg).getPrivateDnsServerName());
        dnsServers.add(InetAddress.getByName("145.100.185.16"));
        checkDnsServers(cbi.arg, dnsServers);

        // Send a validation event containing a hostname that is not part of
        // the current resolver config. The validation event should be ignored.
        mService.mNetdEventCallback.onPrivateDnsValidationEvent(
                mCellNetworkAgent.getNetwork().netId, "145.100.185.16", "hostname", true);
        cellNetworkCallback.assertNoCallback();

        // Send a validation event where validation failed.
        mService.mNetdEventCallback.onPrivateDnsValidationEvent(
                mCellNetworkAgent.getNetwork().netId, "145.100.185.16", "", false);
        cellNetworkCallback.assertNoCallback();

        // Send a validation event where validation succeeded for a server in
        // the current resolver config. A LinkProperties callback with updated
        // private dns fields should be sent.
        mService.mNetdEventCallback.onPrivateDnsValidationEvent(
                mCellNetworkAgent.getNetwork().netId, "145.100.185.16", "", true);
        cbi = cellNetworkCallback.expectCallback(CallbackState.LINK_PROPERTIES,
                mCellNetworkAgent);
        cellNetworkCallback.assertNoCallback();
        assertTrue(((LinkProperties)cbi.arg).isPrivateDnsActive());
        assertNull(((LinkProperties)cbi.arg).getPrivateDnsServerName());
        checkDnsServers(cbi.arg, dnsServers);

        // The private dns fields in LinkProperties should be preserved when
        // the network agent sends unrelated changes.
        LinkProperties lp3 = new LinkProperties(lp2);
        lp3.setMtu(1300);
        mCellNetworkAgent.sendLinkProperties(lp3);
        cbi = cellNetworkCallback.expectCallback(CallbackState.LINK_PROPERTIES,
                mCellNetworkAgent);
        cellNetworkCallback.assertNoCallback();
        assertTrue(((LinkProperties)cbi.arg).isPrivateDnsActive());
        assertNull(((LinkProperties)cbi.arg).getPrivateDnsServerName());
        checkDnsServers(cbi.arg, dnsServers);
        assertEquals(1300, ((LinkProperties)cbi.arg).getMtu());

        // Removing the only validated server should affect the private dns
        // fields in LinkProperties.
        LinkProperties lp4 = new LinkProperties(lp3);
        lp4.removeDnsServer(InetAddress.getByName("145.100.185.16"));
        mCellNetworkAgent.sendLinkProperties(lp4);
        cbi = cellNetworkCallback.expectCallback(CallbackState.LINK_PROPERTIES,
                mCellNetworkAgent);
        cellNetworkCallback.assertNoCallback();
        assertFalse(((LinkProperties)cbi.arg).isPrivateDnsActive());
        assertNull(((LinkProperties)cbi.arg).getPrivateDnsServerName());
        dnsServers.remove(InetAddress.getByName("145.100.185.16"));
        checkDnsServers(cbi.arg, dnsServers);
        assertEquals(1300, ((LinkProperties)cbi.arg).getMtu());
    }

    private void checkDirectlyConnectedRoutes(Object callbackObj,
@@ -3854,6 +3977,13 @@ public class ConnectivityServiceTest {
        assertTrue(observedRoutes.containsAll(expectedRoutes));
    }

    private static void checkDnsServers(Object callbackObj, Set<InetAddress> dnsServers) {
        assertTrue(callbackObj instanceof LinkProperties);
        LinkProperties lp = (LinkProperties) callbackObj;
        assertEquals(dnsServers.size(), lp.getDnsServers().size());
        assertTrue(lp.getDnsServers().containsAll(dnsServers));
    }

    private static <T> void assertEmpty(T[] ts) {
        int length = ts.length;
        assertEquals("expected empty array, but length was " + length, 0, length);
+201 −0

File added.

Preview size limit exceeded, changes collapsed.