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