From 7a3423b65b1be980a36e470ccf2b4985bebd91b0 Mon Sep 17 00:00:00 2001 From: Remi NGUYEN VAN Date: Wed, 18 Dec 2024 15:28:13 +0900 Subject: [PATCH] Add API for FLAG_SKIP_PROBING The flag, used in the newly exposed AdvertisingRequest, allows skipping the probing step when the service name is known to be unique already. This allows implementing RFC 6762 section 8.1 that specifies probing SHOULD be skipped if it is known via other means that the record set is unique. Bug: 384841047 Test: atest CtsNetTestCases:NsdManagerTest Change-Id: I19d67ff2863af631bd9c2f518b004f1688ac5f87 --- framework-t/api/current.txt | 17 +++++ .../android/net/nsd/AdvertisingRequest.java | 73 +++++++++++++++---- .../src/android/net/nsd/NsdManager.java | 11 ++- .../src/android/net/nsd/NsdServiceInfo.java | 2 + .../src/com/android/server/NsdService.java | 6 +- .../mdns/MdnsAdvertisingOptions.java | 22 +++++- .../mdns/MdnsInterfaceAdvertiser.java | 51 +++++++------ .../net/src/android/net/cts/NsdManagerTest.kt | 48 +++++++++++- .../android/net/nsd/AdvertisingRequestTest.kt | 12 +-- 9 files changed, 194 insertions(+), 48 deletions(-) diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt index 75578aa59b..d66482cb16 100644 --- a/framework-t/api/current.txt +++ b/framework-t/api/current.txt @@ -210,6 +210,23 @@ package android.net { package android.net.nsd { + @FlaggedApi("com.android.net.flags.ipv6_over_ble") public final class AdvertisingRequest implements android.os.Parcelable { + method public int describeContents(); + method public long getFlags(); + method public int getProtocolType(); + method @NonNull public android.net.nsd.NsdServiceInfo getServiceInfo(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + field public static final long FLAG_SKIP_PROBING = 2L; // 0x2L + } + + @FlaggedApi("com.android.net.flags.ipv6_over_ble") public static final class AdvertisingRequest.Builder { + ctor public AdvertisingRequest.Builder(@NonNull android.net.nsd.NsdServiceInfo); + method @NonNull public android.net.nsd.AdvertisingRequest build(); + method @NonNull public android.net.nsd.AdvertisingRequest.Builder setFlags(long); + method @NonNull public android.net.nsd.AdvertisingRequest.Builder setProtocolType(int); + } + @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public final class DiscoveryRequest implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.net.Network getNetwork(); diff --git a/framework-t/src/android/net/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java index 6afb2d58a5..a62df65901 100644 --- a/framework-t/src/android/net/nsd/AdvertisingRequest.java +++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java @@ -15,12 +15,16 @@ */ package android.net.nsd; +import android.annotation.FlaggedApi; import android.annotation.LongDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.net.nsd.NsdManager.ProtocolType; import android.os.Parcel; import android.os.Parcelable; +import com.android.net.flags.Flags; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.time.Duration; @@ -28,16 +32,32 @@ import java.util.Objects; /** * Encapsulates parameters for {@link NsdManager#registerService}. - * @hide */ -//@FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API) +@FlaggedApi(Flags.FLAG_IPV6_OVER_BLE) public final class AdvertisingRequest implements Parcelable { /** * Only update the registration without sending exit and re-announcement. + * @hide */ public static final long NSD_ADVERTISING_UPDATE_ONLY = 1; + // TODO: if apps are allowed to set hostnames, the below doc should be updated to mention that + // passed in hostnames must also be known unique to use this flag. + /** + * Skip the probing step when advertising. + * + *

This must only be used when the service name ({@link NsdServiceInfo#getServiceName()} is + * known to be unique and cannot possibly be used by any other device on the network. + */ + public static final long FLAG_SKIP_PROBING = 1 << 1; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @LongDef(flag = true, prefix = {"FLAG_"}, value = { + FLAG_SKIP_PROBING, + }) + public @interface AdvertisingFlags {} @NonNull public static final Creator CREATOR = @@ -79,7 +99,7 @@ public final class AdvertisingRequest implements Parcelable { /** * The constructor for the advertiseRequest */ - private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, int protocolType, + private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, @ProtocolType int protocolType, long advertisingConfig, @NonNull Duration ttl) { mServiceInfo = serviceInfo; mProtocolType = protocolType; @@ -88,7 +108,7 @@ public final class AdvertisingRequest implements Parcelable { } /** - * Returns the {@link NsdServiceInfo} + * @return the {@link NsdServiceInfo} describing the service to advertise. */ @NonNull public NsdServiceInfo getServiceInfo() { @@ -96,16 +116,18 @@ public final class AdvertisingRequest implements Parcelable { } /** - * Returns the service advertise protocol + * @return the service advertisement protocol. */ + @ProtocolType public int getProtocolType() { return mProtocolType; } /** - * Returns the advertising config. + * @return the flags affecting advertising behavior. */ - public long getAdvertisingConfig() { + @AdvertisingFlags + public long getFlags() { return mAdvertisingConfig; } @@ -165,34 +187,45 @@ public final class AdvertisingRequest implements Parcelable { dest.writeLong(mTtl == null ? -1L : mTtl.getSeconds()); } -// @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API) /** - * The builder for creating new {@link AdvertisingRequest} objects. - * @hide + * A builder for creating new {@link AdvertisingRequest} objects. */ + @FlaggedApi(Flags.FLAG_IPV6_OVER_BLE) public static final class Builder { @NonNull private final NsdServiceInfo mServiceInfo; - private final int mProtocolType; + private int mProtocolType; private long mAdvertisingConfig; @Nullable private Duration mTtl; + /** * Creates a new {@link Builder} object. + * @param serviceInfo the {@link NsdServiceInfo} describing the service to advertise. + * @param protocolType the advertising protocol to use. + * @hide */ - public Builder(@NonNull NsdServiceInfo serviceInfo, int protocolType) { + public Builder(@NonNull NsdServiceInfo serviceInfo, @ProtocolType int protocolType) { mServiceInfo = serviceInfo; mProtocolType = protocolType; } + /** + * Creates a new {@link Builder} object. + * @param serviceInfo the {@link NsdServiceInfo} describing the service to advertise. + */ + public Builder(@NonNull NsdServiceInfo serviceInfo) { + this(serviceInfo, NsdManager.PROTOCOL_DNS_SD); + } + /** * Sets advertising configuration flags. * - * @param advertisingConfigFlags Bitmask of {@code AdvertisingConfig} flags. + * @param flags flags to use for advertising. */ @NonNull - public Builder setAdvertisingConfig(long advertisingConfigFlags) { - mAdvertisingConfig = advertisingConfigFlags; + public Builder setFlags(@AdvertisingFlags long flags) { + mAdvertisingConfig = flags; return this; } @@ -232,6 +265,16 @@ public final class AdvertisingRequest implements Parcelable { return this; } + /** + * Sets the protocol to use for advertising. + * @param protocolType the advertising protocol to use. + */ + @NonNull + public Builder setProtocolType(@ProtocolType int protocolType) { + mProtocolType = protocolType; + return this; + } + /** Creates a new {@link AdvertisingRequest} object. */ @NonNull public AdvertisingRequest build() { diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java index 116bea6a0e..426a92d2bf 100644 --- a/framework-t/src/android/net/nsd/NsdManager.java +++ b/framework-t/src/android/net/nsd/NsdManager.java @@ -314,6 +314,13 @@ public final class NsdManager { /** Dns based service discovery protocol */ public static final int PROTOCOL_DNS_SD = 0x0001; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"PROTOCOL_"}, value = { + PROTOCOL_DNS_SD, + }) + public @interface ProtocolType {} + /** * The minimum TTL seconds which is allowed for a service registration. * @@ -1272,7 +1279,7 @@ public final class NsdManager { // documented in the NsdServiceInfo.setSubtypes API instead, but this provides a limited // option for users of the older undocumented behavior, only for subtype changes. if (isSubtypeUpdateRequest(serviceInfo, listener)) { - builder.setAdvertisingConfig(AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY); + builder.setFlags(AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY); } registerService(builder.build(), executor, listener); } @@ -1358,7 +1365,7 @@ public final class NsdManager { checkProtocol(protocolType); final int key; // For update only request, the old listener has to be reused - if ((advertisingRequest.getAdvertisingConfig() + if ((advertisingRequest.getFlags() & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0) { key = updateRegisteredListener(listener, executor, serviceInfo); } else { diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java index 52d6e7eb84..6a5ab4d978 100644 --- a/framework-t/src/android/net/nsd/NsdServiceInfo.java +++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java @@ -211,6 +211,8 @@ public final class NsdServiceInfo implements Parcelable { return mHostname; } + // TODO: if setHostname is made public, AdvertisingRequest#FLAG_SKIP_PROBING javadoc must be + // updated to mention that hostnames must also be known unique to use that flag. /** * Set a custom hostname for this service instance for registration. * diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java index 0adb2908c9..7c8585a508 100644 --- a/service-t/src/com/android/server/NsdService.java +++ b/service-t/src/com/android/server/NsdService.java @@ -23,6 +23,7 @@ import static android.net.ConnectivityManager.NETID_UNSET; import static android.net.NetworkCapabilities.TRANSPORT_VPN; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK; +import static android.net.nsd.AdvertisingRequest.FLAG_SKIP_PROBING; import static android.net.nsd.NsdManager.MDNS_DISCOVERY_MANAGER_EVENT; import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT; import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED; @@ -981,7 +982,7 @@ public class NsdService extends INsdManager.Stub { NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */); break; } - boolean isUpdateOnly = (advertisingRequest.getAdvertisingConfig() + boolean isUpdateOnly = (advertisingRequest.getFlags() & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0; // If it is an update request, then reuse the old transactionId if (isUpdateOnly) { @@ -1046,9 +1047,12 @@ public class NsdService extends INsdManager.Stub { serviceInfo.setSubtypes(subtypes); maybeStartMonitoringSockets(); + final boolean skipProbing = (advertisingRequest.getFlags() + & FLAG_SKIP_PROBING) > 0; final MdnsAdvertisingOptions mdnsAdvertisingOptions = MdnsAdvertisingOptions.newBuilder() .setIsOnlyUpdate(isUpdateOnly) + .setSkipProbing(skipProbing) .setTtl(advertisingRequest.getTtl()) .build(); mAdvertiser.addOrUpdateService(transactionId, serviceInfo, diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java index a81d1e489b..5133d4f730 100644 --- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java +++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java @@ -34,13 +34,15 @@ public class MdnsAdvertisingOptions { private final boolean mIsOnlyUpdate; @Nullable private final Duration mTtl; + private final boolean mSkipProbing; /** * Parcelable constructs for a {@link MdnsAdvertisingOptions}. */ - MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl) { + MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl, boolean skipProbing) { this.mIsOnlyUpdate = isOnlyUpdate; this.mTtl = ttl; + this.mSkipProbing = skipProbing; } /** @@ -67,6 +69,13 @@ public class MdnsAdvertisingOptions { return mIsOnlyUpdate; } + /** + * @return {@code true} if the probing step should be skipped. + */ + public boolean skipProbing() { + return mSkipProbing; + } + /** * Returns the TTL for all records in a service. */ @@ -104,6 +113,7 @@ public class MdnsAdvertisingOptions { */ public static final class Builder { private boolean mIsOnlyUpdate = false; + private boolean mSkipProbing = false; @Nullable private Duration mTtl; @@ -126,11 +136,19 @@ public class MdnsAdvertisingOptions { return this; } + /** + * Sets whether to skip the probing step. + */ + public Builder setSkipProbing(boolean skipProbing) { + this.mSkipProbing = skipProbing; + return this; + } + /** * Builds a {@link MdnsAdvertisingOptions} with the arguments supplied to this builder. */ public MdnsAdvertisingOptions build() { - return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl); + return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl, mSkipProbing); } } } diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java index 58defa9886..b9b09ed0c8 100644 --- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java +++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java @@ -122,28 +122,32 @@ public class MdnsInterfaceAdvertiser implements MulticastPacketReader.PacketHand } @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; - } + handleProbingFinished(info); + } + } - mAnnouncer.startSending(info.getServiceId(), announcementInfo, - 0L /* initialDelayMs */); + private void handleProbingFinished(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; + } - // 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); - } + 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); } } @@ -280,7 +284,12 @@ public class MdnsInterfaceAdvertiser implements MulticastPacketReader.PacketHand + " getting re-added, cancelling exit announcements"); mAnnouncer.stop(replacedExitingService); } - mProber.startProbing(mRecordRepository.setServiceProbing(id)); + final MdnsProber.ProbingInfo probingInfo = mRecordRepository.setServiceProbing(id); + if (advertisingOptions.skipProbing()) { + handleProbingFinished(probingInfo); + } else { + mProber.startProbing(probingInfo); + } } /** diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt index 7fc8863092..c981a1b7c3 100644 --- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt +++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt @@ -40,8 +40,11 @@ import android.net.TestNetworkManager import android.net.TestNetworkSpecifier import android.net.connectivity.ConnectivityCompatChanges import android.net.cts.util.CtsNetUtils +import android.net.nsd.AdvertisingRequest +import android.net.nsd.AdvertisingRequest.FLAG_SKIP_PROBING import android.net.nsd.DiscoveryRequest import android.net.nsd.NsdManager +import android.net.nsd.NsdManager.PROTOCOL_DNS_SD import android.net.nsd.NsdServiceInfo import android.net.nsd.OffloadEngine import android.net.nsd.OffloadServiceInfo @@ -98,9 +101,9 @@ import com.android.testutils.NsdServiceInfoCallbackRecord import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdated import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.ServiceUpdatedLost import com.android.testutils.NsdServiceInfoCallbackRecord.ServiceInfoCallbackEvent.UnregisterCallbackSucceeded +import com.android.testutils.PollPacketReader import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged -import com.android.testutils.PollPacketReader import com.android.testutils.TestDnsPacket import com.android.testutils.TestableNetworkAgent import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated @@ -2629,6 +2632,49 @@ class NsdManagerTest { verifyCachedServicesRemoval(isCachedServiceRemoved = true) } + @Test + fun testSkipProbing() { + val si = makeTestServiceInfo(testNetwork1.network) + val request = AdvertisingRequest.Builder(si) + .setFlags(FLAG_SKIP_PROBING) + .build() + assertEquals(FLAG_SKIP_PROBING, request.flags) + assertEquals(PROTOCOL_DNS_SD, request.protocolType) + assertEquals(si.serviceName, request.serviceInfo.serviceName) + + // Register service on testNetwork1 + val registrationRecord = NsdRegistrationRecord() + nsdManager.registerService(request, { it.run() }, registrationRecord) + registrationRecord.expectCallback() + val packetReader = makePacketReader() + + tryTest { + val srvRecordName = "$serviceName.$serviceType.local" + // Look for either announcements or probes + val packet = packetReader.pollForMdnsPacket { + it.isProbeFor(srvRecordName) || it.isReplyFor(srvRecordName) + } + assertNotNull(packet, "Probe or announcement not received within timeout") + // The first packet should be an announcement, not a probe. + assertTrue("Found initial probes with NSD_ADVERTISING_SKIP_PROBING enabled", + packet.isReplyFor(srvRecordName)) + + // Force a conflict now that the service is getting announced + val conflictingAnnouncement = buildConflictingAnnouncement() + packetReader.sendResponse(conflictingAnnouncement) + + // Expect to see probes now (RFC6762 9., service is reset to probing state) + assertNotNull(packetReader.pollForProbe(serviceName, serviceType), + "Probe not received within timeout after conflict") + } cleanupStep { + nsdManager.unregisterService(registrationRecord) + registrationRecord.expectCallback() + } cleanup { + packetReader.handler.post { packetReader.stop() } + handlerThread.waitForIdle(TIMEOUT_MS) + } + } + private fun hasServiceTypeClientsForNetwork(clients: List, network: Network): Boolean { return clients.any { client -> client.substring( client.indexOf("network=") + "network=".length, diff --git a/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt index c491f373a6..8117431e06 100644 --- a/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt +++ b/tests/unit/java/android/net/nsd/AdvertisingRequestTest.kt @@ -44,14 +44,14 @@ class AdvertisingRequestTest { serviceType = "_ipp._tcp" } val beforeParcel = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD) - .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY) + .setFlags(NSD_ADVERTISING_UPDATE_ONLY) .setTtl(Duration.ofSeconds(30L)) .build() val afterParcel = parcelingRoundTrip(beforeParcel) assertEquals(beforeParcel.serviceInfo.serviceType, afterParcel.serviceInfo.serviceType) - assertEquals(beforeParcel.advertisingConfig, afterParcel.advertisingConfig) + assertEquals(beforeParcel.flags, afterParcel.flags) } @Test @@ -72,13 +72,13 @@ class AdvertisingRequestTest { serviceType = "_ipp._tcp" } val request = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD) - .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY) + .setFlags(NSD_ADVERTISING_UPDATE_ONLY) .setTtl(Duration.ofSeconds(100L)) .build() assertEquals("_ipp._tcp", request.serviceInfo.serviceType) assertEquals(PROTOCOL_DNS_SD, request.protocolType) - assertEquals(NSD_ADVERTISING_UPDATE_ONLY, request.advertisingConfig) + assertEquals(NSD_ADVERTISING_UPDATE_ONLY, request.flags) assertEquals(Duration.ofSeconds(100L), request.ttl) } @@ -90,11 +90,11 @@ class AdvertisingRequestTest { val request1 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build() val request2 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD).build() val request3 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD) - .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY) + .setFlags(NSD_ADVERTISING_UPDATE_ONLY) .setTtl(Duration.ofSeconds(120L)) .build() val request4 = AdvertisingRequest.Builder(info, PROTOCOL_DNS_SD) - .setAdvertisingConfig(NSD_ADVERTISING_UPDATE_ONLY) + .setFlags(NSD_ADVERTISING_UPDATE_ONLY) .setTtl(Duration.ofSeconds(120L)) .build()