1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.networkstack.tethering;
18 
19 import static android.net.TetheringManager.TETHERING_WIFI;
20 
21 import android.net.MacAddress;
22 import android.net.TetheredClient;
23 import android.net.TetheredClient.AddressInfo;
24 import android.net.ip.IpServer;
25 import android.net.wifi.WifiClient;
26 import android.os.SystemClock;
27 
28 import androidx.annotation.NonNull;
29 import androidx.annotation.Nullable;
30 import androidx.annotation.VisibleForTesting;
31 
32 import java.util.ArrayList;
33 import java.util.Collections;
34 import java.util.HashMap;
35 import java.util.HashSet;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Set;
39 
40 /**
41  * Tracker for clients connected to downstreams.
42  *
43  * <p>This class is not thread safe, it is intended to be used only from the tethering handler
44  * thread.
45  */
46 public class ConnectedClientsTracker {
47     private final Clock mClock;
48 
49     @NonNull
50     private List<WifiClient> mLastWifiClients = Collections.emptyList();
51     @NonNull
52     private List<TetheredClient> mLastTetheredClients = Collections.emptyList();
53 
54     @VisibleForTesting
55     static class Clock {
elapsedRealtime()56         public long elapsedRealtime() {
57             return SystemClock.elapsedRealtime();
58         }
59     }
60 
ConnectedClientsTracker()61     public ConnectedClientsTracker() {
62         this(new Clock());
63     }
64 
65     @VisibleForTesting
ConnectedClientsTracker(Clock clock)66     ConnectedClientsTracker(Clock clock) {
67         mClock = clock;
68     }
69 
70     /**
71      * Update the tracker with new connected clients.
72      *
73      * <p>The new list can be obtained through {@link #getLastTetheredClients()}.
74      * @param ipServers The IpServers used to assign addresses to clients.
75      * @param wifiClients The list of L2-connected WiFi clients. Null for no change since last
76      *                    update.
77      * @return True if the list of clients changed since the last calculation.
78      */
updateConnectedClients( Iterable<IpServer> ipServers, @Nullable List<WifiClient> wifiClients)79     public boolean updateConnectedClients(
80             Iterable<IpServer> ipServers, @Nullable List<WifiClient> wifiClients) {
81         final long now = mClock.elapsedRealtime();
82 
83         if (wifiClients != null) {
84             mLastWifiClients = wifiClients;
85         }
86         final Set<MacAddress> wifiClientMacs = getClientMacs(mLastWifiClients);
87 
88         // Build the list of non-expired leases from all IpServers, grouped by mac address
89         final Map<MacAddress, TetheredClient> clientsMap = new HashMap<>();
90         for (IpServer server : ipServers) {
91             for (TetheredClient client : server.getAllLeases()) {
92                 if (client.getTetheringType() == TETHERING_WIFI
93                         && !wifiClientMacs.contains(client.getMacAddress())) {
94                     // Skip leases of WiFi clients that are not (or no longer) L2-connected
95                     continue;
96                 }
97                 final TetheredClient prunedClient = pruneExpired(client, now);
98                 if (prunedClient == null) continue; // All addresses expired
99 
100                 addLease(clientsMap, prunedClient);
101             }
102         }
103 
104         // TODO: add IPv6 addresses from netlink
105 
106         // Add connected WiFi clients that do not have any known address
107         for (MacAddress client : wifiClientMacs) {
108             if (clientsMap.containsKey(client)) continue;
109             clientsMap.put(client, new TetheredClient(
110                     client, Collections.emptyList() /* addresses */, TETHERING_WIFI));
111         }
112 
113         final HashSet<TetheredClient> clients = new HashSet<>(clientsMap.values());
114         final boolean clientsChanged = clients.size() != mLastTetheredClients.size()
115                 || !clients.containsAll(mLastTetheredClients);
116         mLastTetheredClients = Collections.unmodifiableList(new ArrayList<>(clients));
117         return clientsChanged;
118     }
119 
addLease(Map<MacAddress, TetheredClient> clientsMap, TetheredClient lease)120     private static void addLease(Map<MacAddress, TetheredClient> clientsMap, TetheredClient lease) {
121         final TetheredClient aggregateClient = clientsMap.getOrDefault(
122                 lease.getMacAddress(), lease);
123         if (aggregateClient == lease) {
124             // This is the first lease with this mac address
125             clientsMap.put(lease.getMacAddress(), lease);
126             return;
127         }
128 
129         // Only add the address info; this assumes that the tethering type is the same when the mac
130         // address is the same. If a client is connected through different tethering types with the
131         // same mac address, connected clients callbacks will report all of its addresses under only
132         // one of these tethering types. This keeps the API simple considering that such a scenario
133         // would really be a rare edge case.
134         clientsMap.put(lease.getMacAddress(), aggregateClient.addAddresses(lease));
135     }
136 
137     /**
138      * Get the last list of tethered clients, as calculated in {@link #updateConnectedClients}.
139      *
140      * <p>The returned list is immutable.
141      */
142     @NonNull
getLastTetheredClients()143     public List<TetheredClient> getLastTetheredClients() {
144         return mLastTetheredClients;
145     }
146 
hasExpiredAddress(List<AddressInfo> addresses, long now)147     private static boolean hasExpiredAddress(List<AddressInfo> addresses, long now) {
148         for (AddressInfo info : addresses) {
149             if (info.getExpirationTime() <= now) {
150                 return true;
151             }
152         }
153         return false;
154     }
155 
156     @Nullable
pruneExpired(TetheredClient client, long now)157     private static TetheredClient pruneExpired(TetheredClient client, long now) {
158         final List<AddressInfo> addresses = client.getAddresses();
159         if (addresses.size() == 0) return null;
160         if (!hasExpiredAddress(addresses, now)) return client;
161 
162         final ArrayList<AddressInfo> newAddrs = new ArrayList<>(addresses.size() - 1);
163         for (AddressInfo info : addresses) {
164             if (info.getExpirationTime() > now) {
165                 newAddrs.add(info);
166             }
167         }
168 
169         if (newAddrs.size() == 0) {
170             return null;
171         }
172         return new TetheredClient(client.getMacAddress(), newAddrs, client.getTetheringType());
173     }
174 
175     @NonNull
getClientMacs(@onNull List<WifiClient> clients)176     private static Set<MacAddress> getClientMacs(@NonNull List<WifiClient> clients) {
177         final Set<MacAddress> macs = new HashSet<>(clients.size());
178         for (WifiClient c : clients) {
179             macs.add(c.getMacAddress());
180         }
181         return macs;
182     }
183 }
184