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