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

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

Merge "Pass all offload-exempt prefixes into OffloadController" am: 7fc4f63f

am: 96ef8589

Change-Id: Ifd3817bd4569682d77a70f8192432391a573f6d8
parents b6b95424 96ef8589
Loading
Loading
Loading
Loading
+180 −170
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ import android.hardware.usb.UsbManager;
import android.net.ConnectivityManager;
import android.net.INetworkPolicyManager;
import android.net.INetworkStatsService;
import android.net.IpPrefix;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
@@ -107,6 +108,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;


@@ -213,7 +215,7 @@ public class Tethering extends BaseNetworkObserver {
                mContext.getContentResolver(),
                mLog);
        mUpstreamNetworkMonitor = new UpstreamNetworkMonitor(
                mContext, mTetherMasterSM, TetherMasterSM.EVENT_UPSTREAM_CALLBACK, mLog);
                mContext, mTetherMasterSM, mLog, TetherMasterSM.EVENT_UPSTREAM_CALLBACK );
        mForwardedDownstreams = new HashSet<>();
        mSimChange = new SimChangeListener(
                mContext, mTetherMasterSM.getHandler(), () -> reevaluateSimCardProvisioning());
@@ -1076,7 +1078,7 @@ public class Tethering extends BaseNetworkObserver {
    // Needed because the canonical source of upstream truth is just the
    // upstream interface name, |mCurrentUpstreamIface|.  This is ripe for
    // future simplification, once the upstream Network is canonical.
    boolean pertainsToCurrentUpstream(NetworkState ns) {
    private boolean pertainsToCurrentUpstream(NetworkState ns) {
        if (ns != null && ns.linkProperties != null && mCurrentUpstreamIface != null) {
            for (String ifname : ns.linkProperties.getAllInterfaceNames()) {
                if (mCurrentUpstreamIface.equals(ifname)) {
@@ -1110,6 +1112,12 @@ public class Tethering extends BaseNetworkObserver {
        }
    }

    private void startOffloadController() {
        mOffloadController.start();
        mOffloadController.updateExemptPrefixes(
                mUpstreamNetworkMonitor.getOffloadExemptPrefixes());
    }

    class TetherMasterSM extends StateMachine {
        private static final int BASE_MASTER                    = Protocol.BASE_TETHERING;
        // an interface SM has requested Tethering/Local Hotspot
@@ -1203,12 +1211,6 @@ public class Tethering extends BaseNetworkObserver {
            }
        }

        class TetherMasterUtilState extends State {
            @Override
            public boolean processMessage(Message m) {
                return false;
            }

        protected boolean turnOnMasterTetherSettings() {
            final TetheringConfiguration cfg = mConfig;
            try {
@@ -1338,9 +1340,7 @@ public class Tethering extends BaseNetworkObserver {

        protected void handleNewUpstreamNetworkState(NetworkState ns) {
            mIPv6TetheringCoordinator.updateUpstreamNetworkState(ns);
                mOffloadController.setUpstreamLinkProperties(
                        (ns != null) ? ns.linkProperties : null);
            }
            mOffloadController.setUpstreamLinkProperties((ns != null) ? ns.linkProperties : null);
        }

        private void handleInterfaceServingStateActive(int mode, TetherInterfaceStateMachine who) {
@@ -1389,7 +1389,61 @@ public class Tethering extends BaseNetworkObserver {
            }
        }

        class TetherModeAliveState extends TetherMasterUtilState {
        private void handleUpstreamNetworkMonitorCallback(int arg1, Object o) {
            if (arg1 == UpstreamNetworkMonitor.NOTIFY_EXEMPT_PREFIXES) {
                mOffloadController.updateExemptPrefixes((Set<IpPrefix>) o);
                return;
            }

            final NetworkState ns = (NetworkState) o;

            if (ns == null || !pertainsToCurrentUpstream(ns)) {
                // TODO: In future, this is where upstream evaluation and selection
                // could be handled for notifications which include sufficient data.
                // For example, after CONNECTIVITY_ACTION listening is removed, here
                // is where we could observe a Wi-Fi network becoming available and
                // passing validation.
                if (mCurrentUpstreamIface == null) {
                    // If we have no upstream interface, try to run through upstream
                    // selection again.  If, for example, IPv4 connectivity has shown up
                    // after IPv6 (e.g., 464xlat became available) we want the chance to
                    // notice and act accordingly.
                    chooseUpstreamType(false);
                }
                return;
            }

            switch (arg1) {
                case UpstreamNetworkMonitor.EVENT_ON_AVAILABLE:
                    // The default network changed, or DUN connected
                    // before this callback was processed. Updates
                    // for the current NetworkCapabilities and
                    // LinkProperties have been requested (default
                    // request) or are being sent shortly (DUN). Do
                    // nothing until they arrive; if no updates
                    // arrive there's nothing to do.
                    break;
                case UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES:
                    handleNewUpstreamNetworkState(ns);
                    break;
                case UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES:
                    setDnsForwarders(ns.network, ns.linkProperties);
                    handleNewUpstreamNetworkState(ns);
                    break;
                case UpstreamNetworkMonitor.EVENT_ON_LOST:
                    // TODO: Re-evaluate possible upstreams. Currently upstream
                    // reevaluation is triggered via received CONNECTIVITY_ACTION
                    // broadcasts that result in being passed a
                    // TetherMasterSM.CMD_UPSTREAM_CHANGED.
                    handleNewUpstreamNetworkState(null);
                    break;
                default:
                    mLog.e("Unknown arg1 value: " + arg1);
                    break;
            }
        }

        class TetherModeAliveState extends State {
            boolean mUpstreamWanted = false;
            boolean mTryCell = true;

@@ -1407,7 +1461,7 @@ public class Tethering extends BaseNetworkObserver {
                // TODO: De-duplicate with updateUpstreamWanted() below.
                if (upstreamWanted()) {
                    mUpstreamWanted = true;
                    mOffloadController.start();
                    startOffloadController();
                    chooseUpstreamType(true);
                    mTryCell = false;
                }
@@ -1427,7 +1481,7 @@ public class Tethering extends BaseNetworkObserver {
                mUpstreamWanted = upstreamWanted();
                if (mUpstreamWanted != previousUpstreamWanted) {
                    if (mUpstreamWanted) {
                        mOffloadController.start();
                        startOffloadController();
                    } else {
                        mOffloadController.stop();
                    }
@@ -1507,52 +1561,8 @@ public class Tethering extends BaseNetworkObserver {
                        break;
                    case EVENT_UPSTREAM_CALLBACK: {
                        updateUpstreamWanted();
                        if (!mUpstreamWanted) break;

                        final NetworkState ns = (NetworkState) message.obj;

                        if (ns == null || !pertainsToCurrentUpstream(ns)) {
                            // TODO: In future, this is where upstream evaluation and selection
                            // could be handled for notifications which include sufficient data.
                            // For example, after CONNECTIVITY_ACTION listening is removed, here
                            // is where we could observe a Wi-Fi network becoming available and
                            // passing validation.
                            if (mCurrentUpstreamIface == null) {
                                // If we have no upstream interface, try to run through upstream
                                // selection again.  If, for example, IPv4 connectivity has shown up
                                // after IPv6 (e.g., 464xlat became available) we want the chance to
                                // notice and act accordingly.
                                chooseUpstreamType(false);
                            }
                            break;
                        }

                        switch (message.arg1) {
                            case UpstreamNetworkMonitor.EVENT_ON_AVAILABLE:
                                // The default network changed, or DUN connected
                                // before this callback was processed. Updates
                                // for the current NetworkCapabilities and
                                // LinkProperties have been requested (default
                                // request) or are being sent shortly (DUN). Do
                                // nothing until they arrive; if no updates
                                // arrive there's nothing to do.
                                break;
                            case UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES:
                                handleNewUpstreamNetworkState(ns);
                                break;
                            case UpstreamNetworkMonitor.EVENT_ON_LINKPROPERTIES:
                                setDnsForwarders(ns.network, ns.linkProperties);
                                handleNewUpstreamNetworkState(ns);
                                break;
                            case UpstreamNetworkMonitor.EVENT_ON_LOST:
                                // TODO: Re-evaluate possible upstreams. Currently upstream
                                // reevaluation is triggered via received CONNECTIVITY_ACTION
                                // broadcasts that result in being passed a
                                // TetherMasterSM.CMD_UPSTREAM_CHANGED.
                                handleNewUpstreamNetworkState(null);
                                break;
                            default:
                                break;
                        if (mUpstreamWanted) {
                            handleUpstreamNetworkMonitorCallback(message.arg1, message.obj);
                        }
                        break;
                    }
+14 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.server.connectivity.tethering;
import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;

import android.content.ContentResolver;
import android.net.IpPrefix;
import android.net.LinkProperties;
import android.net.RouteInfo;
import android.net.util.SharedLog;
@@ -28,6 +29,7 @@ import android.provider.Settings;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Set;

/**
 * A class to encapsulate the business logic of programming the tethering
@@ -45,6 +47,7 @@ public class OffloadController {
    private boolean mConfigInitialized;
    private boolean mControlInitialized;
    private LinkProperties mUpstreamLinkProperties;
    private Set<IpPrefix> mExemptPrefixes;

    public OffloadController(Handler h, OffloadHardwareInterface hwi,
            ContentResolver contentResolver, SharedLog log) {
@@ -108,6 +111,17 @@ public class OffloadController {
        pushUpstreamParameters();
    }

    public void updateExemptPrefixes(Set<IpPrefix> exemptPrefixes) {
        if (!started()) return;

        mExemptPrefixes = exemptPrefixes;
        // TODO:
        //     - add IP addresses from all downstream link properties
        //     - add routes from all non-tethering downstream link properties
        //     - remove any 64share prefixes
        //     - push this to the HAL
    }

    public void notifyDownstreamLinkProperties(LinkProperties lp) {
        if (!started()) return;

+68 −8
Original line number Diff line number Diff line
@@ -26,18 +26,24 @@ import android.os.Handler;
import android.os.Looper;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.NetworkState;
import android.net.util.NetworkConstants;
import android.net.util.SharedLog;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.StateMachine;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;


/**
@@ -66,10 +72,16 @@ public class UpstreamNetworkMonitor {
    private static final boolean DBG = false;
    private static final boolean VDBG = false;

    private static final IpPrefix[] MINIMUM_LOCAL_PREFIXES_SET = {
            prefix("127.0.0.0/8"), prefix("169.254.0.0/16"),
            prefix("::/3"), prefix("fe80::/64"), prefix("fc00::/7"), prefix("ff00::/8"),
    };

    public static final int EVENT_ON_AVAILABLE      = 1;
    public static final int EVENT_ON_CAPABILITIES   = 2;
    public static final int EVENT_ON_LINKPROPERTIES = 3;
    public static final int EVENT_ON_LOST           = 4;
    public static final int NOTIFY_EXEMPT_PREFIXES  = 10;

    private static final int CALLBACK_LISTEN_ALL = 1;
    private static final int CALLBACK_TRACK_DEFAULT = 2;
@@ -81,6 +93,7 @@ public class UpstreamNetworkMonitor {
    private final Handler mHandler;
    private final int mWhat;
    private final HashMap<Network, NetworkState> mNetworkMap = new HashMap<>();
    private HashSet<IpPrefix> mOffloadExemptPrefixes;
    private ConnectivityManager mCM;
    private NetworkCallback mListenAllCallback;
    private NetworkCallback mDefaultNetworkCallback;
@@ -88,18 +101,19 @@ public class UpstreamNetworkMonitor {
    private boolean mDunRequired;
    private Network mCurrentDefault;

    public UpstreamNetworkMonitor(Context ctx, StateMachine tgt, int what, SharedLog log) {
    public UpstreamNetworkMonitor(Context ctx, StateMachine tgt, SharedLog log, int what) {
        mContext = ctx;
        mTarget = tgt;
        mHandler = mTarget.getHandler();
        mWhat = what;
        mLog = log.forSubComponent(TAG);
        mWhat = what;
        mOffloadExemptPrefixes = allOffloadExemptPrefixes(mNetworkMap.values());
    }

    @VisibleForTesting
    public UpstreamNetworkMonitor(
            StateMachine tgt, int what, ConnectivityManager cm, SharedLog log) {
        this(null, tgt, what, log);
            ConnectivityManager cm, StateMachine tgt, SharedLog log, int what) {
        this((Context) null, tgt, log, what);
        mCM = cm;
    }

@@ -209,6 +223,10 @@ public class UpstreamNetworkMonitor {
        return typeStatePair.ns;
    }

    public Set<IpPrefix> getOffloadExemptPrefixes() {
        return (Set<IpPrefix>) mOffloadExemptPrefixes.clone();
    }

    private void handleAvailable(int callbackType, Network network) {
        if (VDBG) Log.d(TAG, "EVENT_ON_AVAILABLE for " + network);

@@ -342,6 +360,14 @@ public class UpstreamNetworkMonitor {
        notifyTarget(EVENT_ON_LOST, mNetworkMap.remove(network));
    }

    private void recomputeOffloadExemptPrefixes() {
        final HashSet<IpPrefix> exemptPrefixes = allOffloadExemptPrefixes(mNetworkMap.values());
        if (!mOffloadExemptPrefixes.equals(exemptPrefixes)) {
            mOffloadExemptPrefixes = exemptPrefixes;
            notifyTarget(NOTIFY_EXEMPT_PREFIXES, exemptPrefixes.clone());
        }
    }

    // Fetch (and cache) a ConnectivityManager only if and when we need one.
    private ConnectivityManager cm() {
        if (mCM == null) {
@@ -376,6 +402,7 @@ public class UpstreamNetworkMonitor {
        @Override
        public void onLinkPropertiesChanged(Network network, LinkProperties newLp) {
            handleLinkProp(network, newLp);
            recomputeOffloadExemptPrefixes();
        }

        // TODO: Handle onNetworkSuspended();
@@ -384,6 +411,7 @@ public class UpstreamNetworkMonitor {
        @Override
        public void onLost(Network network) {
            handleLost(mCallbackType, network);
            recomputeOffloadExemptPrefixes();
        }
    }

@@ -395,16 +423,16 @@ public class UpstreamNetworkMonitor {
        notifyTarget(which, mNetworkMap.get(network));
    }

    private void notifyTarget(int which, NetworkState netstate) {
        mTarget.sendMessage(mWhat, which, 0, netstate);
    private void notifyTarget(int which, Object obj) {
        mTarget.sendMessage(mWhat, which, 0, obj);
    }

    static private class TypeStatePair {
    private static class TypeStatePair {
        public int type = TYPE_NONE;
        public NetworkState ns = null;
    }

    static private TypeStatePair findFirstAvailableUpstreamByType(
    private static TypeStatePair findFirstAvailableUpstreamByType(
            Iterable<NetworkState> netStates, Iterable<Integer> preferredTypes) {
        final TypeStatePair result = new TypeStatePair();

@@ -431,4 +459,36 @@ public class UpstreamNetworkMonitor {

        return result;
    }

    private static HashSet<IpPrefix> allOffloadExemptPrefixes(Iterable<NetworkState> netStates) {
        final HashSet<IpPrefix> prefixSet = new HashSet<>();

        addDefaultLocalPrefixes(prefixSet);

        for (NetworkState ns : netStates) {
            addOffloadExemptPrefixes(prefixSet, ns.linkProperties);
        }

        return prefixSet;
    }

    private static void addDefaultLocalPrefixes(Set<IpPrefix> prefixSet) {
        Collections.addAll(prefixSet, MINIMUM_LOCAL_PREFIXES_SET);
    }

    private static void addOffloadExemptPrefixes(Set<IpPrefix> prefixSet, LinkProperties lp) {
        if (lp == null) return;

        for (LinkAddress linkAddr : lp.getAllLinkAddresses()) {
            prefixSet.add(new IpPrefix(linkAddr.getAddress(), linkAddr.getPrefixLength()));
        }

        // TODO: Consider adding other non-default routes associated with this
        // network. Traffic to these destinations should perhaps not go through
        // the Internet (upstream).
    }

    private static IpPrefix prefix(String prefixStr) {
        return new IpPrefix(prefixStr);
    }
}
+125 −1
Original line number Diff line number Diff line
@@ -44,6 +44,9 @@ import android.os.Message;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.IConnectivityManager;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
@@ -66,6 +69,7 @@ import org.mockito.MockitoAnnotations;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@@ -77,6 +81,9 @@ import java.util.Set;
public class UpstreamNetworkMonitorTest {
    private static final int EVENT_UNM_UPDATE = 1;

    private static final boolean INCLUDES = true;
    private static final boolean EXCLUDES = false;

    @Mock private Context mContext;
    @Mock private IConnectivityManager mCS;
    @Mock private SharedLog mLog;
@@ -94,7 +101,8 @@ public class UpstreamNetworkMonitorTest {

        mCM = spy(new TestConnectivityManager(mContext, mCS));
        mSM = new TestStateMachine();
        mUNM = new UpstreamNetworkMonitor(mSM, EVENT_UNM_UPDATE, (ConnectivityManager) mCM, mLog);
        mUNM = new UpstreamNetworkMonitor(
                (ConnectivityManager) mCM, mSM, mLog, EVENT_UNM_UPDATE);
    }

    @After public void tearDown() throws Exception {
@@ -315,6 +323,101 @@ public class UpstreamNetworkMonitorTest {
        assertTrue(netReq.networkCapabilities.hasCapability(NET_CAPABILITY_DUN));
    }

    @Test
    public void testOffloadExemptPrefixes() throws Exception {
        mUNM.start();

        // [0] Test minimum set of exempt prefixes.
        Set<IpPrefix> exempt = mUNM.getOffloadExemptPrefixes();
        final String[] MINSET = {
                "127.0.0.0/8", "169.254.0.0/16",
                "::/3", "fe80::/64", "fc00::/7", "ff00::/8",
        };
        assertPrefixSet(exempt, INCLUDES, MINSET);
        final Set<String> alreadySeen = new HashSet<>();
        Collections.addAll(alreadySeen, MINSET);
        assertEquals(alreadySeen.size(), exempt.size());

        // [1] Pretend Wi-Fi connects.
        final TestNetworkAgent wifiAgent = new TestNetworkAgent(mCM, TRANSPORT_WIFI);
        final LinkProperties wifiLp = new LinkProperties();
        wifiLp.setInterfaceName("wlan0");
        final String[] WIFI_ADDRS = {
                "fe80::827a:bfff:fe6f:374d", "100.112.103.18",
                "2001:db8:4:fd00:827a:bfff:fe6f:374d",
                "2001:db8:4:fd00:6dea:325a:fdae:4ef4",
                "fd6a:a640:60bf:e985::123",  // ULA address for good measure.
        };
        for (String addrStr : WIFI_ADDRS) {
            final String cidr = addrStr.contains(":") ? "/64" : "/20";
            wifiLp.addLinkAddress(new LinkAddress(addrStr + cidr));
        }
        wifiAgent.fakeConnect();
        wifiAgent.sendLinkProperties(wifiLp);

        exempt = mUNM.getOffloadExemptPrefixes();
        assertPrefixSet(exempt, INCLUDES, alreadySeen);
        final String[] wifiLinkPrefixes = {
                // Excludes link-local as that's already tested within MINSET.
                "100.112.96.0/20", "2001:db8:4:fd00::/64", "fd6a:a640:60bf:e985::/64",
        };
        assertPrefixSet(exempt, INCLUDES, wifiLinkPrefixes);
        Collections.addAll(alreadySeen, wifiLinkPrefixes);
        assertEquals(alreadySeen.size(), exempt.size());

        // [2] Pretend mobile connects.
        final TestNetworkAgent cellAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
        final LinkProperties cellLp = new LinkProperties();
        cellLp.setInterfaceName("rmnet_data0");
        final String[] CELL_ADDRS = {
                "10.102.211.48", "2001:db8:0:1:b50e:70d9:10c9:433d",
        };
        for (String addrStr : CELL_ADDRS) {
            final String cidr = addrStr.contains(":") ? "/64" : "/27";
            cellLp.addLinkAddress(new LinkAddress(addrStr + cidr));
        }
        cellAgent.fakeConnect();
        cellAgent.sendLinkProperties(cellLp);

        exempt = mUNM.getOffloadExemptPrefixes();
        assertPrefixSet(exempt, INCLUDES, alreadySeen);
        final String[] cellLinkPrefixes = { "10.102.211.32/27", "2001:db8:0:1::/64" };
        assertPrefixSet(exempt, INCLUDES, cellLinkPrefixes);
        Collections.addAll(alreadySeen, cellLinkPrefixes);
        assertEquals(alreadySeen.size(), exempt.size());

        // [3] Pretend DUN connects.
        final TestNetworkAgent dunAgent = new TestNetworkAgent(mCM, TRANSPORT_CELLULAR);
        dunAgent.networkCapabilities.addCapability(NET_CAPABILITY_DUN);
        final LinkProperties dunLp = new LinkProperties();
        dunLp.setInterfaceName("rmnet_data1");
        final String[] DUN_ADDRS = {
                "192.0.2.48", "2001:db8:1:2:b50e:70d9:10c9:433d",
        };
        for (String addrStr : DUN_ADDRS) {
            final String cidr = addrStr.contains(":") ? "/64" : "/27";
            cellLp.addLinkAddress(new LinkAddress(addrStr + cidr));
        }
        dunAgent.fakeConnect();
        dunAgent.sendLinkProperties(dunLp);

        exempt = mUNM.getOffloadExemptPrefixes();
        assertPrefixSet(exempt, INCLUDES, alreadySeen);
        final String[] dunLinkPrefixes = { "192.0.2.32/27", "2001:db8:1:2::/64" };
        assertPrefixSet(exempt, INCLUDES, dunLinkPrefixes);
        Collections.addAll(alreadySeen, dunLinkPrefixes);
        assertEquals(alreadySeen.size(), exempt.size());

        // [4] Pretend Wi-Fi disconnected.  It's addresses/prefixes should no
        // longer be included (should be properly removed).
        wifiAgent.fakeDisconnect();
        exempt = mUNM.getOffloadExemptPrefixes();
        assertPrefixSet(exempt, INCLUDES, MINSET);
        assertPrefixSet(exempt, EXCLUDES, wifiLinkPrefixes);
        assertPrefixSet(exempt, INCLUDES, cellLinkPrefixes);
        assertPrefixSet(exempt, INCLUDES, dunLinkPrefixes);
    }

    private void assertSatisfiesLegacyType(int legacyType, NetworkState ns) {
        if (legacyType == TYPE_NONE) {
            assertTrue(ns == null);
@@ -476,6 +579,12 @@ public class UpstreamNetworkMonitorTest {
                cb.onLost(networkId);
            }
        }

        public void sendLinkProperties(LinkProperties lp) {
            for (NetworkCallback cb : cm.listening.keySet()) {
                cb.onLinkPropertiesChanged(networkId, lp);
            }
        }
    }

    public static class TestStateMachine extends StateMachine {
@@ -504,4 +613,19 @@ public class UpstreamNetworkMonitorTest {
    static NetworkCapabilities copy(NetworkCapabilities nc) {
        return new NetworkCapabilities(nc);
    }

    static void assertPrefixSet(Set<IpPrefix> prefixes, boolean expectation, String... expected) {
        final Set<String> expectedSet = new HashSet<>();
        Collections.addAll(expectedSet, expected);
        assertPrefixSet(prefixes, expectation, expectedSet);
    }

    static void assertPrefixSet(Set<IpPrefix> prefixes, boolean expectation, Set<String> expected) {
        for (String expectedPrefix : expected) {
            final String errStr = expectation ? "did not find" : "found";
            assertEquals(
                    String.format("Failed expectation: %s prefix: %s", errStr, expectedPrefix),
                    expectation, prefixes.contains(new IpPrefix(expectedPrefix)));
        }
    }
}