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

Commit 787d9356 authored by Erik Kline's avatar Erik Kline
Browse files

Initial IpReachabilityMonitor implementation.

Add a class that monitors on-link IP reachability and notifies
callers whenever any on-link addresses of interest appear to have
become unresponsive.

Bug: 18581716
Change-Id: I333eee6fe30fa97a21297c6745de14ceb95bb661
parent 5a5745b3
Loading
Loading
Loading
Loading
+354 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 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;

import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.ProxyInfo;
import android.net.RouteInfo;
import android.net.netlink.NetlinkConstants;
import android.net.netlink.NetlinkErrorMessage;
import android.net.netlink.NetlinkMessage;
import android.net.netlink.NetlinkSocket;
import android.net.netlink.RtNetlinkNeighborMessage;
import android.net.netlink.StructNdaCacheInfo;
import android.net.netlink.StructNdMsg;
import android.net.netlink.StructNlMsgHdr;
import android.os.SystemClock;
import android.system.ErrnoException;
import android.system.NetlinkSocketAddress;
import android.system.OsConstants;
import android.text.TextUtils;
import android.util.Log;

import java.io.InterruptedIOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;


/**
 * IpReachabilityMonitor.
 *
 * Monitors on-link IP reachability and notifies callers whenever any on-link
 * addresses of interest appear to have become unresponsive.
 *
 * @hide
 */
public class IpReachabilityMonitor {
    private static final String TAG = "IpReachabilityMonitor";
    private static final boolean DBG = true;
    private static final boolean VDBG = false;

    public interface Callback {
        public void notifyLost(InetAddress ip, String logMsg);
    }

    private final Object mLock = new Object();
    private final String mInterfaceName;
    private final int mInterfaceIndex;
    private final Callback mCallback;
    private final Set<InetAddress> mIpWatchList;
    private int mIpWatchListVersion;
    private boolean mRunning;
    final private Thread mObserverThread;

    // TODO: consider passing in a NetworkInterface object from the caller.
    public IpReachabilityMonitor(String ifName, Callback callback) throws IllegalArgumentException {
        mInterfaceName = ifName;
        int ifIndex = -1;
        try {
            NetworkInterface netIf = NetworkInterface.getByName(ifName);
            mInterfaceIndex = netIf.getIndex();
        } catch (SocketException | NullPointerException e) {
            throw new IllegalArgumentException("invalid interface '" + ifName + "': ", e);
        }
        mCallback = callback;
        mIpWatchList = new HashSet<InetAddress>();
        mIpWatchListVersion = 0;
        mRunning = false;
        mObserverThread = new Thread(new NetlinkSocketObserver());
        mObserverThread.start();
    }

    public void stop() {
        synchronized (mLock) {
            mRunning = false;
            mIpWatchList.clear();
        }
    }

    // TODO: add a public dump() method that can be called during a bug report.

    private static Set<InetAddress> getOnLinkNeighbors(LinkProperties lp) {
        Set<InetAddress> allIps = new HashSet<InetAddress>();

        final List<RouteInfo> routes = lp.getRoutes();
        for (RouteInfo route : routes) {
            if (route.hasGateway()) {
                allIps.add(route.getGateway());
            }
        }

        for (InetAddress nameserver : lp.getDnsServers()) {
            allIps.add(nameserver);
        }

        try {
            // Don't block here for DNS lookups.  If the proxy happens to be an
            // IP literal then we add it the list, but otherwise skip it.
            allIps.add(NetworkUtils.numericToInetAddress(lp.getHttpProxy().getHost()));
        } catch (NullPointerException|IllegalArgumentException e) {
            // No proxy, PAC proxy, or proxy is not a literal IP address.
        }

        Set<InetAddress> neighbors = new HashSet<InetAddress>();
        for (InetAddress ip : allIps) {
            // TODO: consider using the prefixes of the LinkAddresses instead
            // of the routes--it may be more accurate.
            for (RouteInfo route : routes) {
                if (route.hasGateway()) {
                    continue;  // Not directly connected.
                }
                if (route.matches(ip)) {
                    neighbors.add(ip);
                    break;
                }
            }
        }
        return neighbors;
    }

    private String describeWatchList() {
        synchronized (mLock) {
            return "version{" + mIpWatchListVersion + "}, " +
                    "ips=[" + TextUtils.join(",", mIpWatchList) + "]";
        }
    }

    private boolean isWatching(InetAddress ip) {
        synchronized (mLock) {
            return mRunning && mIpWatchList.contains(ip);
        }
    }

    public void updateLinkProperties(LinkProperties lp) {
        if (!mInterfaceName.equals(lp.getInterfaceName())) {
            // TODO: figure out how to cope with interface changes.
            Log.wtf(TAG, "requested LinkProperties interface '" + lp.getInterfaceName() +
                    "' does not match: " + mInterfaceName);
            return;
        }

        // We rely upon the caller to determine when LinkProperties have actually
        // changed and call this at the appropriate time.  Note that even though
        // the LinkProperties may change, the set of on-link neighbors might not.
        //
        // Nevertheless, just clear and re-add everything.
        final Set<InetAddress> neighbors = getOnLinkNeighbors(lp);
        if (neighbors.isEmpty()) {
            return;
        }

        synchronized (mLock) {
            mIpWatchList.clear();
            mIpWatchList.addAll(neighbors);
            mIpWatchListVersion++;
        }
        if (DBG) { Log.d(TAG, "watch: " + describeWatchList()); }
    }

    public void clearLinkProperties() {
        synchronized (mLock) {
            mIpWatchList.clear();
            mIpWatchListVersion++;
        }
        if (DBG) { Log.d(TAG, "clear: " + describeWatchList()); }
    }

    private void notifyLost(InetAddress ip, String msg) {
        if (!isWatching(ip)) {
            // Ignore stray notifications.  This can happen when, for example,
            // several neighbors are reported unreachable or deleted
            // back-to-back.  Because these messages are parsed serially, and
            // this method is called for each notification, the caller above us
            // may have already processed an earlier lost notification and
            // cleared the watch list as it moves to handle the situation.
            return;
        }
        Log.w(TAG, "ALERT: " + ip.getHostAddress() + " -- " + msg);
        if (mCallback != null) {
            mCallback.notifyLost(ip, msg);
        }
    }


    private final class NetlinkSocketObserver implements Runnable {
        private static final String TAG = "NetlinkSocketObserver";
        private NetlinkSocket mSocket;

        @Override
        public void run() {
            if (VDBG) { Log.d(TAG, "Starting observing thread."); }
            synchronized (mLock) { mRunning = true; }

            try {
                setupNetlinkSocket();
            } catch (ErrnoException | SocketException e) {
                Log.e(TAG, "Failed to suitably initialize a netlink socket", e);
                synchronized (mLock) { mRunning = false; }
            }

            ByteBuffer byteBuffer;
            while (stillRunning()) {
                try {
                    byteBuffer = recvKernelReply();
                } catch (ErrnoException e) {
                    Log.w(TAG, "ErrnoException: ", e);
                    break;
                }
                final long whenMs = SystemClock.elapsedRealtime();
                if (byteBuffer == null) {
                    continue;
                }
                parseNetlinkMessageBuffer(byteBuffer, whenMs);
            }

            clearNetlinkSocket();

            synchronized (mLock) { mRunning = false; }
            if (VDBG) { Log.d(TAG, "Finishing observing thread."); }
        }

        private boolean stillRunning() {
            synchronized (mLock) {
                return mRunning;
            }
        }

        private void clearNetlinkSocket() {
            if (mSocket != null) {
                mSocket.close();
            }
            mSocket = null;
        }

            // TODO: Refactor the main loop to recreate the socket upon recoverable errors.
        private void setupNetlinkSocket() throws ErrnoException, SocketException {
            clearNetlinkSocket();
            mSocket = new NetlinkSocket(OsConstants.NETLINK_ROUTE);

            final NetlinkSocketAddress listenAddr = new NetlinkSocketAddress(
                    0, OsConstants.RTMGRP_NEIGH);
            mSocket.bind(listenAddr);

            if (VDBG) {
                final NetlinkSocketAddress nlAddr = mSocket.getLocalAddress();
                Log.d(TAG, "bound to sockaddr_nl{"
                        + ((long) (nlAddr.getPortId() & 0xffffffff)) + ", "
                        + nlAddr.getGroupsMask()
                        + "}");
            }
        }

        private ByteBuffer recvKernelReply() throws ErrnoException {
            try {
                return mSocket.recvMessage(0);
            } catch (InterruptedIOException e) {
                // Interruption or other error, e.g. another thread closed our file descriptor.
            } catch (ErrnoException e) {
                if (e.errno != OsConstants.EAGAIN) {
                    throw e;
                }
            }
            return null;
        }

        private void parseNetlinkMessageBuffer(ByteBuffer byteBuffer, long whenMs) {
            while (byteBuffer.remaining() > 0) {
                final int position = byteBuffer.position();
                final NetlinkMessage nlMsg = NetlinkMessage.parse(byteBuffer);
                if (nlMsg == null || nlMsg.getHeader() == null) {
                    byteBuffer.position(position);
                    Log.e(TAG, "unparsable netlink msg: " + NetlinkConstants.hexify(byteBuffer));
                    break;
                }

                final int srcPortId = nlMsg.getHeader().nlmsg_pid;
                if (srcPortId !=  0) {
                    Log.e(TAG, "non-kernel source portId: " + ((long) (srcPortId & 0xffffffff)));
                    break;
                }

                if (nlMsg instanceof NetlinkErrorMessage) {
                    Log.e(TAG, "netlink error: " + nlMsg);
                    continue;
                } else if (!(nlMsg instanceof RtNetlinkNeighborMessage)) {
                    if (DBG) {
                        Log.d(TAG, "non-rtnetlink neighbor msg: " + nlMsg);
                    }
                    continue;
                }

                evaluateRtNetlinkNeighborMessage((RtNetlinkNeighborMessage) nlMsg, whenMs);
            }
        }

        private void evaluateRtNetlinkNeighborMessage(
                RtNetlinkNeighborMessage neighMsg, long whenMs) {
            final StructNdMsg ndMsg = neighMsg.getNdHeader();
            if (ndMsg == null || ndMsg.ndm_ifindex != mInterfaceIndex) {
                return;
            }

            final InetAddress destination = neighMsg.getDestination();
            if (!isWatching(destination)) {
                return;
            }

            final short msgType = neighMsg.getHeader().nlmsg_type;
            final short nudState = ndMsg.ndm_state;
            final String eventMsg = "NeighborEvent{"
                    + "elapsedMs=" + whenMs + ", "
                    + destination.getHostAddress() + ", "
                    + "[" + NetlinkConstants.hexify(neighMsg.getLinkLayerAddress()) + "], "
                    + NetlinkConstants.stringForNlMsgType(msgType) + ", "
                    + StructNdMsg.stringForNudState(nudState)
                    + "}";

            if (VDBG) {
                Log.d(TAG, neighMsg.toString());
            } else if (DBG) {
                Log.d(TAG, eventMsg);
            }

            if ((msgType == NetlinkConstants.RTM_DELNEIGH) ||
                (nudState == StructNdMsg.NUD_FAILED)) {
                final String logMsg = "FAILURE: " + eventMsg;
                notifyLost(destination, logMsg);
            }
        }
    }
}