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,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()
}