diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp index 4b15b16980..f4799f0c50 100644 --- a/staticlibs/Android.bp +++ b/staticlibs/Android.bp @@ -98,6 +98,7 @@ java_library { ], static_libs: [ "androidx.test.ext.junit", + "libnanohttpd", "net-tests-utils-host-device-common", "net-utils-device-common", ], diff --git a/staticlibs/devicetests/com/android/testutils/ArpResponder.kt b/staticlibs/devicetests/com/android/testutils/ArpResponder.kt new file mode 100644 index 0000000000..86631c3d4b --- /dev/null +++ b/staticlibs/devicetests/com/android/testutils/ArpResponder.kt @@ -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, + 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) diff --git a/staticlibs/devicetests/com/android/testutils/PacketResponder.kt b/staticlibs/devicetests/com/android/testutils/PacketResponder.kt new file mode 100644 index 0000000000..daf29e4363 --- /dev/null +++ b/staticlibs/devicetests/com/android/testutils/PacketResponder.kt @@ -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, + 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 + } + } + } +} diff --git a/staticlibs/devicetests/com/android/testutils/TestHttpServer.kt b/staticlibs/devicetests/com/android/testutils/TestHttpServer.kt new file mode 100644 index 0000000000..7aae8e3b59 --- /dev/null +++ b/staticlibs/devicetests/com/android/testutils/TestHttpServer.kt @@ -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() + + /** + * A record of all requests received by the server since it was started. + */ + val requestsRecord = ArrayTrackRecord() + + /** + * 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) + } +} \ No newline at end of file diff --git a/staticlibs/hostdevice/com/android/testutils/PacketFilter.kt b/staticlibs/hostdevice/com/android/testutils/PacketFilter.kt index cd8d6a55c2..7c615d0b25 100644 --- a/staticlibs/hostdevice/com/android/testutils/PacketFilter.kt +++ b/staticlibs/hostdevice/com/android/testutils/PacketFilter.kt @@ -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 { 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 { + 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 { + 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 { 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) }