• 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.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