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