• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.server.connectivity.mdns;
18 
19 import android.Manifest.permission;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.RequiresPermission;
23 import android.os.Looper;
24 import android.util.ArrayMap;
25 import android.util.Log;
26 import android.util.Pair;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 import com.android.net.module.util.DnsUtils;
30 import com.android.net.module.util.SharedLog;
31 
32 import java.io.IOException;
33 import java.io.PrintWriter;
34 import java.util.ArrayList;
35 import java.util.List;
36 import java.util.Objects;
37 
38 /**
39  * This class keeps tracking the set of registered {@link MdnsServiceBrowserListener} instances, and
40  * notify them when a mDNS service instance is found, updated, or removed?
41  */
42 public class MdnsDiscoveryManager implements MdnsSocketClientBase.Callback {
43     private static final String TAG = MdnsDiscoveryManager.class.getSimpleName();
44     public static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
45 
46     private final ExecutorProvider executorProvider;
47     private final MdnsSocketClientBase socketClient;
48     @NonNull private final SharedLog sharedLog;
49 
50     @NonNull private final PerSocketServiceTypeClients perSocketServiceTypeClients;
51     @NonNull private final DiscoveryExecutor discoveryExecutor;
52     @NonNull private final MdnsFeatureFlags mdnsFeatureFlags;
53 
54     // Only accessed on the handler thread, initialized before first use
55     @Nullable
56     private MdnsServiceCache serviceCache;
57 
58     private static class PerSocketServiceTypeClients {
59         private final ArrayMap<Pair<String, SocketKey>, MdnsServiceTypeClient> clients =
60                 new ArrayMap<>();
61 
put(@onNull String serviceType, @NonNull SocketKey socketKey, @NonNull MdnsServiceTypeClient client)62         public void put(@NonNull String serviceType, @NonNull SocketKey socketKey,
63                 @NonNull MdnsServiceTypeClient client) {
64             final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
65             final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsUpperServiceType,
66                     socketKey);
67             clients.put(perSocketServiceType, client);
68         }
69 
70         @Nullable
get( @onNull String serviceType, @NonNull SocketKey socketKey)71         public MdnsServiceTypeClient get(
72                 @NonNull String serviceType, @NonNull SocketKey socketKey) {
73             final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
74             final Pair<String, SocketKey> perSocketServiceType = new Pair<>(dnsUpperServiceType,
75                     socketKey);
76             return clients.getOrDefault(perSocketServiceType, null);
77         }
78 
getByServiceType(@onNull String serviceType)79         public List<MdnsServiceTypeClient> getByServiceType(@NonNull String serviceType) {
80             final String dnsUpperServiceType = DnsUtils.toDnsUpperCase(serviceType);
81             final List<MdnsServiceTypeClient> list = new ArrayList<>();
82             for (int i = 0; i < clients.size(); i++) {
83                 final Pair<String, SocketKey> perSocketServiceType = clients.keyAt(i);
84                 if (dnsUpperServiceType.equals(perSocketServiceType.first)) {
85                     list.add(clients.valueAt(i));
86                 }
87             }
88             return list;
89         }
90 
getBySocketKey(@onNull SocketKey socketKey)91         public List<MdnsServiceTypeClient> getBySocketKey(@NonNull SocketKey socketKey) {
92             final List<MdnsServiceTypeClient> list = new ArrayList<>();
93             for (int i = 0; i < clients.size(); i++) {
94                 final Pair<String, SocketKey> perSocketServiceType = clients.keyAt(i);
95                 if (socketKey.equals(perSocketServiceType.second)) {
96                     list.add(clients.valueAt(i));
97                 }
98             }
99             return list;
100         }
101 
getAllMdnsServiceTypeClient()102         public List<MdnsServiceTypeClient> getAllMdnsServiceTypeClient() {
103             return new ArrayList<>(clients.values());
104         }
105 
remove(@onNull MdnsServiceTypeClient client)106         public void remove(@NonNull MdnsServiceTypeClient client) {
107             for (int i = 0; i < clients.size(); ++i) {
108                 if (Objects.equals(client, clients.valueAt(i))) {
109                     clients.removeAt(i);
110                     break;
111                 }
112             }
113         }
114 
isEmpty()115         public boolean isEmpty() {
116             return clients.isEmpty();
117         }
118     }
119 
MdnsDiscoveryManager(@onNull ExecutorProvider executorProvider, @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags)120     public MdnsDiscoveryManager(@NonNull ExecutorProvider executorProvider,
121             @NonNull MdnsSocketClientBase socketClient, @NonNull SharedLog sharedLog,
122             @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
123         this.executorProvider = executorProvider;
124         this.socketClient = socketClient;
125         this.sharedLog = sharedLog;
126         this.perSocketServiceTypeClients = new PerSocketServiceTypeClients();
127         this.mdnsFeatureFlags = mdnsFeatureFlags;
128         this.discoveryExecutor = new DiscoveryExecutor(socketClient.getLooper(), mdnsFeatureFlags);
129     }
130 
131     /**
132      * Do the cleanup of the MdnsDiscoveryManager
133      */
shutDown()134     public void shutDown() {
135         discoveryExecutor.shutDown();
136     }
137 
138     /**
139      * Starts (or continue) to discovery mDNS services with given {@code serviceType}, and registers
140      * {@code listener} for receiving mDNS service discovery responses.
141      *
142      * @param serviceType   The type of the service to discover.
143      * @param listener      The {@link MdnsServiceBrowserListener} listener.
144      * @param searchOptions The {@link MdnsSearchOptions} to be used for discovering {@code
145      *                      serviceType}.
146      */
147     @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
registerListener( @onNull String serviceType, @NonNull MdnsServiceBrowserListener listener, @NonNull MdnsSearchOptions searchOptions)148     public void registerListener(
149             @NonNull String serviceType,
150             @NonNull MdnsServiceBrowserListener listener,
151             @NonNull MdnsSearchOptions searchOptions) {
152         sharedLog.i("Registering listener for serviceType: " + serviceType);
153         discoveryExecutor.checkAndRunOnHandlerThread(() ->
154                 handleRegisterListener(serviceType, listener, searchOptions));
155     }
156 
handleRegisterListener( @onNull String serviceType, @NonNull MdnsServiceBrowserListener listener, @NonNull MdnsSearchOptions searchOptions)157     private void handleRegisterListener(
158             @NonNull String serviceType,
159             @NonNull MdnsServiceBrowserListener listener,
160             @NonNull MdnsSearchOptions searchOptions) {
161         if (perSocketServiceTypeClients.isEmpty()) {
162             // First listener. Starts the socket client.
163             try {
164                 socketClient.startDiscovery();
165             } catch (IOException e) {
166                 sharedLog.e("Failed to start discover.", e);
167                 return;
168             }
169         }
170         // Request the network for discovery.
171         // This requests sockets on all networks even if the searchOptions have a given interface
172         // index (with getNetwork==null, for local interfaces), and only uses matching interfaces
173         // in that case. While this is a simple solution to only use matching sockets, a better
174         // practice would be to only request the correct socket for discovery.
175         // TODO: avoid requesting extra sockets after migrating P2P and tethering networks to local
176         // NetworkAgents.
177         socketClient.notifyNetworkRequested(listener, searchOptions.getNetwork(),
178                 new MdnsSocketClientBase.SocketCreationCallback() {
179                     @Override
180                     public void onSocketCreated(@NonNull SocketKey socketKey) {
181                         discoveryExecutor.ensureRunningOnHandlerThread();
182                         final int searchInterfaceIndex = searchOptions.getInterfaceIndex();
183                         if (searchOptions.getNetwork() == null
184                                 && searchInterfaceIndex > 0
185                                 // The interface index in options should only match interfaces that
186                                 // do not have any Network; a matching Network should be provided
187                                 // otherwise.
188                                 && (socketKey.getNetwork() != null
189                                     || socketKey.getInterfaceIndex() != searchInterfaceIndex)) {
190                             sharedLog.i("Skipping " + socketKey + " as ifIndex "
191                                     + searchInterfaceIndex + " was requested.");
192                             return;
193                         }
194 
195                         // All listeners of the same service types shares the same
196                         // MdnsServiceTypeClient.
197                         MdnsServiceTypeClient serviceTypeClient =
198                                 perSocketServiceTypeClients.get(serviceType, socketKey);
199                         if (serviceTypeClient == null) {
200                             serviceTypeClient = createServiceTypeClient(serviceType, socketKey);
201                             perSocketServiceTypeClients.put(serviceType, socketKey,
202                                     serviceTypeClient);
203                         }
204                         serviceTypeClient.startSendAndReceive(listener, searchOptions);
205                     }
206 
207                     @Override
208                     public void onSocketDestroyed(@NonNull SocketKey socketKey) {
209                         discoveryExecutor.ensureRunningOnHandlerThread();
210                         final MdnsServiceTypeClient serviceTypeClient =
211                                 perSocketServiceTypeClients.get(serviceType, socketKey);
212                         if (serviceTypeClient == null) return;
213                         // Notify all listeners that all services are removed from this socket.
214                         serviceTypeClient.notifySocketDestroyed();
215                         executorProvider.shutdownExecutorService(serviceTypeClient.getExecutor());
216                         perSocketServiceTypeClients.remove(serviceTypeClient);
217                         // The cached services may not be reliable after the socket is disconnected,
218                         // the service type client won't receive any updates for them. Therefore,
219                         // remove these cached services after exceeding the retention time
220                         // (currently 10s) if no service type client requires them.
221                         if (mdnsFeatureFlags.isCachedServicesRemovalEnabled()) {
222                             final MdnsServiceCache.CacheKey cacheKey =
223                                     serviceTypeClient.getCacheKey();
224                             discoveryExecutor.executeDelayed(
225                                     () -> handleRemoveCachedServices(cacheKey),
226                                     mdnsFeatureFlags.getCachedServicesRetentionTime());
227                         }
228                     }
229                 });
230     }
231 
232     /**
233      * Unregister {@code listener} for receiving mDNS service discovery responses. IF no listener is
234      * registered for the given service type, stops discovery for the service type.
235      *
236      * @param serviceType The type of the service to discover.
237      * @param listener    The {@link MdnsServiceBrowserListener} listener.
238      */
239     @RequiresPermission(permission.CHANGE_WIFI_MULTICAST_STATE)
unregisterListener( @onNull String serviceType, @NonNull MdnsServiceBrowserListener listener)240     public void unregisterListener(
241             @NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
242         sharedLog.i("Unregistering listener for serviceType:" + serviceType);
243         discoveryExecutor.checkAndRunOnHandlerThread(() ->
244                 handleUnregisterListener(serviceType, listener));
245     }
246 
handleUnregisterListener( @onNull String serviceType, @NonNull MdnsServiceBrowserListener listener)247     private void handleUnregisterListener(
248             @NonNull String serviceType, @NonNull MdnsServiceBrowserListener listener) {
249         // Unrequested the network.
250         socketClient.notifyNetworkUnrequested(listener);
251 
252         final List<MdnsServiceTypeClient> serviceTypeClients =
253                 perSocketServiceTypeClients.getByServiceType(serviceType);
254         if (serviceTypeClients.isEmpty()) {
255             return;
256         }
257         for (int i = 0; i < serviceTypeClients.size(); i++) {
258             final MdnsServiceTypeClient serviceTypeClient = serviceTypeClients.get(i);
259             if (serviceTypeClient.stopSendAndReceive(listener)) {
260                 // No listener is registered for the service type anymore, remove it from the list
261                 // of the service type clients.
262                 executorProvider.shutdownExecutorService(serviceTypeClient.getExecutor());
263                 perSocketServiceTypeClients.remove(serviceTypeClient);
264                 // The cached services may not be reliable after the socket is disconnected, the
265                 // service type client won't receive any updates for them. Therefore, remove these
266                 // cached services after exceeding the retention time (currently 10s) if no service
267                 // type client requires them.
268                 // Note: This removal is only called if the requested socket is still active for
269                 // other requests. If the requested socket is no longer needed after the listener
270                 // is unregistered, SocketCreationCallback#onSocketDestroyed callback will remove
271                 // both the service type client and cached services there.
272                 //
273                 // List some multiple listener cases for the cached service removal flow.
274                 //
275                 // Case 1 - Same service type, different network requests
276                 //  - Register Listener A (service type X, requesting all networks: Y and Z)
277                 //  - Create service type clients X-Y and X-Z
278                 //  - Register Listener B (service type X, requesting network Y)
279                 //  - Reuse service type client X-Y
280                 //  - Unregister Listener A
281                 //  - Socket destroyed on network Z; remove the X-Z client. Unregister the listener
282                 //    from the X-Y client and keep it, as it's still being used by Listener B.
283                 //  - Remove cached services associated with the X-Z client after 10 seconds.
284                 //
285                 // Case 2 - Different service types, same network request
286                 //  - Register Listener A (service type X, requesting network Y)
287                 //  - Create service type client X-Y
288                 //  - Register Listener B (service type Z, requesting network Y)
289                 //  - Create service type client Z-Y
290                 //  - Unregister Listener A
291                 //  - No socket is destroyed because network Y is still being used by Listener B.
292                 //  - Unregister the listener from the X-Y client, then remove it.
293                 //  - Remove cached services associated with the X-Y client after 10 seconds.
294                 if (mdnsFeatureFlags.isCachedServicesRemovalEnabled()) {
295                     final MdnsServiceCache.CacheKey cacheKey = serviceTypeClient.getCacheKey();
296                     discoveryExecutor.executeDelayed(
297                             () -> handleRemoveCachedServices(cacheKey),
298                             mdnsFeatureFlags.getCachedServicesRetentionTime());
299                 }
300             }
301         }
302         if (perSocketServiceTypeClients.isEmpty()) {
303             // No discovery request. Stops the socket client.
304             sharedLog.i("All service type listeners unregistered; stopping discovery");
305             socketClient.stopDiscovery();
306         }
307     }
308 
309     @Override
onResponseReceived(@onNull MdnsPacket packet, @NonNull SocketKey socketKey)310     public void onResponseReceived(@NonNull MdnsPacket packet, @NonNull SocketKey socketKey) {
311         discoveryExecutor.checkAndRunOnHandlerThread(() ->
312                 handleOnResponseReceived(packet, socketKey));
313     }
314 
handleOnResponseReceived(@onNull MdnsPacket packet, @NonNull SocketKey socketKey)315     private void handleOnResponseReceived(@NonNull MdnsPacket packet,
316             @NonNull SocketKey socketKey) {
317         for (MdnsServiceTypeClient serviceTypeClient : getMdnsServiceTypeClient(socketKey)) {
318             serviceTypeClient.processResponse(packet, socketKey);
319         }
320     }
321 
getMdnsServiceTypeClient(@onNull SocketKey socketKey)322     private List<MdnsServiceTypeClient> getMdnsServiceTypeClient(@NonNull SocketKey socketKey) {
323         if (socketClient.supportsRequestingSpecificNetworks()) {
324             return perSocketServiceTypeClients.getBySocketKey(socketKey);
325         } else {
326             return perSocketServiceTypeClients.getAllMdnsServiceTypeClient();
327         }
328     }
329 
330     @Override
onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode, @NonNull SocketKey socketKey)331     public void onFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
332             @NonNull SocketKey socketKey) {
333         discoveryExecutor.checkAndRunOnHandlerThread(() ->
334                 handleOnFailedToParseMdnsResponse(receivedPacketNumber, errorCode, socketKey));
335     }
336 
handleOnFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode, @NonNull SocketKey socketKey)337     private void handleOnFailedToParseMdnsResponse(int receivedPacketNumber, int errorCode,
338             @NonNull SocketKey socketKey) {
339         for (MdnsServiceTypeClient serviceTypeClient : getMdnsServiceTypeClient(socketKey)) {
340             serviceTypeClient.onFailedToParseMdnsResponse(receivedPacketNumber, errorCode);
341         }
342     }
343 
handleRemoveCachedServices(@onNull MdnsServiceCache.CacheKey cacheKey)344     private void handleRemoveCachedServices(@NonNull MdnsServiceCache.CacheKey cacheKey) {
345         // Check if there is an active service type client that requires the cached services. If so,
346         // do not remove associated services from cache.
347         for (MdnsServiceTypeClient client : getMdnsServiceTypeClient(cacheKey.mSocketKey)) {
348             if (client.getCacheKey().equals(cacheKey)) {
349                 // Found a client that has same CacheKey.
350                 return;
351             }
352         }
353         sharedLog.log("Remove cached services for " + cacheKey);
354         // No client has same CacheKey. Remove associated services.
355         getServiceCache().removeServices(cacheKey);
356     }
357 
358     @VisibleForTesting
359     @NonNull
getServiceCache()360     MdnsServiceCache getServiceCache() {
361         return serviceCache;
362     }
363 
364     @VisibleForTesting
createServiceTypeClient(@onNull String serviceType, @NonNull SocketKey socketKey)365     MdnsServiceTypeClient createServiceTypeClient(@NonNull String serviceType,
366             @NonNull SocketKey socketKey) {
367         discoveryExecutor.ensureRunningOnHandlerThread();
368         sharedLog.log("createServiceTypeClient for type:" + serviceType + " " + socketKey);
369         final String tag = serviceType + "-" + socketKey.getNetwork()
370                 + "/" + socketKey.getInterfaceIndex();
371         final Looper looper = Looper.myLooper();
372         if (serviceCache == null) {
373             serviceCache = new MdnsServiceCache(looper, mdnsFeatureFlags);
374         }
375         return new MdnsServiceTypeClient(
376                 serviceType, socketClient,
377                 executorProvider.newServiceTypeClientSchedulerExecutor(), socketKey,
378                 sharedLog.forSubComponent(tag), looper, serviceCache, mdnsFeatureFlags);
379     }
380 
381     /**
382      * Dump DiscoveryManager state.
383      */
dump(PrintWriter pw)384     public void dump(PrintWriter pw) {
385         discoveryExecutor.runWithScissorsForDumpIfReady(() -> {
386             pw.println("Clients:");
387             // Dump ServiceTypeClients
388             for (MdnsServiceTypeClient serviceTypeClient
389                     : perSocketServiceTypeClients.getAllMdnsServiceTypeClient()) {
390                 serviceTypeClient.dump(pw);
391             }
392             pw.println();
393             // Dump ServiceCache
394             pw.println("Cached services:");
395             if (serviceCache != null) {
396                 serviceCache.dump(pw, "  ");
397             }
398         });
399     }
400 }