• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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