[Thread] add an integration test case for resolving a host via DNS recursive resolver

This CL adds new test cases for verifying the Thread Border Router's
DNS Recursive Resolver feature: The DNS clients resolves google.com
successfully when the feature is enabled.

Bug: 354868095
Test: atest ThreadNetworkIntegrationTests:android.net.thread.InternetAccessTest

Change-Id: I1b021151c6843b30c3f47559ab20dec405af5dea
This commit is contained in:
Handa Wang 2024-10-25 08:32:04 +00:00
commit 5e8a1fdfda
3 changed files with 370 additions and 0 deletions

View file

@ -0,0 +1,173 @@
/*
* Copyright (C) 2024 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 android.net.thread
import android.content.Context
import android.net.DnsResolver.CLASS_IN
import android.net.DnsResolver.TYPE_A
import android.net.DnsResolver.TYPE_AAAA
import android.net.InetAddresses.parseNumericAddress
import android.net.thread.utils.FullThreadDevice
import android.net.thread.utils.InfraNetworkDevice
import android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET
import android.net.thread.utils.IntegrationTestUtils.joinNetworkAndWaitForOmr
import android.net.thread.utils.IntegrationTestUtils.newPacketReader
import android.net.thread.utils.IntegrationTestUtils.setUpInfraNetwork
import android.net.thread.utils.IntegrationTestUtils.startInfraDeviceAndWaitForOnLinkAddr
import android.net.thread.utils.IntegrationTestUtils.tearDownInfraNetwork
import android.net.thread.utils.IntegrationTestUtils.waitFor
import android.net.thread.utils.OtDaemonController
import android.net.thread.utils.TestDnsServer
import android.net.thread.utils.ThreadFeatureCheckerRule
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresSimulationThreadDevice
import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature
import android.net.thread.utils.ThreadNetworkControllerWrapper
import android.os.Handler
import android.os.HandlerThread
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.android.net.module.util.DnsPacket
import com.android.net.module.util.DnsPacket.ANSECTION
import com.android.testutils.PollPacketReader
import com.android.testutils.TestNetworkTracker
import com.google.common.truth.Truth.assertThat
import java.net.Inet4Address
import java.net.InetAddress
import java.time.Duration
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/** Integration test cases for Thread Internet Access features. */
@RunWith(AndroidJUnit4::class)
@RequiresThreadFeature
@RequiresSimulationThreadDevice
@LargeTest
class InternetAccessTest {
private val TAG = BorderRoutingTest::class.java.simpleName
private val NUM_FTD = 1
private val DNS_SERVER_ADDR = parseNumericAddress("8.8.8.8") as Inet4Address
private val ANSWER_RECORDS =
listOf(
DnsPacket.DnsRecord.makeAOrAAAARecord(
ANSECTION,
"google.com",
CLASS_IN,
30 /* ttl */,
parseNumericAddress("1.2.3.4"),
),
DnsPacket.DnsRecord.makeAOrAAAARecord(
ANSECTION,
"google.com",
CLASS_IN,
30 /* ttl */,
parseNumericAddress("2001::234"),
),
)
@get:Rule val threadRule = ThreadFeatureCheckerRule()
private val context: Context = ApplicationProvider.getApplicationContext()
private val controller = requireNotNull(ThreadNetworkControllerWrapper.newInstance(context))
private lateinit var otCtl: OtDaemonController
private lateinit var handlerThread: HandlerThread
private lateinit var handler: Handler
private lateinit var infraNetworkTracker: TestNetworkTracker
private lateinit var ftds: ArrayList<FullThreadDevice>
private lateinit var infraNetworkReader: PollPacketReader
private lateinit var infraDevice: InfraNetworkDevice
private lateinit var dnsServer: TestDnsServer
@Before
@Throws(Exception::class)
fun setUp() {
// TODO: b/323301831 - This is a workaround to avoid unnecessary delay to re-form a network
otCtl = OtDaemonController()
otCtl.factoryReset()
handlerThread = HandlerThread(javaClass.simpleName)
handlerThread.start()
handler = Handler(handlerThread.looper)
ftds = ArrayList()
infraNetworkTracker = setUpInfraNetwork(context, controller)
controller.setEnabledAndWait(true)
controller.joinAndWait(DEFAULT_DATASET)
// Creates a infra network device.
infraNetworkReader = newPacketReader(infraNetworkTracker.testIface, handler)
infraDevice = startInfraDeviceAndWaitForOnLinkAddr(infraNetworkReader)
// Create a DNS server
dnsServer = TestDnsServer(infraNetworkReader, DNS_SERVER_ADDR, ANSWER_RECORDS)
// Create Ftds
for (i in 0 until NUM_FTD) {
ftds.add(FullThreadDevice(15 + i /* node ID */))
}
}
@After
@Throws(Exception::class)
fun tearDown() {
controller.setTestNetworkAsUpstreamAndWait(null)
controller.leaveAndWait()
tearDownInfraNetwork(infraNetworkTracker)
dnsServer.stop()
handlerThread.quitSafely()
handlerThread.join()
ftds.forEach { it.destroy() }
ftds.clear()
}
@Test
fun nat64Enabled_threadDeviceResolvesHost_hostIsResolved() {
controller.setNat64EnabledAndWait(true)
waitFor({ otCtl.hasNat64PrefixInNetdata() }, Duration.ofSeconds(10))
val ftd = ftds[0]
joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET)
dnsServer.start()
val ipv4Addresses =
ftd.resolveHost("google.com", TYPE_A).map { extractIpv4AddressFromMappedAddress(it) }
assertThat(ipv4Addresses).isEqualTo(listOf(parseNumericAddress("1.2.3.4")))
val ipv6Addresses = ftd.resolveHost("google.com", TYPE_AAAA)
assertThat(ipv6Addresses).isEqualTo(listOf(parseNumericAddress("2001::234")))
}
@Test
fun nat64Disabled_threadDeviceResolvesHost_hostIsNotResolved() {
controller.setNat64EnabledAndWait(false)
val ftd = ftds[0]
joinNetworkAndWaitForOmr(ftd, DEFAULT_DATASET)
dnsServer.start()
assertThat(ftd.resolveHost("google.com", TYPE_A)).isEmpty()
assertThat(ftd.resolveHost("google.com", TYPE_AAAA)).isEmpty()
}
private fun extractIpv4AddressFromMappedAddress(address: InetAddress): Inet4Address {
return InetAddress.getByAddress(address.address.slice(12 until 16).toByteArray())
as Inet4Address
}
}

View file

@ -15,6 +15,8 @@
*/
package android.net.thread.utils;
import static android.net.DnsResolver.TYPE_A;
import static android.net.DnsResolver.TYPE_AAAA;
import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
import static android.net.thread.utils.IntegrationTestUtils.waitFor;
@ -354,6 +356,31 @@ public final class FullThreadDevice {
executeCommand("dns config " + address);
}
/** Resolves the {@code queryType} record of the {@code hostname} via DNS. */
public List<InetAddress> resolveHost(String hostname, int queryType) {
// CLI output:
// DNS response for hostname.com. - fd12::abc1 TTL:50 fd12::abc2 TTL:50 fd12::abc3 TTL:50
String command;
switch (queryType) {
case TYPE_A -> command = "resolve4";
case TYPE_AAAA -> command = "resolve";
default -> throw new IllegalArgumentException("Invalid query type: " + queryType);
}
final List<InetAddress> addresses = new ArrayList<>();
String line;
try {
line = executeCommand("dns " + command + " " + hostname).get(0);
} catch (IllegalStateException e) {
return addresses;
}
final String[] addressTtlPairs = line.split("-")[1].strip().split(" ");
for (int i = 0; i < addressTtlPairs.length; i += 2) {
addresses.add(InetAddresses.parseNumericAddress(addressTtlPairs[i]));
}
return addresses;
}
/** Returns the first browsed service instance of {@code serviceType}. */
public NsdServiceInfo browseService(String serviceType) {
// CLI output:

View file

@ -0,0 +1,170 @@
/*
* Copyright (C) 2024 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 android.net.thread.utils
import android.net.thread.utils.IntegrationTestUtils.pollForPacket
import android.system.OsConstants.IPPROTO_IP
import android.system.OsConstants.IPPROTO_UDP
import com.android.net.module.util.DnsPacket
import com.android.net.module.util.PacketBuilder
import com.android.net.module.util.Struct
import com.android.net.module.util.structs.Ipv4Header
import com.android.net.module.util.structs.UdpHeader
import com.android.testutils.PollPacketReader
import java.net.InetAddress
import java.nio.ByteBuffer
import kotlin.concurrent.thread
/**
* A class that simulates a DNS server.
*
* <p>The server responds to DNS requests with the given {@code answerRecords}.
*
* @param packetReader the packet reader to poll DNS requests from
* @param serverAddress the address of the DNS server
* @param answerRecords the records to respond to the DNS requests
*/
class TestDnsServer(
private val packetReader: PollPacketReader,
private val serverAddress: InetAddress,
private val answerRecords: List<DnsPacket.DnsRecord>,
) {
private val TAG = TestDnsServer::class.java.simpleName
private val DNS_UDP_PORT = 53
private var workerThread: Thread? = null
private class TestDnsPacket : DnsPacket {
constructor(buf: ByteArray) : super(buf)
constructor(
header: DnsHeader,
qd: List<DnsRecord>,
an: List<DnsRecord>,
) : super(header, qd, an) {}
val header = super.mHeader
val records = super.mRecords
}
/**
* Starts the DNS server to respond to DNS requests.
*
* <p> The server polls the DNS requests from the {@code packetReader} and responds with the
* {@code answerRecords}. The server will automatically stop when it fails to poll a DNS request
* within the timeout (3000 ms, as defined in IntegrationTestUtils).
*/
fun start() {
workerThread = thread {
var requestPacket: ByteArray
while (true) {
requestPacket = pollForDnsPacket() ?: break
val buf = ByteBuffer.wrap(requestPacket)
packetReader.sendResponse(buildDnsResponse(buf, answerRecords))
}
}
}
/** Stops the DNS server. */
fun stop() {
workerThread?.join()
}
private fun pollForDnsPacket(): ByteArray? {
val filter =
fun(packet: ByteArray): Boolean {
val buf = ByteBuffer.wrap(packet)
val ipv4Header = Struct.parse(Ipv4Header::class.java, buf) ?: return false
val udpHeader = Struct.parse(UdpHeader::class.java, buf) ?: return false
return ipv4Header.dstIp == serverAddress && udpHeader.dstPort == DNS_UDP_PORT
}
return pollForPacket(packetReader, filter)
}
private fun buildDnsResponse(
requestPacket: ByteBuffer,
serverAnswers: List<DnsPacket.DnsRecord>,
): ByteBuffer? {
val requestIpv4Header = Struct.parse(Ipv4Header::class.java, requestPacket) ?: return null
val requestUdpHeader = Struct.parse(UdpHeader::class.java, requestPacket) ?: return null
val remainingRequestPacket = ByteArray(requestPacket.remaining())
requestPacket.get(remainingRequestPacket)
val requestDnsPacket = TestDnsPacket(remainingRequestPacket)
val requestDnsHeader = requestDnsPacket.header
val answerRecords =
buildDnsAnswerRecords(requestDnsPacket.records[DnsPacket.QDSECTION], serverAnswers)
// TODO: return NXDOMAIN if no answer is found.
val responseFlags = 1 shl 15 // QR bit
val responseDnsHeader =
DnsPacket.DnsHeader(
requestDnsHeader.id,
responseFlags,
requestDnsPacket.records[DnsPacket.QDSECTION].size,
answerRecords.size,
)
val responseDnsPacket =
TestDnsPacket(
responseDnsHeader,
requestDnsPacket.records[DnsPacket.QDSECTION],
answerRecords,
)
val buf =
PacketBuilder.allocate(
false /* hasEther */,
IPPROTO_IP,
IPPROTO_UDP,
responseDnsPacket.bytes.size,
)
val packetBuilder = PacketBuilder(buf)
packetBuilder.writeIpv4Header(
requestIpv4Header.tos,
requestIpv4Header.id,
requestIpv4Header.flagsAndFragmentOffset,
0x40 /* ttl */,
IPPROTO_UDP.toByte(),
requestIpv4Header.dstIp, /* srcIp */
requestIpv4Header.srcIp, /* dstIp */
)
packetBuilder.writeUdpHeader(
requestUdpHeader.dstPort.toShort() /* srcPort */,
requestUdpHeader.srcPort.toShort(), /* dstPort */
)
buf.put(responseDnsPacket.bytes)
return packetBuilder.finalizePacket()
}
private fun buildDnsAnswerRecords(
questions: List<DnsPacket.DnsRecord>,
serverAnswers: List<DnsPacket.DnsRecord>,
): List<DnsPacket.DnsRecord> {
val answers = ArrayList<DnsPacket.DnsRecord>()
for (answer in serverAnswers) {
if (
questions.any {
answer.dName.equals(it.dName, ignoreCase = true) && answer.nsType == it.nsType
}
) {
answers.add(answer)
}
}
return answers
}
}