[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:
parent
40311a2263
commit
5e8a1fdfda
3 changed files with 370 additions and 0 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue