Add MdnsAnnouncer

MdnsAnnouncer is a implementations of MdnsPacketRepeater, which sends a
packet at various intervals.

It will be used to send initial advertisements when a service is
registered or there is a network change.

Bug: 241738458
Test: atest
Change-Id: If187d023dd48d9b575431759cc6b67460bc0d33b
This commit is contained in:
Remi NGUYEN VAN
2022-10-18 18:32:55 +09:00
parent e274170ac6
commit a066e55bc5
2 changed files with 382 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
/*
* 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 android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Looper;
import com.android.internal.annotations.VisibleForTesting;
import java.net.SocketAddress;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
/**
* Sends mDns announcements when a service registration changes and at regular intervals.
*
* This allows maintaining other hosts' caches up-to-date. See RFC6762 8.3.
*/
public class MdnsAnnouncer extends MdnsPacketRepeater<MdnsAnnouncer.AnnouncementInfo> {
private static final long ANNOUNCEMENT_INITIAL_DELAY_MS = 1000L;
@VisibleForTesting
static final int ANNOUNCEMENT_COUNT = 8;
@NonNull
private final String mLogTag;
static class AnnouncementInfo implements MdnsPacketRepeater.Request {
@NonNull
private final MdnsPacket mPacket;
@NonNull
private final Supplier<Iterable<SocketAddress>> mDestinationsSupplier;
AnnouncementInfo(List<MdnsRecord> announcedRecords, List<MdnsRecord> additionalRecords,
Supplier<Iterable<SocketAddress>> destinationsSupplier) {
// Records to announce (as answers)
// Records to place in the "Additional records", with NSEC negative responses
// to mark records that have been verified unique
final int flags = 0x8400; // Response, authoritative (rfc6762 18.4)
mPacket = new MdnsPacket(flags,
Collections.emptyList() /* questions */,
announcedRecords,
Collections.emptyList() /* authorityRecords */,
additionalRecords);
mDestinationsSupplier = destinationsSupplier;
}
@Override
public MdnsPacket getPacket(int index) {
return mPacket;
}
@Override
public Iterable<SocketAddress> getDestinations(int index) {
return mDestinationsSupplier.get();
}
@Override
public long getDelayMs(int nextIndex) {
// Delay is doubled for each announcement
return ANNOUNCEMENT_INITIAL_DELAY_MS << (nextIndex - 1);
}
@Override
public int getNumSends() {
return ANNOUNCEMENT_COUNT;
}
}
public MdnsAnnouncer(@NonNull String interfaceTag, @NonNull Looper looper,
@NonNull MdnsReplySender replySender,
@Nullable PacketRepeaterCallback<AnnouncementInfo> cb) {
super(looper, replySender, cb);
mLogTag = MdnsAnnouncer.class.getSimpleName() + "/" + interfaceTag;
}
@Override
protected String getTag() {
return mLogTag;
}
// TODO: Notify MdnsRecordRepository that the records were announced for that service ID,
// so it can update the last advertised timestamp of the associated records.
}

View File

@@ -0,0 +1,283 @@
/*
* 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 android.net.InetAddresses.parseNumericAddress
import android.os.Build
import android.os.HandlerThread
import android.os.SystemClock
import com.android.internal.util.HexDump
import com.android.server.connectivity.mdns.MdnsAnnouncer.AnnouncementInfo
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
import java.net.DatagramPacket
import java.net.Inet6Address
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.MulticastSocket
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mockito.any
import org.mockito.Mockito.atLeast
import org.mockito.Mockito.mock
import org.mockito.Mockito.timeout
import org.mockito.Mockito.verify
private const val FIRST_ANNOUNCES_DELAY = 100L
private const val FIRST_ANNOUNCES_COUNT = 2
private const val NEXT_ANNOUNCES_DELAY = 1L
private const val TEST_TIMEOUT_MS = 1000L
private val destinationsSupplier = {
listOf(InetSocketAddress(MdnsConstants.getMdnsIPv6Address(), MdnsConstants.MDNS_PORT)) }
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsAnnouncerTest {
private val thread = HandlerThread(MdnsAnnouncerTest::class.simpleName)
private val socket = mock(MulticastSocket::class.java)
private val buffer = ByteArray(1500)
@Before
fun setUp() {
thread.start()
}
@After
fun tearDown() {
thread.quitSafely()
}
private class TestAnnouncementInfo(
announcedRecords: List<MdnsRecord>,
additionalRecords: List<MdnsRecord>
)
: AnnouncementInfo(announcedRecords, additionalRecords, destinationsSupplier) {
override fun getDelayMs(nextIndex: Int) =
if (nextIndex < FIRST_ANNOUNCES_COUNT) {
FIRST_ANNOUNCES_DELAY
} else {
NEXT_ANNOUNCES_DELAY
}
}
@Test
fun testAnnounce() {
val replySender = MdnsReplySender(thread.looper, socket, buffer)
@Suppress("UNCHECKED_CAST")
val cb = mock(MdnsPacketRepeater.PacketRepeaterCallback::class.java)
as MdnsPacketRepeater.PacketRepeaterCallback<AnnouncementInfo>
val announcer = MdnsAnnouncer("testiface", thread.looper, replySender, cb)
/*
The expected packet replicates records announced when registering a service, as observed in
the legacy mDNS implementation (some ordering differs to be more readable).
Obtained with scapy 2.5.0 RC3 (2.4.5 does not compress TLDs like .arpa properly) with:
scapy.raw(scapy.dns_compress(scapy.DNS(rd=0, qr=1, aa=1,
qd = None,
an =
scapy.DNSRR(type='PTR', rrname='123.0.2.192.in-addr.arpa.', rdata='Android.local',
rclass=0x8001, ttl=120) /
scapy.DNSRR(type='PTR',
rrname='3.2.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.B.D.0.1.0.0.2.ip6.arpa',
rdata='Android.local', rclass=0x8001, ttl=120) /
scapy.DNSRR(type='PTR',
rrname='6.5.4.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.B.D.0.1.0.0.2.ip6.arpa',
rdata='Android.local', rclass=0x8001, ttl=120) /
scapy.DNSRR(type='PTR', rrname='_testtype._tcp.local',
rdata='testservice._testtype._tcp.local', rclass='IN', ttl=4500) /
scapy.DNSRRSRV(rrname='testservice._testtype._tcp.local', rclass=0x8001, port=31234,
target='Android.local', ttl=120) /
scapy.DNSRR(type='TXT', rrname='testservice._testtype._tcp.local', rclass=0x8001, rdata='',
ttl=4500) /
scapy.DNSRR(type='A', rrname='Android.local', rclass=0x8001, rdata='192.0.2.123', ttl=120) /
scapy.DNSRR(type='AAAA', rrname='Android.local', rclass=0x8001, rdata='2001:db8::123',
ttl=120) /
scapy.DNSRR(type='AAAA', rrname='Android.local', rclass=0x8001, rdata='2001:db8::456',
ttl=120),
ar =
scapy.DNSRRNSEC(rrname='123.0.2.192.in-addr.arpa.', rclass=0x8001, ttl=120,
nextname='123.0.2.192.in-addr.arpa.', typebitmaps=[12]) /
scapy.DNSRRNSEC(
rrname='3.2.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.B.D.0.1.0.0.2.ip6.arpa',
rclass=0x8001, ttl=120,
nextname='3.2.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.B.D.0.1.0.0.2.ip6.arpa',
typebitmaps=[12]) /
scapy.DNSRRNSEC(
rrname='6.5.4.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.B.D.0.1.0.0.2.ip6.arpa',
rclass=0x8001, ttl=120,
nextname='6.5.4.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.B.D.0.1.0.0.2.ip6.arpa',
typebitmaps=[12]) /
scapy.DNSRRNSEC(
rrname='testservice._testtype._tcp.local', rclass=0x8001, ttl=4500,
nextname='testservice._testtype._tcp.local', typebitmaps=[16, 33]) /
scapy.DNSRRNSEC(
rrname='Android.local', rclass=0x8001, ttl=120, nextname='Android.local',
typebitmaps=[1, 28]))
)).hex().upper()
*/
val expected = "00008400000000090000000503313233013001320331393207696E2D61646472046172706" +
"100000C800100000078000F07416E64726F6964056C6F63616C00013301320131013001300130013" +
"00130013001300130013001300130013001300130013001300130013001300130013001380142014" +
"40130013101300130013203697036C020000C8001000000780002C030013601350134C045000C800" +
"1000000780002C030095F7465737474797065045F746370C038000C000100001194000E0B7465737" +
"473657276696365C0A5C0C000218001000000780008000000007A02C030C0C000108001000011940" +
"000C03000018001000000780004C000027BC030001C800100000078001020010DB80000000000000" +
"00000000123C030001C800100000078001020010DB8000000000000000000000456C00C002F80010" +
"00000780006C00C00020008C03F002F8001000000780006C03F00020008C091002F8001000000780" +
"006C09100020008C0C0002F8001000011940009C0C000050000800040C030002F800100000078000" +
"8C030000440000008"
val hostname = arrayOf("Android", "local")
val serviceType = arrayOf("_testtype", "_tcp", "local")
val serviceName = arrayOf("testservice", "_testtype", "_tcp", "local")
val v4Addr = parseNumericAddress("192.0.2.123")
val v6Addr1 = parseNumericAddress("2001:DB8::123")
val v6Addr2 = parseNumericAddress("2001:DB8::456")
val v4AddrRev = arrayOf("123", "0", "2", "192", "in-addr", "arpa")
val v6Addr1Rev = getReverseV6AddressName(v6Addr1)
val v6Addr2Rev = getReverseV6AddressName(v6Addr2)
val announcedRecords = listOf(
// Reverse address records
MdnsPointerRecord(v4AddrRev,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
hostname),
MdnsPointerRecord(v6Addr1Rev,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
hostname),
MdnsPointerRecord(v6Addr2Rev,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
hostname),
// Service registration records (RFC6763)
MdnsPointerRecord(
serviceType,
0L /* receiptTimeMillis */,
// Not a unique name owned by the announcer, so cacheFlush=false
false /* cacheFlush */,
4500000L /* ttlMillis */,
serviceName),
MdnsServiceRecord(
serviceName,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
0 /* servicePriority */,
0 /* serviceWeight */,
31234 /* servicePort */,
hostname),
MdnsTextRecord(
serviceName,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
4500000L /* ttlMillis */,
emptyList() /* entries */),
// Address records for the hostname
MdnsInetAddressRecord(hostname,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
v4Addr),
MdnsInetAddressRecord(hostname,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
v6Addr1),
MdnsInetAddressRecord(hostname,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
v6Addr2))
// Negative responses (RFC6762 6.1)
val additionalRecords = listOf(
MdnsNsecRecord(v4AddrRev,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
v4AddrRev,
intArrayOf(MdnsRecord.TYPE_PTR)),
MdnsNsecRecord(v6Addr1Rev,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
v6Addr1Rev,
intArrayOf(MdnsRecord.TYPE_PTR)),
MdnsNsecRecord(v6Addr2Rev,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
v6Addr2Rev,
intArrayOf(MdnsRecord.TYPE_PTR)),
MdnsNsecRecord(serviceName,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
4500000L /* ttlMillis */,
serviceName,
intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
MdnsNsecRecord(hostname,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
hostname,
intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)))
val request = TestAnnouncementInfo(announcedRecords, additionalRecords)
val timeStart = SystemClock.elapsedRealtime()
val startDelay = 50L
val sendId = 1
announcer.startSending(sendId, request, startDelay)
val captor = ArgumentCaptor.forClass(DatagramPacket::class.java)
repeat(FIRST_ANNOUNCES_COUNT) { i ->
verify(cb, timeout(TEST_TIMEOUT_MS)).onSent(i, request)
verify(socket, atLeast(i + 1)).send(any())
val now = SystemClock.elapsedRealtime()
assertTrue(now > timeStart + startDelay + i * FIRST_ANNOUNCES_DELAY)
assertTrue(now < timeStart + startDelay + (i + 1) * FIRST_ANNOUNCES_DELAY)
}
// Subsequent announces should happen quickly (NEXT_ANNOUNCES_DELAY)
verify(socket, timeout(TEST_TIMEOUT_MS).times(MdnsAnnouncer.ANNOUNCEMENT_COUNT))
.send(captor.capture())
verify(cb, timeout(TEST_TIMEOUT_MS)).onFinished(request)
captor.allValues.forEach {
assertEquals(expected, HexDump.toHexString(it.data))
}
}
}
/**
* Compute 2001:db8::1 --> 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.B.D.1.0.0.2.ip6.arpa
*/
private fun getReverseV6AddressName(addr: InetAddress): Array<String> {
assertTrue(addr is Inet6Address)
return addr.address.flatMapTo(mutableListOf("arpa", "ip6")) {
HexDump.toHexString(it).toCharArray().map(Char::toString)
}.reversed().toTypedArray()
}