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