Loading services/net/java/android/net/ip/IpManager.java +154 −40 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import android.net.dhcp.DhcpClient; import android.net.metrics.IpConnectivityLog; import android.net.metrics.IpManagerEvent; import android.net.util.MultinetworkPolicyTracker; import android.net.util.NetdService; import android.net.util.NetworkConstants; import android.net.util.SharedLog; import android.os.INetworkManagementService; Loading Loading @@ -66,9 +67,11 @@ import java.net.Inet6Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Objects; import java.util.List; import java.util.Set; import java.util.StringJoiner; import java.util.function.Predicate; Loading Loading @@ -423,6 +426,10 @@ public class IpManager extends StateMachine { } public boolean isValid() { if (ipAddresses.isEmpty()) { return false; } // For every IP address, there must be at least one prefix containing that address. for (LinkAddress addr : ipAddresses) { if (!any(directlyConnectedRoutes, (p) -> p.contains(addr.getAddress()))) { Loading Loading @@ -459,48 +466,60 @@ public class IpManager extends StateMachine { return true; } private static boolean isPrefixLengthCompliant(LinkAddress addr) { return (addr.getAddress() instanceof Inet4Address) || isCompliantIPv6PrefixLength(addr.getPrefixLength()); /** * @return true if the given list of addressess and routes satisfies provisioning for this * InitialConfiguration. LinkAddresses and RouteInfo objects are not compared with equality * because addresses and routes seen by Netlink will contain additional fields like flags, * interfaces, and so on. If this InitialConfiguration has no IP address specified, the * provisioning check always fails. * * If the given list of routes is null, only addresses are taken into considerations. */ public boolean isProvisionedBy(List<LinkAddress> addresses, List<RouteInfo> routes) { if (ipAddresses.isEmpty()) { return false; } private static boolean isPrefixLengthCompliant(IpPrefix prefix) { return (prefix.getAddress() instanceof Inet4Address) || isCompliantIPv6PrefixLength(prefix.getPrefixLength()); for (LinkAddress addr : ipAddresses) { if (!any(addresses, (addrSeen) -> addr.isSameAddressAs(addrSeen))) { return false; } private static boolean isCompliantIPv6PrefixLength(int prefixLength) { return (NetworkConstants.RFC6177_MIN_PREFIX_LENGTH <= prefixLength) && (prefixLength <= NetworkConstants.RFC7421_PREFIX_LENGTH); } private static boolean isIPv6DefaultRoute(IpPrefix prefix) { return prefix.getAddress().equals(Inet6Address.ANY); if (routes != null) { for (IpPrefix prefix : directlyConnectedRoutes) { if (!any(routes, (routeSeen) -> isDirectlyConnectedRoute(routeSeen, prefix))) { return false; } } private static boolean isIPv6GUA(LinkAddress addr) { return (addr.getAddress() instanceof Inet6Address) && addr.isGlobalPreferred(); } private static <T> boolean any(Iterable<T> coll, Predicate<T> fn) { for (T t : coll) { if (fn.test(t)) { return true; } private static boolean isDirectlyConnectedRoute(RouteInfo route, IpPrefix prefix) { return !route.hasGateway() && prefix.equals(route.getDestination()); } return false; private static boolean isPrefixLengthCompliant(LinkAddress addr) { return addr.isIPv4() || isCompliantIPv6PrefixLength(addr.getPrefixLength()); } private static <T> boolean all(Iterable<T> coll, Predicate<T> fn) { return !any(coll, not(fn)); private static boolean isPrefixLengthCompliant(IpPrefix prefix) { return prefix.isIPv4() || isCompliantIPv6PrefixLength(prefix.getPrefixLength()); } private static <T> Predicate<T> not(Predicate<T> fn) { return (t) -> !fn.test(t); private static boolean isCompliantIPv6PrefixLength(int prefixLength) { return (NetworkConstants.RFC6177_MIN_PREFIX_LENGTH <= prefixLength) && (prefixLength <= NetworkConstants.RFC7421_PREFIX_LENGTH); } private static <T> String join(String delimiter, Collection<T> coll) { return coll.stream().map(Object::toString).collect(Collectors.joining(delimiter)); private static boolean isIPv6DefaultRoute(IpPrefix prefix) { return prefix.getAddress().equals(Inet6Address.ANY); } private static boolean isIPv6GUA(LinkAddress addr) { return addr.isIPv6() && addr.isGlobalPreferred(); } } Loading Loading @@ -549,6 +568,7 @@ public class IpManager extends StateMachine { private final LocalLog mConnectivityPacketLog; private final MessageHandlingLogger mMsgStateLogger; private final IpConnectivityLog mMetricsLog = new IpConnectivityLog(); private final INetd mNetd; private NetworkInterface mNetworkInterface; Loading @@ -568,14 +588,22 @@ public class IpManager extends StateMachine { public IpManager(Context context, String ifName, Callback callback) { this(context, ifName, callback, INetworkManagementService.Stub.asInterface( ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE))); ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)), NetdService.getInstance()); } /** * An expanded constructor, useful for dependency injection. * TODO: migrate all test users to mock IpManager directly and remove this ctor. */ public IpManager(Context context, String ifName, Callback callback, INetworkManagementService nwService) { this(context, ifName, callback, nwService, NetdService.getInstance()); } @VisibleForTesting IpManager(Context context, String ifName, Callback callback, INetworkManagementService nwService, INetd netd) { super(IpManager.class.getSimpleName() + "." + ifName); mTag = getName(); Loading @@ -584,6 +612,7 @@ public class IpManager extends StateMachine { mClatInterfaceName = CLAT_PREFIX + ifName; mCallback = new LoggingCallbackWrapper(callback); mNwService = nwService; mNetd = netd; mLog = new SharedLog(MAX_LOG_RECORDS, mTag); mConnectivityPacketLog = new LocalLog(MAX_PACKET_RECORDS); Loading Loading @@ -886,10 +915,20 @@ public class IpManager extends StateMachine { } // For now: use WifiStateMachine's historical notion of provisioned. private static boolean isProvisioned(LinkProperties lp) { @VisibleForTesting static boolean isProvisioned(LinkProperties lp, InitialConfiguration config) { // For historical reasons, we should connect even if all we have is // an IPv4 address and nothing else. return lp.isProvisioned() || lp.hasIPv4Address(); if (lp.hasIPv4Address() || lp.isProvisioned()) { return true; } if (config == null) { return false; } // When an InitialConfiguration is specified, ignore any difference with previous // properties and instead check if properties observed match the desired properties. return config.isProvisionedBy(lp.getLinkAddresses(), lp.getRoutes()); } // TODO: Investigate folding all this into the existing static function Loading @@ -898,12 +937,11 @@ public class IpManager extends StateMachine { // object that is a correct and complete assessment of what changed, taking // account of the asymmetries described in the comments in this function. // Then switch to using it everywhere (IpReachabilityMonitor, etc.). private ProvisioningChange compareProvisioning( LinkProperties oldLp, LinkProperties newLp) { private ProvisioningChange compareProvisioning(LinkProperties oldLp, LinkProperties newLp) { ProvisioningChange delta; final boolean wasProvisioned = isProvisioned(oldLp); final boolean isProvisioned = isProvisioned(newLp); InitialConfiguration config = mConfiguration != null ? mConfiguration.mInitialConfig : null; final boolean wasProvisioned = isProvisioned(oldLp, config); final boolean isProvisioned = isProvisioned(newLp, config); if (!wasProvisioned && isProvisioned) { delta = ProvisioningChange.GAINED_PROVISIONING; Loading Loading @@ -1016,10 +1054,6 @@ public class IpManager extends StateMachine { return delta; } private boolean linkPropertiesUnchanged(LinkProperties newLp) { return Objects.equals(newLp, mLinkProperties); } private LinkProperties assembleLinkProperties() { // [1] Create a new LinkProperties object to populate. LinkProperties newLp = new LinkProperties(); Loading Loading @@ -1066,9 +1100,26 @@ public class IpManager extends StateMachine { newLp.setHttpProxy(mHttpProxy); } // [5] Add data from InitialConfiguration if (mConfiguration != null && mConfiguration.mInitialConfig != null) { InitialConfiguration config = mConfiguration.mInitialConfig; // Add InitialConfiguration routes and dns server addresses once all addresses // specified in the InitialConfiguration have been observed with Netlink. if (config.isProvisionedBy(newLp.getLinkAddresses(), null)) { for (IpPrefix prefix : config.directlyConnectedRoutes) { newLp.addRoute(new RouteInfo(prefix, null, mInterfaceName)); } } addAllReachableDnsServers(newLp, config.dnsServers); } final LinkProperties oldLp = mLinkProperties; if (VDBG) { Log.d(mTag, "newLp{" + newLp + "}"); Log.d(mTag, String.format("Netlink-seen LPs: %s, new LPs: %s; old LPs: %s", netlinkLinkProperties, newLp, oldLp)); } // TODO: also learn via netlink routes specified by an InitialConfiguration and specified // from a static IP v4 config instead of manually patching them in in steps [3] and [5]. return newLp; } Loading @@ -1087,7 +1138,7 @@ public class IpManager extends StateMachine { // Returns false if we have lost provisioning, true otherwise. private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) { final LinkProperties newLp = assembleLinkProperties(); if (linkPropertiesUnchanged(newLp)) { if (Objects.equals(newLp, mLinkProperties)) { return true; } final ProvisioningChange delta = setLinkProperties(newLp); Loading Loading @@ -1218,6 +1269,26 @@ public class IpManager extends StateMachine { return true; } private boolean applyInitialConfig(InitialConfiguration config) { if (mNetd == null) { logError("tried to add %s to %s but INetd was null", config, mInterfaceName); return false; } // TODO: also support specifying a static IPv4 configuration in InitialConfiguration. for (LinkAddress addr : findAll(config.ipAddresses, LinkAddress::isIPv6)) { try { mNetd.interfaceAddAddress( mInterfaceName, addr.getAddress().getHostAddress(), addr.getPrefixLength()); } catch (ServiceSpecificException | RemoteException e) { logError("failed to add %s to %s: %s", addr, mInterfaceName, e); return false; } } return true; } private boolean startIpReachabilityMonitor() { try { mIpReachabilityMonitor = new IpReachabilityMonitor( Loading Loading @@ -1446,6 +1517,14 @@ public class IpManager extends StateMachine { return; } InitialConfiguration config = mConfiguration.mInitialConfig; if ((config != null) && !applyInitialConfig(config)) { // TODO introduce a new IpManagerEvent constant to distinguish this error case. doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING); transitionTo(mStoppingState); return; } if (mConfiguration.mUsingIpReachabilityMonitor && !startIpReachabilityMonitor()) { doImmediateProvisioningFailure( IpManagerEvent.ERROR_STARTING_IPREACHABILITYMONITOR); Loading Loading @@ -1652,4 +1731,39 @@ public class IpManager extends StateMachine { receivedInState, processedInState); } } // TODO: extract out into CollectionUtils. static <T> boolean any(Iterable<T> coll, Predicate<T> fn) { for (T t : coll) { if (fn.test(t)) { return true; } } return false; } static <T> boolean all(Iterable<T> coll, Predicate<T> fn) { return !any(coll, not(fn)); } static <T> Predicate<T> not(Predicate<T> fn) { return (t) -> !fn.test(t); } static <T> String join(String delimiter, Collection<T> coll) { return coll.stream().map(Object::toString).collect(Collectors.joining(delimiter)); } static <T> T find(Iterable<T> coll, Predicate<T> fn) { for (T t: coll) { if (fn.test(t)) { return t; } } return null; } static <T> List<T> findAll(Collection<T> coll, Predicate<T> fn) { return coll.stream().filter(fn).collect(Collectors.toList()); } } tests/net/java/android/net/ip/IpManagerTest.java +206 −5 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ package android.net.ip; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.any; import static org.mockito.Mockito.eq; Loading @@ -32,8 +34,11 @@ import android.app.AlarmManager; import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.net.INetd; import android.net.IpPrefix; import android.net.LinkAddress; import android.net.LinkProperties; import android.net.RouteInfo; import android.net.ip.IpManager.Callback; import android.net.ip.IpManager.InitialConfiguration; import android.net.ip.IpManager.ProvisioningConfiguration; Loading @@ -45,16 +50,20 @@ import android.test.mock.MockContentResolver; import com.android.internal.util.test.FakeSettingsProvider; import com.android.internal.R; import com.android.server.net.BaseNetworkObserver; 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.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.util.Arrays; import java.util.List; import java.util.HashSet; import java.util.Set; Loading @@ -71,11 +80,14 @@ public class IpManagerTest { @Mock private Context mContext; @Mock private INetworkManagementService mNMService; @Mock private INetd mNetd; @Mock private Resources mResources; @Mock private Callback mCb; @Mock private AlarmManager mAlarm; private MockContentResolver mContentResolver; BaseNetworkObserver mObserver; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); Loading @@ -91,9 +103,13 @@ public class IpManagerTest { } private IpManager makeIpManager(String ifname) throws Exception { final IpManager ipm = new IpManager(mContext, ifname, mCb, mNMService); final IpManager ipm = new IpManager(mContext, ifname, mCb, mNMService, mNetd); verify(mNMService, timeout(100).times(1)).disableIpv6(ifname); verify(mNMService, timeout(100).times(1)).clearInterfaceAddresses(ifname); ArgumentCaptor<BaseNetworkObserver> arg = ArgumentCaptor.forClass(BaseNetworkObserver.class); verify(mNMService, times(1)).registerObserver(arg.capture()); mObserver = arg.getValue(); reset(mNMService); return ipm; } Loading Loading @@ -130,6 +146,134 @@ public class IpManagerTest { verify(mNMService, timeout(100).times(1)).clearInterfaceAddresses(iface); } @Test public void testProvisioningWithInitialConfiguration() throws Exception { final String iface = "test_wlan0"; final IpManager ipm = makeIpManager(iface); String[] addresses = { "fe80::a4be:f92:e1f7:22d1/64", "fe80::f04a:8f6:6a32:d756/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64" }; String[] prefixes = { "fe80::/64", "fd2c:4e57:8e3c::/64" }; ProvisioningConfiguration config = new ProvisioningConfiguration.Builder() .withoutIPv4() .withoutIpReachabilityMonitor() .withInitialConfiguration(conf(links(addresses), prefixes(prefixes), ips())) .build(); ipm.startProvisioning(config); verify(mCb, times(1)).setNeighborDiscoveryOffload(true); verify(mCb, timeout(100).times(1)).setFallbackMulticastFilter(false); verify(mCb, never()).onProvisioningFailure(any()); for (String addr : addresses) { String[] parts = addr.split("/"); verify(mNetd, timeout(100).times(1)) .interfaceAddAddress(iface, parts[0], Integer.parseInt(parts[1])); } final int lastAddr = addresses.length - 1; // Add N - 1 addresses for (int i = 0; i < lastAddr; i++) { mObserver.addressUpdated(iface, new LinkAddress(addresses[i])); verify(mCb, timeout(100).times(1)).onLinkPropertiesChange(any()); } // Add Nth address mObserver.addressUpdated(iface, new LinkAddress(addresses[lastAddr])); LinkProperties want = linkproperties(links(addresses), routes(prefixes)); want.setInterfaceName(iface); verify(mCb, timeout(100).times(1)).onProvisioningSuccess(eq(want)); ipm.stop(); verify(mNMService, timeout(100).times(1)).disableIpv6(iface); verify(mNMService, timeout(100).times(1)).clearInterfaceAddresses(iface); } @Test public void testIsProvisioned() throws Exception { InitialConfiguration empty = conf(links(), prefixes()); IsProvisionedTestCase[] testcases = { // nothing notProvisionedCase(links(), routes(), dns(), null), notProvisionedCase(links(), routes(), dns(), empty), // IPv4 provisionedCase(links("192.0.2.12/24"), routes(), dns(), empty), // IPv6 notProvisionedCase( links("fe80::a4be:f92:e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"), routes(), dns(), empty), notProvisionedCase( links("fe80::a4be:f92:e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"), routes("fe80::/64", "fd2c:4e57:8e3c::/64"), dns("fd00:1234:5678::1000"), empty), provisionedCase( links("2001:db8:dead:beef:f00::a0/64", "fe80::1/64"), routes("::/0"), dns("2001:db8:dead:beef:f00::02"), empty), // Initial configuration provisionedCase( links("fe80::e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"), routes("fe80::/64", "fd2c:4e57:8e3c::/64"), dns(), conf(links("fe80::e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"), prefixes( "fe80::/64", "fd2c:4e57:8e3c::/64"), ips())) }; for (IsProvisionedTestCase testcase : testcases) { if (IpManager.isProvisioned(testcase.lp, testcase.config) != testcase.isProvisioned) { fail(testcase.errorMessage()); } } } static class IsProvisionedTestCase { boolean isProvisioned; LinkProperties lp; InitialConfiguration config; String errorMessage() { return String.format("expected %s with config %s to be %s, but was %s", lp, config, provisioned(isProvisioned), provisioned(!isProvisioned)); } static String provisioned(boolean isProvisioned) { return isProvisioned ? "provisioned" : "not provisioned"; } } static IsProvisionedTestCase provisionedCase(Set<LinkAddress> lpAddrs, Set<RouteInfo> lpRoutes, Set<InetAddress> lpDns, InitialConfiguration config) { return provisioningTest(true, lpAddrs, lpRoutes, lpDns, config); } static IsProvisionedTestCase notProvisionedCase(Set<LinkAddress> lpAddrs, Set<RouteInfo> lpRoutes, Set<InetAddress> lpDns, InitialConfiguration config) { return provisioningTest(false, lpAddrs, lpRoutes, lpDns, config); } static IsProvisionedTestCase provisioningTest(boolean isProvisioned, Set<LinkAddress> lpAddrs, Set<RouteInfo> lpRoutes, Set<InetAddress> lpDns, InitialConfiguration config) { IsProvisionedTestCase testcase = new IsProvisionedTestCase(); testcase.isProvisioned = isProvisioned; testcase.lp = new LinkProperties(); testcase.lp.setLinkAddresses(lpAddrs); for (RouteInfo route : lpRoutes) { testcase.lp.addRoute(route); } for (InetAddress dns : lpDns) { testcase.lp.addDnsServer(dns); } testcase.config = config; return testcase; } @Test public void testInitialConfigurations() throws Exception { InitialConfigurationTestCase[] testcases = { Loading @@ -152,6 +296,7 @@ public class IpManagerTest { prefixes("fd00:1234:5678::/48"), dns("fd00:1234:5678::1000")), invalidConf("empty configuration", links(), prefixes(), dns()), invalidConf("v4 addr and dns not in any prefix", links("192.0.2.12/24"), prefixes("198.51.100.0/24"), dns("192.0.2.2")), invalidConf("v4 addr not in any prefix", Loading Loading @@ -189,11 +334,10 @@ public class IpManagerTest { return String.format("%s: expected configuration %s to be %s, but was %s", descr, config, validString(isValid), validString(!isValid)); } } static String validString(boolean isValid) { return isValid ? VALID : INVALID; } } static InitialConfigurationTestCase validConf(String descr, Set<LinkAddress> links, Set<IpPrefix> prefixes, Set<InetAddress> dns) { Loading @@ -214,6 +358,19 @@ public class IpManagerTest { return testcase; } static LinkProperties linkproperties(Set<LinkAddress> addresses, Set<RouteInfo> routes) { LinkProperties lp = new LinkProperties(); lp.setLinkAddresses(addresses); for (RouteInfo route : routes) { lp.addRoute(route); } return lp; } static InitialConfiguration conf(Set<LinkAddress> links, Set<IpPrefix> prefixes) { return conf(links, prefixes, new HashSet<>()); } static InitialConfiguration conf( Set<LinkAddress> links, Set<IpPrefix> prefixes, Set<InetAddress> dns) { InitialConfiguration conf = new InitialConfiguration(); Loading @@ -223,6 +380,10 @@ public class IpManagerTest { return conf; } static Set<RouteInfo> routes(String... routes) { return mapIntoSet(routes, (r) -> new RouteInfo(new IpPrefix(r))); } static Set<IpPrefix> prefixes(String... prefixes) { return mapIntoSet(prefixes, IpPrefix::new); } Loading Loading @@ -254,4 +415,44 @@ public class IpManagerTest { interface Fn<A,B> { B call(A a) throws Exception; } @Test public void testAll() { List<String> list1 = Arrays.asList(); List<String> list2 = Arrays.asList("foo"); List<String> list3 = Arrays.asList("bar", "baz"); List<String> list4 = Arrays.asList("foo", "bar", "baz"); assertTrue(IpManager.all(list1, (x) -> false)); assertFalse(IpManager.all(list2, (x) -> false)); assertTrue(IpManager.all(list3, (x) -> true)); assertTrue(IpManager.all(list2, (x) -> x.charAt(0) == 'f')); assertFalse(IpManager.all(list4, (x) -> x.charAt(0) == 'f')); } @Test public void testAny() { List<String> list1 = Arrays.asList(); List<String> list2 = Arrays.asList("foo"); List<String> list3 = Arrays.asList("bar", "baz"); List<String> list4 = Arrays.asList("foo", "bar", "baz"); assertFalse(IpManager.any(list1, (x) -> true)); assertTrue(IpManager.any(list2, (x) -> true)); assertTrue(IpManager.any(list2, (x) -> x.charAt(0) == 'f')); assertFalse(IpManager.any(list3, (x) -> x.charAt(0) == 'f')); assertTrue(IpManager.any(list4, (x) -> x.charAt(0) == 'f')); } @Test public void testFindAll() { List<String> list1 = Arrays.asList(); List<String> list2 = Arrays.asList("foo"); List<String> list3 = Arrays.asList("foo", "bar", "baz"); assertEquals(list1, IpManager.findAll(list1, (x) -> true)); assertEquals(list1, IpManager.findAll(list3, (x) -> false)); assertEquals(list3, IpManager.findAll(list3, (x) -> true)); assertEquals(list2, IpManager.findAll(list3, (x) -> x.charAt(0) == 'f')); } } Loading
services/net/java/android/net/ip/IpManager.java +154 −40 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import android.net.dhcp.DhcpClient; import android.net.metrics.IpConnectivityLog; import android.net.metrics.IpManagerEvent; import android.net.util.MultinetworkPolicyTracker; import android.net.util.NetdService; import android.net.util.NetworkConstants; import android.net.util.SharedLog; import android.os.INetworkManagementService; Loading Loading @@ -66,9 +67,11 @@ import java.net.Inet6Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Objects; import java.util.List; import java.util.Set; import java.util.StringJoiner; import java.util.function.Predicate; Loading Loading @@ -423,6 +426,10 @@ public class IpManager extends StateMachine { } public boolean isValid() { if (ipAddresses.isEmpty()) { return false; } // For every IP address, there must be at least one prefix containing that address. for (LinkAddress addr : ipAddresses) { if (!any(directlyConnectedRoutes, (p) -> p.contains(addr.getAddress()))) { Loading Loading @@ -459,48 +466,60 @@ public class IpManager extends StateMachine { return true; } private static boolean isPrefixLengthCompliant(LinkAddress addr) { return (addr.getAddress() instanceof Inet4Address) || isCompliantIPv6PrefixLength(addr.getPrefixLength()); /** * @return true if the given list of addressess and routes satisfies provisioning for this * InitialConfiguration. LinkAddresses and RouteInfo objects are not compared with equality * because addresses and routes seen by Netlink will contain additional fields like flags, * interfaces, and so on. If this InitialConfiguration has no IP address specified, the * provisioning check always fails. * * If the given list of routes is null, only addresses are taken into considerations. */ public boolean isProvisionedBy(List<LinkAddress> addresses, List<RouteInfo> routes) { if (ipAddresses.isEmpty()) { return false; } private static boolean isPrefixLengthCompliant(IpPrefix prefix) { return (prefix.getAddress() instanceof Inet4Address) || isCompliantIPv6PrefixLength(prefix.getPrefixLength()); for (LinkAddress addr : ipAddresses) { if (!any(addresses, (addrSeen) -> addr.isSameAddressAs(addrSeen))) { return false; } private static boolean isCompliantIPv6PrefixLength(int prefixLength) { return (NetworkConstants.RFC6177_MIN_PREFIX_LENGTH <= prefixLength) && (prefixLength <= NetworkConstants.RFC7421_PREFIX_LENGTH); } private static boolean isIPv6DefaultRoute(IpPrefix prefix) { return prefix.getAddress().equals(Inet6Address.ANY); if (routes != null) { for (IpPrefix prefix : directlyConnectedRoutes) { if (!any(routes, (routeSeen) -> isDirectlyConnectedRoute(routeSeen, prefix))) { return false; } } private static boolean isIPv6GUA(LinkAddress addr) { return (addr.getAddress() instanceof Inet6Address) && addr.isGlobalPreferred(); } private static <T> boolean any(Iterable<T> coll, Predicate<T> fn) { for (T t : coll) { if (fn.test(t)) { return true; } private static boolean isDirectlyConnectedRoute(RouteInfo route, IpPrefix prefix) { return !route.hasGateway() && prefix.equals(route.getDestination()); } return false; private static boolean isPrefixLengthCompliant(LinkAddress addr) { return addr.isIPv4() || isCompliantIPv6PrefixLength(addr.getPrefixLength()); } private static <T> boolean all(Iterable<T> coll, Predicate<T> fn) { return !any(coll, not(fn)); private static boolean isPrefixLengthCompliant(IpPrefix prefix) { return prefix.isIPv4() || isCompliantIPv6PrefixLength(prefix.getPrefixLength()); } private static <T> Predicate<T> not(Predicate<T> fn) { return (t) -> !fn.test(t); private static boolean isCompliantIPv6PrefixLength(int prefixLength) { return (NetworkConstants.RFC6177_MIN_PREFIX_LENGTH <= prefixLength) && (prefixLength <= NetworkConstants.RFC7421_PREFIX_LENGTH); } private static <T> String join(String delimiter, Collection<T> coll) { return coll.stream().map(Object::toString).collect(Collectors.joining(delimiter)); private static boolean isIPv6DefaultRoute(IpPrefix prefix) { return prefix.getAddress().equals(Inet6Address.ANY); } private static boolean isIPv6GUA(LinkAddress addr) { return addr.isIPv6() && addr.isGlobalPreferred(); } } Loading Loading @@ -549,6 +568,7 @@ public class IpManager extends StateMachine { private final LocalLog mConnectivityPacketLog; private final MessageHandlingLogger mMsgStateLogger; private final IpConnectivityLog mMetricsLog = new IpConnectivityLog(); private final INetd mNetd; private NetworkInterface mNetworkInterface; Loading @@ -568,14 +588,22 @@ public class IpManager extends StateMachine { public IpManager(Context context, String ifName, Callback callback) { this(context, ifName, callback, INetworkManagementService.Stub.asInterface( ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE))); ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)), NetdService.getInstance()); } /** * An expanded constructor, useful for dependency injection. * TODO: migrate all test users to mock IpManager directly and remove this ctor. */ public IpManager(Context context, String ifName, Callback callback, INetworkManagementService nwService) { this(context, ifName, callback, nwService, NetdService.getInstance()); } @VisibleForTesting IpManager(Context context, String ifName, Callback callback, INetworkManagementService nwService, INetd netd) { super(IpManager.class.getSimpleName() + "." + ifName); mTag = getName(); Loading @@ -584,6 +612,7 @@ public class IpManager extends StateMachine { mClatInterfaceName = CLAT_PREFIX + ifName; mCallback = new LoggingCallbackWrapper(callback); mNwService = nwService; mNetd = netd; mLog = new SharedLog(MAX_LOG_RECORDS, mTag); mConnectivityPacketLog = new LocalLog(MAX_PACKET_RECORDS); Loading Loading @@ -886,10 +915,20 @@ public class IpManager extends StateMachine { } // For now: use WifiStateMachine's historical notion of provisioned. private static boolean isProvisioned(LinkProperties lp) { @VisibleForTesting static boolean isProvisioned(LinkProperties lp, InitialConfiguration config) { // For historical reasons, we should connect even if all we have is // an IPv4 address and nothing else. return lp.isProvisioned() || lp.hasIPv4Address(); if (lp.hasIPv4Address() || lp.isProvisioned()) { return true; } if (config == null) { return false; } // When an InitialConfiguration is specified, ignore any difference with previous // properties and instead check if properties observed match the desired properties. return config.isProvisionedBy(lp.getLinkAddresses(), lp.getRoutes()); } // TODO: Investigate folding all this into the existing static function Loading @@ -898,12 +937,11 @@ public class IpManager extends StateMachine { // object that is a correct and complete assessment of what changed, taking // account of the asymmetries described in the comments in this function. // Then switch to using it everywhere (IpReachabilityMonitor, etc.). private ProvisioningChange compareProvisioning( LinkProperties oldLp, LinkProperties newLp) { private ProvisioningChange compareProvisioning(LinkProperties oldLp, LinkProperties newLp) { ProvisioningChange delta; final boolean wasProvisioned = isProvisioned(oldLp); final boolean isProvisioned = isProvisioned(newLp); InitialConfiguration config = mConfiguration != null ? mConfiguration.mInitialConfig : null; final boolean wasProvisioned = isProvisioned(oldLp, config); final boolean isProvisioned = isProvisioned(newLp, config); if (!wasProvisioned && isProvisioned) { delta = ProvisioningChange.GAINED_PROVISIONING; Loading Loading @@ -1016,10 +1054,6 @@ public class IpManager extends StateMachine { return delta; } private boolean linkPropertiesUnchanged(LinkProperties newLp) { return Objects.equals(newLp, mLinkProperties); } private LinkProperties assembleLinkProperties() { // [1] Create a new LinkProperties object to populate. LinkProperties newLp = new LinkProperties(); Loading Loading @@ -1066,9 +1100,26 @@ public class IpManager extends StateMachine { newLp.setHttpProxy(mHttpProxy); } // [5] Add data from InitialConfiguration if (mConfiguration != null && mConfiguration.mInitialConfig != null) { InitialConfiguration config = mConfiguration.mInitialConfig; // Add InitialConfiguration routes and dns server addresses once all addresses // specified in the InitialConfiguration have been observed with Netlink. if (config.isProvisionedBy(newLp.getLinkAddresses(), null)) { for (IpPrefix prefix : config.directlyConnectedRoutes) { newLp.addRoute(new RouteInfo(prefix, null, mInterfaceName)); } } addAllReachableDnsServers(newLp, config.dnsServers); } final LinkProperties oldLp = mLinkProperties; if (VDBG) { Log.d(mTag, "newLp{" + newLp + "}"); Log.d(mTag, String.format("Netlink-seen LPs: %s, new LPs: %s; old LPs: %s", netlinkLinkProperties, newLp, oldLp)); } // TODO: also learn via netlink routes specified by an InitialConfiguration and specified // from a static IP v4 config instead of manually patching them in in steps [3] and [5]. return newLp; } Loading @@ -1087,7 +1138,7 @@ public class IpManager extends StateMachine { // Returns false if we have lost provisioning, true otherwise. private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) { final LinkProperties newLp = assembleLinkProperties(); if (linkPropertiesUnchanged(newLp)) { if (Objects.equals(newLp, mLinkProperties)) { return true; } final ProvisioningChange delta = setLinkProperties(newLp); Loading Loading @@ -1218,6 +1269,26 @@ public class IpManager extends StateMachine { return true; } private boolean applyInitialConfig(InitialConfiguration config) { if (mNetd == null) { logError("tried to add %s to %s but INetd was null", config, mInterfaceName); return false; } // TODO: also support specifying a static IPv4 configuration in InitialConfiguration. for (LinkAddress addr : findAll(config.ipAddresses, LinkAddress::isIPv6)) { try { mNetd.interfaceAddAddress( mInterfaceName, addr.getAddress().getHostAddress(), addr.getPrefixLength()); } catch (ServiceSpecificException | RemoteException e) { logError("failed to add %s to %s: %s", addr, mInterfaceName, e); return false; } } return true; } private boolean startIpReachabilityMonitor() { try { mIpReachabilityMonitor = new IpReachabilityMonitor( Loading Loading @@ -1446,6 +1517,14 @@ public class IpManager extends StateMachine { return; } InitialConfiguration config = mConfiguration.mInitialConfig; if ((config != null) && !applyInitialConfig(config)) { // TODO introduce a new IpManagerEvent constant to distinguish this error case. doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING); transitionTo(mStoppingState); return; } if (mConfiguration.mUsingIpReachabilityMonitor && !startIpReachabilityMonitor()) { doImmediateProvisioningFailure( IpManagerEvent.ERROR_STARTING_IPREACHABILITYMONITOR); Loading Loading @@ -1652,4 +1731,39 @@ public class IpManager extends StateMachine { receivedInState, processedInState); } } // TODO: extract out into CollectionUtils. static <T> boolean any(Iterable<T> coll, Predicate<T> fn) { for (T t : coll) { if (fn.test(t)) { return true; } } return false; } static <T> boolean all(Iterable<T> coll, Predicate<T> fn) { return !any(coll, not(fn)); } static <T> Predicate<T> not(Predicate<T> fn) { return (t) -> !fn.test(t); } static <T> String join(String delimiter, Collection<T> coll) { return coll.stream().map(Object::toString).collect(Collectors.joining(delimiter)); } static <T> T find(Iterable<T> coll, Predicate<T> fn) { for (T t: coll) { if (fn.test(t)) { return t; } } return null; } static <T> List<T> findAll(Collection<T> coll, Predicate<T> fn) { return coll.stream().filter(fn).collect(Collectors.toList()); } }
tests/net/java/android/net/ip/IpManagerTest.java +206 −5 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ package android.net.ip; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.any; import static org.mockito.Mockito.eq; Loading @@ -32,8 +34,11 @@ import android.app.AlarmManager; import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.net.INetd; import android.net.IpPrefix; import android.net.LinkAddress; import android.net.LinkProperties; import android.net.RouteInfo; import android.net.ip.IpManager.Callback; import android.net.ip.IpManager.InitialConfiguration; import android.net.ip.IpManager.ProvisioningConfiguration; Loading @@ -45,16 +50,20 @@ import android.test.mock.MockContentResolver; import com.android.internal.util.test.FakeSettingsProvider; import com.android.internal.R; import com.android.server.net.BaseNetworkObserver; 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.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.util.Arrays; import java.util.List; import java.util.HashSet; import java.util.Set; Loading @@ -71,11 +80,14 @@ public class IpManagerTest { @Mock private Context mContext; @Mock private INetworkManagementService mNMService; @Mock private INetd mNetd; @Mock private Resources mResources; @Mock private Callback mCb; @Mock private AlarmManager mAlarm; private MockContentResolver mContentResolver; BaseNetworkObserver mObserver; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); Loading @@ -91,9 +103,13 @@ public class IpManagerTest { } private IpManager makeIpManager(String ifname) throws Exception { final IpManager ipm = new IpManager(mContext, ifname, mCb, mNMService); final IpManager ipm = new IpManager(mContext, ifname, mCb, mNMService, mNetd); verify(mNMService, timeout(100).times(1)).disableIpv6(ifname); verify(mNMService, timeout(100).times(1)).clearInterfaceAddresses(ifname); ArgumentCaptor<BaseNetworkObserver> arg = ArgumentCaptor.forClass(BaseNetworkObserver.class); verify(mNMService, times(1)).registerObserver(arg.capture()); mObserver = arg.getValue(); reset(mNMService); return ipm; } Loading Loading @@ -130,6 +146,134 @@ public class IpManagerTest { verify(mNMService, timeout(100).times(1)).clearInterfaceAddresses(iface); } @Test public void testProvisioningWithInitialConfiguration() throws Exception { final String iface = "test_wlan0"; final IpManager ipm = makeIpManager(iface); String[] addresses = { "fe80::a4be:f92:e1f7:22d1/64", "fe80::f04a:8f6:6a32:d756/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64" }; String[] prefixes = { "fe80::/64", "fd2c:4e57:8e3c::/64" }; ProvisioningConfiguration config = new ProvisioningConfiguration.Builder() .withoutIPv4() .withoutIpReachabilityMonitor() .withInitialConfiguration(conf(links(addresses), prefixes(prefixes), ips())) .build(); ipm.startProvisioning(config); verify(mCb, times(1)).setNeighborDiscoveryOffload(true); verify(mCb, timeout(100).times(1)).setFallbackMulticastFilter(false); verify(mCb, never()).onProvisioningFailure(any()); for (String addr : addresses) { String[] parts = addr.split("/"); verify(mNetd, timeout(100).times(1)) .interfaceAddAddress(iface, parts[0], Integer.parseInt(parts[1])); } final int lastAddr = addresses.length - 1; // Add N - 1 addresses for (int i = 0; i < lastAddr; i++) { mObserver.addressUpdated(iface, new LinkAddress(addresses[i])); verify(mCb, timeout(100).times(1)).onLinkPropertiesChange(any()); } // Add Nth address mObserver.addressUpdated(iface, new LinkAddress(addresses[lastAddr])); LinkProperties want = linkproperties(links(addresses), routes(prefixes)); want.setInterfaceName(iface); verify(mCb, timeout(100).times(1)).onProvisioningSuccess(eq(want)); ipm.stop(); verify(mNMService, timeout(100).times(1)).disableIpv6(iface); verify(mNMService, timeout(100).times(1)).clearInterfaceAddresses(iface); } @Test public void testIsProvisioned() throws Exception { InitialConfiguration empty = conf(links(), prefixes()); IsProvisionedTestCase[] testcases = { // nothing notProvisionedCase(links(), routes(), dns(), null), notProvisionedCase(links(), routes(), dns(), empty), // IPv4 provisionedCase(links("192.0.2.12/24"), routes(), dns(), empty), // IPv6 notProvisionedCase( links("fe80::a4be:f92:e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"), routes(), dns(), empty), notProvisionedCase( links("fe80::a4be:f92:e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"), routes("fe80::/64", "fd2c:4e57:8e3c::/64"), dns("fd00:1234:5678::1000"), empty), provisionedCase( links("2001:db8:dead:beef:f00::a0/64", "fe80::1/64"), routes("::/0"), dns("2001:db8:dead:beef:f00::02"), empty), // Initial configuration provisionedCase( links("fe80::e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"), routes("fe80::/64", "fd2c:4e57:8e3c::/64"), dns(), conf(links("fe80::e1f7:22d1/64", "fd2c:4e57:8e3c:0:548d:2db2:4fcf:ef75/64"), prefixes( "fe80::/64", "fd2c:4e57:8e3c::/64"), ips())) }; for (IsProvisionedTestCase testcase : testcases) { if (IpManager.isProvisioned(testcase.lp, testcase.config) != testcase.isProvisioned) { fail(testcase.errorMessage()); } } } static class IsProvisionedTestCase { boolean isProvisioned; LinkProperties lp; InitialConfiguration config; String errorMessage() { return String.format("expected %s with config %s to be %s, but was %s", lp, config, provisioned(isProvisioned), provisioned(!isProvisioned)); } static String provisioned(boolean isProvisioned) { return isProvisioned ? "provisioned" : "not provisioned"; } } static IsProvisionedTestCase provisionedCase(Set<LinkAddress> lpAddrs, Set<RouteInfo> lpRoutes, Set<InetAddress> lpDns, InitialConfiguration config) { return provisioningTest(true, lpAddrs, lpRoutes, lpDns, config); } static IsProvisionedTestCase notProvisionedCase(Set<LinkAddress> lpAddrs, Set<RouteInfo> lpRoutes, Set<InetAddress> lpDns, InitialConfiguration config) { return provisioningTest(false, lpAddrs, lpRoutes, lpDns, config); } static IsProvisionedTestCase provisioningTest(boolean isProvisioned, Set<LinkAddress> lpAddrs, Set<RouteInfo> lpRoutes, Set<InetAddress> lpDns, InitialConfiguration config) { IsProvisionedTestCase testcase = new IsProvisionedTestCase(); testcase.isProvisioned = isProvisioned; testcase.lp = new LinkProperties(); testcase.lp.setLinkAddresses(lpAddrs); for (RouteInfo route : lpRoutes) { testcase.lp.addRoute(route); } for (InetAddress dns : lpDns) { testcase.lp.addDnsServer(dns); } testcase.config = config; return testcase; } @Test public void testInitialConfigurations() throws Exception { InitialConfigurationTestCase[] testcases = { Loading @@ -152,6 +296,7 @@ public class IpManagerTest { prefixes("fd00:1234:5678::/48"), dns("fd00:1234:5678::1000")), invalidConf("empty configuration", links(), prefixes(), dns()), invalidConf("v4 addr and dns not in any prefix", links("192.0.2.12/24"), prefixes("198.51.100.0/24"), dns("192.0.2.2")), invalidConf("v4 addr not in any prefix", Loading Loading @@ -189,11 +334,10 @@ public class IpManagerTest { return String.format("%s: expected configuration %s to be %s, but was %s", descr, config, validString(isValid), validString(!isValid)); } } static String validString(boolean isValid) { return isValid ? VALID : INVALID; } } static InitialConfigurationTestCase validConf(String descr, Set<LinkAddress> links, Set<IpPrefix> prefixes, Set<InetAddress> dns) { Loading @@ -214,6 +358,19 @@ public class IpManagerTest { return testcase; } static LinkProperties linkproperties(Set<LinkAddress> addresses, Set<RouteInfo> routes) { LinkProperties lp = new LinkProperties(); lp.setLinkAddresses(addresses); for (RouteInfo route : routes) { lp.addRoute(route); } return lp; } static InitialConfiguration conf(Set<LinkAddress> links, Set<IpPrefix> prefixes) { return conf(links, prefixes, new HashSet<>()); } static InitialConfiguration conf( Set<LinkAddress> links, Set<IpPrefix> prefixes, Set<InetAddress> dns) { InitialConfiguration conf = new InitialConfiguration(); Loading @@ -223,6 +380,10 @@ public class IpManagerTest { return conf; } static Set<RouteInfo> routes(String... routes) { return mapIntoSet(routes, (r) -> new RouteInfo(new IpPrefix(r))); } static Set<IpPrefix> prefixes(String... prefixes) { return mapIntoSet(prefixes, IpPrefix::new); } Loading Loading @@ -254,4 +415,44 @@ public class IpManagerTest { interface Fn<A,B> { B call(A a) throws Exception; } @Test public void testAll() { List<String> list1 = Arrays.asList(); List<String> list2 = Arrays.asList("foo"); List<String> list3 = Arrays.asList("bar", "baz"); List<String> list4 = Arrays.asList("foo", "bar", "baz"); assertTrue(IpManager.all(list1, (x) -> false)); assertFalse(IpManager.all(list2, (x) -> false)); assertTrue(IpManager.all(list3, (x) -> true)); assertTrue(IpManager.all(list2, (x) -> x.charAt(0) == 'f')); assertFalse(IpManager.all(list4, (x) -> x.charAt(0) == 'f')); } @Test public void testAny() { List<String> list1 = Arrays.asList(); List<String> list2 = Arrays.asList("foo"); List<String> list3 = Arrays.asList("bar", "baz"); List<String> list4 = Arrays.asList("foo", "bar", "baz"); assertFalse(IpManager.any(list1, (x) -> true)); assertTrue(IpManager.any(list2, (x) -> true)); assertTrue(IpManager.any(list2, (x) -> x.charAt(0) == 'f')); assertFalse(IpManager.any(list3, (x) -> x.charAt(0) == 'f')); assertTrue(IpManager.any(list4, (x) -> x.charAt(0) == 'f')); } @Test public void testFindAll() { List<String> list1 = Arrays.asList(); List<String> list2 = Arrays.asList("foo"); List<String> list3 = Arrays.asList("foo", "bar", "baz"); assertEquals(list1, IpManager.findAll(list1, (x) -> true)); assertEquals(list1, IpManager.findAll(list3, (x) -> false)); assertEquals(list3, IpManager.findAll(list3, (x) -> true)); assertEquals(list2, IpManager.findAll(list3, (x) -> x.charAt(0) == 'f')); } }