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 }