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