• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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