[mdns] add service-side impl for NSD service TTL support
This is the service-side implementation of the custom service TTL
support added in aosp/2606573.
Bug: 284903641
Test: atest CtsNetTestCases FrameworksNetTests
Change-Id: I35246dae37b9fd1710b99cdda10068928e418457
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 9ba49d2..e8f48a3 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -28,6 +28,7 @@
import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED;
import static android.net.nsd.NsdManager.SUBTYPE_LABEL_REGEX;
import static android.net.nsd.NsdManager.TYPE_REGEX;
+import static android.os.Process.SYSTEM_UID;
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
@@ -115,6 +116,7 @@
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -738,6 +740,33 @@
return new ArraySet<>(subtypeMap.values());
}
+ private boolean checkTtl(
+ @Nullable Duration ttl, @NonNull ClientInfo clientInfo) {
+ if (ttl == null) {
+ return true;
+ }
+
+ final long ttlSeconds = ttl.toSeconds();
+ final int uid = clientInfo.getUid();
+
+ // Allows Thread module in the system_server to register TTL that is smaller than
+ // 30 seconds
+ final long minTtlSeconds = uid == SYSTEM_UID ? 0 : NsdManager.TTL_SECONDS_MIN;
+
+ // Allows Thread module in the system_server to register TTL that is larger than
+ // 10 hours
+ final long maxTtlSeconds =
+ uid == SYSTEM_UID ? 0xffffffffL : NsdManager.TTL_SECONDS_MAX;
+
+ if (ttlSeconds < minTtlSeconds || ttlSeconds > maxTtlSeconds) {
+ mServiceLogs.e("ttlSeconds exceeds allowed range (value = "
+ + ttlSeconds + ", allowedRange = [" + minTtlSeconds
+ + ", " + maxTtlSeconds + " ])");
+ return false;
+ }
+ return true;
+ }
+
@Override
public boolean processMessage(Message msg) {
final ClientInfo clientInfo;
@@ -964,11 +993,19 @@
break;
}
+ if (!checkTtl(advertisingRequest.getTtl(), clientInfo)) {
+ clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
+ break;
+ }
+
serviceInfo.setSubtypes(subtypes);
maybeStartMonitoringSockets();
final MdnsAdvertisingOptions mdnsAdvertisingOptions =
- MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(
- isUpdateOnly).build();
+ MdnsAdvertisingOptions.newBuilder()
+ .setIsOnlyUpdate(isUpdateOnly)
+ .setTtl(advertisingRequest.getTtl())
+ .build();
mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
mdnsAdvertisingOptions, clientInfo.mUid);
storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
@@ -1511,6 +1548,7 @@
network == null ? INetd.LOCAL_NET_ID : network.netId,
serviceInfo.getInterfaceIndex());
servInfo.setSubtypes(dedupSubtypeLabels(serviceInfo.getSubtypes()));
+ servInfo.setExpirationTime(serviceInfo.getExpirationTime());
return servInfo;
}
@@ -2671,6 +2709,10 @@
return sb.toString();
}
+ public int getUid() {
+ return mUid;
+ }
+
private boolean isPreSClient() {
return mIsPreSClient;
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index 0b60572..c162bcc 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -449,7 +449,8 @@
mPendingRegistrations.put(id, registration);
for (int i = 0; i < mAdvertisers.size(); i++) {
try {
- mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo());
+ mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo(),
+ registration.getAdvertisingOptions());
} catch (NameConflictException e) {
mSharedLog.wtf("Name conflict adding services that should have unique names",
e);
@@ -515,7 +516,7 @@
final Registration registration = mPendingRegistrations.valueAt(i);
try {
advertiser.addService(mPendingRegistrations.keyAt(i),
- registration.getServiceInfo());
+ registration.getServiceInfo(), registration.getAdvertisingOptions());
} catch (NameConflictException e) {
mSharedLog.wtf("Name conflict adding services that should have unique names",
e);
@@ -587,15 +588,17 @@
@NonNull
private NsdServiceInfo mServiceInfo;
final int mClientUid;
+ private final MdnsAdvertisingOptions mAdvertisingOptions;
int mConflictDuringProbingCount;
int mConflictAfterProbingCount;
-
- private Registration(@NonNull NsdServiceInfo serviceInfo, int clientUid) {
+ private Registration(@NonNull NsdServiceInfo serviceInfo, int clientUid,
+ @NonNull MdnsAdvertisingOptions advertisingOptions) {
this.mOriginalServiceName = serviceInfo.getServiceName();
this.mOriginalHostname = serviceInfo.getHostname();
this.mServiceInfo = serviceInfo;
this.mClientUid = clientUid;
+ this.mAdvertisingOptions = advertisingOptions;
}
/** Check if the new {@link NsdServiceInfo} doesn't update any data other than subtypes. */
@@ -697,6 +700,11 @@
public NsdServiceInfo getServiceInfo() {
return mServiceInfo;
}
+
+ @NonNull
+ public MdnsAdvertisingOptions getAdvertisingOptions() {
+ return mAdvertisingOptions;
+ }
}
/**
@@ -855,7 +863,7 @@
}
mSharedLog.i("Adding service " + service + " with ID " + id + " and subtypes "
+ subtypes + " advertisingOptions " + advertisingOptions);
- registration = new Registration(service, clientUid);
+ registration = new Registration(service, clientUid, advertisingOptions);
final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
if (network == null) {
// If registering on all networks, no advertiser must have conflicts
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 e7a6ca7..a81d1e4 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertisingOptions.java
@@ -16,6 +16,11 @@
package com.android.server.connectivity.mdns;
+import android.annotation.Nullable;
+
+import java.time.Duration;
+import java.util.Objects;
+
/**
* API configuration parameters for advertising the mDNS service.
*
@@ -27,13 +32,15 @@
private static MdnsAdvertisingOptions sDefaultOptions;
private final boolean mIsOnlyUpdate;
+ @Nullable
+ private final Duration mTtl;
/**
* Parcelable constructs for a {@link MdnsAdvertisingOptions}.
*/
- MdnsAdvertisingOptions(
- boolean isOnlyUpdate) {
+ MdnsAdvertisingOptions(boolean isOnlyUpdate, @Nullable Duration ttl) {
this.mIsOnlyUpdate = isOnlyUpdate;
+ this.mTtl = ttl;
}
/**
@@ -60,9 +67,36 @@
return mIsOnlyUpdate;
}
+ /**
+ * Returns the TTL for all records in a service.
+ */
+ @Nullable
+ public Duration getTtl() {
+ return mTtl;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof MdnsAdvertisingOptions)) {
+ return false;
+ } else {
+ final MdnsAdvertisingOptions otherOptions = (MdnsAdvertisingOptions) other;
+ return mIsOnlyUpdate == otherOptions.mIsOnlyUpdate
+ && Objects.equals(mTtl, otherOptions.mTtl);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIsOnlyUpdate, mTtl);
+ }
+
@Override
public String toString() {
- return "MdnsAdvertisingOptions{" + "mIsOnlyUpdate=" + mIsOnlyUpdate + '}';
+ return "MdnsAdvertisingOptions{" + "mIsOnlyUpdate=" + mIsOnlyUpdate + ", mTtl=" + mTtl
+ + '}';
}
/**
@@ -70,6 +104,8 @@
*/
public static final class Builder {
private boolean mIsOnlyUpdate = false;
+ @Nullable
+ private Duration mTtl;
private Builder() {
}
@@ -83,10 +119,18 @@
}
/**
+ * Sets the TTL duration for all records of the service.
+ */
+ public Builder setTtl(@Nullable Duration ttl) {
+ this.mTtl = ttl;
+ return this;
+ }
+
+ /**
* Builds a {@link MdnsAdvertisingOptions} with the arguments supplied to this builder.
*/
public MdnsAdvertisingOptions build() {
- return new MdnsAdvertisingOptions(mIsOnlyUpdate);
+ return new MdnsAdvertisingOptions(mIsOnlyUpdate, mTtl);
}
}
}
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 aa51c41..c2363c0 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -258,8 +258,10 @@
*
* @throws NameConflictException There is already a service being advertised with that name.
*/
- public void addService(int id, NsdServiceInfo service) throws NameConflictException {
- final int replacedExitingService = mRecordRepository.addService(id, service);
+ 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) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index ed0bde2..ac64c3a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -45,6 +45,7 @@
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -75,9 +76,9 @@
// TTL for records with a host name as the resource record's name (e.g., A, AAAA, HINFO) or a
// host name contained within the resource record's rdata (e.g., SRV, reverse mapping PTR
// record)
- private static final long NAME_RECORDS_TTL_MILLIS = TimeUnit.SECONDS.toMillis(120);
+ private static final long DEFAULT_NAME_RECORDS_TTL_MILLIS = TimeUnit.SECONDS.toMillis(120);
// TTL for other records
- private static final long NON_NAME_RECORDS_TTL_MILLIS = TimeUnit.MINUTES.toMillis(75);
+ private static final long DEFAULT_NON_NAME_RECORDS_TTL_MILLIS = TimeUnit.MINUTES.toMillis(75);
// Top-level domain for link-local queries, as per RFC6762 3.
private static final String LOCAL_TLD = "local";
@@ -193,6 +194,9 @@
*/
private boolean isProbing;
+ @Nullable
+ private Duration ttl;
+
/**
* Create a ServiceRegistration with only update the subType.
*/
@@ -200,16 +204,32 @@
NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
newServiceInfo.setSubtypes(newSubtypes);
return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
- repliedServiceCount, sentPacketCount, exiting, isProbing);
+ repliedServiceCount, sentPacketCount, exiting, isProbing, ttl);
}
/**
* Create a ServiceRegistration for dns-sd service registration (RFC6763).
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
- int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing) {
+ int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing,
+ @Nullable Duration ttl) {
this.serviceInfo = serviceInfo;
+ final long nonNameRecordsTtlMillis;
+ final long nameRecordsTtlMillis;
+
+ // When custom TTL is specified, all records of the service will use the custom TTL.
+ // This is typically useful for SRP (Service Registration Protocol:
+ // https://datatracker.ietf.org/doc/html/draft-ietf-dnssd-srp-24) Advertising Proxy
+ // where all records in a single SRP are required the same TTL.
+ if (ttl != null) {
+ nonNameRecordsTtlMillis = ttl.toMillis();
+ nameRecordsTtlMillis = ttl.toMillis();
+ } else {
+ nonNameRecordsTtlMillis = DEFAULT_NON_NAME_RECORDS_TTL_MILLIS;
+ nameRecordsTtlMillis = DEFAULT_NAME_RECORDS_TTL_MILLIS;
+ }
+
final boolean hasService = !TextUtils.isEmpty(serviceInfo.getServiceType());
final boolean hasCustomHost = !TextUtils.isEmpty(serviceInfo.getHostname());
final String[] hostname =
@@ -229,7 +249,7 @@
serviceType,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- NON_NAME_RECORDS_TTL_MILLIS,
+ nonNameRecordsTtlMillis,
serviceName),
true /* sharedName */));
for (String subtype : serviceInfo.getSubtypes()) {
@@ -239,7 +259,7 @@
MdnsUtils.constructFullSubtype(serviceType, subtype),
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- NON_NAME_RECORDS_TTL_MILLIS,
+ nonNameRecordsTtlMillis,
serviceName),
true /* sharedName */));
}
@@ -249,7 +269,7 @@
new MdnsServiceRecord(serviceName,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- NAME_RECORDS_TTL_MILLIS,
+ nameRecordsTtlMillis,
0 /* servicePriority */, 0 /* serviceWeight */,
serviceInfo.getPort(),
hostname),
@@ -261,7 +281,7 @@
0L /* receiptTimeMillis */,
// Service name is verified unique after probing
true /* cacheFlush */,
- NON_NAME_RECORDS_TTL_MILLIS,
+ nonNameRecordsTtlMillis,
attrsToTextEntries(serviceInfo.getAttributes())),
false /* sharedName */);
@@ -275,7 +295,7 @@
DNS_SD_SERVICE_TYPE,
0L /* receiptTimeMillis */,
false /* cacheFlush */,
- NON_NAME_RECORDS_TTL_MILLIS,
+ nonNameRecordsTtlMillis,
serviceType),
true /* sharedName */));
} else {
@@ -292,7 +312,7 @@
new MdnsInetAddressRecord(hostname,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- NAME_RECORDS_TTL_MILLIS,
+ nameRecordsTtlMillis,
address),
false /* sharedName */));
}
@@ -315,9 +335,9 @@
* @param serviceInfo Service to advertise
*/
ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
- int repliedServiceCount, int sentPacketCount) {
+ int repliedServiceCount, int sentPacketCount, @Nullable Duration ttl) {
this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
- false /* exiting */, true /* isProbing */);
+ false /* exiting */, true /* isProbing */, ttl);
}
void setProbing(boolean probing) {
@@ -339,7 +359,7 @@
revDnsAddr,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- NAME_RECORDS_TTL_MILLIS,
+ DEFAULT_NAME_RECORDS_TTL_MILLIS,
mDeviceHostname),
false /* sharedName */));
@@ -349,7 +369,7 @@
mDeviceHostname,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
- NAME_RECORDS_TTL_MILLIS,
+ DEFAULT_NAME_RECORDS_TTL_MILLIS,
addr.getAddress()),
false /* sharedName */));
}
@@ -378,11 +398,13 @@
* This may remove/replace any existing service that used the name added but is exiting.
* @param serviceId A unique service ID.
* @param serviceInfo Service info to add.
+ * @param ttl the TTL duration for all records of {@code serviceInfo} or {@code null}
* @return If the added service replaced another with a matching name (which was exiting), the
* ID of the replaced service.
* @throws NameConflictException There is already a (non-exiting) service using the name.
*/
- public int addService(int serviceId, NsdServiceInfo serviceInfo) throws NameConflictException {
+ public int addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable Duration ttl)
+ throws NameConflictException {
if (mServices.contains(serviceId)) {
throw new IllegalArgumentException(
"Service ID must not be reused across registrations: " + serviceId);
@@ -397,7 +419,7 @@
final ServiceRegistration registration = new ServiceRegistration(
mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
- NO_PACKET /* sentPacketCount */);
+ NO_PACKET /* sentPacketCount */, ttl);
mServices.put(serviceId, registration);
// Remove existing exiting service
@@ -776,7 +798,7 @@
true /* cacheFlush */,
// TODO: RFC6762 6.1: "In general, the TTL given for an NSEC record SHOULD
// be the same as the TTL that the record would have had, had it existed."
- NAME_RECORDS_TTL_MILLIS,
+ DEFAULT_NAME_RECORDS_TTL_MILLIS,
question.getName(),
new int[] { question.getType() });
additionalAnswerInfo.add(
@@ -1211,7 +1233,7 @@
if (existing == null) return null;
final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
- existing.repliedServiceCount, existing.sentPacketCount);
+ existing.repliedServiceCount, existing.sentPacketCount, existing.ttl);
mServices.put(serviceId, newService);
return makeProbingInfo(serviceId, newService);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
index 78df6df..f60a95e 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -28,6 +28,7 @@
import com.android.net.module.util.ByteUtils;
import java.nio.charset.Charset;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -62,7 +63,8 @@
source.createStringArrayList(),
source.createTypedArrayList(TextEntry.CREATOR),
source.readInt(),
- source.readParcelable(null));
+ source.readParcelable(Network.class.getClassLoader()),
+ Instant.ofEpochSecond(source.readLong()));
}
@Override
@@ -89,6 +91,9 @@
@Nullable
private final Network network;
+ @NonNull
+ private final Instant expirationTime;
+
/** Constructs a {@link MdnsServiceInfo} object with default values. */
public MdnsServiceInfo(
String serviceInstanceName,
@@ -110,7 +115,8 @@
textStrings,
/* textEntries= */ null,
/* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
- /* network= */ null);
+ /* network= */ null,
+ /* expirationTime= */ Instant.MAX);
}
/** Constructs a {@link MdnsServiceInfo} object with default values. */
@@ -135,7 +141,8 @@
textStrings,
textEntries,
/* interfaceIndex= */ INTERFACE_INDEX_UNSPECIFIED,
- /* network= */ null);
+ /* network= */ null,
+ /* expirationTime= */ Instant.MAX);
}
/**
@@ -165,7 +172,8 @@
textStrings,
textEntries,
interfaceIndex,
- /* network= */ null);
+ /* network= */ null,
+ /* expirationTime= */ Instant.MAX);
}
/**
@@ -184,7 +192,8 @@
@Nullable List<String> textStrings,
@Nullable List<TextEntry> textEntries,
int interfaceIndex,
- @Nullable Network network) {
+ @Nullable Network network,
+ @NonNull Instant expirationTime) {
this.serviceInstanceName = serviceInstanceName;
this.serviceType = serviceType;
this.subtypes = new ArrayList<>();
@@ -217,6 +226,7 @@
this.attributes = Collections.unmodifiableMap(attributes);
this.interfaceIndex = interfaceIndex;
this.network = network;
+ this.expirationTime = Instant.ofEpochSecond(expirationTime.getEpochSecond());
}
private static List<TextEntry> parseTextStrings(List<String> textStrings) {
@@ -314,6 +324,17 @@
}
/**
+ * Returns the timestamp after when this service is expired or {@code null} if the expiration
+ * time is unknown.
+ *
+ * A service is considered expired if any of its DNS record is expired.
+ */
+ @NonNull
+ public Instant getExpirationTime() {
+ return expirationTime;
+ }
+
+ /**
* Returns attribute value for {@code key} as a UTF-8 string. It's the caller who must make sure
* that the value of {@code key} is indeed a UTF-8 string. {@code null} will be returned if no
* attribute value exists for {@code key}.
@@ -364,6 +385,7 @@
out.writeTypedList(textEntries);
out.writeInt(interfaceIndex);
out.writeParcelable(network, 0);
+ out.writeLong(expirationTime.getEpochSecond());
}
@Override
@@ -377,7 +399,8 @@
+ ", interfaceIndex: " + interfaceIndex
+ ", network: " + network
+ ", textStrings: " + textStrings
- + ", textEntries: " + textEntries;
+ + ", textEntries: " + textEntries
+ + ", expirationTime: " + expirationTime;
}
@@ -496,4 +519,4 @@
out.writeByteArray(value);
}
}
-}
\ No newline at end of file
+}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 4cb88b4..16f6362 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -37,6 +37,7 @@
import java.net.Inet4Address;
import java.net.Inet6Address;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -309,6 +310,7 @@
textStrings = response.getTextRecord().getStrings();
textEntries = response.getTextRecord().getEntries();
}
+ Instant now = Instant.now();
// TODO: Throw an error message if response doesn't have Inet6 or Inet4 address.
return new MdnsServiceInfo(
serviceInstanceName,
@@ -321,7 +323,8 @@
textStrings,
textEntries,
response.getInterfaceIndex(),
- response.getNetwork());
+ response.getNetwork(),
+ now.plusMillis(response.getMinRemainingTtl(now.toEpochMilli())));
}
/**