diff options
Diffstat (limited to 'service-t/src')
8 files changed, 2247 insertions, 114 deletions
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java index fa86f3965a..e4efa9261b 100644 --- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java +++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java @@ -21,6 +21,7 @@ import android.util.Log; import com.android.modules.utils.build.SdkLevel; import com.android.networkstack.apishim.ConstantsShim; +import com.android.server.connectivity.ConnectivityNativeService; import com.android.server.ethernet.EthernetService; import com.android.server.ethernet.EthernetServiceImpl; import com.android.server.nearby.NearbyService; @@ -31,6 +32,7 @@ import com.android.server.nearby.NearbyService; */ public final class ConnectivityServiceInitializer extends SystemService { private static final String TAG = ConnectivityServiceInitializer.class.getSimpleName(); + private final ConnectivityNativeService mConnectivityNative; private final ConnectivityService mConnectivity; private final IpSecService mIpSecService; private final NsdService mNsdService; @@ -44,6 +46,7 @@ public final class ConnectivityServiceInitializer extends SystemService { mEthernetServiceImpl = createEthernetService(context); mConnectivity = new ConnectivityService(context); mIpSecService = createIpSecService(context); + mConnectivityNative = createConnectivityNativeService(context); mNsdService = createNsdService(context); mNearbyService = createNearbyService(context); } @@ -65,6 +68,12 @@ public final class ConnectivityServiceInitializer extends SystemService { publishBinderService(Context.IPSEC_SERVICE, mIpSecService, /* allowIsolated= */ false); } + if (mConnectivityNative != null) { + Log.i(TAG, "Registering " + ConnectivityNativeService.SERVICE_NAME); + publishBinderService(ConnectivityNativeService.SERVICE_NAME, mConnectivityNative, + /* allowIsolated= */ false); + } + if (mNsdService != null) { Log.i(TAG, "Registering " + Context.NSD_SERVICE); publishBinderService(Context.NSD_SERVICE, mNsdService, /* allowIsolated= */ false); @@ -98,6 +107,19 @@ public final class ConnectivityServiceInitializer extends SystemService { return new IpSecService(context); } + /** + * Return ConnectivityNativeService instance, or null if current SDK is lower than T. + */ + private ConnectivityNativeService createConnectivityNativeService(final Context context) { + if (!SdkLevel.isAtLeastT()) return null; + try { + return new ConnectivityNativeService(context); + } catch (UnsupportedOperationException e) { + Log.d(TAG, "Unable to get ConnectivityNative service", e); + return null; + } + } + /** Return NsdService instance or null if current SDK is lower than T */ private NsdService createNsdService(final Context context) { if (!SdkLevel.isAtLeastT()) return null; diff --git a/service-t/src/com/android/server/ethernet/EthernetConfigStore.java b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java new file mode 100644 index 0000000000..6b623f48ff --- /dev/null +++ b/service-t/src/com/android/server/ethernet/EthernetConfigStore.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2014 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.net.IpConfiguration; +import android.os.Environment; +import android.util.ArrayMap; + +import com.android.server.net.IpConfigStore; + + +/** + * This class provides an API to store and manage Ethernet network configuration. + */ +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 void read() { + synchronized (mSync) { + ArrayMap<String, IpConfiguration> configs = + IpConfigStore.readIpConfigurations(ipConfigFile); + + // 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; + } + } + + 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); + } + } + + @Nullable + public IpConfiguration getIpConfigurationForDefaultInterface() { + synchronized (mSync) { + return mIpConfigurationForDefaultInterface == null + ? null : new IpConfiguration(mIpConfigurationForDefaultInterface); + } + } +} diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java b/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java new file mode 100644 index 0000000000..57fbce7e86 --- /dev/null +++ b/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2021 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.content.Context; +import android.net.LinkProperties; +import android.net.NetworkAgent; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkProvider; +import android.net.NetworkScore; +import android.os.Looper; +import android.annotation.NonNull; +import android.annotation.Nullable; + +public class EthernetNetworkAgent extends NetworkAgent { + + private static final String TAG = "EthernetNetworkAgent"; + + public interface Callbacks { + void onNetworkUnwanted(); + } + + private final Callbacks mCallbacks; + + EthernetNetworkAgent( + @NonNull Context context, + @NonNull Looper looper, + @NonNull NetworkCapabilities nc, + @NonNull LinkProperties lp, + @NonNull NetworkAgentConfig config, + @Nullable NetworkProvider provider, + @NonNull Callbacks cb) { + super(context, looper, TAG, nc, lp, new NetworkScore.Builder().build(), config, provider); + mCallbacks = cb; + } + + @Override + public void onNetworkUnwanted() { + mCallbacks.onNetworkUnwanted(); + } + + // sendLinkProperties is final in NetworkAgent, so it cannot be mocked. + public void sendLinkPropertiesImpl(LinkProperties lp) { + sendLinkProperties(lp); + } + + public Callbacks getCallbacks() { + return mCallbacks; + } +} diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java new file mode 100644 index 0000000000..eb22f7875b --- /dev/null +++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java @@ -0,0 +1,784 @@ +/* + * Copyright (C) 2014 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.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.ConnectivityResources; +import android.net.EthernetManager; +import android.net.EthernetNetworkManagementException; +import android.net.EthernetNetworkSpecifier; +import android.net.INetworkInterfaceOutcomeReceiver; +import android.net.IpConfiguration; +import android.net.IpConfiguration.IpAssignment; +import android.net.IpConfiguration.ProxySettings; +import android.net.LinkProperties; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkFactory; +import android.net.NetworkProvider; +import android.net.NetworkRequest; +import android.net.NetworkSpecifier; +import android.net.ip.IIpClient; +import android.net.ip.IpClientCallbacks; +import android.net.ip.IpClientManager; +import android.net.ip.IpClientUtil; +import android.net.shared.ProvisioningConfiguration; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.AndroidRuntimeException; +import android.util.Log; +import android.util.SparseArray; + +import com.android.connectivity.resources.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.net.module.util.InterfaceParams; + +import java.io.FileDescriptor; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * {@link NetworkFactory} that represents Ethernet networks. + * + * 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. + */ +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 LEGACY_TCP_BUFFER_SIZES = + "524288,1048576,3145728,524288,1048576,2097152"; + + private final ConcurrentHashMap<String, NetworkInterfaceState> mTrackingInterfaces = + new ConcurrentHashMap<>(); + private final Handler mHandler; + private final Context mContext; + final Dependencies mDeps; + + public static class Dependencies { + public void makeIpClient(Context context, String iface, IpClientCallbacks callbacks) { + IpClientUtil.makeIpClient(context, iface, callbacks); + } + + public IpClientManager makeIpClientManager(@NonNull final IIpClient ipClient) { + return new IpClientManager(ipClient, TAG); + } + + public EthernetNetworkAgent makeEthernetNetworkAgent(Context context, Looper looper, + NetworkCapabilities nc, LinkProperties lp, NetworkAgentConfig config, + NetworkProvider provider, EthernetNetworkAgent.Callbacks cb) { + return new EthernetNetworkAgent(context, looper, nc, lp, config, provider, cb); + } + + public InterfaceParams getNetworkInterfaceByName(String name) { + return InterfaceParams.getByName(name); + } + + // TODO: remove legacy resource fallback after migrating its overlays. + private String getPlatformTcpBufferSizes(Context context) { + final Resources r = context.getResources(); + final int resId = r.getIdentifier("config_ethernet_tcp_buffers", "string", + context.getPackageName()); + return r.getString(resId); + } + + public String getTcpBufferSizesFromResource(Context context) { + final String tcpBufferSizes; + final String platformTcpBufferSizes = getPlatformTcpBufferSizes(context); + if (!LEGACY_TCP_BUFFER_SIZES.equals(platformTcpBufferSizes)) { + // Platform resource is not the historical default: use the overlay. + tcpBufferSizes = platformTcpBufferSizes; + } else { + final ConnectivityResources resources = new ConnectivityResources(context); + tcpBufferSizes = resources.get().getString(R.string.config_ethernet_tcp_buffers); + } + return tcpBufferSizes; + } + } + + public static class ConfigurationException extends AndroidRuntimeException { + public ConfigurationException(String msg) { + super(msg); + } + } + + public EthernetNetworkFactory(Handler handler, Context context) { + this(handler, context, new Dependencies()); + } + + @VisibleForTesting + EthernetNetworkFactory(Handler handler, Context context, Dependencies deps) { + super(handler.getLooper(), context, NETWORK_TYPE, createDefaultNetworkCapabilities()); + + mHandler = handler; + mContext = context; + mDeps = deps; + + setScoreFilter(NETWORK_SCORE); + } + + @Override + public boolean acceptRequest(NetworkRequest request) { + if (DBG) { + Log.d(TAG, "acceptRequest, request: " + request); + } + + return networkForRequest(request) != null; + } + + @Override + protected void needNetworkFor(NetworkRequest networkRequest) { + NetworkInterfaceState network = networkForRequest(networkRequest); + + if (network == null) { + Log.e(TAG, "needNetworkFor, failed to get a network for " + networkRequest); + return; + } + + if (++network.refCount == 1) { + network.start(); + } + } + + @Override + protected void releaseNetworkFor(NetworkRequest networkRequest) { + NetworkInterfaceState network = networkForRequest(networkRequest); + if (network == null) { + Log.e(TAG, "releaseNetworkFor, failed to get a network for " + networkRequest); + return; + } + + if (--network.refCount == 0) { + network.stop(); + } + } + + /** + * Returns an array of available interface names. The array is sorted: unrestricted interfaces + * goes first, then sorted by name. + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + protected 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); + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + protected void addInterface(@NonNull final String ifaceName, @NonNull final String hwAddress, + @NonNull final IpConfiguration ipConfig, + @NonNull final NetworkCapabilities capabilities) { + if (mTrackingInterfaces.containsKey(ifaceName)) { + Log.e(TAG, "Interface with name " + ifaceName + " already exists."); + return; + } + + final NetworkCapabilities nc = new NetworkCapabilities.Builder(capabilities) + .setNetworkSpecifier(new EthernetNetworkSpecifier(ifaceName)) + .build(); + + if (DBG) { + Log.d(TAG, "addInterface, iface: " + ifaceName + ", capabilities: " + nc); + } + + final NetworkInterfaceState iface = new NetworkInterfaceState( + ifaceName, hwAddress, mHandler, mContext, ipConfig, nc, this, mDeps); + mTrackingInterfaces.put(ifaceName, iface); + updateCapabilityFilter(); + } + + @VisibleForTesting + protected int getInterfaceState(@NonNull String iface) { + final NetworkInterfaceState interfaceState = mTrackingInterfaces.get(iface); + if (interfaceState == null) { + return EthernetManager.STATE_ABSENT; + } else if (!interfaceState.mLinkUp) { + return EthernetManager.STATE_LINK_DOWN; + } else { + return EthernetManager.STATE_LINK_UP; + } + } + + /** + * Update a network's configuration and restart it if necessary. + * + * @param ifaceName the interface name of the network to be updated. + * @param ipConfig the desired {@link IpConfiguration} for the given network or null. If + * {@code null} is passed, the existing IpConfiguration is not updated. + * @param capabilities the desired {@link NetworkCapabilities} for the given network. If + * {@code null} is passed, then the network's current + * {@link NetworkCapabilities} will be used in support of existing APIs as + * the public API does not allow this. + * @param listener an optional {@link INetworkInterfaceOutcomeReceiver} to notify callers of + * completion. + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + protected void updateInterface(@NonNull final String ifaceName, + @Nullable final IpConfiguration ipConfig, + @Nullable final NetworkCapabilities capabilities, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + if (!hasInterface(ifaceName)) { + maybeSendNetworkManagementCallbackForUntracked(ifaceName, listener); + return; + } + + final NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName); + iface.updateInterface(ipConfig, capabilities, listener); + mTrackingInterfaces.put(ifaceName, iface); + updateCapabilityFilter(); + } + + private static NetworkCapabilities mixInCapabilities(NetworkCapabilities nc, + NetworkCapabilities addedNc) { + final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder(nc); + for (int transport : addedNc.getTransportTypes()) builder.addTransportType(transport); + for (int capability : addedNc.getCapabilities()) builder.addCapability(capability); + return builder.build(); + } + + private void updateCapabilityFilter() { + NetworkCapabilities capabilitiesFilter = createDefaultNetworkCapabilities(); + for (NetworkInterfaceState iface: mTrackingInterfaces.values()) { + capabilitiesFilter = mixInCapabilities(capabilitiesFilter, iface.mCapabilities); + } + + if (DBG) Log.d(TAG, "updateCapabilityFilter: " + capabilitiesFilter); + setCapabilityFilter(capabilitiesFilter); + } + + private static NetworkCapabilities createDefaultNetworkCapabilities() { + return NetworkCapabilities.Builder + .withoutDefaultCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET).build(); + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + protected void removeInterface(String interfaceName) { + NetworkInterfaceState iface = mTrackingInterfaces.remove(interfaceName); + if (iface != null) { + iface.maybeSendNetworkManagementCallbackForAbort(); + iface.stop(); + } + + updateCapabilityFilter(); + } + + /** Returns true if state has been modified */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + protected boolean updateInterfaceLinkState(@NonNull final String ifaceName, final boolean up, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + if (!hasInterface(ifaceName)) { + maybeSendNetworkManagementCallbackForUntracked(ifaceName, listener); + return false; + } + + if (DBG) { + Log.d(TAG, "updateInterfaceLinkState, iface: " + ifaceName + ", up: " + up); + } + + NetworkInterfaceState iface = mTrackingInterfaces.get(ifaceName); + return iface.updateLinkState(up, listener); + } + + private void maybeSendNetworkManagementCallbackForUntracked( + String ifaceName, INetworkInterfaceOutcomeReceiver listener) { + maybeSendNetworkManagementCallback(listener, null, + new EthernetNetworkManagementException( + ifaceName + " can't be updated as it is not available.")); + } + + @VisibleForTesting + protected boolean hasInterface(String ifaceName) { + return mTrackingInterfaces.containsKey(ifaceName); + } + + private NetworkInterfaceState networkForRequest(NetworkRequest request) { + String requestedIface = null; + + NetworkSpecifier specifier = request.getNetworkSpecifier(); + if (specifier instanceof EthernetNetworkSpecifier) { + requestedIface = ((EthernetNetworkSpecifier) specifier) + .getInterfaceName(); + } + + NetworkInterfaceState network = null; + if (!TextUtils.isEmpty(requestedIface)) { + NetworkInterfaceState n = mTrackingInterfaces.get(requestedIface); + if (n != null && request.canBeSatisfiedBy(n.mCapabilities)) { + network = n; + } + } else { + for (NetworkInterfaceState n : mTrackingInterfaces.values()) { + if (request.canBeSatisfiedBy(n.mCapabilities) && n.mLinkUp) { + network = n; + break; + } + } + } + + if (DBG) { + Log.i(TAG, "networkForRequest, request: " + request + ", network: " + network); + } + + return network; + } + + private static void maybeSendNetworkManagementCallback( + @Nullable final INetworkInterfaceOutcomeReceiver listener, + @Nullable final String iface, + @Nullable final EthernetNetworkManagementException e) { + if (null == listener) { + return; + } + + try { + if (iface != null) { + listener.onResult(iface); + } else { + listener.onError(e); + } + } catch (RemoteException re) { + Log.e(TAG, "Can't send onComplete for network management callback", re); + } + } + + @VisibleForTesting + static class NetworkInterfaceState { + final String name; + + private final String mHwAddress; + private final Handler mHandler; + private final Context mContext; + private final NetworkFactory mNetworkFactory; + private final Dependencies mDeps; + + private static String sTcpBufferSizes = null; // Lazy initialized. + + private boolean mLinkUp; + private int mLegacyType; + private LinkProperties mLinkProperties = new LinkProperties(); + + private volatile @Nullable IpClientManager mIpClient; + private @NonNull NetworkCapabilities mCapabilities; + private @Nullable EthernetIpClientCallback mIpClientCallback; + private @Nullable EthernetNetworkAgent mNetworkAgent; + private @Nullable IpConfiguration mIpConfig; + + /** + * A map of TRANSPORT_* types to legacy transport types available for each type an ethernet + * interface could propagate. + * + * There are no legacy type equivalents to LOWPAN or WIFI_AWARE. These types are set to + * TYPE_NONE to match the behavior of their own network factories. + */ + private static final SparseArray<Integer> sTransports = new SparseArray(); + static { + sTransports.put(NetworkCapabilities.TRANSPORT_ETHERNET, + ConnectivityManager.TYPE_ETHERNET); + sTransports.put(NetworkCapabilities.TRANSPORT_BLUETOOTH, + ConnectivityManager.TYPE_BLUETOOTH); + sTransports.put(NetworkCapabilities.TRANSPORT_WIFI, ConnectivityManager.TYPE_WIFI); + sTransports.put(NetworkCapabilities.TRANSPORT_CELLULAR, + ConnectivityManager.TYPE_MOBILE); + sTransports.put(NetworkCapabilities.TRANSPORT_LOWPAN, ConnectivityManager.TYPE_NONE); + sTransports.put(NetworkCapabilities.TRANSPORT_WIFI_AWARE, + ConnectivityManager.TYPE_NONE); + } + + long refCount = 0; + + private class EthernetIpClientCallback extends IpClientCallbacks { + private final ConditionVariable mIpClientStartCv = new ConditionVariable(false); + private final ConditionVariable mIpClientShutdownCv = new ConditionVariable(false); + @Nullable INetworkInterfaceOutcomeReceiver mNetworkManagementListener; + + EthernetIpClientCallback(@Nullable final INetworkInterfaceOutcomeReceiver listener) { + mNetworkManagementListener = listener; + } + + @Override + public void onIpClientCreated(IIpClient ipClient) { + mIpClient = mDeps.makeIpClientManager(ipClient); + mIpClientStartCv.open(); + } + + private void awaitIpClientStart() { + mIpClientStartCv.block(); + } + + private void awaitIpClientShutdown() { + mIpClientShutdownCv.block(); + } + + // At the time IpClient is stopped, an IpClient event may have already been posted on + // the back of the handler and is awaiting execution. Once that event is executed, the + // associated callback object may not be valid anymore + // (NetworkInterfaceState#mIpClientCallback points to a different object / null). + private boolean isCurrentCallback() { + return this == mIpClientCallback; + } + + private void handleIpEvent(final @NonNull Runnable r) { + mHandler.post(() -> { + if (!isCurrentCallback()) { + Log.i(TAG, "Ignoring stale IpClientCallbacks " + this); + return; + } + r.run(); + }); + } + + @Override + public void onProvisioningSuccess(LinkProperties newLp) { + handleIpEvent(() -> onIpLayerStarted(newLp, mNetworkManagementListener)); + } + + @Override + public void onProvisioningFailure(LinkProperties newLp) { + // This cannot happen due to provisioning timeout, because our timeout is 0. It can + // happen due to errors while provisioning or on provisioning loss. + handleIpEvent(() -> onIpLayerStopped(mNetworkManagementListener)); + } + + @Override + public void onLinkPropertiesChange(LinkProperties newLp) { + handleIpEvent(() -> updateLinkProperties(newLp)); + } + + @Override + public void onReachabilityLost(String logMsg) { + handleIpEvent(() -> updateNeighborLostEvent(logMsg)); + } + + @Override + public void onQuit() { + mIpClient = null; + mIpClientShutdownCv.open(); + } + } + + NetworkInterfaceState(String ifaceName, String hwAddress, Handler handler, Context context, + @NonNull IpConfiguration ipConfig, @NonNull NetworkCapabilities capabilities, + NetworkFactory networkFactory, Dependencies deps) { + name = ifaceName; + mIpConfig = Objects.requireNonNull(ipConfig); + mCapabilities = Objects.requireNonNull(capabilities); + mLegacyType = getLegacyType(mCapabilities); + mHandler = handler; + mContext = context; + mNetworkFactory = networkFactory; + mDeps = deps; + mHwAddress = hwAddress; + } + + /** + * Determines the legacy transport type from a NetworkCapabilities transport type. Defaults + * to legacy TYPE_NONE if there is no known conversion + */ + private static int getLegacyType(int transport) { + return sTransports.get(transport, ConnectivityManager.TYPE_NONE); + } + + private static int getLegacyType(@NonNull final NetworkCapabilities capabilities) { + final int[] transportTypes = capabilities.getTransportTypes(); + if (transportTypes.length > 0) { + return getLegacyType(transportTypes[0]); + } + + // Should never happen as transport is always one of ETHERNET or a valid override + throw new ConfigurationException("Network Capabilities do not have an associated " + + "transport type."); + } + + private void setCapabilities(@NonNull final NetworkCapabilities capabilities) { + mCapabilities = new NetworkCapabilities(capabilities); + mLegacyType = getLegacyType(mCapabilities); + } + + void updateInterface(@Nullable final IpConfiguration ipConfig, + @Nullable final NetworkCapabilities capabilities, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + if (DBG) { + Log.d(TAG, "updateInterface, iface: " + name + + ", ipConfig: " + ipConfig + ", old ipConfig: " + mIpConfig + + ", capabilities: " + capabilities + ", old capabilities: " + mCapabilities + + ", listener: " + listener + ); + } + + if (null != ipConfig){ + mIpConfig = ipConfig; + } + if (null != capabilities) { + setCapabilities(capabilities); + } + // Send an abort callback if a request is filed before the previous one has completed. + maybeSendNetworkManagementCallbackForAbort(); + // TODO: Update this logic to only do a restart if required. Although a restart may + // be required due to the capabilities or ipConfiguration values, not all + // capabilities changes require a restart. + restart(listener); + } + + boolean isRestricted() { + return !mCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED); + } + + private void start() { + start(null); + } + + private void start(@Nullable final INetworkInterfaceOutcomeReceiver listener) { + if (mIpClient != null) { + if (DBG) Log.d(TAG, "IpClient already started"); + return; + } + if (DBG) { + Log.d(TAG, String.format("Starting Ethernet IpClient(%s)", name)); + } + + mIpClientCallback = new EthernetIpClientCallback(listener); + mDeps.makeIpClient(mContext, name, mIpClientCallback); + mIpClientCallback.awaitIpClientStart(); + + if (sTcpBufferSizes == null) { + sTcpBufferSizes = mDeps.getTcpBufferSizesFromResource(mContext); + } + provisionIpClient(mIpClient, mIpConfig, sTcpBufferSizes); + } + + void onIpLayerStarted(@NonNull final LinkProperties linkProperties, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + if (mNetworkAgent != null) { + Log.e(TAG, "Already have a NetworkAgent - aborting new request"); + stop(); + return; + } + mLinkProperties = linkProperties; + + // Create our NetworkAgent. + final NetworkAgentConfig config = new NetworkAgentConfig.Builder() + .setLegacyType(mLegacyType) + .setLegacyTypeName(NETWORK_TYPE) + .setLegacyExtraInfo(mHwAddress) + .build(); + mNetworkAgent = mDeps.makeEthernetNetworkAgent(mContext, mHandler.getLooper(), + mCapabilities, mLinkProperties, config, mNetworkFactory.getProvider(), + new EthernetNetworkAgent.Callbacks() { + @Override + public void onNetworkUnwanted() { + // if mNetworkAgent is null, we have already called stop. + if (mNetworkAgent == null) return; + + if (this == mNetworkAgent.getCallbacks()) { + stop(); + } else { + Log.d(TAG, "Ignoring unwanted as we have a more modern " + + "instance"); + } + } + }); + mNetworkAgent.register(); + mNetworkAgent.markConnected(); + realizeNetworkManagementCallback(name, null); + } + + void onIpLayerStopped(@Nullable final INetworkInterfaceOutcomeReceiver listener) { + // There is no point in continuing if the interface is gone as stop() will be triggered + // by removeInterface() when processed on the handler thread and start() won't + // work for a non-existent interface. + if (null == mDeps.getNetworkInterfaceByName(name)) { + if (DBG) Log.d(TAG, name + " is no longer available."); + // Send a callback in case a provisioning request was in progress. + maybeSendNetworkManagementCallbackForAbort(); + return; + } + restart(listener); + } + + private void maybeSendNetworkManagementCallbackForAbort() { + realizeNetworkManagementCallback(null, + new EthernetNetworkManagementException( + "The IP provisioning request has been aborted.")); + } + + // Must be called on the handler thread + private void realizeNetworkManagementCallback(@Nullable final String iface, + @Nullable final EthernetNetworkManagementException e) { + ensureRunningOnEthernetHandlerThread(); + if (null == mIpClientCallback) { + return; + } + + EthernetNetworkFactory.maybeSendNetworkManagementCallback( + mIpClientCallback.mNetworkManagementListener, iface, e); + // Only send a single callback per listener. + mIpClientCallback.mNetworkManagementListener = null; + } + + private void ensureRunningOnEthernetHandlerThread() { + if (mHandler.getLooper().getThread() != Thread.currentThread()) { + throw new IllegalStateException( + "Not running on the Ethernet thread: " + + Thread.currentThread().getName()); + } + } + + void updateLinkProperties(LinkProperties linkProperties) { + mLinkProperties = linkProperties; + if (mNetworkAgent != null) { + mNetworkAgent.sendLinkPropertiesImpl(linkProperties); + } + } + + void updateNeighborLostEvent(String logMsg) { + Log.i(TAG, "updateNeighborLostEvent " + logMsg); + // Reachability lost will be seen only if the gateway is not reachable. + // Since ethernet FW doesn't have the mechanism to scan for new networks + // like WiFi, simply restart. + // If there is a better network, that will become default and apps + // will be able to use internet. If ethernet gets connected again, + // and has backhaul connectivity, it will become default. + restart(); + } + + /** Returns true if state has been modified */ + boolean updateLinkState(final boolean up, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + if (mLinkUp == up) { + EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, null, + new EthernetNetworkManagementException( + "No changes with requested link state " + up + " for " + name)); + return false; + } + mLinkUp = up; + + if (!up) { // was up, goes down + // Send an abort on a provisioning request callback if necessary before stopping. + maybeSendNetworkManagementCallbackForAbort(); + stop(); + // If only setting the interface down, send a callback to signal completion. + EthernetNetworkFactory.maybeSendNetworkManagementCallback(listener, name, null); + } else { // was down, goes up + stop(); + start(listener); + } + + return true; + } + + void stop() { + // Invalidate all previous start requests + if (mIpClient != null) { + mIpClient.shutdown(); + mIpClientCallback.awaitIpClientShutdown(); + mIpClient = null; + } + mIpClientCallback = null; + + if (mNetworkAgent != null) { + mNetworkAgent.unregister(); + mNetworkAgent = null; + } + mLinkProperties.clear(); + } + + private static void provisionIpClient(@NonNull final IpClientManager ipClient, + @NonNull final IpConfiguration config, @NonNull final String tcpBufferSizes) { + if (config.getProxySettings() == ProxySettings.STATIC || + config.getProxySettings() == ProxySettings.PAC) { + ipClient.setHttpProxy(config.getHttpProxy()); + } + + if (!TextUtils.isEmpty(tcpBufferSizes)) { + ipClient.setTcpBufferSizes(tcpBufferSizes); + } + + ipClient.startProvisioning(createProvisioningConfiguration(config)); + } + + private static ProvisioningConfiguration createProvisioningConfiguration( + @NonNull final IpConfiguration config) { + if (config.getIpAssignment() == IpAssignment.STATIC) { + return new ProvisioningConfiguration.Builder() + .withStaticConfiguration(config.getStaticIpConfiguration()) + .build(); + } + return new ProvisioningConfiguration.Builder() + .withProvisioningTimeoutMs(0) + .build(); + } + + void restart() { + restart(null); + } + + void restart(@Nullable final INetworkInterfaceOutcomeReceiver listener) { + if (DBG) Log.d(TAG, "reconnecting Ethernet"); + stop(); + start(listener); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{ " + + "refCount: " + refCount + ", " + + "iface: " + name + ", " + + "up: " + mLinkUp + ", " + + "hwAddress: " + mHwAddress + ", " + + "networkCapabilities: " + mCapabilities + ", " + + "networkAgent: " + mNetworkAgent + ", " + + "ipClient: " + mIpClient + "," + + "linkProperties: " + mLinkProperties + + "}"; + } + } + + void dump(FileDescriptor fd, IndentingPrintWriter pw, String[] args) { + 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(); + if (null == ifaceState.mIpClient) { + pw.println("IpClient is null"); + } + pw.decreaseIndent(); + } + pw.decreaseIndent(); + } +} diff --git a/service-t/src/com/android/server/ethernet/EthernetService.java b/service-t/src/com/android/server/ethernet/EthernetService.java new file mode 100644 index 0000000000..d405fd59fb --- /dev/null +++ b/service-t/src/com/android/server/ethernet/EthernetService.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2014 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.content.Context; +import android.net.INetd; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; + +import java.util.Objects; + +// TODO: consider renaming EthernetServiceImpl to EthernetService and deleting this file. +public final class EthernetService { + private static final String TAG = "EthernetService"; + private static final String THREAD_NAME = "EthernetServiceThread"; + + private static INetd getNetd(Context context) { + final INetd netd = + INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)); + Objects.requireNonNull(netd, "could not get netd instance"); + return netd; + } + + public static EthernetServiceImpl create(Context context) { + final HandlerThread handlerThread = new HandlerThread(THREAD_NAME); + handlerThread.start(); + final Handler handler = new Handler(handlerThread.getLooper()); + final EthernetNetworkFactory factory = new EthernetNetworkFactory(handler, context); + return new EthernetServiceImpl(context, handler, + new EthernetTracker(context, handler, factory, getNetd(context))); + } +} diff --git a/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java new file mode 100644 index 0000000000..5e830ad83a --- /dev/null +++ b/service-t/src/com/android/server/ethernet/EthernetServiceImpl.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2014 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.NetworkCapabilities.TRANSPORT_TEST; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.IEthernetManager; +import android.net.IEthernetServiceListener; +import android.net.INetworkInterfaceOutcomeReceiver; +import android.net.ITetheredInterfaceCallback; +import android.net.EthernetNetworkUpdateRequest; +import android.net.IpConfiguration; +import android.net.NetworkCapabilities; +import android.os.Binder; +import android.os.Handler; +import android.os.RemoteException; +import android.util.Log; +import android.util.PrintWriterPrinter; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; +import com.android.net.module.util.PermissionUtils; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * EthernetServiceImpl handles remote Ethernet operation requests by implementing + * the IEthernetManager interface. + */ +public class EthernetServiceImpl extends IEthernetManager.Stub { + private static final String TAG = "EthernetServiceImpl"; + + @VisibleForTesting + final AtomicBoolean mStarted = new AtomicBoolean(false); + private final Context mContext; + private final Handler mHandler; + private final EthernetTracker mTracker; + + EthernetServiceImpl(@NonNull final Context context, @NonNull final Handler handler, + @NonNull final EthernetTracker tracker) { + mContext = context; + mHandler = handler; + mTracker = tracker; + } + + private void enforceAutomotiveDevice(final @NonNull String methodName) { + PermissionUtils.enforceSystemFeature(mContext, PackageManager.FEATURE_AUTOMOTIVE, + methodName + " is only available on automotive devices."); + } + + private boolean checkUseRestrictedNetworksPermission() { + return PermissionUtils.checkAnyPermissionOf(mContext, + android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS); + } + + public void start() { + Log.i(TAG, "Starting Ethernet service"); + mTracker.start(); + mStarted.set(true); + } + + private void throwIfEthernetNotStarted() { + if (!mStarted.get()) { + throw new IllegalStateException("System isn't ready to change ethernet configurations"); + } + } + + @Override + public String[] getAvailableInterfaces() throws RemoteException { + PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG); + return mTracker.getInterfaces(checkUseRestrictedNetworksPermission()); + } + + /** + * Get Ethernet configuration + * @return the Ethernet Configuration, contained in {@link IpConfiguration}. + */ + @Override + public IpConfiguration getConfiguration(String iface) { + PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG); + if (mTracker.isRestrictedInterface(iface)) { + PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG); + } + + return new IpConfiguration(mTracker.getIpConfiguration(iface)); + } + + /** + * Set Ethernet configuration + */ + @Override + public void setConfiguration(String iface, IpConfiguration config) { + throwIfEthernetNotStarted(); + + PermissionUtils.enforceNetworkStackPermission(mContext); + if (mTracker.isRestrictedInterface(iface)) { + PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG); + } + + // 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 given interface is available. + */ + @Override + public boolean isAvailable(String iface) { + PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG); + if (mTracker.isRestrictedInterface(iface)) { + PermissionUtils.enforceRestrictedNetworkPermission(mContext, TAG); + } + + return mTracker.isTrackingInterface(iface); + } + + /** + * Adds a listener. + * @param listener A {@link IEthernetServiceListener} to add. + */ + public void addListener(IEthernetServiceListener listener) throws RemoteException { + Objects.requireNonNull(listener, "listener must not be null"); + PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG); + mTracker.addListener(listener, checkUseRestrictedNetworksPermission()); + } + + /** + * Removes a listener. + * @param listener A {@link IEthernetServiceListener} to remove. + */ + public void removeListener(IEthernetServiceListener listener) { + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG); + mTracker.removeListener(listener); + } + + @Override + public void setIncludeTestInterfaces(boolean include) { + PermissionUtils.enforceNetworkStackPermissionOr(mContext, + android.Manifest.permission.NETWORK_SETTINGS); + mTracker.setIncludeTestInterfaces(include); + } + + @Override + public void requestTetheredInterface(ITetheredInterfaceCallback callback) { + Objects.requireNonNull(callback, "callback must not be null"); + PermissionUtils.enforceNetworkStackPermissionOr(mContext, + android.Manifest.permission.NETWORK_SETTINGS); + mTracker.requestTetheredInterface(callback); + } + + @Override + public void releaseTetheredInterface(ITetheredInterfaceCallback callback) { + Objects.requireNonNull(callback, "callback must not be null"); + PermissionUtils.enforceNetworkStackPermissionOr(mContext, + android.Manifest.permission.NETWORK_SETTINGS); + mTracker.releaseTetheredInterface(callback); + } + + @Override + protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump EthernetService from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + pw.println("Current Ethernet state: "); + pw.increaseIndent(); + mTracker.dump(fd, pw, args); + pw.decreaseIndent(); + + pw.println("Handler:"); + pw.increaseIndent(); + mHandler.dump(new PrintWriterPrinter(pw), "EthernetServiceImpl"); + pw.decreaseIndent(); + } + + private void enforceNetworkManagementPermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.MANAGE_ETHERNET_NETWORKS, + "EthernetServiceImpl"); + } + + private void enforceManageTestNetworksPermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.MANAGE_TEST_NETWORKS, + "EthernetServiceImpl"); + } + + private void maybeValidateTestCapabilities(final String iface, + @Nullable final NetworkCapabilities nc) { + if (!mTracker.isValidTestInterface(iface)) { + return; + } + // For test interfaces, only null or capabilities that include TRANSPORT_TEST are + // allowed. + if (nc != null && !nc.hasTransport(TRANSPORT_TEST)) { + throw new IllegalArgumentException( + "Updates to test interfaces must have NetworkCapabilities.TRANSPORT_TEST."); + } + } + + private void enforceAdminPermission(final String iface, boolean enforceAutomotive, + final String logMessage) { + if (mTracker.isValidTestInterface(iface)) { + enforceManageTestNetworksPermission(); + } else { + enforceNetworkManagementPermission(); + if (enforceAutomotive) { + enforceAutomotiveDevice(logMessage); + } + } + } + + @Override + public void updateConfiguration(@NonNull final String iface, + @NonNull final EthernetNetworkUpdateRequest request, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + Objects.requireNonNull(iface); + Objects.requireNonNull(request); + throwIfEthernetNotStarted(); + + // TODO: validate that iface is listed in overlay config_ethernet_interfaces + // only automotive devices are allowed to set the NetworkCapabilities using this API + enforceAdminPermission(iface, request.getNetworkCapabilities() != null, + "updateConfiguration() with non-null capabilities"); + maybeValidateTestCapabilities(iface, request.getNetworkCapabilities()); + + mTracker.updateConfiguration( + iface, request.getIpConfiguration(), request.getNetworkCapabilities(), listener); + } + + @Override + public void connectNetwork(@NonNull final String iface, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + Log.i(TAG, "connectNetwork called with: iface=" + iface + ", listener=" + listener); + Objects.requireNonNull(iface); + throwIfEthernetNotStarted(); + + enforceAdminPermission(iface, true, "connectNetwork()"); + + mTracker.connectNetwork(iface, listener); + } + + @Override + public void disconnectNetwork(@NonNull final String iface, + @Nullable final INetworkInterfaceOutcomeReceiver listener) { + Log.i(TAG, "disconnectNetwork called with: iface=" + iface + ", listener=" + listener); + Objects.requireNonNull(iface); + throwIfEthernetNotStarted(); + + enforceAdminPermission(iface, true, "connectNetwork()"); + + mTracker.disconnectNetwork(iface, listener); + } + + @Override + public void setEthernetEnabled(boolean enabled) { + PermissionUtils.enforceNetworkStackPermissionOr(mContext, + android.Manifest.permission.NETWORK_SETTINGS); + + mTracker.setEthernetEnabled(enabled); + } + + @Override + public List<String> getInterfaceList() { + PermissionUtils.enforceAccessNetworkStatePermission(mContext, TAG); + return mTracker.getInterfaceList(); + } +} 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 0000000000..c291b3ff66 --- /dev/null +++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java @@ -0,0 +1,942 @@ +/* + * 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.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.INetworkInterfaceOutcomeReceiver; +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.List; +import java.util.Objects; +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. + */ +@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 volatile 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<String, NetworkCapabilities> mNetworkCapabilities = + new ConcurrentHashMap<>(); + private final ConcurrentHashMap<String, IpConfiguration> 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<IEthernetServiceListener> 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 int mEthernetState = ETHERNET_STATE_ENABLED; + + private class TetheredInterfaceRequestList extends + RemoteCallbackList<ITetheredInterfaceCallback> { + @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<String, IpConfiguration> 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 INetworkInterfaceOutcomeReceiver 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 INetworkInterfaceOutcomeReceiver listener) { + mHandler.post(() -> updateInterfaceState(iface, true, listener)); + } + + @VisibleForTesting(visibility = PACKAGE) + protected void disconnectNetwork(@NonNull final String iface, + @Nullable final INetworkInterfaceOutcomeReceiver 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); + } + + List<String> getInterfaceList() { + final List<String> interfaceList = new ArrayList<String>(); + final String[] ifaces; + try { + ifaces = mNetd.interfaceGetList(); + } catch (RemoteException e) { + Log.e(TAG, "Could not get list of interfaces " + e); + return interfaceList; + } + final String ifaceMatch = mIfaceMatch; + for (String iface : ifaces) { + if (iface.matches(ifaceMatch)) interfaceList.add(iface); + } + 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 : getInterfaces(canUseRestrictedNetworks)) { + unicastInterfaceStateChange(listener, iface); + } + + unicastEthernetStateChange(listener, mEthernetState); + }); + } + + 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 INetworkInterfaceOutcomeReceiver 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 + * <interface name|mac address>;[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=<ip-address/mask> gateway=<ip-address> dns=<comma-sep-ip-addresses> + * domains=<comma-sep-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<InetAddress> 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); + } + } + + @VisibleForTesting(visibility = PACKAGE) + protected void setEthernetEnabled(boolean enabled) { + mHandler.post(() -> { + int newState = enabled ? ETHERNET_STATE_ENABLED : ETHERNET_STATE_DISABLED; + if (mEthernetState == newState) return; + + mEthernetState = newState; + + if (enabled) { + trackAvailableInterfaces(); + } else { + // TODO: maybe also disable server mode interface as well. + untrackFactoryInterfaces(); + } + broadcastEthernetStateChange(mEthernetState); + }); + } + + private void untrackFactoryInterfaces() { + for (String iface : mFactory.getAvailableInterfaces(true /* includeRestricted */)) { + stopTrackingInterface(iface); + } + } + + private void unicastEthernetStateChange(@NonNull IEthernetServiceListener listener, + int state) { + ensureRunningOnEthernetServiceThread(); + try { + listener.onEthernetStateChanged(state); + } catch (RemoteException e) { + // Do nothing here. + } + } + + private void broadcastEthernetStateChange(int state) { + ensureRunningOnEthernetServiceThread(); + final int n = mListeners.beginBroadcast(); + for (int i = 0; i < n; i++) { + try { + mListeners.getBroadcastItem(i).onEthernetStateChanged(state); + } catch (RemoteException e) { + // Do nothing here. + } + } + mListeners.finishBroadcast(); + } + + 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; + } + } +} diff --git a/service-t/src/com/android/server/net/DelayedDiskWrite.java b/service-t/src/com/android/server/net/DelayedDiskWrite.java deleted file mode 100644 index 35dc455725..0000000000 --- a/service-t/src/com/android/server/net/DelayedDiskWrite.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2014 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.net; - -import android.os.Handler; -import android.os.HandlerThread; -import android.text.TextUtils; -import android.util.Log; - -import java.io.BufferedOutputStream; -import java.io.DataOutputStream; -import java.io.FileOutputStream; -import java.io.IOException; - -/** - * This class provides APIs to do a delayed data write to a given {@link OutputStream}. - */ -public class DelayedDiskWrite { - private static final String TAG = "DelayedDiskWrite"; - - private HandlerThread mDiskWriteHandlerThread; - private Handler mDiskWriteHandler; - /* Tracks multiple writes on the same thread */ - private int mWriteSequence = 0; - - /** - * Used to do a delayed data write to a given {@link OutputStream}. - */ - public interface Writer { - /** - * write data to a given {@link OutputStream}. - */ - void onWriteCalled(DataOutputStream out) throws IOException; - } - - /** - * Do a delayed data write to a given output stream opened from filePath. - */ - public void write(final String filePath, final Writer w) { - write(filePath, w, true); - } - - /** - * Do a delayed data write to a given output stream opened from filePath. - */ - public void write(final String filePath, final Writer w, final boolean open) { - if (TextUtils.isEmpty(filePath)) { - throw new IllegalArgumentException("empty file path"); - } - - /* Do a delayed write to disk on a separate handler thread */ - synchronized (this) { - if (++mWriteSequence == 1) { - mDiskWriteHandlerThread = new HandlerThread("DelayedDiskWriteThread"); - mDiskWriteHandlerThread.start(); - mDiskWriteHandler = new Handler(mDiskWriteHandlerThread.getLooper()); - } - } - - mDiskWriteHandler.post(new Runnable() { - @Override - public void run() { - doWrite(filePath, w, open); - } - }); - } - - private void doWrite(String filePath, Writer w, boolean open) { - DataOutputStream out = null; - try { - if (open) { - out = new DataOutputStream(new BufferedOutputStream( - new FileOutputStream(filePath))); - } - w.onWriteCalled(out); - } catch (IOException e) { - loge("Error writing data file " + filePath); - } finally { - if (out != null) { - try { - out.close(); - } catch (Exception e) { } - } - - // Quit if no more writes sent - synchronized (this) { - if (--mWriteSequence == 0) { - mDiskWriteHandler.getLooper().quit(); - mDiskWriteHandler = null; - mDiskWriteHandlerThread = null; - } - } - } - } - - private void loge(String s) { - Log.e(TAG, s); - } -} - |
