Move static utils to the static library
These classes used to live as a static utility in the
NetworkStack module, but that's not the right place for
them.
The point of this patch is to *not* require topics, as
any change in this space will very quickly spin out of
control and become unmanageable. As such, this starts
with creating equivalent classes in a single, easier to
manage change. Followup changes will migrate users of
the old classes to use these ones instead. Finally, the
old classes can be removed. This way, work can be
broken down into separate changes and be checked in
little by little, rather than one huge topic with many
changes doing everything in one go, which is unlikely
to be manageable.
There are no code changes from the originals, but a
number of reorganizations, most of them unavoidable.
• Vertical spacing (these classes fix it)
• Package names/imports are adjusted
• Added @hide on NetlinkMonitor
• Move all contents of RouteUtils to NetdUtils, because
all methods depend on INetd, and some of the targets
do not/cannot depend on netd-client
• Restrict the files used by the filegroup
net-utils-framework-wifi-common-srcs, since that
target does not necessarily provide all dependencies
to all its users.
• Don't move NetworkMonitorUtils, since it depends on
SdkLevel, and net-utils-framework-common-srcs is
using **/*.java. It would have been possible to list
explicitly all files actually necessary in this
filegroup, but NetworkMonitorUtils is actually only
used by the networking modules and not the framework.
Eventually it should move but it doesn't have to
be in this patch, which is complicated enough as
it is.
• RouteUtils is now empty, because some new methods
will be added to it soon and it is less expensive
to keep it empty than to remove it now and add it
again later.
• Merge NetdUtils, and unify the constants.
Some changes to satisfy checkstyle :
• Remove unused imports
• Reorder modifiers in InterfaceController
• Remove {} in IpNetworkMonitor
• Remove redundant public modifier in IpNetworkMonitor
• Add javadoc to a few methods
• Add whitespace around | in InterfaceParams
However, don't rename members in IpNeighborMonitor
like checkstyle would prefer because this would make
migration to this more involved.
Test: NetworkStaticLibsTests NetdStaticLibTests
Change-Id: I439121cba5d7ea95aa4d6c80ea25207c316880a0
diff --git a/staticlibs/client-libs/Android.bp b/staticlibs/client-libs/Android.bp
index 8c79d2a..5cc011e 100644
--- a/staticlibs/client-libs/Android.bp
+++ b/staticlibs/client-libs/Android.bp
@@ -16,6 +16,7 @@
"//frameworks/base/services:__subpackages__",
"//frameworks/base/packages:__subpackages__",
"//frameworks/libs/net/client-libs/tests:__subpackages__",
+ "//frameworks/libs/net/common:__subpackages__",
],
libs: ["androidx.annotation_annotation"],
static_libs: [
diff --git a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
index 1f31912..e78e336 100644
--- a/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
+++ b/staticlibs/client-libs/netd/com/android/net/module/util/NetdUtils.java
@@ -18,21 +18,39 @@
import static android.net.INetd.IF_STATE_DOWN;
import static android.net.INetd.IF_STATE_UP;
+import static android.net.RouteInfo.RTN_THROW;
+import static android.net.RouteInfo.RTN_UNICAST;
+import static android.net.RouteInfo.RTN_UNREACHABLE;
+import static android.system.OsConstants.EBUSY;
import android.net.INetd;
import android.net.InterfaceConfigurationParcel;
+import android.net.IpPrefix;
+import android.net.RouteInfo;
+import android.net.TetherConfigParcel;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
+import android.os.SystemClock;
+import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.util.ArrayList;
+import java.util.List;
/**
* Collection of utilities for netd.
*/
public class NetdUtils {
+ private static final String TAG = NetdUtils.class.getSimpleName();
+
+ /** Used to modify the specified route. */
+ public enum ModifyOperation {
+ ADD,
+ REMOVE,
+ }
+
/**
* Get InterfaceConfigurationParcel from netd.
*/
@@ -102,4 +120,142 @@
IF_STATE_DOWN /* add */);
setInterfaceConfig(netd, configParcel);
}
+
+ /** Start tethering. */
+ public static void tetherStart(final INetd netd, final boolean usingLegacyDnsProxy,
+ final String[] dhcpRange) throws RemoteException, ServiceSpecificException {
+ final TetherConfigParcel config = new TetherConfigParcel();
+ config.usingLegacyDnsProxy = usingLegacyDnsProxy;
+ config.dhcpRanges = dhcpRange;
+ netd.tetherStartWithConfiguration(config);
+ }
+
+ /** Setup interface for tethering. */
+ public static void tetherInterface(final INetd netd, final String iface, final IpPrefix dest)
+ throws RemoteException, ServiceSpecificException {
+ tetherInterface(netd, iface, dest, 20 /* maxAttempts */, 50 /* pollingIntervalMs */);
+ }
+
+ /** Setup interface with configurable retries for tethering. */
+ public static void tetherInterface(final INetd netd, final String iface, final IpPrefix dest,
+ int maxAttempts, int pollingIntervalMs)
+ throws RemoteException, ServiceSpecificException {
+ netd.tetherInterfaceAdd(iface);
+ networkAddInterface(netd, iface, maxAttempts, pollingIntervalMs);
+ List<RouteInfo> routes = new ArrayList<>();
+ routes.add(new RouteInfo(dest, null, iface, RTN_UNICAST));
+ addRoutesToLocalNetwork(netd, iface, routes);
+ }
+
+ /**
+ * Retry Netd#networkAddInterface for EBUSY error code.
+ * If the same interface (e.g., wlan0) is in client mode and then switches to tethered mode.
+ * There can be a race where puts the interface into the local network but interface is still
+ * in use in netd because the ConnectivityService thread hasn't processed the disconnect yet.
+ * See b/158269544 for detail.
+ */
+ private static void networkAddInterface(final INetd netd, final String iface,
+ int maxAttempts, int pollingIntervalMs)
+ throws ServiceSpecificException, RemoteException {
+ for (int i = 1; i <= maxAttempts; i++) {
+ try {
+ netd.networkAddInterface(INetd.LOCAL_NET_ID, iface);
+ return;
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == EBUSY && i < maxAttempts) {
+ SystemClock.sleep(pollingIntervalMs);
+ continue;
+ }
+
+ Log.e(TAG, "Retry Netd#networkAddInterface failure: " + e);
+ throw e;
+ }
+ }
+ }
+
+ /** Reset interface for tethering. */
+ public static void untetherInterface(final INetd netd, String iface)
+ throws RemoteException, ServiceSpecificException {
+ try {
+ netd.tetherInterfaceRemove(iface);
+ } finally {
+ netd.networkRemoveInterface(INetd.LOCAL_NET_ID, iface);
+ }
+ }
+
+ /** Add |routes| to local network. */
+ public static void addRoutesToLocalNetwork(final INetd netd, final String iface,
+ final List<RouteInfo> routes) {
+
+ for (RouteInfo route : routes) {
+ if (!route.isDefaultRoute()) {
+ modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID, route);
+ }
+ }
+
+ // IPv6 link local should be activated always.
+ modifyRoute(netd, ModifyOperation.ADD, INetd.LOCAL_NET_ID,
+ new RouteInfo(new IpPrefix("fe80::/64"), null, iface, RTN_UNICAST));
+ }
+
+ /** Remove routes from local network. */
+ public static int removeRoutesFromLocalNetwork(final INetd netd, final List<RouteInfo> routes) {
+ int failures = 0;
+
+ for (RouteInfo route : routes) {
+ try {
+ modifyRoute(netd, ModifyOperation.REMOVE, INetd.LOCAL_NET_ID, route);
+ } catch (IllegalStateException e) {
+ failures++;
+ }
+ }
+
+ return failures;
+ }
+
+ private static String findNextHop(final RouteInfo route) {
+ final String nextHop;
+ switch (route.getType()) {
+ case RTN_UNICAST:
+ if (route.hasGateway()) {
+ nextHop = route.getGateway().getHostAddress();
+ } else {
+ nextHop = INetd.NEXTHOP_NONE;
+ }
+ break;
+ case RTN_UNREACHABLE:
+ nextHop = INetd.NEXTHOP_UNREACHABLE;
+ break;
+ case RTN_THROW:
+ nextHop = INetd.NEXTHOP_THROW;
+ break;
+ default:
+ nextHop = INetd.NEXTHOP_NONE;
+ break;
+ }
+ return nextHop;
+ }
+
+ /** Add or remove |route|. */
+ public static void modifyRoute(final INetd netd, final ModifyOperation op, final int netId,
+ final RouteInfo route) {
+ final String ifName = route.getInterface();
+ final String dst = route.getDestination().toString();
+ final String nextHop = findNextHop(route);
+
+ try {
+ switch(op) {
+ case ADD:
+ netd.networkAddRoute(netId, ifName, dst, nextHop);
+ break;
+ case REMOVE:
+ netd.networkRemoveRoute(netId, ifName, dst, nextHop);
+ break;
+ default:
+ throw new IllegalStateException("Unsupported modify operation:" + op);
+ }
+ } catch (RemoteException | ServiceSpecificException e) {
+ throw new IllegalStateException(e);
+ }
+ }
}
diff --git a/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
index 306b1bb..dedda7e 100644
--- a/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
+++ b/staticlibs/client-libs/tests/unit/src/com/android/net/module/util/NetdUtilsTest.java
@@ -16,15 +16,31 @@
package com.android.net.module.util;
+import static android.net.INetd.LOCAL_NET_ID;
+import static android.system.OsConstants.EBUSY;
+
import static com.android.testutils.MiscAsserts.assertThrows;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.net.INetd;
import android.net.InterfaceConfigurationParcel;
+import android.net.IpPrefix;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -43,6 +59,7 @@
@Mock private INetd mNetd;
private static final String IFACE = "TEST_IFACE";
+ private static final IpPrefix TEST_IPPREFIX = new IpPrefix("192.168.42.1/24");
@Before
public void setUp() throws Exception {
@@ -99,5 +116,106 @@
assertThrows(IllegalStateException.class,
() -> NetdUtils.removeAndAddFlags(flags, "down" /* remove */, "u p" /* add */));
}
-}
+ private void setNetworkAddInterfaceOutcome(final Exception cause, int numLoops)
+ throws Exception {
+ // This cannot be an int because local variables referenced from a lambda expression must
+ // be final or effectively final.
+ final Counter myCounter = new Counter();
+ doAnswer((invocation) -> {
+ myCounter.count();
+ if (myCounter.isCounterReached(numLoops)) {
+ if (cause == null) return null;
+
+ throw cause;
+ }
+
+ throw new ServiceSpecificException(EBUSY);
+ }).when(mNetd).networkAddInterface(LOCAL_NET_ID, IFACE);
+ }
+
+ class Counter {
+ private int mValue = 0;
+
+ private void count() {
+ mValue++;
+ }
+
+ private boolean isCounterReached(int target) {
+ return mValue >= target;
+ }
+ }
+
+ @Test
+ public void testTetherInterfaceSuccessful() throws Exception {
+ // Expect #networkAddInterface successful at first tries.
+ verifyTetherInterfaceSucceeds(1);
+
+ // Expect #networkAddInterface successful after 10 tries.
+ verifyTetherInterfaceSucceeds(10);
+ }
+
+ private void runTetherInterfaceWithServiceSpecificException(int expectedTries,
+ int expectedCode) throws Exception {
+ setNetworkAddInterfaceOutcome(new ServiceSpecificException(expectedCode), expectedTries);
+
+ try {
+ NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX, 20, 0);
+ fail("Expect throw ServiceSpecificException");
+ } catch (ServiceSpecificException e) {
+ assertEquals(e.errorCode, expectedCode);
+ }
+
+ verifyNetworkAddInterfaceFails(expectedTries);
+ reset(mNetd);
+ }
+
+ private void runTetherInterfaceWithRemoteException(int expectedTries) throws Exception {
+ setNetworkAddInterfaceOutcome(new RemoteException(), expectedTries);
+
+ try {
+ NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX, 20, 0);
+ fail("Expect throw RemoteException");
+ } catch (RemoteException e) { }
+
+ verifyNetworkAddInterfaceFails(expectedTries);
+ reset(mNetd);
+ }
+
+ private void verifyNetworkAddInterfaceFails(int expectedTries) throws Exception {
+ verify(mNetd).tetherInterfaceAdd(IFACE);
+ verify(mNetd, times(expectedTries)).networkAddInterface(LOCAL_NET_ID, IFACE);
+ verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any());
+ verifyNoMoreInteractions(mNetd);
+ }
+
+ private void verifyTetherInterfaceSucceeds(int expectedTries) throws Exception {
+ setNetworkAddInterfaceOutcome(null, expectedTries);
+
+ NetdUtils.tetherInterface(mNetd, IFACE, TEST_IPPREFIX);
+ verify(mNetd).tetherInterfaceAdd(IFACE);
+ verify(mNetd, times(expectedTries)).networkAddInterface(LOCAL_NET_ID, IFACE);
+ verify(mNetd, times(2)).networkAddRoute(eq(LOCAL_NET_ID), eq(IFACE), any(), any());
+ verifyNoMoreInteractions(mNetd);
+ reset(mNetd);
+ }
+
+ @Test
+ public void testTetherInterfaceFailOnNetworkAddInterface() throws Exception {
+ // Test throwing ServiceSpecificException with EBUSY failure.
+ runTetherInterfaceWithServiceSpecificException(20, EBUSY);
+
+ // Test throwing ServiceSpecificException with unexpectedError.
+ final int unexpectedError = 999;
+ runTetherInterfaceWithServiceSpecificException(1, unexpectedError);
+
+ // Test throwing ServiceSpecificException with unexpectedError after 7 tries.
+ runTetherInterfaceWithServiceSpecificException(7, unexpectedError);
+
+ // Test throwing RemoteException.
+ runTetherInterfaceWithRemoteException(1);
+
+ // Test throwing RemoteException after 3 tries.
+ runTetherInterfaceWithRemoteException(3);
+ }
+}