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 }