/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.connectivity.mdns; import static com.android.server.connectivity.mdns.MdnsConstants.NO_PACKET; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresApi; import android.net.LinkAddress; import android.net.nsd.NsdServiceInfo; import android.os.Build; import android.os.Handler; import android.os.Looper; import com.android.internal.annotations.VisibleForTesting; import com.android.net.module.util.HexDump; import com.android.net.module.util.SharedLog; import com.android.server.connectivity.mdns.MdnsAnnouncer.BaseAnnouncementInfo; import com.android.server.connectivity.mdns.MdnsPacketRepeater.PacketRepeaterCallback; import com.android.server.connectivity.mdns.util.MdnsUtils; import java.io.IOException; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; /** * A class that handles advertising services on a {@link MdnsInterfaceSocket} tied to an interface. */ @RequiresApi(Build.VERSION_CODES.TIRAMISU) public class MdnsInterfaceAdvertiser implements MulticastPacketReader.PacketHandler { public static final int CONFLICT_SERVICE = 1 << 0; public static final int CONFLICT_HOST = 1 << 1; private static final boolean DBG = MdnsAdvertiser.DBG; @VisibleForTesting public static final long EXIT_ANNOUNCEMENT_DELAY_MS = 100L; @NonNull private final ProbingCallback mProbingCallback = new ProbingCallback(); @NonNull private final AnnouncingCallback mAnnouncingCallback = new AnnouncingCallback(); @NonNull private final MdnsRecordRepository mRecordRepository; @NonNull private final Callback mCb; // Callbacks are on the same looper thread, but posted to the next handler loop @NonNull private final Handler mCbHandler; @NonNull private final MdnsInterfaceSocket mSocket; @NonNull private final MdnsAnnouncer mAnnouncer; @NonNull private final MdnsProber mProber; @NonNull private final MdnsReplySender mReplySender; @NonNull private final SharedLog mSharedLog; @NonNull private final byte[] mPacketCreationBuffer; @NonNull private final MdnsFeatureFlags mMdnsFeatureFlags; /** * Callbacks called by {@link MdnsInterfaceAdvertiser} to report status updates. */ interface Callback { /** * Called by the advertiser after it successfully registered a service, after probing. */ void onServiceProbingSucceeded(@NonNull MdnsInterfaceAdvertiser advertiser, int serviceId); /** * Called by the advertiser when a conflict was found, during or after probing. * *

If a conflict is found during probing, the {@link #renameServiceForConflict} must be * called to restart probing and attempt registration with a different name. * *

{@code conflictType} is a bitmap telling which part of the service is conflicting. See * {@link MdnsInterfaceAdvertiser#CONFLICT_SERVICE} and {@link * MdnsInterfaceAdvertiser#CONFLICT_HOST}. */ void onServiceConflict( @NonNull MdnsInterfaceAdvertiser advertiser, int serviceId, int conflictType); /** * Called when all services on this interface advertiser has already been removed and exit * announcements have been sent. * *

It's guaranteed that there are no service registrations in the * MdnsInterfaceAdvertiser when this callback is invoked. * *

This is typically listened by the {@link MdnsAdvertiser} to release the resources */ void onAllServicesRemoved(@NonNull MdnsInterfaceSocket socket); } /** * Callbacks from {@link MdnsProber}. */ private class ProbingCallback implements PacketRepeaterCallback { @Override public void onSent(int index, @NonNull MdnsProber.ProbingInfo info, int sentPacketCount) { mRecordRepository.onProbingSent(info.getServiceId(), sentPacketCount); } @Override public void onFinished(MdnsProber.ProbingInfo info) { final MdnsAnnouncer.AnnouncementInfo announcementInfo; mSharedLog.i("Probing finished for service " + info.getServiceId()); mCbHandler.post(() -> mCb.onServiceProbingSucceeded( MdnsInterfaceAdvertiser.this, info.getServiceId())); try { announcementInfo = mRecordRepository.onProbingSucceeded(info); } catch (IOException e) { mSharedLog.e("Error building announcements", e); return; } mAnnouncer.startSending(info.getServiceId(), announcementInfo, 0L /* initialDelayMs */); // Re-announce the services which have the same custom hostname. final String hostname = mRecordRepository.getHostnameForServiceId(info.getServiceId()); if (hostname != null) { final List announcementInfos = new ArrayList<>(mRecordRepository.restartAnnouncingForHostname(hostname)); announcementInfos.removeIf((i) -> i.getServiceId() == info.getServiceId()); reannounceServices(announcementInfos); } } } /** * Callbacks from {@link MdnsAnnouncer}. */ private class AnnouncingCallback implements PacketRepeaterCallback { @Override public void onSent(int index, @NonNull BaseAnnouncementInfo info, int sentPacketCount) { mRecordRepository.onAdvertisementSent(info.getServiceId(), sentPacketCount); } @Override public void onFinished(@NonNull BaseAnnouncementInfo info) { if (info instanceof MdnsAnnouncer.ExitAnnouncementInfo) { mRecordRepository.removeService(info.getServiceId()); mCbHandler.post(() -> { if (mRecordRepository.getServicesCount() == 0) { mCb.onAllServicesRemoved(mSocket); } }); } } } /** * Dependencies for {@link MdnsInterfaceAdvertiser}, useful for testing. */ @VisibleForTesting public static class Dependencies { /** @see MdnsRecordRepository */ @NonNull public MdnsRecordRepository makeRecordRepository(@NonNull Looper looper, @NonNull String[] deviceHostName, @NonNull MdnsFeatureFlags mdnsFeatureFlags) { return new MdnsRecordRepository(looper, deviceHostName, mdnsFeatureFlags); } /** @see MdnsReplySender */ @NonNull public MdnsReplySender makeReplySender(@NonNull String interfaceTag, @NonNull Looper looper, @NonNull MdnsInterfaceSocket socket, @NonNull byte[] packetCreationBuffer, @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) { return new MdnsReplySender(looper, socket, packetCreationBuffer, sharedLog.forSubComponent( MdnsReplySender.class.getSimpleName() + "/" + interfaceTag), DBG, mdnsFeatureFlags); } /** @see MdnsAnnouncer */ public MdnsAnnouncer makeMdnsAnnouncer(@NonNull String interfaceTag, @NonNull Looper looper, @NonNull MdnsReplySender replySender, @Nullable PacketRepeaterCallback cb, @NonNull SharedLog sharedLog) { return new MdnsAnnouncer(looper, replySender, cb, sharedLog.forSubComponent( MdnsAnnouncer.class.getSimpleName() + "/" + interfaceTag)); } /** @see MdnsProber */ public MdnsProber makeMdnsProber(@NonNull String interfaceTag, @NonNull Looper looper, @NonNull MdnsReplySender replySender, @NonNull PacketRepeaterCallback cb, @NonNull SharedLog sharedLog) { return new MdnsProber(looper, replySender, cb, sharedLog.forSubComponent( MdnsProber.class.getSimpleName() + "/" + interfaceTag)); } } public MdnsInterfaceAdvertiser(@NonNull MdnsInterfaceSocket socket, @NonNull List initialAddresses, @NonNull Looper looper, @NonNull byte[] packetCreationBuffer, @NonNull Callback cb, @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) { this(socket, initialAddresses, looper, packetCreationBuffer, cb, new Dependencies(), deviceHostName, sharedLog, mdnsFeatureFlags); } public MdnsInterfaceAdvertiser(@NonNull MdnsInterfaceSocket socket, @NonNull List initialAddresses, @NonNull Looper looper, @NonNull byte[] packetCreationBuffer, @NonNull Callback cb, @NonNull Dependencies deps, @NonNull String[] deviceHostName, @NonNull SharedLog sharedLog, @NonNull MdnsFeatureFlags mdnsFeatureFlags) { mRecordRepository = deps.makeRecordRepository(looper, deviceHostName, mdnsFeatureFlags); mRecordRepository.updateAddresses(initialAddresses); mSocket = socket; mCb = cb; mCbHandler = new Handler(looper); mReplySender = deps.makeReplySender(sharedLog.getTag(), looper, socket, packetCreationBuffer, sharedLog, mdnsFeatureFlags); mPacketCreationBuffer = packetCreationBuffer; mAnnouncer = deps.makeMdnsAnnouncer(sharedLog.getTag(), looper, mReplySender, mAnnouncingCallback, sharedLog); mProber = deps.makeMdnsProber(sharedLog.getTag(), looper, mReplySender, mProbingCallback, sharedLog); mSharedLog = sharedLog; mMdnsFeatureFlags = mdnsFeatureFlags; } /** * Start the advertiser. * * The advertiser will stop itself when all services are removed and exit announcements sent, * notifying via {@link Callback#onAllServicesRemoved}. */ public void start() { mSocket.addPacketHandler(this); } /** * Update an already registered service without sending exit/re-announcement packet. * * @param id An exiting service id * @param subtypes New subtypes */ public void updateService(int id, @NonNull Set subtypes) { // The current implementation is intended to be used in cases where subtypes don't get // announced. mRecordRepository.updateService(id, subtypes); } /** * Start advertising a service. * * @throws NameConflictException There is already a service being advertised with that name. */ public void addService(int id, NsdServiceInfo service, @NonNull MdnsAdvertisingOptions advertisingOptions) throws NameConflictException { final int replacedExitingService = mRecordRepository.addService(id, service, advertisingOptions.getTtl()); // Cancel announcements for the existing service. This only happens for exiting services // (so cancelling exiting announcements), as per RecordRepository.addService. if (replacedExitingService >= 0) { mSharedLog.i("Service " + replacedExitingService + " getting re-added, cancelling exit announcements"); mAnnouncer.stop(replacedExitingService); } mProber.startProbing(mRecordRepository.setServiceProbing(id)); } /** * Stop advertising a service. * * This will trigger exit announcements for the service. */ public void removeService(int id) { if (!mRecordRepository.hasActiveService(id)) return; mProber.stop(id); mAnnouncer.stop(id); final String hostname = mRecordRepository.getHostnameForServiceId(id); final MdnsAnnouncer.ExitAnnouncementInfo exitInfo = mRecordRepository.exitService(id); if (exitInfo != null) { // This effectively schedules onAllServicesRemoved(), as it is to be called when the // exit announcement finishes if there is no service left. // A non-zero exit announcement delay follows legacy mdnsresponder behavior, and is // also useful to ensure that when a host receives the exit announcement, the service // has been unregistered on all interfaces; so an announcement sent from interface A // that was already in-flight while unregistering won't be received after the exit on // interface B. mAnnouncer.startSending(id, exitInfo, EXIT_ANNOUNCEMENT_DELAY_MS); } else { // No exit announcement necessary: remove the service immediately. mRecordRepository.removeService(id); mCbHandler.post(() -> { if (mRecordRepository.getServicesCount() == 0) { mCb.onAllServicesRemoved(mSocket); } }); } // Re-probe/re-announce the services which have the same custom hostname. These services // were probed/announced using host addresses which were just removed so they should be // re-probed/re-announced without those addresses. if (hostname != null) { final List probingInfos = mRecordRepository.restartProbingForHostname(hostname); reprobeServices(probingInfos); final List announcementInfos = mRecordRepository.restartAnnouncingForHostname(hostname); reannounceServices(announcementInfos); } } /** * Get the replied request count from given service id. */ public int getServiceRepliedRequestsCount(int id) { if (!mRecordRepository.hasActiveService(id)) return NO_PACKET; return mRecordRepository.getServiceRepliedRequestsCount(id); } /** * Get the total sent packet count from given service id. */ public int getSentPacketCount(int id) { if (!mRecordRepository.hasActiveService(id)) return NO_PACKET; return mRecordRepository.getSentPacketCount(id); } /** * Update interface addresses used to advertise. * * This causes new address records to be announced. */ public void updateAddresses(@NonNull List newAddresses) { mRecordRepository.updateAddresses(newAddresses); // TODO: restart advertising, but figure out what exit messages need to be sent for the // previous addresses } /** * Destroy the advertiser immediately, not sending any exit announcement. * *

This is typically called when all services on the interface are removed or when the * underlying network went away. */ public void destroyNow() { for (int serviceId : mRecordRepository.clearServices()) { mProber.stop(serviceId); mAnnouncer.stop(serviceId); } mReplySender.cancelAll(); mSocket.removePacketHandler(this); } /** * Reset a service to the probing state due to a conflict found on the network. */ public boolean maybeRestartProbingForConflict(int serviceId) { final MdnsProber.ProbingInfo probingInfo = mRecordRepository.setServiceProbing(serviceId); if (probingInfo == null) return false; mAnnouncer.stop(serviceId); mProber.restartForConflict(probingInfo); return true; } /** * Rename a service following a conflict found on the network, and restart probing. * * If the service was not registered on this {@link MdnsInterfaceAdvertiser}, this is a no-op. */ public void renameServiceForConflict(int serviceId, NsdServiceInfo newInfo) { final MdnsProber.ProbingInfo probingInfo = mRecordRepository.renameServiceForConflict( serviceId, newInfo); if (probingInfo == null) return; mProber.restartForConflict(probingInfo); } /** * Indicates whether probing is in progress for the given service on this interface. * * Also returns false if the specified service is not registered. */ public boolean isProbing(int serviceId) { return mRecordRepository.isProbing(serviceId); } @Override public void handlePacket(byte[] recvbuf, int length, InetSocketAddress src) { final MdnsPacket packet; try { packet = MdnsPacket.parse(new MdnsPacketReader(recvbuf, length, mMdnsFeatureFlags)); } catch (MdnsPacket.ParseException e) { mSharedLog.e("Error parsing mDNS packet", e); if (DBG) { mSharedLog.v("Packet: " + HexDump.toHexString(recvbuf, 0, length)); } return; } // recvbuf and src are reused after this returns; ensure references to src are not kept. final InetSocketAddress srcCopy = new InetSocketAddress(src.getAddress(), src.getPort()); if (DBG) { mSharedLog.v("Parsed packet with " + packet.questions.size() + " questions, " + packet.answers.size() + " answers, " + packet.authorityRecords.size() + " authority, " + packet.additionalRecords.size() + " additional from " + srcCopy); } Map conflictingServices = mRecordRepository.getConflictingServices(packet); for (Map.Entry entry : conflictingServices.entrySet()) { int serviceId = entry.getKey(); int conflictType = entry.getValue(); mCbHandler.post( () -> { mCb.onServiceConflict(this, serviceId, conflictType); }); } // Even in case of conflict, add replies for other services. But in general conflicts would // happen when the incoming packet has answer records (not a question), so there will be no // answer. One exception is simultaneous probe tiebreaking (rfc6762 8.2), in which case the // conflicting service is still probing and won't reply either. final MdnsReplyInfo answers = mRecordRepository.getReply(packet, srcCopy); if (answers == null) return; mReplySender.queueReply(answers); } /** * Get the socket interface name. */ public String getSocketInterfaceName() { return mSocket.getInterface().getName(); } /** * Gets the offload MdnsPacket. * @param serviceId The serviceId. * @return the raw offload payload */ @NonNull public byte[] getRawOffloadPayload(int serviceId) { try { return MdnsUtils.createRawDnsPacket(mPacketCreationBuffer, mRecordRepository.getOffloadPacket(serviceId)); } catch (IOException | IllegalArgumentException e) { mSharedLog.wtf("Cannot create rawOffloadPacket: ", e); return new byte[0]; } } private void reprobeServices(List probingInfos) { for (MdnsProber.ProbingInfo probingInfo : probingInfos) { mProber.stop(probingInfo.getServiceId()); mProber.startProbing(probingInfo); } } private void reannounceServices(List announcementInfos) { for (MdnsAnnouncer.AnnouncementInfo announcementInfo : announcementInfos) { mAnnouncer.stop(announcementInfo.getServiceId()); mAnnouncer.startSending( announcementInfo.getServiceId(), announcementInfo, 0 /* initialDelayMs */); } } }