Merge "Initial RDNSS tracking implementation."
diff --git a/core/java/com/android/server/net/NetlinkTracker.java b/core/java/com/android/server/net/NetlinkTracker.java
index 7dd8dd8..ff905bb 100644
--- a/core/java/com/android/server/net/NetlinkTracker.java
+++ b/core/java/com/android/server/net/NetlinkTracker.java
@@ -21,6 +21,16 @@
 import android.net.RouteInfo;
 import android.util.Log;
 
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
 /**
  * Keeps track of link configuration received from Netlink.
  *
@@ -67,6 +77,7 @@
     private final String mInterfaceName;
     private final Callback mCallback;
     private final LinkProperties mLinkProperties;
+    private DnsServerRepository mDnsServerRepository;
 
     private static final boolean DBG = true;
 
@@ -76,6 +87,7 @@
         mCallback = callback;
         mLinkProperties = new LinkProperties();
         mLinkProperties.setInterfaceName(mInterfaceName);
+        mDnsServerRepository = new DnsServerRepository();
     }
 
     private void maybeLog(String operation, String iface, LinkAddress address) {
@@ -147,6 +159,20 @@
         }
     }
 
+    @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.
      */
@@ -155,7 +181,185 @@
     }
 
     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;
+    }
+}