Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2757fcf3 authored by Hugo Benichi's avatar Hugo Benichi
Browse files

IpManager: define InitialConfiguration

This patch adds a InitialConfiguration class to IpManager for specifying
IP information in IpManager ProvisioningConfiguration at IpManager
startup.

At the moment this InitialConfiguration is not used, but is validated in
startProvsiioning if ProvisioningConfiguration includes one. It will be
integrated into IpManager IP provisioning logic in follow-up patches.

This patch also includes an example of data driven unit tests using a
table of test case. The highlights of this methodology are:
  1) easy extensibility for new test case,
  2) rich and informative error messages,
Unfortunately Java support for inlined data structure literals is poor
and some companion static methods for data generation are required for
enabling this methodology.

Bug: 62988545
Test: added new test in FrameworksNetTests,
      $ runtest frameworks-net
      $ runtest frameworks-wifi

Change-Id: I060b02603af7d73a6407df89344bf0c000574af2
parent 08246c0a
Loading
Loading
Loading
Loading
+10 −10
Original line number Diff line number Diff line
@@ -16,6 +16,15 @@

package android.net;

import static android.system.OsConstants.IFA_F_DADFAILED;
import static android.system.OsConstants.IFA_F_DEPRECATED;
import static android.system.OsConstants.IFA_F_OPTIMISTIC;
import static android.system.OsConstants.IFA_F_TENTATIVE;
import static android.system.OsConstants.RT_SCOPE_HOST;
import static android.system.OsConstants.RT_SCOPE_LINK;
import static android.system.OsConstants.RT_SCOPE_SITE;
import static android.system.OsConstants.RT_SCOPE_UNIVERSE;

import android.os.Parcel;
import android.os.Parcelable;
import android.util.Pair;
@@ -26,15 +35,6 @@ import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.UnknownHostException;

import static android.system.OsConstants.IFA_F_DADFAILED;
import static android.system.OsConstants.IFA_F_DEPRECATED;
import static android.system.OsConstants.IFA_F_OPTIMISTIC;
import static android.system.OsConstants.IFA_F_TENTATIVE;
import static android.system.OsConstants.RT_SCOPE_HOST;
import static android.system.OsConstants.RT_SCOPE_LINK;
import static android.system.OsConstants.RT_SCOPE_SITE;
import static android.system.OsConstants.RT_SCOPE_UNIVERSE;

/**
 * Identifies an IP address on a network link.
 *
@@ -101,7 +101,7 @@ public class LinkAddress implements Parcelable {
     * Per RFC 4193 section 8, fc00::/7 identifies these addresses.
     */
    private boolean isIPv6ULA() {
        if (address != null && address instanceof Inet6Address) {
        if (address instanceof Inet6Address) {
            byte[] bytes = address.getAddress();
            return ((bytes[0] & (byte)0xfe) == (byte)0xfc);
        }
+2 −0
Original line number Diff line number Diff line
@@ -39,10 +39,12 @@ public final class IpManagerEvent implements Parcelable {
    public static final int ERROR_STARTING_IPV4                   = 4;
    public static final int ERROR_STARTING_IPV6                   = 5;
    public static final int ERROR_STARTING_IPREACHABILITYMONITOR  = 6;
    public static final int ERROR_INVALID_PROVISIONING            = 7;

    @IntDef(value = {
            PROVISIONING_OK, PROVISIONING_FAIL, COMPLETE_LIFECYCLE,
            ERROR_STARTING_IPV4, ERROR_STARTING_IPV6, ERROR_STARTING_IPREACHABILITYMONITOR,
            ERROR_INVALID_PROVISIONING,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface EventType {}
+137 −4
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.content.Context;
import android.net.DhcpResults;
import android.net.INetd;
import android.net.InterfaceConfiguration;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties.ProvisioningChange;
import android.net.LinkProperties;
@@ -35,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.NetworkConstants;
import android.net.util.SharedLog;
import android.os.INetworkManagementService;
import android.os.Message;
@@ -51,17 +53,25 @@ import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.IState;
import com.android.internal.util.Preconditions;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.server.net.NetlinkTracker;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.Predicate;
import java.util.stream.Collectors;


/**
@@ -308,6 +318,11 @@ public class IpManager extends StateMachine {
                return this;
            }

            public Builder withInitialConfiguration(InitialConfiguration initialConfig) {
                mConfig.mInitialConfig = initialConfig;
                return this;
            }

            public Builder withStaticConfiguration(StaticIpConfiguration staticConfig) {
                mConfig.mStaticIpConfig = staticConfig;
                return this;
@@ -342,18 +357,20 @@ public class IpManager extends StateMachine {
        /* package */ boolean mEnableIPv6 = true;
        /* package */ boolean mUsingIpReachabilityMonitor = true;
        /* package */ int mRequestedPreDhcpActionMs;
        /* package */ InitialConfiguration mInitialConfig;
        /* package */ StaticIpConfiguration mStaticIpConfig;
        /* package */ ApfCapabilities mApfCapabilities;
        /* package */ int mProvisioningTimeoutMs = DEFAULT_TIMEOUT_MS;
        /* package */ int mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_STABLE_PRIVACY;

        public ProvisioningConfiguration() {}
        public ProvisioningConfiguration() {} // used by Builder

        public ProvisioningConfiguration(ProvisioningConfiguration other) {
            mEnableIPv4 = other.mEnableIPv4;
            mEnableIPv6 = other.mEnableIPv6;
            mUsingIpReachabilityMonitor = other.mUsingIpReachabilityMonitor;
            mRequestedPreDhcpActionMs = other.mRequestedPreDhcpActionMs;
            mInitialConfig = InitialConfiguration.copy(other.mInitialConfig);
            mStaticIpConfig = other.mStaticIpConfig;
            mApfCapabilities = other.mApfCapabilities;
            mProvisioningTimeoutMs = other.mProvisioningTimeoutMs;
@@ -366,12 +383,124 @@ public class IpManager extends StateMachine {
                    .add("mEnableIPv6: " + mEnableIPv6)
                    .add("mUsingIpReachabilityMonitor: " + mUsingIpReachabilityMonitor)
                    .add("mRequestedPreDhcpActionMs: " + mRequestedPreDhcpActionMs)
                    .add("mInitialConfig: " + mInitialConfig)
                    .add("mStaticIpConfig: " + mStaticIpConfig)
                    .add("mApfCapabilities: " + mApfCapabilities)
                    .add("mProvisioningTimeoutMs: " + mProvisioningTimeoutMs)
                    .add("mIPv6AddrGenMode: " + mIPv6AddrGenMode)
                    .toString();
        }

        public boolean isValid() {
            return (mInitialConfig == null) || mInitialConfig.isValid();
        }
    }

    public static class InitialConfiguration {
        public final Set<LinkAddress> ipAddresses = new HashSet<>();
        public final Set<IpPrefix> directlyConnectedRoutes = new HashSet<>();
        public final Set<InetAddress> dnsServers = new HashSet<>();
        public Inet4Address gateway; // WiFi legacy behavior with static ipv4 config

        public static InitialConfiguration copy(InitialConfiguration config) {
            if (config == null) {
                return null;
            }
            InitialConfiguration configCopy = new InitialConfiguration();
            configCopy.ipAddresses.addAll(config.ipAddresses);
            configCopy.directlyConnectedRoutes.addAll(config.directlyConnectedRoutes);
            configCopy.dnsServers.addAll(config.dnsServers);
            return configCopy;
        }

        @Override
        public String toString() {
            return String.format(
                    "InitialConfiguration(IPs: {%s}, prefixes: {%s}, DNS: {%s}, v4 gateway: %s)",
                    join(", ", ipAddresses), join(", ", directlyConnectedRoutes),
                    join(", ", dnsServers), gateway);
        }

        public boolean isValid() {
            // 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()))) {
                    return false;
                }
            }
            // For every dns server, there must be at least one prefix containing that address.
            for (InetAddress addr : dnsServers) {
                if (!any(directlyConnectedRoutes, (p) -> p.contains(addr))) {
                    return false;
                }
            }
            // All IPv6 LinkAddresses have an RFC7421-suitable prefix length
            // (read: compliant with RFC4291#section2.5.4).
            if (any(ipAddresses, not(InitialConfiguration::isPrefixLengthCompliant))) {
                return false;
            }
            // If directlyConnectedRoutes contains an IPv6 default route
            // then ipAddresses MUST contain at least one non-ULA GUA.
            if (any(directlyConnectedRoutes, InitialConfiguration::isIPv6DefaultRoute)
                    && all(ipAddresses, not(InitialConfiguration::isIPv6GUA))) {
                return false;
            }
            // The prefix length of routes in directlyConnectedRoutes be within reasonable
            // bounds for IPv6: /48-/64 just as we’d accept in RIOs.
            if (any(directlyConnectedRoutes, not(InitialConfiguration::isPrefixLengthCompliant))) {
                return false;
            }
            // There no more than one IPv4 address
            if (ipAddresses.stream().filter(Inet4Address.class::isInstance).count() > 1) {
                return false;
            }

            return true;
        }

        private static boolean isPrefixLengthCompliant(LinkAddress addr) {
            return (addr.getAddress() instanceof Inet4Address)
                    || isCompliantIPv6PrefixLength(addr.getPrefixLength());
        }

        private static boolean isPrefixLengthCompliant(IpPrefix prefix) {
            return (prefix.getAddress() instanceof Inet4Address)
                    || isCompliantIPv6PrefixLength(prefix.getPrefixLength());
        }

        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);
        }

        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;
                }
            }
            return false;
        }

        private static <T> boolean all(Iterable<T> coll, Predicate<T> fn) {
            return !any(coll, not(fn));
        }

        private static <T> Predicate<T> not(Predicate<T> fn) {
            return (t) -> !fn.test(t);
        }

        private static <T> String join(String delimiter, Collection<T> coll) {
            return coll.stream().map(Object::toString).collect(Collectors.joining(delimiter));
        }
    }

    public static final String DUMP_ARG = "ipmanager";
@@ -436,8 +565,7 @@ public class IpManager extends StateMachine {
    private boolean mMulticastFiltering;
    private long mStartTimeMillis;

    public IpManager(Context context, String ifName, Callback callback)
                throws IllegalArgumentException {
    public IpManager(Context context, String ifName, Callback callback) {
        this(context, ifName, callback, INetworkManagementService.Stub.asInterface(
                ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE)));
    }
@@ -446,7 +574,7 @@ public class IpManager extends StateMachine {
     * An expanded constructor, useful for dependency injection.
     */
    public IpManager(Context context, String ifName, Callback callback,
            INetworkManagementService nwService) throws IllegalArgumentException {
            INetworkManagementService nwService) {
        super(IpManager.class.getSimpleName() + "." + ifName);
        mTag = getName();

@@ -563,6 +691,11 @@ public class IpManager extends StateMachine {
    }

    public void startProvisioning(ProvisioningConfiguration req) {
        if (!req.isValid()) {
            doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING);
            return;
        }

        getNetworkInterface();

        mCallback.setNeighborDiscoveryOffload(true);
+1 −0
Original line number Diff line number Diff line
@@ -102,6 +102,7 @@ public final class NetworkConstants {
    public static final int IPV6_ADDR_LEN = 16;
    public static final int IPV6_MIN_MTU = 1280;
    public static final int RFC7421_PREFIX_LENGTH = 64;
    public static final int RFC6177_MIN_PREFIX_LENGTH = 48;

    /**
     * ICMPv6 constants.
+175 −3
Original line number Diff line number Diff line
@@ -16,11 +16,26 @@

package android.net.ip;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.AlarmManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.ip.IpManager.Callback;
import android.net.ip.IpManager.InitialConfiguration;
import android.net.ip.IpManager.ProvisioningConfiguration;
import android.os.INetworkManagementService;
import android.provider.Settings;
import android.support.test.filters.SmallTest;
@@ -31,11 +46,17 @@ import com.android.internal.util.test.FakeSettingsProvider;
import com.android.internal.R;

import org.junit.Before;
import org.junit.runner.RunWith;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.util.HashSet;
import java.util.Set;

/**
 * Tests for IpManager.
 */
@@ -44,14 +65,20 @@ import org.mockito.MockitoAnnotations;
public class IpManagerTest {
    private static final int DEFAULT_AVOIDBADWIFI_CONFIG_VALUE = 1;

    private static final String VALID = "VALID";
    private static final String INVALID = "INVALID";

    @Mock private Context mContext;
    @Mock private INetworkManagementService mNMService;
    @Mock private Resources mResources;
    @Mock private Callback mCb;
    @Mock private AlarmManager mAlarm;
    private MockContentResolver mContentResolver;

    @Before public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);

        when(mContext.getSystemService(eq(Context.ALARM_SERVICE))).thenReturn(mAlarm);
        when(mContext.getResources()).thenReturn(mResources);
        when(mResources.getInteger(R.integer.config_networkAvoidBadWifi))
                .thenReturn(DEFAULT_AVOIDBADWIFI_CONFIG_VALUE);
@@ -68,7 +95,152 @@ public class IpManagerTest {

    @Test
    public void testInvalidInterfaceDoesNotThrow() throws Exception {
        final IpManager.Callback cb = new IpManager.Callback();
        final IpManager ipm = new IpManager(mContext, "test_wlan0", cb, mNMService);
        final IpManager ipm = new IpManager(mContext, "test_wlan0", mCb, mNMService);
    }

    @Test
    public void testDefaultProvisioningConfiguration() throws Exception {
        final String iface = "test_wlan0";
        final IpManager ipm = new IpManager(mContext, iface, mCb, mNMService);
        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                .withoutIPv4()
                // TODO: mock IpReachabilityMonitor's dependencies (NetworkInterface, PowerManager)
                // and enable it in this test
                .withoutIpReachabilityMonitor()
                .build();

        ipm.startProvisioning(config);
        verify(mCb, times(1)).setNeighborDiscoveryOffload(true);
        verify(mCb, timeout(100).times(1)).setFallbackMulticastFilter(false);
        verify(mCb, never()).onProvisioningFailure(any());

        ipm.stop();
        verify(mNMService, timeout(100).times(1)).disableIpv6(iface);
        verify(mNMService, timeout(100).times(1)).clearInterfaceAddresses(iface);
    }

    @Test
    public void testInitialConfigurations() throws Exception {
        InitialConfigurationTestCase[] testcases = {
            validConf("valid IPv4 configuration",
                    links("192.0.2.12/24"), prefixes("192.0.2.0/24"), dns("192.0.2.2")),
            validConf("another valid IPv4 configuration",
                    links("192.0.2.12/24"), prefixes("192.0.2.0/24"), dns()),
            validConf("valid IPv6 configurations",
                    links("2001:db8:dead:beef:f00::a0/64", "fe80::1/64"),
                    prefixes("2001:db8:dead:beef::/64", "fe80::/64"),
                    dns("2001:db8:dead:beef:f00::02")),
            validConf("valid IPv6 configurations",
                    links("fe80::1/64"), prefixes("fe80::/64"), dns()),
            validConf("valid IPv6/v4 configuration",
                    links("2001:db8:dead:beef:f00::a0/48", "192.0.2.12/24"),
                    prefixes("2001:db8:dead:beef::/64", "192.0.2.0/24"),
                    dns("192.0.2.2", "2001:db8:dead:beef:f00::02")),
            validConf("valid IPv6 configuration without any GUA.",
                    links("fd00:1234:5678::1/48"),
                    prefixes("fd00:1234:5678::/48"),
                    dns("fd00:1234:5678::1000")),

            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",
                    links("198.51.2.12/24"), prefixes("198.51.100.0/24"), dns("192.0.2.2")),
            invalidConf("v4 dns addr not in any prefix",
                    links("192.0.2.12/24"), prefixes("192.0.2.0/24"), dns("198.51.100.2")),
            invalidConf("v6 addr not in any prefix",
                    links("2001:db8:dead:beef:f00::a0/64", "fe80::1/64"),
                    prefixes("2001:db8:dead:beef::/64"),
                    dns("2001:db8:dead:beef:f00::02")),
            invalidConf("v6 dns addr not in any prefix",
                    links("fe80::1/64"), prefixes("fe80::/64"), dns("2001:db8:dead:beef:f00::02")),
            invalidConf("default ipv6 route and no GUA",
                    links("fd01:1111:2222:3333::a0/128"), prefixes("::/0"), dns()),
            invalidConf("invalid v6 prefix length",
                    links("2001:db8:dead:beef:f00::a0/128"), prefixes("2001:db8:dead:beef::/32"),
                    dns()),
            invalidConf("another invalid v6 prefix length",
                    links("2001:db8:dead:beef:f00::a0/128"), prefixes("2001:db8:dead:beef::/72"),
                    dns())
        };

        for (InitialConfigurationTestCase testcase : testcases) {
            if (testcase.config.isValid() != testcase.isValid) {
                fail(testcase.errorMessage());
            }
        }
    }

    static class InitialConfigurationTestCase {
        String descr;
        boolean isValid;
        InitialConfiguration config;
        public String errorMessage() {
            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) {
        return confTestCase(descr, true, conf(links, prefixes, dns));
    }

    static InitialConfigurationTestCase invalidConf(String descr, Set<LinkAddress> links,
            Set<IpPrefix> prefixes, Set<InetAddress> dns) {
        return confTestCase(descr, false, conf(links, prefixes, dns));
    }

    static InitialConfigurationTestCase confTestCase(
            String descr, boolean isValid, InitialConfiguration config) {
        InitialConfigurationTestCase testcase = new InitialConfigurationTestCase();
        testcase.descr = descr;
        testcase.isValid = isValid;
        testcase.config = config;
        return testcase;
    }

    static InitialConfiguration conf(
            Set<LinkAddress> links, Set<IpPrefix> prefixes, Set<InetAddress> dns) {
        InitialConfiguration conf = new InitialConfiguration();
        conf.ipAddresses.addAll(links);
        conf.directlyConnectedRoutes.addAll(prefixes);
        conf.dnsServers.addAll(dns);
        return conf;
    }

    static Set<IpPrefix> prefixes(String... prefixes) {
        return mapIntoSet(prefixes, IpPrefix::new);
    }

    static Set<LinkAddress> links(String... addresses) {
        return mapIntoSet(addresses, LinkAddress::new);
    }

    static Set<InetAddress> ips(String... addresses) {
        return mapIntoSet(addresses, InetAddress::getByName);
    }

    static Set<InetAddress> dns(String... addresses) {
        return ips(addresses);
    }

    static <A, B> Set<B> mapIntoSet(A[] in, Fn<A, B> fn) {
        Set<B> out = new HashSet<>(in.length);
        for (A item : in) {
            try {
                out.add(fn.call(item));
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return out;
    }

    interface Fn<A,B> {
        B call(A a) throws Exception;
    }
}