Merge changes If187d023,I41c557d6

* changes:
  Add MdnsAnnouncer
  Also use other compressed names in DNS compression
This commit is contained in:
Treehugger Robot
2022-12-15 04:31:37 +00:00
committed by Gerrit Code Review
5 changed files with 462 additions and 23 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

@@ -192,22 +192,31 @@ public class MdnsPacketWriter {
}
}
final int[] offsets;
if (suffixLength > 0) {
for (int i = 0; i < (labels.length - suffixLength); ++i) {
writeString(labels[i]);
}
offsets = writePartialLabelsNoCompression(labels, labels.length - suffixLength);
writePointer(suffixPointer);
} else {
int[] offsets = writeLabelsNoCompression(labels);
offsets = writeLabelsNoCompression(labels);
}
// Add entries to the label dictionary for each suffix of the label list, including
// the whole list itself.
for (int i = 0, len = labels.length; i < labels.length; ++i, --len) {
// Do not replace the last suffixLength suffixes that already have dictionary entries.
for (int i = 0, len = labels.length; i < labels.length - suffixLength; ++i, --len) {
String[] value = new String[len];
System.arraycopy(labels, i, value, 0, len);
labelDictionary.put(offsets[i], value);
}
}
private int[] writePartialLabelsNoCompression(String[] labels, int count) throws IOException {
int[] offsets = new int[count];
for (int i = 0; i < count; ++i) {
offsets[i] = getWritePosition();
writeString(labels[i]);
}
return offsets;
}
/**
@@ -216,11 +225,7 @@ public class MdnsPacketWriter {
* @return The offsets where each label was written to.
*/
public int[] writeLabelsNoCompression(String[] labels) throws IOException {
int[] offsets = new int[labels.length];
for (int i = 0; i < labels.length; ++i) {
offsets[i] = getWritePosition();
writeString(labels[i]);
}
final int[] offsets = writePartialLabelsNoCompression(labels, labels.length);
writeUInt8(0); // NUL terminator
return offsets;
}

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

View File

@@ -0,0 +1,55 @@
/*
* 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
import android.os.Build
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
import java.net.InetSocketAddress
import kotlin.test.assertContentEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.S_V2)
class MdnsPacketWriterTest {
@Test
fun testNameCompression() {
val writer = MdnsPacketWriter(ByteArray(1000))
writer.writeLabels(arrayOf("my", "first", "name"))
writer.writeLabels(arrayOf("my", "second", "name"))
writer.writeLabels(arrayOf("other", "first", "name"))
writer.writeLabels(arrayOf("my", "second", "name"))
writer.writeLabels(arrayOf("unrelated"))
val packet = writer.getPacket(
InetSocketAddress(InetAddresses.parseNumericAddress("2001:db8::123"), 123))
// Each label takes length + 1. So "first.name" offset = 3, "name" offset = 9
val expected = "my".label() + "first".label() + "name".label() + 0x00.toByte() +
// "my.second.name" offset = 15
"my".label() + "second".label() + byteArrayOf(0xC0.toByte(), 9) +
"other".label() + byteArrayOf(0xC0.toByte(), 3) +
byteArrayOf(0xC0.toByte(), 15) +
"unrelated".label() + 0x00.toByte()
assertContentEquals(expected, packet.data.copyOfRange(0, packet.length))
}
}
private fun String.label() = byteArrayOf(length.toByte()) + encodeToByteArray()

View File

@@ -160,14 +160,11 @@ class MdnsProberTest {
scapy.DNSRR(type='TXT', ttl=120, rrname='testservice._nmt._tcp.local.',
rdata='testKey=testValue'))
)).hex().upper()
// NOTE: due to a bug the second "myhostname" is not getting DNS compressed in the current
// actual probe, so data below is slightly different. Fix compression so it gets compressed.
*/
val expected = "0000000000020000000300000B7465737473657276696365045F6E6D74045F746370056C6" +
"F63616C0000FF00010C746573747365727669636532C01800FF0001C00C002100010000007800130" +
"000000094020A6D79686F73746E616D65C0220C746573747365727669636532C0180021000100000" +
"07800130000000094030A6D79686F73746E616D65C022C00C0010000100000078001211746573744" +
"B65793D7465737456616C7565"
"000000094020A6D79686F73746E616D65C022C02D00210001000000780008000000009403C052C00" +
"C0010000100000078001211746573744B65793D7465737456616C7565"
assertProbesSent(probeInfo, expected)
}