content: /*
   * Copyright (C) 2014 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 com.android.server.net;
  
  import android.annotation.UnsupportedAppUsage;
  import android.net.LinkAddress;
  import android.net.LinkProperties;
  import android.net.RouteInfo;
  import android.util.Log;
  
  import java.net.InetAddress;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Set;
  
  /**
   * Keeps track of link configuration received from Netlink.
   *
   * Instances of this class are expected to be owned by subsystems such as Wi-Fi
   * or Ethernet that manage one or more network interfaces. Each interface to be
   * tracked needs its own {@code NetlinkTracker}.
   *
   * An instance of this class is constructed by passing in an interface name and
   * a callback. The owner is then responsible for registering the tracker with
   * NetworkManagementService. When the class receives update notifications from
   * the NetworkManagementService notification threads, it applies the update to
   * its local LinkProperties, and if something has changed, notifies its owner of
   * the update via the callback.
   *
   * The owner can then call {@code getLinkProperties()} in order to find out
   * what changed. If in the meantime the LinkProperties stored here have changed,
   * this class will return the current LinkProperties. Because each change
   * triggers an update callback after the change is made, the owner may get more
   * callbacks than strictly necessary (some of which may be no-ops), but will not
   * be out of sync once all callbacks have been processed.
   *
   * Threading model:
   *
   * - The owner of this class is expected to create it, register it, and call
   *   getLinkProperties or clearLinkProperties on its thread.
   * - Most of the methods in the class are inherited from BaseNetworkObserver
   *   and are called by NetworkManagementService notification threads.
   * - All accesses to mLinkProperties must be synchronized(this). All the other
   *   member variables are immutable once the object is constructed.
   *
   * This class currently tracks IPv4 and IPv6 addresses. In the future it will
   * track routes and DNS servers.
   *
   * @hide
   */
  public class NetlinkTracker extends BaseNetworkObserver {
  
      private final String TAG;
  
      public interface Callback {
          public void update();
      }
  
      private final String mInterfaceName;
      private final Callback mCallback;
      private final LinkProperties mLinkProperties;
      private DnsServerRepository mDnsServerRepository;
  
      private static final boolean DBG = false;
  
      @UnsupportedAppUsage
      public NetlinkTracker(String iface, Callback callback) {
          TAG = "NetlinkTracker/" + iface;
          mInterfaceName = iface;
          mCallback = callback;
          mLinkProperties = new LinkProperties();
          mLinkProperties.setInterfaceName(mInterfaceName);
          mDnsServerRepository = new DnsServerRepository();
      }
  
      private void maybeLog(String operation, String iface, LinkAddress address) {
          if (DBG) {
              Log.d(TAG, operation + ": " + address + " on " + iface +
                      " flags " + address.getFlags() + " scope " + address.getScope());
          }
      }
  
      private void maybeLog(String operation, Object o) {
          if (DBG) {
              Log.d(TAG, operation + ": " + o.toString());
          }
      }
  
      @Override
      public void interfaceRemoved(String iface) {
          maybeLog("interfaceRemoved", iface);
          if (mInterfaceName.equals(iface)) {
              // Our interface was removed. Clear our LinkProperties and tell our owner that they are
              // now empty. Note that from the moment that the interface is removed, any further
              // interface-specific messages (e.g., RTM_DELADDR) will not reach us, because the netd
              // code that parses them will not be able to resolve the ifindex to an interface name.
              clearLinkProperties();
              mCallback.update();
          }
      }
  
      @Override
      public void addressUpdated(String iface, LinkAddress address) {
          if (mInterfaceName.equals(iface)) {
              maybeLog("addressUpdated", iface, address);
              boolean changed;
              synchronized (this) {
                  changed = mLinkProperties.addLinkAddress(address);
              }
              if (changed) {
                  mCallback.update();
              }
          }
      }
  
      @Override
      public void addressRemoved(String iface, LinkAddress address) {
          if (mInterfaceName.equals(iface)) {
              maybeLog("addressRemoved", iface, address);
              boolean changed;
              synchronized (this) {
                  changed = mLinkProperties.removeLinkAddress(address);
              }
              if (changed) {
                  mCallback.update();
              }
          }
      }
  
      @Override
      public void routeUpdated(RouteInfo route) {
          if (mInterfaceName.equals(route.getInterface())) {
              maybeLog("routeUpdated", route);
              boolean changed;
              synchronized (this) {
                  changed = mLinkProperties.addRoute(route);
              }
              if (changed) {
                  mCallback.update();
              }
          }
      }
  
      @Override
      public void routeRemoved(RouteInfo route) {
          if (mInterfaceName.equals(route.getInterface())) {
              maybeLog("routeRemoved", route);
              boolean changed;
              synchronized (this) {
                  changed = mLinkProperties.removeRoute(route);
              }
              if (changed) {
                  mCallback.update();
              }
          }
      }
  
      @Override
      public void interfaceDnsServerInfo(String iface, long lifetime, String[] addresses) {
          if (mInterfaceName.equals(iface)) {
              maybeLog("interfaceDnsServerInfo", Arrays.toString(addresses));
              boolean changed = mDnsServerRepository.addServers(lifetime, addresses);
              if (changed) {
                  synchronized (this) {
                      mDnsServerRepository.setDnsServersOn(mLinkProperties);
                  }
                  mCallback.update();
              }
          }
      }
  
      /**
       * Returns a copy of this object's LinkProperties.
       */
      @UnsupportedAppUsage
      public synchronized LinkProperties getLinkProperties() {
          return new LinkProperties(mLinkProperties);
      }
  
      @UnsupportedAppUsage
      public synchronized void clearLinkProperties() {
          // Clear the repository before clearing mLinkProperties. That way, if a clear() happens
          // while interfaceDnsServerInfo() is being called, we'll end up with no DNS servers in
          // mLinkProperties, as desired.
          mDnsServerRepository = new DnsServerRepository();
          mLinkProperties.clear();
          mLinkProperties.setInterfaceName(mInterfaceName);
      }
  }
  
  /**
   * Represents a DNS server entry with an expiry time.
   *
   * Implements Comparable so DNS server entries can be sorted by lifetime, longest-lived first.
   * The ordering of entries with the same lifetime is unspecified, because given two servers with
   * identical lifetimes, we don't care which one we use, and only comparing the lifetime is much
   * faster than comparing the IP address as well.
   *
   * Note: this class has a natural ordering that is inconsistent with equals.
   */
  class DnsServerEntry implements Comparable<DnsServerEntry> {
      /** The IP address of the DNS server. */
      public final InetAddress address;
      /** The time until which the DNS server may be used. A Java millisecond time as might be
        * returned by currentTimeMillis(). */
      public long expiry;
  
      public DnsServerEntry(InetAddress address, long expiry) throws IllegalArgumentException {
          this.address = address;
          this.expiry = expiry;
      }
  
      public int compareTo(DnsServerEntry other) {
          return Long.compare(other.expiry, this.expiry);
      }
  }
  
  /**
   * Tracks DNS server updates received from Netlink.
   *
   * The network may announce an arbitrary number of DNS servers in Router Advertisements at any
   * time. Each announcement has a lifetime; when the lifetime expires, the servers should not be used
   * any more. In this way, the network can gracefully migrate clients from one set of DNS servers to
   * another. Announcements can both raise and lower the lifetime, and an announcement can expire
   * servers by announcing them with a lifetime of zero.
   *
   * Typically the system will only use a small number (2 or 3; {@code NUM_CURRENT_SERVERS}) of DNS
   * servers at any given time. These are referred to as the current servers. In case all the
   * current servers expire, the class also keeps track of a larger (but limited) number of servers
   * that are promoted to current servers when the current ones expire. In order to minimize updates
   * to the rest of the system (and potentially expensive cache flushes) this class attempts to keep
   * the list of current servers constant where possible. More specifically, the list of current
   * servers is only updated if a new server is learned and there are not yet {@code
   * NUM_CURRENT_SERVERS} current servers, or if one or more of the current servers expires or is
   * pushed out of the set. Therefore, the current servers will not necessarily be the ones with the
   * highest lifetime, but the ones learned first.
   *
   * This is by design: if instead the class always preferred the servers with the highest lifetime, a
   * (misconfigured?) network where two or more routers announce more than {@code NUM_CURRENT_SERVERS}
   * unique servers would cause persistent oscillations.
   *
   * TODO: Currently servers are only expired when a new DNS update is received.
   * Update them using timers, or possibly on every notification received by NetlinkTracker.
   *
   * Threading model: run by NetlinkTracker. Methods are synchronized(this) just in case netlink
   * notifications are sent by multiple threads. If future threads use alarms to expire, those
   * alarms must also be synchronized(this).
   *
   */
  class DnsServerRepository {
  
      /** How many DNS servers we will use. 3 is suggested by RFC 6106. */
      public static final int NUM_CURRENT_SERVERS = 3;
  
      /** How many DNS servers we'll keep track of, in total. */
      public static final int NUM_SERVERS = 12;
  
      /** Stores up to {@code NUM_CURRENT_SERVERS} DNS servers we're currently using. */
      private Set<InetAddress> mCurrentServers;
  
      public static final String TAG = "DnsServerRepository";
  
      /**
       * Stores all the DNS servers we know about, for use when the current servers expire.
       * Always sorted in order of decreasing expiry. The elements in this list are also the values
       * of mIndex, and may be elements in mCurrentServers.
       */
      private ArrayList<DnsServerEntry> mAllServers;
  
      /**
       * Indexes the servers so we can update their lifetimes more quickly in the common case where
       * servers are not being added, but only being refreshed.
       */
      private HashMap<InetAddress, DnsServerEntry> mIndex;
  
      public DnsServerRepository() {
          mCurrentServers = new HashSet();
          mAllServers = new ArrayList<DnsServerEntry>(NUM_SERVERS);
          mIndex = new HashMap<InetAddress, DnsServerEntry>(NUM_SERVERS);
      }
  
      /** Sets the DNS servers of the provided LinkProperties object to the current servers. */
      public synchronized void setDnsServersOn(LinkProperties lp) {
          lp.setDnsServers(mCurrentServers);
      }
  
      /**
       * Notifies the class of new DNS server information.
       * @param lifetime the time in seconds that the DNS servers are valid.
       * @param addresses the string representations of the IP addresses of the DNS servers to use.
       */
      public synchronized boolean addServers(long lifetime, String[] addresses) {
          // The lifetime is actually an unsigned 32-bit number, but Java doesn't have unsigned.
          // Technically 0xffffffff (the maximum) is special and means "forever", but 2^32 seconds
          // (136 years) is close enough.
          long now = System.currentTimeMillis();
          long expiry = now + 1000 * lifetime;
  
          // Go through the list of servers. For each one, update the entry if one exists, and
          // create one if it doesn't.
          for (String addressString : addresses) {
              InetAddress address;
              try {
                  address = InetAddress.parseNumericAddress(addressString);
              } catch (IllegalArgumentException ex) {
                  continue;
              }
  
              if (!updateExistingEntry(address, expiry)) {
                  // There was no entry for this server. Create one, unless it's already expired
                  // (i.e., if the lifetime is zero; it cannot be < 0 because it's unsigned).
                  if (expiry > now) {
                      DnsServerEntry entry = new DnsServerEntry(address, expiry);
                      mAllServers.add(entry);
                      mIndex.put(address, entry);
                  }
              }
          }
  
          // Sort the servers by expiry.
          Collections.sort(mAllServers);
  
          // Prune excess entries and update the current server list.
          return updateCurrentServers();
      }
  
      private synchronized boolean updateExistingEntry(InetAddress address, long expiry) {
          DnsServerEntry existing = mIndex.get(address);
          if (existing != null) {
              existing.expiry = expiry;
              return true;
          }
          return false;
      }
  
      private synchronized boolean updateCurrentServers() {
          long now = System.currentTimeMillis();
          boolean changed = false;
  
          // Prune excess or expired entries.
          for (int i = mAllServers.size() - 1; i >= 0; i--) {
              if (i >= NUM_SERVERS || mAllServers.get(i).expiry < now) {
                  DnsServerEntry removed = mAllServers.remove(i);
                  mIndex.remove(removed.address);
                  changed |= mCurrentServers.remove(removed.address);
              } else {
                  break;
              }
          }
  
          // Add servers to the current set, in order of decreasing lifetime, until it has enough.
          // Prefer existing servers over new servers in order to minimize updates to the rest of the
          // system and avoid persistent oscillations.
          for (DnsServerEntry entry : mAllServers) {
              if (mCurrentServers.size() < NUM_CURRENT_SERVERS) {
                  changed |= mCurrentServers.add(entry.address);
              } else {
                  break;
              }
          }
          return changed;
      }
  }
  