Add CTS to validate socket FwMark value

Bug: 373280981
Bug: 331139818
Flag: EXEMPT test code
Test: atest android.net.cts.NetworkAgentTest
NO_IFTTT=no change required
Change-Id: I32da9f0c703f4572cd28f85111f199aad3d83541
This commit is contained in:
Sewook Seo 2025-04-27 18:51:02 +00:00
commit 8251949339

View file

@ -88,10 +88,13 @@ import android.os.Process
import android.os.SystemClock
import android.platform.test.annotations.AppModeFull
import android.system.Os
import android.system.OsConstants.AF_INET
import android.system.OsConstants.AF_INET6
import android.system.OsConstants.IPPROTO_TCP
import android.system.OsConstants.IPPROTO_UDP
import android.system.OsConstants.SOCK_DGRAM
import android.system.OsConstants.SOCK_STREAM
import android.system.OsConstants.SOL_SOCKET
import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import android.telephony.data.EpsBearerQosSessionAttributes
@ -138,6 +141,7 @@ import com.android.testutils.runAsShell
import com.android.testutils.tryTest
import com.android.testutils.waitForIdle
import java.io.Closeable
import java.io.FileDescriptor
import java.io.IOException
import java.net.DatagramSocket
import java.net.InetAddress
@ -158,6 +162,7 @@ import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlin.test.fail
import org.junit.After
import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
@ -190,6 +195,16 @@ private const val NO_CALLBACK_TIMEOUT = 200L
private const val WORSE_NETWORK_SCORE = 65
private const val BETTER_NETWORK_SCORE = 75
private const val FAKE_NET_ID = 1098
// These bit shifts and masks must be kept in sync with system/netd/include/Fwmark.h
// LINT.IfChange
private const val EXPLICITLY_SELECTED_BIT_MASK = 1 shl 16
private const val PROTECTED_FROM_VPN_BIT_MASK = 1 shl 17
private const val NETWORK_PERMISSION_BIT_SHIFT = 18
private const val NETWORK_PERMISSION_BIT_MASK = 0xc0000
private const val NETWORK_ID_MASK = 0xffff
// LINT.ThenChange(//system/netd/include/Fwmark.h)
private val instrumentation: Instrumentation
get() = InstrumentationRegistry.getInstrumentation()
private val realContext: Context
@ -203,6 +218,7 @@ private fun Message(what: Int, arg1: Int, arg2: Int, obj: Any?) = Message.obtain
private val LINK_ADDRESS = LinkAddress("2001:db8::1/64")
private val REMOTE_ADDRESS = InetAddresses.parseNumericAddress("2001:db8::123")
private val REMOTE_ADDRESS_V4 = InetAddresses.parseNumericAddress("192.0.2.11")
private val PREFIX = IpPrefix("2001:db8::/64")
private val NEXTHOP = InetAddresses.parseNumericAddress("fe80::abcd")
@ -1309,6 +1325,147 @@ class NetworkAgentTest {
// tearDown() will unregister the requests and agents
}
private fun prepareSocketForFwmarkTest(
network: Network,
select: Boolean,
proto: Int,
addressFamily: Int,
destAddress: InetSocketAddress?
): FileDescriptor {
val sockFd = if (proto == IPPROTO_TCP) {
Os.socket(addressFamily, SOCK_STREAM, IPPROTO_TCP)
} else {
Os.socket(addressFamily, SOCK_DGRAM, IPPROTO_UDP)
}
if (select) {
network.bindSocket(sockFd)
}
if (destAddress != null) {
Os.connect(sockFd, destAddress.address, destAddress.port)
}
return sockFd
}
private fun validateSocketFwmark(
sockFd: FileDescriptor,
network: Network,
expectExplicitlySelected: Boolean
) {
val socketMark = Os.getsockoptInt(sockFd, SOL_SOCKET, 36 /* SO_MARK */)
val explicitlySelected = (socketMark and EXPLICITLY_SELECTED_BIT_MASK) != 0
val protectedFromVpn = (socketMark and PROTECTED_FROM_VPN_BIT_MASK) != 0
val networkPermission = (
(socketMark and NETWORK_PERMISSION_BIT_MASK) shr NETWORK_PERMISSION_BIT_SHIFT)
// TODO: Applying the verification method for the upper bit of netId that remains 0.
assertEquals(socketMark and NETWORK_ID_MASK, network.netId)
assertEquals(expectExplicitlySelected, explicitlySelected)
assertFalse(protectedFromVpn)
assertEquals(1, networkPermission) // This test APK has the CHANGE_NETWORK_STATE permission
}
@Test
fun testValidSocketFwmarkValue() {
// This test should be run starting with SDK version 36.1.
// TODO: @IgnoreUpTo doesn't support SDK_INT_FULL. We need a new annotation for it.
assumeTrue(Build.VERSION.SDK_INT_FULL > Build.VERSION_CODES_FULL.BAKLAVA)
data class TestCondition(
val net: Network,
val select: Boolean,
val proto: Int,
val addressFamily: Int,
val dest: InetSocketAddress?
)
val defaultNetworkCallback = TestableNetworkCallback()
mCM.registerDefaultNetworkCallback(defaultNetworkCallback)
val available = defaultNetworkCallback.eventuallyExpect<Available>()
val defaultNetwork = available.network
defaultNetworkCallback.expect<CapabilitiesChanged>(defaultNetwork)
val linkProperties = defaultNetworkCallback.expect<LinkPropertiesChanged>(defaultNetwork)
defaultNetworkCallback.expect<BlockedStatus>(defaultNetwork)
val afDefaultNet = if (linkProperties.lp.hasIPv6DefaultRoute()) {
AF_INET6
} else {
AF_INET
}
val address = if (afDefaultNet == AF_INET6) {
REMOTE_ADDRESS
} else {
REMOTE_ADDRESS_V4
}
val dest = InetSocketAddress(address, 32315 /* port */)
val (testAgent1, callback1) = createConnectedNetworkAgent()
val (testAgent2, callback2) = createConnectedNetworkAgent(specifier = "1")
val testNetwork1 = testAgent1.network!!
val testNetwork2 = testAgent2.network!!
val testSocketMap: MutableMap<TestCondition, FileDescriptor?> = mutableMapOf(
TestCondition(testNetwork1, true, IPPROTO_UDP, AF_INET6, null) to null,
TestCondition(testNetwork1, true, IPPROTO_TCP, AF_INET6, null) to null,
TestCondition(testNetwork2, true, IPPROTO_UDP, AF_INET6, null) to null,
TestCondition(testNetwork2, true, IPPROTO_TCP, AF_INET6, null) to null,
TestCondition(defaultNetwork, true, IPPROTO_UDP, afDefaultNet, null) to null,
TestCondition(defaultNetwork, true, IPPROTO_TCP, afDefaultNet, null) to null,
TestCondition(defaultNetwork, false, IPPROTO_UDP, afDefaultNet, dest) to null
)
fun prepareTestSockets(map: MutableMap<TestCondition, FileDescriptor?>) {
map.keys.forEach { cond ->
map[cond] = prepareSocketForFwmarkTest(
cond.net, cond.select, cond.proto, cond.addressFamily, cond.dest)
}
}
fun closeSockets(map: MutableMap<TestCondition, FileDescriptor?>) {
map.keys.forEach { cond ->
if (map[cond] != null) {
Os.close(map[cond])
map[cond] = null
}
}
}
fun verifySockets(map: MutableMap<TestCondition, FileDescriptor?>) {
map.keys.forEach { cond ->
assertNotNull(map[cond])
validateSocketFwmark(map[cond]!!, cond.net, cond.select)
}
}
var retryCount = 0
val maxRetries = 1
try {
while (retryCount <= maxRetries) {
try {
prepareTestSockets(testSocketMap)
defaultNetworkCallback.assertNoCallback(NO_CALLBACK_TIMEOUT)
break
} catch (e: AssertionError) {
closeSockets(testSocketMap)
retryCount++
if (retryCount > maxRetries) {
throw e
}
}
}
// Even if the default network changes, below test verifySockets() will not fail,
// because the socket's fwmark remains after the network disconnection, and they
// are not connected TCP sockets, so not destroyed until test program closes it.
verifySockets(testSocketMap)
} finally {
// This block is the final cleanup, ensuring no resources are left open.
closeSockets(testSocketMap)
mCM.unregisterNetworkCallback(defaultNetworkCallback)
mCM.unregisterNetworkCallback(callback1)
mCM.unregisterNetworkCallback(callback2)
testAgent1.unregister()
testAgent2.unregister()
}
}
private class TestableQosCallback : QosCallback() {
val history = ArrayTrackRecord<Event>().newReadHead()