1 /* 2 * Copyright (C) 2022 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 static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.RequiresApi; 24 import android.net.LinkAddress; 25 import android.net.nsd.NsdServiceInfo; 26 import android.os.Build; 27 import android.os.Handler; 28 import android.os.Looper; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.net.module.util.HexDump; 32 import com.android.net.module.util.SharedLog; 33 import com.android.server.connectivity.mdns.MdnsAnnouncer.BaseAnnouncementInfo; 34 import com.android.server.connectivity.mdns.MdnsPacketRepeater.PacketRepeaterCallback; 35 import com.android.server.connectivity.mdns.util.MdnsUtils; 36 37 import java.io.IOException; 38 import java.net.InetSocketAddress; 39 import java.util.ArrayList; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.Set; 43 44 /** 45 * A class that handles advertising services on a {@link MdnsInterfaceSocket} tied to an interface. 46 */ 47 @RequiresApi(Build.VERSION_CODES.TIRAMISU) 48 public class MdnsInterfaceAdvertiser implements MulticastPacketReader.PacketHandler { 49 public static final int CONFLICT_SERVICE = 1 << 0; 50 public static final int CONFLICT_HOST = 1 << 1; 51 52 private static final boolean DBG = MdnsAdvertiser.DBG; 53 @VisibleForTesting 54 public static final long EXIT_ANNOUNCEMENT_DELAY_MS = 100L; 55 @NonNull 56 private final ProbingCallback mProbingCallback = new ProbingCallback(); 57 @NonNull 58 private final AnnouncingCallback mAnnouncingCallback = new AnnouncingCallback(); 59 @NonNull 60 private final MdnsRecordRepository mRecordRepository; 61 @NonNull 62 private final Callback mCb; 63 // Callbacks are on the same looper thread, but posted to the next handler loop 64 @NonNull 65 private final Handler mCbHandler; 66 @NonNull 67 private final MdnsInterfaceSocket mSocket; 68 @NonNull 69 private final MdnsAnnouncer mAnnouncer; 70 @NonNull 71 private final MdnsProber mProber; 72 @NonNull 73 private final MdnsReplySender mReplySender; 74 @NonNull 75 private final SharedLog mSharedLog; 76 @NonNull 77 private final byte[] mPacketCreationBuffer; 78 @NonNull 79 private final MdnsFeatureFlags mMdnsFeatureFlags; 80 81 /** 82 * Callbacks called by {@link MdnsInterfaceAdvertiser} to report status updates. 83 */ 84 interface Callback { 85 /** 86 * Called by the advertiser after it successfully registered a service, after probing. 87 */ onServiceProbingSucceeded(@onNull MdnsInterfaceAdvertiser advertiser, int serviceId)88 void onServiceProbingSucceeded(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId); 89 90 /** 91 * Called by the advertiser when a conflict was found, during or after probing. 92 * 93 * <p>If a conflict is found during probing, the {@link #renameServiceForConflict} must be 94 * called to restart probing and attempt registration with a different name. 95 * 96 * <p>{@code conflictType} is a bitmap telling which part of the service is conflicting. See 97 * {@link MdnsInterfaceAdvertiser#CONFLICT_SERVICE} and {@link 98 * MdnsInterfaceAdvertiser#CONFLICT_HOST}. 99 */ onServiceConflict( @onNull MdnsInterfaceAdvertiser advertiser, int serviceId, int conflictType)100 void onServiceConflict( 101 @NonNull MdnsInterfaceAdvertiser advertiser, int serviceId, int conflictType); 102 103 /** 104 * Called when all services on this interface advertiser has already been removed and exit 105 * announcements have been sent. 106 * 107 * <p>It's guaranteed that there are no service registrations in the 108 * MdnsInterfaceAdvertiser when this callback is invoked. 109 * 110 * <p>This is typically listened by the {@link MdnsAdvertiser} to release the resources 111 */ onAllServicesRemoved(@onNull MdnsInterfaceSocket socket)112 void onAllServicesRemoved(@NonNull MdnsInterfaceSocket socket); 113 } 114 115 /** 116 * Callbacks from {@link MdnsProber}. 117 */ 118 private class ProbingCallback implements PacketRepeaterCallback<MdnsProber.ProbingInfo> { 119 @Override onSent(int index, @NonNull MdnsProber.ProbingInfo info, int sentPacketCount)120 public void onSent(int index, @NonNull MdnsProber.ProbingInfo info, int sentPacketCount) { 121 mRecordRepository.onProbingSent(info.getServiceId(), sentPacketCount); 122 } 123 @Override onFinished(MdnsProber.ProbingInfo info)124 public void onFinished(MdnsProber.ProbingInfo info) { 125 handleProbingFinished(info); 126 } 127 } 128 handleProbingFinished(MdnsProber.ProbingInfo info)129 private void handleProbingFinished(MdnsProber.ProbingInfo info) { 130 final MdnsAnnouncer.AnnouncementInfo announcementInfo; 131 mSharedLog.i("Probing finished for service " + info.getServiceId()); 132 mCbHandler.post(() -> mCb.onServiceProbingSucceeded( 133 MdnsInterfaceAdvertiser.this, info.getServiceId())); 134 try { 135 announcementInfo = mRecordRepository.onProbingSucceeded(info); 136 } catch (IOException e) { 137 mSharedLog.e("Error building announcements", e); 138 return; 139 } 140 141 mAnnouncer.startSending(info.getServiceId(), announcementInfo, 142 0L /* initialDelayMs */); 143 144 // Re-announce the services which have the same custom hostname. 145 final String hostname = mRecordRepository.getHostnameForServiceId(info.getServiceId()); 146 if (hostname != null) { 147 final List<MdnsAnnouncer.AnnouncementInfo> announcementInfos = 148 new ArrayList<>(mRecordRepository.restartAnnouncingForHostname(hostname)); 149 announcementInfos.removeIf((i) -> i.getServiceId() == info.getServiceId()); 150 reannounceServices(announcementInfos); 151 } 152 } 153 154 /** 155 * Callbacks from {@link MdnsAnnouncer}. 156 */ 157 private class AnnouncingCallback implements PacketRepeaterCallback<BaseAnnouncementInfo> { 158 @Override onSent(int index, @NonNull BaseAnnouncementInfo info, int sentPacketCount)159 public void onSent(int index, @NonNull BaseAnnouncementInfo info, int sentPacketCount) { 160 mRecordRepository.onAdvertisementSent(info.getServiceId(), sentPacketCount); 161 } 162 163 @Override onFinished(@onNull BaseAnnouncementInfo info)164 public void onFinished(@NonNull BaseAnnouncementInfo info) { 165 if (info instanceof MdnsAnnouncer.ExitAnnouncementInfo) { 166 mRecordRepository.removeService(info.getServiceId()); 167 mCbHandler.post(() -> { 168 if (mRecordRepository.getServicesCount() == 0) { 169 mCb.onAllServicesRemoved(mSocket); 170 } 171 }); 172 } 173 } 174 } 175 176 /** 177 * Dependencies for {@link MdnsInterfaceAdvertiser}, useful for testing. 178 */ 179 @VisibleForTesting 180 public static class Dependencies { 181 /** @see MdnsRecordRepository */ 182 @NonNull makeRecordRepository(@onNull Looper looper, @NonNull String[] deviceHostName, @NonNull MdnsFeatureFlags mdnsFeatureFlags)183 public MdnsRecordRepository makeRecordRepository(@NonNull Looper looper, 184 @NonNull String[] deviceHostName, @NonNull MdnsFeatureFlags mdnsFeatureFlags) { 185 return new MdnsRecordRepository(looper, deviceHostName, mdnsFeatureFlags); 186 } 187 188 /** @see MdnsReplySender */ 189 @NonNull makeReplySender(@onNull String interfaceTag, @NonNull Looper looper, @NonNull MdnsInterfaceSocket socket, @NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags)190 public MdnsReplySender makeReplySender(@NonNull String interfaceTag, @NonNull Looper looper, 191 @NonNull MdnsInterfaceSocket socket, @NonNull byte[] packetCreationBuffer, 192 @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) { 193 return new MdnsReplySender(looper, socket, packetCreationBuffer, 194 sharedLog.forSubComponent( 195 MdnsReplySender.class.getSimpleName() + "/" + interfaceTag), DBG, 196 mdnsFeatureFlags); 197 } 198 199 /** @see MdnsAnnouncer */ makeMdnsAnnouncer(@onNull String interfaceTag, @NonNull Looper looper, @NonNull MdnsReplySender replySender, @Nullable PacketRepeaterCallback<MdnsAnnouncer.BaseAnnouncementInfo> cb, @NonNull SharedLog sharedLog)200 public MdnsAnnouncer makeMdnsAnnouncer(@NonNull String interfaceTag, @NonNull Looper looper, 201 @NonNull MdnsReplySender replySender, 202 @Nullable PacketRepeaterCallback<MdnsAnnouncer.BaseAnnouncementInfo> cb, 203 @NonNull SharedLog sharedLog) { 204 return new MdnsAnnouncer(looper, replySender, cb, 205 sharedLog.forSubComponent( 206 MdnsAnnouncer.class.getSimpleName() + "/" + interfaceTag)); 207 } 208 209 /** @see MdnsProber */ makeMdnsProber(@onNull String interfaceTag, @NonNull Looper looper, @NonNull MdnsReplySender replySender, @NonNull PacketRepeaterCallback<MdnsProber.ProbingInfo> cb, @NonNull SharedLog sharedLog)210 public MdnsProber makeMdnsProber(@NonNull String interfaceTag, @NonNull Looper looper, 211 @NonNull MdnsReplySender replySender, 212 @NonNull PacketRepeaterCallback<MdnsProber.ProbingInfo> cb, 213 @NonNull SharedLog sharedLog) { 214 return new MdnsProber(looper, replySender, cb, sharedLog.forSubComponent( 215 MdnsProber.class.getSimpleName() + "/" + interfaceTag)); 216 } 217 } 218 MdnsInterfaceAdvertiser(@onNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> initialAddresses, @NonNull Looper looper, @NonNull byte[] packetCreationBuffer, @NonNull Callback cb, @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags)219 public MdnsInterfaceAdvertiser(@NonNull MdnsInterfaceSocket socket, 220 @NonNull List<LinkAddress> initialAddresses, @NonNull Looper looper, 221 @NonNull byte[] packetCreationBuffer, @NonNull Callback cb, 222 @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog, 223 @NonNull MdnsFeatureFlags mdnsFeatureFlags) { 224 this(socket, initialAddresses, looper, packetCreationBuffer, cb, 225 new Dependencies(), deviceHostName, sharedLog, mdnsFeatureFlags); 226 } 227 MdnsInterfaceAdvertiser(@onNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> initialAddresses, @NonNull Looper looper, @NonNull byte[] packetCreationBuffer, @NonNull Callback cb, @NonNull Dependencies deps, @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags)228 public MdnsInterfaceAdvertiser(@NonNull MdnsInterfaceSocket socket, 229 @NonNull List<LinkAddress> initialAddresses, @NonNull Looper looper, 230 @NonNull byte[] packetCreationBuffer, @NonNull Callback cb, @NonNull Dependencies deps, 231 @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog, 232 @NonNull MdnsFeatureFlags mdnsFeatureFlags) { 233 mRecordRepository = deps.makeRecordRepository(looper, deviceHostName, mdnsFeatureFlags); 234 mRecordRepository.updateAddresses(initialAddresses); 235 mSocket = socket; 236 mCb = cb; 237 mCbHandler = new Handler(looper); 238 mReplySender = deps.makeReplySender(sharedLog.getTag(), looper, socket, 239 packetCreationBuffer, sharedLog, mdnsFeatureFlags); 240 mPacketCreationBuffer = packetCreationBuffer; 241 mAnnouncer = deps.makeMdnsAnnouncer(sharedLog.getTag(), looper, mReplySender, 242 mAnnouncingCallback, sharedLog); 243 mProber = deps.makeMdnsProber(sharedLog.getTag(), looper, mReplySender, mProbingCallback, 244 sharedLog); 245 mSharedLog = sharedLog; 246 mMdnsFeatureFlags = mdnsFeatureFlags; 247 } 248 249 /** 250 * Start the advertiser. 251 * 252 * The advertiser will stop itself when all services are removed and exit announcements sent, 253 * notifying via {@link Callback#onAllServicesRemoved}. 254 */ start()255 public void start() { 256 mSocket.addPacketHandler(this); 257 } 258 259 /** 260 * Update an already registered service without sending exit/re-announcement packet. 261 * 262 * @param id An exiting service id 263 * @param subtypes New subtypes 264 */ updateService(int id, @NonNull Set<String> subtypes)265 public void updateService(int id, @NonNull Set<String> subtypes) { 266 // The current implementation is intended to be used in cases where subtypes don't get 267 // announced. 268 mRecordRepository.updateService(id, subtypes); 269 } 270 271 /** 272 * Start advertising a service. 273 * 274 * @throws NameConflictException There is already a service being advertised with that name. 275 */ addService(int id, NsdServiceInfo service, @NonNull MdnsAdvertisingOptions advertisingOptions)276 public void addService(int id, NsdServiceInfo service, 277 @NonNull MdnsAdvertisingOptions advertisingOptions) throws NameConflictException { 278 final int replacedExitingService = 279 mRecordRepository.addService(id, service, advertisingOptions.getTtl()); 280 // Cancel announcements for the existing service. This only happens for exiting services 281 // (so cancelling exiting announcements), as per RecordRepository.addService. 282 if (replacedExitingService >= 0) { 283 mSharedLog.i("Service " + replacedExitingService 284 + " getting re-added, cancelling exit announcements"); 285 mAnnouncer.stop(replacedExitingService); 286 } 287 final MdnsProber.ProbingInfo probingInfo = mRecordRepository.setServiceProbing(id); 288 if (advertisingOptions.skipProbing()) { 289 handleProbingFinished(probingInfo); 290 } else { 291 mProber.startProbing(probingInfo); 292 } 293 } 294 295 /** 296 * Stop advertising a service. 297 * 298 * This will trigger exit announcements for the service. 299 */ removeService(int id)300 public void removeService(int id) { 301 if (!mRecordRepository.hasActiveService(id)) return; 302 mProber.stop(id); 303 mAnnouncer.stop(id); 304 final String hostname = mRecordRepository.getHostnameForServiceId(id); 305 final MdnsAnnouncer.ExitAnnouncementInfo exitInfo = mRecordRepository.exitService(id); 306 if (exitInfo != null) { 307 // This effectively schedules onAllServicesRemoved(), as it is to be called when the 308 // exit announcement finishes if there is no service left. 309 // A non-zero exit announcement delay follows legacy mdnsresponder behavior, and is 310 // also useful to ensure that when a host receives the exit announcement, the service 311 // has been unregistered on all interfaces; so an announcement sent from interface A 312 // that was already in-flight while unregistering won't be received after the exit on 313 // interface B. 314 mAnnouncer.startSending(id, exitInfo, EXIT_ANNOUNCEMENT_DELAY_MS); 315 } else { 316 // No exit announcement necessary: remove the service immediately. 317 mRecordRepository.removeService(id); 318 mCbHandler.post(() -> { 319 if (mRecordRepository.getServicesCount() == 0) { 320 mCb.onAllServicesRemoved(mSocket); 321 } 322 }); 323 } 324 // Re-probe/re-announce the services which have the same custom hostname. These services 325 // were probed/announced using host addresses which were just removed so they should be 326 // re-probed/re-announced without those addresses. 327 if (hostname != null) { 328 final List<MdnsProber.ProbingInfo> probingInfos = 329 mRecordRepository.restartProbingForHostname(hostname); 330 reprobeServices(probingInfos); 331 final List<MdnsAnnouncer.AnnouncementInfo> announcementInfos = 332 mRecordRepository.restartAnnouncingForHostname(hostname); 333 reannounceServices(announcementInfos); 334 } 335 } 336 337 /** 338 * Get the replied request count from given service id. 339 */ getServiceRepliedRequestsCount(int id)340 public int getServiceRepliedRequestsCount(int id) { 341 if (!mRecordRepository.hasActiveService(id)) return NO_PACKET; 342 return mRecordRepository.getServiceRepliedRequestsCount(id); 343 } 344 345 /** 346 * Get the total sent packet count from given service id. 347 */ getSentPacketCount(int id)348 public int getSentPacketCount(int id) { 349 if (!mRecordRepository.hasActiveService(id)) return NO_PACKET; 350 return mRecordRepository.getSentPacketCount(id); 351 } 352 353 /** 354 * Update interface addresses used to advertise. 355 * 356 * This causes new address records to be announced. 357 */ updateAddresses(@onNull List<LinkAddress> newAddresses)358 public void updateAddresses(@NonNull List<LinkAddress> newAddresses) { 359 mRecordRepository.updateAddresses(newAddresses); 360 // TODO: restart advertising, but figure out what exit messages need to be sent for the 361 // previous addresses 362 } 363 364 /** 365 * Destroy the advertiser immediately, not sending any exit announcement. 366 * 367 * <p>This is typically called when all services on the interface are removed or when the 368 * underlying network went away. 369 */ destroyNow()370 public void destroyNow() { 371 for (int serviceId : mRecordRepository.clearServices()) { 372 mProber.stop(serviceId); 373 mAnnouncer.stop(serviceId); 374 } 375 mReplySender.cancelAll(); 376 mSocket.removePacketHandler(this); 377 } 378 379 /** 380 * Reset a service to the probing state due to a conflict found on the network. 381 */ maybeRestartProbingForConflict(int serviceId)382 public boolean maybeRestartProbingForConflict(int serviceId) { 383 final MdnsProber.ProbingInfo probingInfo = mRecordRepository.setServiceProbing(serviceId); 384 if (probingInfo == null) return false; 385 386 mAnnouncer.stop(serviceId); 387 mProber.restartForConflict(probingInfo); 388 return true; 389 } 390 391 /** 392 * Rename a service following a conflict found on the network, and restart probing. 393 * 394 * If the service was not registered on this {@link MdnsInterfaceAdvertiser}, this is a no-op. 395 */ renameServiceForConflict(int serviceId, NsdServiceInfo newInfo)396 public void renameServiceForConflict(int serviceId, NsdServiceInfo newInfo) { 397 final MdnsProber.ProbingInfo probingInfo = mRecordRepository.renameServiceForConflict( 398 serviceId, newInfo); 399 if (probingInfo == null) return; 400 401 mProber.restartForConflict(probingInfo); 402 } 403 404 /** 405 * Indicates whether probing is in progress for the given service on this interface. 406 * 407 * Also returns false if the specified service is not registered. 408 */ isProbing(int serviceId)409 public boolean isProbing(int serviceId) { 410 return mRecordRepository.isProbing(serviceId); 411 } 412 413 @Override handlePacket(byte[] recvbuf, int length, InetSocketAddress src)414 public void handlePacket(byte[] recvbuf, int length, InetSocketAddress src) { 415 final MdnsPacket packet; 416 try { 417 packet = MdnsPacket.parse(new MdnsPacketReader(recvbuf, length, mMdnsFeatureFlags)); 418 } catch (MdnsPacket.ParseException e) { 419 mSharedLog.e("Error parsing mDNS packet", e); 420 if (DBG) { 421 mSharedLog.v("Packet: " + HexDump.toHexString(recvbuf, 0, length)); 422 } 423 return; 424 } 425 // recvbuf and src are reused after this returns; ensure references to src are not kept. 426 final InetSocketAddress srcCopy = new InetSocketAddress(src.getAddress(), src.getPort()); 427 428 Map<Integer, Integer> conflictingServices = 429 mRecordRepository.getConflictingServices(packet); 430 431 for (Map.Entry<Integer, Integer> entry : conflictingServices.entrySet()) { 432 int serviceId = entry.getKey(); 433 int conflictType = entry.getValue(); 434 mCbHandler.post( 435 () -> { 436 mCb.onServiceConflict(this, serviceId, conflictType); 437 }); 438 } 439 440 // Even in case of conflict, add replies for other services. But in general conflicts would 441 // happen when the incoming packet has answer records (not a question), so there will be no 442 // answer. One exception is simultaneous probe tiebreaking (rfc6762 8.2), in which case the 443 // conflicting service is still probing and won't reply either. 444 final MdnsReplyInfo answers = mRecordRepository.getReply(packet, srcCopy); 445 // Dump the query packet. 446 if (DBG || answers != null) { 447 mSharedLog.v("Parsed packet with transactionId(" + packet.transactionId + "): " 448 + packet.questions.size() + " questions, " 449 + packet.answers.size() + " answers, " 450 + packet.authorityRecords.size() + " authority, " 451 + packet.additionalRecords.size() + " additional from " + srcCopy); 452 } 453 if (answers == null) return; 454 mReplySender.queueReply(answers); 455 } 456 457 /** 458 * Get the socket interface name. 459 */ getSocketInterfaceName()460 public String getSocketInterfaceName() { 461 return mSocket.getInterface().getName(); 462 } 463 464 /** 465 * Gets the offload MdnsPacket. 466 * @param serviceId The serviceId. 467 * @return the raw offload payload 468 */ 469 @NonNull getRawOffloadPayload(int serviceId)470 public byte[] getRawOffloadPayload(int serviceId) { 471 try { 472 return MdnsUtils.createRawDnsPacket(mPacketCreationBuffer, 473 mRecordRepository.getOffloadPacket(serviceId)); 474 } catch (IOException | IllegalArgumentException e) { 475 mSharedLog.wtf("Cannot create rawOffloadPacket: ", e); 476 return new byte[0]; 477 } 478 } 479 reprobeServices(List<MdnsProber.ProbingInfo> probingInfos)480 private void reprobeServices(List<MdnsProber.ProbingInfo> probingInfos) { 481 for (MdnsProber.ProbingInfo probingInfo : probingInfos) { 482 mProber.stop(probingInfo.getServiceId()); 483 mProber.startProbing(probingInfo); 484 } 485 } 486 reannounceServices(List<MdnsAnnouncer.AnnouncementInfo> announcementInfos)487 private void reannounceServices(List<MdnsAnnouncer.AnnouncementInfo> announcementInfos) { 488 for (MdnsAnnouncer.AnnouncementInfo announcementInfo : announcementInfos) { 489 mAnnouncer.stop(announcementInfo.getServiceId()); 490 mAnnouncer.startSending( 491 announcementInfo.getServiceId(), announcementInfo, 0 /* initialDelayMs */); 492 } 493 } 494 } 495