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
This commit is contained in:
Patrick Rohr 2025-04-11 15:06:42 -07:00
commit 59f53ac46f
2 changed files with 207 additions and 0 deletions

View file

@ -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",

View file

@ -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<TestableNetworkCallback>()
// Cannot be initialized before setUp as eventuallyExpect<LinkPropertiesChanged>() 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<LinkPropertiesChanged>() 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())
}
}