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