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 com.android.networkstack.metrics;
18 
19 import static android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH;
20 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
21 import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
22 import static android.net.NetworkCapabilities.TRANSPORT_LOWPAN;
23 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
24 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
25 import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE;
26 
27 import static java.lang.System.currentTimeMillis;
28 
29 import android.net.INetworkMonitor;
30 import android.net.NetworkCapabilities;
31 import android.net.captiveportal.CaptivePortalProbeResult;
32 import android.net.metrics.ValidationProbeEvent;
33 import android.net.util.NetworkStackUtils;
34 import android.net.util.Stopwatch;
35 import android.stats.connectivity.ProbeResult;
36 import android.stats.connectivity.ProbeType;
37 import android.stats.connectivity.TransportType;
38 import android.stats.connectivity.ValidationResult;
39 
40 import androidx.annotation.Nullable;
41 import androidx.annotation.VisibleForTesting;
42 
43 import com.android.networkstack.apishim.common.CaptivePortalDataShim;
44 
45 /**
46  * Class to record the network validation into statsd.
47  * 1. Fill in NetworkValidationReported proto.
48  * 2. Write the NetworkValidationReported proto into statsd.
49  * @hide
50  */
51 
52 public class NetworkValidationMetrics {
53     private final NetworkValidationReported.Builder mStatsBuilder =
54             NetworkValidationReported.newBuilder();
55     private final ProbeEvents.Builder mProbeEventsBuilder = ProbeEvents.newBuilder();
56     private final CapportApiData.Builder mCapportApiDataBuilder = CapportApiData.newBuilder();
57     private final Stopwatch mWatch = new Stopwatch();
58     private int mValidationIndex = 0;
59     // Define a maximum size that can store events.
60     public static final int MAX_PROBE_EVENTS_COUNT = 20;
61 
62     /**
63      * Reset this NetworkValidationMetrics and start collecting timing and metrics.
64      *
65      * <p>This must be called when validation starts.
66      */
startCollection(@ullable NetworkCapabilities nc)67     public void startCollection(@Nullable NetworkCapabilities nc) {
68         mStatsBuilder.clear();
69         mProbeEventsBuilder.clear();
70         mCapportApiDataBuilder.clear();
71         mWatch.restart();
72         mStatsBuilder.setTransportType(getTransportTypeFromNC(nc));
73         mValidationIndex++;
74     }
75 
76     /**
77      * Returns the enum TransportType.
78      *
79      * <p>This method only supports a limited set of common transport type combinations that can be
80      * measured through metrics, and will return {@link TransportType#TT_UNKNOWN} for others. This
81      * ensures that, for example, metrics for a TRANSPORT_NEW_UNKNOWN | TRANSPORT_ETHERNET network
82      * cannot get aggregated with / compared with a "normal" TRANSPORT_ETHERNET network without
83      * noticing.
84      *
85      * @param nc Capabilities to extract transport type from.
86      * @return the TransportType which is defined in
87      * core/proto/android/stats/connectivity/network_stack.proto
88      */
89     @VisibleForTesting
getTransportTypeFromNC(@ullable NetworkCapabilities nc)90     public static TransportType getTransportTypeFromNC(@Nullable NetworkCapabilities nc) {
91         if (nc == null) return TransportType.TT_UNKNOWN;
92 
93         final int trCount = nc.getTransportTypes().length;
94         boolean hasCellular = nc.hasTransport(TRANSPORT_CELLULAR);
95         boolean hasWifi = nc.hasTransport(TRANSPORT_WIFI);
96         boolean hasBT = nc.hasTransport(TRANSPORT_BLUETOOTH);
97         boolean hasEthernet = nc.hasTransport(TRANSPORT_ETHERNET);
98         boolean hasVpn = nc.hasTransport(TRANSPORT_VPN);
99         boolean hasWifiAware = nc.hasTransport(TRANSPORT_WIFI_AWARE);
100         boolean hasLopan = nc.hasTransport(TRANSPORT_LOWPAN);
101 
102         // VPN networks are not subject to validation and should not see validation stats, but
103         // metrics could be added to measure private DNS probes only.
104         if (trCount == 3 && hasCellular && hasWifi && hasVpn) {
105             return TransportType.TT_WIFI_CELLULAR_VPN;
106         }
107 
108         if (trCount == 2 && hasVpn) {
109             if (hasWifi) return TransportType.TT_WIFI_VPN;
110             if (hasCellular) return TransportType.TT_CELLULAR_VPN;
111             if (hasBT) return TransportType.TT_BLUETOOTH_VPN;
112             if (hasEthernet) return TransportType.TT_ETHERNET_VPN;
113         }
114 
115         if (trCount == 1) {
116             if (hasWifi) return TransportType.TT_WIFI;
117             if (hasCellular) return TransportType.TT_CELLULAR;
118             if (hasBT) return TransportType.TT_BLUETOOTH;
119             if (hasEthernet) return TransportType.TT_ETHERNET;
120             if (hasWifiAware) return TransportType.TT_WIFI_AWARE;
121             if (hasLopan) return TransportType.TT_LOWPAN;
122             // TODO: consider having a TT_VPN for VPN-only transport
123         }
124 
125         return TransportType.TT_UNKNOWN;
126     }
127 
128     /**
129      * Map {@link ValidationProbeEvent} to {@link ProbeType}.
130      */
probeTypeToEnum(final int probeType)131     public static ProbeType probeTypeToEnum(final int probeType) {
132         switch(probeType) {
133             case ValidationProbeEvent.PROBE_DNS:
134                 return ProbeType.PT_DNS;
135             case ValidationProbeEvent.PROBE_HTTP:
136                 return ProbeType.PT_HTTP;
137             case ValidationProbeEvent.PROBE_HTTPS:
138                 return ProbeType.PT_HTTPS;
139             case ValidationProbeEvent.PROBE_PAC:
140                 return ProbeType.PT_PAC;
141             case ValidationProbeEvent.PROBE_FALLBACK:
142                 return ProbeType.PT_FALLBACK;
143             case ValidationProbeEvent.PROBE_PRIVDNS:
144                 return ProbeType.PT_PRIVDNS;
145             default:
146                 return ProbeType.PT_UNKNOWN;
147         }
148     }
149 
150     /**
151      * Map {@link CaptivePortalProbeResult} to {@link ProbeResult}.
152      */
httpProbeResultToEnum(final CaptivePortalProbeResult result)153     public static ProbeResult httpProbeResultToEnum(final CaptivePortalProbeResult result) {
154         if (result == null) return ProbeResult.PR_UNKNOWN;
155 
156         if (result.isSuccessful()) {
157             return ProbeResult.PR_SUCCESS;
158         } else if (result.isDnsPrivateIpResponse()) {
159             return ProbeResult.PR_PRIVATE_IP_DNS;
160         } else if (result.isFailed()) {
161             return ProbeResult.PR_FAILURE;
162         } else if (result.isPortal()) {
163             return ProbeResult.PR_PORTAL;
164         } else {
165             return ProbeResult.PR_UNKNOWN;
166         }
167     }
168 
169     /**
170      * Map  validation result (as per INetworkMonitor) to {@link ValidationResult}.
171      */
172     @VisibleForTesting
validationResultToEnum(int result, String redirectUrl)173     public static ValidationResult validationResultToEnum(int result, String redirectUrl) {
174         // TODO: consider adding a VR_PARTIAL_SUCCESS field to track cases where users accepted
175         // partial connectivity
176         if ((result & INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID) != 0) {
177             return ValidationResult.VR_SUCCESS;
178         } else if (redirectUrl != null) {
179             return ValidationResult.VR_PORTAL;
180         } else if ((result & INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL) != 0) {
181             return ValidationResult.VR_PARTIAL;
182         } else {
183             return ValidationResult.VR_FAILURE;
184         }
185     }
186 
187     /**
188      * Add a network probe event to the metrics builder.
189      */
addProbeEvent(final ProbeType type, final long durationUs, final ProbeResult result, @Nullable final CaptivePortalDataShim capportData)190     public void addProbeEvent(final ProbeType type, final long durationUs, final ProbeResult result,
191             @Nullable final CaptivePortalDataShim capportData) {
192         // When the number of ProbeEvents of mProbeEventsBuilder exceeds
193         // MAX_PROBE_EVENTS_COUNT, stop adding ProbeEvent.
194         // TODO: consider recording the total number of probes in a separate field to know how
195         // many probes are skipped.
196         if (mProbeEventsBuilder.getProbeEventCount() >= MAX_PROBE_EVENTS_COUNT) return;
197 
198         int latencyUs = NetworkStackUtils.saturatedCast(durationUs);
199 
200         final ProbeEvent.Builder probeEventBuilder = ProbeEvent.newBuilder()
201                 .setLatencyMicros(latencyUs)
202                 .setProbeType(type)
203                 .setProbeResult(result);
204 
205         if (capportData != null) {
206             final long secondsRemaining =
207                     (capportData.getExpiryTimeMillis() - currentTimeMillis()) / 1000;
208             mCapportApiDataBuilder
209                 .setRemainingTtlSecs(NetworkStackUtils.saturatedCast(secondsRemaining))
210                 // TODO: rename this field to setRemainingKBytes, or use a long
211                 .setRemainingBytes(
212                         NetworkStackUtils.saturatedCast(capportData.getByteLimit() / 1000))
213                 .setHasPortalUrl((capportData.getUserPortalUrl() != null))
214                 .setHasVenueInfo((capportData.getVenueInfoUrl() != null));
215             probeEventBuilder.setCapportApiData(mCapportApiDataBuilder);
216         }
217 
218         mProbeEventsBuilder.addProbeEvent(probeEventBuilder);
219     }
220 
221     /**
222      * Write the network validation info to mStatsBuilder.
223      */
setValidationResult(int result, String redirectUrl)224     public void setValidationResult(int result, String redirectUrl) {
225         mStatsBuilder.setValidationResult(validationResultToEnum(result, redirectUrl));
226     }
227 
228     /**
229      * Write the NetworkValidationReported proto to statsd.
230      *
231      * <p>This is a no-op if {@link #startCollection(NetworkCapabilities)} was not called since the
232      * last call to this method.
233      */
maybeStopCollectionAndSend()234     public NetworkValidationReported maybeStopCollectionAndSend() {
235         if (!mWatch.isStarted()) return null;
236         mStatsBuilder.setProbeEvents(mProbeEventsBuilder);
237         mStatsBuilder.setLatencyMicros(NetworkStackUtils.saturatedCast(mWatch.stop()));
238         mStatsBuilder.setValidationIndex(mValidationIndex);
239         // write a random value(0 ~ 999) for sampling.
240         mStatsBuilder.setRandomNumber((int) (Math.random() * 1000));
241         final NetworkValidationReported stats = mStatsBuilder.build();
242         final byte[] probeEvents = stats.getProbeEvents().toByteArray();
243 
244         NetworkStackStatsLog.write(NetworkStackStatsLog.NETWORK_VALIDATION_REPORTED,
245                 stats.getTransportType().getNumber(),
246                 probeEvents,
247                 stats.getValidationResult().getNumber(),
248                 stats.getLatencyMicros(),
249                 stats.getValidationIndex(),
250                 stats.getRandomNumber());
251         mWatch.reset();
252         return stats;
253     }
254 }
255