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