| /* |
| * 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.annotations.VisibleForTesting; |
| import com.android.internal.util.IndentingPrintWriter; |
| import com.android.server.net.BaseNetworkObserver; |
| |
| import java.io.FileDescriptor; |
| import java.net.InetAddress; |
| import java.util.ArrayList; |
| 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 = parseStaticIpConfiguration(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); |
| nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED); |
| |
| 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; |
| } |
| |
| /** |
| * Parses static IP configuration. |
| * |
| * @param staticIpConfig represents static IP configuration in the following format: {@code |
| * ip=<ip-address/mask> gateway=<ip-address> dns=<comma-sep-ip-addresses> |
| * domains=<comma-sep-domains>} |
| */ |
| @VisibleForTesting |
| static IpConfiguration parseStaticIpConfiguration(String staticIpConfig) { |
| StaticIpConfiguration ipConfig = new StaticIpConfiguration(); |
| |
| 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": |
| ipConfig.ipAddress = new LinkAddress(value); |
| break; |
| case "domains": |
| ipConfig.domains = value; |
| break; |
| case "gateway": |
| ipConfig.gateway = InetAddress.parseNumericAddress(value); |
| break; |
| case "dns": { |
| ArrayList<InetAddress> dnsAddresses = new ArrayList<>(); |
| for (String address: value.split(",")) { |
| dnsAddresses.add(InetAddress.parseNumericAddress(address)); |
| } |
| ipConfig.dnsServers.addAll(dnsAddresses); |
| break; |
| } |
| default : { |
| throw new IllegalArgumentException("Unexpected key: " + key |
| + " in " + staticIpConfig); |
| } |
| } |
| } |
| return new IpConfiguration(IpAssignment.STATIC, ProxySettings.NONE, ipConfig, 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); |
| }); |
| } |
| } |