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 mIpConfigurations; + private IpConfiguration mIpConfigurationForDefaultInterface; + private final Object mSync = new Object(); + + public EthernetConfigStore() { + mIpConfigurations = new ArrayMap<>(0); + } + + public void read() { + synchronized (mSync) { + ArrayMap 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 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..d910629983 --- /dev/null +++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java @@ -0,0 +1,785 @@ +/* + * 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.EthernetNetworkSpecifier; +import android.net.EthernetNetworkManagementException; +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.Network; +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 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 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 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. + * + *

Interfaces may have different {@link android.net.NetworkCapabilities}. This mapping is defined + * in {@code config_ethernet_interfaces}. Notably, some interfaces could be marked as restricted by + * not specifying {@link android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED} flag. + * Interfaces could have associated {@link android.net.IpConfiguration}. + * Ethernet Interfaces may be present at boot time or appear after boot (e.g., for Ethernet adapters + * connected over USB). This class supports multiple interfaces. When an interface appears on the + * system (or is present at boot time) this class will start tracking it and bring it up. Only + * interfaces whose names match the {@code config_ethernet_iface_regex} regular expression are + * tracked. + * + *

All public or package private methods must be thread-safe unless stated otherwise. + */ +@VisibleForTesting(visibility = PACKAGE) +public class EthernetTracker { + private static final int INTERFACE_MODE_CLIENT = 1; + private static final int INTERFACE_MODE_SERVER = 2; + + private static final String TAG = EthernetTracker.class.getSimpleName(); + private static final boolean DBG = EthernetNetworkFactory.DBG; + + private static final String TEST_IFACE_REGEXP = TEST_TAP_PREFIX + "\\d+"; + private static final String LEGACY_IFACE_REGEXP = "eth\\d"; + + /** + * Interface names we track. This is a product-dependent regular expression, plus, + * if setIncludeTestInterfaces is true, any test interfaces. + */ + private 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 mNetworkCapabilities = + new ConcurrentHashMap<>(); + private final ConcurrentHashMap mIpConfigurations = + new ConcurrentHashMap<>(); + + private final Context mContext; + private final INetd mNetd; + private final Handler mHandler; + private final EthernetNetworkFactory mFactory; + private final EthernetConfigStore mConfigStore; + private final Dependencies mDeps; + + private final RemoteCallbackList mListeners = + new RemoteCallbackList<>(); + private final TetheredInterfaceRequestList mTetheredInterfaceRequests = + new TetheredInterfaceRequestList(); + + // Used only on the handler thread + private String mDefaultInterface; + private int mDefaultInterfaceMode = INTERFACE_MODE_CLIENT; + // Tracks whether clients were notified that the tethered interface is available + private boolean mTetheredInterfaceWasAvailable = false; + private volatile IpConfiguration mIpConfigForDefaultInterface; + + private int mEthernetState = ETHERNET_STATE_ENABLED; + + private class TetheredInterfaceRequestList extends + RemoteCallbackList { + @Override + public void onCallbackDied(ITetheredInterfaceCallback cb, Object cookie) { + mHandler.post(EthernetTracker.this::maybeUntetherDefaultInterface); + } + } + + public static class Dependencies { + // TODO: remove legacy resource fallback after migrating its overlays. + private String getPlatformRegexResource(Context context) { + final Resources r = context.getResources(); + final int resId = + r.getIdentifier("config_ethernet_iface_regex", "string", context.getPackageName()); + return r.getString(resId); + } + + // TODO: remove legacy resource fallback after migrating its overlays. + private String[] getPlatformInterfaceConfigs(Context context) { + final Resources r = context.getResources(); + final int resId = r.getIdentifier("config_ethernet_interfaces", "array", + context.getPackageName()); + return r.getStringArray(resId); + } + + public String getInterfaceRegexFromResource(Context context) { + final String platformRegex = getPlatformRegexResource(context); + final String match; + if (!LEGACY_IFACE_REGEXP.equals(platformRegex)) { + // Platform resource is not the historical default: use the overlay + match = platformRegex; + } else { + final ConnectivityResources resources = new ConnectivityResources(context); + match = resources.get().getString( + com.android.connectivity.resources.R.string.config_ethernet_iface_regex); + } + return match; + } + + public String[] getInterfaceConfigFromResource(Context context) { + final String[] platformInterfaceConfigs = getPlatformInterfaceConfigs(context); + final String[] interfaceConfigs; + if (platformInterfaceConfigs.length != 0) { + // Platform resource is not the historical default: use the overlay + interfaceConfigs = platformInterfaceConfigs; + } else { + final ConnectivityResources resources = new ConnectivityResources(context); + interfaceConfigs = resources.get().getStringArray( + com.android.connectivity.resources.R.array.config_ethernet_interfaces); + } + return interfaceConfigs; + } + } + + EthernetTracker(@NonNull final Context context, @NonNull final Handler handler, + @NonNull final EthernetNetworkFactory factory, @NonNull final INetd netd) { + this(context, handler, factory, netd, new Dependencies()); + } + + @VisibleForTesting + EthernetTracker(@NonNull final Context context, @NonNull final Handler handler, + @NonNull final EthernetNetworkFactory factory, @NonNull final INetd netd, + @NonNull final Dependencies deps) { + mContext = context; + mHandler = handler; + mFactory = factory; + mNetd = netd; + mDeps = deps; + + // Interface match regex. + updateIfaceMatchRegexp(); + + // Read default Ethernet interface configuration from resources + final String[] interfaceConfigs = mDeps.getInterfaceConfigFromResource(context); + for (String strConfig : interfaceConfigs) { + parseEthernetConfig(strConfig); + } + + mConfigStore = new EthernetConfigStore(); + } + + void start() { + mFactory.register(); + mConfigStore.read(); + + // Default interface is just the first one we want to track. + mIpConfigForDefaultInterface = mConfigStore.getIpConfigurationForDefaultInterface(); + final ArrayMap configs = mConfigStore.getIpConfigurations(); + for (int i = 0; i < configs.size(); i++) { + mIpConfigurations.put(configs.keyAt(i), configs.valueAt(i)); + } + + try { + PermissionUtils.enforceNetworkStackPermission(mContext); + mNetd.registerUnsolicitedEventListener(new InterfaceObserver()); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Could not register InterfaceObserver " + e); + } + + mHandler.post(this::trackAvailableInterfaces); + } + + void updateIpConfiguration(String iface, IpConfiguration ipConfiguration) { + if (DBG) { + Log.i(TAG, "updateIpConfiguration, iface: " + iface + ", cfg: " + ipConfiguration); + } + writeIpConfiguration(iface, ipConfiguration); + mHandler.post(() -> { + mFactory.updateInterface(iface, ipConfiguration, null, null); + broadcastInterfaceStateChange(iface); + }); + } + + private void writeIpConfiguration(@NonNull final String iface, + @NonNull final IpConfiguration ipConfig) { + mConfigStore.write(iface, ipConfig); + mIpConfigurations.put(iface, ipConfig); + } + + private IpConfiguration getIpConfigurationForCallback(String iface, int state) { + return (state == EthernetManager.STATE_ABSENT) ? null : getOrCreateIpConfiguration(iface); + } + + private void ensureRunningOnEthernetServiceThread() { + if (mHandler.getLooper().getThread() != Thread.currentThread()) { + throw new IllegalStateException( + "Not running on EthernetService thread: " + + Thread.currentThread().getName()); + } + } + + /** + * Broadcast the link state or IpConfiguration change of existing Ethernet interfaces to all + * listeners. + */ + protected void broadcastInterfaceStateChange(@NonNull String iface) { + ensureRunningOnEthernetServiceThread(); + final int state = mFactory.getInterfaceState(iface); + final int role = getInterfaceRole(iface); + final IpConfiguration config = getIpConfigurationForCallback(iface, state); + final int n = mListeners.beginBroadcast(); + for (int i = 0; i < n; i++) { + try { + mListeners.getBroadcastItem(i).onInterfaceStateChanged(iface, state, role, config); + } catch (RemoteException e) { + // Do nothing here. + } + } + mListeners.finishBroadcast(); + } + + /** + * Unicast the interface state or IpConfiguration change of existing Ethernet interfaces to a + * specific listener. + */ + protected void unicastInterfaceStateChange(@NonNull IEthernetServiceListener listener, + @NonNull String iface) { + ensureRunningOnEthernetServiceThread(); + final int state = mFactory.getInterfaceState(iface); + final int role = getInterfaceRole(iface); + final IpConfiguration config = getIpConfigurationForCallback(iface, state); + try { + listener.onInterfaceStateChanged(iface, state, role, config); + } catch (RemoteException e) { + // Do nothing here. + } + } + + @VisibleForTesting(visibility = PACKAGE) + protected void updateConfiguration(@NonNull final String iface, + @Nullable final IpConfiguration ipConfig, + @Nullable final NetworkCapabilities capabilities, + @Nullable final 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 getInterfaceList() { + final List interfaceList = new ArrayList(); + 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 + * ;[Network Capabilities];[IP config];[Override Transport]} + */ + private void parseEthernetConfig(String configString) { + final EthernetTrackerConfig config = createEthernetTrackerConfig(configString); + NetworkCapabilities nc = createNetworkCapabilities( + !TextUtils.isEmpty(config.mCapabilities) /* clear default capabilities */, + config.mCapabilities, config.mTransport).build(); + mNetworkCapabilities.put(config.mIface, nc); + + if (null != config.mIpConfig) { + IpConfiguration ipConfig = parseStaticIpConfiguration(config.mIpConfig); + mIpConfigurations.put(config.mIface, ipConfig); + } + } + + @VisibleForTesting + static EthernetTrackerConfig createEthernetTrackerConfig(@NonNull final String configString) { + Objects.requireNonNull(configString, "EthernetTrackerConfig requires non-null config"); + return new EthernetTrackerConfig(configString.split(";", /* limit of tokens */ 4)); + } + + private static NetworkCapabilities createDefaultNetworkCapabilities(boolean isTestIface) { + NetworkCapabilities.Builder builder = createNetworkCapabilities( + false /* clear default capabilities */, null, null) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED); + + if (isTestIface) { + builder.addTransportType(NetworkCapabilities.TRANSPORT_TEST); + } else { + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + } + + return builder.build(); + } + + /** + * Parses a static list of network capabilities + * + * @param clearDefaultCapabilities Indicates whether or not to clear any default capabilities + * @param commaSeparatedCapabilities A comma separated string list of integer encoded + * NetworkCapability.NET_CAPABILITY_* values + * @param overrideTransport A string representing a single integer encoded override transport + * type. Must be one of the NetworkCapability.TRANSPORT_* + * values. TRANSPORT_VPN is not supported. Errors with input + * will cause the override to be ignored. + */ + @VisibleForTesting + static NetworkCapabilities.Builder createNetworkCapabilities( + boolean clearDefaultCapabilities, @Nullable String commaSeparatedCapabilities, + @Nullable String overrideTransport) { + + final NetworkCapabilities.Builder builder = clearDefaultCapabilities + ? NetworkCapabilities.Builder.withoutDefaultCapabilities() + : new NetworkCapabilities.Builder(); + + // Determine the transport type. If someone has tried to define an override transport then + // attempt to add it. Since we can only have one override, all errors with it will + // gracefully default back to TRANSPORT_ETHERNET and warn the user. VPN is not allowed as an + // override type. Wifi Aware and LoWPAN are currently unsupported as well. + int transport = NetworkCapabilities.TRANSPORT_ETHERNET; + if (!TextUtils.isEmpty(overrideTransport)) { + try { + int parsedTransport = Integer.valueOf(overrideTransport); + if (parsedTransport == NetworkCapabilities.TRANSPORT_VPN + || parsedTransport == NetworkCapabilities.TRANSPORT_WIFI_AWARE + || parsedTransport == NetworkCapabilities.TRANSPORT_LOWPAN) { + Log.e(TAG, "Override transport '" + parsedTransport + "' is not supported. " + + "Defaulting to TRANSPORT_ETHERNET"); + } else { + transport = parsedTransport; + } + } catch (NumberFormatException nfe) { + Log.e(TAG, "Override transport type '" + overrideTransport + "' " + + "could not be parsed. Defaulting to TRANSPORT_ETHERNET"); + } + } + + // Apply the transport. If the user supplied a valid number that is not a valid transport + // then adding will throw an exception. Default back to TRANSPORT_ETHERNET if that happens + try { + builder.addTransportType(transport); + } catch (IllegalArgumentException iae) { + Log.e(TAG, transport + " is not a valid NetworkCapability.TRANSPORT_* value. " + + "Defaulting to TRANSPORT_ETHERNET"); + builder.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET); + } + + builder.setLinkUpstreamBandwidthKbps(100 * 1000); + builder.setLinkDownstreamBandwidthKbps(100 * 1000); + + if (!TextUtils.isEmpty(commaSeparatedCapabilities)) { + for (String strNetworkCapability : commaSeparatedCapabilities.split(",")) { + if (!TextUtils.isEmpty(strNetworkCapability)) { + try { + builder.addCapability(Integer.valueOf(strNetworkCapability)); + } catch (NumberFormatException nfe) { + Log.e(TAG, "Capability '" + strNetworkCapability + "' could not be parsed"); + } catch (IllegalArgumentException iae) { + Log.e(TAG, strNetworkCapability + " is not a valid " + + "NetworkCapability.NET_CAPABILITY_* value"); + } + } + } + } + // Ethernet networks have no way to update the following capabilities, so they always + // have them. + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING); + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED); + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED); + + return builder; + } + + /** + * Parses static IP configuration. + * + * @param staticIpConfig represents static IP configuration in the following format: {@code + * ip= gateway= dns= + * domains=} + */ + @VisibleForTesting + static IpConfiguration parseStaticIpConfiguration(String staticIpConfig) { + final StaticIpConfiguration.Builder staticIpConfigBuilder = + new StaticIpConfiguration.Builder(); + + for (String keyValueAsString : staticIpConfig.trim().split(" ")) { + if (TextUtils.isEmpty(keyValueAsString)) continue; + + String[] pair = keyValueAsString.split("="); + if (pair.length != 2) { + throw new IllegalArgumentException("Unexpected token: " + keyValueAsString + + " in " + staticIpConfig); + } + + String key = pair[0]; + String value = pair[1]; + + switch (key) { + case "ip": + staticIpConfigBuilder.setIpAddress(new LinkAddress(value)); + break; + case "domains": + staticIpConfigBuilder.setDomains(value); + break; + case "gateway": + staticIpConfigBuilder.setGateway(InetAddress.parseNumericAddress(value)); + break; + case "dns": { + ArrayList dnsAddresses = new ArrayList<>(); + for (String address: value.split(",")) { + dnsAddresses.add(InetAddress.parseNumericAddress(address)); + } + staticIpConfigBuilder.setDnsServers(dnsAddresses); + break; + } + default : { + throw new IllegalArgumentException("Unexpected key: " + key + + " in " + staticIpConfig); + } + } + } + return createIpConfiguration(staticIpConfigBuilder.build()); + } + + private static IpConfiguration createIpConfiguration( + @NonNull final StaticIpConfiguration staticIpConfig) { + return new IpConfiguration.Builder().setStaticIpConfiguration(staticIpConfig).build(); + } + + private IpConfiguration getOrCreateIpConfiguration(String iface) { + IpConfiguration ret = mIpConfigurations.get(iface); + if (ret != null) return ret; + ret = new IpConfiguration(); + ret.setIpAssignment(IpAssignment.DHCP); + ret.setProxySettings(ProxySettings.NONE); + return ret; + } + + private void updateIfaceMatchRegexp() { + final String match = mDeps.getInterfaceRegexFromResource(mContext); + mIfaceMatch = mIncludeTestInterfaces + ? "(" + match + "|" + TEST_IFACE_REGEXP + ")" + : match; + Log.d(TAG, "Interface match regexp set to '" + mIfaceMatch + "'"); + } + + /** + * Validate if a given interface is valid for testing. + * + * @param iface the name of the interface to validate. + * @return {@code true} if test interfaces are enabled and the given {@code iface} has a test + * interface prefix, {@code false} otherwise. + */ + public boolean isValidTestInterface(@NonNull final String iface) { + return mIncludeTestInterfaces && iface.matches(TEST_IFACE_REGEXP); + } + + private void postAndWaitForRunnable(Runnable r) { + final ConditionVariable cv = new ConditionVariable(); + if (mHandler.post(() -> { + r.run(); + cv.open(); + })) { + cv.block(2000L); + } + } + + @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/tests/ethernet/Android.bp b/tests/ethernet/Android.bp new file mode 100644 index 0000000000..6cfebdcc83 --- /dev/null +++ b/tests/ethernet/Android.bp @@ -0,0 +1,53 @@ +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +// TODO: merge the tests into service-connectivity tests after +// ethernet service migration completes. So far just import the +// ethernet service source to fix the dependencies. +android_test { + name: "EthernetServiceTests", + + srcs: [ + ":ethernet-service-updatable-sources", + ":services.connectivity-ethernet-sources", + "java/**/*.java", + ], + + certificate: "platform", + platform_apis: true, + + libs: [ + "android.test.runner", + "android.test.base", + "android.test.mock", + "framework-connectivity.impl", + "framework-connectivity-t.impl", + "ServiceConnectivityResources", + ], + + static_libs: [ + "androidx.test.rules", + "frameworks-base-testutils", + "mockito-target-minus-junit4", + "net-tests-utils", + "services.core", + "services.net", + ], + test_suites: ["general-tests"], +} diff --git a/tests/ethernet/AndroidManifest.xml b/tests/ethernet/AndroidManifest.xml new file mode 100644 index 0000000000..cd875b0209 --- /dev/null +++ b/tests/ethernet/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/tests/ethernet/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/ethernet/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java new file mode 100644 index 0000000000..4d3e4d36d2 --- /dev/null +++ b/tests/ethernet/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java @@ -0,0 +1,783 @@ +/* + * Copyright (C) 2020 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 com.android.testutils.DevSdkIgnoreRuleKt.SC_V2; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.NonNull; +import android.app.test.MockAnswerUtil.AnswerWithArguments; +import android.content.Context; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.EthernetNetworkSpecifier; +import android.net.EthernetNetworkManagementException; +import android.net.INetworkInterfaceOutcomeReceiver; +import android.net.IpConfiguration; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkAgentConfig; +import android.net.NetworkCapabilities; +import android.net.NetworkProvider; +import android.net.NetworkRequest; +import android.net.StaticIpConfiguration; +import android.net.ip.IpClientCallbacks; +import android.net.ip.IpClientManager; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.test.TestLooper; +import android.util.Pair; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.connectivity.resources.R; +import com.android.net.module.util.InterfaceParams; + +import com.android.testutils.DevSdkIgnoreRule; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class EthernetNetworkFactoryTest { + private static final int TIMEOUT_MS = 2_000; + private static final String TEST_IFACE = "test123"; + private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null; + private static final String IP_ADDR = "192.0.2.2/25"; + private static final LinkAddress LINK_ADDR = new LinkAddress(IP_ADDR); + private static final String HW_ADDR = "01:02:03:04:05:06"; + private TestLooper mLooper; + private Handler mHandler; + private EthernetNetworkFactory mNetFactory = null; + private IpClientCallbacks mIpClientCallbacks; + @Mock private Context mContext; + @Mock private Resources mResources; + @Mock private EthernetNetworkFactory.Dependencies mDeps; + @Mock private IpClientManager mIpClient; + @Mock private EthernetNetworkAgent mNetworkAgent; + @Mock private InterfaceParams mInterfaceParams; + @Mock private Network mMockNetwork; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + setupNetworkAgentMock(); + setupIpClientMock(); + setupContext(); + } + + //TODO: Move away from usage of TestLooper in order to move this logic back into @Before. + private void initEthernetNetworkFactory() { + mLooper = new TestLooper(); + mHandler = new Handler(mLooper.getLooper()); + mNetFactory = new EthernetNetworkFactory(mHandler, mContext, mDeps); + } + + private void setupNetworkAgentMock() { + when(mDeps.makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any())) + .thenAnswer(new AnswerWithArguments() { + public EthernetNetworkAgent answer( + Context context, + Looper looper, + NetworkCapabilities nc, + LinkProperties lp, + NetworkAgentConfig config, + NetworkProvider provider, + EthernetNetworkAgent.Callbacks cb) { + when(mNetworkAgent.getCallbacks()).thenReturn(cb); + when(mNetworkAgent.getNetwork()) + .thenReturn(mMockNetwork); + return mNetworkAgent; + } + } + ); + } + + private void setupIpClientMock() throws Exception { + doAnswer(inv -> { + // these tests only support one concurrent IpClient, so make sure we do not accidentally + // create a mess. + assertNull("An IpClient has already been created.", mIpClientCallbacks); + + mIpClientCallbacks = inv.getArgument(2); + mIpClientCallbacks.onIpClientCreated(null); + mLooper.dispatchAll(); + return null; + }).when(mDeps).makeIpClient(any(Context.class), anyString(), any()); + + doAnswer(inv -> { + mIpClientCallbacks.onQuit(); + mLooper.dispatchAll(); + mIpClientCallbacks = null; + return null; + }).when(mIpClient).shutdown(); + + when(mDeps.makeIpClientManager(any())).thenReturn(mIpClient); + } + + private void triggerOnProvisioningSuccess() { + mIpClientCallbacks.onProvisioningSuccess(new LinkProperties()); + mLooper.dispatchAll(); + } + + private void triggerOnProvisioningFailure() { + mIpClientCallbacks.onProvisioningFailure(new LinkProperties()); + mLooper.dispatchAll(); + } + + private void triggerOnReachabilityLost() { + mIpClientCallbacks.onReachabilityLost("ReachabilityLost"); + mLooper.dispatchAll(); + } + + private void setupContext() { + when(mDeps.getTcpBufferSizesFromResource(eq(mContext))).thenReturn(""); + } + + @After + public void tearDown() { + // looper is shared with the network agents, so there may still be messages to dispatch on + // tear down. + mLooper.dispatchAll(); + } + + private NetworkCapabilities createDefaultFilterCaps() { + return NetworkCapabilities.Builder.withoutDefaultCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .build(); + } + + private NetworkCapabilities.Builder createInterfaceCapsBuilder(final int transportType) { + return new NetworkCapabilities.Builder() + .addTransportType(transportType) + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED); + } + + private NetworkRequest.Builder createDefaultRequestBuilder() { + return new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + } + + private NetworkRequest createDefaultRequest() { + return createDefaultRequestBuilder().build(); + } + + private IpConfiguration createDefaultIpConfig() { + IpConfiguration ipConfig = new IpConfiguration(); + ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP); + ipConfig.setProxySettings(IpConfiguration.ProxySettings.NONE); + return ipConfig; + } + + /** + * Create an {@link IpConfiguration} with an associated {@link StaticIpConfiguration}. + * + * @return {@link IpConfiguration} with its {@link StaticIpConfiguration} set. + */ + private IpConfiguration createStaticIpConfig() { + final IpConfiguration ipConfig = new IpConfiguration(); + ipConfig.setIpAssignment(IpConfiguration.IpAssignment.STATIC); + ipConfig.setStaticIpConfiguration( + new StaticIpConfiguration.Builder().setIpAddress(LINK_ADDR).build()); + return ipConfig; + } + + // creates an interface with provisioning in progress (since updating the interface link state + // automatically starts the provisioning process) + private void createInterfaceUndergoingProvisioning(String iface) { + // Default to the ethernet transport type. + createInterfaceUndergoingProvisioning(iface, NetworkCapabilities.TRANSPORT_ETHERNET); + } + + private void createInterfaceUndergoingProvisioning( + @NonNull final String iface, final int transportType) { + final IpConfiguration ipConfig = createDefaultIpConfig(); + mNetFactory.addInterface(iface, HW_ADDR, ipConfig, + createInterfaceCapsBuilder(transportType).build()); + assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER)); + verifyStart(ipConfig); + clearInvocations(mDeps); + clearInvocations(mIpClient); + } + + // creates a provisioned interface + private void createAndVerifyProvisionedInterface(String iface) throws Exception { + // Default to the ethernet transport type. + createAndVerifyProvisionedInterface(iface, NetworkCapabilities.TRANSPORT_ETHERNET, + ConnectivityManager.TYPE_ETHERNET); + } + + private void createVerifyAndRemoveProvisionedInterface(final int transportType, + final int expectedLegacyType) throws Exception { + createAndVerifyProvisionedInterface(TEST_IFACE, transportType, + expectedLegacyType); + mNetFactory.removeInterface(TEST_IFACE); + } + + private void createAndVerifyProvisionedInterface( + @NonNull final String iface, final int transportType, final int expectedLegacyType) + throws Exception { + createInterfaceUndergoingProvisioning(iface, transportType); + triggerOnProvisioningSuccess(); + // provisioning succeeded, verify that the network agent is created, registered, marked + // as connected and legacy type are correctly set. + final ArgumentCaptor ncCaptor = ArgumentCaptor.forClass( + NetworkCapabilities.class); + verify(mDeps).makeEthernetNetworkAgent(any(), any(), ncCaptor.capture(), any(), + argThat(x -> x.getLegacyType() == expectedLegacyType), any(), any()); + assertEquals( + new EthernetNetworkSpecifier(iface), ncCaptor.getValue().getNetworkSpecifier()); + verifyNetworkAgentRegistersAndConnects(); + clearInvocations(mDeps); + clearInvocations(mNetworkAgent); + } + + // creates an unprovisioned interface + private void createUnprovisionedInterface(String iface) throws Exception { + // To create an unprovisioned interface, provision and then "stop" it, i.e. stop its + // NetworkAgent and IpClient. One way this can be done is by provisioning an interface and + // then calling onNetworkUnwanted. + createAndVerifyProvisionedInterface(iface); + + mNetworkAgent.getCallbacks().onNetworkUnwanted(); + mLooper.dispatchAll(); + verifyStop(); + + clearInvocations(mIpClient); + clearInvocations(mNetworkAgent); + } + + @Test + public void testAcceptRequest() throws Exception { + initEthernetNetworkFactory(); + createInterfaceUndergoingProvisioning(TEST_IFACE); + assertTrue(mNetFactory.acceptRequest(createDefaultRequest())); + + NetworkRequest wifiRequest = createDefaultRequestBuilder() + .removeTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build(); + assertFalse(mNetFactory.acceptRequest(wifiRequest)); + } + + @Test + public void testUpdateInterfaceLinkStateForActiveProvisioningInterface() throws Exception { + initEthernetNetworkFactory(); + createInterfaceUndergoingProvisioning(TEST_IFACE); + final TestNetworkManagementListener listener = new TestNetworkManagementListener(); + + // verify that the IpClient gets shut down when interface state changes to down. + final boolean ret = + mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener); + + assertTrue(ret); + verify(mIpClient).shutdown(); + assertEquals(listener.expectOnResult(), TEST_IFACE); + } + + @Test + public void testUpdateInterfaceLinkStateForProvisionedInterface() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + final TestNetworkManagementListener listener = new TestNetworkManagementListener(); + + final boolean ret = + mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener); + + assertTrue(ret); + verifyStop(); + assertEquals(listener.expectOnResult(), TEST_IFACE); + } + + @Test + public void testUpdateInterfaceLinkStateForUnprovisionedInterface() throws Exception { + initEthernetNetworkFactory(); + createUnprovisionedInterface(TEST_IFACE); + final TestNetworkManagementListener listener = new TestNetworkManagementListener(); + + final boolean ret = + mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener); + + assertTrue(ret); + // There should not be an active IPClient or NetworkAgent. + verify(mDeps, never()).makeIpClient(any(), any(), any()); + verify(mDeps, never()) + .makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any()); + assertEquals(listener.expectOnResult(), TEST_IFACE); + } + + @Test + public void testUpdateInterfaceLinkStateForNonExistingInterface() throws Exception { + initEthernetNetworkFactory(); + final TestNetworkManagementListener listener = new TestNetworkManagementListener(); + + // if interface was never added, link state cannot be updated. + final boolean ret = + mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listener); + + assertFalse(ret); + verifyNoStopOrStart(); + listener.expectOnErrorWithMessage("can't be updated as it is not available"); + } + + @Test + public void testUpdateInterfaceLinkStateWithNoChanges() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + final TestNetworkManagementListener listener = new TestNetworkManagementListener(); + + final boolean ret = + mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listener); + + assertFalse(ret); + verifyNoStopOrStart(); + listener.expectOnErrorWithMessage("No changes"); + } + + @Test + public void testNeedNetworkForOnProvisionedInterface() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + mNetFactory.needNetworkFor(createDefaultRequest()); + verify(mIpClient, never()).startProvisioning(any()); + } + + @Test + public void testNeedNetworkForOnUnprovisionedInterface() throws Exception { + initEthernetNetworkFactory(); + createUnprovisionedInterface(TEST_IFACE); + mNetFactory.needNetworkFor(createDefaultRequest()); + verify(mIpClient).startProvisioning(any()); + + triggerOnProvisioningSuccess(); + verifyNetworkAgentRegistersAndConnects(); + } + + @Test + public void testNeedNetworkForOnInterfaceUndergoingProvisioning() throws Exception { + initEthernetNetworkFactory(); + createInterfaceUndergoingProvisioning(TEST_IFACE); + mNetFactory.needNetworkFor(createDefaultRequest()); + verify(mIpClient, never()).startProvisioning(any()); + + triggerOnProvisioningSuccess(); + verifyNetworkAgentRegistersAndConnects(); + } + + @Test + public void testProvisioningLoss() throws Exception { + initEthernetNetworkFactory(); + when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams); + createAndVerifyProvisionedInterface(TEST_IFACE); + + triggerOnProvisioningFailure(); + verifyStop(); + // provisioning loss should trigger a retry, since the interface is still there + verify(mIpClient).startProvisioning(any()); + } + + @Test + public void testProvisioningLossForDisappearedInterface() throws Exception { + initEthernetNetworkFactory(); + // mocked method returns null by default, but just to be explicit in the test: + when(mDeps.getNetworkInterfaceByName(eq(TEST_IFACE))).thenReturn(null); + + createAndVerifyProvisionedInterface(TEST_IFACE); + triggerOnProvisioningFailure(); + + // the interface disappeared and getNetworkInterfaceByName returns null, we should not retry + verify(mIpClient, never()).startProvisioning(any()); + verifyNoStopOrStart(); + } + + private void verifyNoStopOrStart() { + verify(mNetworkAgent, never()).register(); + verify(mIpClient, never()).shutdown(); + verify(mNetworkAgent, never()).unregister(); + verify(mIpClient, never()).startProvisioning(any()); + } + + @Test + public void testIpClientIsNotStartedWhenLinkIsDown() throws Exception { + initEthernetNetworkFactory(); + createUnprovisionedInterface(TEST_IFACE); + mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER); + + mNetFactory.needNetworkFor(createDefaultRequest()); + + verify(mDeps, never()).makeIpClient(any(), any(), any()); + + // BUG(b/191854824): requesting a network with a specifier (Android Auto use case) should + // not start an IpClient when the link is down, but fixing this may make matters worse by + // tiggering b/197548738. + NetworkRequest specificNetRequest = new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .setNetworkSpecifier(new EthernetNetworkSpecifier(TEST_IFACE)) + .build(); + mNetFactory.needNetworkFor(specificNetRequest); + mNetFactory.releaseNetworkFor(specificNetRequest); + + mNetFactory.updateInterfaceLinkState(TEST_IFACE, true, NULL_LISTENER); + // TODO: change to once when b/191854824 is fixed. + verify(mDeps, times(2)).makeIpClient(any(), eq(TEST_IFACE), any()); + } + + @Test + public void testLinkPropertiesChanged() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + + LinkProperties lp = new LinkProperties(); + mIpClientCallbacks.onLinkPropertiesChange(lp); + mLooper.dispatchAll(); + verify(mNetworkAgent).sendLinkPropertiesImpl(same(lp)); + } + + @Test + public void testNetworkUnwanted() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + + mNetworkAgent.getCallbacks().onNetworkUnwanted(); + mLooper.dispatchAll(); + verifyStop(); + } + + @Test + public void testNetworkUnwantedWithStaleNetworkAgent() throws Exception { + initEthernetNetworkFactory(); + // ensures provisioning is restarted after provisioning loss + when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams); + createAndVerifyProvisionedInterface(TEST_IFACE); + + EthernetNetworkAgent.Callbacks oldCbs = mNetworkAgent.getCallbacks(); + // replace network agent in EthernetNetworkFactory + // Loss of provisioning will restart the ip client and network agent. + triggerOnProvisioningFailure(); + verify(mDeps).makeIpClient(any(), any(), any()); + + triggerOnProvisioningSuccess(); + verify(mDeps).makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any()); + + // verify that unwanted is ignored + clearInvocations(mIpClient); + clearInvocations(mNetworkAgent); + oldCbs.onNetworkUnwanted(); + verify(mIpClient, never()).shutdown(); + verify(mNetworkAgent, never()).unregister(); + } + + @Test + public void testTransportOverrideIsCorrectlySet() throws Exception { + initEthernetNetworkFactory(); + // createProvisionedInterface() has verifications in place for transport override + // functionality which for EthernetNetworkFactory is network score and legacy type mappings. + createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_ETHERNET, + ConnectivityManager.TYPE_ETHERNET); + createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_BLUETOOTH, + ConnectivityManager.TYPE_BLUETOOTH); + createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_WIFI, + ConnectivityManager.TYPE_WIFI); + createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_CELLULAR, + ConnectivityManager.TYPE_MOBILE); + createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_LOWPAN, + ConnectivityManager.TYPE_NONE); + createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_WIFI_AWARE, + ConnectivityManager.TYPE_NONE); + createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_TEST, + ConnectivityManager.TYPE_NONE); + } + + @Test + public void testReachabilityLoss() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + + triggerOnReachabilityLost(); + + // Reachability loss should trigger a stop and start, since the interface is still there + verifyRestart(createDefaultIpConfig()); + } + + private IpClientCallbacks getStaleIpClientCallbacks() throws Exception { + createAndVerifyProvisionedInterface(TEST_IFACE); + final IpClientCallbacks staleIpClientCallbacks = mIpClientCallbacks; + mNetFactory.removeInterface(TEST_IFACE); + verifyStop(); + assertNotSame(mIpClientCallbacks, staleIpClientCallbacks); + return staleIpClientCallbacks; + } + + @Test + public void testIgnoreOnIpLayerStartedCallbackForStaleCallback() throws Exception { + initEthernetNetworkFactory(); + final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks(); + + staleIpClientCallbacks.onProvisioningSuccess(new LinkProperties()); + mLooper.dispatchAll(); + + verify(mIpClient, never()).startProvisioning(any()); + verify(mNetworkAgent, never()).register(); + } + + @Test + public void testIgnoreOnIpLayerStoppedCallbackForStaleCallback() throws Exception { + initEthernetNetworkFactory(); + when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams); + final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks(); + + staleIpClientCallbacks.onProvisioningFailure(new LinkProperties()); + mLooper.dispatchAll(); + + verify(mIpClient, never()).startProvisioning(any()); + } + + @Test + public void testIgnoreLinkPropertiesCallbackForStaleCallback() throws Exception { + initEthernetNetworkFactory(); + final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks(); + final LinkProperties lp = new LinkProperties(); + + staleIpClientCallbacks.onLinkPropertiesChange(lp); + mLooper.dispatchAll(); + + verify(mNetworkAgent, never()).sendLinkPropertiesImpl(eq(lp)); + } + + @Test + public void testIgnoreNeighborLossCallbackForStaleCallback() throws Exception { + initEthernetNetworkFactory(); + final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks(); + + staleIpClientCallbacks.onReachabilityLost("Neighbor Lost"); + mLooper.dispatchAll(); + + verify(mIpClient, never()).startProvisioning(any()); + verify(mNetworkAgent, never()).register(); + } + + private void verifyRestart(@NonNull final IpConfiguration ipConfig) { + verifyStop(); + verifyStart(ipConfig); + } + + private void verifyStart(@NonNull final IpConfiguration ipConfig) { + verify(mDeps).makeIpClient(any(Context.class), anyString(), any()); + verify(mIpClient).startProvisioning( + argThat(x -> Objects.equals(x.mStaticIpConfig, ipConfig.getStaticIpConfiguration())) + ); + } + + private void verifyStop() { + verify(mIpClient).shutdown(); + verify(mNetworkAgent).unregister(); + } + + private void verifyNetworkAgentRegistersAndConnects() { + verify(mNetworkAgent).register(); + verify(mNetworkAgent).markConnected(); + } + + private static final class TestNetworkManagementListener + implements INetworkInterfaceOutcomeReceiver { + private final CompletableFuture mResult = new CompletableFuture<>(); + private final CompletableFuture mError = + new CompletableFuture<>(); + + @Override + public void onResult(@NonNull String iface) { + mResult.complete(iface); + } + + @Override + public void onError(@NonNull EthernetNetworkManagementException exception) { + mError.complete(exception); + } + + String expectOnResult() throws Exception { + return mResult.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + + EthernetNetworkManagementException expectOnError() throws Exception { + return mError.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + + void expectOnErrorWithMessage(String msg) throws Exception { + assertTrue(expectOnError().getMessage().contains(msg)); + } + + @Override + public IBinder asBinder() { + return null; + } + } + + @Test + public void testUpdateInterfaceCallsListenerCorrectlyOnSuccess() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + final NetworkCapabilities capabilities = createDefaultFilterCaps(); + final IpConfiguration ipConfiguration = createStaticIpConfig(); + final TestNetworkManagementListener listener = new TestNetworkManagementListener(); + + mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener); + triggerOnProvisioningSuccess(); + + assertEquals(listener.expectOnResult(), TEST_IFACE); + } + + @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available + @Test + public void testUpdateInterfaceAbortsOnConcurrentRemoveInterface() throws Exception { + initEthernetNetworkFactory(); + verifyNetworkManagementCallIsAbortedWhenInterrupted( + TEST_IFACE, + () -> mNetFactory.removeInterface(TEST_IFACE)); + } + + @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available + @Test + public void testUpdateInterfaceAbortsOnConcurrentUpdateInterfaceLinkState() throws Exception { + initEthernetNetworkFactory(); + verifyNetworkManagementCallIsAbortedWhenInterrupted( + TEST_IFACE, + () -> mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER)); + } + + @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available + @Test + public void testUpdateInterfaceCallsListenerCorrectlyOnConcurrentRequests() throws Exception { + initEthernetNetworkFactory(); + final NetworkCapabilities capabilities = createDefaultFilterCaps(); + final IpConfiguration ipConfiguration = createStaticIpConfig(); + final TestNetworkManagementListener successfulListener = + new TestNetworkManagementListener(); + + // If two calls come in before the first one completes, the first listener will be aborted + // and the second one will be successful. + verifyNetworkManagementCallIsAbortedWhenInterrupted( + TEST_IFACE, + () -> { + mNetFactory.updateInterface( + TEST_IFACE, ipConfiguration, capabilities, successfulListener); + triggerOnProvisioningSuccess(); + }); + + assertEquals(successfulListener.expectOnResult(), TEST_IFACE); + } + + private void verifyNetworkManagementCallIsAbortedWhenInterrupted( + @NonNull final String iface, + @NonNull final Runnable interruptingRunnable) throws Exception { + createAndVerifyProvisionedInterface(iface); + final NetworkCapabilities capabilities = createDefaultFilterCaps(); + final IpConfiguration ipConfiguration = createStaticIpConfig(); + final TestNetworkManagementListener failedListener = new TestNetworkManagementListener(); + + // An active update request will be aborted on interrupt prior to provisioning completion. + mNetFactory.updateInterface(iface, ipConfiguration, capabilities, failedListener); + interruptingRunnable.run(); + + failedListener.expectOnErrorWithMessage("aborted"); + } + + @Test + public void testUpdateInterfaceRestartsAgentCorrectly() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + final NetworkCapabilities capabilities = createDefaultFilterCaps(); + final IpConfiguration ipConfiguration = createStaticIpConfig(); + final TestNetworkManagementListener listener = new TestNetworkManagementListener(); + + mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener); + triggerOnProvisioningSuccess(); + + assertEquals(listener.expectOnResult(), TEST_IFACE); + verify(mDeps).makeEthernetNetworkAgent(any(), any(), + eq(capabilities), any(), any(), any(), any()); + verifyRestart(ipConfiguration); + } + + @Test + public void testUpdateInterfaceForNonExistingInterface() throws Exception { + initEthernetNetworkFactory(); + // No interface exists due to not calling createAndVerifyProvisionedInterface(...). + final NetworkCapabilities capabilities = createDefaultFilterCaps(); + final IpConfiguration ipConfiguration = createStaticIpConfig(); + final TestNetworkManagementListener listener = new TestNetworkManagementListener(); + + mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener); + + verifyNoStopOrStart(); + listener.expectOnErrorWithMessage("can't be updated as it is not available"); + } + + @Test + public void testUpdateInterfaceWithNullIpConfiguration() throws Exception { + initEthernetNetworkFactory(); + createAndVerifyProvisionedInterface(TEST_IFACE); + + final IpConfiguration initialIpConfig = createStaticIpConfig(); + mNetFactory.updateInterface(TEST_IFACE, initialIpConfig, null /*capabilities*/, + null /*listener*/); + triggerOnProvisioningSuccess(); + verifyRestart(initialIpConfig); + + // TODO: have verifyXyz functions clear invocations. + clearInvocations(mDeps); + clearInvocations(mIpClient); + clearInvocations(mNetworkAgent); + + + // verify that sending a null ipConfig does not update the current ipConfig. + mNetFactory.updateInterface(TEST_IFACE, null /*ipConfig*/, null /*capabilities*/, + null /*listener*/); + triggerOnProvisioningSuccess(); + verifyRestart(initialIpConfig); + } +} diff --git a/tests/ethernet/java/com/android/server/ethernet/EthernetServiceImplTest.java b/tests/ethernet/java/com/android/server/ethernet/EthernetServiceImplTest.java new file mode 100644 index 0000000000..dd1f1edba7 --- /dev/null +++ b/tests/ethernet/java/com/android/server/ethernet/EthernetServiceImplTest.java @@ -0,0 +1,372 @@ +/* + * 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 static android.net.NetworkCapabilities.TRANSPORT_TEST; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.Manifest; +import android.annotation.NonNull; +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.INetworkInterfaceOutcomeReceiver; +import android.net.EthernetNetworkUpdateRequest; +import android.net.IpConfiguration; +import android.net.NetworkCapabilities; +import android.os.Handler; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class EthernetServiceImplTest { + private static final String TEST_IFACE = "test123"; + private static final EthernetNetworkUpdateRequest UPDATE_REQUEST = + new EthernetNetworkUpdateRequest.Builder() + .setIpConfiguration(new IpConfiguration()) + .setNetworkCapabilities(new NetworkCapabilities.Builder().build()) + .build(); + private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_CAPABILITIES = + new EthernetNetworkUpdateRequest.Builder() + .setIpConfiguration(new IpConfiguration()) + .build(); + private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_IP_CONFIG = + new EthernetNetworkUpdateRequest.Builder() + .setNetworkCapabilities(new NetworkCapabilities.Builder().build()) + .build(); + private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null; + private EthernetServiceImpl mEthernetServiceImpl; + @Mock private Context mContext; + @Mock private Handler mHandler; + @Mock private EthernetTracker mEthernetTracker; + @Mock private PackageManager mPackageManager; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + doReturn(mPackageManager).when(mContext).getPackageManager(); + mEthernetServiceImpl = new EthernetServiceImpl(mContext, mHandler, mEthernetTracker); + mEthernetServiceImpl.mStarted.set(true); + toggleAutomotiveFeature(true); + shouldTrackIface(TEST_IFACE, true); + } + + private void toggleAutomotiveFeature(final boolean isEnabled) { + doReturn(isEnabled) + .when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); + } + + private void shouldTrackIface(@NonNull final String iface, final boolean shouldTrack) { + doReturn(shouldTrack).when(mEthernetTracker).isTrackingInterface(iface); + } + + @Test + public void testSetConfigurationRejectsWhenEthNotStarted() { + mEthernetServiceImpl.mStarted.set(false); + assertThrows(IllegalStateException.class, () -> { + mEthernetServiceImpl.setConfiguration("" /* iface */, new IpConfiguration()); + }); + } + + @Test + public void testUpdateConfigurationRejectsWhenEthNotStarted() { + mEthernetServiceImpl.mStarted.set(false); + assertThrows(IllegalStateException.class, () -> { + mEthernetServiceImpl.updateConfiguration( + "" /* iface */, UPDATE_REQUEST, null /* listener */); + }); + } + + @Test + public void testConnectNetworkRejectsWhenEthNotStarted() { + mEthernetServiceImpl.mStarted.set(false); + assertThrows(IllegalStateException.class, () -> { + mEthernetServiceImpl.connectNetwork("" /* iface */, null /* listener */); + }); + } + + @Test + public void testDisconnectNetworkRejectsWhenEthNotStarted() { + mEthernetServiceImpl.mStarted.set(false); + assertThrows(IllegalStateException.class, () -> { + mEthernetServiceImpl.disconnectNetwork("" /* iface */, null /* listener */); + }); + } + + @Test + public void testUpdateConfigurationRejectsNullIface() { + assertThrows(NullPointerException.class, () -> { + mEthernetServiceImpl.updateConfiguration(null, UPDATE_REQUEST, NULL_LISTENER); + }); + } + + @Test + public void testConnectNetworkRejectsNullIface() { + assertThrows(NullPointerException.class, () -> { + mEthernetServiceImpl.connectNetwork(null /* iface */, NULL_LISTENER); + }); + } + + @Test + public void testDisconnectNetworkRejectsNullIface() { + assertThrows(NullPointerException.class, () -> { + mEthernetServiceImpl.disconnectNetwork(null /* iface */, NULL_LISTENER); + }); + } + + @Test + public void testUpdateConfigurationWithCapabilitiesRejectsWithoutAutomotiveFeature() { + toggleAutomotiveFeature(false); + assertThrows(UnsupportedOperationException.class, () -> { + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER); + }); + } + + @Test + public void testUpdateConfigurationWithCapabilitiesWithAutomotiveFeature() { + toggleAutomotiveFeature(false); + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST_WITHOUT_CAPABILITIES, + NULL_LISTENER); + verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE), + eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getIpConfiguration()), + eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getNetworkCapabilities()), isNull()); + } + + @Test + public void testConnectNetworkRejectsWithoutAutomotiveFeature() { + toggleAutomotiveFeature(false); + assertThrows(UnsupportedOperationException.class, () -> { + mEthernetServiceImpl.connectNetwork("" /* iface */, NULL_LISTENER); + }); + } + + @Test + public void testDisconnectNetworkRejectsWithoutAutomotiveFeature() { + toggleAutomotiveFeature(false); + assertThrows(UnsupportedOperationException.class, () -> { + mEthernetServiceImpl.disconnectNetwork("" /* iface */, NULL_LISTENER); + }); + } + + private void denyManageEthPermission() { + doThrow(new SecurityException("")).when(mContext) + .enforceCallingOrSelfPermission( + eq(Manifest.permission.MANAGE_ETHERNET_NETWORKS), anyString()); + } + + private void denyManageTestNetworksPermission() { + doThrow(new SecurityException("")).when(mContext) + .enforceCallingOrSelfPermission( + eq(Manifest.permission.MANAGE_TEST_NETWORKS), anyString()); + } + + @Test + public void testUpdateConfigurationRejectsWithoutManageEthPermission() { + denyManageEthPermission(); + assertThrows(SecurityException.class, () -> { + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER); + }); + } + + @Test + public void testConnectNetworkRejectsWithoutManageEthPermission() { + denyManageEthPermission(); + assertThrows(SecurityException.class, () -> { + mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER); + }); + } + + @Test + public void testDisconnectNetworkRejectsWithoutManageEthPermission() { + denyManageEthPermission(); + assertThrows(SecurityException.class, () -> { + mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER); + }); + } + + private void enableTestInterface() { + when(mEthernetTracker.isValidTestInterface(eq(TEST_IFACE))).thenReturn(true); + } + + @Test + public void testUpdateConfigurationRejectsTestRequestWithoutTestPermission() { + enableTestInterface(); + denyManageTestNetworksPermission(); + assertThrows(SecurityException.class, () -> { + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER); + }); + } + + @Test + public void testConnectNetworkRejectsTestRequestWithoutTestPermission() { + enableTestInterface(); + denyManageTestNetworksPermission(); + assertThrows(SecurityException.class, () -> { + mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER); + }); + } + + @Test + public void testDisconnectNetworkRejectsTestRequestWithoutTestPermission() { + enableTestInterface(); + denyManageTestNetworksPermission(); + assertThrows(SecurityException.class, () -> { + mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER); + }); + } + + @Test + public void testUpdateConfiguration() { + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER); + verify(mEthernetTracker).updateConfiguration( + eq(TEST_IFACE), + eq(UPDATE_REQUEST.getIpConfiguration()), + eq(UPDATE_REQUEST.getNetworkCapabilities()), eq(NULL_LISTENER)); + } + + @Test + public void testConnectNetwork() { + mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER); + verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER)); + } + + @Test + public void testDisconnectNetwork() { + mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER); + verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER)); + } + + @Test + public void testUpdateConfigurationAcceptsTestRequestWithNullCapabilities() { + enableTestInterface(); + final EthernetNetworkUpdateRequest request = + new EthernetNetworkUpdateRequest + .Builder() + .setIpConfiguration(new IpConfiguration()).build(); + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, request, NULL_LISTENER); + verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE), + eq(request.getIpConfiguration()), + eq(request.getNetworkCapabilities()), isNull()); + } + + @Test + public void testUpdateConfigurationAcceptsRequestWithNullIpConfiguration() { + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST_WITHOUT_IP_CONFIG, + NULL_LISTENER); + verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE), + eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getIpConfiguration()), + eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getNetworkCapabilities()), isNull()); + } + + @Test + public void testUpdateConfigurationRejectsInvalidTestRequest() { + enableTestInterface(); + assertThrows(IllegalArgumentException.class, () -> { + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER); + }); + } + + private EthernetNetworkUpdateRequest createTestNetworkUpdateRequest() { + final NetworkCapabilities nc = new NetworkCapabilities + .Builder(UPDATE_REQUEST.getNetworkCapabilities()) + .addTransportType(TRANSPORT_TEST).build(); + + return new EthernetNetworkUpdateRequest + .Builder(UPDATE_REQUEST) + .setNetworkCapabilities(nc).build(); + } + + @Test + public void testUpdateConfigurationForTestRequestDoesNotRequireAutoOrEthernetPermission() { + enableTestInterface(); + toggleAutomotiveFeature(false); + denyManageEthPermission(); + final EthernetNetworkUpdateRequest request = createTestNetworkUpdateRequest(); + + mEthernetServiceImpl.updateConfiguration(TEST_IFACE, request, NULL_LISTENER); + verify(mEthernetTracker).updateConfiguration( + eq(TEST_IFACE), + eq(request.getIpConfiguration()), + eq(request.getNetworkCapabilities()), eq(NULL_LISTENER)); + } + + @Test + public void testConnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() { + enableTestInterface(); + toggleAutomotiveFeature(false); + denyManageEthPermission(); + + mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER); + verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER)); + } + + @Test + public void testDisconnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() { + enableTestInterface(); + toggleAutomotiveFeature(false); + denyManageEthPermission(); + + mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER); + verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER)); + } + + private void denyPermissions(String... permissions) { + for (String permission: permissions) { + doReturn(PackageManager.PERMISSION_DENIED).when(mContext) + .checkCallingOrSelfPermission(eq(permission)); + } + } + + @Test + public void testSetEthernetEnabled() { + denyPermissions(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK); + mEthernetServiceImpl.setEthernetEnabled(true); + verify(mEthernetTracker).setEthernetEnabled(true); + reset(mEthernetTracker); + + denyPermissions(Manifest.permission.NETWORK_STACK); + mEthernetServiceImpl.setEthernetEnabled(false); + verify(mEthernetTracker).setEthernetEnabled(false); + reset(mEthernetTracker); + + denyPermissions(Manifest.permission.NETWORK_SETTINGS); + try { + mEthernetServiceImpl.setEthernetEnabled(true); + fail("Should get SecurityException"); + } catch (SecurityException e) { } + verify(mEthernetTracker, never()).setEthernetEnabled(false); + } +} diff --git a/tests/ethernet/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/ethernet/java/com/android/server/ethernet/EthernetTrackerTest.java new file mode 100644 index 0000000000..b1831c411b --- /dev/null +++ b/tests/ethernet/java/com/android/server/ethernet/EthernetTrackerTest.java @@ -0,0 +1,456 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.ethernet; + +import static android.net.TestNetworkManager.TEST_TAP_PREFIX; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.net.EthernetManager; +import android.net.InetAddresses; +import android.net.INetworkInterfaceOutcomeReceiver; +import android.net.IEthernetServiceListener; +import android.net.INetd; +import android.net.IpConfiguration; +import android.net.IpConfiguration.IpAssignment; +import android.net.IpConfiguration.ProxySettings; +import android.net.InterfaceConfigurationParcel; +import android.net.LinkAddress; +import android.net.NetworkCapabilities; +import android.net.StaticIpConfiguration; +import android.os.HandlerThread; +import android.os.RemoteException; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.connectivity.resources.R; +import com.android.testutils.HandlerUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.net.InetAddress; +import java.util.ArrayList; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class EthernetTrackerTest { + private static final String TEST_IFACE = "test123"; + private static final int TIMEOUT_MS = 1_000; + private static final String THREAD_NAME = "EthernetServiceThread"; + private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null; + private EthernetTracker tracker; + private HandlerThread mHandlerThread; + @Mock private Context mContext; + @Mock private EthernetNetworkFactory mFactory; + @Mock private INetd mNetd; + @Mock private EthernetTracker.Dependencies mDeps; + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + initMockResources(); + when(mFactory.updateInterfaceLinkState(anyString(), anyBoolean(), any())).thenReturn(false); + when(mNetd.interfaceGetList()).thenReturn(new String[0]); + mHandlerThread = new HandlerThread(THREAD_NAME); + mHandlerThread.start(); + tracker = new EthernetTracker(mContext, mHandlerThread.getThreadHandler(), mFactory, mNetd, + mDeps); + } + + @After + public void cleanUp() { + mHandlerThread.quitSafely(); + } + + private void initMockResources() { + when(mDeps.getInterfaceRegexFromResource(eq(mContext))).thenReturn(""); + when(mDeps.getInterfaceConfigFromResource(eq(mContext))).thenReturn(new String[0]); + } + + private void waitForIdle() { + HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS); + } + + /** + * Test: Creation of various valid static IP configurations + */ + @Test + public void createStaticIpConfiguration() { + // Empty gives default StaticIPConfiguration object + assertStaticConfiguration(new StaticIpConfiguration(), ""); + + // Setting only the IP address properly cascades and assumes defaults + assertStaticConfiguration(new StaticIpConfiguration.Builder() + .setIpAddress(new LinkAddress("192.0.2.10/24")).build(), "ip=192.0.2.10/24"); + + final ArrayList dnsAddresses = new ArrayList<>(); + dnsAddresses.add(InetAddresses.parseNumericAddress("4.4.4.4")); + dnsAddresses.add(InetAddresses.parseNumericAddress("8.8.8.8")); + // Setting other fields properly cascades them + assertStaticConfiguration(new StaticIpConfiguration.Builder() + .setIpAddress(new LinkAddress("192.0.2.10/24")) + .setDnsServers(dnsAddresses) + .setGateway(InetAddresses.parseNumericAddress("192.0.2.1")) + .setDomains("android").build(), + "ip=192.0.2.10/24 dns=4.4.4.4,8.8.8.8 gateway=192.0.2.1 domains=android"); + + // Verify order doesn't matter + assertStaticConfiguration(new StaticIpConfiguration.Builder() + .setIpAddress(new LinkAddress("192.0.2.10/24")) + .setDnsServers(dnsAddresses) + .setGateway(InetAddresses.parseNumericAddress("192.0.2.1")) + .setDomains("android").build(), + "domains=android ip=192.0.2.10/24 gateway=192.0.2.1 dns=4.4.4.4,8.8.8.8 "); + } + + /** + * Test: Attempt creation of various bad static IP configurations + */ + @Test + public void createStaticIpConfiguration_Bad() { + assertStaticConfigurationFails("ip=192.0.2.1/24 gateway= blah=20.20.20.20"); // Unknown key + assertStaticConfigurationFails("ip=192.0.2.1"); // mask is missing + assertStaticConfigurationFails("ip=a.b.c"); // not a valid ip address + assertStaticConfigurationFails("dns=4.4.4.4,1.2.3.A"); // not valid ip address in dns + assertStaticConfigurationFails("="); // Key and value is empty + assertStaticConfigurationFails("ip="); // Value is empty + assertStaticConfigurationFails("ip=192.0.2.1/24 gateway="); // Gateway is empty + } + + private void assertStaticConfigurationFails(String config) { + try { + EthernetTracker.parseStaticIpConfiguration(config); + fail("Expected to fail: " + config); + } catch (IllegalArgumentException e) { + // expected + } + } + + private void assertStaticConfiguration(StaticIpConfiguration expectedStaticIpConfig, + String configAsString) { + final IpConfiguration expectedIpConfiguration = new IpConfiguration(); + expectedIpConfiguration.setIpAssignment(IpAssignment.STATIC); + expectedIpConfiguration.setProxySettings(ProxySettings.NONE); + expectedIpConfiguration.setStaticIpConfiguration(expectedStaticIpConfig); + + assertEquals(expectedIpConfiguration, + EthernetTracker.parseStaticIpConfiguration(configAsString)); + } + + private NetworkCapabilities.Builder makeEthernetCapabilitiesBuilder(boolean clearAll) { + final NetworkCapabilities.Builder builder = + clearAll ? NetworkCapabilities.Builder.withoutDefaultCapabilities() + : new NetworkCapabilities.Builder(); + return builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED); + } + + /** + * Test: Attempt to create a capabilties with various valid sets of capabilities/transports + */ + @Test + public void createNetworkCapabilities() { + + // Particularly common expected results + NetworkCapabilities defaultEthernetCleared = + makeEthernetCapabilitiesBuilder(true /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .build(); + + NetworkCapabilities ethernetClearedWithCommonCaps = + makeEthernetCapabilitiesBuilder(true /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .addCapability(12) + .addCapability(13) + .addCapability(14) + .addCapability(15) + .build(); + + // Empty capabilities and transports lists with a "please clear defaults" should + // yield an empty capabilities set with TRANPORT_ETHERNET + assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", ""); + + // Empty capabilities and transports without the clear defaults flag should return the + // default capabilities set with TRANSPORT_ETHERNET + assertParsedNetworkCapabilities( + makeEthernetCapabilitiesBuilder(false /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .build(), + false, "", ""); + + // A list of capabilities without the clear defaults flag should return the default + // capabilities, mixed with the desired capabilities, and TRANSPORT_ETHERNET + assertParsedNetworkCapabilities( + makeEthernetCapabilitiesBuilder(false /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .addCapability(11) + .addCapability(12) + .build(), + false, "11,12", ""); + + // Adding a list of capabilities with a clear defaults will leave exactly those capabilities + // with a default TRANSPORT_ETHERNET since no overrides are specified + assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15", ""); + + // Adding any invalid capabilities to the list will cause them to be ignored + assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,65,73", ""); + assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,abcdefg", ""); + + // Adding a valid override transport will remove the default TRANSPORT_ETHERNET transport + // and apply only the override to the capabiltities object + assertParsedNetworkCapabilities( + makeEthernetCapabilitiesBuilder(true /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addTransportType(0) + .build(), + true, "", "0"); + assertParsedNetworkCapabilities( + makeEthernetCapabilitiesBuilder(true /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addTransportType(1) + .build(), + true, "", "1"); + assertParsedNetworkCapabilities( + makeEthernetCapabilitiesBuilder(true /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addTransportType(2) + .build(), + true, "", "2"); + assertParsedNetworkCapabilities( + makeEthernetCapabilitiesBuilder(true /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addTransportType(3) + .build(), + true, "", "3"); + + // "4" is TRANSPORT_VPN, which is unsupported. Should default back to TRANPORT_ETHERNET + assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "4"); + + // "5" is TRANSPORT_WIFI_AWARE, which is currently supported due to no legacy TYPE_NONE + // conversion. When that becomes available, this test must be updated + assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "5"); + + // "6" is TRANSPORT_LOWPAN, which is currently supported due to no legacy TYPE_NONE + // conversion. When that becomes available, this test must be updated + assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "6"); + + // Adding an invalid override transport will leave the transport as TRANSPORT_ETHERNET + assertParsedNetworkCapabilities(defaultEthernetCleared,true, "", "100"); + assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "abcdefg"); + + // Ensure the adding of both capabilities and transports work + assertParsedNetworkCapabilities( + makeEthernetCapabilitiesBuilder(true /* clearAll */) + .setLinkUpstreamBandwidthKbps(100000) + .setLinkDownstreamBandwidthKbps(100000) + .addCapability(12) + .addCapability(13) + .addCapability(14) + .addCapability(15) + .addTransportType(3) + .build(), + true, "12,13,14,15", "3"); + + // Ensure order does not matter for capability list + assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "13,12,15,14", ""); + } + + private void assertParsedNetworkCapabilities(NetworkCapabilities expectedNetworkCapabilities, + boolean clearCapabilties, String configCapabiltiies,String configTransports) { + assertEquals(expectedNetworkCapabilities, + EthernetTracker.createNetworkCapabilities(clearCapabilties, configCapabiltiies, + configTransports).build()); + } + + @Test + public void testCreateEthernetTrackerConfigReturnsCorrectValue() { + final String capabilities = "2"; + final String ipConfig = "3"; + final String transport = "4"; + final String configString = String.join(";", TEST_IFACE, capabilities, ipConfig, transport); + + final EthernetTracker.EthernetTrackerConfig config = + EthernetTracker.createEthernetTrackerConfig(configString); + + assertEquals(TEST_IFACE, config.mIface); + assertEquals(capabilities, config.mCapabilities); + assertEquals(ipConfig, config.mIpConfig); + assertEquals(transport, config.mTransport); + } + + @Test + public void testCreateEthernetTrackerConfigThrowsNpeWithNullInput() { + assertThrows(NullPointerException.class, + () -> EthernetTracker.createEthernetTrackerConfig(null)); + } + + @Test + public void testUpdateConfiguration() { + final NetworkCapabilities capabilities = new NetworkCapabilities.Builder().build(); + final LinkAddress linkAddr = new LinkAddress("192.0.2.2/25"); + final StaticIpConfiguration staticIpConfig = + new StaticIpConfiguration.Builder().setIpAddress(linkAddr).build(); + final IpConfiguration ipConfig = + new IpConfiguration.Builder().setStaticIpConfiguration(staticIpConfig).build(); + final INetworkInterfaceOutcomeReceiver listener = null; + + tracker.updateConfiguration(TEST_IFACE, ipConfig, capabilities, listener); + waitForIdle(); + + verify(mFactory).updateInterface( + eq(TEST_IFACE), eq(ipConfig), eq(capabilities), eq(listener)); + } + + @Test + public void testConnectNetworkCorrectlyCallsFactory() { + tracker.connectNetwork(TEST_IFACE, NULL_LISTENER); + waitForIdle(); + + verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(true /* up */), + eq(NULL_LISTENER)); + } + + @Test + public void testDisconnectNetworkCorrectlyCallsFactory() { + tracker.disconnectNetwork(TEST_IFACE, NULL_LISTENER); + waitForIdle(); + + verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(false /* up */), + eq(NULL_LISTENER)); + } + + @Test + public void testIsValidTestInterfaceIsFalseWhenTestInterfacesAreNotIncluded() { + final String validIfaceName = TEST_TAP_PREFIX + "123"; + tracker.setIncludeTestInterfaces(false); + waitForIdle(); + + final boolean isValidTestInterface = tracker.isValidTestInterface(validIfaceName); + + assertFalse(isValidTestInterface); + } + + @Test + public void testIsValidTestInterfaceIsFalseWhenTestInterfaceNameIsInvalid() { + final String invalidIfaceName = "123" + TEST_TAP_PREFIX; + tracker.setIncludeTestInterfaces(true); + waitForIdle(); + + final boolean isValidTestInterface = tracker.isValidTestInterface(invalidIfaceName); + + assertFalse(isValidTestInterface); + } + + @Test + public void testIsValidTestInterfaceIsTrueWhenTestInterfacesIncludedAndValidName() { + final String validIfaceName = TEST_TAP_PREFIX + "123"; + tracker.setIncludeTestInterfaces(true); + waitForIdle(); + + final boolean isValidTestInterface = tracker.isValidTestInterface(validIfaceName); + + assertTrue(isValidTestInterface); + } + + public static class EthernetStateListener extends IEthernetServiceListener.Stub { + @Override + public void onEthernetStateChanged(int state) { } + + @Override + public void onInterfaceStateChanged(String iface, int state, int role, + IpConfiguration configuration) { } + } + + @Test + public void testListenEthernetStateChange() throws Exception { + final String testIface = "testtap123"; + final String testHwAddr = "11:22:33:44:55:66"; + final InterfaceConfigurationParcel ifaceParcel = new InterfaceConfigurationParcel(); + ifaceParcel.ifName = testIface; + ifaceParcel.hwAddr = testHwAddr; + ifaceParcel.flags = new String[] {INetd.IF_STATE_UP}; + + tracker.setIncludeTestInterfaces(true); + waitForIdle(); + + when(mNetd.interfaceGetList()).thenReturn(new String[] {testIface}); + when(mNetd.interfaceGetCfg(eq(testIface))).thenReturn(ifaceParcel); + doReturn(new String[] {testIface}).when(mFactory).getAvailableInterfaces(anyBoolean()); + doReturn(EthernetManager.STATE_LINK_UP).when(mFactory).getInterfaceState(eq(testIface)); + + final EthernetStateListener listener = spy(new EthernetStateListener()); + tracker.addListener(listener, true /* canUseRestrictedNetworks */); + // Check default state. + waitForIdle(); + verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP), + anyInt(), any()); + verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_ENABLED)); + reset(listener); + + doReturn(EthernetManager.STATE_ABSENT).when(mFactory).getInterfaceState(eq(testIface)); + tracker.setEthernetEnabled(false); + waitForIdle(); + verify(mFactory).removeInterface(eq(testIface)); + verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_DISABLED)); + verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_ABSENT), + anyInt(), any()); + reset(listener); + + doReturn(EthernetManager.STATE_LINK_UP).when(mFactory).getInterfaceState(eq(testIface)); + tracker.setEthernetEnabled(true); + waitForIdle(); + verify(mFactory).addInterface(eq(testIface), eq(testHwAddr), any(), any()); + verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_ENABLED)); + verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP), + anyInt(), any()); + } +}