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