Enable multiple active Ethernet interfaces

- reworked EthernetNetworkFactory to support multiple active Ethernet
interfaces
- allow vendors to specify network capabilities + ip config through XML
config overlay

Test: manual using hikey960 + multiple usb->eth adapters
Change-Id: Ie39bcb0d2a3f960f497222159c7bd5797accaa68
diff --git a/service-t/src/com/android/server/ethernet/EthernetConfigStore.java b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
index 676bc99..6445603 100644
--- a/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
+++ b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java
@@ -17,11 +17,8 @@
 package com.android.server.ethernet;
 
 import android.net.IpConfiguration;
-import android.net.IpConfiguration.IpAssignment;
-import android.net.IpConfiguration.ProxySettings;
 import android.os.Environment;
-import android.util.Log;
-import android.util.SparseArray;
+import android.util.ArrayMap;
 
 import com.android.server.net.IpConfigStore;
 
@@ -29,34 +26,60 @@
 /**
  * This class provides an API to store and manage Ethernet network configuration.
  */
-public class EthernetConfigStore extends IpConfigStore {
-    private static final String TAG = "EthernetConfigStore";
-
+public class EthernetConfigStore {
     private static final String ipConfigFile = Environment.getDataDirectory() +
             "/misc/ethernet/ipconfig.txt";
 
+    private IpConfigStore mStore = new IpConfigStore();
+    private ArrayMap<String, IpConfiguration> mIpConfigurations;
+    private IpConfiguration mIpConfigurationForDefaultInterface;
+    private final Object mSync = new Object();
+
     public EthernetConfigStore() {
+        mIpConfigurations = new ArrayMap<>(0);
     }
 
-    public IpConfiguration readIpAndProxyConfigurations() {
-        SparseArray<IpConfiguration> networks = readIpAndProxyConfigurations(ipConfigFile);
+    public void read() {
+        synchronized (mSync) {
+            ArrayMap<String, IpConfiguration> configs =
+                    IpConfigStore.readIpConfigurations(ipConfigFile);
 
-        if (networks.size() == 0) {
-            Log.w(TAG, "No Ethernet configuration found. Using default.");
-            return new IpConfiguration(IpAssignment.DHCP, ProxySettings.NONE, null, null);
+            // This configuration may exist in old file versions when there was only a single active
+            // Ethernet interface.
+            if (configs.containsKey("0")) {
+                mIpConfigurationForDefaultInterface = configs.remove("0");
+            }
+
+            mIpConfigurations = configs;
         }
-
-        if (networks.size() > 1) {
-            // Currently we only support a single Ethernet interface.
-            Log.w(TAG, "Multiple Ethernet configurations detected. Only reading first one.");
-        }
-
-        return networks.valueAt(0);
     }
 
-    public void writeIpAndProxyConfigurations(IpConfiguration config) {
-        SparseArray<IpConfiguration> networks = new SparseArray<IpConfiguration>();
-        networks.put(0, config);
-        writeIpAndProxyConfigurations(ipConfigFile, networks);
+    public void write(String iface, IpConfiguration config) {
+        boolean modified;
+
+        synchronized (mSync) {
+            if (config == null) {
+                modified = mIpConfigurations.remove(iface) != null;
+            } else {
+                IpConfiguration oldConfig = mIpConfigurations.put(iface, config);
+                modified = !config.equals(oldConfig);
+            }
+
+            if (modified) {
+                mStore.writeIpConfigurations(ipConfigFile, mIpConfigurations);
+            }
+        }
+    }
+
+    public ArrayMap<String, IpConfiguration> getIpConfigurations() {
+        synchronized (mSync) {
+            return new ArrayMap<>(mIpConfigurations);
+        }
+    }
+
+    public IpConfiguration getIpConfigurationForDefaultInterface() {
+        synchronized (mSync) {
+            return new IpConfiguration(mIpConfigurationForDefaultInterface);
+        }
     }
 }
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index 2e5d09e..29464b7 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -16,12 +16,9 @@
 
 package com.android.server.ethernet;
 
+import static android.net.ConnectivityManager.TYPE_ETHERNET;
+
 import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.DhcpResults;
-import android.net.EthernetManager;
-import android.net.IEthernetServiceListener;
-import android.net.InterfaceConfiguration;
 import android.net.IpConfiguration;
 import android.net.IpConfiguration.IpAssignment;
 import android.net.IpConfiguration.ProxySettings;
@@ -31,292 +28,215 @@
 import android.net.NetworkFactory;
 import android.net.NetworkInfo;
 import android.net.NetworkInfo.DetailedState;
-import android.net.StaticIpConfiguration;
-import android.net.ip.IpManager;
-import android.net.ip.IpManager.ProvisioningConfiguration;
+import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
+import android.net.StringNetworkSpecifier;
+import android.net.ip.IpClient;
+import android.net.ip.IpClient.ProvisioningConfiguration;
 import android.os.Handler;
-import android.os.IBinder;
-import android.os.INetworkManagementService;
-import android.os.Looper;
-import android.os.RemoteCallbackList;
-import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.text.TextUtils;
 import android.util.Log;
 
 import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.net.BaseNetworkObserver;
 
 import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.Arrays;
-import java.util.concurrent.CountDownLatch;
-
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
- * Manages connectivity for an Ethernet interface.
+ * {@link NetworkFactory} that represents Ethernet networks.
  *
- * Ethernet Interfaces may be present at boot time or appear after boot (e.g.,
- * for Ethernet adapters connected over USB). This class currently supports
- * only one interface. When an interface appears on the system (or is present
- * at boot time) this class will start tracking it and bring it up, and will
- * attempt to connect when requested. Any other interfaces that subsequently
- * appear will be ignored until the tracked interface disappears. Only
- * interfaces whose names match the <code>config_ethernet_iface_regex</code>
- * regular expression are tracked.
- *
- * This class reports a static network score of 70 when it is tracking an
- * interface and that interface's link is up, and a score of 0 otherwise.
- *
- * @hide
+ * This class reports a static network score of 70 when it is tracking an interface and that
+ * interface's link is up, and a score of 0 otherwise.
  */
-class EthernetNetworkFactory {
+public class EthernetNetworkFactory extends NetworkFactory {
+    private final static String TAG = EthernetNetworkFactory.class.getSimpleName();
+    final static boolean DBG = true;
+
+    private final static int NETWORK_SCORE = 70;
     private static final String NETWORK_TYPE = "Ethernet";
-    private static final String TAG = "EthernetNetworkFactory";
-    private static final int NETWORK_SCORE = 70;
-    private static final boolean DBG = true;
 
-    /** Tracks interface changes. Called from NetworkManagementService. */
-    private InterfaceObserver mInterfaceObserver;
+    private final ConcurrentHashMap<String, NetworkInterfaceState> mTrackingInterfaces =
+            new ConcurrentHashMap<>();
+    private final Handler mHandler;
+    private final Context mContext;
 
-    /** For static IP configuration */
-    private EthernetManager mEthernetManager;
+    public EthernetNetworkFactory(Handler handler, Context context, NetworkCapabilities filter) {
+        super(handler.getLooper(), context, NETWORK_TYPE, filter);
 
-    /** To set link state and configure IP addresses. */
-    private INetworkManagementService mNMService;
+        mHandler = handler;
+        mContext = context;
 
-    /** All code runs here, including start(). */
-    private Handler mHandler;
-
-    /* To communicate with ConnectivityManager */
-    private NetworkCapabilities mNetworkCapabilities;
-    private NetworkAgent mNetworkAgent;
-    private LocalNetworkFactory mFactory;
-    private Context mContext;
-
-    /** Product-dependent regular expression of interface names we track. */
-    private static String mIfaceMatch = "";
-
-    /** To notify Ethernet status. */
-    private final RemoteCallbackList<IEthernetServiceListener> mListeners;
-
-    /** Data members. All accesses to these must be on the handler thread. */
-    private String mIface = "";
-    private String mHwAddr;
-    private boolean mLinkUp;
-    private NetworkInfo mNetworkInfo;
-    private LinkProperties mLinkProperties;
-    private IpManager mIpManager;
-    private boolean mNetworkRequested = false;
-
-    EthernetNetworkFactory(RemoteCallbackList<IEthernetServiceListener> listeners) {
-        initNetworkCapabilities();
-        clearInfo();
-        mListeners = listeners;
+        setScoreFilter(NETWORK_SCORE);
     }
 
-    private class LocalNetworkFactory extends NetworkFactory {
-        LocalNetworkFactory(String name, Context context, Looper looper) {
-            super(looper, context, name, new NetworkCapabilities());
+    @Override
+    public boolean acceptRequest(NetworkRequest request, int score) {
+        if (DBG) {
+            Log.d(TAG, "acceptRequest, request: " + request + ", score: " + score);
         }
 
-        protected void startNetwork() {
-            if (!mNetworkRequested) {
-                mNetworkRequested = true;
-                maybeStartIpManager();
-            }
+        return networkForRequest(request) != null;
+    }
+
+    @Override
+    protected void needNetworkFor(NetworkRequest networkRequest, int score) {
+        NetworkInterfaceState network = networkForRequest(networkRequest);
+
+        if (network == null) {
+            Log.e(TAG, "needNetworkFor, failed to get a network for " + networkRequest);
+            return;
         }
 
-        protected void stopNetwork() {
-            mNetworkRequested = false;
-            stopIpManager();
+        if (++network.refCount == 1) {
+            network.start();
         }
     }
 
-    private void clearInfo() {
-        mLinkProperties = new LinkProperties();
-        mNetworkInfo = new NetworkInfo(ConnectivityManager.TYPE_ETHERNET, 0, NETWORK_TYPE, "");
-        mNetworkInfo.setExtraInfo(mHwAddr);
-        mNetworkInfo.setIsAvailable(isTrackingInterface());
-    }
+    @Override
+    protected void releaseNetworkFor(NetworkRequest networkRequest) {
+        NetworkInterfaceState network = networkForRequest(networkRequest);
+        if (network == null) {
+            Log.e(TAG, "needNetworkFor, failed to get a network for " + networkRequest);
+            return;
+        }
 
-    private void stopIpManager() {
-        if (mIpManager != null) {
-            mIpManager.shutdown();
-            mIpManager = null;
+        if (--network.refCount == 1) {
+            network.stop();
         }
-        // ConnectivityService will only forget our NetworkAgent if we send it a NetworkInfo object
-        // with a state of DISCONNECTED or SUSPENDED. So we can't simply clear our NetworkInfo here:
-        // that sets the state to IDLE, and ConnectivityService will still think we're connected.
-        //
-        mNetworkInfo.setDetailedState(DetailedState.DISCONNECTED, null, mHwAddr);
-        if (mNetworkAgent != null) {
-            updateAgent();
-            mNetworkAgent = null;
-        }
-        clearInfo();
     }
 
     /**
-     * Updates interface state variables.
-     * Called on link state changes or on startup.
+     * Returns an array of available interface names. The array is sorted: unrestricted interfaces
+     * goes first, then sorted by name.
      */
-    private void updateInterfaceState(String iface, boolean up) {
-        if (!mIface.equals(iface)) {
+    String[] getAvailableInterfaces(boolean includeRestricted) {
+        return mTrackingInterfaces.values()
+                .stream()
+                .filter(iface -> !iface.isRestricted() || includeRestricted)
+                .sorted((iface1, iface2) -> {
+                    int r = Boolean.compare(iface1.isRestricted(), iface2.isRestricted());
+                    return r == 0 ? iface1.name.compareTo(iface2.name) : r;
+                })
+                .map(iface -> iface.name)
+                .toArray(String[]::new);
+    }
+
+    void addInterface(String ifaceName, String hwAddress, NetworkCapabilities capabilities,
+             IpConfiguration ipConfiguration) {
+        if (mTrackingInterfaces.containsKey(ifaceName)) {
+            Log.e(TAG, "Interface with name " + ifaceName + " already exists.");
             return;
         }
-        Log.d(TAG, "updateInterface: " + iface + " link " + (up ? "up" : "down"));
 
-        mLinkUp = up;
-        if (up) {
-            maybeStartIpManager();
+        if (DBG) {
+            Log.d(TAG, "addInterface, iface: " + ifaceName + ", capabilities: " + capabilities);
+        }
+
+        NetworkInterfaceState iface = new NetworkInterfaceState(
+                ifaceName, hwAddress, mHandler, mContext, capabilities);
+        iface.setIpConfig(ipConfiguration);
+        mTrackingInterfaces.put(ifaceName, iface);
+
+        updateCapabilityFilter();
+    }
+
+    private void updateCapabilityFilter() {
+        NetworkCapabilities capabilitiesFilter = new NetworkCapabilities();
+        capabilitiesFilter.clearAll();
+
+        for (NetworkInterfaceState iface:  mTrackingInterfaces.values()) {
+            capabilitiesFilter.combineCapabilities(iface.mCapabilities);
+        }
+
+        if (DBG) Log.d(TAG, "updateCapabilityFilter: " + capabilitiesFilter);
+        setCapabilityFilter(capabilitiesFilter);
+    }
+
+    void removeInterface(String interfaceName) {
+        NetworkInterfaceState iface = mTrackingInterfaces.remove(interfaceName);
+        if (iface != null) {
+            iface.stop();
+        }
+
+        updateCapabilityFilter();
+    }
+
+    /** Returns true if state has been modified */
+    boolean updateInterfaceLinkState(String ifaceName, boolean up) {
+        if (!mTrackingInterfaces.containsKey(ifaceName)) {
+            return false;
+        }
+
+        if (DBG) {
+            Log.d(TAG, "updateInterfaceLinkState, iface: " + ifaceName + ", up: " + up);
+        }
+
+        NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName);
+        return iface.updateLinkState(up);
+    }
+
+    boolean hasInterface(String interfacName) {
+        return mTrackingInterfaces.containsKey(interfacName);
+    }
+
+    void updateIpConfiguration(String iface, IpConfiguration ipConfiguration) {
+        NetworkInterfaceState network = mTrackingInterfaces.get(iface);
+        if (network != null) {
+            network.setIpConfig(ipConfiguration);
+        }
+    }
+
+    private NetworkInterfaceState networkForRequest(NetworkRequest request) {
+        String requestedIface = null;
+
+        NetworkSpecifier specifier = request.networkCapabilities.getNetworkSpecifier();
+        if (specifier instanceof StringNetworkSpecifier) {
+            requestedIface = ((StringNetworkSpecifier) specifier).specifier;
+        }
+
+        NetworkInterfaceState network = null;
+        if (!TextUtils.isEmpty(requestedIface)) {
+            NetworkInterfaceState n = mTrackingInterfaces.get(requestedIface);
+            if (n != null && n.statisified(request.networkCapabilities)) {
+                network = n;
+            }
         } else {
-            stopIpManager();
-        }
-    }
-
-    private class InterfaceObserver extends BaseNetworkObserver {
-        @Override
-        public void interfaceLinkStateChanged(String iface, boolean up) {
-            mHandler.post(() -> {
-                updateInterfaceState(iface, up);
-            });
-        }
-
-        @Override
-        public void interfaceAdded(String iface) {
-            mHandler.post(() -> {
-                maybeTrackInterface(iface);
-            });
-        }
-
-        @Override
-        public void interfaceRemoved(String iface) {
-            mHandler.post(() -> {
-                if (stopTrackingInterface(iface)) {
-                    trackFirstAvailableInterface();
+            for (NetworkInterfaceState n : mTrackingInterfaces.values()) {
+                if (n.statisified(request.networkCapabilities)) {
+                    network = n;
+                    break;
                 }
-            });
-        }
-    }
-
-    private void setInterfaceUp(String iface) {
-        // Bring up the interface so we get link status indications.
-        try {
-            mNMService.setInterfaceUp(iface);
-            String hwAddr = null;
-            InterfaceConfiguration config = mNMService.getInterfaceConfig(iface);
-
-            if (config == null) {
-                Log.e(TAG, "Null interface config for " + iface + ". Bailing out.");
-                return;
             }
-
-            if (!isTrackingInterface()) {
-                setInterfaceInfo(iface, config.getHardwareAddress());
-                mNetworkInfo.setIsAvailable(true);
-                mNetworkInfo.setExtraInfo(mHwAddr);
-            } else {
-                Log.e(TAG, "Interface unexpectedly changed from " + iface + " to " + mIface);
-                mNMService.setInterfaceDown(iface);
-            }
-        } catch (RemoteException | IllegalStateException e) {
-            // Either the system is crashing or the interface has disappeared. Just ignore the
-            // error; we haven't modified any state because we only do that if our calls succeed.
-            Log.e(TAG, "Error upping interface " + mIface + ": " + e);
         }
-    }
 
-    private boolean maybeTrackInterface(String iface) {
-        // If we don't already have an interface, and if this interface matches
-        // our regex, start tracking it.
-        if (!iface.matches(mIfaceMatch) || isTrackingInterface())
-            return false;
-
-        Log.d(TAG, "Started tracking interface " + iface);
-        setInterfaceUp(iface);
-        return true;
-    }
-
-    private boolean stopTrackingInterface(String iface) {
-        if (!iface.equals(mIface))
-            return false;
-
-        Log.d(TAG, "Stopped tracking interface " + iface);
-        setInterfaceInfo("", null);
-        stopIpManager();
-        return true;
-    }
-
-    public void updateAgent() {
-        if (mNetworkAgent == null) return;
         if (DBG) {
-            Log.i(TAG, "Updating mNetworkAgent with: " +
-                  mNetworkCapabilities + ", " +
-                  mNetworkInfo + ", " +
-                  mLinkProperties);
-        }
-        mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
-        mNetworkAgent.sendNetworkInfo(mNetworkInfo);
-        mNetworkAgent.sendLinkProperties(mLinkProperties);
-        // never set the network score below 0.
-        mNetworkAgent.sendNetworkScore(mLinkUp? NETWORK_SCORE : 0);
-    }
-
-    void onIpLayerStarted(LinkProperties linkProperties) {
-        if (mNetworkAgent != null) {
-            Log.e(TAG, "Already have a NetworkAgent - aborting new request");
-            stopIpManager();
-            return;
-        }
-        mLinkProperties = linkProperties;
-        mNetworkInfo.setDetailedState(DetailedState.CONNECTED, null, mHwAddr);
-
-        // Create our NetworkAgent.
-        mNetworkAgent = new NetworkAgent(mHandler.getLooper(), mContext,
-                NETWORK_TYPE, mNetworkInfo, mNetworkCapabilities, mLinkProperties,
-                NETWORK_SCORE) {
-            public void unwanted() {
-                if (this == mNetworkAgent) {
-                    stopIpManager();
-                } else if (mNetworkAgent != null) {
-                    Log.d(TAG, "Ignoring unwanted as we have a more modern " +
-                            "instance");
-                }  // Otherwise, we've already called stopIpManager.
-            }
-        };
-    }
-
-    void onIpLayerStopped(LinkProperties linkProperties) {
-        // This cannot happen due to provisioning timeout, because our timeout is 0. It can only
-        // happen if we're provisioned and we lose provisioning.
-        stopIpManager();
-        maybeStartIpManager();
-    }
-
-    void updateLinkProperties(LinkProperties linkProperties) {
-        mLinkProperties = linkProperties;
-        if (mNetworkAgent != null) {
-            mNetworkAgent.sendLinkProperties(linkProperties);
-        }
-    }
-
-    public void maybeStartIpManager() {
-        if (mNetworkRequested && mIpManager == null && isTrackingInterface()) {
-            startIpManager();
-        }
-    }
-
-    public void startIpManager() {
-        if (DBG) {
-            Log.d(TAG, String.format("starting IpManager(%s): mNetworkInfo=%s", mIface,
-                    mNetworkInfo));
+            Log.i(TAG, "networkForRequest, request: " + request + ", network: " + network);
         }
 
-        IpConfiguration config = mEthernetManager.getConfiguration();
+        return network;
+    }
 
-        mNetworkInfo.setDetailedState(DetailedState.OBTAINING_IPADDR, null, mHwAddr);
-        IpManager.Callback ipmCallback = new IpManager.Callback() {
+    private static class NetworkInterfaceState {
+        final String name;
+
+        private final String mHwAddress;
+        private final NetworkCapabilities mCapabilities;
+        private final Handler mHandler;
+        private final Context mContext;
+        private final NetworkInfo mNetworkInfo;
+
+        private static String sTcpBufferSizes = null;  // Lazy initialized.
+
+        private boolean mLinkUp;
+        private LinkProperties mLinkProperties = new LinkProperties();
+
+        private IpClient mIpClient;
+        private NetworkAgent mNetworkAgent;
+        private IpConfiguration mIpConfig;
+
+        long refCount = 0;
+
+        private final IpClient.Callback mIpClientCallback = new IpClient.Callback() {
             @Override
             public void onProvisioningSuccess(LinkProperties newLp) {
                 mHandler.post(() -> onIpLayerStarted(newLp));
@@ -333,178 +253,191 @@
             }
         };
 
-        stopIpManager();
-        mIpManager = new IpManager(mContext, mIface, ipmCallback);
+        NetworkInterfaceState(String ifaceName, String hwAddress, Handler handler, Context context,
+                NetworkCapabilities capabilities) {
+            name = ifaceName;
+            mCapabilities = capabilities;
+            mHandler = handler;
+            mContext = context;
 
-        if (config.getProxySettings() == ProxySettings.STATIC ||
-                config.getProxySettings() == ProxySettings.PAC) {
-            mIpManager.setHttpProxy(config.getHttpProxy());
+            mHwAddress = hwAddress;
+            mNetworkInfo = new NetworkInfo(TYPE_ETHERNET, 0, NETWORK_TYPE, "");
+            mNetworkInfo.setExtraInfo(mHwAddress);
+            mNetworkInfo.setIsAvailable(true);
         }
 
-        final String tcpBufferSizes = mContext.getResources().getString(
-                com.android.internal.R.string.config_ethernet_tcp_buffers);
-        if (!TextUtils.isEmpty(tcpBufferSizes)) {
-            mIpManager.setTcpBufferSizes(tcpBufferSizes);
+        void setIpConfig(IpConfiguration ipConfig) {
+
+            this.mIpConfig = ipConfig;
         }
 
-        final ProvisioningConfiguration provisioningConfiguration;
-        if (config.getIpAssignment() == IpAssignment.STATIC) {
-            provisioningConfiguration = IpManager.buildProvisioningConfiguration()
-                    .withStaticConfiguration(config.getStaticIpConfiguration())
-                    .build();
-        } else {
-            provisioningConfiguration = mIpManager.buildProvisioningConfiguration()
-                    .withProvisioningTimeoutMs(0)
-                    .build();
+        boolean statisified(NetworkCapabilities requestedCapabilities) {
+            return requestedCapabilities.satisfiedByNetworkCapabilities(mCapabilities);
         }
 
-        mIpManager.startProvisioning(provisioningConfiguration);
-    }
-
-    /**
-     * Begin monitoring connectivity
-     */
-    public void start(Context context, Handler handler) {
-        mHandler = handler;
-
-        // The services we use.
-        IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE);
-        mNMService = INetworkManagementService.Stub.asInterface(b);
-        mEthernetManager = (EthernetManager) context.getSystemService(Context.ETHERNET_SERVICE);
-
-        // Interface match regex.
-        mIfaceMatch = context.getResources().getString(
-                com.android.internal.R.string.config_ethernet_iface_regex);
-
-        // Create and register our NetworkFactory.
-        mFactory = new LocalNetworkFactory(NETWORK_TYPE, context, mHandler.getLooper());
-        mFactory.setCapabilityFilter(mNetworkCapabilities);
-        mFactory.setScoreFilter(NETWORK_SCORE);
-        mFactory.register();
-
-        mContext = context;
-
-        // Start tracking interface change events.
-        mInterfaceObserver = new InterfaceObserver();
-        try {
-            mNMService.registerObserver(mInterfaceObserver);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Could not register InterfaceObserver " + e);
+        boolean isRestricted() {
+            return mCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
         }
 
-        // If an Ethernet interface is already connected, start tracking that.
-        // Otherwise, the first Ethernet interface to appear will be tracked.
-        mHandler.post(() -> trackFirstAvailableInterface());
-    }
+        private void start() {
+            if (DBG) {
+                Log.d(TAG, String.format("starting IpClient(%s): mNetworkInfo=%s", name,
+                        mNetworkInfo));
+            }
+            if (mIpClient != null) stop();
 
-    public void trackFirstAvailableInterface() {
-        try {
-            final String[] ifaces = mNMService.listInterfaces();
-            for (String iface : ifaces) {
-                if (maybeTrackInterface(iface)) {
-                    // We have our interface. Track it.
-                    // Note: if the interface already has link (e.g., if we crashed and got
-                    // restarted while it was running), we need to fake a link up notification so we
-                    // start configuring it.
-                    if (mNMService.getInterfaceConfig(iface).hasFlag("running")) {
-                        updateInterfaceState(iface, true);
-                    }
-                    break;
+            mNetworkInfo.setDetailedState(DetailedState.OBTAINING_IPADDR, null, mHwAddress);
+
+            mIpClient = new IpClient(mContext, name, mIpClientCallback);
+
+            if (sTcpBufferSizes == null) {
+                sTcpBufferSizes = mContext.getResources().getString(
+                        com.android.internal.R.string.config_ethernet_tcp_buffers);
+            }
+            provisionIpClient(mIpClient, mIpConfig, sTcpBufferSizes);
+        }
+
+        void onIpLayerStarted(LinkProperties linkProperties) {
+            if (mNetworkAgent != null) {
+                Log.e(TAG, "Already have a NetworkAgent - aborting new request");
+                stop();
+                return;
+            }
+            mLinkProperties = linkProperties;
+            mNetworkInfo.setDetailedState(DetailedState.CONNECTED, null, mHwAddress);
+            mNetworkInfo.setIsAvailable(true);
+
+            // Create our NetworkAgent.
+            mNetworkAgent = new NetworkAgent(mHandler.getLooper(), mContext,
+                    NETWORK_TYPE, mNetworkInfo, mCapabilities, mLinkProperties,
+                    NETWORK_SCORE) {
+                public void unwanted() {
+                    if (this == mNetworkAgent) {
+                        stop();
+                    } else if (mNetworkAgent != null) {
+                        Log.d(TAG, "Ignoring unwanted as we have a more modern " +
+                                "instance");
+                    }  // Otherwise, we've already called stop.
                 }
+            };
+        }
+
+        void onIpLayerStopped(LinkProperties linkProperties) {
+            // This cannot happen due to provisioning timeout, because our timeout is 0. It can only
+            // happen if we're provisioned and we lose provisioning.
+            start();
+        }
+
+        void updateLinkProperties(LinkProperties linkProperties) {
+            mLinkProperties = linkProperties;
+            if (mNetworkAgent != null) {
+                mNetworkAgent.sendLinkProperties(linkProperties);
             }
-        } catch (RemoteException|IllegalStateException e) {
-            Log.e(TAG, "Could not get list of interfaces " + e);
+        }
+
+        /** Returns true if state has been modified */
+        boolean updateLinkState(boolean up) {
+            if (mLinkUp == up) return false;
+
+            mLinkUp = up;
+            if (up) {
+                start();
+            } else {
+                stop();
+            }
+
+            return true;
+        }
+
+        void stop() {
+            if (mIpClient != null) {
+                mIpClient.shutdown();
+                mIpClient = null;
+            }
+            // ConnectivityService will only forget our NetworkAgent if we send it a NetworkInfo object
+            // with a state of DISCONNECTED or SUSPENDED. So we can't simply clear our NetworkInfo here:
+            // that sets the state to IDLE, and ConnectivityService will still think we're connected.
+            //
+            mNetworkInfo.setDetailedState(DetailedState.DISCONNECTED, null, mHwAddress);
+            if (mNetworkAgent != null) {
+                updateAgent();
+                mNetworkAgent = null;
+            }
+            clear();
+        }
+
+        private void updateAgent() {
+            if (mNetworkAgent == null) return;
+            if (DBG) {
+                Log.i(TAG, "Updating mNetworkAgent with: " +
+                        mCapabilities + ", " +
+                        mNetworkInfo + ", " +
+                        mLinkProperties);
+            }
+            mNetworkAgent.sendNetworkCapabilities(mCapabilities);
+            mNetworkAgent.sendNetworkInfo(mNetworkInfo);
+            mNetworkAgent.sendLinkProperties(mLinkProperties);
+            // never set the network score below 0.
+            mNetworkAgent.sendNetworkScore(mLinkUp? NETWORK_SCORE : 0);
+        }
+
+        private void clear() {
+            mLinkProperties.clear();
+            mNetworkInfo.setDetailedState(DetailedState.IDLE, null, null);
+            mNetworkInfo.setIsAvailable(false);
+        }
+
+        private static void provisionIpClient(IpClient ipClient, IpConfiguration config,
+                String tcpBufferSizes) {
+            if (config.getProxySettings() == ProxySettings.STATIC ||
+                    config.getProxySettings() == ProxySettings.PAC) {
+                ipClient.setHttpProxy(config.getHttpProxy());
+            }
+
+            if (!TextUtils.isEmpty(tcpBufferSizes)) {
+                ipClient.setTcpBufferSizes(tcpBufferSizes);
+            }
+
+            final ProvisioningConfiguration provisioningConfiguration;
+            if (config.getIpAssignment() == IpAssignment.STATIC) {
+                provisioningConfiguration = IpClient.buildProvisioningConfiguration()
+                        .withStaticConfiguration(config.getStaticIpConfiguration())
+                        .build();
+            } else {
+                provisioningConfiguration = IpClient.buildProvisioningConfiguration()
+                        .withProvisioningTimeoutMs(0)
+                        .build();
+            }
+
+            ipClient.startProvisioning(provisioningConfiguration);
+        }
+
+        @Override
+        public String toString() {
+            return getClass().getSimpleName() + "{ "
+                    + "iface: " + name + ", "
+                    + "up: " + mLinkUp + ", "
+                    + "hwAddress: " + mHwAddress + ", "
+                    + "networkInfo: " + mNetworkInfo + ", "
+                    + "networkAgent: " + mNetworkAgent + ", "
+                    + "ipClient: " + mIpClient + ","
+                    + "linkProperties: " + mLinkProperties
+                    + "}";
         }
     }
 
-    public void stop() {
-        stopIpManager();
-        setInterfaceInfo("", null);
-        mFactory.unregister();
-    }
-
-    private void initNetworkCapabilities() {
-        mNetworkCapabilities = new NetworkCapabilities();
-        mNetworkCapabilities.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET);
-        mNetworkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
-        mNetworkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
-        mNetworkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
-        mNetworkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED);
-        mNetworkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
-        // We have no useful data on bandwidth. Say 100M up and 100M down. :-(
-        mNetworkCapabilities.setLinkUpstreamBandwidthKbps(100 * 1000);
-        mNetworkCapabilities.setLinkDownstreamBandwidthKbps(100 * 1000);
-    }
-
-    public boolean isTrackingInterface() {
-        return !TextUtils.isEmpty(mIface);
-    }
-
-    /**
-     * Set interface information and notify listeners if availability is changed.
-     */
-    private void setInterfaceInfo(String iface, String hwAddr) {
-        boolean oldAvailable = isTrackingInterface();
-        mIface = iface;
-        mHwAddr = hwAddr;
-        boolean available = isTrackingInterface();
-
-        mNetworkInfo.setExtraInfo(mHwAddr);
-        mNetworkInfo.setIsAvailable(available);
-
-        if (oldAvailable != available) {
-            int n = mListeners.beginBroadcast();
-            for (int i = 0; i < n; i++) {
-                try {
-                    mListeners.getBroadcastItem(i).onAvailabilityChanged(available);
-                } catch (RemoteException e) {
-                    // Do nothing here.
-                }
-            }
-            mListeners.finishBroadcast();
-        }
-    }
-
-    private void postAndWaitForRunnable(Runnable r) throws InterruptedException {
-        CountDownLatch latch = new CountDownLatch(1);
-        mHandler.post(() -> {
-            try {
-                r.run();
-            } finally {
-                latch.countDown();
-            }
-        });
-        latch.await();
-    }
-
-
     void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
-        try {
-            postAndWaitForRunnable(() -> {
-                pw.println("Network Requested: " + mNetworkRequested);
-                if (isTrackingInterface()) {
-                    pw.println("Tracking interface: " + mIface);
-                    pw.increaseIndent();
-                    pw.println("MAC address: " + mHwAddr);
-                    pw.println("Link state: " + (mLinkUp ? "up" : "down"));
-                    pw.decreaseIndent();
-                } else {
-                    pw.println("Not tracking any interface");
-                }
-
-                pw.println();
-                pw.println("NetworkInfo: " + mNetworkInfo);
-                pw.println("LinkProperties: " + mLinkProperties);
-                pw.println("NetworkAgent: " + mNetworkAgent);
-                if (mIpManager != null) {
-                    pw.println("IpManager:");
-                    pw.increaseIndent();
-                    mIpManager.dump(fd, pw, args);
-                    pw.decreaseIndent();
-                }
-            });
-        } catch (InterruptedException e) {
-            throw new IllegalStateException("dump() interrupted");
+        super.dump(fd, pw, args);
+        pw.println(getClass().getSimpleName());
+        pw.println("Tracking interfaces:");
+        pw.increaseIndent();
+        for (String iface: mTrackingInterfaces.keySet()) {
+            NetworkInterfaceState ifaceState = mTrackingInterfaces.get(iface);
+            pw.println(iface + ":" + ifaceState);
+            pw.increaseIndent();
+            ifaceState.mIpClient.dump(fd, pw, args);
+            pw.decreaseIndent();
         }
+        pw.decreaseIndent();
     }
 }
diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
index 42996d6..d5beec1 100644
--- a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
+++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java
@@ -21,14 +21,10 @@
 import android.net.IEthernetManager;
 import android.net.IEthernetServiceListener;
 import android.net.IpConfiguration;
-import android.net.IpConfiguration.IpAssignment;
-import android.net.IpConfiguration.ProxySettings;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.HandlerThread;
-import android.os.RemoteCallbackList;
 import android.os.RemoteException;
-import android.provider.Settings;
 import android.util.Log;
 import android.util.PrintWriterPrinter;
 
@@ -41,31 +37,18 @@
 /**
  * EthernetServiceImpl handles remote Ethernet operation requests by implementing
  * the IEthernetManager interface.
- *
- * @hide
  */
 public class EthernetServiceImpl extends IEthernetManager.Stub {
     private static final String TAG = "EthernetServiceImpl";
 
     private final Context mContext;
-    private final EthernetConfigStore mEthernetConfigStore;
     private final AtomicBoolean mStarted = new AtomicBoolean(false);
-    private IpConfiguration mIpConfiguration;
 
     private Handler mHandler;
-    private final EthernetNetworkFactory mTracker;
-    private final RemoteCallbackList<IEthernetServiceListener> mListeners =
-            new RemoteCallbackList<IEthernetServiceListener>();
+    private EthernetTracker mTracker;
 
     public EthernetServiceImpl(Context context) {
         mContext = context;
-        Log.i(TAG, "Creating EthernetConfigStore");
-        mEthernetConfigStore = new EthernetConfigStore();
-        mIpConfiguration = mEthernetConfigStore.readIpAndProxyConfigurations();
-
-        Log.i(TAG, "Read stored IP configuration: " + mIpConfiguration);
-
-        mTracker = new EthernetNetworkFactory(mListeners);
     }
 
     private void enforceAccessPermission() {
@@ -80,6 +63,18 @@
                 "ConnectivityService");
     }
 
+    private void enforceUseRestrictedNetworksPermission() {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS,
+                "ConnectivityService");
+    }
+
+    private boolean checkUseRestrictedNetworksPermission() {
+        return mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS)
+                == PackageManager.PERMISSION_GRANTED;
+    }
+
     public void start() {
         Log.i(TAG, "Starting Ethernet service");
 
@@ -87,60 +82,68 @@
         handlerThread.start();
         mHandler = new Handler(handlerThread.getLooper());
 
-        mTracker.start(mContext, mHandler);
+        mTracker = new EthernetTracker(mContext, mHandler);
+        mTracker.start();
 
         mStarted.set(true);
     }
 
+    @Override
+    public String[] getAvailableInterfaces() throws RemoteException {
+        return mTracker.getInterfaces(checkUseRestrictedNetworksPermission());
+    }
+
     /**
      * Get Ethernet configuration
      * @return the Ethernet Configuration, contained in {@link IpConfiguration}.
      */
     @Override
-    public IpConfiguration getConfiguration() {
+    public IpConfiguration getConfiguration(String iface) {
         enforceAccessPermission();
 
-        synchronized (mIpConfiguration) {
-            return new IpConfiguration(mIpConfiguration);
+        if (mTracker.isRestrictedInterface(iface)) {
+            enforceUseRestrictedNetworksPermission();
         }
+
+        return new IpConfiguration(mTracker.getIpConfiguration(iface));
     }
 
     /**
      * Set Ethernet configuration
      */
     @Override
-    public void setConfiguration(IpConfiguration config) {
+    public void setConfiguration(String iface, IpConfiguration config) {
         if (!mStarted.get()) {
             Log.w(TAG, "System isn't ready enough to change ethernet configuration");
         }
 
         enforceConnectivityInternalPermission();
 
-        synchronized (mIpConfiguration) {
-            mEthernetConfigStore.writeIpAndProxyConfigurations(config);
-
-            // TODO: this does not check proxy settings, gateways, etc.
-            // Fix this by making IpConfiguration a complete representation of static configuration.
-            if (!config.equals(mIpConfiguration)) {
-                mIpConfiguration = new IpConfiguration(config);
-                mTracker.stop();
-                mTracker.start(mContext, mHandler);
-            }
+        if (mTracker.isRestrictedInterface(iface)) {
+            enforceUseRestrictedNetworksPermission();
         }
+
+        // TODO: this does not check proxy settings, gateways, etc.
+        // Fix this by making IpConfiguration a complete representation of static configuration.
+        mTracker.updateIpConfiguration(iface, new IpConfiguration(config));
     }
 
     /**
-     * Indicates whether the system currently has one or more
-     * Ethernet interfaces.
+     * Indicates whether given interface is available.
      */
     @Override
-    public boolean isAvailable() {
+    public boolean isAvailable(String iface) {
         enforceAccessPermission();
-        return mTracker.isTrackingInterface();
+
+        if (mTracker.isRestrictedInterface(iface)) {
+            enforceUseRestrictedNetworksPermission();
+        }
+
+        return mTracker.isTrackingInterface(iface);
     }
 
     /**
-     * Addes a listener.
+     * Adds a listener.
      * @param listener A {@link IEthernetServiceListener} to add.
      */
     public void addListener(IEthernetServiceListener listener) {
@@ -148,7 +151,7 @@
             throw new IllegalArgumentException("listener must not be null");
         }
         enforceAccessPermission();
-        mListeners.register(listener);
+        mTracker.addListener(listener, checkUseRestrictedNetworksPermission());
     }
 
     /**
@@ -160,7 +163,7 @@
             throw new IllegalArgumentException("listener must not be null");
         }
         enforceAccessPermission();
-        mListeners.unregister(listener);
+        mTracker.removeListener(listener);
     }
 
     @Override
@@ -179,12 +182,6 @@
         mTracker.dump(fd, pw, args);
         pw.decreaseIndent();
 
-        pw.println();
-        pw.println("Stored Ethernet configuration: ");
-        pw.increaseIndent();
-        pw.println(mIpConfiguration);
-        pw.decreaseIndent();
-
         pw.println("Handler:");
         pw.increaseIndent();
         mHandler.dump(new PrintWriterPrinter(pw), "EthernetServiceImpl");
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
new file mode 100644
index 0000000..7f893e0
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.ethernet;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.IEthernetServiceListener;
+import android.net.InterfaceConfiguration;
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.LinkAddress;
+import android.net.NetworkCapabilities;
+import android.net.StaticIpConfiguration;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.INetworkManagementService;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.net.BaseNetworkObserver;
+
+import java.io.FileDescriptor;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Tracks Ethernet interfaces and manages interface configurations.
+ *
+ * <p>Interfaces may have different {@link android.net.NetworkCapabilities}. This mapping is defined
+ * in {@code config_ethernet_interfaces}. Notably, some interfaces could be marked as restricted by
+ * not specifying {@link android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED} flag.
+ * Interfaces could have associated {@link android.net.IpConfiguration}.
+ * Ethernet Interfaces may be present at boot time or appear after boot (e.g., for Ethernet adapters
+ * connected over USB). This class supports multiple interfaces. When an interface appears on the
+ * system (or is present at boot time) this class will start tracking it and bring it up. Only
+ * interfaces whose names match the {@code config_ethernet_iface_regex} regular expression are
+ * tracked.
+ *
+ * <p>All public or package private methods must be thread-safe unless stated otherwise.
+ */
+final class EthernetTracker {
+    private final static String TAG = EthernetTracker.class.getSimpleName();
+    private final static boolean DBG = EthernetNetworkFactory.DBG;
+
+    /** Product-dependent regular expression of interface names we track. */
+    private final String mIfaceMatch;
+
+    /** Mapping between {iface name | mac address} -> {NetworkCapabilities} */
+    private final ConcurrentHashMap<String, NetworkCapabilities> mNetworkCapabilities =
+            new ConcurrentHashMap<>();
+    private final ConcurrentHashMap<String, IpConfiguration> mIpConfigurations =
+            new ConcurrentHashMap<>();
+
+    private final INetworkManagementService mNMService;
+    private final Handler mHandler;
+    private final EthernetNetworkFactory mFactory;
+    private final EthernetConfigStore mConfigStore;
+
+    private final RemoteCallbackList<IEthernetServiceListener> mListeners =
+            new RemoteCallbackList<>();
+
+    private volatile IpConfiguration mIpConfigForDefaultInterface;
+
+    EthernetTracker(Context context, Handler handler) {
+        mHandler = handler;
+
+        // The services we use.
+        IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE);
+        mNMService = INetworkManagementService.Stub.asInterface(b);
+
+        // Interface match regex.
+        mIfaceMatch = context.getResources().getString(
+                com.android.internal.R.string.config_ethernet_iface_regex);
+
+        // Read default Ethernet interface configuration from resources
+        final String[] interfaceConfigs = context.getResources().getStringArray(
+                com.android.internal.R.array.config_ethernet_interfaces);
+        for (String strConfig : interfaceConfigs) {
+            parseEthernetConfig(strConfig);
+        }
+
+        mConfigStore = new EthernetConfigStore();
+
+        NetworkCapabilities nc = createNetworkCapabilities(true /* clear default capabilities */);
+        mFactory = new EthernetNetworkFactory(handler, context, nc);
+        mFactory.register();
+    }
+
+    void start() {
+        mConfigStore.read();
+
+        // Default interface is just the first one we want to track.
+        mIpConfigForDefaultInterface = mConfigStore.getIpConfigurationForDefaultInterface();
+        final ArrayMap<String, IpConfiguration> configs = mConfigStore.getIpConfigurations();
+        for (int i = 0; i < configs.size(); i++) {
+            mIpConfigurations.put(configs.keyAt(i), configs.valueAt(i));
+        }
+
+        try {
+            mNMService.registerObserver(new InterfaceObserver());
+        } catch (RemoteException e) {
+            Log.e(TAG, "Could not register InterfaceObserver " + e);
+        }
+
+        mHandler.post(this::trackAvailableInterfaces);
+    }
+
+    void updateIpConfiguration(String iface, IpConfiguration ipConfiguration) {
+        if (DBG) {
+            Log.i(TAG, "updateIpConfiguration, iface: " + iface + ", cfg: " + ipConfiguration);
+        }
+
+        mConfigStore.write(iface, ipConfiguration);
+        mIpConfigurations.put(iface, ipConfiguration);
+
+        mHandler.post(() -> mFactory.updateIpConfiguration(iface, ipConfiguration));
+    }
+
+    IpConfiguration getIpConfiguration(String iface) {
+        return mIpConfigurations.get(iface);
+    }
+
+    boolean isTrackingInterface(String iface) {
+        return mFactory.hasInterface(iface);
+    }
+
+    String[] getInterfaces(boolean includeRestricted) {
+        return mFactory.getAvailableInterfaces(includeRestricted);
+    }
+
+    /**
+     * Returns true if given interface was configured as restricted (doesn't have
+     * NET_CAPABILITY_NOT_RESTRICTED) capability. Otherwise, returns false.
+     */
+    boolean isRestrictedInterface(String iface) {
+        final NetworkCapabilities nc = mNetworkCapabilities.get(iface);
+        return nc != null && !nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+    }
+
+    void addListener(IEthernetServiceListener listener, boolean canUseRestrictedNetworks) {
+        mListeners.register(listener, new ListenerInfo(canUseRestrictedNetworks));
+    }
+
+    void removeListener(IEthernetServiceListener listener) {
+        mListeners.unregister(listener);
+    }
+
+    private void removeInterface(String iface) {
+        mFactory.removeInterface(iface);
+    }
+
+    private void addInterface(String iface) {
+        InterfaceConfiguration config = null;
+        // Bring up the interface so we get link status indications.
+        try {
+            mNMService.setInterfaceUp(iface);
+            config = mNMService.getInterfaceConfig(iface);
+        } catch (RemoteException | IllegalStateException e) {
+            // Either the system is crashing or the interface has disappeared. Just ignore the
+            // error; we haven't modified any state because we only do that if our calls succeed.
+            Log.e(TAG, "Error upping interface " + iface, e);
+        }
+
+        if (config == null) {
+            Log.e(TAG, "Null interface config for " + iface + ". Bailing out.");
+            return;
+        }
+
+        final String hwAddress = config.getHardwareAddress();
+
+        NetworkCapabilities nc = mNetworkCapabilities.get(iface);
+        if (nc == null) {
+            // Try to resolve using mac address
+            nc = mNetworkCapabilities.get(hwAddress);
+            if (nc == null) {
+                nc = createDefaultNetworkCapabilities();
+            }
+        }
+        IpConfiguration ipConfiguration = mIpConfigurations.get(iface);
+        if (ipConfiguration == null) {
+            ipConfiguration = createDefaultIpConfiguration();
+        }
+
+        Log.d(TAG, "Started tracking interface " + iface);
+        mFactory.addInterface(iface, hwAddress, nc, ipConfiguration);
+
+        // Note: if the interface already has link (e.g., if we crashed and got
+        // restarted while it was running), we need to fake a link up notification so we
+        // start configuring it.
+        if (config.hasFlag("running")) {
+            updateInterfaceState(iface, true);
+        }
+    }
+
+    private void updateInterfaceState(String iface, boolean up) {
+        boolean modified = mFactory.updateInterfaceLinkState(iface, up);
+        if (modified) {
+            boolean restricted = isRestrictedInterface(iface);
+            int n = mListeners.beginBroadcast();
+            for (int i = 0; i < n; i++) {
+                try {
+                    if (restricted) {
+                        ListenerInfo listenerInfo = (ListenerInfo) mListeners.getBroadcastCookie(i);
+                        if (!listenerInfo.canUseRestrictedNetworks) {
+                            continue;
+                        }
+                    }
+                    mListeners.getBroadcastItem(i).onAvailabilityChanged(iface, up);
+                } catch (RemoteException e) {
+                    // Do nothing here.
+                }
+            }
+            mListeners.finishBroadcast();
+        }
+    }
+
+    private void maybeTrackInterface(String iface) {
+        if (DBG) Log.i(TAG, "maybeTrackInterface " + iface);
+        // If we don't already track this interface, and if this interface matches
+        // our regex, start tracking it.
+        if (!iface.matches(mIfaceMatch) || mFactory.hasInterface(iface)) {
+            return;
+        }
+
+        if (mIpConfigForDefaultInterface != null) {
+            updateIpConfiguration(iface, mIpConfigForDefaultInterface);
+            mIpConfigForDefaultInterface = null;
+        }
+
+        addInterface(iface);
+    }
+
+    private void trackAvailableInterfaces() {
+        try {
+            final String[] ifaces = mNMService.listInterfaces();
+            for (String iface : ifaces) {
+                maybeTrackInterface(iface);
+            }
+        } catch (RemoteException | IllegalStateException e) {
+            Log.e(TAG, "Could not get list of interfaces " + e);
+        }
+    }
+
+
+    private class InterfaceObserver extends BaseNetworkObserver {
+
+        @Override
+        public void interfaceLinkStateChanged(String iface, boolean up) {
+            if (DBG) {
+                Log.i(TAG, "interfaceLinkStateChanged, iface: " + iface + ", up: " + up);
+            }
+            mHandler.post(() -> updateInterfaceState(iface, up));
+        }
+
+        @Override
+        public void interfaceAdded(String iface) {
+            mHandler.post(() -> maybeTrackInterface(iface));
+        }
+
+        @Override
+        public void interfaceRemoved(String iface) {
+            mHandler.post(() -> removeInterface(iface));
+        }
+    }
+
+    private static class ListenerInfo {
+
+        boolean canUseRestrictedNetworks = false;
+
+        ListenerInfo(boolean canUseRestrictedNetworks) {
+            this.canUseRestrictedNetworks = canUseRestrictedNetworks;
+        }
+    }
+
+    private void parseEthernetConfig(String configString) {
+        String[] tokens = configString.split(";");
+        String name = tokens[0];
+        String capabilities = tokens.length > 1 ? tokens[1] : null;
+        NetworkCapabilities nc = createNetworkCapabilities(
+                !TextUtils.isEmpty(capabilities)  /* clear default capabilities */, capabilities);
+        mNetworkCapabilities.put(name, nc);
+
+        if (tokens.length > 2 && !TextUtils.isEmpty(tokens[2])) {
+            IpConfiguration ipConfig = createStaticIpConfiguration(tokens[2]);
+            mIpConfigurations.put(name, ipConfig);
+        }
+    }
+
+    private static NetworkCapabilities createDefaultNetworkCapabilities() {
+        NetworkCapabilities nc = createNetworkCapabilities(false /* clear default capabilities */);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+        nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
+
+        return nc;
+    }
+
+    private static NetworkCapabilities createNetworkCapabilities(boolean clearDefaultCapabilities) {
+        return createNetworkCapabilities(clearDefaultCapabilities, null);
+    }
+
+    private static NetworkCapabilities createNetworkCapabilities(
+            boolean clearDefaultCapabilities, @Nullable String commaSeparatedCapabilities) {
+
+        NetworkCapabilities nc = new NetworkCapabilities();
+        if (clearDefaultCapabilities) {
+            nc.clearAll();  // Remove default capabilities.
+        }
+        nc.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET);
+        nc.setLinkUpstreamBandwidthKbps(100 * 1000);
+        nc.setLinkDownstreamBandwidthKbps(100 * 1000);
+
+        if (!TextUtils.isEmpty(commaSeparatedCapabilities)) {
+            for (String strNetworkCapability : commaSeparatedCapabilities.split(",")) {
+                if (!TextUtils.isEmpty(strNetworkCapability)) {
+                    nc.addCapability(Integer.valueOf(strNetworkCapability));
+                }
+            }
+        }
+
+        return nc;
+    }
+
+    private static IpConfiguration createStaticIpConfiguration(String strIpAddress) {
+        StaticIpConfiguration staticIpConfiguration = new StaticIpConfiguration();
+        staticIpConfiguration.ipAddress = new LinkAddress(strIpAddress);
+        return new IpConfiguration(
+                IpAssignment.STATIC, ProxySettings.NONE, staticIpConfiguration, null);
+    }
+
+    private static IpConfiguration createDefaultIpConfiguration() {
+        return new IpConfiguration(IpAssignment.DHCP, ProxySettings.NONE, null, null);
+    }
+
+    private void postAndWaitForRunnable(Runnable r) {
+        mHandler.runWithScissors(r, 2000L /* timeout */);
+    }
+
+    void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
+        postAndWaitForRunnable(() -> {
+            pw.println(getClass().getSimpleName());
+            pw.println("Ethernet interface name filter: " + mIfaceMatch);
+            pw.println("Listeners: " + mListeners.getRegisteredCallbackCount());
+            pw.println("IP Configurations:");
+            pw.increaseIndent();
+            for (String iface : mIpConfigurations.keySet()) {
+                pw.println(iface + ": " + mIpConfigurations.get(iface));
+            }
+            pw.decreaseIndent();
+            pw.println();
+
+            pw.println("Network Capabilities:");
+            pw.increaseIndent();
+            for (String iface : mNetworkCapabilities.keySet()) {
+                pw.println(iface + ": " + mNetworkCapabilities.get(iface));
+            }
+            pw.decreaseIndent();
+            pw.println();
+
+            mFactory.dump(fd, pw, args);
+        });
+    }
+}