1 /*
2 * Copyright (C) 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package android.net.cts
18
19 import android.Manifest.permission.MANAGE_TEST_NETWORKS
20 import android.Manifest.permission.NETWORK_SETTINGS
21 import android.content.Context
22 import android.content.pm.PackageManager
23 import android.net.ConnectivityManager
24 import android.net.EthernetManager
25 import android.net.InetAddresses
26 import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
27 import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
28 import android.net.NetworkCapabilities.TRANSPORT_TEST
29 import android.net.NetworkRequest
30 import android.net.TestNetworkInterface
31 import android.net.TestNetworkManager
32 import android.net.Uri
33 import android.net.dhcp.DhcpDiscoverPacket
34 import android.net.dhcp.DhcpPacket
35 import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE
36 import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_DISCOVER
37 import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE_REQUEST
38 import android.net.dhcp.DhcpRequestPacket
39 import android.os.Build
40 import android.os.HandlerThread
41 import android.platform.test.annotations.AppModeFull
42 import androidx.test.platform.app.InstrumentationRegistry
43 import androidx.test.runner.AndroidJUnit4
44 import com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress
45 import com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address
46 import com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY
47 import com.android.testutils.DevSdkIgnoreRule
48 import com.android.testutils.DhcpClientPacketFilter
49 import com.android.testutils.DhcpOptionFilter
50 import com.android.testutils.RecorderCallback.CallbackEntry
51 import com.android.testutils.TapPacketReader
52 import com.android.testutils.TestHttpServer
53 import com.android.testutils.TestableNetworkCallback
54 import com.android.testutils.runAsShell
55 import fi.iki.elonen.NanoHTTPD.Response.Status
56 import org.junit.After
57 import org.junit.Assume.assumeFalse
58 import org.junit.Before
59 import org.junit.Rule
60 import org.junit.Test
61 import org.junit.runner.RunWith
62 import java.net.Inet4Address
63 import kotlin.test.assertEquals
64 import kotlin.test.assertNotNull
65 import kotlin.test.assertTrue
66 import kotlin.test.fail
67
68 private const val MAX_PACKET_LENGTH = 1500
69 private const val TEST_TIMEOUT_MS = 10_000L
70
71 private const val TEST_LEASE_TIMEOUT_SECS = 3600 * 12
72 private const val TEST_PREFIX_LENGTH = 24
73
74 private const val TEST_LOGIN_URL = "https://login.capport.android.com"
75 private const val TEST_VENUE_INFO_URL = "https://venueinfo.capport.android.com"
76 private const val TEST_DOMAIN_NAME = "lan"
77 private const val TEST_MTU = 1500.toShort()
78
79 @AppModeFull(reason = "Instant apps cannot create test networks")
80 @RunWith(AndroidJUnit4::class)
81 class NetworkValidationTest {
82 @JvmField
83 @Rule
84 val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
85
<lambda>null86 private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
<lambda>null87 private val tnm by lazy { context.assertHasService(TestNetworkManager::class.java) }
<lambda>null88 private val eth by lazy { context.assertHasService(EthernetManager::class.java) }
<lambda>null89 private val cm by lazy { context.assertHasService(ConnectivityManager::class.java) }
90
91 private val handlerThread = HandlerThread(NetworkValidationTest::class.java.simpleName)
92 private val serverIpAddr = InetAddresses.parseNumericAddress("192.0.2.222") as Inet4Address
93 private val clientIpAddr = InetAddresses.parseNumericAddress("192.0.2.111") as Inet4Address
94 private val httpServer = TestHttpServer()
95 private val ethRequest = NetworkRequest.Builder()
96 // ETHERNET|TEST transport networks do not have NET_CAPABILITY_TRUSTED
97 .removeCapability(NET_CAPABILITY_TRUSTED)
98 .addTransportType(TRANSPORT_TEST).build()
99 private val ethRequestCb = TestableNetworkCallback()
100
101 private lateinit var iface: TestNetworkInterface
102 private lateinit var reader: TapPacketReader
103 private lateinit var capportUrl: Uri
104
105 private var testSkipped = false
106
107 @Before
setUpnull108 fun setUp() {
109 // This test requires using a tap interface as an ethernet interface.
110 val pm = context.getPackageManager()
111 testSkipped = !pm.hasSystemFeature(PackageManager.FEATURE_ETHERNET) &&
112 context.getSystemService(EthernetManager::class.java) == null
113 assumeFalse(testSkipped)
114
115 // Register a request so the network does not get torn down
116 cm.requestNetwork(ethRequest, ethRequestCb)
117 runAsShell(NETWORK_SETTINGS, MANAGE_TEST_NETWORKS) {
118 eth.setIncludeTestInterfaces(true)
119 // Keeping a reference to the test interface also makes sure the ParcelFileDescriptor
120 // does not go out of scope, which would cause it to close the underlying FileDescriptor
121 // in its finalizer.
122 iface = tnm.createTapInterface()
123 }
124
125 handlerThread.start()
126 reader = TapPacketReader(
127 handlerThread.threadHandler,
128 iface.fileDescriptor.fileDescriptor,
129 MAX_PACKET_LENGTH)
130 reader.startAsyncForTest()
131 httpServer.start()
132
133 // Pad the listening port to make sure it is always of length 5. This ensures the URL has
134 // always the same length so the test can use constant IP and UDP header lengths.
135 // The maximum port number is 65535 so a length of 5 is always enough.
136 capportUrl = Uri.parse("http://localhost:${httpServer.listeningPort}/testapi.html?par=val")
137 }
138
139 @After
tearDownnull140 fun tearDown() {
141 if (testSkipped) return
142 cm.unregisterNetworkCallback(ethRequestCb)
143
144 runAsShell(NETWORK_SETTINGS) { eth.setIncludeTestInterfaces(false) }
145
146 httpServer.stop()
147 handlerThread.threadHandler.post { reader.stop() }
148 handlerThread.quitSafely()
149 handlerThread.join()
150
151 iface.fileDescriptor.close()
152 }
153
154 @Test
testCapportApiCallbacksnull155 fun testCapportApiCallbacks() {
156 httpServer.addResponse(capportUrl, Status.OK, content = """
157 |{
158 | "captive": true,
159 | "user-portal-url": "$TEST_LOGIN_URL",
160 | "venue-info-url": "$TEST_VENUE_INFO_URL"
161 |}
162 """.trimMargin())
163
164 // Handle the DHCP handshake that includes the capport API URL
165 val discover = reader.assertDhcpPacketReceived(
166 DhcpDiscoverPacket::class.java, TEST_TIMEOUT_MS, DHCP_MESSAGE_TYPE_DISCOVER)
167 reader.sendResponse(makeOfferPacket(discover.clientMac, discover.transactionId))
168
169 val request = reader.assertDhcpPacketReceived(
170 DhcpRequestPacket::class.java, TEST_TIMEOUT_MS, DHCP_MESSAGE_TYPE_REQUEST)
171 assertEquals(discover.transactionId, request.transactionId)
172 assertEquals(clientIpAddr, request.mRequestedIp)
173 reader.sendResponse(makeAckPacket(request.clientMac, request.transactionId))
174
175 // The first request received by the server should be for the portal API
176 assertTrue(httpServer.requestsRecord.poll(TEST_TIMEOUT_MS, 0)?.matches(capportUrl) ?: false,
177 "The device did not fetch captive portal API data within timeout")
178
179 // Expect network callbacks with capport info
180 val testCb = TestableNetworkCallback(TEST_TIMEOUT_MS)
181 // LinkProperties do not contain captive portal info if the callback is registered without
182 // NETWORK_SETTINGS permissions.
183 val lp = runAsShell(NETWORK_SETTINGS) {
184 cm.registerNetworkCallback(ethRequest, testCb)
185
186 try {
187 val ncCb = testCb.eventuallyExpect<CallbackEntry.CapabilitiesChanged> {
188 it.caps.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)
189 }
190 testCb.eventuallyExpect<CallbackEntry.LinkPropertiesChanged> {
191 it.network == ncCb.network && it.lp.captivePortalData != null
192 }.lp
193 } finally {
194 cm.unregisterNetworkCallback(testCb)
195 }
196 }
197
198 assertEquals(capportUrl, lp.captivePortalApiUrl)
199 with(lp.captivePortalData) {
200 assertNotNull(this)
201 assertTrue(isCaptive)
202 assertEquals(Uri.parse(TEST_LOGIN_URL), userPortalUrl)
203 assertEquals(Uri.parse(TEST_VENUE_INFO_URL), venueInfoUrl)
204 }
205 }
206
makeOfferPacketnull207 private fun makeOfferPacket(clientMac: ByteArray, transactionId: Int) =
208 DhcpPacket.buildOfferPacket(DhcpPacket.ENCAP_L2, transactionId,
209 false /* broadcast */, serverIpAddr, IPV4_ADDR_ANY /* relayIp */, clientIpAddr,
210 clientMac, TEST_LEASE_TIMEOUT_SECS,
211 getPrefixMaskAsInet4Address(TEST_PREFIX_LENGTH),
212 getBroadcastAddress(clientIpAddr, TEST_PREFIX_LENGTH),
213 listOf(serverIpAddr) /* gateways */, listOf(serverIpAddr) /* dnsServers */,
214 serverIpAddr, TEST_DOMAIN_NAME, null /* hostname */, true /* metered */,
215 TEST_MTU, capportUrl.toString())
216
217 private fun makeAckPacket(clientMac: ByteArray, transactionId: Int) =
218 DhcpPacket.buildAckPacket(DhcpPacket.ENCAP_L2, transactionId,
219 false /* broadcast */, serverIpAddr, IPV4_ADDR_ANY /* relayIp */, clientIpAddr,
220 clientIpAddr /* requestClientIp */, clientMac, TEST_LEASE_TIMEOUT_SECS,
221 getPrefixMaskAsInet4Address(TEST_PREFIX_LENGTH),
222 getBroadcastAddress(clientIpAddr, TEST_PREFIX_LENGTH),
223 listOf(serverIpAddr) /* gateways */, listOf(serverIpAddr) /* dnsServers */,
224 serverIpAddr, TEST_DOMAIN_NAME, null /* hostname */, true /* metered */,
225 TEST_MTU, false /* rapidCommit */, capportUrl.toString())
226 }
227
228 private fun <T : DhcpPacket> TapPacketReader.assertDhcpPacketReceived(
229 packetType: Class<T>,
230 timeoutMs: Long,
231 type: Byte
232 ): T {
233 val packetBytes = poll(timeoutMs, DhcpClientPacketFilter()
234 .and(DhcpOptionFilter(DHCP_MESSAGE_TYPE, type)))
235 ?: fail("${packetType.simpleName} not received within timeout")
236 val packet = DhcpPacket.decodeFullPacket(packetBytes, packetBytes.size, DhcpPacket.ENCAP_L2)
237 assertTrue(packetType.isInstance(packet),
238 "Expected ${packetType.simpleName} but got ${packet.javaClass.simpleName}")
239 return packetType.cast(packet)
240 }
241
assertHasServicenull242 private fun <T> Context.assertHasService(manager: Class<T>): T {
243 return getSystemService(manager) ?: fail("Service $manager not found")
244 }
245