summaryrefslogtreecommitdiffstats
path: root/core/java/com/android/server/net/NetlinkTracker.java
blob: d45982eff8bae6bf9e81d6d3bfeeaff6edf5b992 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
/*
 * 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.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;

    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 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.
     */
    public synchronized LinkProperties getLinkProperties() {
        return new LinkProperties(mLinkProperties);
    }

    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;
    }
}