/* * 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 android.net.ip; import static android.system.OsConstants.AF_INET6; import static android.system.OsConstants.AF_UNSPEC; import static android.system.OsConstants.IFF_LOOPBACK; import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT; import static com.android.net.module.util.NetworkStackConstants.INFINITE_LEASE; import static com.android.net.module.util.netlink.NetlinkConstants.IFF_LOWER_UP; import static com.android.net.module.util.netlink.NetlinkConstants.RTM_F_CLONED; import static com.android.net.module.util.netlink.NetlinkConstants.RTN_UNICAST; import static com.android.net.module.util.netlink.NetlinkConstants.RTPROT_KERNEL; import static com.android.net.module.util.netlink.NetlinkConstants.RTPROT_RA; import static com.android.net.module.util.netlink.NetlinkConstants.RT_SCOPE_UNIVERSE; import android.app.AlarmManager; import android.content.Context; import android.net.InetAddresses; import android.net.IpPrefix; import android.net.LinkAddress; import android.net.LinkProperties; import android.net.RouteInfo; import android.os.Handler; import android.os.SystemClock; import android.system.OsConstants; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.net.module.util.HexDump; import com.android.net.module.util.InetAddressUtils; import com.android.net.module.util.InterfaceParams; import com.android.net.module.util.SharedLog; import com.android.net.module.util.ip.NetlinkMonitor; import com.android.net.module.util.netlink.NduseroptMessage; import com.android.net.module.util.netlink.NetlinkConstants; import com.android.net.module.util.netlink.NetlinkMessage; import com.android.net.module.util.netlink.RtNetlinkAddressMessage; import com.android.net.module.util.netlink.RtNetlinkLinkMessage; import com.android.net.module.util.netlink.RtNetlinkRouteMessage; import com.android.net.module.util.netlink.StructIfacacheInfo; import com.android.net.module.util.netlink.StructIfaddrMsg; import com.android.net.module.util.netlink.StructIfinfoMsg; import com.android.net.module.util.netlink.StructNdOptPref64; import com.android.net.module.util.netlink.StructNdOptRdnss; import com.android.networkstack.apishim.NetworkInformationShimImpl; import com.android.networkstack.apishim.common.NetworkInformationShim; import java.net.Inet6Address; 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; import java.util.concurrent.TimeUnit; /** * Keeps track of link configuration received from Netd. * * An instance of this class is constructed by passing in an interface name and a callback. When * the class receives update notifications, 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. * - All accesses to mLinkProperties must be synchronized(this). All the other * member variables are immutable once the object is constructed. * * TODO: Now that all the methods are called on the handler thread, remove synchronization and * pass the LinkProperties to the update() callback. * * @hide */ public class IpClientLinkObserver { private final String mTag; /** * Callback used by {@link IpClientLinkObserver} to send update notifications. */ public interface Callback { /** * Called when some properties of the link were updated. * * @param linkState Whether the interface link state is up as per the latest * {@link #onInterfaceLinkStateChanged(String, boolean)} callback. This * should only be used for metrics purposes, as it could be inconsistent * with {@link #getLinkProperties()} in particular. */ void update(boolean linkState); /** * Called when an IPv6 address was removed from the interface. * * @param addr The removed IPv6 address. */ void onIpv6AddressRemoved(Inet6Address addr); /** * Called when the clat interface was added/removed. * * @param add True: clat interface was added. * False: clat interface was removed. */ void onClatInterfaceStateUpdate(boolean add); } /** Configuration parameters for IpClientLinkObserver. */ public static class Configuration { public final int minRdnssLifetime; public final boolean populateLinkAddressLifetime; public Configuration(int minRdnssLifetime, boolean populateLinkAddressLifetime) { this.minRdnssLifetime = minRdnssLifetime; this.populateLinkAddressLifetime = populateLinkAddressLifetime; } } private final Context mContext; private final String mInterfaceName; private final Callback mCallback; private final LinkProperties mLinkProperties; private boolean mInterfaceLinkState; private DnsServerRepository mDnsServerRepository; private final AlarmManager mAlarmManager; private final Configuration mConfig; private final Handler mHandler; private final IpClient.Dependencies mDependencies; private final String mClatInterfaceName; private final IpClientNetlinkMonitor mNetlinkMonitor; private final NetworkInformationShim mShim; private final AlarmManager.OnAlarmListener mExpirePref64Alarm; private long mNat64PrefixExpiry; /** * Current interface index. Most of this class (and of IpClient), only uses interface names, * not interface indices. This means that the interface index can in theory change, and that * it's not necessarily correct to get the interface name at object creation time (and in * fact, when the object is created, the interface might not even exist). * TODO: once all netlink events pass through this class, stop depending on interface names. */ private int mIfindex; // This must match the interface prefix in clatd.c. // TODO: Revert this hack once IpClient and Nat464Xlat work in concert. protected static final String CLAT_PREFIX = "v4-"; private static final boolean DBG = true; // The default socket receive buffer size in bytes(4MB). If too many netlink messages are // sent too quickly, those messages can overflow the socket receive buffer. Set a large-enough // recv buffer size to avoid the ENOBUFS as much as possible. @VisibleForTesting static final String CONFIG_SOCKET_RECV_BUFSIZE = "ipclient_netlink_sock_recv_buf_size"; @VisibleForTesting static final int SOCKET_RECV_BUFSIZE = 4 * 1024 * 1024; public IpClientLinkObserver(Context context, Handler h, String iface, Callback callback, Configuration config, SharedLog log, IpClient.Dependencies deps) { mContext = context; mInterfaceName = iface; mClatInterfaceName = CLAT_PREFIX + iface; mTag = "IpClient/" + mInterfaceName; mCallback = callback; mLinkProperties = new LinkProperties(); mLinkProperties.setInterfaceName(mInterfaceName); mConfig = config; mHandler = h; mInterfaceLinkState = true; // Assume up by default mDnsServerRepository = new DnsServerRepository(config.minRdnssLifetime); mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); mDependencies = deps; mNetlinkMonitor = deps.makeIpClientNetlinkMonitor(h, log, mTag, getSocketReceiveBufferSize(), (nlMsg, whenMs) -> processNetlinkMessage(nlMsg, whenMs)); mShim = NetworkInformationShimImpl.newInstance(); mExpirePref64Alarm = new IpClientObserverAlarmListener(); mHandler.post(() -> { if (!mNetlinkMonitor.start()) { Log.wtf(mTag, "Fail to start NetlinkMonitor."); } }); } public void shutdown() { mHandler.post(mNetlinkMonitor::stop); } private void maybeLog(String operation, String iface, LinkAddress address) { if (DBG) { Log.d(mTag, operation + ": " + address + " on " + iface + " flags " + "0x" + HexDump.toHexString(address.getFlags()) + " scope " + address.getScope()); } } private void maybeLog(String operation, int ifindex, LinkAddress address) { maybeLog(operation, "ifindex " + ifindex, address); } private void maybeLog(String operation, Object o) { if (DBG) { Log.d(mTag, operation + ": " + o.toString()); } } private int getSocketReceiveBufferSize() { final int size = mDependencies.getDeviceConfigPropertyInt(CONFIG_SOCKET_RECV_BUFSIZE, SOCKET_RECV_BUFSIZE /* default value */); if (size < 0) { throw new IllegalArgumentException("Invalid SO_RCVBUF " + size); } return size; } private synchronized void updateInterfaceLinkStateChanged(boolean state) { setInterfaceLinkStateLocked(state); } private void updateInterfaceDnsServerInfo(long lifetime, final String[] addresses) { final boolean changed = mDnsServerRepository.addServers(lifetime, addresses); final boolean linkState; if (changed) { maybeLog("interfaceDnsServerInfo", Arrays.toString(addresses)); synchronized (this) { mDnsServerRepository.setDnsServersOn(mLinkProperties); linkState = getInterfaceLinkStateLocked(); } mCallback.update(linkState); } } private boolean updateInterfaceAddress(@NonNull final LinkAddress address, boolean add) { final boolean changed; final boolean linkState; synchronized (this) { if (add) { changed = mLinkProperties.addLinkAddress(address); } else { changed = mLinkProperties.removeLinkAddress(address); } linkState = getInterfaceLinkStateLocked(); } if (changed) { mCallback.update(linkState); if (!add && address.isIpv6()) { final Inet6Address addr = (Inet6Address) address.getAddress(); mCallback.onIpv6AddressRemoved(addr); } } return changed; } private boolean updateInterfaceRoute(final RouteInfo route, boolean add) { final boolean changed; final boolean linkState; synchronized (this) { if (add) { changed = mLinkProperties.addRoute(route); } else { changed = mLinkProperties.removeRoute(route); } linkState = getInterfaceLinkStateLocked(); } if (changed) { mCallback.update(linkState); } return changed; } private void updateInterfaceRemoved() { // 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. final boolean linkState; synchronized (this) { clearLinkProperties(); linkState = getInterfaceLinkStateLocked(); } mCallback.update(linkState); } /** * Returns a copy of this object's LinkProperties. */ public synchronized LinkProperties getLinkProperties() { return new LinkProperties(mLinkProperties); } /** * Reset this object's LinkProperties. */ 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(mConfig.minRdnssLifetime); cancelPref64Alarm(); mLinkProperties.clear(); mLinkProperties.setInterfaceName(mInterfaceName); } private boolean getInterfaceLinkStateLocked() { return mInterfaceLinkState; } private void setInterfaceLinkStateLocked(boolean state) { mInterfaceLinkState = state; } /** Notifies this object of new interface parameters. */ public void setInterfaceParams(InterfaceParams params) { setIfindex(params.index); } /** Notifies this object not to listen on any interface. */ public void clearInterfaceParams() { setIfindex(0); // 0 is never a valid ifindex. } private void setIfindex(int ifindex) { if (!mNetlinkMonitor.isRunning()) { Log.wtf(mTag, "NetlinkMonitor is not running when setting interface parameter!"); } mIfindex = ifindex; } private static boolean isSupportedRouteProtocol(RtNetlinkRouteMessage msg) { // Checks whether the protocol is supported. The behaviour is defined by the legacy // implementation in NetlinkEvent.cpp. return msg.getRtMsgHeader().protocol == RTPROT_KERNEL || msg.getRtMsgHeader().protocol == RTPROT_RA; } private static boolean isGlobalUnicastRoute(RtNetlinkRouteMessage msg) { return msg.getRtMsgHeader().scope == RT_SCOPE_UNIVERSE && msg.getRtMsgHeader().type == RTN_UNICAST; } /** * Simple NetlinkMonitor. Listen for netlink events from kernel. * All methods except the constructor must be called on the handler thread. */ static class IpClientNetlinkMonitor extends NetlinkMonitor { /** * An interface used to process the received netlink messages, which is easiler to inject * the function in unit test. */ public interface INetlinkMessageProcessor { void processNetlinkMessage(@NonNull NetlinkMessage nlMsg, long whenMs); } private final Handler mHandler; private final INetlinkMessageProcessor mNetlinkMessageProcessor; IpClientNetlinkMonitor(Handler h, SharedLog log, String tag, int sockRcvbufSize, INetlinkMessageProcessor p) { super(h, log, tag, OsConstants.NETLINK_ROUTE, (NetlinkConstants.RTMGRP_ND_USEROPT | NetlinkConstants.RTMGRP_LINK | NetlinkConstants.RTMGRP_IPV4_IFADDR | NetlinkConstants.RTMGRP_IPV6_IFADDR | NetlinkConstants.RTMGRP_IPV6_ROUTE), sockRcvbufSize); mHandler = h; mNetlinkMessageProcessor = p; } @Override protected void processNetlinkMessage(NetlinkMessage nlMsg, long whenMs) { mNetlinkMessageProcessor.processNetlinkMessage(nlMsg, whenMs); } protected boolean isRunning() { return super.isRunning(); } } private class IpClientObserverAlarmListener implements AlarmManager.OnAlarmListener { @Override public void onAlarm() { // Ignore the alarm if cancelPref64Alarm has already been called. // // TODO: in the rare case where the alarm fires and posts the lambda to the handler // thread while we are processing an RA that changes the lifetime of the same prefix, // this code will run anyway even if the alarm is rescheduled or cancelled. If the // lifetime in the RA is zero this code will correctly do nothing, but if the lifetime // is nonzero then the prefix will be added and immediately removed by this code. if (mNat64PrefixExpiry == 0) return; updatePref64(mShim.getNat64Prefix(mLinkProperties), mNat64PrefixExpiry, mNat64PrefixExpiry); } } private void cancelPref64Alarm() { // Clear the expiry in case the alarm just fired and has not been processed yet. if (mNat64PrefixExpiry == 0) return; mNat64PrefixExpiry = 0; mAlarmManager.cancel(mExpirePref64Alarm); } private void schedulePref64Alarm() { // There is no need to cancel any existing alarms, because we are using the same // OnAlarmListener object, and each such listener can only have at most one alarm. final String tag = mTag + ".PREF64"; mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, mNat64PrefixExpiry, tag, mExpirePref64Alarm, mHandler); } /** * Processes a PREF64 ND option. * * @param prefix The NAT64 prefix. * @param now The time (as determined by SystemClock.elapsedRealtime) when the event * that triggered this method was received. * @param expiry The time (as determined by SystemClock.elapsedRealtime) when the option * expires. */ private synchronized void updatePref64(IpPrefix prefix, final long now, final long expiry) { final IpPrefix currentPrefix = mShim.getNat64Prefix(mLinkProperties); // If the prefix matches the current prefix, refresh its lifetime. if (prefix.equals(currentPrefix)) { mNat64PrefixExpiry = expiry; if (expiry > now) schedulePref64Alarm(); } // If we already have a prefix, continue using it and ignore the new one. Stopping and // restarting clatd is disruptive because it will break existing IPv4 connections. // Note: this means that if we receive an RA that adds a new prefix and deletes the old // prefix, we might receive and ignore the new prefix, then delete the old prefix, and // have no prefix until the next RA is received. This is because the kernel returns ND // user options one at a time even if they are in the same RA. // TODO: keep track of the last few prefixes seen, like DnsServerRepository does. if (mNat64PrefixExpiry > now) return; // The current prefix has expired. Either replace it with the new one or delete it. if (expiry > now) { // If expiry > now, then prefix != currentPrefix (due to the return statement above) mShim.setNat64Prefix(mLinkProperties, prefix); mNat64PrefixExpiry = expiry; schedulePref64Alarm(); } else { mShim.setNat64Prefix(mLinkProperties, null); cancelPref64Alarm(); } mCallback.update(getInterfaceLinkStateLocked()); } private void processPref64Option(StructNdOptPref64 opt, final long now) { final long expiry = now + TimeUnit.SECONDS.toMillis(opt.lifetime); updatePref64(opt.prefix, now, expiry); } private void processRdnssOption(StructNdOptRdnss opt) { final String[] addresses = new String[opt.servers.length]; for (int i = 0; i < opt.servers.length; i++) { final Inet6Address addr = InetAddressUtils.withScopeId(opt.servers[i], mIfindex); addresses[i] = addr.getHostAddress(); } updateInterfaceDnsServerInfo(opt.header.lifetime, addresses); } private void processNduseroptMessage(NduseroptMessage msg, final long whenMs) { if (msg.family != AF_INET6 || msg.option == null || msg.ifindex != mIfindex) return; if (msg.icmp_type != (byte) ICMPV6_ROUTER_ADVERTISEMENT) return; switch (msg.option.type) { case StructNdOptPref64.TYPE: processPref64Option((StructNdOptPref64) msg.option, whenMs); break; case StructNdOptRdnss.TYPE: processRdnssOption((StructNdOptRdnss) msg.option); break; default: // TODO: implement DNSSL. break; } } private void updateClatInterfaceLinkState(@Nullable final String ifname, short nlMsgType) { switch (nlMsgType) { case NetlinkConstants.RTM_NEWLINK: mCallback.onClatInterfaceStateUpdate(true /* add interface */); break; case NetlinkConstants.RTM_DELLINK: mCallback.onClatInterfaceStateUpdate(false /* remove interface */); break; default: Log.e(mTag, "unsupported rtnetlink link msg type " + nlMsgType); break; } } private void processRtNetlinkLinkMessage(RtNetlinkLinkMessage msg) { // Check if receiving netlink link state update for clat interface. final String ifname = msg.getInterfaceName(); final short nlMsgType = msg.getHeader().nlmsg_type; final StructIfinfoMsg ifinfoMsg = msg.getIfinfoHeader(); if (mClatInterfaceName.equals(ifname)) { updateClatInterfaceLinkState(ifname, nlMsgType); return; } if (ifinfoMsg.family != AF_UNSPEC || ifinfoMsg.index != mIfindex) return; if ((ifinfoMsg.flags & IFF_LOOPBACK) != 0) return; switch (nlMsgType) { case NetlinkConstants.RTM_NEWLINK: final boolean state = (ifinfoMsg.flags & IFF_LOWER_UP) != 0; maybeLog("interfaceLinkStateChanged", "ifindex " + mIfindex + (state ? " up" : " down")); updateInterfaceLinkStateChanged(state); break; case NetlinkConstants.RTM_DELLINK: maybeLog("interfaceRemoved", ifname); updateInterfaceRemoved(); break; default: Log.e(mTag, "Unknown rtnetlink link msg type " + nlMsgType); break; } } private void processRtNetlinkAddressMessage(RtNetlinkAddressMessage msg) { final StructIfaddrMsg ifaddrMsg = msg.getIfaddrHeader(); if (ifaddrMsg.index != mIfindex) return; final StructIfacacheInfo cacheInfo = msg.getIfacacheInfo(); long deprecationTime = LinkAddress.LIFETIME_UNKNOWN; long expirationTime = LinkAddress.LIFETIME_UNKNOWN; if (cacheInfo != null && mConfig.populateLinkAddressLifetime) { deprecationTime = LinkAddress.LIFETIME_PERMANENT; expirationTime = LinkAddress.LIFETIME_PERMANENT; final long now = SystemClock.elapsedRealtime(); // TODO: change INFINITE_LEASE to long so the pesky conversions can be removed. if (cacheInfo.preferred < Integer.toUnsignedLong(INFINITE_LEASE)) { deprecationTime = now + (cacheInfo.preferred /* seconds */ * 1000); } if (cacheInfo.valid < Integer.toUnsignedLong(INFINITE_LEASE)) { expirationTime = now + (cacheInfo.valid /* seconds */ * 1000); } } final LinkAddress la = new LinkAddress(msg.getIpAddress(), ifaddrMsg.prefixLen, msg.getFlags(), ifaddrMsg.scope, deprecationTime, expirationTime); switch (msg.getHeader().nlmsg_type) { case NetlinkConstants.RTM_NEWADDR: if (updateInterfaceAddress(la, true /* add address */)) { maybeLog("addressUpdated", mIfindex, la); } break; case NetlinkConstants.RTM_DELADDR: if (updateInterfaceAddress(la, false /* remove address */)) { maybeLog("addressRemoved", mIfindex, la); } break; default: Log.e(mTag, "Unknown rtnetlink address msg type " + msg.getHeader().nlmsg_type); } } private void processRtNetlinkRouteMessage(RtNetlinkRouteMessage msg) { if (msg.getInterfaceIndex() != mIfindex) return; // Ignore the unsupported route protocol and non-global unicast routes. if (!isSupportedRouteProtocol(msg) || !isGlobalUnicastRoute(msg) // don't support source routing || (msg.getRtMsgHeader().srcLen != 0) // don't support cloned routes || ((msg.getRtMsgHeader().flags & RTM_F_CLONED) != 0)) { return; } final RouteInfo route = new RouteInfo(msg.getDestination(), msg.getGateway(), mInterfaceName, msg.getRtMsgHeader().type); switch (msg.getHeader().nlmsg_type) { case NetlinkConstants.RTM_NEWROUTE: if (updateInterfaceRoute(route, true /* add route */)) { maybeLog("routeUpdated", route); } break; case NetlinkConstants.RTM_DELROUTE: if (updateInterfaceRoute(route, false /* remove route */)) { maybeLog("routeRemoved", route); } break; default: Log.e(mTag, "Unknown rtnetlink route msg type " + msg.getHeader().nlmsg_type); break; } } private void processNetlinkMessage(NetlinkMessage nlMsg, long whenMs) { if (nlMsg instanceof NduseroptMessage) { processNduseroptMessage((NduseroptMessage) nlMsg, whenMs); } else if (nlMsg instanceof RtNetlinkLinkMessage) { processRtNetlinkLinkMessage((RtNetlinkLinkMessage) nlMsg); } else if (nlMsg instanceof RtNetlinkAddressMessage) { processRtNetlinkAddressMessage((RtNetlinkAddressMessage) nlMsg); } else if (nlMsg instanceof RtNetlinkRouteMessage) { processRtNetlinkRouteMessage((RtNetlinkRouteMessage) nlMsg); } else { Log.e(mTag, "Unknown netlink message: " + nlMsg); } } /** * 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). * */ private static class DnsServerRepository { /** How many DNS servers we will use. 3 is suggested by RFC 6106. */ static final int NUM_CURRENT_SERVERS = 3; /** How many DNS servers we'll keep track of, in total. */ static final int NUM_SERVERS = 12; /** Stores up to {@code NUM_CURRENT_SERVERS} DNS servers we're currently using. */ private Set 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 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 mIndex; /** * Minimum (non-zero) RDNSS lifetime to accept. */ private final int mMinLifetime; DnsServerRepository(int minLifetime) { mCurrentServers = new HashSet<>(); mAllServers = new ArrayList<>(NUM_SERVERS); mIndex = new HashMap<>(NUM_SERVERS); mMinLifetime = minLifetime; } /** 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 DNS servers to use. */ public synchronized boolean addServers(long lifetime, String[] addresses) { // If the servers are below the minimum lifetime, don't change anything. if (lifetime != 0 && lifetime < mMinLifetime) return false; // 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 = InetAddresses.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; } } /** * 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. */ private static class DnsServerEntry implements Comparable { /** 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; 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); } } }