1 /* <lambda>null2 * Copyright (C) 2023 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 18 19 import android.Manifest.permission.MANAGE_TEST_NETWORKS 20 import android.app.usage.NetworkStats 21 import android.app.usage.NetworkStats.Bucket.TAG_NONE 22 import android.app.usage.NetworkStatsManager 23 import android.net.ConnectivityManager.TYPE_TEST 24 import android.net.NetworkTemplate.MATCH_TEST 25 import android.os.Build 26 import android.os.Process 27 import androidx.test.platform.app.InstrumentationRegistry 28 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo 29 import com.android.testutils.DevSdkIgnoreRunner 30 import com.android.testutils.PacketBridge 31 import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged 32 import com.android.testutils.TestDnsServer 33 import com.android.testutils.TestHttpServer 34 import com.android.testutils.TestableNetworkCallback 35 import com.android.testutils.runAsShell 36 import fi.iki.elonen.NanoHTTPD 37 import java.io.BufferedInputStream 38 import java.io.BufferedOutputStream 39 import java.net.HttpURLConnection 40 import java.net.HttpURLConnection.HTTP_OK 41 import java.net.InetSocketAddress 42 import java.net.URL 43 import java.nio.charset.Charset 44 import kotlin.test.assertEquals 45 import kotlin.test.assertTrue 46 import org.junit.After 47 import org.junit.Assume.assumeTrue 48 import org.junit.Before 49 import org.junit.Test 50 import org.junit.runner.RunWith 51 52 @RunWith(DevSdkIgnoreRunner::class) 53 @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) 54 class NetworkStatsIntegrationTest { 55 private val INTERNAL_V6ADDR = 56 LinkAddress(InetAddresses.parseNumericAddress("2001:db8::1234"), 64) 57 private val EXTERNAL_V6ADDR = 58 LinkAddress(InetAddresses.parseNumericAddress("2001:db8::5678"), 64) 59 60 // Remote address, both the client and server will have a hallucination that 61 // they are talking to this address. 62 private val REMOTE_V6ADDR = 63 LinkAddress(InetAddresses.parseNumericAddress("dead:beef::808:808"), 64) 64 private val REMOTE_V4ADDR = 65 LinkAddress(InetAddresses.parseNumericAddress("8.8.8.8"), 32) 66 private val DEFAULT_BUFFER_SIZE = 1000 67 private val CONNECTION_TIMEOUT_MILLIS = 15000 68 private val TEST_DOWNLOAD_SIZE = 10000L 69 private val TEST_UPLOAD_SIZE = 20000L 70 private val HTTP_SERVER_NAME = "test.com" 71 private val DNS_SERVER_PORT = 53 72 private val TEST_TAG = 0xF00D 73 74 // Set up the packet bridge with two IPv6 address only test networks. 75 private val inst = InstrumentationRegistry.getInstrumentation() 76 private val context = inst.getContext() 77 private val packetBridge = runAsShell(MANAGE_TEST_NETWORKS) { 78 PacketBridge(context, INTERNAL_V6ADDR, EXTERNAL_V6ADDR, REMOTE_V6ADDR.address) 79 }.apply { 80 start() 81 } 82 private val cm = context.getSystemService(ConnectivityManager::class.java) 83 84 // Set up DNS server for testing server and DNS64. 85 private val fakeDns = TestDnsServer( 86 packetBridge.externalNetwork, InetSocketAddress(EXTERNAL_V6ADDR.address, DNS_SERVER_PORT) 87 ).apply { 88 start() 89 setAnswer( 90 "ipv4only.arpa", 91 listOf(IpPrefix(REMOTE_V6ADDR.address, REMOTE_V6ADDR.prefixLength).address) 92 ) 93 setAnswer(HTTP_SERVER_NAME, listOf(REMOTE_V4ADDR.address)) 94 } 95 96 // Start up test http server. 97 private val httpServer = TestHttpServer(EXTERNAL_V6ADDR.address.hostAddress).apply { 98 start() 99 } 100 101 @Before 102 fun setUp() { 103 assumeTrue(shouldRunTests()) 104 } 105 106 // For networkstack tests, it is not guaranteed that the tethering module will be 107 // updated at the same time. If the tethering module is not new enough, it may not contain 108 // the necessary abilities to run these tests. For example, The tests depends on test 109 // network stats being counted, which can only be achieved when they are marked as TYPE_TEST. 110 // If the tethering module does not support TYPE_TEST stats, then these tests will need 111 // to be skipped. 112 fun shouldRunTests() = cm.getNetworkInfo(packetBridge.internalNetwork).type == TYPE_TEST 113 114 @After 115 fun tearDown() { 116 packetBridge.stop() 117 fakeDns.stop() 118 httpServer.stop() 119 } 120 121 private fun waitFor464XlatReady(network: Network): String { 122 val iface = cm.getLinkProperties(network).interfaceName 123 124 // Make a network request to listen to the specific test network. 125 val nr = NetworkRequest.Builder() 126 .clearCapabilities() 127 .addTransportType(NetworkCapabilities.TRANSPORT_TEST) 128 .setNetworkSpecifier(TestNetworkSpecifier(iface)) 129 .build() 130 val testCb = TestableNetworkCallback() 131 cm.registerNetworkCallback(nr, testCb) 132 // Wait for the stacked address to be available. 133 testCb.eventuallyExpect<LinkPropertiesChanged> { 134 it.lp.stackedLinks?.getOrNull(0)?.linkAddresses?.getOrNull(0) != null 135 } 136 return iface 137 } 138 139 /** 140 * Verify data usage download stats with test 464xlat networks. 141 * 142 * This test starts two test networks and binds them together, the internal one is for the 143 * client to make http traffic on the test network, and the external one is for the mocked 144 * http and dns server to bind to and provide responses. 145 * 146 * After Clat setup, the client will use clat v4 address to send packets to the mocked 147 * server v4 address, which will be translated into a v6 packet by the clat daemon with 148 * NAT64 prefix learned from the mocked DNS64 response. And send to the interface. 149 * 150 * While the packets are being forwarded to the external interface, the servers will see 151 * the packets originated from the mocked v6 address, and destined to a local v6 address. 152 */ 153 @Test 154 fun test464XlatTcpStats() { 155 // Wait for 464Xlat to be ready. 156 val internalInterfaceName = waitFor464XlatReady(packetBridge.internalNetwork) 157 158 val (_, rxBytesBeforeTest) = getTotalTxRxBytes(internalInterfaceName) 159 val (_, rxTaggedBytesBeforeTest) = getTaggedTxRxBytes(internalInterfaceName, TEST_TAG) 160 161 // Generate the download traffic. 162 genHttpTraffic(packetBridge.internalNetwork, uploadSize = 0L, TEST_DOWNLOAD_SIZE) 163 164 // In practice, for one way 10k download payload, the download usage is about 165 // 11222~12880 bytes. And the upload usage is about 1279~1626 bytes, which is majorly 166 // contributed by TCP ACK packets. 167 val (txBytesAfterDownload, rxBytesAfterDownload) = 168 getTotalTxRxBytes(internalInterfaceName) 169 val (txTaggedBytesAfterDownload, rxTaggedBytesAfterDownload) = getTaggedTxRxBytes( 170 internalInterfaceName, 171 TEST_TAG 172 ) 173 assertInRange( 174 "Download size", internalInterfaceName, 175 rxBytesAfterDownload - rxBytesBeforeTest, 176 TEST_DOWNLOAD_SIZE, (TEST_DOWNLOAD_SIZE * 1.3).toLong() 177 ) 178 // Increment of tagged data should be zero since no tagged traffic was generated. 179 assertEquals( 180 rxTaggedBytesBeforeTest, 181 rxTaggedBytesAfterDownload, 182 "Tagged download size of uid ${Process.myUid()} on $internalInterfaceName" 183 ) 184 185 // Generate upload traffic with tag to verify tagged data accounting as well. 186 genHttpTrafficWithTag( 187 packetBridge.internalNetwork, 188 TEST_UPLOAD_SIZE, 189 downloadSize = 0L, 190 TEST_TAG 191 ) 192 193 // Verify upload data usage accounting. 194 val (txBytesAfterUpload, _) = getTotalTxRxBytes(internalInterfaceName) 195 val (txTaggedBytesAfterUpload, _) = getTaggedTxRxBytes(internalInterfaceName, TEST_TAG) 196 assertInRange( 197 "Upload size", internalInterfaceName, 198 txBytesAfterUpload - txBytesAfterDownload, 199 TEST_UPLOAD_SIZE, (TEST_UPLOAD_SIZE * 1.3).toLong() 200 ) 201 assertInRange( 202 "Tagged upload size of uid ${Process.myUid()}", 203 internalInterfaceName, 204 txTaggedBytesAfterUpload - txTaggedBytesAfterDownload, 205 TEST_UPLOAD_SIZE, 206 (TEST_UPLOAD_SIZE * 1.3).toLong() 207 ) 208 } 209 210 private fun genHttpTraffic(network: Network, uploadSize: Long, downloadSize: Long) = 211 genHttpTrafficWithTag(network, uploadSize, downloadSize, NetworkStats.Bucket.TAG_NONE) 212 213 private fun genHttpTrafficWithTag( 214 network: Network, 215 uploadSize: Long, 216 downloadSize: Long, 217 tag: Int 218 ) { 219 val path = "/test_upload_download" 220 val buf = ByteArray(DEFAULT_BUFFER_SIZE) 221 222 httpServer.addResponse( 223 TestHttpServer.Request(path, NanoHTTPD.Method.POST), NanoHTTPD.Response.Status.OK, 224 content = getRandomString(downloadSize) 225 ) 226 var httpConnection: HttpURLConnection? = null 227 try { 228 TrafficStats.setThreadStatsTag(tag) 229 val spec = "http://$HTTP_SERVER_NAME:${httpServer.listeningPort}$path" 230 val url = URL(spec) 231 httpConnection = network.openConnection(url) as HttpURLConnection 232 httpConnection.connectTimeout = CONNECTION_TIMEOUT_MILLIS 233 httpConnection.requestMethod = "POST" 234 httpConnection.doOutput = true 235 // Tell the server that the response should not be compressed. Otherwise, the data usage 236 // accounted will be less than expected. 237 httpConnection.setRequestProperty("Accept-Encoding", "identity") 238 // Tell the server that to close connection after this request, this is needed to 239 // prevent from reusing the same socket that has different tagging requirement. 240 httpConnection.setRequestProperty("Connection", "close") 241 242 // Send http body. 243 val outputStream = BufferedOutputStream(httpConnection.outputStream) 244 outputStream.write(getRandomString(uploadSize).toByteArray(Charset.forName("UTF-8"))) 245 outputStream.close() 246 assertEquals(HTTP_OK, httpConnection.responseCode) 247 248 // Receive response from the server. 249 val inputStream = BufferedInputStream(httpConnection.getInputStream()) 250 var total = 0L 251 while (true) { 252 val count = inputStream.read(buf) 253 if (count == -1) break // End-of-Stream 254 total += count 255 } 256 assertEquals(downloadSize, total) 257 } finally { 258 httpConnection?.inputStream?.close() 259 TrafficStats.clearThreadStatsTag() 260 } 261 } 262 263 private fun getTotalTxRxBytes(iface: String): Pair<Long, Long> { 264 return getNetworkStatsThat(iface, TAG_NONE) { nsm, template -> 265 nsm.querySummary(template, Long.MIN_VALUE, Long.MAX_VALUE) 266 } 267 } 268 269 private fun getTaggedTxRxBytes(iface: String, tag: Int): Pair<Long, Long> { 270 return getNetworkStatsThat(iface, tag) { nsm, template -> 271 nsm.queryTaggedSummary(template, Long.MIN_VALUE, Long.MAX_VALUE) 272 } 273 } 274 275 private fun getNetworkStatsThat( 276 iface: String, 277 tag: Int, 278 queryApi: (nsm: NetworkStatsManager, template: NetworkTemplate) -> NetworkStats 279 ): Pair<Long, Long> { 280 val nsm = context.getSystemService(NetworkStatsManager::class.java) 281 nsm.forceUpdate() 282 val testTemplate = NetworkTemplate.Builder(MATCH_TEST) 283 .setWifiNetworkKeys(setOf(iface)).build() 284 val stats = queryApi.invoke(nsm, testTemplate) 285 val recycled = NetworkStats.Bucket() 286 var rx = 0L 287 var tx = 0L 288 while (stats.hasNextBucket()) { 289 stats.getNextBucket(recycled) 290 if (recycled.uid != Process.myUid() || recycled.tag != tag) continue 291 rx += recycled.rxBytes 292 tx += recycled.txBytes 293 } 294 return tx to rx 295 } 296 297 /** Verify the given value is in range [lower, upper] */ 298 private fun assertInRange(tag: String, iface: String, value: Long, lower: Long, upper: Long) = 299 assertTrue( 300 value in lower..upper, 301 "$tag on $iface: $value is not within range [$lower, $upper]" 302 ) 303 304 fun getRandomString(length: Long): String { 305 val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') 306 return (1..length) 307 .map { allowedChars.random() } 308 .joinToString("") 309 } 310 } 311