/*
* 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.EthernetManager.ETHERNET_STATE_DISABLED;
import static android.net.EthernetManager.ETHERNET_STATE_ENABLED;
import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import static com.android.net.module.util.netlink.NetlinkConstants.IFF_UP;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.net.EthernetManager;
import android.net.IEthernetServiceListener;
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.system.OsConstants;
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.modules.utils.build.SdkLevel;
import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.SharedLog;
import com.android.net.module.util.ip.NetlinkMonitor;
import com.android.net.module.util.netlink.NetlinkConstants;
import com.android.net.module.util.netlink.NetlinkMessage;
import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.net.module.util.netlink.RtNetlinkLinkMessage;
import com.android.net.module.util.netlink.StructIfinfoMsg;
import com.android.server.connectivity.ConnectivityResources;
import java.io.FileDescriptor;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
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+";
// TODO: consider using SharedLog consistently across ethernet service.
private static final SharedLog sLog = new SharedLog(TAG);
@VisibleForTesting
public static final NetworkCapabilities DEFAULT_CAPABILITIES = new NetworkCapabilities.Builder()
.addTransportType(TRANSPORT_ETHERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
// TODO: do not hardcode link bandwidth.
.setLinkUpstreamBandwidthKbps(100 * 1000 /* 100 Mbps */)
.setLinkDownstreamBandwidthKbps(100 * 1000 /* 100 Mbps */)
.build();
/**
* Interface names we track. This is a product-dependent regular expression.
* Use isValidEthernetInterface to check if a interface name is a valid ethernet interface (this
* includes test interfaces if setIncludeTestInterfaces is set to true).
*/
private final String mIfaceMatch;
/**
* Track test interfaces if true, don't track otherwise.
* Volatile is needed as getEthernetInterfaceList() does not run on the handler thread.
*/
private volatile 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 NetlinkMonitor mNetlinkMonitor;
private final Dependencies mDeps;
private final RemoteCallbackList mListeners =
new RemoteCallbackList<>();
private final TetheredInterfaceRequestList mTetheredInterfaceRequests =
new TetheredInterfaceRequestList();
// The first interface discovered is set as the mTetheringInterface. It is the interface that is
// returned when a tethered interface is requested; until then, it remains in client mode. Its
// current mode is reflected in mTetheringInterfaceMode.
private String mTetheringInterface;
// If the tethering interface is in server mode, it is not tracked by factory. The HW address
// must be maintained by the EthernetTracker. Its current mode is reflected in
// mTetheringInterfaceMode.
private String mTetheringInterfaceHwAddr;
private int mTetheringInterfaceMode = INTERFACE_MODE_CLIENT;
// Tracks whether clients were notified that the tethered interface is available
private boolean mTetheredInterfaceWasAvailable = false;
// Tracks the current state of ethernet as configured by EthernetManager#setEthernetEnabled.
private boolean mIsEthernetEnabled = true;
private class TetheredInterfaceRequestList extends
RemoteCallbackList {
@Override
public void onCallbackDied(ITetheredInterfaceCallback cb, Object cookie) {
mHandler.post(EthernetTracker.this::maybeUntetherInterface);
}
}
public static class Dependencies {
public String getInterfaceRegexFromResource(Context context) {
final ConnectivityResources resources = new ConnectivityResources(context);
return resources.get().getString(
com.android.connectivity.resources.R.string.config_ethernet_iface_regex);
}
public String[] getInterfaceConfigFromResource(Context context) {
final ConnectivityResources resources = new ConnectivityResources(context);
return resources.get().getStringArray(
com.android.connectivity.resources.R.array.config_ethernet_interfaces);
}
public boolean isAtLeastB() {
return SdkLevel.isAtLeastB();
}
}
private class EthernetNetlinkMonitor extends NetlinkMonitor {
EthernetNetlinkMonitor(Handler handler) {
super(handler, sLog, EthernetNetlinkMonitor.class.getSimpleName(),
OsConstants.NETLINK_ROUTE, NetlinkConstants.RTMGRP_LINK);
}
private void onNewLink(String ifname, boolean linkUp) {
if (!mFactory.hasInterface(ifname) && !ifname.equals(mTetheringInterface)) {
Log.i(TAG, "onInterfaceAdded, iface: " + ifname);
maybeTrackInterface(ifname);
}
Log.i(TAG, "interfaceLinkStateChanged, iface: " + ifname + ", up: " + linkUp);
updateInterfaceState(ifname, linkUp);
}
private void onDelLink(String ifname) {
Log.i(TAG, "onInterfaceRemoved, iface: " + ifname);
stopTrackingInterface(ifname);
}
private void processRtNetlinkLinkMessage(RtNetlinkLinkMessage msg) {
final StructIfinfoMsg ifinfomsg = msg.getIfinfoHeader();
// check if the message is valid
if (ifinfomsg.family != OsConstants.AF_UNSPEC) return;
// ignore messages for the loopback interface
if ((ifinfomsg.flags & OsConstants.IFF_LOOPBACK) != 0) return;
// check if the received message applies to an ethernet interface.
final String ifname = msg.getInterfaceName();
if (!isValidEthernetInterface(ifname)) return;
switch (msg.getHeader().nlmsg_type) {
case NetlinkConstants.RTM_NEWLINK:
final boolean linkUp = (ifinfomsg.flags & NetlinkConstants.IFF_LOWER_UP) != 0;
onNewLink(ifname, linkUp);
break;
case NetlinkConstants.RTM_DELLINK:
onDelLink(ifname);
break;
default:
Log.e(TAG, "Unknown rtnetlink link msg type: " + msg);
break;
}
}
// Note: processNetlinkMessage is called on the handler thread.
@Override
protected void processNetlinkMessage(NetlinkMessage nlMsg, long whenMs) {
if (nlMsg instanceof RtNetlinkLinkMessage) {
processRtNetlinkLinkMessage((RtNetlinkLinkMessage) nlMsg);
} else {
Log.e(TAG, "Unknown netlink message: " + nlMsg);
}
}
}
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.
String ifaceMatchRegex = mDeps.getInterfaceRegexFromResource(mContext);
// "*" is a magic string to indicate "pick the default".
if (ifaceMatchRegex.equals("*")) {
if (SdkLevel.isAtLeastV()) {
// On V+, include both usb%d and eth%d interfaces.
ifaceMatchRegex = "(usb|eth)\\d+";
} else {
// On T and U, include only eth%d interfaces.
ifaceMatchRegex = "eth\\d+";
}
}
mIfaceMatch = ifaceMatchRegex;
// Read default Ethernet interface configuration from resources
final String[] interfaceConfigs = mDeps.getInterfaceConfigFromResource(context);
for (String strConfig : interfaceConfigs) {
parseEthernetConfig(strConfig);
}
mConfigStore = new EthernetConfigStore();
mNetlinkMonitor = new EthernetNetlinkMonitor(mHandler);
}
void start() {
mFactory.register();
mConfigStore.read();
final ArrayMap configs = mConfigStore.getIpConfigurations();
for (int i = 0; i < configs.size(); i++) {
mIpConfigurations.put(configs.keyAt(i), configs.valueAt(i));
}
mHandler.post(() -> {
mNetlinkMonitor.start();
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);
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() {
HandlerUtils.ensureRunningOnHandlerThread(mHandler);
}
/**
* Broadcast the link state or IpConfiguration change of existing Ethernet interfaces to all
* listeners.
*/
protected void broadcastInterfaceStateChange(@NonNull String iface) {
ensureRunningOnEthernetServiceThread();
final int state = getInterfaceState(iface);
final int role = getInterfaceRole(iface);
final IpConfiguration config = getIpConfigurationForCallback(iface, state);
final boolean isRestricted = isRestrictedInterface(iface);
final int n = mListeners.beginBroadcast();
for (int i = 0; i < n; i++) {
try {
if (isRestricted) {
final ListenerInfo info = (ListenerInfo) mListeners.getBroadcastCookie(i);
if (!info.canUseRestrictedNetworks) continue;
}
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 = 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 EthernetCallback cb) {
if (DBG) {
Log.i(TAG, "updateConfiguration, iface: " + iface + ", capabilities: " + capabilities
+ ", ipConfig: " + ipConfig);
}
// TODO: do the right thing if the interface was in server mode: either fail this operation,
// or take the interface out of server mode.
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);
// only broadcast state change when the ip configuration is updated.
if (ipConfig != null) {
broadcastInterfaceStateChange(iface);
}
// Always return success. Even if the interface does not currently exist, the
// IpConfiguration and NetworkCapabilities were saved and will be applied if an
// interface with the given name is ever added.
cb.onResult(iface);
});
}
/** Configure the administrative state of ethernet interface by toggling IFF_UP. */
public void setInterfaceEnabled(String iface, boolean enabled, EthernetCallback cb) {
mHandler.post(() -> setInterfaceAdministrativeState(iface, enabled, cb));
}
IpConfiguration getIpConfiguration(String iface) {
return mIpConfigurations.get(iface);
}
@VisibleForTesting(visibility = PACKAGE)
protected boolean isTrackingInterface(String iface) {
return mFactory.hasInterface(iface);
}
private List getAllInterfaces() {
final ArrayList interfaces = new ArrayList<>(
List.of(mFactory.getAvailableInterfaces(/* includeRestricted */ true)));
if (mTetheringInterfaceMode == INTERFACE_MODE_SERVER && mTetheringInterface != null) {
interfaces.add(mTetheringInterface);
}
return interfaces;
}
String[] getClientModeInterfaces(boolean includeRestricted) {
return mFactory.getAvailableInterfaces(includeRestricted);
}
List getEthernetInterfaceList() {
final List interfaceList = new ArrayList();
final Enumeration ifaces;
try {
ifaces = NetworkInterface.getNetworkInterfaces();
} catch (SocketException e) {
Log.e(TAG, "Failed to get ethernet interfaces: ", e);
return interfaceList;
}
// There is a possible race with setIncludeTestInterfaces() which can affect
// isValidEthernetInterface (it returns true for test interfaces if setIncludeTestInterfaces
// is set to true).
// setIncludeTestInterfaces() is only used in tests, and since getEthernetInterfaceList()
// does not run on the handler thread, the behavior around setIncludeTestInterfaces() is
// indeterminate either way. This can easily be circumvented by waiting on a callback from
// a test interface after calling setIncludeTestInterfaces() before calling this function.
// In production code, this has no effect.
while (ifaces.hasMoreElements()) {
NetworkInterface iface = ifaces.nextElement();
if (isValidEthernetInterface(iface.getName())) interfaceList.add(iface.getName());
}
return interfaceList;
}
/**
* 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 : getClientModeInterfaces(canUseRestrictedNetworks)) {
unicastInterfaceStateChange(listener, iface);
}
if (mTetheringInterface != null && mTetheringInterfaceMode == INTERFACE_MODE_SERVER) {
unicastInterfaceStateChange(listener, mTetheringInterface);
}
unicastEthernetStateChange(listener, mIsEthernetEnabled);
});
}
void removeListener(IEthernetServiceListener listener) {
mHandler.post(() -> mListeners.unregister(listener));
}
public void setIncludeTestInterfaces(boolean include) {
mHandler.post(() -> {
mIncludeTestInterfaces = include;
if (include) {
trackAvailableInterfaces();
} else {
removeTestData();
// remove all test interfaces
for (String iface : getAllInterfaces()) {
if (isValidEthernetInterface(iface)) continue;
stopTrackingInterface(iface);
}
}
});
}
private void removeTestData() {
removeTestIpData();
removeTestCapabilityData();
}
private void removeTestIpData() {
final Iterator iterator = mIpConfigurations.keySet().iterator();
while (iterator.hasNext()) {
final String iface = iterator.next();
if (iface.matches(TEST_IFACE_REGEXP)) {
mConfigStore.write(iface, null);
iterator.remove();
}
}
}
private void removeTestCapabilityData() {
mNetworkCapabilities.keySet().removeIf(iface -> iface.matches(TEST_IFACE_REGEXP));
}
public void requestTetheredInterface(ITetheredInterfaceCallback callback) {
mHandler.post(() -> {
if (!mTetheredInterfaceRequests.register(callback)) {
// Remote process has already died
return;
}
if (mTetheringInterfaceMode == INTERFACE_MODE_SERVER) {
if (mTetheredInterfaceWasAvailable) {
notifyTetheredInterfaceAvailable(callback, mTetheringInterface);
}
return;
}
setTetheringInterfaceMode(INTERFACE_MODE_SERVER);
});
}
public void releaseTetheredInterface(ITetheredInterfaceCallback callback) {
mHandler.post(() -> {
mTetheredInterfaceRequests.unregister(callback);
maybeUntetherInterface();
});
}
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 maybeUntetherInterface() {
if (mTetheredInterfaceRequests.getRegisteredCallbackCount() > 0) return;
if (mTetheringInterfaceMode == INTERFACE_MODE_CLIENT) return;
setTetheringInterfaceMode(INTERFACE_MODE_CLIENT);
}
private void setTetheringInterfaceMode(int mode) {
Log.d(TAG, "Setting tethering interface mode to " + mode);
mTetheringInterfaceMode = mode;
if (mTetheringInterface != null) {
removeInterface(mTetheringInterface);
addInterface(mTetheringInterface);
// when this broadcast is sent, any calls to notifyTetheredInterfaceAvailable or
// notifyTetheredInterfaceUnavailable have already happened
broadcastInterfaceStateChange(mTetheringInterface);
}
}
private int getInterfaceState(final String iface) {
if (mFactory.hasInterface(iface)) {
return mFactory.getInterfaceState(iface);
}
if (getInterfaceMode(iface) == INTERFACE_MODE_SERVER) {
// server mode interfaces are not tracked by the factory.
// TODO(b/234743836): interface state for server mode interfaces is not tracked
// properly; just return link up.
return EthernetManager.STATE_LINK_UP;
}
return EthernetManager.STATE_ABSENT;
}
private int getInterfaceRole(final String iface) {
if (mFactory.hasInterface(iface)) {
// only client mode interfaces are tracked by the factory.
return EthernetManager.ROLE_CLIENT;
}
if (getInterfaceMode(iface) == INTERFACE_MODE_SERVER) {
return EthernetManager.ROLE_SERVER;
}
return EthernetManager.ROLE_NONE;
}
private int getInterfaceMode(final String iface) {
if (iface.equals(mTetheringInterface)) {
return mTetheringInterfaceMode;
}
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(mTetheringInterface)) {
mTetheringInterface = null;
mTetheringInterfaceHwAddr = null;
}
broadcastInterfaceStateChange(iface);
}
private void addInterface(String iface) {
InterfaceConfigurationParcel config = null;
// Bring up the interface so we get link status indications.
try {
// Read the flags before attempting to bring up the interface. If the interface is
// already running an UP event is created after adding the interface.
config = NetdUtils.getInterfaceConfigParcel(mNetd, iface);
// Only bring the interface up when ethernet is enabled, otherwise set interface down.
setInterfaceUpState(iface, mIsEthernetEnabled);
} 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;
if (getInterfaceMode(iface) == INTERFACE_MODE_SERVER) {
maybeUpdateServerModeInterfaceState(iface, true);
mTetheringInterfaceHwAddr = hwAddress;
return;
}
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);
}
}
IpConfiguration ipConfiguration = getOrCreateIpConfiguration(iface);
Log.d(TAG, "Tracking interface in client mode: " + iface);
mFactory.addInterface(iface, hwAddress, ipConfiguration, nc);
// 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, INetd.IF_FLAG_RUNNING)) {
// no need to send an interface state change as this is not a true "state change". The
// callers (maybeTrackInterface() and setTetheringInterfaceMode()) already broadcast the
// state change.
mFactory.updateInterfaceLinkState(iface, true);
}
}
private void setInterfaceAdministrativeState(String iface, boolean up, EthernetCallback cb) {
if (!mIsEthernetEnabled) {
cb.onError("Cannot enable/disable interface when ethernet is disabled");
return;
}
if (getInterfaceState(iface) == EthernetManager.STATE_ABSENT) {
cb.onError("Failed to enable/disable absent interface: " + iface);
return;
}
if (getInterfaceRole(iface) == EthernetManager.ROLE_SERVER) {
// TODO: support setEthernetState for server mode interfaces.
cb.onError("Failed to enable/disable interface in server mode: " + iface);
return;
}
setInterfaceUpState(iface, up);
cb.onResult(iface);
}
private void updateInterfaceState(String iface, boolean up) {
final int mode = getInterfaceMode(iface);
if (mode == INTERFACE_MODE_SERVER) {
// TODO: support tracking link state for interfaces in server mode.
return;
}
// If updateInterfaceLinkState returns false, the interface is already in the correct state.
if (mFactory.updateInterfaceLinkState(iface, up)) {
broadcastInterfaceStateChange(iface);
}
}
private void maybeUpdateServerModeInterfaceState(String iface, boolean available) {
if (available == mTetheredInterfaceWasAvailable || !iface.equals(mTetheringInterface)) {
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 we don't already track this interface, and if this interface matches
// our regex, start tracking it.
if (mFactory.hasInterface(iface) || iface.equals(mTetheringInterface)) {
if (DBG) Log.w(TAG, "Ignoring already-tracked interface " + iface);
return;
}
if (DBG) Log.i(TAG, "maybeTrackInterface: " + iface);
// Do not use an interface for tethering if it has configured NetworkCapabilities.
if (mTetheringInterface == null && !mNetworkCapabilities.containsKey(iface)) {
mTetheringInterface = iface;
}
addInterface(iface);
broadcastInterfaceStateChange(iface);
}
private void trackAvailableInterfaces() {
final List ifaces = getEthernetInterfaceList();
for (String iface : ifaces) {
maybeTrackInterface(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 EthernetConfigParser config =
new EthernetConfigParser(configString, mDeps.isAtLeastB());
mNetworkCapabilities.put(config.mIface, config.mCaps);
if (null != config.mIpConfig) {
IpConfiguration ipConfig = parseStaticIpConfiguration(config.mIpConfig);
mIpConfigurations.put(config.mIface, ipConfig);
}
}
private static NetworkCapabilities createDefaultNetworkCapabilities(boolean isTestIface) {
final NetworkCapabilities.Builder builder =
new NetworkCapabilities.Builder(DEFAULT_CAPABILITIES);
if (isTestIface) {
builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST);
// TODO: do not remove INTERNET capability for test networks.
builder.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
}
return builder.build();
}
/**
* 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 boolean isValidEthernetInterface(String iface) {
return iface.matches(mIfaceMatch) || isValidTestInterface(iface);
}
/**
* 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);
}
}
@VisibleForTesting(visibility = PACKAGE)
protected void setEthernetEnabled(boolean enabled) {
mHandler.post(() -> {
if (mIsEthernetEnabled == enabled) return;
mIsEthernetEnabled = enabled;
for (String iface : getAllInterfaces()) {
setInterfaceUpState(iface, enabled);
}
broadcastEthernetStateChange(mIsEthernetEnabled);
});
}
private int isEthernetEnabledAsInt(boolean state) {
return state ? ETHERNET_STATE_ENABLED : ETHERNET_STATE_DISABLED;
}
private void unicastEthernetStateChange(@NonNull IEthernetServiceListener listener,
boolean enabled) {
ensureRunningOnEthernetServiceThread();
try {
listener.onEthernetStateChanged(isEthernetEnabledAsInt(enabled));
} catch (RemoteException e) {
// Do nothing here.
}
}
private void broadcastEthernetStateChange(boolean enabled) {
ensureRunningOnEthernetServiceThread();
final int n = mListeners.beginBroadcast();
for (int i = 0; i < n; i++) {
try {
mListeners.getBroadcastItem(i)
.onEthernetStateChanged(isEthernetEnabledAsInt(enabled));
} catch (RemoteException e) {
// Do nothing here.
}
}
mListeners.finishBroadcast();
}
private void setInterfaceUpState(@NonNull String interfaceName, boolean up) {
if (!NetlinkUtils.setInterfaceFlags(interfaceName, up ? IFF_UP : ~IFF_UP)) {
Log.e(TAG, "Failed to set interface " + interfaceName + (up ? " up" : " down"));
}
}
void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) {
postAndWaitForRunnable(() -> {
pw.println(getClass().getSimpleName());
pw.println("Ethernet State: "
+ (mIsEthernetEnabled ? "enabled" : "disabled"));
pw.println("Ethernet interface name filter: " + mIfaceMatch);
pw.println("Interface used for tethering: " + mTetheringInterface);
pw.println("Tethering interface mode: " + mTetheringInterfaceMode);
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 EthernetConfigParser {
final String mIface;
final NetworkCapabilities mCaps;
final String mIpConfig;
private static NetworkCapabilities parseCapabilities(@Nullable String capabilitiesString,
boolean isAtLeastB) {
final NetworkCapabilities.Builder builder =
NetworkCapabilities.Builder.withoutDefaultCapabilities();
builder.setLinkUpstreamBandwidthKbps(100 * 1000 /* 100 Mbps */);
builder.setLinkDownstreamBandwidthKbps(100 * 1000 /* 100 Mbps */);
// 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);
if (capabilitiesString == null) {
return builder.build();
}
if (isAtLeastB && capabilitiesString.equals("*")) {
// On Android B+, a "*" string defaults to the same set of default
// capabilities assigned to unconfigured interfaces.
// Note that the transport type is populated later with the result of
// parseTransportType().
return new NetworkCapabilities.Builder(DEFAULT_CAPABILITIES)
.removeTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.build();
}
for (String strNetworkCapability : capabilitiesString.split(",")) {
if (TextUtils.isEmpty(strNetworkCapability)) {
continue;
}
final Integer capability;
try {
builder.addCapability(Integer.valueOf(strNetworkCapability));
} catch (NumberFormatException e) {
Log.e(TAG, "Failed to parse capability: " + strNetworkCapability, e);
continue;
}
}
return builder.build();
}
private static int parseTransportType(@Nullable String transportString) {
if (TextUtils.isEmpty(transportString)) {
return TRANSPORT_ETHERNET;
}
final int parsedTransport;
try {
parsedTransport = Integer.valueOf(transportString);
} catch (NumberFormatException e) {
Log.e(TAG, "Failed to parse transport type", e);
return TRANSPORT_ETHERNET;
}
if (!NetworkCapabilities.isValidTransport(parsedTransport)) {
return TRANSPORT_ETHERNET;
}
switch (parsedTransport) {
case TRANSPORT_VPN:
case TRANSPORT_WIFI_AWARE:
case TRANSPORT_LOWPAN:
Log.e(TAG, "Unsupported transport type '" + parsedTransport + "'");
return TRANSPORT_ETHERNET;
default:
return parsedTransport;
}
}
EthernetConfigParser(String configString, boolean isAtLeastB) {
Objects.requireNonNull(configString, "EthernetConfigParser requires non-null config");
final String[] tokens = configString.split(";", /* limit of tokens */ 4);
mIface = tokens[0];
final NetworkCapabilities nc =
parseCapabilities(tokens.length > 1 ? tokens[1] : null, isAtLeastB);
final int transportType = parseTransportType(tokens.length > 3 ? tokens[3] : null);
nc.addTransportType(transportType);
mCaps = nc;
mIpConfig = tokens.length > 2 && !TextUtils.isEmpty(tokens[2]) ? tokens[2] : null;
}
}
}