From 59f53ac46f5ea7f2bbd55e5704685970b149d7a1 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Fri, 11 Apr 2025 15:06:42 -0700 Subject: [PATCH] Add (very) basic Clat Test This test brings up a tap interface and starts clat on it by responding with an RA including a PREF64 option. Test: TH Flag: EXEMPT mainline Bug: 1000001 Change-Id: Idf8e58d56a322185c97495585ecddf2432f1c820 --- tests/integration/Android.bp | 1 + tests/integration/src/android/net/ClatTest.kt | 206 ++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 tests/integration/src/android/net/ClatTest.kt diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp index 6c3b7a0710..b9f67e1181 100644 --- a/tests/integration/Android.bp +++ b/tests/integration/Android.bp @@ -51,6 +51,7 @@ android_test { "kotlin-reflect", "mockito-target-extended-minus-junit4", "net-tests-utils", + "net-utils-networkstack", "service-connectivity-pre-jarjar", "service-connectivity-tiramisu-pre-jarjar", "services.net", diff --git a/tests/integration/src/android/net/ClatTest.kt b/tests/integration/src/android/net/ClatTest.kt new file mode 100644 index 0000000000..fb2fad8c5f --- /dev/null +++ b/tests/integration/src/android/net/ClatTest.kt @@ -0,0 +1,206 @@ +/* + * 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 android.net + +import android.Manifest.permission.CHANGE_NETWORK_STATE +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED +import android.net.NetworkCapabilities.TRANSPORT_ETHERNET +import android.net.NetworkCapabilities.TRANSPORT_TEST +import android.net.TestNetworkManager.TestInterfaceRequest +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import android.platform.test.annotations.AppModeFull +import android.system.Os +import android.system.OsConstants.AF_INET +import android.system.OsConstants.IPPROTO_UDP +import android.system.OsConstants.SOCK_DGRAM +import androidx.test.platform.app.InstrumentationRegistry +import com.android.net.module.util.ProcfsParsingUtils +import com.android.testutils.AutoCloseTestInterfaceRule +import com.android.testutils.DataPkt +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.EtherPkt +import com.android.testutils.EthernetTestInterface +import com.android.testutils.Ip6Pkt +import com.android.testutils.NdResponder +import com.android.testutils.RaPkt +import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged +import com.android.testutils.TestableNetworkCallback +import com.android.testutils.UdpPkt +import com.android.testutils.runAsShell +import com.google.common.truth.Truth.assertThat +import java.io.FileDescriptor +import java.net.Inet4Address +import java.net.Inet6Address +import java.net.InetAddress +import java.net.InetSocketAddress +import java.nio.ByteBuffer +import kotlin.test.fail +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +private const val TAG = "ClatTest" +private const val TIMEOUT_MS = 2000L + +private val REQUEST: NetworkRequest = NetworkRequest.Builder() + .addTransportType(TRANSPORT_ETHERNET) + .addTransportType(TRANSPORT_TEST) + .removeCapability(NET_CAPABILITY_INTERNET) + .removeCapability(NET_CAPABILITY_TRUSTED) + .build() +private val ROUTER_MAC = MacAddress.fromString("01:02:03:04:05:06") +private val ROUTER_V6 = InetAddress.getByName("fe80::0102:03ff:fe04:0506") as Inet6Address + +// For ByteArray.toHexString +@kotlin.ExperimentalStdlibApi +@AppModeFull(reason = "Instant apps can't access EthernetManager") +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2) +class ClatTest { + private val context = InstrumentationRegistry.getInstrumentation().context + private val cm = context.getSystemService(ConnectivityManager::class.java)!! + private val handlerThread = HandlerThread("$TAG thread").apply { start() } + private val handler = Handler(handlerThread.looper) + private val registeredCallbacks = ArrayList() + + // Cannot be initialized before setUp as eventuallyExpect() can fail. + private lateinit var lp: LinkProperties + private lateinit var network: Network + private var socket: FileDescriptor? = null + + @get:Rule + val testInterfaceRule = AutoCloseTestInterfaceRule(context) + + private val iface: EthernetTestInterface + init { + val req = TestInterfaceRequest.Builder().setTap().build() + val tap = testInterfaceRule.createTestInterface(req) + iface = EthernetTestInterface(context, handler, tap) + } + + private val localMac = iface.testIface.macAddress!! + + // Available after provisioning. + private lateinit var clatV6Addr: Inet6Address + + private val ndResponder = NdResponder(iface.packetReader).apply { + val ra = RaPkt() + .addPioOption(prefix = prefix.toString(), flags = "LA") + .addRdnssOption(dns = "2001:4860:4860::8888,2001:4860:4860::8844") + .addPref64Option(prefix = "64:ff9b::/96") + addRouterEntry(ROUTER_MAC, ROUTER_V6, ra) + start() + } + + private fun requestNetwork(request: NetworkRequest): TestableNetworkCallback { + val cb = TestableNetworkCallback() + runAsShell(CHANGE_NETWORK_STATE) { + cm.requestNetwork(request, cb, handler) + registeredCallbacks.add(cb) + } + return cb + } + + @Before + fun setUp() { + val cb = requestNetwork(REQUEST) + // b/233534110: eventuallyExpect() does not advance ReadHead, use + // eventuallyExpect(LinkProperties::class) instead. + cb.eventuallyExpect(LinkPropertiesChanged::class) + val linkPropertiesChanged = cb.eventuallyExpect(LinkPropertiesChanged::class) + network = linkPropertiesChanged.network + lp = linkPropertiesChanged.lp + assertThat(lp.getStackedLinks()).isNotEmpty() + + clatV6Addr = ProcfsParsingUtils.getAnycast6Addresses(iface.name).get(0) + } + + @After + fun tearDown() { + if (socket != null) { + Os.close(socket) + } + for (cb in registeredCallbacks) { + cm.unregisterNetworkCallback(cb) + } + // TODO: AutoCloseTestInterfaceRule should destroy associated EthernetTestInterface. + iface.destroy() + handlerThread.quitSafely() + handlerThread.join() + } + + fun LinkProperties.getInet4Address(): Inet4Address { + val las = getAllLinkAddresses() + for (la in las) { + if (la.isIpv4()) return la.address as Inet4Address + } + // If the v4- interface is present, this cannot happen. + fail("LinkProperties did not include an IPv4 address") + } + + fun expectPacket(expectedPacket: ByteArray) { + val p = iface.packetReader.poll(TIMEOUT_MS) { + it.contentEquals(expectedPacket) + } + assertThat(p).isNotNull() + } + + @Test + fun testClatEgress() { + socket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) + network.bindSocket(socket) + Os.connect(socket, InetAddress.getByName("1.2.3.4"), 12345) + val sockaddr = Os.getsockname(socket) as InetSocketAddress + val localPort = sockaddr.port + val buf = ByteBuffer.wrap("test data".toByteArray()) + Os.write(socket, buf) + + val ether = EtherPkt(dst = ROUTER_MAC, src = localMac) + val ipv6 = Ip6Pkt(src = clatV6Addr.hostAddress!!, dst = "64:ff9b::1.2.3.4", hlim = 64) + val udp = UdpPkt(srcPort = localPort, dstPort = 12345) + val payload = DataPkt(buf.array()) + val pkt = ether / ipv6 / udp / payload + expectPacket(pkt.build()) + } + + @Test + fun testClatIngress() { + socket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) + network.bindSocket(socket) + Os.bind(socket, lp.getInet4Address(), 0) + val sockaddr = Os.getsockname(socket) as InetSocketAddress + val localPort = sockaddr.port + + val ether = EtherPkt(dst = localMac, src = ROUTER_MAC) + val ipv6 = Ip6Pkt(src = "64:ff9b::1.2.3.4", dst = clatV6Addr.hostAddress!!) + val udp = UdpPkt(srcPort = 12345, dstPort = localPort) + val data = "more test data" + val payload = DataPkt(data) + val pkt = ether / ipv6 / udp / payload + iface.packetReader.sendResponse(ByteBuffer.wrap(pkt.build())) + + val buf = ByteBuffer.allocate(data.length) + Os.read(socket, buf) + assertThat(buf.array()).isEqualTo(data.toByteArray()) + } +}