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

Commit a636761b authored by Erik Kline's avatar Erik Kline
Browse files

Initial refactoring to group IP-related elements into an IpManager

Bug: 17345682
Change-Id: I88f3f4bd32d18cd8d4f1404493648c8bcc1deeec
parent 934b3b9c
Loading
Loading
Loading
Loading
+461 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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 android.net.ip;

import android.content.Context;
import android.net.DhcpResults;
import android.net.LinkProperties;
import android.net.LinkProperties.ProvisioningChange;
import android.net.RouteInfo;
import android.net.StaticIpConfiguration;
import android.os.INetworkManagementService;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.server.net.NetlinkTracker;

import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;


/**
 * IpManager
 *
 * This class provides the interface to IP-layer provisioning and maintenance
 * functionality that can be used by transport layers like Wi-Fi, Ethernet,
 * et cetera.
 *
 * [ Lifetime ]
 * IpManager is designed to be instantiated as soon as the interface name is
 * known and can be as long-lived as the class containing it (i.e. declaring
 * it "private final" is okay).
 *
 * @hide
 */
public class IpManager extends StateMachine {
    private static final String TAG = IpManager.class.getSimpleName();
    private static final boolean DBG = true;
    private static final boolean VDBG = false;

    /**
     * Callbacks for both configuration of IpManager and for handling
     * events as desired.
     */
    public static class Callback {
        /**
         * Configuration callbacks.
         *
         * Override methods as desired in order to control which features
         * IpManager will use at run time.
         */

        // An IpReachabilityMonitor will always be started, if only for logging.
        // This method is checked before probing neighbors and before calling
        // onProvisioningLost() (see below).
        public boolean usingIpReachabilityMonitor() {
            return false;
        }

        /**
         * Event callbacks.
         *
         * Override methods as desired in order to handle event callbacks
         * as IpManager invokes them.
         */

        // TODO: Kill with fire once DHCP and static configuration are moved
        // out of WifiStateMachine.
        public void onIPv4ProvisioningSuccess(DhcpResults dhcpResults, int reason) {}
        public void onIPv4ProvisioningFailure(int reason) {}

        public void onProvisioningSuccess(LinkProperties newLp) {}
        public void onProvisioningFailure(LinkProperties newLp) {}

        // Invoked on LinkProperties changes.
        public void onLinkPropertiesChange(LinkProperties newLp) {}

        // Called when the internal IpReachabilityMonitor (if enabled) has
        // detected the loss of a critical number of required neighbors.
        public void onReachabilityLost(String logMsg) {}
    }

    private static final int CMD_STOP = 1;
    private static final int CMD_START = 2;
    private static final int CMD_CONFIRM = 3;
    private static final int CMD_UPDATE_DHCPV4_RESULTS = 4;
    // Sent by NetlinkTracker to communicate netlink events.
    private static final int EVENT_NETLINK_LINKPROPERTIES_CHANGED = 5;

    private static final int MAX_LOG_RECORDS = 1000;

    private final Object mLock = new Object();
    private final State mStoppedState = new StoppedState();
    private final State mStartedState = new StartedState();

    private final Context mContext;
    private final String mInterfaceName;
    @VisibleForTesting
    protected final Callback mCallback;
    private final INetworkManagementService mNwService;
    private final NetlinkTracker mNetlinkTracker;

    private int mInterfaceIndex;

    /**
     * Non-final member variables accessed only from within our StateMachine.
     */
    private IpReachabilityMonitor mIpReachabilityMonitor;
    private DhcpResults mDhcpResults;
    private StaticIpConfiguration mStaticIpConfig;

    /**
     * Member variables accessed both from within the StateMachine thread
     * and via accessors from other threads.
     */
    @GuardedBy("mLock")
    private LinkProperties mLinkProperties;

    public IpManager(Context context, String ifName, Callback callback)
                throws IllegalArgumentException {
        super(TAG + "." + ifName);

        mContext = context;
        mInterfaceName = ifName;

        mCallback = callback;

        mNwService = INetworkManagementService.Stub.asInterface(
                ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE));

        mNetlinkTracker = new NetlinkTracker(
                mInterfaceName,
                new NetlinkTracker.Callback() {
                    @Override
                    public void update() {
                        sendMessage(EVENT_NETLINK_LINKPROPERTIES_CHANGED);
                    }
                });
        try {
            mNwService.registerObserver(mNetlinkTracker);
        } catch (RemoteException e) {
            Log.e(TAG, "Couldn't register NetlinkTracker: " + e.toString());
        }

        resetLinkProperties();

        // Super simple StateMachine.
        addState(mStoppedState);
        addState(mStartedState);
        setInitialState(mStoppedState);
        setLogRecSize(MAX_LOG_RECORDS);
        super.start();
    }

    /**
     * A special constructor for use in testing that bypasses some of the more
     * complicated setup bits.
     *
     * TODO: Figure out how to delete this yet preserve testability.
     */
    @VisibleForTesting
    protected IpManager(String ifName, Callback callback) {
        super(TAG + ".test-" + ifName);
        mInterfaceName = ifName;
        mCallback = callback;

        mContext = null;
        mNwService = null;
        mNetlinkTracker = null;
    }

    public void startProvisioning(StaticIpConfiguration staticIpConfig) {
        getInterfaceIndex();

        sendMessage(CMD_START, staticIpConfig);
    }

    public void startProvisioning() {
        getInterfaceIndex();

        sendMessage(CMD_START);
    }

    public void stop() {
        sendMessage(CMD_STOP);
    }

    public void confirmConfiguration() {
        sendMessage(CMD_CONFIRM);
    }

    public LinkProperties getLinkProperties() {
        synchronized (mLock) {
            return new LinkProperties(mLinkProperties);
        }
    }

    // TODO: Kill with fire once DHCPv4/static config is moved into IpManager.
    public void updateWithDhcpResults(DhcpResults dhcpResults, int reason) {
        sendMessage(CMD_UPDATE_DHCPV4_RESULTS, reason, 0, dhcpResults);
    }


    /**
     * Internals.
     */

    private void getInterfaceIndex() {
        try {
            mInterfaceIndex = NetworkInterface.getByName(mInterfaceName).getIndex();
        } catch (SocketException | NullPointerException e) {
            // TODO: throw new IllegalStateException.
            Log.e(TAG, "ALERT: Failed to get interface index: ", e);
        }
    }

    // This needs to be called with care to ensure that our LinkProperties
    // are in sync with the actual LinkProperties of the interface. For example,
    // we should only call this if we know for sure that there are no IP addresses
    // assigned to the interface, etc.
    private void resetLinkProperties() {
        mNetlinkTracker.clearLinkProperties();
        mDhcpResults = null;
        mStaticIpConfig = null;

        synchronized (mLock) {
            mLinkProperties = new LinkProperties();
            mLinkProperties.setInterfaceName(mInterfaceName);
        }
    }

    private ProvisioningChange setLinkProperties(LinkProperties newLp) {
        if (mIpReachabilityMonitor != null) {
            mIpReachabilityMonitor.updateLinkProperties(newLp);
        }

        // TODO: Figure out whether and how to incorporate static configuration
        // into the notion of provisioning.
        ProvisioningChange delta;
        synchronized (mLock) {
            delta = LinkProperties.compareProvisioning(mLinkProperties, newLp);
            mLinkProperties = new LinkProperties(newLp);
        }

        if (DBG) {
            switch (delta) {
                case GAINED_PROVISIONING:
                case LOST_PROVISIONING:
                    Log.d(TAG, "provisioning: " + delta);
                    break;
            }
        }

        return delta;
    }

    private LinkProperties assembleLinkProperties() {
        // [1] Create a new LinkProperties object to populate.
        LinkProperties newLp = new LinkProperties();
        newLp.setInterfaceName(mInterfaceName);

        // [2] Pull in data from netlink:
        //         - IPv4 addresses
        //         - IPv6 addresses
        //         - IPv6 routes
        //         - IPv6 DNS servers
        LinkProperties netlinkLinkProperties = mNetlinkTracker.getLinkProperties();
        newLp.setLinkAddresses(netlinkLinkProperties.getLinkAddresses());
        for (RouteInfo route : netlinkLinkProperties.getRoutes()) {
            newLp.addRoute(route);
        }
        for (InetAddress dns : netlinkLinkProperties.getDnsServers()) {
            // Only add likely reachable DNS servers.
            // TODO: investigate deleting this.
            if (newLp.isReachable(dns)) {
                newLp.addDnsServer(dns);
            }
        }

        // [3] Add in data from DHCPv4, if available.
        //
        // mDhcpResults is never shared with any other owner so we don't have
        // to worry about concurrent modification.
        if (mDhcpResults != null) {
            for (RouteInfo route : mDhcpResults.getRoutes(mInterfaceName)) {
                newLp.addRoute(route);
            }
            for (InetAddress dns : mDhcpResults.dnsServers) {
                // Only add likely reachable DNS servers.
                // TODO: investigate deleting this.
                if (newLp.isReachable(dns)) {
                    newLp.addDnsServer(dns);
                }
            }
            newLp.setDomains(mDhcpResults.domains);
        }

        if (VDBG) {
            Log.d(TAG, "newLp{" + newLp + "}");
        }

        return newLp;
    }

    class StoppedState extends State {
        @Override
        public void enter() {
            try {
                mNwService.disableIpv6(mInterfaceName);
                mNwService.clearInterfaceAddresses(mInterfaceName);
            } catch (Exception e) {
                Log.e(TAG, "Failed to clear addresses or disable IPv6" + e);
            }

            resetLinkProperties();
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case CMD_STOP:
                    break;

                case CMD_START:
                    mStaticIpConfig = (StaticIpConfiguration) msg.obj;
                    transitionTo(mStartedState);
                    break;

                case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
                    setLinkProperties(assembleLinkProperties());
                    break;

                default:
                    return NOT_HANDLED;
            }
            return HANDLED;
        }
    }

    class StartedState extends State {
        @Override
        public void enter() {
            // Set privacy extensions.
            try {
                mNwService.setInterfaceIpv6PrivacyExtensions(mInterfaceName, true);
                mNwService.enableIpv6(mInterfaceName);
            } catch (RemoteException re) {
                Log.e(TAG, "Unable to change interface settings: " + re);
            } catch (IllegalStateException ie) {
                Log.e(TAG, "Unable to change interface settings: " + ie);
            }

            mIpReachabilityMonitor = new IpReachabilityMonitor(
                    mContext,
                    mInterfaceName,
                    new IpReachabilityMonitor.Callback() {
                        @Override
                        public void notifyLost(InetAddress ip, String logMsg) {
                            if (mCallback.usingIpReachabilityMonitor()) {
                                mCallback.onReachabilityLost(logMsg);
                            }
                        }
                    });

            // TODO: Check mStaticIpConfig and handle accordingly.
        }

        @Override
        public void exit() {
            mIpReachabilityMonitor.stop();
            mIpReachabilityMonitor = null;

            resetLinkProperties();
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case CMD_STOP:
                    transitionTo(mStoppedState);
                    break;

                case CMD_START:
                    // TODO: Defer this message to be delivered after a state transition
                    // to StoppedState.  That way, receiving CMD_START in StartedState
                    // effects a restart.
                    Log.e(TAG, "ALERT: START received in StartedState.");
                    break;

                case CMD_CONFIRM:
                    if (mCallback.usingIpReachabilityMonitor()) {
                        mIpReachabilityMonitor.probeAll();
                    }
                    break;

                case CMD_UPDATE_DHCPV4_RESULTS:
                    final DhcpResults dhcpResults = (DhcpResults) msg.obj;
                    final int reason = msg.arg1;
                    if (dhcpResults != null) {
                        mDhcpResults = new DhcpResults(dhcpResults);
                        setLinkProperties(assembleLinkProperties());
                        mCallback.onIPv4ProvisioningSuccess(dhcpResults, reason);
                    } else {
                        mDhcpResults = null;
                        setLinkProperties(assembleLinkProperties());
                        mCallback.onIPv4ProvisioningFailure(reason);
                    }
                    break;

                case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
                    final LinkProperties newLp = assembleLinkProperties();
                    final ProvisioningChange delta = setLinkProperties(newLp);

                    // NOTE: The only receiver of these callbacks currently
                    // treats all three of them identically, namely it calls
                    // IpManager#getLinkProperties() and makes its own determination.
                    switch (delta) {
                        case GAINED_PROVISIONING:
                            mCallback.onProvisioningSuccess(newLp);
                            break;

                        case LOST_PROVISIONING:
                            mCallback.onProvisioningFailure(newLp);
                            break;

                        default:
                            // TODO: Only notify on STILL_PROVISIONED?
                            mCallback.onLinkPropertiesChange(newLp);
                            break;
                    }

                    break;

                default:
                    return NOT_HANDLED;
            }
            return HANDLED;
        }

    }
}