Merge changes from topic "metrics_cts_tests"
* changes: Add utilities to test tap interface networks Add PacketFilter utilities for DNS testing
This commit is contained in:
commit
edb36498eb
5 changed files with 250 additions and 3 deletions
|
|
@ -98,6 +98,7 @@ java_library {
|
|||
],
|
||||
static_libs: [
|
||||
"androidx.test.ext.junit",
|
||||
"libnanohttpd",
|
||||
"net-tests-utils-host-device-common",
|
||||
"net-utils-device-common",
|
||||
],
|
||||
|
|
|
|||
60
staticlibs/devicetests/com/android/testutils/ArpResponder.kt
Normal file
60
staticlibs/devicetests/com/android/testutils/ArpResponder.kt
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (C) 2020 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.testutils
|
||||
|
||||
import android.net.MacAddress
|
||||
import java.net.Inet4Address
|
||||
import java.net.InetAddress
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
private val TYPE_ARP = byteArrayOf(0x08, 0x06)
|
||||
// Arp reply header for IPv4 over ethernet
|
||||
private val ARP_REPLY_IPV4 = byteArrayOf(0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x02)
|
||||
|
||||
/**
|
||||
* A class that can be used to reply to ARP packets on a [TapPacketReader].
|
||||
*/
|
||||
class ArpResponder(
|
||||
reader: TapPacketReader,
|
||||
table: Map<Inet4Address, MacAddress>,
|
||||
name: String = ArpResponder::class.java.simpleName
|
||||
) : PacketResponder(reader, ArpRequestFilter(), name) {
|
||||
// Copy the map if not already immutable (toMap) to make sure it is not modified
|
||||
private val table = table.toMap()
|
||||
|
||||
override fun replyToPacket(packet: ByteArray, reader: TapPacketReader) {
|
||||
val targetIp = InetAddress.getByAddress(
|
||||
packet.copyFromIndexWithLength(ARP_TARGET_IPADDR_OFFSET, 4))
|
||||
as Inet4Address
|
||||
|
||||
val macAddr = table[targetIp]?.toByteArray() ?: return
|
||||
val senderMac = packet.copyFromIndexWithLength(ARP_SENDER_MAC_OFFSET, 6)
|
||||
reader.sendResponse(ByteBuffer.wrap(
|
||||
// Ethernet header
|
||||
senderMac + macAddr + TYPE_ARP +
|
||||
// ARP message
|
||||
ARP_REPLY_IPV4 +
|
||||
macAddr /* sender MAC */ +
|
||||
targetIp.address /* sender IP addr */ +
|
||||
macAddr /* target mac */ +
|
||||
targetIp.address /* target IP addr */
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private fun ByteArray.copyFromIndexWithLength(start: Int, len: Int) =
|
||||
copyOfRange(start, start + len)
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright (C) 2020 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.testutils
|
||||
|
||||
import java.util.function.Predicate
|
||||
|
||||
private const val POLL_FREQUENCY_MS = 1000L
|
||||
|
||||
/**
|
||||
* A class that can be used to reply to packets from a [TapPacketReader].
|
||||
*
|
||||
* A reply thread will be created to reply to incoming packets asynchronously.
|
||||
* The receiver creates a new read head on the [TapPacketReader], to read packets, so it does not
|
||||
* affect packets obtained through [TapPacketReader.popPacket].
|
||||
*
|
||||
* @param reader a [TapPacketReader] to obtain incoming packets and reply to them.
|
||||
* @param packetFilter A filter to apply to incoming packets.
|
||||
* @param name Name to use for the internal responder thread.
|
||||
*/
|
||||
abstract class PacketResponder(
|
||||
private val reader: TapPacketReader,
|
||||
private val packetFilter: Predicate<ByteArray>,
|
||||
name: String
|
||||
) {
|
||||
private val replyThread = ReplyThread(name)
|
||||
|
||||
protected abstract fun replyToPacket(packet: ByteArray, reader: TapPacketReader)
|
||||
|
||||
/**
|
||||
* Start the [PacketResponder].
|
||||
*/
|
||||
fun start() {
|
||||
replyThread.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the [PacketResponder].
|
||||
*
|
||||
* The responder cannot be used anymore after being stopped.
|
||||
*/
|
||||
fun stop() {
|
||||
replyThread.interrupt()
|
||||
}
|
||||
|
||||
private inner class ReplyThread(name: String) : Thread(name) {
|
||||
override fun run() {
|
||||
try {
|
||||
// Create a new ReadHead so other packets polled on the reader are not affected
|
||||
val recvPackets = reader.receivedPackets.newReadHead()
|
||||
while (!isInterrupted) {
|
||||
recvPackets.poll(POLL_FREQUENCY_MS, packetFilter::test)?.let {
|
||||
replyToPacket(it, reader)
|
||||
}
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
// Exit gracefully
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright (C) 2020 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.testutils
|
||||
|
||||
import android.net.Uri
|
||||
import com.android.net.module.util.ArrayTrackRecord
|
||||
import fi.iki.elonen.NanoHTTPD
|
||||
|
||||
/**
|
||||
* A minimal HTTP server running on a random available port.
|
||||
*
|
||||
* @param host The host to listen to, or null to listen on all hosts
|
||||
*/
|
||||
class TestHttpServer(host: String? = null) : NanoHTTPD(host, 0 /* auto-select the port */) {
|
||||
// Map of URL path -> HTTP response code
|
||||
private val responses = HashMap<Request, Response>()
|
||||
|
||||
/**
|
||||
* A record of all requests received by the server since it was started.
|
||||
*/
|
||||
val requestsRecord = ArrayTrackRecord<Request>()
|
||||
|
||||
/**
|
||||
* A request received by the test server.
|
||||
*/
|
||||
data class Request(
|
||||
val path: String,
|
||||
val method: Method = Method.GET,
|
||||
val queryParameters: String = ""
|
||||
) {
|
||||
/**
|
||||
* Returns whether the specified [Uri] matches parameters of this request.
|
||||
*/
|
||||
fun matches(uri: Uri) = (uri.path ?: "") == path && (uri.query ?: "") == queryParameters
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a response for GET requests with the path and query parameters of the specified [Uri].
|
||||
*/
|
||||
fun addResponse(
|
||||
uri: Uri,
|
||||
statusCode: Response.IStatus,
|
||||
locationHeader: String? = null,
|
||||
content: String = ""
|
||||
) {
|
||||
addResponse(Request(uri.path
|
||||
?: "", Method.GET, uri.query ?: ""),
|
||||
statusCode, locationHeader, content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a response for the given request.
|
||||
*/
|
||||
fun addResponse(
|
||||
request: Request,
|
||||
statusCode: Response.IStatus,
|
||||
locationHeader: String? = null,
|
||||
content: String = ""
|
||||
) {
|
||||
val response = newFixedLengthResponse(statusCode, "text/plain", content)
|
||||
locationHeader?.let { response.addHeader("Location", it) }
|
||||
responses[request] = response
|
||||
}
|
||||
|
||||
override fun serve(session: IHTTPSession): Response {
|
||||
val request = Request(session.uri
|
||||
?: "", session.method, session.queryParameterString ?: "")
|
||||
requestsRecord.add(request)
|
||||
// Default response is a 404
|
||||
return responses[request] ?: super.serve(session)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,19 +16,28 @@
|
|||
|
||||
package com.android.testutils
|
||||
|
||||
import java.net.Inet4Address
|
||||
import java.util.function.Predicate
|
||||
|
||||
const val ETHER_TYPE_OFFSET = 12
|
||||
const val ETHER_HEADER_LENGTH = 14
|
||||
const val IPV4_PROTOCOL_OFFSET = ETHER_HEADER_LENGTH + 9
|
||||
const val IPV4_CHECKSUM_OFFSET = ETHER_HEADER_LENGTH + 10
|
||||
const val IPV4_DST_OFFSET = ETHER_HEADER_LENGTH + 16
|
||||
const val IPV4_HEADER_LENGTH = 20
|
||||
const val IPV4_UDP_OFFSET = ETHER_HEADER_LENGTH + IPV4_HEADER_LENGTH
|
||||
const val BOOTP_OFFSET = IPV4_UDP_OFFSET + 8
|
||||
const val IPV4_UDP_SRCPORT_OFFSET = IPV4_UDP_OFFSET
|
||||
const val IPV4_UDP_DSTPORT_OFFSET = IPV4_UDP_OFFSET + 2
|
||||
const val UDP_HEADER_LENGTH = 8
|
||||
const val BOOTP_OFFSET = IPV4_UDP_OFFSET + UDP_HEADER_LENGTH
|
||||
const val BOOTP_TID_OFFSET = BOOTP_OFFSET + 4
|
||||
const val BOOTP_CLIENT_MAC_OFFSET = BOOTP_OFFSET + 28
|
||||
const val DHCP_OPTIONS_OFFSET = BOOTP_OFFSET + 240
|
||||
|
||||
const val ARP_OPCODE_OFFSET = ETHER_HEADER_LENGTH + 6
|
||||
const val ARP_SENDER_MAC_OFFSET = ETHER_HEADER_LENGTH + 8
|
||||
const val ARP_TARGET_IPADDR_OFFSET = ETHER_HEADER_LENGTH + 24
|
||||
|
||||
/**
|
||||
* A [Predicate] that matches a [ByteArray] if it contains the specified [bytes] at the specified
|
||||
* [offset].
|
||||
|
|
@ -47,13 +56,30 @@ class IPv4UdpFilter : Predicate<ByteArray> {
|
|||
override fun test(t: ByteArray) = impl.test(t)
|
||||
}
|
||||
|
||||
/**
|
||||
* A [Predicate] that matches ethernet-encapped packets sent to the specified IPv4 destination.
|
||||
*/
|
||||
class IPv4DstFilter(dst: Inet4Address) : Predicate<ByteArray> {
|
||||
private val impl = OffsetFilter(IPV4_DST_OFFSET, *dst.address)
|
||||
override fun test(t: ByteArray) = impl.test(t)
|
||||
}
|
||||
|
||||
/**
|
||||
* A [Predicate] that matches ethernet-encapped ARP requests.
|
||||
*/
|
||||
class ArpRequestFilter : Predicate<ByteArray> {
|
||||
private val impl = OffsetFilter(ETHER_TYPE_OFFSET, 0x08, 0x06 /* ARP */)
|
||||
.and(OffsetFilter(ARP_OPCODE_OFFSET, 0x00, 0x01 /* request */))
|
||||
override fun test(t: ByteArray) = impl.test(t)
|
||||
}
|
||||
|
||||
/**
|
||||
* A [Predicate] that matches ethernet-encapped DHCP packets sent from a DHCP client.
|
||||
*/
|
||||
class DhcpClientPacketFilter : Predicate<ByteArray> {
|
||||
private val impl = IPv4UdpFilter()
|
||||
.and(OffsetFilter(IPV4_UDP_OFFSET /* source port */, 0x00, 0x44 /* 68 */))
|
||||
.and(OffsetFilter(IPV4_UDP_OFFSET + 2 /* dest port */, 0x00, 0x43 /* 67 */))
|
||||
.and(OffsetFilter(IPV4_UDP_SRCPORT_OFFSET, 0x00, 0x44 /* 68 */))
|
||||
.and(OffsetFilter(IPV4_UDP_DSTPORT_OFFSET, 0x00, 0x43 /* 67 */))
|
||||
override fun test(t: ByteArray) = impl.test(t)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue