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 com.android.modules.utils.build.SdkLevel;
33 
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.Set;
41 
42 /**
43  * Tracker for clients connected to downstreams.
44  *
45  * <p>This class is not thread safe, it is intended to be used only from the tethering handler
46  * thread.
47  */
48 public class ConnectedClientsTracker {
49     private final Clock mClock;
50 
51     @NonNull
52     private List<WifiClient> mLastWifiClients = Collections.emptyList();
53     @NonNull
54     private List<WifiClient> mLastLocalOnlyClients = Collections.emptyList();
55     @NonNull
56     private List<TetheredClient> mLastTetheredClients = Collections.emptyList();
57 
58     @VisibleForTesting
59     static class Clock {
elapsedRealtime()60         public long elapsedRealtime() {
61             return SystemClock.elapsedRealtime();
62         }
63     }
64 
ConnectedClientsTracker()65     public ConnectedClientsTracker() {
66         this(new Clock());
67     }
68 
69     @VisibleForTesting
ConnectedClientsTracker(Clock clock)70     ConnectedClientsTracker(Clock clock) {
71         mClock = clock;
72     }
73 
74     /**
75      * Update the tracker with new connected clients.
76      *
77      * <p>The new list can be obtained through {@link #getLastTetheredClients()}.
78      * @param ipServers The IpServers used to assign addresses to clients.
79      * @param wifiClients The list of L2-connected WiFi clients that are connected to a global
80      *                    hotspot. Null for no change since last update.
81      * @param localOnlyClients The list of L2-connected WiFi clients that are connected to localOnly
82      *                    hotspot. Null for no change since last update.
83      * @return True if the list of clients changed since the last calculation.
84      */
updateConnectedClients( Iterable<IpServer> ipServers, @Nullable List<WifiClient> wifiClients, @Nullable List<WifiClient> localOnlyClients)85     public boolean updateConnectedClients(
86             Iterable<IpServer> ipServers, @Nullable List<WifiClient> wifiClients,
87             @Nullable List<WifiClient> localOnlyClients) {
88         final long now = mClock.elapsedRealtime();
89 
90         if (wifiClients != null) mLastWifiClients = wifiClients;
91         if (localOnlyClients != null) mLastLocalOnlyClients = localOnlyClients;
92 
93         final Set<MacAddress> wifiClientMacs = getClientMacs(mLastWifiClients);
94         final Set<MacAddress> localOnlyClientMacs = getClientMacs(mLastLocalOnlyClients);
95 
96         // Build the list of non-expired leases from all IpServers, grouped by mac address
97         final Map<MacAddress, TetheredClient> clientsMap = new HashMap<>();
98         for (IpServer server : ipServers) {
99             final Set<MacAddress> connectedClientMacs;
100             switch (server.servingMode()) {
101                 case IpServer.STATE_TETHERED:
102                     connectedClientMacs = wifiClientMacs;
103                     break;
104                 case IpServer.STATE_LOCAL_ONLY:
105                     // Before T, SAP and LOHS both use wifiClientMacs because
106                     // registerLocalOnlyHotspotSoftApCallback didn't exist.
107                     connectedClientMacs = SdkLevel.isAtLeastT()
108                             ? localOnlyClientMacs : wifiClientMacs;
109                     break;
110                 default:
111                     continue;
112             }
113 
114             for (TetheredClient client : server.getAllLeases()) {
115                 if (client.getTetheringType() == TETHERING_WIFI
116                         && !connectedClientMacs.contains(client.getMacAddress())) {
117                     // Skip leases of WiFi clients that are not (or no longer) L2-connected
118                     continue;
119                 }
120                 final TetheredClient prunedClient = pruneExpired(client, now);
121                 if (prunedClient == null) continue; // All addresses expired
122 
123                 addLease(clientsMap, prunedClient);
124             }
125         }
126 
127         // TODO: add IPv6 addresses from netlink
128 
129         // Add connected WiFi clients that do not have any known address
130         addWifiClientsIfNoLeases(clientsMap, wifiClientMacs);
131         addWifiClientsIfNoLeases(clientsMap, localOnlyClientMacs);
132 
133         final HashSet<TetheredClient> clients = new HashSet<>(clientsMap.values());
134         final boolean clientsChanged = clients.size() != mLastTetheredClients.size()
135                 || !clients.containsAll(mLastTetheredClients);
136         mLastTetheredClients = Collections.unmodifiableList(new ArrayList<>(clients));
137         return clientsChanged;
138     }
139 
addWifiClientsIfNoLeases( final Map<MacAddress, TetheredClient> clientsMap, final Set<MacAddress> clientMacs)140     private static void addWifiClientsIfNoLeases(
141             final Map<MacAddress, TetheredClient> clientsMap, final Set<MacAddress> clientMacs) {
142         for (MacAddress mac : clientMacs) {
143             if (clientsMap.containsKey(mac)) continue;
144             clientsMap.put(mac, new TetheredClient(
145                     mac, Collections.emptyList() /* addresses */, TETHERING_WIFI));
146         }
147     }
148 
addLease(Map<MacAddress, TetheredClient> clientsMap, TetheredClient lease)149     private static void addLease(Map<MacAddress, TetheredClient> clientsMap, TetheredClient lease) {
150         final TetheredClient aggregateClient = clientsMap.getOrDefault(
151                 lease.getMacAddress(), lease);
152         if (aggregateClient == lease) {
153             // This is the first lease with this mac address
154             clientsMap.put(lease.getMacAddress(), lease);
155             return;
156         }
157 
158         // Only add the address info; this assumes that the tethering type is the same when the mac
159         // address is the same. If a client is connected through different tethering types with the
160         // same mac address, connected clients callbacks will report all of its addresses under only
161         // one of these tethering types. This keeps the API simple considering that such a scenario
162         // would really be a rare edge case.
163         clientsMap.put(lease.getMacAddress(), aggregateClient.addAddresses(lease));
164     }
165 
166     /**
167      * Get the last list of tethered clients, as calculated in {@link #updateConnectedClients}.
168      *
169      * <p>The returned list is immutable.
170      */
171     @NonNull
getLastTetheredClients()172     public List<TetheredClient> getLastTetheredClients() {
173         return mLastTetheredClients;
174     }
175 
hasExpiredAddress(List<AddressInfo> addresses, long now)176     private static boolean hasExpiredAddress(List<AddressInfo> addresses, long now) {
177         for (AddressInfo info : addresses) {
178             if (info.getExpirationTime() <= now) {
179                 return true;
180             }
181         }
182         return false;
183     }
184 
185     @Nullable
pruneExpired(TetheredClient client, long now)186     private static TetheredClient pruneExpired(TetheredClient client, long now) {
187         final List<AddressInfo> addresses = client.getAddresses();
188         if (addresses.size() == 0) return null;
189         if (!hasExpiredAddress(addresses, now)) return client;
190 
191         final ArrayList<AddressInfo> newAddrs = new ArrayList<>(addresses.size() - 1);
192         for (AddressInfo info : addresses) {
193             if (info.getExpirationTime() > now) {
194                 newAddrs.add(info);
195             }
196         }
197 
198         if (newAddrs.size() == 0) {
199             return null;
200         }
201         return new TetheredClient(client.getMacAddress(), newAddrs, client.getTetheringType());
202     }
203 
204     @NonNull
getClientMacs(@onNull List<WifiClient> clients)205     private static Set<MacAddress> getClientMacs(@NonNull List<WifiClient> clients) {
206         final Set<MacAddress> macs = new HashSet<>(clients.size());
207         for (WifiClient c : clients) {
208             macs.add(c.getMacAddress());
209         }
210         return macs;
211     }
212 }
213