summaryrefslogtreecommitdiff
path: root/src/android/net/ip/IpClientLinkObserver.java
blob: dc58e7bbecbc11315ca83a480e0018ddf327aabd (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
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
/*
 * 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 com.android.server.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;

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.net.netlink.NduseroptMessage;
import android.net.netlink.NetlinkConstants;
import android.net.netlink.NetlinkMessage;
import android.net.netlink.StructNdOptPref64;
import android.net.util.InterfaceParams;
import android.net.util.SharedLog;
import android.os.Handler;
import android.system.OsConstants;
import android.util.Log;

import com.android.networkstack.apishim.NetworkInformationShim;
import com.android.networkstack.apishim.NetworkInformationShimImpl;
import com.android.server.NetworkObserver;

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. The
 * owner is then responsible for registering the tracker with NetworkObserverRegistry. 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.
 * - Most of the methods in the class are implementing NetworkObserver and are called
 *   on the handler used to register the observer.
 * - 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.
 * TODO: Stop extending NetworkObserver and get events from netlink directly.
 *
 * @hide
 */
public class IpClientLinkObserver implements NetworkObserver {
    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.
         */
        void update();
    }

    /** Configuration parameters for IpClientLinkObserver. */
    public static class Configuration {
        public final int minRdnssLifetime;

        public Configuration(int minRdnssLifetime) {
            this.minRdnssLifetime = minRdnssLifetime;
        }
    }

    private final String mInterfaceName;
    private final Callback mCallback;
    private final LinkProperties mLinkProperties;
    private DnsServerRepository mDnsServerRepository;
    private final AlarmManager mAlarmManager;
    private final Configuration mConfig;

    private final MyNetlinkMonitor mNetlinkMonitor;

    private static final boolean DBG = false;

    public IpClientLinkObserver(Context context, Handler h, String iface, Callback callback,
            Configuration config, SharedLog log) {
        mInterfaceName = iface;
        mTag = "NetlinkTracker/" + mInterfaceName;
        mCallback = callback;
        mLinkProperties = new LinkProperties();
        mLinkProperties.setInterfaceName(mInterfaceName);
        mConfig = config;
        mDnsServerRepository = new DnsServerRepository(config.minRdnssLifetime);
        mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        mNetlinkMonitor = new MyNetlinkMonitor(h, log, mTag);
        h.post(mNetlinkMonitor::start);
    }

    private void maybeLog(String operation, String iface, LinkAddress address) {
        if (DBG) {
            Log.d(mTag, operation + ": " + address + " on " + iface
                    + " flags " + address.getFlags() + " scope " + address.getScope());
        }
    }

    private void maybeLog(String operation, Object o) {
        if (DBG) {
            Log.d(mTag, operation + ": " + o.toString());
        }
    }

    @Override
    public void onInterfaceRemoved(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 onInterfaceAddressUpdated(LinkAddress address, String iface) {
        if (mInterfaceName.equals(iface)) {
            maybeLog("addressUpdated", iface, address);
            boolean changed;
            synchronized (this) {
                changed = mLinkProperties.addLinkAddress(address);
            }
            if (changed) {
                mCallback.update();
            }
        }
    }

    @Override
    public void onInterfaceAddressRemoved(LinkAddress address, String iface) {
        if (mInterfaceName.equals(iface)) {
            maybeLog("addressRemoved", iface, address);
            boolean changed;
            synchronized (this) {
                changed = mLinkProperties.removeLinkAddress(address);
            }
            if (changed) {
                mCallback.update();
            }
        }
    }

    @Override
    public void onRouteUpdated(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 onRouteRemoved(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 onInterfaceDnsServerInfo(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);
    }

    /**
     * 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);
        mLinkProperties.clear();
        mLinkProperties.setInterfaceName(mInterfaceName);
    }

    /** Notifies this object of new interface parameters. */
    public void setInterfaceParams(InterfaceParams params) {
        mNetlinkMonitor.setIfindex(params.index);
    }

    /** Notifies this object not to listen on any interface. */
    public void clearInterfaceParams() {
        mNetlinkMonitor.setIfindex(0);  // 0 is never a valid ifindex.
    }

    /**
     * Simple NetlinkMonitor. Currently only listens for PREF64 events.
     * All methods except the constructor must be called on the handler thread.
     */
    private class MyNetlinkMonitor extends NetlinkMonitor {
        private final Handler mHandler;

        MyNetlinkMonitor(Handler h, SharedLog log, String tag) {
            super(h, log, tag, OsConstants.NETLINK_ROUTE, NetlinkConstants.RTMGRP_ND_USEROPT);
            mHandler = h;
        }

        private final NetworkInformationShim mShim = NetworkInformationShimImpl.newInstance();

        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;

        void setIfindex(int ifindex) {
            mIfindex = ifindex;
        }

        private final AlarmManager.OnAlarmListener mExpirePref64Alarm = () -> {
            // 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 doesn't matter (we just harmlessly cancel the alarm
            // one extra time) but if the lifetime is nonzero then the prefix will be added and
            // immediately removed by this code.
            updatePref64(mShim.getNat64Prefix(mLinkProperties),
                    mNat64PrefixExpiry, mNat64PrefixExpiry);
        };

        private void cancelPref64Alarm() {
            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);
                mNat64PrefixExpiry = 0;
                cancelPref64Alarm();
            }

            mCallback.update();
        }

        private void processPref64Option(StructNdOptPref64 opt, final long now) {
            final long expiry = now + TimeUnit.SECONDS.toMillis(opt.lifetime);
            updatePref64(opt.prefix, now, expiry);
        }

        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;

                default:
                    // TODO: implement RDNSS and DNSSL.
                    break;
            }
        }

        @Override
        protected void processNetlinkMessage(NetlinkMessage nlMsg, long whenMs) {
            if (!(nlMsg instanceof NduseroptMessage)) return;
            processNduseroptMessage((NduseroptMessage) nlMsg, whenMs);
        }
    }

    /**
     * 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<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;

        /**
         * 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<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;

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