[mdns] add hidden API for public key

This commit adds support of registering a public key for a host/service.
This is required to enable Advertising Proxy feature for Thread devices.

For example:
```
NsdServiceInfo info = new NsdServiceInfo();

info.setServiceName("My Service");
info.setServiceType("_test._tcp");
info.setHostname("MyHost");
info.setHostAddresses(List.of(address1, address2));
info.setPublicKey(/* KEY RDATA */);

nsdManager.registerService(info, PROTOCOL_DNS_SD, listener);
```

Bug: 317946010

Change-Id: I367ebff8119d5c1dff0410c85e6fb86dca6c66b8
This commit is contained in:
Handa Wang 2024-01-07 08:54:59 +00:00
commit 364aa570e2
6 changed files with 600 additions and 26 deletions

View file

@ -389,6 +389,7 @@ public final class NsdManager {
}
private static final int FIRST_LISTENER_KEY = 1;
private static final int DNSSEC_PROTOCOL = 3;
private final INsdServiceConnector mService;
private final Context mContext;
@ -1754,45 +1755,132 @@ public final class NsdManager {
}
}
private enum ServiceValidationType {
NO_SERVICE,
HAS_SERVICE, // A service with a positive port
HAS_SERVICE_ZERO_PORT, // A service with a zero port
}
private enum HostValidationType {
DEFAULT_HOST, // No host is specified so the default host will be used
CUSTOM_HOST, // A custom host with addresses is specified
CUSTOM_HOST_NO_ADDRESS, // A custom host without address is specified
}
private enum PublicKeyValidationType {
NO_KEY,
HAS_KEY,
}
/**
* Check if the service is valid for registration and classify it as one of {@link
* ServiceValidationType}.
*/
private static ServiceValidationType validateService(NsdServiceInfo serviceInfo) {
final boolean hasServiceName = !TextUtils.isEmpty(serviceInfo.getServiceName());
final boolean hasServiceType = !TextUtils.isEmpty(serviceInfo.getServiceType());
if (!hasServiceName && !hasServiceType && serviceInfo.getPort() == 0) {
return ServiceValidationType.NO_SERVICE;
}
if (hasServiceName && hasServiceType) {
if (serviceInfo.getPort() < 0) {
throw new IllegalArgumentException("Invalid port");
}
if (serviceInfo.getPort() == 0) {
return ServiceValidationType.HAS_SERVICE_ZERO_PORT;
}
return ServiceValidationType.HAS_SERVICE;
}
throw new IllegalArgumentException("The service name or the service type is missing");
}
/**
* Check if the host is valid for registration and classify it as one of {@link
* HostValidationType}.
*/
private static HostValidationType validateHost(NsdServiceInfo serviceInfo) {
final boolean hasHostname = !TextUtils.isEmpty(serviceInfo.getHostname());
final boolean hasHostAddresses = !CollectionUtils.isEmpty(serviceInfo.getHostAddresses());
if (!hasHostname) {
// Keep compatible with the legacy behavior: It's allowed to set host
// addresses for a service registration although the host addresses
// won't be registered. To register the addresses for a host, the
// hostname must be specified.
return HostValidationType.DEFAULT_HOST;
}
if (!hasHostAddresses) {
return HostValidationType.CUSTOM_HOST_NO_ADDRESS;
}
return HostValidationType.CUSTOM_HOST;
}
/**
* Check if the public key is valid for registration and classify it as one of {@link
* PublicKeyValidationType}.
*
* <p>For simplicity, it only checks if the protocol is DNSSEC and the RDATA is not fewer than 4
* bytes. See RFC 3445 Section 3.
*/
private static PublicKeyValidationType validatePublicKey(NsdServiceInfo serviceInfo) {
byte[] publicKey = serviceInfo.getPublicKey();
if (publicKey == null) {
return PublicKeyValidationType.NO_KEY;
}
if (publicKey.length < 4) {
throw new IllegalArgumentException("The public key should be at least 4 bytes long");
}
int protocol = publicKey[2];
if (protocol == DNSSEC_PROTOCOL) {
return PublicKeyValidationType.HAS_KEY;
}
throw new IllegalArgumentException(
"The public key's protocol ("
+ protocol
+ ") is invalid. It should be DNSSEC_PROTOCOL (3)");
}
/**
* Check if the {@link NsdServiceInfo} is valid for registration.
*
* The following can be registered:
* - A service with an optional host.
* - A hostname with addresses.
* <p>Firstly, check if service, host and public key are all valid respectively. Then check if
* the combination of service, host and public key is valid.
*
* Note that:
* - When registering a service, the service name, service type and port must be specified. If
* hostname is specified, the host addresses can optionally be specified.
* - When registering a host without a service, the addresses must be specified.
* <p>If the {@code serviceInfo} is invalid, throw an {@link IllegalArgumentException}
* describing the reason.
*
* <p>There are the invalid combinations of service, host and public key:
*
* <ul>
* <li>Neither service nor host is specified.
* <li>No public key is specified and the service has a zero port.
* <li>The registration only contains the hostname but addresses are missing.
* </ul>
*
* <p>Keys are used to reserve hostnames or service names while the service/host is temporarily
* inactive, so registrations with a key and just a hostname or a service name are acceptable.
*
* @hide
*/
public static void checkServiceInfoForRegistration(NsdServiceInfo serviceInfo) {
Objects.requireNonNull(serviceInfo, "NsdServiceInfo cannot be null");
boolean hasServiceName = !TextUtils.isEmpty(serviceInfo.getServiceName());
boolean hasServiceType = !TextUtils.isEmpty(serviceInfo.getServiceType());
boolean hasHostname = !TextUtils.isEmpty(serviceInfo.getHostname());
boolean hasHostAddresses = !CollectionUtils.isEmpty(serviceInfo.getHostAddresses());
if (serviceInfo.getPort() < 0) {
throw new IllegalArgumentException("Invalid port");
final ServiceValidationType serviceValidation = validateService(serviceInfo);
final HostValidationType hostValidation = validateHost(serviceInfo);
final PublicKeyValidationType publicKeyValidation = validatePublicKey(serviceInfo);
if (serviceValidation == ServiceValidationType.NO_SERVICE
&& hostValidation == HostValidationType.DEFAULT_HOST) {
throw new IllegalArgumentException("Nothing to register");
}
if (hasServiceType || hasServiceName || (serviceInfo.getPort() > 0)) {
if (!(hasServiceType && hasServiceName && (serviceInfo.getPort() > 0))) {
if (publicKeyValidation == PublicKeyValidationType.NO_KEY) {
if (serviceValidation == ServiceValidationType.HAS_SERVICE_ZERO_PORT) {
throw new IllegalArgumentException("The port is missing");
}
if (serviceValidation == ServiceValidationType.NO_SERVICE
&& hostValidation == HostValidationType.CUSTOM_HOST_NO_ADDRESS) {
throw new IllegalArgumentException(
"The service type, service name or port is missing");
"The host addresses must be specified unless there is a service");
}
}
if (!hasServiceType && !hasHostname) {
throw new IllegalArgumentException("No service or host specified in NsdServiceInfo");
}
if (!hasServiceType && hasHostname && !hasHostAddresses) {
// TODO: b/317946010 - This may be allowed when it supports registering KEY RR.
throw new IllegalArgumentException("No host addresses specified in NsdServiceInfo");
}
}
}

View file

@ -37,6 +37,7 @@ import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -68,6 +69,9 @@ public final class NsdServiceInfo implements Parcelable {
private int mPort;
@Nullable
private byte[] mPublicKey;
@Nullable
private Network mNetwork;
@ -219,6 +223,40 @@ public final class NsdServiceInfo implements Parcelable {
mHostname = hostname;
}
/**
* Set the public key RDATA to be advertised in a KEY RR (RFC 2535).
*
* <p>This is the public key of the key pair used for signing a DNS message (e.g. SRP). Clients
* typically don't need this information, but the KEY RR is usually published to claim the use
* of the DNS name so that another mDNS advertiser can't take over the ownership during a
* temporary power down of the original host device.
*
* <p>When the public key is set to non-null, exactly one KEY RR will be advertised for each of
* the service and host name if they are not null.
*
* @hide // For Thread only
*/
public void setPublicKey(@Nullable byte[] publicKey) {
if (publicKey == null) {
mPublicKey = null;
return;
}
mPublicKey = Arrays.copyOf(publicKey, publicKey.length);
}
/**
* Get the public key RDATA in the KEY RR (RFC 2535) or {@code null} if no KEY RR exists.
*
* @hide // For Thread only
*/
@Nullable
public byte[] getPublicKey() {
if (mPublicKey == null) {
return null;
}
return Arrays.copyOf(mPublicKey, mPublicKey.length);
}
/**
* Unpack txt information from a base-64 encoded byte array.
*
@ -622,6 +660,7 @@ public final class NsdServiceInfo implements Parcelable {
}
dest.writeString(mHostname);
dest.writeLong(mExpirationTime != null ? mExpirationTime.getEpochSecond() : -1);
dest.writeByteArray(mPublicKey);
}
/** Implement the Parcelable interface */
@ -654,6 +693,7 @@ public final class NsdServiceInfo implements Parcelable {
info.mHostname = in.readString();
final long seconds = in.readLong();
info.setExpirationTime(seconds < 0 ? null : Instant.ofEpochSecond(seconds));
info.mPublicKey = in.createByteArray();
return info;
}

View file

@ -16,6 +16,7 @@
package android.net.nsd;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
@ -51,6 +52,23 @@ public class NsdServiceInfoTest {
private static final InetAddress IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1");
private static final InetAddress IPV6_ADDRESS = InetAddresses.parseNumericAddress("2001:db8::");
private static final byte[] PUBLIC_KEY_RDATA = new byte[] {
(byte) 0x02, (byte)0x01, // flag
(byte) 0x03, // protocol
(byte) 0x0d, // algorithm
// 64-byte public key below
(byte) 0xC1, (byte) 0x41, (byte) 0xD0, (byte) 0x63, (byte) 0x79, (byte) 0x60,
(byte) 0xB9, (byte) 0x8C, (byte) 0xBC, (byte) 0x12, (byte) 0xCF, (byte) 0xCA,
(byte) 0x22, (byte) 0x1D, (byte) 0x28, (byte) 0x79, (byte) 0xDA, (byte) 0xC2,
(byte) 0x6E, (byte) 0xE5, (byte) 0xB4, (byte) 0x60, (byte) 0xE9, (byte) 0x00,
(byte) 0x7C, (byte) 0x99, (byte) 0x2E, (byte) 0x19, (byte) 0x02, (byte) 0xD8,
(byte) 0x97, (byte) 0xC3, (byte) 0x91, (byte) 0xB0, (byte) 0x37, (byte) 0x64,
(byte) 0xD4, (byte) 0x48, (byte) 0xF7, (byte) 0xD0, (byte) 0xC7, (byte) 0x72,
(byte) 0xFD, (byte) 0xB0, (byte) 0x3B, (byte) 0x1D, (byte) 0x9D, (byte) 0x6D,
(byte) 0x52, (byte) 0xFF, (byte) 0x88, (byte) 0x86, (byte) 0x76, (byte) 0x9E,
(byte) 0x8E, (byte) 0x23, (byte) 0x62, (byte) 0x51, (byte) 0x35, (byte) 0x65,
(byte) 0x27, (byte) 0x09, (byte) 0x62, (byte) 0xD3
};
@Test
public void testLimits() throws Exception {
@ -120,6 +138,7 @@ public class NsdServiceInfoTest {
fullInfo.setPort(4242);
fullInfo.setHostAddresses(List.of(IPV4_ADDRESS));
fullInfo.setHostname("home");
fullInfo.setPublicKey(PUBLIC_KEY_RDATA);
fullInfo.setNetwork(new Network(123));
fullInfo.setInterfaceIndex(456);
checkParcelable(fullInfo);
@ -136,6 +155,7 @@ public class NsdServiceInfoTest {
attributedInfo.setPort(4242);
attributedInfo.setHostAddresses(List.of(IPV6_ADDRESS, IPV4_ADDRESS));
attributedInfo.setHostname("home");
attributedInfo.setPublicKey(PUBLIC_KEY_RDATA);
attributedInfo.setAttribute("color", "pink");
attributedInfo.setAttribute("sound", (new String("にゃあ")).getBytes("UTF-8"));
attributedInfo.setAttribute("adorable", (String) null);
@ -172,6 +192,7 @@ public class NsdServiceInfoTest {
assertEquals(original.getServiceType(), result.getServiceType());
assertEquals(original.getHost(), result.getHost());
assertEquals(original.getHostname(), result.getHostname());
assertArrayEquals(original.getPublicKey(), result.getPublicKey());
assertTrue(original.getPort() == result.getPort());
assertEquals(original.getNetwork(), result.getNetwork());
assertEquals(original.getInterfaceIndex(), result.getInterfaceIndex());

View file

@ -286,6 +286,12 @@ fun TapPacketReader.pollForQuery(
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
): TestDnsPacket? = pollForMdnsPacket(timeoutMs) { it.isQueryFor(recordName, *requiredTypes) }
fun TapPacketReader.pollForReply(
recordName: String,
type: Int,
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
): TestDnsPacket? = pollForMdnsPacket(timeoutMs) { it.isReplyFor(recordName, type) }
fun TapPacketReader.pollForReply(
serviceName: String,
serviceType: String,

View file

@ -81,7 +81,9 @@ import com.android.compatibility.common.util.PropertyUtil
import com.android.compatibility.common.util.SystemUtil
import com.android.modules.utils.build.SdkLevel.isAtLeastU
import com.android.net.module.util.DnsPacket
import com.android.net.module.util.DnsPacket.ANSECTION
import com.android.net.module.util.HexDump
import com.android.net.module.util.HexDump.hexStringToByteArray
import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN
import com.android.net.module.util.PacketBuilder
import com.android.testutils.ConnectivityModuleTest
@ -96,6 +98,7 @@ import com.android.testutils.TapPacketReader
import com.android.testutils.TestableNetworkAgent
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
import com.android.testutils.TestableNetworkCallback
import com.android.testutils.assertContainsExactly
import com.android.testutils.assertEmpty
import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk33
@ -138,6 +141,9 @@ private const val REGISTRATION_TIMEOUT_MS = 10_000L
private const val DBG = false
private const val TEST_PORT = 12345
private const val MDNS_PORT = 5353.toShort()
private const val TYPE_KEY = 25
private const val QCLASS_INTERNET = 0x0001
private const val NAME_RECORDS_TTL_MILLIS: Long = 120
private val multicastIpv6Addr = parseNumericAddress("ff02::fb") as Inet6Address
private val testSrcAddr = parseNumericAddress("2001:db8::123") as Inet6Address
@ -167,6 +173,12 @@ class NsdManagerTest {
private val serviceType2 = "_nmt%09d._tcp".format(Random().nextInt(1_000_000_000))
private val customHostname = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000))
private val customHostname2 = "NsdTestHost%09d".format(Random().nextInt(1_000_000_000))
private val publicKey = hexStringToByteArray(
"0201030dc141d0637960b98cbc12cfca"
+ "221d2879dac26ee5b460e9007c992e19"
+ "02d897c391b03764d448f7d0c772fdb0"
+ "3b1d9d6d52ff8886769e8e2362513565"
+ "270962d3")
private val handlerThread = HandlerThread(NsdManagerTest::class.java.simpleName)
private val ctsNetUtils by lazy{ CtsNetUtils(context) }
@ -2265,6 +2277,165 @@ class NsdManagerTest {
}
}
@Test
fun testAdvertising_registerServiceAndPublicKey_keyAnnounced() {
val si = NsdServiceInfo().also {
it.network = testNetwork1.network
it.serviceType = serviceType
it.serviceName = serviceName
it.port = TEST_PORT
it.publicKey = publicKey
}
val packetReader = TapPacketReader(Handler(handlerThread.looper),
testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
packetReader.startAsyncForTest()
handlerThread.waitForIdle(TIMEOUT_MS)
val registrationRecord = NsdRegistrationRecord()
val discoveryRecord = NsdDiscoveryRecord()
tryTest {
registerService(registrationRecord, si)
val announcement = packetReader.pollForReply(
"$serviceName.$serviceType.local",
TYPE_KEY
)
assertNotNull(announcement)
val keyRecords = announcement.records[ANSECTION].filter { it.nsType == TYPE_KEY }
assertEquals(1, keyRecords.size)
val actualRecord = keyRecords.get(0)
assertEquals(TYPE_KEY, actualRecord.nsType)
assertEquals("$serviceName.$serviceType.local", actualRecord.dName)
assertEquals(NAME_RECORDS_TTL_MILLIS, actualRecord.ttl)
assertArrayEquals(publicKey, actualRecord.rr)
nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
testNetwork1.network, Executor { it.run() }, discoveryRecord)
val discoveredInfo1 = discoveryRecord.waitForServiceDiscovered(
serviceName, serviceType, testNetwork1.network)
val resolvedInfo1 = resolveService(discoveredInfo1)
assertEquals(serviceName, discoveredInfo1.serviceName)
assertEquals(TEST_PORT, resolvedInfo1.port)
} cleanupStep {
nsdManager.stopServiceDiscovery(discoveryRecord)
discoveryRecord.expectCallback<DiscoveryStopped>()
} cleanup {
nsdManager.unregisterService(registrationRecord)
}
}
@Test
fun testAdvertising_registerCustomHostAndPublicKey_keyAnnounced() {
val si = NsdServiceInfo().also {
it.network = testNetwork1.network
it.hostname = customHostname
it.hostAddresses = listOf(
parseNumericAddress("192.0.2.23"),
parseNumericAddress("2001:db8::1"),
parseNumericAddress("2001:db8::2"))
it.publicKey = publicKey
}
val packetReader = TapPacketReader(Handler(handlerThread.looper),
testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
packetReader.startAsyncForTest()
handlerThread.waitForIdle(TIMEOUT_MS)
val registrationRecord = NsdRegistrationRecord()
tryTest {
registerService(registrationRecord, si)
val announcement = packetReader.pollForReply("$customHostname.local", TYPE_KEY)
assertNotNull(announcement)
val keyRecords = announcement.records[ANSECTION].filter { it.nsType == TYPE_KEY }
assertEquals(1, keyRecords.size)
val actualRecord = keyRecords.get(0)
assertEquals(TYPE_KEY, actualRecord.nsType)
assertEquals("$customHostname.local", actualRecord.dName)
assertEquals(NAME_RECORDS_TTL_MILLIS, actualRecord.ttl)
assertArrayEquals(publicKey, actualRecord.rr)
// This test case focuses on key announcement so we don't check the details of the
// announcement of the custom host addresses.
val addressRecords = announcement.records[ANSECTION].filter {
it.nsType == DnsResolver.TYPE_AAAA ||
it.nsType == DnsResolver.TYPE_A
}
assertEquals(3, addressRecords.size)
} cleanup {
nsdManager.unregisterService(registrationRecord)
}
}
@Test
fun testAdvertising_registerTwoServicesWithSameCustomHostAndPublicKey_keyAnnounced() {
val si1 = NsdServiceInfo().also {
it.network = testNetwork1.network
it.serviceType = serviceType
it.serviceName = serviceName
it.port = TEST_PORT
it.hostname = customHostname
it.hostAddresses = listOf(
parseNumericAddress("192.0.2.23"),
parseNumericAddress("2001:db8::1"),
parseNumericAddress("2001:db8::2"))
it.publicKey = publicKey
}
val si2 = NsdServiceInfo().also {
it.network = testNetwork1.network
it.serviceType = serviceType2
it.serviceName = serviceName2
it.port = TEST_PORT + 1
it.hostname = customHostname
it.hostAddresses = listOf()
it.publicKey = publicKey
}
val packetReader = TapPacketReader(Handler(handlerThread.looper),
testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
packetReader.startAsyncForTest()
handlerThread.waitForIdle(TIMEOUT_MS)
val registrationRecord1 = NsdRegistrationRecord()
val registrationRecord2 = NsdRegistrationRecord()
tryTest {
registerService(registrationRecord1, si1)
var announcement =
packetReader.pollForReply("$serviceName.$serviceType.local", TYPE_KEY)
assertNotNull(announcement)
var keyRecords = announcement.records[ANSECTION].filter { it.nsType == TYPE_KEY }
assertEquals(2, keyRecords.size)
assertTrue(keyRecords.any { it.dName == "$serviceName.$serviceType.local" })
assertTrue(keyRecords.any { it.dName == "$customHostname.local" })
assertTrue(keyRecords.all { it.ttl == NAME_RECORDS_TTL_MILLIS })
assertTrue(keyRecords.all { it.rr.contentEquals(publicKey) })
// This test case focuses on key announcement so we don't check the details of the
// announcement of the custom host addresses.
val addressRecords = announcement.records[ANSECTION].filter {
it.nsType == DnsResolver.TYPE_AAAA ||
it.nsType == DnsResolver.TYPE_A
}
assertEquals(3, addressRecords.size)
registerService(registrationRecord2, si2)
announcement = packetReader.pollForReply("$serviceName2.$serviceType2.local", TYPE_KEY)
assertNotNull(announcement)
keyRecords = announcement.records[ANSECTION].filter { it.nsType == TYPE_KEY }
assertEquals(2, keyRecords.size)
assertTrue(keyRecords.any { it.dName == "$serviceName2.$serviceType2.local" })
assertTrue(keyRecords.any { it.dName == "$customHostname.local" })
assertTrue(keyRecords.all { it.ttl == NAME_RECORDS_TTL_MILLIS })
assertTrue(keyRecords.all { it.rr.contentEquals(publicKey) })
} cleanup {
nsdManager.unregisterService(registrationRecord1)
nsdManager.unregisterService(registrationRecord2)
}
}
@Test
fun testServiceTypeClientRemovedAfterSocketDestroyed() {
val si = makeTestServiceInfo(testNetwork1.network)

View file

@ -16,6 +16,11 @@
package android.net.nsd;
import static android.net.InetAddresses.parseNumericAddress;
import static android.net.nsd.NsdManager.checkServiceInfoForRegistration;
import static com.android.net.module.util.HexDump.hexStringToByteArray;
import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
@ -54,6 +59,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.net.InetAddress;
import java.util.Collections;
import java.util.List;
import java.time.Duration;
@ -395,6 +401,7 @@ public class NsdManagerTest {
NsdManager.RegistrationListener listener4 = mock(NsdManager.RegistrationListener.class);
NsdManager.RegistrationListener listener5 = mock(NsdManager.RegistrationListener.class);
NsdManager.RegistrationListener listener6 = mock(NsdManager.RegistrationListener.class);
NsdManager.RegistrationListener listener7 = mock(NsdManager.RegistrationListener.class);
NsdServiceInfo invalidService = new NsdServiceInfo(null, null);
NsdServiceInfo validService = new NsdServiceInfo("a_name", "_a_type._tcp");
@ -439,6 +446,19 @@ public class NsdManagerTest {
validServiceWithCustomHostNoAddresses.setPort(2222);
validServiceWithCustomHostNoAddresses.setHostname("a_host");
NsdServiceInfo validServiceWithPublicKey = new NsdServiceInfo("a_name", "_a_type._tcp");
validServiceWithPublicKey.setPublicKey(
hexStringToByteArray(
"0201030dc141d0637960b98cbc12cfca"
+ "221d2879dac26ee5b460e9007c992e19"
+ "02d897c391b03764d448f7d0c772fdb0"
+ "3b1d9d6d52ff8886769e8e2362513565"
+ "270962d3"));
NsdServiceInfo invalidServiceWithTooShortPublicKey =
new NsdServiceInfo("a_name", "_a_type._tcp");
invalidServiceWithTooShortPublicKey.setPublicKey(hexStringToByteArray("0201"));
// Service registration
// - invalid arguments
mustFail(() -> { manager.unregisterService(null); });
@ -449,6 +469,8 @@ public class NsdManagerTest {
mustFail(() -> { manager.registerService(validService, PROTOCOL, null); });
mustFail(() -> {
manager.registerService(invalidMissingHostnameWithAddresses, PROTOCOL, listener1); });
mustFail(() -> {
manager.registerService(invalidServiceWithTooShortPublicKey, PROTOCOL, listener1); });
manager.registerService(validService, PROTOCOL, listener1);
// - update without subtype is not allowed
mustFail(() -> { manager.registerService(validServiceDuplicate, PROTOCOL, listener1); });
@ -479,6 +501,9 @@ public class NsdManagerTest {
// - registering a service with a custom host with no addresses is valid
manager.registerService(validServiceWithCustomHostNoAddresses, PROTOCOL, listener6);
manager.unregisterService(listener6);
// - registering a service with a public key is valid
manager.registerService(validServiceWithPublicKey, PROTOCOL, listener7);
manager.unregisterService(listener7);
// Discover service
// - invalid arguments
@ -506,6 +531,229 @@ public class NsdManagerTest {
mustFail(() -> { manager.resolveService(validService, listener3); });
}
private static final class NsdServiceInfoBuilder {
private static final String SERVICE_NAME = "TestService";
private static final String SERVICE_TYPE = "_testservice._tcp";
private static final int SERVICE_PORT = 12345;
private static final String HOSTNAME = "TestHost";
private static final List<InetAddress> HOST_ADDRESSES =
List.of(parseNumericAddress("192.168.2.23"), parseNumericAddress("2001:db8::3"));
private static final byte[] PUBLIC_KEY =
hexStringToByteArray(
"0201030dc141d0637960b98cbc12cfca"
+ "221d2879dac26ee5b460e9007c992e19"
+ "02d897c391b03764d448f7d0c772fdb0"
+ "3b1d9d6d52ff8886769e8e2362513565"
+ "270962d3");
private final NsdServiceInfo mNsdServiceInfo = new NsdServiceInfo();
NsdServiceInfo build() {
return mNsdServiceInfo;
}
NsdServiceInfoBuilder setNoService() {
mNsdServiceInfo.setServiceName(null);
mNsdServiceInfo.setServiceType(null);
mNsdServiceInfo.setPort(0);
return this;
}
NsdServiceInfoBuilder setService() {
mNsdServiceInfo.setServiceName(SERVICE_NAME);
mNsdServiceInfo.setServiceType(SERVICE_TYPE);
mNsdServiceInfo.setPort(SERVICE_PORT);
return this;
}
NsdServiceInfoBuilder setZeroPortService() {
mNsdServiceInfo.setServiceName(SERVICE_NAME);
mNsdServiceInfo.setServiceType(SERVICE_TYPE);
mNsdServiceInfo.setPort(0);
return this;
}
NsdServiceInfoBuilder setInvalidService() {
mNsdServiceInfo.setServiceName(SERVICE_NAME);
mNsdServiceInfo.setServiceType(null);
mNsdServiceInfo.setPort(SERVICE_PORT);
return this;
}
NsdServiceInfoBuilder setDefaultHost() {
mNsdServiceInfo.setHostname(null);
mNsdServiceInfo.setHostAddresses(Collections.emptyList());
return this;
}
NsdServiceInfoBuilder setCustomHost() {
mNsdServiceInfo.setHostname(HOSTNAME);
mNsdServiceInfo.setHostAddresses(HOST_ADDRESSES);
return this;
}
NsdServiceInfoBuilder setCustomHostNoAddress() {
mNsdServiceInfo.setHostname(HOSTNAME);
mNsdServiceInfo.setHostAddresses(Collections.emptyList());
return this;
}
NsdServiceInfoBuilder setHostAddressesNoHostname() {
mNsdServiceInfo.setHostname(null);
mNsdServiceInfo.setHostAddresses(HOST_ADDRESSES);
return this;
}
NsdServiceInfoBuilder setNoPublicKey() {
mNsdServiceInfo.setPublicKey(null);
return this;
}
NsdServiceInfoBuilder setPublicKey() {
mNsdServiceInfo.setPublicKey(PUBLIC_KEY);
return this;
}
NsdServiceInfoBuilder setInvalidPublicKey() {
mNsdServiceInfo.setPublicKey(new byte[3]);
return this;
}
}
@Test
public void testCheckServiceInfoForRegistration() {
// The service is invalid
mustFail(() -> checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setInvalidService()
.setCustomHost()
.setPublicKey().build()));
// Keep compatible with the legacy behavior: It's allowed to set host
// addresses for a service registration although the host addresses
// won't be registered. To register the addresses for a host, the
// hostname must be specified.
checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setService()
.setHostAddressesNoHostname()
.setPublicKey().build());
// The public key is invalid
mustFail(() -> checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setService()
.setCustomHost()
.setInvalidPublicKey().build()));
// Invalid combinations
// 1. (service, custom host, key): valid
checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setService()
.setCustomHost()
.setPublicKey().build());
// 2. (service, custom host, no key): valid
checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setService()
.setCustomHost()
.setNoPublicKey().build());
// 3. (service, no-address custom host, key): valid
checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setService()
.setCustomHostNoAddress()
.setPublicKey().build());
// 4. (service, no-address custom host, no key): valid
checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setService()
.setCustomHostNoAddress()
.setNoPublicKey().build());
// 5. (service, default host, key): valid
checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setService()
.setDefaultHost()
.setPublicKey().build());
// 6. (service, default host, no key): valid
checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setService()
.setDefaultHost()
.setNoPublicKey().build());
// 7. (0-port service, custom host, valid key): valid
checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setZeroPortService()
.setCustomHost()
.setPublicKey().build());
// 8. (0-port service, custom host, no key): invalid
mustFail(() -> checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setZeroPortService()
.setCustomHost()
.setNoPublicKey().build()));
// 9. (0-port service, no-address custom host, key): valid
checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setZeroPortService()
.setCustomHostNoAddress()
.setPublicKey().build());
// 10. (0-port service, no-address custom host, no key): invalid
mustFail(() -> checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setZeroPortService()
.setCustomHostNoAddress()
.setNoPublicKey().build()));
// 11. (0-port service, default host, key): valid
checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setZeroPortService()
.setDefaultHost()
.setPublicKey().build());
// 12. (0-port service, default host, no key): invalid
mustFail(() -> checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setZeroPortService()
.setDefaultHost()
.setNoPublicKey().build()));
// 13. (no service, custom host, key): valid
checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setNoService()
.setCustomHost()
.setPublicKey().build());
// 14. (no service, custom host, no key): valid
checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setNoService()
.setCustomHost()
.setNoPublicKey().build());
// 15. (no service, no-address custom host, key): valid
checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setNoService()
.setCustomHostNoAddress()
.setPublicKey().build());
// 16. (no service, no-address custom host, no key): invalid
mustFail(() -> checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setNoService()
.setCustomHostNoAddress()
.setNoPublicKey().build()));
// 17. (no service, default host, key): invalid
mustFail(() -> checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setNoService()
.setDefaultHost()
.setPublicKey().build()));
// 18. (no service, default host, no key): invalid
mustFail(() -> checkServiceInfoForRegistration(
new NsdServiceInfoBuilder()
.setNoService()
.setDefaultHost()
.setNoPublicKey().build()));
}
public void mustFail(Runnable fn) {
try {
fn.run();