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 import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_HOST;
21 import static com.android.server.connectivity.mdns.MdnsInterfaceAdvertiser.CONFLICT_SERVICE;
22 import static com.android.server.connectivity.mdns.MdnsRecord.MAX_LABEL_LENGTH;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.annotation.RequiresApi;
27 import android.content.Context;
28 import android.net.LinkAddress;
29 import android.net.Network;
30 import android.net.nsd.NsdManager;
31 import android.net.nsd.NsdServiceInfo;
32 import android.net.nsd.OffloadEngine;
33 import android.net.nsd.OffloadServiceInfo;
34 import android.os.Build;
35 import android.os.Looper;
36 import android.text.TextUtils;
37 import android.util.ArrayMap;
38 import android.util.Log;
39 import android.util.SparseArray;
40 
41 import com.android.connectivity.resources.R;
42 import com.android.internal.annotations.VisibleForTesting;
43 import com.android.net.module.util.CollectionUtils;
44 import com.android.net.module.util.SharedLog;
45 import com.android.server.connectivity.ConnectivityResources;
46 import com.android.server.connectivity.mdns.util.MdnsUtils;
47 
48 import java.util.ArrayList;
49 import java.util.Collections;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Objects;
53 import java.util.Set;
54 import java.util.UUID;
55 import java.util.function.BiPredicate;
56 import java.util.function.Consumer;
57 
58 /**
59  * MdnsAdvertiser manages advertising services per {@link com.android.server.NsdService} requests.
60  *
61  * All methods except the constructor must be called on the looper thread.
62  */
63 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
64 public class MdnsAdvertiser {
65     private static final String TAG = MdnsAdvertiser.class.getSimpleName();
66     static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
67 
68     // Top-level domain for link-local queries, as per RFC6762 3.
69     private static final String LOCAL_TLD = "local";
70 
71 
72     private final Looper mLooper;
73     private final AdvertiserCallback mCb;
74 
75     // Max-sized buffers to be used as temporary buffer to read/build packets. May be used by
76     // multiple components, but only for self-contained operations in the looper thread, so not
77     // concurrently.
78     // TODO: set according to MTU. 1300 should fit for ethernet MTU 1500 with some overhead.
79     private final byte[] mPacketCreationBuffer = new byte[1300];
80 
81     private final MdnsSocketProvider mSocketProvider;
82     private final ArrayMap<Network, InterfaceAdvertiserRequest> mAdvertiserRequests =
83             new ArrayMap<>();
84     private final ArrayMap<MdnsInterfaceSocket, MdnsInterfaceAdvertiser> mAllAdvertisers =
85             new ArrayMap<>();
86     private final SparseArray<Registration> mRegistrations = new SparseArray<>();
87     private final Dependencies mDeps;
88     private String[] mDeviceHostName;
89     @NonNull private final SharedLog mSharedLog;
90     private final Map<String, List<OffloadServiceInfoWrapper>> mInterfaceOffloadServices =
91             new ArrayMap<>();
92     private final MdnsFeatureFlags mMdnsFeatureFlags;
93     private final Map<String, Integer> mServiceTypeToOffloadPriority;
94 
95     /**
96      * Dependencies for {@link MdnsAdvertiser}, useful for testing.
97      */
98     @VisibleForTesting
99     public static class Dependencies {
100         /**
101          * @see MdnsInterfaceAdvertiser
102          */
makeAdvertiser(@onNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> initialAddresses, @NonNull Looper looper, @NonNull byte[] packetCreationBuffer, @NonNull MdnsInterfaceAdvertiser.Callback cb, @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags)103         public MdnsInterfaceAdvertiser makeAdvertiser(@NonNull MdnsInterfaceSocket socket,
104                 @NonNull List<LinkAddress> initialAddresses,
105                 @NonNull Looper looper, @NonNull byte[] packetCreationBuffer,
106                 @NonNull MdnsInterfaceAdvertiser.Callback cb,
107                 @NonNull String[] deviceHostName,
108                 @NonNull SharedLog sharedLog,
109                 @NonNull MdnsFeatureFlags mdnsFeatureFlags) {
110             // Note NetworkInterface is final and not mockable
111             return new MdnsInterfaceAdvertiser(socket, initialAddresses, looper,
112                     packetCreationBuffer, cb, deviceHostName, sharedLog, mdnsFeatureFlags);
113         }
114 
115         /**
116          * Generates a unique hostname to be used by the device.
117          */
118         @NonNull
generateHostname()119         public String[] generateHostname() {
120             // Generate a very-probably-unique hostname. This allows minimizing possible conflicts
121             // to the point that probing for it is no longer necessary (as per RFC6762 8.1 last
122             // paragraph), and does not leak more information than what could already be obtained by
123             // looking at the mDNS packets source address.
124             // This differs from historical behavior that just used "Android.local" for many
125             // devices, creating a lot of conflicts.
126             // Having a different hostname per interface is an acceptable option as per RFC6762 14.
127             // This hostname will change every time the interface is reconnected, so this does not
128             // allow tracking the device.
129             // TODO: consider deriving a hostname from other sources, such as the IPv6 addresses
130             // (reusing the same privacy-protecting mechanics).
131             return new String[] {
132                     "Android_" + UUID.randomUUID().toString().replace("-", ""), LOCAL_TLD };
133         }
134     }
135 
136     /**
137      * Gets the current status of the OffloadServiceInfos per interface.
138      * @param interfaceName the target interfaceName
139      * @return the list of current offloaded services.
140      */
141     @NonNull
getAllInterfaceOffloadServiceInfos( @onNull String interfaceName)142     public List<OffloadServiceInfoWrapper> getAllInterfaceOffloadServiceInfos(
143             @NonNull String interfaceName) {
144         return mInterfaceOffloadServices.getOrDefault(interfaceName, Collections.emptyList());
145     }
146 
147     private final MdnsInterfaceAdvertiser.Callback mInterfaceAdvertiserCb =
148             new MdnsInterfaceAdvertiser.Callback() {
149         @Override
150         public void onServiceProbingSucceeded(
151                 @NonNull MdnsInterfaceAdvertiser advertiser, int serviceId) {
152             final Registration registration = mRegistrations.get(serviceId);
153             if (registration == null) {
154                 mSharedLog.wtf("Register succeeded for unknown registration");
155                 return;
156             }
157             if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled
158                     // TODO: Enable offload when the serviceInfo contains a custom host.
159                     && TextUtils.isEmpty(registration.getServiceInfo().getHostname())) {
160                 final String interfaceName = advertiser.getSocketInterfaceName();
161                 final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers =
162                         mInterfaceOffloadServices.computeIfAbsent(interfaceName,
163                                 k -> new ArrayList<>());
164                 // Remove existing offload services from cache for update.
165                 existingOffloadServiceInfoWrappers.removeIf(item -> item.mServiceId == serviceId);
166 
167                 byte[] rawOffloadPacket = advertiser.getRawOffloadPayload(serviceId);
168                 final OffloadServiceInfoWrapper newOffloadServiceInfoWrapper = createOffloadService(
169                         serviceId, registration, rawOffloadPacket);
170                 existingOffloadServiceInfoWrappers.add(newOffloadServiceInfoWrapper);
171                 mCb.onOffloadStartOrUpdate(interfaceName,
172                         newOffloadServiceInfoWrapper.mOffloadServiceInfo);
173             }
174 
175             // Wait for all current interfaces to be done probing before notifying of success.
176             if (any(mAllAdvertisers, (k, a) -> a.isProbing(serviceId))) return;
177             // The service may still be unregistered/renamed if a conflict is found on a later added
178             // interface, or if a conflicting announcement/reply is detected (RFC6762 9.)
179 
180             if (!registration.mNotifiedRegistrationSuccess) {
181                 mCb.onRegisterServiceSucceeded(serviceId, registration.getServiceInfo());
182                 registration.mNotifiedRegistrationSuccess = true;
183             }
184         }
185 
186         @Override
187         public void onServiceConflict(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId,
188                 int conflictType) {
189             mSharedLog.i("Found conflict, restarted probing for service "
190                     + serviceId + " "
191                     + conflictType);
192 
193             final Registration registration = mRegistrations.get(serviceId);
194             if (registration == null) return;
195             if (registration.mNotifiedRegistrationSuccess) {
196                 // TODO: consider notifying clients that the service is no longer registered with
197                 // the old name (back to probing). The legacy implementation did not send any
198                 // callback though; it only sent onServiceRegistered after re-probing finishes
199                 // (with the old, conflicting, actually not used name as argument... The new
200                 // implementation will send callbacks with the new name).
201                 registration.mNotifiedRegistrationSuccess = false;
202                 registration.mConflictAfterProbingCount++;
203 
204                 // The service was done probing, just reset it to probing state (RFC6762 9.)
205                 forAllAdvertisers(a -> {
206                     if (!a.maybeRestartProbingForConflict(serviceId)) {
207                         return;
208                     }
209                     if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled) {
210                         maybeSendOffloadStop(a.getSocketInterfaceName(), serviceId);
211                     }
212                 });
213                 return;
214             }
215 
216             if ((conflictType & CONFLICT_SERVICE) != 0) {
217                 // Service conflict was found during probing; rename once to find a name that has no
218                 // conflict
219                 registration.updateForServiceConflict(
220                         registration.makeNewServiceInfoForServiceConflict(1 /* renameCount */),
221                         1 /* renameCount */);
222             }
223 
224             if ((conflictType & CONFLICT_HOST) != 0) {
225                 // Host conflict was found during probing; rename once to find a name that has no
226                 // conflict
227                 registration.updateForHostConflict(
228                         registration.makeNewServiceInfoForHostConflict(1 /* renameCount */),
229                         1 /* renameCount */);
230             }
231 
232             registration.mConflictDuringProbingCount++;
233 
234             // Keep renaming if the new name conflicts in local registrations
235             updateRegistrationUntilNoConflict((net, adv) -> adv.hasRegistration(registration),
236                     registration);
237 
238             // Update advertisers to use the new name
239             forAllAdvertisers(a -> a.renameServiceForConflict(
240                     serviceId, registration.getServiceInfo()));
241         }
242 
243         @Override
244         public void onAllServicesRemoved(@NonNull MdnsInterfaceSocket socket) {
245             if (DBG) { mSharedLog.i("onAllServicesRemoved: " + socket); }
246             // Try destroying the advertiser if all services has been removed
247             destroyAdvertiser(socket, false /* interfaceDestroyed */);
248         }
249     };
250 
hasAnyServiceConflict( @onNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter, @NonNull NsdServiceInfo newInfo, @NonNull Registration originalRegistration)251     private boolean hasAnyServiceConflict(
252             @NonNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter,
253             @NonNull NsdServiceInfo newInfo,
254             @NonNull Registration originalRegistration) {
255         return any(
256                 mAdvertiserRequests,
257                 (network, adv) ->
258                         applicableAdvertiserFilter.test(network, adv)
259                                 && adv.hasServiceConflict(newInfo, originalRegistration));
260     }
261 
hasAnyHostConflict( @onNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter, @NonNull NsdServiceInfo newInfo, int clientUid)262     private boolean hasAnyHostConflict(
263             @NonNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter,
264             @NonNull NsdServiceInfo newInfo,
265             int clientUid) {
266         // Check if it conflicts with custom hosts.
267         if (any(
268                 mAdvertiserRequests,
269                 (network, adv) ->
270                         applicableAdvertiserFilter.test(network, adv)
271                                 && adv.hasHostConflict(newInfo, clientUid))) {
272             return true;
273         }
274         // Check if it conflicts with the default hostname.
275         return MdnsUtils.equalsIgnoreDnsCase(newInfo.getHostname(), mDeviceHostName[0]);
276     }
277 
updateRegistrationUntilNoConflict( @onNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter, @NonNull Registration registration)278     private void updateRegistrationUntilNoConflict(
279             @NonNull BiPredicate<Network, InterfaceAdvertiserRequest> applicableAdvertiserFilter,
280             @NonNull Registration registration) {
281         NsdServiceInfo newInfo = registration.getServiceInfo();
282 
283         int renameServiceCount = 0;
284         while (hasAnyServiceConflict(applicableAdvertiserFilter, newInfo, registration)) {
285             renameServiceCount++;
286             newInfo = registration.makeNewServiceInfoForServiceConflict(renameServiceCount);
287         }
288         registration.updateForServiceConflict(newInfo, renameServiceCount);
289 
290         if (!TextUtils.isEmpty(registration.getServiceInfo().getHostname())) {
291             int renameHostCount = 0;
292             while (hasAnyHostConflict(
293                     applicableAdvertiserFilter, newInfo, registration.mClientUid)) {
294                 renameHostCount++;
295                 newInfo = registration.makeNewServiceInfoForHostConflict(renameHostCount);
296             }
297             registration.updateForHostConflict(newInfo, renameHostCount);
298         }
299     }
300 
maybeSendOffloadStop(final String interfaceName, int serviceId)301     private void maybeSendOffloadStop(final String interfaceName, int serviceId) {
302         final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers =
303                 mInterfaceOffloadServices.get(interfaceName);
304         if (existingOffloadServiceInfoWrappers == null) {
305             return;
306         }
307         // Stop the offloaded service by matching the service id
308         int idx = CollectionUtils.indexOf(existingOffloadServiceInfoWrappers,
309                 item -> item.mServiceId == serviceId);
310         if (idx >= 0) {
311             mCb.onOffloadStop(interfaceName,
312                     existingOffloadServiceInfoWrappers.get(idx).mOffloadServiceInfo);
313             existingOffloadServiceInfoWrappers.remove(idx);
314         }
315     }
316 
317     /**
318      * Destroys the advertiser for the interface indicated by {@code socket}.
319      *
320      * {@code interfaceDestroyed} should be set to {@code true} if this method is called because
321      * the associated interface has been destroyed.
322      */
destroyAdvertiser(MdnsInterfaceSocket socket, boolean interfaceDestroyed)323     private void destroyAdvertiser(MdnsInterfaceSocket socket, boolean interfaceDestroyed) {
324         InterfaceAdvertiserRequest advertiserRequest;
325 
326         MdnsInterfaceAdvertiser advertiser = mAllAdvertisers.remove(socket);
327         if (advertiser != null) {
328             advertiser.destroyNow();
329             if (DBG) { mSharedLog.i("MdnsInterfaceAdvertiser is destroyed: " + advertiser); }
330         }
331 
332         for (int i = mAdvertiserRequests.size() - 1; i >= 0; i--) {
333             advertiserRequest = mAdvertiserRequests.valueAt(i);
334             if (advertiserRequest.onAdvertiserDestroyed(socket, interfaceDestroyed)) {
335                 if (DBG) { mSharedLog.i("AdvertiserRequest is removed: " + advertiserRequest); }
336                 mAdvertiserRequests.removeAt(i);
337             }
338         }
339     }
340 
341     /**
342      * A request for a {@link MdnsInterfaceAdvertiser}.
343      *
344      * This class tracks services to be advertised on all sockets provided via a registered
345      * {@link MdnsSocketProvider.SocketCallback}.
346      */
347     private class InterfaceAdvertiserRequest implements MdnsSocketProvider.SocketCallback {
348         /** Registrations to add to newer MdnsInterfaceAdvertisers when sockets are created. */
349         @NonNull
350         private final SparseArray<Registration> mPendingRegistrations = new SparseArray<>();
351         @NonNull
352         private final ArrayMap<MdnsInterfaceSocket, MdnsInterfaceAdvertiser> mAdvertisers =
353                 new ArrayMap<>();
354 
InterfaceAdvertiserRequest(@ullable Network requestedNetwork)355         InterfaceAdvertiserRequest(@Nullable Network requestedNetwork) {
356             mSocketProvider.requestSocket(requestedNetwork, this);
357         }
358 
359         /**
360          * Called when the interface advertiser associated with {@code socket} has been destroyed.
361          *
362          * {@code interfaceDestroyed} should be set to {@code true} if this method is called because
363          * the associated interface has been destroyed.
364          *
365          * @return true if the {@link InterfaceAdvertiserRequest} should now be deleted
366          */
onAdvertiserDestroyed( @onNull MdnsInterfaceSocket socket, boolean interfaceDestroyed)367         boolean onAdvertiserDestroyed(
368                 @NonNull MdnsInterfaceSocket socket, boolean interfaceDestroyed) {
369             final MdnsInterfaceAdvertiser removedAdvertiser = mAdvertisers.remove(socket);
370             if (removedAdvertiser != null
371                     && !interfaceDestroyed && mPendingRegistrations.size() > 0) {
372                 mSharedLog.wtf(
373                         "unexpected onAdvertiserDestroyed() when there are pending registrations");
374             }
375 
376             if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled && removedAdvertiser != null) {
377                 final String interfaceName = removedAdvertiser.getSocketInterfaceName();
378                 // If the interface is destroyed, stop all hardware offloading on that
379                 // interface.
380                 final List<OffloadServiceInfoWrapper> offloadServiceInfoWrappers =
381                         mInterfaceOffloadServices.remove(interfaceName);
382                 if (offloadServiceInfoWrappers != null) {
383                     for (OffloadServiceInfoWrapper offloadServiceInfoWrapper :
384                             offloadServiceInfoWrappers) {
385                         mCb.onOffloadStop(interfaceName,
386                                 offloadServiceInfoWrapper.mOffloadServiceInfo);
387                     }
388                 }
389             }
390 
391             if (mAdvertisers.size() == 0 && mPendingRegistrations.size() == 0) {
392                 // No advertiser is using sockets from this request anymore (in particular for exit
393                 // announcements), and there is no registration so newer sockets will not be
394                 // necessary, so the request can be unregistered.
395                 mSocketProvider.unrequestSocket(this);
396                 return true;
397             }
398             return false;
399         }
400 
401         /**
402          * Return whether this {@link InterfaceAdvertiserRequest} has the given registration.
403          */
hasRegistration(@onNull Registration registration)404         boolean hasRegistration(@NonNull Registration registration) {
405             return mPendingRegistrations.indexOfValue(registration) >= 0;
406         }
407 
408         /**
409          * Return whether using the proposed new {@link NsdServiceInfo} to add a registration would
410          * cause a conflict of the service in this {@link InterfaceAdvertiserRequest}.
411          */
hasServiceConflict( @onNull NsdServiceInfo newInfo, @NonNull Registration originalRegistration)412         boolean hasServiceConflict(
413                 @NonNull NsdServiceInfo newInfo, @NonNull Registration originalRegistration) {
414             return getConflictingRegistrationDueToService(newInfo, originalRegistration) >= 0;
415         }
416 
417         /**
418          * Return whether using the proposed new {@link NsdServiceInfo} to add a registration would
419          * cause a conflict of the host in this {@link InterfaceAdvertiserRequest}.
420          *
421          * @param clientUid UID of the user who wants to advertise the serviceInfo.
422          */
hasHostConflict(@onNull NsdServiceInfo newInfo, int clientUid)423         boolean hasHostConflict(@NonNull NsdServiceInfo newInfo, int clientUid) {
424             return getConflictingRegistrationDueToHost(newInfo, clientUid) >= 0;
425         }
426 
427         /** Get the ID of a conflicting registration due to service, or -1 if none. */
getConflictingRegistrationDueToService( @onNull NsdServiceInfo info, @NonNull Registration originalRegistration)428         int getConflictingRegistrationDueToService(
429                 @NonNull NsdServiceInfo info, @NonNull Registration originalRegistration) {
430             if (TextUtils.isEmpty(info.getServiceName())) {
431                 return -1;
432             }
433             for (int i = 0; i < mPendingRegistrations.size(); i++) {
434                 // Never conflict with itself
435                 if (mPendingRegistrations.valueAt(i) == originalRegistration) {
436                     continue;
437                 }
438                 final NsdServiceInfo other = mPendingRegistrations.valueAt(i).getServiceInfo();
439                 if (MdnsUtils.equalsIgnoreDnsCase(info.getServiceName(), other.getServiceName())
440                         && MdnsUtils.equalsIgnoreDnsCase(info.getServiceType(),
441                         other.getServiceType())) {
442                     return mPendingRegistrations.keyAt(i);
443                 }
444             }
445             return -1;
446         }
447 
448         /**
449          * Get the ID of a conflicting registration due to host, or -1 if none.
450          *
451          * <p>If there's already another registration with the same hostname requested by another
452          * user, this is a conflict.
453          *
454          * <p>If there're two registrations both containing address records using the same hostname,
455          * this is a conflict.
456          */
getConflictingRegistrationDueToHost(@onNull NsdServiceInfo info, int clientUid)457         int getConflictingRegistrationDueToHost(@NonNull NsdServiceInfo info, int clientUid) {
458             if (TextUtils.isEmpty(info.getHostname())) {
459                 return -1;
460             }
461             for (int i = 0; i < mPendingRegistrations.size(); i++) {
462                 final Registration otherRegistration = mPendingRegistrations.valueAt(i);
463                 final NsdServiceInfo otherInfo = otherRegistration.getServiceInfo();
464                 final int otherServiceId = mPendingRegistrations.keyAt(i);
465                 if (clientUid != otherRegistration.mClientUid
466                         && MdnsUtils.equalsIgnoreDnsCase(
467                                 info.getHostname(), otherInfo.getHostname())) {
468                     return otherServiceId;
469                 }
470                 if (!info.getHostAddresses().isEmpty()
471                         && !otherInfo.getHostAddresses().isEmpty()
472                         && MdnsUtils.equalsIgnoreDnsCase(
473                                 info.getHostname(), otherInfo.getHostname())) {
474                     return otherServiceId;
475                 }
476             }
477             return -1;
478         }
479 
480         /**
481          * Add a service to advertise.
482          *
483          * <p>Conflicts must be checked via {@link #getConflictingRegistrationDueToService} and
484          * {@link #getConflictingRegistrationDueToHost} before attempting to add.
485          */
addService(int id, @NonNull Registration registration)486         void addService(int id, @NonNull Registration registration) {
487             mPendingRegistrations.put(id, registration);
488             for (int i = 0; i < mAdvertisers.size(); i++) {
489                 try {
490                     mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo(),
491                             registration.getAdvertisingOptions());
492                 } catch (NameConflictException e) {
493                     mSharedLog.wtf("Name conflict adding services that should have unique names",
494                             e);
495                 }
496             }
497         }
498 
499         /**
500          * Update an already registered service.
501          * The caller is expected to check that the service being updated doesn't change its name
502          */
updateService(int id, @NonNull Registration registration)503         void updateService(int id, @NonNull Registration registration) {
504             mPendingRegistrations.put(id, registration);
505             for (int i = 0; i < mAdvertisers.size(); i++) {
506                 mAdvertisers.valueAt(i).updateService(
507                         id, registration.getServiceInfo().getSubtypes());
508             }
509         }
510 
removeService(int id)511         void removeService(int id) {
512             mPendingRegistrations.remove(id);
513             for (int i = 0; i < mAdvertisers.size(); i++) {
514                 final MdnsInterfaceAdvertiser advertiser = mAdvertisers.valueAt(i);
515                 advertiser.removeService(id);
516 
517                 if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled) {
518                     maybeSendOffloadStop(advertiser.getSocketInterfaceName(), id);
519                 }
520             }
521         }
522 
getServiceRepliedRequestsCount(int id)523         int getServiceRepliedRequestsCount(int id) {
524             int repliedRequestsCount = NO_PACKET;
525             for (int i = 0; i < mAdvertisers.size(); i++) {
526                 repliedRequestsCount += mAdvertisers.valueAt(i).getServiceRepliedRequestsCount(id);
527             }
528             return repliedRequestsCount;
529         }
530 
getSentPacketCount(int id)531         int getSentPacketCount(int id) {
532             int sentPacketCount = NO_PACKET;
533             for (int i = 0; i < mAdvertisers.size(); i++) {
534                 sentPacketCount += mAdvertisers.valueAt(i).getSentPacketCount(id);
535             }
536             return sentPacketCount;
537         }
538 
539         @Override
onSocketCreated(@onNull SocketKey socketKey, @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> addresses)540         public void onSocketCreated(@NonNull SocketKey socketKey,
541                 @NonNull MdnsInterfaceSocket socket,
542                 @NonNull List<LinkAddress> addresses) {
543             MdnsInterfaceAdvertiser advertiser = mAllAdvertisers.get(socket);
544             if (advertiser == null) {
545                 advertiser = mDeps.makeAdvertiser(socket, addresses, mLooper, mPacketCreationBuffer,
546                         mInterfaceAdvertiserCb, mDeviceHostName,
547                         mSharedLog.forSubComponent(socket.getInterface().getName()),
548                         mMdnsFeatureFlags);
549                 mAllAdvertisers.put(socket, advertiser);
550                 advertiser.start();
551             }
552             mAdvertisers.put(socket, advertiser);
553             for (int i = 0; i < mPendingRegistrations.size(); i++) {
554                 final Registration registration = mPendingRegistrations.valueAt(i);
555                 try {
556                     advertiser.addService(mPendingRegistrations.keyAt(i),
557                             registration.getServiceInfo(), registration.getAdvertisingOptions());
558                 } catch (NameConflictException e) {
559                     mSharedLog.wtf("Name conflict adding services that should have unique names",
560                             e);
561                 }
562             }
563         }
564 
565         @Override
onInterfaceDestroyed(@onNull SocketKey socketKey, @NonNull MdnsInterfaceSocket socket)566         public void onInterfaceDestroyed(@NonNull SocketKey socketKey,
567                 @NonNull MdnsInterfaceSocket socket) {
568             final MdnsInterfaceAdvertiser advertiser = mAdvertisers.get(socket);
569             if (advertiser != null) destroyAdvertiser(socket, true /* interfaceDestroyed */);
570         }
571 
572         @Override
onAddressesChanged(@onNull SocketKey socketKey, @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> addresses)573         public void onAddressesChanged(@NonNull SocketKey socketKey,
574                 @NonNull MdnsInterfaceSocket socket, @NonNull List<LinkAddress> addresses) {
575             final MdnsInterfaceAdvertiser advertiser = mAdvertisers.get(socket);
576             if (advertiser == null)  {
577                 return;
578             }
579             advertiser.updateAddresses(addresses);
580 
581             if (mMdnsFeatureFlags.mIsMdnsOffloadFeatureEnabled) {
582                 // Update address should trigger offload packet update.
583                 final String interfaceName = advertiser.getSocketInterfaceName();
584                 final List<OffloadServiceInfoWrapper> existingOffloadServiceInfoWrappers =
585                         mInterfaceOffloadServices.get(interfaceName);
586                 if (existingOffloadServiceInfoWrappers == null) {
587                     return;
588                 }
589                 final List<OffloadServiceInfoWrapper> updatedOffloadServiceInfoWrappers =
590                         new ArrayList<>(existingOffloadServiceInfoWrappers.size());
591                 for (OffloadServiceInfoWrapper oldWrapper : existingOffloadServiceInfoWrappers) {
592                     OffloadServiceInfoWrapper newWrapper = new OffloadServiceInfoWrapper(
593                             oldWrapper.mServiceId,
594                             oldWrapper.mOffloadServiceInfo.withOffloadPayload(
595                                     advertiser.getRawOffloadPayload(oldWrapper.mServiceId))
596                     );
597                     updatedOffloadServiceInfoWrappers.add(newWrapper);
598                     mCb.onOffloadStartOrUpdate(interfaceName, newWrapper.mOffloadServiceInfo);
599                 }
600                 mInterfaceOffloadServices.put(interfaceName, updatedOffloadServiceInfoWrappers);
601             }
602         }
603     }
604 
605     /**
606      * The wrapper class for OffloadServiceInfo including the serviceId.
607      */
608     public static class OffloadServiceInfoWrapper {
609         public final @NonNull OffloadServiceInfo mOffloadServiceInfo;
610         public final int mServiceId;
611 
OffloadServiceInfoWrapper(int serviceId, OffloadServiceInfo offloadServiceInfo)612         OffloadServiceInfoWrapper(int serviceId, OffloadServiceInfo offloadServiceInfo) {
613             mOffloadServiceInfo = offloadServiceInfo;
614             mServiceId = serviceId;
615         }
616     }
617 
618     private static class Registration {
619         @Nullable
620         final String mOriginalServiceName;
621         @Nullable
622         final String mOriginalHostname;
623         boolean mNotifiedRegistrationSuccess;
624         private int mServiceNameConflictCount;
625         private int mHostnameConflictCount;
626         @NonNull
627         private NsdServiceInfo mServiceInfo;
628         final int mClientUid;
629         private final MdnsAdvertisingOptions mAdvertisingOptions;
630         int mConflictDuringProbingCount;
631         int mConflictAfterProbingCount;
632 
Registration(@onNull NsdServiceInfo serviceInfo, int clientUid, @NonNull MdnsAdvertisingOptions advertisingOptions)633         private Registration(@NonNull NsdServiceInfo serviceInfo, int clientUid,
634                 @NonNull MdnsAdvertisingOptions advertisingOptions) {
635             this.mOriginalServiceName = serviceInfo.getServiceName();
636             this.mOriginalHostname = serviceInfo.getHostname();
637             this.mServiceInfo = serviceInfo;
638             this.mClientUid = clientUid;
639             this.mAdvertisingOptions = advertisingOptions;
640         }
641 
642         /** Check if the new {@link NsdServiceInfo} doesn't update any data other than subtypes. */
isSubtypeOnlyUpdate(@onNull NsdServiceInfo newInfo)643         public boolean isSubtypeOnlyUpdate(@NonNull NsdServiceInfo newInfo) {
644             return Objects.equals(newInfo.getServiceName(), mOriginalServiceName)
645                     && Objects.equals(newInfo.getServiceType(), mServiceInfo.getServiceType())
646                     && newInfo.getPort() == mServiceInfo.getPort()
647                     && Objects.equals(newInfo.getHostname(), mOriginalHostname)
648                     && Objects.equals(newInfo.getHostAddresses(), mServiceInfo.getHostAddresses())
649                     && Objects.equals(newInfo.getNetwork(), mServiceInfo.getNetwork());
650         }
651 
652         /**
653          * Update subTypes for the registration.
654          */
updateSubtypes(@onNull Set<String> subtypes)655         public void updateSubtypes(@NonNull Set<String> subtypes) {
656             mServiceInfo = new NsdServiceInfo(mServiceInfo);
657             mServiceInfo.setSubtypes(subtypes);
658         }
659 
660         /**
661          * Update the registration to use a different service name, after a conflict was found.
662          *
663          * @param newInfo New service info to use.
664          * @param renameCount How many renames were done before reaching the current name.
665          */
updateForServiceConflict(@onNull NsdServiceInfo newInfo, int renameCount)666         private void updateForServiceConflict(@NonNull NsdServiceInfo newInfo, int renameCount) {
667             mServiceNameConflictCount += renameCount;
668             mServiceInfo = newInfo;
669         }
670 
671         /**
672          * Update the registration to use a different host name, after a conflict was found.
673          *
674          * @param newInfo New service info to use.
675          * @param renameCount How many renames were done before reaching the current name.
676          */
updateForHostConflict(@onNull NsdServiceInfo newInfo, int renameCount)677         private void updateForHostConflict(@NonNull NsdServiceInfo newInfo, int renameCount) {
678             mHostnameConflictCount += renameCount;
679             mServiceInfo = newInfo;
680         }
681 
682         /**
683          * Make a new service name for the registration, after a conflict was found.
684          *
685          * If a name conflict was found during probing or because different advertising requests
686          * used the same name, the registration is attempted again with a new name (here using
687          * a number suffix, (1), (2) etc). Registration success is notified once probing succeeds
688          * with a new name. This matches legacy behavior based on mdnsresponder, and appendix D of
689          * RFC6763.
690          *
691          * @param renameCount How much to increase the number suffix for this conflict.
692          */
693         @NonNull
makeNewServiceInfoForServiceConflict(int renameCount)694         public NsdServiceInfo makeNewServiceInfoForServiceConflict(int renameCount) {
695             // In case of conflict choose a different service name. After the first conflict use
696             // "Name (2)", then "Name (3)" etc.
697             // TODO: use a hidden method in NsdServiceInfo once MdnsAdvertiser is moved to service-t
698             final NsdServiceInfo newInfo = new NsdServiceInfo(mServiceInfo);
699             newInfo.setServiceName(getUpdatedServiceName(renameCount));
700             return newInfo;
701         }
702 
703         /**
704          * Make a new hostname for the registration, after a conflict was found.
705          *
706          * <p>If a name conflict was found during probing or because different advertising requests
707          * used the same name, the registration is attempted again with a new name (here using a
708          * number suffix, -1, -2, etc). Registration success is notified once probing succeeds with
709          * a new name.
710          *
711          * @param renameCount How much to increase the number suffix for this conflict.
712          */
713         @NonNull
makeNewServiceInfoForHostConflict(int renameCount)714         public NsdServiceInfo makeNewServiceInfoForHostConflict(int renameCount) {
715             // In case of conflict choose a different hostname. After the first conflict use
716             // "Name-2", then "Name-3" etc.
717             final NsdServiceInfo newInfo = new NsdServiceInfo(mServiceInfo);
718             newInfo.setHostname(getUpdatedHostname(renameCount));
719             return newInfo;
720         }
721 
getUpdatedServiceName(int renameCount)722         private String getUpdatedServiceName(int renameCount) {
723             final String suffix = " (" + (mServiceNameConflictCount + renameCount + 1) + ")";
724             final String truncatedServiceName = MdnsUtils.truncateServiceName(mOriginalServiceName,
725                     MAX_LABEL_LENGTH - suffix.length());
726             return truncatedServiceName + suffix;
727         }
728 
getUpdatedHostname(int renameCount)729         private String getUpdatedHostname(int renameCount) {
730             final String suffix = "-" + (mHostnameConflictCount + renameCount + 1);
731             final String truncatedHostname =
732                     MdnsUtils.truncateServiceName(
733                             mOriginalHostname, MAX_LABEL_LENGTH - suffix.length());
734             return truncatedHostname + suffix;
735         }
736 
737         @NonNull
getServiceInfo()738         public NsdServiceInfo getServiceInfo() {
739             return mServiceInfo;
740         }
741 
742         @NonNull
getAdvertisingOptions()743         public MdnsAdvertisingOptions getAdvertisingOptions() {
744             return mAdvertisingOptions;
745         }
746     }
747 
748     /**
749      * Callbacks for advertising services.
750      *
751      * Every method is called on the MdnsAdvertiser looper thread.
752      */
753     public interface AdvertiserCallback {
754         /**
755          * Called when a service was successfully registered, after probing.
756          *
757          * @param serviceId ID of the service provided when registering.
758          * @param registeredInfo Registered info, which may be different from the requested info,
759          *                       after probing and possibly choosing alternative service names.
760          */
onRegisterServiceSucceeded(int serviceId, NsdServiceInfo registeredInfo)761         void onRegisterServiceSucceeded(int serviceId, NsdServiceInfo registeredInfo);
762 
763         /**
764          * Called when service registration failed.
765          *
766          * @param serviceId ID of the service provided when registering.
767          * @param errorCode One of {@code NsdManager.FAILURE_*}
768          */
onRegisterServiceFailed(int serviceId, int errorCode)769         void onRegisterServiceFailed(int serviceId, int errorCode);
770 
771         // Unregistration is notified immediately as success in NsdService so no callback is needed
772         // here.
773 
774         /**
775          * Called when a service is ready to be sent for hardware offloading.
776          *
777          * @param interfaceName the interface for sending the update to.
778          * @param offloadServiceInfo the offloading content.
779          */
onOffloadStartOrUpdate(@onNull String interfaceName, @NonNull OffloadServiceInfo offloadServiceInfo)780         void onOffloadStartOrUpdate(@NonNull String interfaceName,
781                 @NonNull OffloadServiceInfo offloadServiceInfo);
782 
783         /**
784          * Called when a service is removed or the MdnsInterfaceAdvertiser is destroyed.
785          *
786          * @param interfaceName the interface for sending the update to.
787          * @param offloadServiceInfo the offloading content.
788          */
onOffloadStop(@onNull String interfaceName, @NonNull OffloadServiceInfo offloadServiceInfo)789         void onOffloadStop(@NonNull String interfaceName,
790                 @NonNull OffloadServiceInfo offloadServiceInfo);
791     }
792 
793     /**
794      * Data class of avdverting metrics.
795      */
796     public static class AdvertiserMetrics {
797         public final int mRepliedRequestsCount;
798         public final int mSentPacketCount;
799         public final int mConflictDuringProbingCount;
800         public final int mConflictAfterProbingCount;
801 
AdvertiserMetrics(int repliedRequestsCount, int sentPacketCount, int conflictDuringProbingCount, int conflictAfterProbingCount)802         public AdvertiserMetrics(int repliedRequestsCount, int sentPacketCount,
803                 int conflictDuringProbingCount, int conflictAfterProbingCount) {
804             mRepliedRequestsCount = repliedRequestsCount;
805             mSentPacketCount = sentPacketCount;
806             mConflictDuringProbingCount = conflictDuringProbingCount;
807             mConflictAfterProbingCount = conflictAfterProbingCount;
808         }
809     }
810 
MdnsAdvertiser(@onNull Looper looper, @NonNull MdnsSocketProvider socketProvider, @NonNull AdvertiserCallback cb, @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mDnsFeatureFlags, @NonNull Context context)811     public MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
812             @NonNull AdvertiserCallback cb, @NonNull SharedLog sharedLog,
813             @NonNull MdnsFeatureFlags mDnsFeatureFlags, @NonNull Context context) {
814         this(looper, socketProvider, cb, new Dependencies(), sharedLog, mDnsFeatureFlags,
815                 context);
816     }
817 
818     @VisibleForTesting
MdnsAdvertiser(@onNull Looper looper, @NonNull MdnsSocketProvider socketProvider, @NonNull AdvertiserCallback cb, @NonNull Dependencies deps, @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mDnsFeatureFlags, @NonNull Context context)819     MdnsAdvertiser(@NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
820             @NonNull AdvertiserCallback cb, @NonNull Dependencies deps,
821             @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mDnsFeatureFlags,
822             @NonNull Context context) {
823         mLooper = looper;
824         mCb = cb;
825         mSocketProvider = socketProvider;
826         mDeps = deps;
827         mDeviceHostName = deps.generateHostname();
828         mSharedLog = sharedLog;
829         mMdnsFeatureFlags = mDnsFeatureFlags;
830         final ConnectivityResources res = new ConnectivityResources(context);
831         mServiceTypeToOffloadPriority = parseOffloadPriorityList(
832                 res.get().getStringArray(R.array.config_nsdOffloadServicesPriority), sharedLog);
833     }
834 
parseOffloadPriorityList( @onNull String[] resValues, SharedLog sharedLog)835     private static Map<String, Integer> parseOffloadPriorityList(
836             @NonNull String[] resValues, SharedLog sharedLog) {
837         final Map<String, Integer> priorities = new ArrayMap<>(resValues.length);
838         for (String entry : resValues) {
839             final String[] priorityAndType = entry.split(":", 2);
840             if (priorityAndType.length != 2) {
841                 sharedLog.wtf("Invalid config_nsdOffloadServicesPriority ignored: " + entry);
842                 continue;
843             }
844 
845             final int priority;
846             try {
847                 priority = Integer.parseInt(priorityAndType[0]);
848             } catch (NumberFormatException e) {
849                 sharedLog.wtf("Invalid priority in config_nsdOffloadServicesPriority: " + entry);
850                 continue;
851             }
852             priorities.put(MdnsUtils.toDnsLowerCase(priorityAndType[1]), priority);
853         }
854         return priorities;
855     }
856 
checkThread()857     private void checkThread() {
858         if (Thread.currentThread() != mLooper.getThread()) {
859             throw new IllegalStateException("This must be called on the looper thread");
860         }
861     }
862 
863     /**
864      * Add or update a service to advertise.
865      *
866      * @param id A unique ID for the service.
867      * @param service The service info to advertise.
868      * @param advertisingOptions The advertising options.
869      * @param clientUid The UID who wants to advertise the service.
870      */
addOrUpdateService(int id, NsdServiceInfo service, MdnsAdvertisingOptions advertisingOptions, int clientUid)871     public void addOrUpdateService(int id, NsdServiceInfo service,
872             MdnsAdvertisingOptions advertisingOptions, int clientUid) {
873         checkThread();
874         final Registration existingRegistration = mRegistrations.get(id);
875         final Network network = service.getNetwork();
876         final Set<String> subtypes = service.getSubtypes();
877         Registration registration;
878         if (advertisingOptions.isOnlyUpdate()) {
879             if (existingRegistration == null) {
880                 mSharedLog.e("Update non existing registration for " + service);
881                 mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
882                 return;
883             }
884             if (!(existingRegistration.isSubtypeOnlyUpdate(service))) {
885                 mSharedLog.e("Update request can only update subType, serviceInfo: " + service
886                         + ", existing serviceInfo: " + existingRegistration.getServiceInfo());
887                 mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
888                 return;
889 
890             }
891             mSharedLog.i("Update service " + service + " with ID " + id + " and subtypes "
892                     + subtypes + " advertisingOptions " + advertisingOptions);
893             registration = existingRegistration;
894             registration.updateSubtypes(subtypes);
895         } else {
896             if (existingRegistration != null) {
897                 mSharedLog.e("Adding duplicate registration for " + service);
898                 // TODO (b/264986328): add a more specific error code
899                 mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
900                 return;
901             }
902             mSharedLog.i("Adding service " + service + " with ID " + id + " and subtypes "
903                     + subtypes + " advertisingOptions " + advertisingOptions);
904             registration = new Registration(service, clientUid, advertisingOptions);
905             final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
906             if (network == null) {
907                 // If registering on all networks, no advertiser must have conflicts
908                 checkConflictFilter = (net, adv) -> true;
909             } else {
910                 // If registering on one network, the matching network advertiser and the one
911                 // for all networks must not have conflicts
912                 checkConflictFilter = (net, adv) -> net == null || network.equals(net);
913             }
914             updateRegistrationUntilNoConflict(checkConflictFilter, registration);
915         }
916 
917         InterfaceAdvertiserRequest advertiser = mAdvertiserRequests.get(network);
918         if (advertiser == null) {
919             advertiser = new InterfaceAdvertiserRequest(network);
920             mAdvertiserRequests.put(network, advertiser);
921         }
922         if (advertisingOptions.isOnlyUpdate()) {
923             advertiser.updateService(id, registration);
924         } else {
925             advertiser.addService(id, registration);
926         }
927         mRegistrations.put(id, registration);
928     }
929 
930     /**
931      * Remove a previously added service.
932      * @param id ID used when registering.
933      */
removeService(int id)934     public void removeService(int id) {
935         checkThread();
936         if (!mRegistrations.contains(id)) return;
937         mSharedLog.i("Removing service with ID " + id);
938         for (int i = mAdvertiserRequests.size() - 1; i >= 0; i--) {
939             final InterfaceAdvertiserRequest advertiser = mAdvertiserRequests.valueAt(i);
940             advertiser.removeService(id);
941         }
942         mRegistrations.remove(id);
943         // Regenerates host name when registrations removed.
944         if (mRegistrations.size() == 0) {
945             mDeviceHostName = mDeps.generateHostname();
946         }
947     }
948 
949     /**
950      * Get advertising metrics.
951      *
952      * @param id ID used when registering.
953      * @return The advertising metrics includes replied requests count, send packet count, conflict
954      *         count during/after probing.
955      */
getAdvertiserMetrics(int id)956     public AdvertiserMetrics getAdvertiserMetrics(int id) {
957         checkThread();
958         final Registration registration = mRegistrations.get(id);
959         if (registration == null) {
960             return new AdvertiserMetrics(
961                     NO_PACKET /* repliedRequestsCount */,
962                     NO_PACKET /* sentPacketCount */,
963                     0 /* conflictDuringProbingCount */,
964                     0 /* conflictAfterProbingCount */);
965         }
966         int repliedRequestsCount = NO_PACKET;
967         int sentPacketCount = NO_PACKET;
968         for (int i = 0; i < mAdvertiserRequests.size(); i++) {
969             repliedRequestsCount +=
970                     mAdvertiserRequests.valueAt(i).getServiceRepliedRequestsCount(id);
971             sentPacketCount += mAdvertiserRequests.valueAt(i).getSentPacketCount(id);
972         }
973         return new AdvertiserMetrics(repliedRequestsCount, sentPacketCount,
974                 registration.mConflictDuringProbingCount, registration.mConflictAfterProbingCount);
975     }
976 
any(@onNull ArrayMap<K, V> map, @NonNull BiPredicate<K, V> predicate)977     private static <K, V> boolean any(@NonNull ArrayMap<K, V> map,
978             @NonNull BiPredicate<K, V> predicate) {
979         for (int i = 0; i < map.size(); i++) {
980             if (predicate.test(map.keyAt(i), map.valueAt(i))) {
981                 return true;
982             }
983         }
984         return false;
985     }
986 
forAllAdvertisers(@onNull Consumer<MdnsInterfaceAdvertiser> consumer)987     private void forAllAdvertisers(@NonNull Consumer<MdnsInterfaceAdvertiser> consumer) {
988         any(mAllAdvertisers, (socket, advertiser) -> {
989             consumer.accept(advertiser);
990             return false;
991         });
992     }
993 
createOffloadService(int serviceId, @NonNull Registration registration, byte[] rawOffloadPacket)994     private OffloadServiceInfoWrapper createOffloadService(int serviceId,
995             @NonNull Registration registration, byte[] rawOffloadPacket) {
996         final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
997         final Integer mapPriority = mServiceTypeToOffloadPriority.get(
998                 MdnsUtils.toDnsLowerCase(nsdServiceInfo.getServiceType()));
999         // Higher values of priority are less prioritized
1000         final int priority = mapPriority == null ? Integer.MAX_VALUE : mapPriority;
1001         final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
1002                 new OffloadServiceInfo.Key(nsdServiceInfo.getServiceName(),
1003                         nsdServiceInfo.getServiceType()),
1004                 new ArrayList<>(nsdServiceInfo.getSubtypes()),
1005                 String.join(".", mDeviceHostName),
1006                 rawOffloadPacket,
1007                 priority,
1008                 // TODO: set the offloadType based on the callback timing.
1009                 OffloadEngine.OFFLOAD_TYPE_REPLY);
1010         return new OffloadServiceInfoWrapper(serviceId, offloadServiceInfo);
1011     }
1012 }
1013