From c3af22facad93a2d85559b7e1afa9dd49786d384 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Fri, 28 Mar 2025 19:50:46 -0700 Subject: [PATCH 1/3] Introduce generic packet builder utilities for use in tests This change introduces a set of generic packet builder utility classes that can be used in tests. The API surface aims to be similar to scapy with a focus on ease of use. Test: TH Flag: EXEMPT mainline Bug: 1 Change-Id: I9c751cece7bbad6bc38953cb52e9ab42393bab89 --- .../android/testutils/PacketBuilderUtils.kt | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 staticlibs/testutils/devicetests/com/android/testutils/PacketBuilderUtils.kt diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketBuilderUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketBuilderUtils.kt new file mode 100644 index 0000000000..688c49eba5 --- /dev/null +++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketBuilderUtils.kt @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2025 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.IpPrefix +import android.net.MacAddress +import java.io.ByteArrayOutputStream +import java.net.Inet6Address +import java.net.InetAddress +import java.nio.ByteBuffer +import java.util.EnumSet + +interface Packet { + fun toByteArray(): ByteArray +} + +interface L3Packet : Packet { + val etherType: Short +} + +interface L4Packet : Packet { + val proto: Byte + val size: Short +} + +private interface Flags { + val value: Int +} + +private fun EnumSet.toByte(): Byte where E : Enum, E : Flags { + val result = this.fold(0) { result, next -> result or next.value } + return result.toByte() +} + +private inline fun enumSetOfFlags(str: String): EnumSet + where E : Enum, E : Flags { + val result = EnumSet.noneOf(E::class.java) + for (char in str) { + result.add(enumValueOf(char.uppercase())) + } + return result +} + +/** + * Class that facilitates the creation of RA packets. + * + * Default argument values reflect the rfc4861 defaults where applicable. + */ +class RaPkt( + private val routerLft: Short, + private val reachableTime: Int, + private val retransTimer: Int, + private val flags: EnumSet, +) : L4Packet { + constructor( + routerLft: Short = 1800, + reachableTime: Int = 0, + retransTimer: Int = 0, + flags: String = "", + ) : this(routerLft, reachableTime, retransTimer, enumSetOfFlags(flags)) + + enum class RaFlags(override val value: Int) : Flags { + M(0x80), + O(0x40), + } + + override val proto: Byte = 58 + override val size: Short + get() = outputStream.size().toShort() + private val outputStream = ByteArrayOutputStream() + + init { + val raHeader = ByteBuffer.allocate(16) + raHeader.put(134.toByte()) // Type = 134 (Router Advertisement) + raHeader.put(0) // Code = 0 + raHeader.putShort(0) // Checksum = 0 (ignored) + raHeader.put(255.toByte()) // Cur Hop Limit + raHeader.put(flags.toByte()) + raHeader.putShort(routerLft) + raHeader.putInt(reachableTime) + raHeader.putInt(retransTimer) + raHeader.flip() + outputStream.write(raHeader.array()) + } + + enum class PioFlags(override val value: Int) : Flags { + L(0x80), + A(0x40), + R(0x20), + P(0x10), + } + + fun addPioOption( + prefix: IpPrefix, + validLft: Int, + preferredLft: Int, + flags: EnumSet, + ) { + if (!prefix.isIPv6()) throw IllegalArgumentException("Invalid prefix") + val pio = ByteBuffer.allocate(32) + pio.put(3) // Type = 3 + pio.put(4) // Length = 4 (*8) + pio.put(prefix.getPrefixLength().toByte()) // Prefix Length + pio.put(flags.toByte()) // Flags + pio.putInt(validLft) // Valid Lifetime + pio.putInt(preferredLft) // Preferred Lifetime + pio.putInt(0) // Reserved2 + pio.put(prefix.getRawAddress()) + pio.flip() + outputStream.write(pio.array()) + } + + fun addPioOption( + prefix: String, + validLft: Int = 2592000, + preferredLft: Int = 604800, + flags: String = "L", + ) { + addPioOption(IpPrefix(prefix), validLft, preferredLft, enumSetOfFlags(flags)) + } + + override fun toByteArray() = outputStream.toByteArray() +} + +/** Class that facilitates the creation of IPv6 packets. */ +class Ip6Pkt( + private val src: Inet6Address, + private val dst: Inet6Address, + private val payload: L4Packet, +) : L3Packet { + constructor(src: String, dst: String, payload: L4Packet) : this( + InetAddress.getByName(src) as Inet6Address, + InetAddress.getByName(dst) as Inet6Address, + payload, + ) {} + + override val etherType = 0x86dd.toShort() + + private val tc = 0 + private val flowlabel = 0 + private val hlim = 255.toByte() + + private val bytes: ByteArray + + init { + val ipv6Header = ByteBuffer.allocate(40) + ipv6Header.putInt((6 shl 28) or (tc shl 20) or flowlabel) + ipv6Header.putShort(payload.size) + ipv6Header.put(payload.proto) + ipv6Header.put(hlim) + ipv6Header.put(src.getAddress()) + ipv6Header.put(dst.getAddress()) + ipv6Header.flip() + + val outputStream = ByteArrayOutputStream() + outputStream.write(ipv6Header.array()) + outputStream.write(payload.toByteArray()) + bytes = outputStream.toByteArray() + } + + override fun toByteArray() = bytes +} + +/** + * Class that facilitates the creation of ethernet packets. + * + * Example code: + * + *
+ * {@code
+ * val ra = RaPkt(routerLft = 50, reachableTime = 100, flags = "O")
+ * ra.addPioOption(prefix = "2001:db8::1/64", flags = "LA")
+ * val ipv6 = Ip6Pkt(src = "fe80::1", dst = "fe80::2", payload = ra)
+ * val ether = EtherPkt(src = "1:2:3:4:5:6", dst = "1:1:1:1:1:1", payload = ipv6)
+ * }
+ * 
+ **/ +class EtherPkt( + dst: MacAddress, + src: MacAddress, + payload: L3Packet, + ) : Packet { + constructor(dst: String, src: String, payload: L3Packet) : + this(MacAddress.fromString(dst), MacAddress.fromString(src), payload) {} + + val bytes: ByteArray + + init { + val ethernetHeader = ByteBuffer.allocate(14) + ethernetHeader.put(dst.toByteArray()) + ethernetHeader.put(src.toByteArray()) + ethernetHeader.putShort(payload.etherType) + ethernetHeader.flip() + + val outputStream = ByteArrayOutputStream() + outputStream.write(ethernetHeader.array()) + outputStream.write(payload.toByteArray()) + bytes = outputStream.toByteArray() + } + + override fun toByteArray() = bytes +} From 62154f61be79e135b61f7ea441ba21a1a692f116 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Tue, 1 Apr 2025 14:45:46 -0700 Subject: [PATCH 2/3] Remove unnecessary {} Test: TH Flag: EXEMPT mainline Bug: 100 Change-Id: I4ca9f92ce6535cf6f07aa74a94ee58ced541e73f --- .../devicetests/com/android/testutils/PacketBuilderUtils.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketBuilderUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketBuilderUtils.kt index 688c49eba5..d40d3a142d 100644 --- a/staticlibs/testutils/devicetests/com/android/testutils/PacketBuilderUtils.kt +++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketBuilderUtils.kt @@ -146,7 +146,7 @@ class Ip6Pkt( InetAddress.getByName(src) as Inet6Address, InetAddress.getByName(dst) as Inet6Address, payload, - ) {} + ) override val etherType = 0x86dd.toShort() @@ -195,7 +195,7 @@ class EtherPkt( payload: L3Packet, ) : Packet { constructor(dst: String, src: String, payload: L3Packet) : - this(MacAddress.fromString(dst), MacAddress.fromString(src), payload) {} + this(MacAddress.fromString(dst), MacAddress.fromString(src), payload) val bytes: ByteArray From 6269bbdedbf0fda3e29298a55ae696a14d49fda8 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Tue, 1 Apr 2025 16:26:30 -0700 Subject: [PATCH 3/3] Add packet builder support for more RA options Test: Built packet and parsed it in packetor Flag: EXEMPT mainline Bug: 1000001 Change-Id: I69d3451cbdd7f229f914ee5d0e4dd2c7a5178f0a --- .../android/testutils/PacketBuilderUtils.kt | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/staticlibs/testutils/devicetests/com/android/testutils/PacketBuilderUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/PacketBuilderUtils.kt index d40d3a142d..b6ef68bb0d 100644 --- a/staticlibs/testutils/devicetests/com/android/testutils/PacketBuilderUtils.kt +++ b/staticlibs/testutils/devicetests/com/android/testutils/PacketBuilderUtils.kt @@ -133,6 +133,66 @@ class RaPkt( addPioOption(IpPrefix(prefix), validLft, preferredLft, enumSetOfFlags(flags)) } + fun addRdnssOption(dns: List, lft: Int) { + val length = 1 + (dns.size * 2) + val rdnss = ByteBuffer.allocate(length * 8) // length is in units of 8 octets. + rdnss.put(25) // Type = 25 + rdnss.put(length.toByte()) // Length = 1 + number of dns servers * 2 in units of 8 octets. + rdnss.putShort(0) // Reserved + rdnss.putInt(lft) + for (dnsServer in dns) { + rdnss.put(dnsServer.address) + } + rdnss.flip() + outputStream.write(rdnss.array()) + } + + fun addRdnssOption(dns: String, lft: Int = 1800) { + val dnsServers = dns.split(",").map { InetAddress.getByName(it) as Inet6Address } + addRdnssOption(dnsServers, lft) + } + + fun addRioOption(prefix: IpPrefix, lft: Int) { + val rio = ByteBuffer.allocate(24) + rio.put(24) // Type = 24 + rio.put(3) // Length = 3 -- TODO: support variable prefix length + rio.put(prefix.prefixLength.toByte()) + // TODO: support setting pref + rio.put(0) // res (3 bits) | pref (2 bits) | res (3 bits) + rio.putInt(lft) + rio.put(prefix.rawAddress) + rio.flip() + outputStream.write(rio.array()) + } + + fun addRioOption(prefix: String, lft: Int = 1800) { + addRioOption(IpPrefix(prefix), lft) + } + + fun addPref64Option(prefix: IpPrefix, lft: Int) { + val pref64 = ByteBuffer.allocate(16) + pref64.put(38) // Type = 38 + pref64.put(2) // Length = 2 (*8) + val scaledLifetime = lft and 0xfff8 + val plc = when (prefix.prefixLength) { + 96 -> 0 + 64 -> 1 + 56 -> 2 + 48 -> 3 + 40 -> 4 + 32 -> 5 + else -> throw IllegalArgumentException("Invalid pref64 prefix length") + } + pref64.putShort((scaledLifetime or plc).toShort()) // Scaled lft (13 bits) | plc (3 bits) + pref64.put(prefix.rawAddress, 0 /*offset*/, 12 /*length*/) // highest 96 bits of prefix. + pref64.flip() + outputStream.write(pref64.array()) + } + + fun addPref64Option(prefix: String, lft: Int = 1800) { + addPref64Option(IpPrefix(prefix), lft) + } + override fun toByteArray() = outputStream.toByteArray() }