/* * 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 static android.net.TestNetworkManager.TEST_TAP_PREFIX; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.res.Resources; import android.net.ConnectivityResources; import android.net.EthernetManager; import android.net.IEthernetServiceListener; import android.net.IEthernetNetworkManagementListener; import android.net.INetd; import android.net.ITetheredInterfaceCallback; import android.net.InterfaceConfigurationParcel; 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.ConditionVariable; import android.os.Handler; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ServiceSpecificException; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; import com.android.net.module.util.BaseNetdUnsolicitedEventListener; import com.android.net.module.util.NetdUtils; import com.android.net.module.util.PermissionUtils; import java.io.FileDescriptor; import java.net.InetAddress; import java.util.ArrayList; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; /** * Tracks Ethernet interfaces and manages interface configurations. * *

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. * *

All public or package private methods must be thread-safe unless stated otherwise. */ @VisibleForTesting(visibility = PACKAGE) public class EthernetTracker { private static final int INTERFACE_MODE_CLIENT = 1; private static final int INTERFACE_MODE_SERVER = 2; private static final String TAG = EthernetTracker.class.getSimpleName(); private static final boolean DBG = EthernetNetworkFactory.DBG; private static final String TEST_IFACE_REGEXP = TEST_TAP_PREFIX + "\\d+"; private static final String LEGACY_IFACE_REGEXP = "eth\\d"; /** * Interface names we track. This is a product-dependent regular expression, plus, * if setIncludeTestInterfaces is true, any test interfaces. */ private String mIfaceMatch; /** * Track test interfaces if true, don't track otherwise. */ private boolean mIncludeTestInterfaces = false; /** Mapping between {iface name | mac address} -> {NetworkCapabilities} */ private final ConcurrentHashMap mNetworkCapabilities = new ConcurrentHashMap<>(); private final ConcurrentHashMap mIpConfigurations = new ConcurrentHashMap<>(); private final Context mContext; private final INetd mNetd; private final Handler mHandler; private final EthernetNetworkFactory mFactory; private final EthernetConfigStore mConfigStore; private final Dependencies mDeps; private final RemoteCallbackList mListeners = new RemoteCallbackList<>(); private final TetheredInterfaceRequestList mTetheredInterfaceRequests = new TetheredInterfaceRequestList(); // Used only on the handler thread private String mDefaultInterface; private int mDefaultInterfaceMode = INTERFACE_MODE_CLIENT; // Tracks whether clients were notified that the tethered interface is available private boolean mTetheredInterfaceWasAvailable = false; private volatile IpConfiguration mIpConfigForDefaultInterface; private class TetheredInterfaceRequestList extends RemoteCallbackList { @Override public void onCallbackDied(ITetheredInterfaceCallback cb, Object cookie) { mHandler.post(EthernetTracker.this::maybeUntetherDefaultInterface); } } public static class Dependencies { // TODO: remove legacy resource fallback after migrating its overlays. private String getPlatformRegexResource(Context context) { final Resources r = context.getResources(); final int resId = r.getIdentifier("config_ethernet_iface_regex", "string", context.getPackageName()); return r.getString(resId); } // TODO: remove legacy resource fallback after migrating its overlays. private String[] getPlatformInterfaceConfigs(Context context) { final Resources r = context.getResources(); final int resId = r.getIdentifier("config_ethernet_interfaces", "array", context.getPackageName()); return r.getStringArray(resId); } public String getInterfaceRegexFromResource(Context context) { final String platformRegex = getPlatformRegexResource(context); final String match; if (!LEGACY_IFACE_REGEXP.equals(platformRegex)) { // Platform resource is not the historical default: use the overlay match = platformRegex; } else { final ConnectivityResources resources = new ConnectivityResources(context); match = resources.get().getString( com.android.connectivity.resources.R.string.config_ethernet_iface_regex); } return match; } public String[] getInterfaceConfigFromResource(Context context) { final String[] platformInterfaceConfigs = getPlatformInterfaceConfigs(context); final String[] interfaceConfigs; if (platformInterfaceConfigs.length != 0) { // Platform resource is not the historical default: use the overlay interfaceConfigs = platformInterfaceConfigs; } else { final ConnectivityResources resources = new ConnectivityResources(context); interfaceConfigs = resources.get().getStringArray( com.android.connectivity.resources.R.array.config_ethernet_interfaces); } return interfaceConfigs; } } EthernetTracker(@NonNull final Context context, @NonNull final Handler handler, @NonNull final EthernetNetworkFactory factory, @NonNull final INetd netd) { this(context, handler, factory, netd, new Dependencies()); } @VisibleForTesting EthernetTracker(@NonNull final Context context, @NonNull final Handler handler, @NonNull final EthernetNetworkFactory factory, @NonNull final INetd netd, @NonNull final Dependencies deps) { mContext = context; mHandler = handler; mFactory = factory; mNetd = netd; mDeps = deps; // Interface match regex. updateIfaceMatchRegexp(); // Read default Ethernet interface configuration from resources final String[] interfaceConfigs = mDeps.getInterfaceConfigFromResource(context); for (String strConfig : interfaceConfigs) { parseEthernetConfig(strConfig); } mConfigStore = new EthernetConfigStore(); } void start() { mFactory.register(); mConfigStore.read(); // Default interface is just the first one we want to track. mIpConfigForDefaultInterface = mConfigStore.getIpConfigurationForDefaultInterface(); final ArrayMap configs = mConfigStore.getIpConfigurations(); for (int i = 0; i < configs.size(); i++) { mIpConfigurations.put(configs.keyAt(i), configs.valueAt(i)); } try { PermissionUtils.enforceNetworkStackPermission(mContext); mNetd.registerUnsolicitedEventListener(new InterfaceObserver()); } catch (RemoteException | ServiceSpecificException 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); } writeIpConfiguration(iface, ipConfiguration); mHandler.post(() -> { mFactory.updateInterface(iface, ipConfiguration, null, null); broadcastInterfaceStateChange(iface); }); } private void writeIpConfiguration(@NonNull final String iface, @NonNull final IpConfiguration ipConfig) { mConfigStore.write(iface, ipConfig); mIpConfigurations.put(iface, ipConfig); } private IpConfiguration getIpConfigurationForCallback(String iface, int state) { return (state == EthernetManager.STATE_ABSENT) ? null : getOrCreateIpConfiguration(iface); } private void ensureRunningOnEthernetServiceThread() { if (mHandler.getLooper().getThread() != Thread.currentThread()) { throw new IllegalStateException( "Not running on EthernetService thread: " + Thread.currentThread().getName()); } } /** * Broadcast the link state or IpConfiguration change of existing Ethernet interfaces to all * listeners. */ protected void broadcastInterfaceStateChange(@NonNull String iface) { ensureRunningOnEthernetServiceThread(); final int state = mFactory.getInterfaceState(iface); final int role = getInterfaceRole(iface); final IpConfiguration config = getIpConfigurationForCallback(iface, state); final int n = mListeners.beginBroadcast(); for (int i = 0; i < n; i++) { try { mListeners.getBroadcastItem(i).onInterfaceStateChanged(iface, state, role, config); } catch (RemoteException e) { // Do nothing here. } } mListeners.finishBroadcast(); } /** * Unicast the interface state or IpConfiguration change of existing Ethernet interfaces to a * specific listener. */ protected void unicastInterfaceStateChange(@NonNull IEthernetServiceListener listener, @NonNull String iface) { ensureRunningOnEthernetServiceThread(); final int state = mFactory.getInterfaceState(iface); final int role = getInterfaceRole(iface); final IpConfiguration config = getIpConfigurationForCallback(iface, state); try { listener.onInterfaceStateChanged(iface, state, role, config); } catch (RemoteException e) { // Do nothing here. } } @VisibleForTesting(visibility = PACKAGE) protected void updateConfiguration(@NonNull final String iface, @Nullable final IpConfiguration ipConfig, @Nullable final NetworkCapabilities capabilities, @Nullable final IEthernetNetworkManagementListener listener) { if (DBG) { Log.i(TAG, "updateConfiguration, iface: " + iface + ", capabilities: " + capabilities + ", ipConfig: " + ipConfig); } final IpConfiguration localIpConfig = ipConfig == null ? null : new IpConfiguration(ipConfig); if (ipConfig != null) { writeIpConfiguration(iface, localIpConfig); } if (null != capabilities) { mNetworkCapabilities.put(iface, capabilities); } mHandler.post(() -> { mFactory.updateInterface(iface, localIpConfig, capabilities, listener); broadcastInterfaceStateChange(iface); }); } @VisibleForTesting(visibility = PACKAGE) protected void connectNetwork(@NonNull final String iface, @Nullable final IEthernetNetworkManagementListener listener) { mHandler.post(() -> updateInterfaceState(iface, true, listener)); } @VisibleForTesting(visibility = PACKAGE) protected void disconnectNetwork(@NonNull final String iface, @Nullable final IEthernetNetworkManagementListener listener) { mHandler.post(() -> updateInterfaceState(iface, false, listener)); } IpConfiguration getIpConfiguration(String iface) { return mIpConfigurations.get(iface); } @VisibleForTesting(visibility = PACKAGE) protected 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) { mHandler.post(() -> { if (!mListeners.register(listener, new ListenerInfo(canUseRestrictedNetworks))) { // Remote process has already died return; } for (String iface : getInterfaces(canUseRestrictedNetworks)) { unicastInterfaceStateChange(listener, iface); } }); } void removeListener(IEthernetServiceListener listener) { mHandler.post(() -> mListeners.unregister(listener)); } public void setIncludeTestInterfaces(boolean include) { mHandler.post(() -> { mIncludeTestInterfaces = include; updateIfaceMatchRegexp(); mHandler.post(() -> trackAvailableInterfaces()); }); } public void requestTetheredInterface(ITetheredInterfaceCallback callback) { mHandler.post(() -> { if (!mTetheredInterfaceRequests.register(callback)) { // Remote process has already died return; } if (mDefaultInterfaceMode == INTERFACE_MODE_SERVER) { if (mTetheredInterfaceWasAvailable) { notifyTetheredInterfaceAvailable(callback, mDefaultInterface); } return; } setDefaultInterfaceMode(INTERFACE_MODE_SERVER); }); } public void releaseTetheredInterface(ITetheredInterfaceCallback callback) { mHandler.post(() -> { mTetheredInterfaceRequests.unregister(callback); maybeUntetherDefaultInterface(); }); } private void notifyTetheredInterfaceAvailable(ITetheredInterfaceCallback cb, String iface) { try { cb.onAvailable(iface); } catch (RemoteException e) { Log.e(TAG, "Error sending tethered interface available callback", e); } } private void notifyTetheredInterfaceUnavailable(ITetheredInterfaceCallback cb) { try { cb.onUnavailable(); } catch (RemoteException e) { Log.e(TAG, "Error sending tethered interface available callback", e); } } private void maybeUntetherDefaultInterface() { if (mTetheredInterfaceRequests.getRegisteredCallbackCount() > 0) return; if (mDefaultInterfaceMode == INTERFACE_MODE_CLIENT) return; setDefaultInterfaceMode(INTERFACE_MODE_CLIENT); } private void setDefaultInterfaceMode(int mode) { Log.d(TAG, "Setting default interface mode to " + mode); mDefaultInterfaceMode = mode; if (mDefaultInterface != null) { removeInterface(mDefaultInterface); addInterface(mDefaultInterface); } } private int getInterfaceRole(final String iface) { if (!mFactory.hasInterface(iface)) return EthernetManager.ROLE_NONE; final int mode = getInterfaceMode(iface); return (mode == INTERFACE_MODE_CLIENT) ? EthernetManager.ROLE_CLIENT : EthernetManager.ROLE_SERVER; } private int getInterfaceMode(final String iface) { if (iface.equals(mDefaultInterface)) { return mDefaultInterfaceMode; } return INTERFACE_MODE_CLIENT; } private void removeInterface(String iface) { mFactory.removeInterface(iface); maybeUpdateServerModeInterfaceState(iface, false); } private void stopTrackingInterface(String iface) { removeInterface(iface); if (iface.equals(mDefaultInterface)) { mDefaultInterface = null; } broadcastInterfaceStateChange(iface); } private void addInterface(String iface) { InterfaceConfigurationParcel config = null; // Bring up the interface so we get link status indications. try { PermissionUtils.enforceNetworkStackPermission(mContext); NetdUtils.setInterfaceUp(mNetd, iface); config = NetdUtils.getInterfaceConfigParcel(mNetd, iface); } catch (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 parcelable for " + iface + ". Bailing out."); return; } final String hwAddress = config.hwAddr; NetworkCapabilities nc = mNetworkCapabilities.get(iface); if (nc == null) { // Try to resolve using mac address nc = mNetworkCapabilities.get(hwAddress); if (nc == null) { final boolean isTestIface = iface.matches(TEST_IFACE_REGEXP); nc = createDefaultNetworkCapabilities(isTestIface); } } final int mode = getInterfaceMode(iface); if (mode == INTERFACE_MODE_CLIENT) { IpConfiguration ipConfiguration = getOrCreateIpConfiguration(iface); Log.d(TAG, "Tracking interface in client mode: " + iface); mFactory.addInterface(iface, hwAddress, ipConfiguration, nc); } else { maybeUpdateServerModeInterfaceState(iface, true); } // 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 (NetdUtils.hasFlag(config, "running")) { updateInterfaceState(iface, true); } } private void updateInterfaceState(String iface, boolean up) { updateInterfaceState(iface, up, null /* listener */); } private void updateInterfaceState(@NonNull final String iface, final boolean up, @Nullable final IEthernetNetworkManagementListener listener) { final int mode = getInterfaceMode(iface); final boolean factoryLinkStateUpdated = (mode == INTERFACE_MODE_CLIENT) && mFactory.updateInterfaceLinkState(iface, up, listener); if (factoryLinkStateUpdated) { broadcastInterfaceStateChange(iface); } } private void maybeUpdateServerModeInterfaceState(String iface, boolean available) { if (available == mTetheredInterfaceWasAvailable || !iface.equals(mDefaultInterface)) return; Log.d(TAG, (available ? "Tracking" : "No longer tracking") + " interface in server mode: " + iface); final int pendingCbs = mTetheredInterfaceRequests.beginBroadcast(); for (int i = 0; i < pendingCbs; i++) { ITetheredInterfaceCallback item = mTetheredInterfaceRequests.getBroadcastItem(i); if (available) { notifyTetheredInterfaceAvailable(item, iface); } else { notifyTetheredInterfaceUnavailable(item); } } mTetheredInterfaceRequests.finishBroadcast(); mTetheredInterfaceWasAvailable = available; } private void maybeTrackInterface(String iface) { if (!iface.matches(mIfaceMatch)) { return; } // If we don't already track this interface, and if this interface matches // our regex, start tracking it. if (mFactory.hasInterface(iface) || iface.equals(mDefaultInterface)) { if (DBG) Log.w(TAG, "Ignoring already-tracked interface " + iface); return; } if (DBG) Log.i(TAG, "maybeTrackInterface: " + iface); // TODO: avoid making an interface default if it has configured NetworkCapabilities. if (mDefaultInterface == null) { mDefaultInterface = iface; } if (mIpConfigForDefaultInterface != null) { updateIpConfiguration(iface, mIpConfigForDefaultInterface); mIpConfigForDefaultInterface = null; } addInterface(iface); broadcastInterfaceStateChange(iface); } private void trackAvailableInterfaces() { try { final String[] ifaces = mNetd.interfaceGetList(); for (String iface : ifaces) { maybeTrackInterface(iface); } } catch (RemoteException | ServiceSpecificException e) { Log.e(TAG, "Could not get list of interfaces " + e); } } private class InterfaceObserver extends BaseNetdUnsolicitedEventListener { @Override public void onInterfaceLinkStateChanged(String iface, boolean up) { if (DBG) { Log.i(TAG, "interfaceLinkStateChanged, iface: " + iface + ", up: " + up); } mHandler.post(() -> updateInterfaceState(iface, up)); } @Override public void onInterfaceAdded(String iface) { if (DBG) { Log.i(TAG, "onInterfaceAdded, iface: " + iface); } mHandler.post(() -> maybeTrackInterface(iface)); } @Override public void onInterfaceRemoved(String iface) { if (DBG) { Log.i(TAG, "onInterfaceRemoved, iface: " + iface); } mHandler.post(() -> stopTrackingInterface(iface)); } } private static class ListenerInfo { boolean canUseRestrictedNetworks = false; ListenerInfo(boolean canUseRestrictedNetworks) { this.canUseRestrictedNetworks = canUseRestrictedNetworks; } } /** * Parses an Ethernet interface configuration * * @param configString represents an Ethernet configuration in the following format: {@code * ;[Network Capabilities];[IP config];[Override Transport]} */ private void parseEthernetConfig(String configString) { final EthernetTrackerConfig config = createEthernetTrackerConfig(configString); NetworkCapabilities nc = createNetworkCapabilities( !TextUtils.isEmpty(config.mCapabilities) /* clear default capabilities */, config.mCapabilities, config.mTransport).build(); mNetworkCapabilities.put(config.mIface, nc); if (null != config.mIpConfig) { IpConfiguration ipConfig = parseStaticIpConfiguration(config.mIpConfig); mIpConfigurations.put(config.mIface, ipConfig); } } @VisibleForTesting static EthernetTrackerConfig createEthernetTrackerConfig(@NonNull final String configString) { Objects.requireNonNull(configString, "EthernetTrackerConfig requires non-null config"); return new EthernetTrackerConfig(configString.split(";", /* limit of tokens */ 4)); } private static NetworkCapabilities createDefaultNetworkCapabilities(boolean isTestIface) { NetworkCapabilities.Builder builder = createNetworkCapabilities( false /* clear default capabilities */, null, null) .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING) .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED) .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED); if (isTestIface) { builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST); } else { builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); } return builder.build(); } /** * Parses a static list of network capabilities * * @param clearDefaultCapabilities Indicates whether or not to clear any default capabilities * @param commaSeparatedCapabilities A comma separated string list of integer encoded * NetworkCapability.NET_CAPABILITY_* values * @param overrideTransport A string representing a single integer encoded override transport * type. Must be one of the NetworkCapability.TRANSPORT_* * values. TRANSPORT_VPN is not supported. Errors with input * will cause the override to be ignored. */ @VisibleForTesting static NetworkCapabilities.Builder createNetworkCapabilities( boolean clearDefaultCapabilities, @Nullable String commaSeparatedCapabilities, @Nullable String overrideTransport) { final NetworkCapabilities.Builder builder = clearDefaultCapabilities ? NetworkCapabilities.Builder.withoutDefaultCapabilities() : new NetworkCapabilities.Builder(); // Determine the transport type. If someone has tried to define an override transport then // attempt to add it. Since we can only have one override, all errors with it will // gracefully default back to TRANSPORT_ETHERNET and warn the user. VPN is not allowed as an // override type. Wifi Aware and LoWPAN are currently unsupported as well. int transport = NetworkCapabilities.TRANSPORT_ETHERNET; if (!TextUtils.isEmpty(overrideTransport)) { try { int parsedTransport = Integer.valueOf(overrideTransport); if (parsedTransport == NetworkCapabilities.TRANSPORT_VPN || parsedTransport == NetworkCapabilities.TRANSPORT_WIFI_AWARE || parsedTransport == NetworkCapabilities.TRANSPORT_LOWPAN) { Log.e(TAG, "Override transport '" + parsedTransport + "' is not supported. " + "Defaulting to TRANSPORT_ETHERNET"); } else { transport = parsedTransport; } } catch (NumberFormatException nfe) { Log.e(TAG, "Override transport type '" + overrideTransport + "' " + "could not be parsed. Defaulting to TRANSPORT_ETHERNET"); } } // Apply the transport. If the user supplied a valid number that is not a valid transport // then adding will throw an exception. Default back to TRANSPORT_ETHERNET if that happens try { builder.addTransportType(transport); } catch (IllegalArgumentException iae) { Log.e(TAG, transport + " is not a valid NetworkCapability.TRANSPORT_* value. " + "Defaulting to TRANSPORT_ETHERNET"); builder.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET); } builder.setLinkUpstreamBandwidthKbps(100 * 1000); builder.setLinkDownstreamBandwidthKbps(100 * 1000); if (!TextUtils.isEmpty(commaSeparatedCapabilities)) { for (String strNetworkCapability : commaSeparatedCapabilities.split(",")) { if (!TextUtils.isEmpty(strNetworkCapability)) { try { builder.addCapability(Integer.valueOf(strNetworkCapability)); } catch (NumberFormatException nfe) { Log.e(TAG, "Capability '" + strNetworkCapability + "' could not be parsed"); } catch (IllegalArgumentException iae) { Log.e(TAG, strNetworkCapability + " is not a valid " + "NetworkCapability.NET_CAPABILITY_* value"); } } } } // Ethernet networks have no way to update the following capabilities, so they always // have them. builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING); builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED); builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED); return builder; } /** * Parses static IP configuration. * * @param staticIpConfig represents static IP configuration in the following format: {@code * ip= gateway= dns= * domains=} */ @VisibleForTesting static IpConfiguration parseStaticIpConfiguration(String staticIpConfig) { final StaticIpConfiguration.Builder staticIpConfigBuilder = new StaticIpConfiguration.Builder(); for (String keyValueAsString : staticIpConfig.trim().split(" ")) { if (TextUtils.isEmpty(keyValueAsString)) continue; String[] pair = keyValueAsString.split("="); if (pair.length != 2) { throw new IllegalArgumentException("Unexpected token: " + keyValueAsString + " in " + staticIpConfig); } String key = pair[0]; String value = pair[1]; switch (key) { case "ip": staticIpConfigBuilder.setIpAddress(new LinkAddress(value)); break; case "domains": staticIpConfigBuilder.setDomains(value); break; case "gateway": staticIpConfigBuilder.setGateway(InetAddress.parseNumericAddress(value)); break; case "dns": { ArrayList dnsAddresses = new ArrayList<>(); for (String address: value.split(",")) { dnsAddresses.add(InetAddress.parseNumericAddress(address)); } staticIpConfigBuilder.setDnsServers(dnsAddresses); break; } default : { throw new IllegalArgumentException("Unexpected key: " + key + " in " + staticIpConfig); } } } return createIpConfiguration(staticIpConfigBuilder.build()); } private static IpConfiguration createIpConfiguration( @NonNull final StaticIpConfiguration staticIpConfig) { return new IpConfiguration.Builder().setStaticIpConfiguration(staticIpConfig).build(); } private IpConfiguration getOrCreateIpConfiguration(String iface) { IpConfiguration ret = mIpConfigurations.get(iface); if (ret != null) return ret; ret = new IpConfiguration(); ret.setIpAssignment(IpAssignment.DHCP); ret.setProxySettings(ProxySettings.NONE); return ret; } private void updateIfaceMatchRegexp() { final String match = mDeps.getInterfaceRegexFromResource(mContext); mIfaceMatch = mIncludeTestInterfaces ? "(" + match + "|" + TEST_IFACE_REGEXP + ")" : match; Log.d(TAG, "Interface match regexp set to '" + mIfaceMatch + "'"); } /** * Validate if a given interface is valid for testing. * * @param iface the name of the interface to validate. * @return {@code true} if test interfaces are enabled and the given {@code iface} has a test * interface prefix, {@code false} otherwise. */ public boolean isValidTestInterface(@NonNull final String iface) { return mIncludeTestInterfaces && iface.matches(TEST_IFACE_REGEXP); } private void postAndWaitForRunnable(Runnable r) { final ConditionVariable cv = new ConditionVariable(); if (mHandler.post(() -> { r.run(); cv.open(); })) { cv.block(2000L); } } void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) { postAndWaitForRunnable(() -> { pw.println(getClass().getSimpleName()); pw.println("Ethernet interface name filter: " + mIfaceMatch); pw.println("Default interface: " + mDefaultInterface); pw.println("Default interface mode: " + mDefaultInterfaceMode); pw.println("Tethered interface requests: " + mTetheredInterfaceRequests.getRegisteredCallbackCount()); 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); }); } @VisibleForTesting static class EthernetTrackerConfig { final String mIface; final String mCapabilities; final String mIpConfig; final String mTransport; EthernetTrackerConfig(@NonNull final String[] tokens) { Objects.requireNonNull(tokens, "EthernetTrackerConfig requires non-null tokens"); mIface = tokens[0]; mCapabilities = tokens.length > 1 ? tokens[1] : null; mIpConfig = tokens.length > 2 && !TextUtils.isEmpty(tokens[2]) ? tokens[2] : null; mTransport = tokens.length > 3 ? tokens[3] : null; } } }