Merge "Make PendingIntent immutable and correct Settings package name"
diff --git a/core/java/android/net/ConnectivityDiagnosticsManager.java b/core/java/android/net/ConnectivityDiagnosticsManager.java
index 275e38c..704f31d 100644
--- a/core/java/android/net/ConnectivityDiagnosticsManager.java
+++ b/core/java/android/net/ConnectivityDiagnosticsManager.java
@@ -711,6 +711,13 @@
* not currently registered. If a ConnectivityDiagnosticsCallback instance is registered with
* multiple NetworkRequests, an IllegalArgumentException will be thrown.
*
+ * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+ * number of outstanding requests to 100 per app (identified by their UID), shared with
+ * callbacks in {@link ConnectivityManager}. Registering a callback with this method will count
+ * toward this limit. If this limit is exceeded, an exception will be thrown. To avoid hitting
+ * this issue and to conserve resources, make sure to unregister the callbacks with
+ * {@link #unregisterConnectivityDiagnosticsCallback}.
+ *
* @param request The NetworkRequest that will be used to match with Networks for which
* callbacks will be fired
* @param e The Executor to be used for running the callback method invocations
@@ -718,6 +725,7 @@
* System
* @throws IllegalArgumentException if the same callback instance is registered with multiple
* NetworkRequests
+ * @throws RuntimeException if the app already has too many callbacks registered.
*/
public void registerConnectivityDiagnosticsCallback(
@NonNull NetworkRequest request,
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index 6b71360..9c994eb 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -2271,27 +2271,6 @@
throwException);
}
- /** {@hide} */
- public static final void enforceTetherChangePermission(Context context, String callingPkg,
- String callingAttributionTag) {
- Preconditions.checkNotNull(context, "Context cannot be null");
- Preconditions.checkNotNull(callingPkg, "callingPkg cannot be null");
-
- if (context.getResources().getStringArray(
- com.android.internal.R.array.config_mobile_hotspot_provision_app).length == 2) {
- // Have a provisioning app - must only let system apps (which check this app)
- // turn on tethering
- context.enforceCallingOrSelfPermission(
- android.Manifest.permission.TETHER_PRIVILEGED, "ConnectivityService");
- } else {
- int uid = Binder.getCallingUid();
- // If callingPkg's uid is not same as Binder.getCallingUid(),
- // AppOpsService throws SecurityException.
- checkAndNoteWriteSettingsOperation(context, uid, callingPkg,
- callingAttributionTag, true /* throwException */);
- }
- }
-
/**
* Check if the package is a allowed to write settings. This also accounts that such an access
* happened.
@@ -3855,13 +3834,22 @@
* or the ability to modify system settings as determined by
* {@link android.provider.Settings.System#canWrite}.</p>
*
+ * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+ * number of outstanding requests to 100 per app (identified by their UID), shared with
+ * all variants of this method, of {@link #registerNetworkCallback} as well as
+ * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+ * Requesting a network with this method will count toward this limit. If this limit is
+ * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+ * make sure to unregister the callbacks with
+ * {@link #unregisterNetworkCallback(NetworkCallback)}.
+ *
* @param request {@link NetworkRequest} describing this request.
* @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
* the callback must not be shared - it uniquely specifies this request.
* The callback is invoked on the default internal Handler.
* @throws IllegalArgumentException if {@code request} contains invalid network capabilities.
* @throws SecurityException if missing the appropriate permissions.
- * @throws RuntimeException if request limit per UID is exceeded.
+ * @throws RuntimeException if the app already has too many callbacks registered.
*/
public void requestNetwork(@NonNull NetworkRequest request,
@NonNull NetworkCallback networkCallback) {
@@ -3875,8 +3863,8 @@
* but runs all the callbacks on the passed Handler.
*
* <p>This method has the same permission requirements as
- * {@link #requestNetwork(NetworkRequest, NetworkCallback)} and throws the same exceptions in
- * the same conditions.
+ * {@link #requestNetwork(NetworkRequest, NetworkCallback)}, is subject to the same limitations,
+ * and throws the same exceptions in the same conditions.
*
* @param request {@link NetworkRequest} describing this request.
* @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
@@ -3907,8 +3895,8 @@
* for that purpose. Calling this method will attempt to bring up the requested network.
*
* <p>This method has the same permission requirements as
- * {@link #requestNetwork(NetworkRequest, NetworkCallback)} and throws the same exceptions in
- * the same conditions.
+ * {@link #requestNetwork(NetworkRequest, NetworkCallback)}, is subject to the same limitations,
+ * and throws the same exceptions in the same conditions.
*
* @param request {@link NetworkRequest} describing this request.
* @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
@@ -3934,8 +3922,8 @@
* on the passed Handler.
*
* <p>This method has the same permission requirements as
- * {@link #requestNetwork(NetworkRequest, NetworkCallback, int)} and throws the same exceptions
- * in the same conditions.
+ * {@link #requestNetwork(NetworkRequest, NetworkCallback)}, is subject to the same limitations,
+ * and throws the same exceptions in the same conditions.
*
* @param request {@link NetworkRequest} describing this request.
* @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
@@ -4004,6 +3992,15 @@
* is unknown prior to bringing up the network so the framework does not
* know how to go about satisfying a request with these capabilities.
*
+ * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+ * number of outstanding requests to 100 per app (identified by their UID), shared with
+ * all variants of this method, of {@link #registerNetworkCallback} as well as
+ * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+ * Requesting a network with this method will count toward this limit. If this limit is
+ * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+ * make sure to unregister the callbacks with {@link #unregisterNetworkCallback(PendingIntent)}
+ * or {@link #releaseNetworkRequest(PendingIntent)}.
+ *
* <p>This method requires the caller to hold either the
* {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
* or the ability to modify system settings as determined by
@@ -4015,7 +4012,7 @@
* comes from {@link PendingIntent#getBroadcast}. Cannot be null.
* @throws IllegalArgumentException if {@code request} contains invalid network capabilities.
* @throws SecurityException if missing the appropriate permissions.
- * @throws RuntimeException if request limit per UID is exceeded.
+ * @throws RuntimeException if the app already has too many callbacks registered.
*/
public void requestNetwork(@NonNull NetworkRequest request,
@NonNull PendingIntent operation) {
@@ -4072,10 +4069,20 @@
* either the application exits or {@link #unregisterNetworkCallback(NetworkCallback)} is
* called.
*
+ * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+ * number of outstanding requests to 100 per app (identified by their UID), shared with
+ * all variants of this method, of {@link #requestNetwork} as well as
+ * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+ * Requesting a network with this method will count toward this limit. If this limit is
+ * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+ * make sure to unregister the callbacks with
+ * {@link #unregisterNetworkCallback(NetworkCallback)}.
+ *
* @param request {@link NetworkRequest} describing this request.
* @param networkCallback The {@link NetworkCallback} that the system will call as suitable
* networks change state.
* The callback is invoked on the default internal Handler.
+ * @throws RuntimeException if the app already has too many callbacks registered.
*/
@RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
public void registerNetworkCallback(@NonNull NetworkRequest request,
@@ -4089,10 +4096,21 @@
* either the application exits or {@link #unregisterNetworkCallback(NetworkCallback)} is
* called.
*
+ * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+ * number of outstanding requests to 100 per app (identified by their UID), shared with
+ * all variants of this method, of {@link #requestNetwork} as well as
+ * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+ * Requesting a network with this method will count toward this limit. If this limit is
+ * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+ * make sure to unregister the callbacks with
+ * {@link #unregisterNetworkCallback(NetworkCallback)}.
+ *
+ *
* @param request {@link NetworkRequest} describing this request.
* @param networkCallback The {@link NetworkCallback} that the system will call as suitable
* networks change state.
* @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+ * @throws RuntimeException if the app already has too many callbacks registered.
*/
@RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
public void registerNetworkCallback(@NonNull NetworkRequest request,
@@ -4126,10 +4144,21 @@
* <p>
* The request may be released normally by calling
* {@link #unregisterNetworkCallback(android.app.PendingIntent)}.
+ *
+ * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+ * number of outstanding requests to 100 per app (identified by their UID), shared with
+ * all variants of this method, of {@link #requestNetwork} as well as
+ * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+ * Requesting a network with this method will count toward this limit. If this limit is
+ * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+ * make sure to unregister the callbacks with {@link #unregisterNetworkCallback(PendingIntent)}
+ * or {@link #releaseNetworkRequest(PendingIntent)}.
+ *
* @param request {@link NetworkRequest} describing this request.
* @param operation Action to perform when the network is available (corresponds
* to the {@link NetworkCallback#onAvailable} call. Typically
* comes from {@link PendingIntent#getBroadcast}. Cannot be null.
+ * @throws RuntimeException if the app already has too many callbacks registered.
*/
@RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
public void registerNetworkCallback(@NonNull NetworkRequest request,
@@ -4151,9 +4180,19 @@
* will continue to be called until either the application exits or
* {@link #unregisterNetworkCallback(NetworkCallback)} is called.
*
+ * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+ * number of outstanding requests to 100 per app (identified by their UID), shared with
+ * all variants of this method, of {@link #requestNetwork} as well as
+ * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+ * Requesting a network with this method will count toward this limit. If this limit is
+ * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+ * make sure to unregister the callbacks with
+ * {@link #unregisterNetworkCallback(NetworkCallback)}.
+ *
* @param networkCallback The {@link NetworkCallback} that the system will call as the
* system default network changes.
* The callback is invoked on the default internal Handler.
+ * @throws RuntimeException if the app already has too many callbacks registered.
*/
@RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
public void registerDefaultNetworkCallback(@NonNull NetworkCallback networkCallback) {
@@ -4165,9 +4204,19 @@
* will continue to be called until either the application exits or
* {@link #unregisterNetworkCallback(NetworkCallback)} is called.
*
+ * <p>To avoid performance issues due to apps leaking callbacks, the system will limit the
+ * number of outstanding requests to 100 per app (identified by their UID), shared with
+ * all variants of this method, of {@link #requestNetwork} as well as
+ * {@link ConnectivityDiagnosticsManager#registerConnectivityDiagnosticsCallback}.
+ * Requesting a network with this method will count toward this limit. If this limit is
+ * exceeded, an exception will be thrown. To avoid hitting this issue and to conserve resources,
+ * make sure to unregister the callbacks with
+ * {@link #unregisterNetworkCallback(NetworkCallback)}.
+ *
* @param networkCallback The {@link NetworkCallback} that the system will call as the
* system default network changes.
* @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+ * @throws RuntimeException if the app already has too many callbacks registered.
*/
@RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
public void registerDefaultNetworkCallback(@NonNull NetworkCallback networkCallback,
@@ -4259,7 +4308,6 @@
* Cannot be null.
*/
public void unregisterNetworkCallback(@NonNull PendingIntent operation) {
- checkPendingIntentNotNull(operation);
releaseNetworkRequest(operation);
}
diff --git a/core/java/android/net/NetworkCapabilities.java b/core/java/android/net/NetworkCapabilities.java
index 0fbffba..ec87091 100644
--- a/core/java/android/net/NetworkCapabilities.java
+++ b/core/java/android/net/NetworkCapabilities.java
@@ -900,9 +900,17 @@
* <p>For NetworkCapability instances being sent from ConnectivityService, this value MUST be
* reset to Process.INVALID_UID unless all the following conditions are met:
*
+ * <p>The caller is the network owner, AND one of the following sets of requirements is met:
+ *
* <ol>
- * <li>The destination app is the network owner
- * <li>The destination app has the ACCESS_FINE_LOCATION permission granted
+ * <li>The described Network is a VPN
+ * </ol>
+ *
+ * <p>OR:
+ *
+ * <ol>
+ * <li>The calling app is the network owner
+ * <li>The calling app has the ACCESS_FINE_LOCATION permission granted
* <li>The user's location toggle is on
* </ol>
*
@@ -928,7 +936,16 @@
/**
* Retrieves the UID of the app that owns this network.
*
- * <p>For user privacy reasons, this field will only be populated if:
+ * <p>For user privacy reasons, this field will only be populated if the following conditions
+ * are met:
+ *
+ * <p>The caller is the network owner, AND one of the following sets of requirements is met:
+ *
+ * <ol>
+ * <li>The described Network is a VPN
+ * </ol>
+ *
+ * <p>OR:
*
* <ol>
* <li>The calling app is the network owner
@@ -936,8 +953,8 @@
* <li>The user's location toggle is on
* </ol>
*
- * Instances of NetworkCapabilities sent to apps without the appropriate permissions will
- * have this field cleared out.
+ * Instances of NetworkCapabilities sent to apps without the appropriate permissions will have
+ * this field cleared out.
*/
public int getOwnerUid() {
return mOwnerUid;
diff --git a/core/java/android/net/RouteInfo.java b/core/java/android/net/RouteInfo.java
index e550f85..9876076 100644
--- a/core/java/android/net/RouteInfo.java
+++ b/core/java/android/net/RouteInfo.java
@@ -26,7 +26,6 @@
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
-import android.util.Pair;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -554,15 +553,45 @@
}
/**
- * A helper class that contains the destination and the gateway in a {@code RouteInfo},
- * used by {@link ConnectivityService#updateRoutes} or
+ * A helper class that contains the destination, the gateway and the interface in a
+ * {@code RouteInfo}, used by {@link ConnectivityService#updateRoutes} or
* {@link LinkProperties#addRoute} to calculate the list to be updated.
+ * {@code RouteInfo} objects with different interfaces are treated as different routes because
+ * *usually* on Android different interfaces use different routing tables, and moving a route
+ * to a new routing table never constitutes an update, but is always a remove and an add.
*
* @hide
*/
- public static class RouteKey extends Pair<IpPrefix, InetAddress> {
- RouteKey(@NonNull IpPrefix destination, @Nullable InetAddress gateway) {
- super(destination, gateway);
+ public static class RouteKey {
+ @NonNull private final IpPrefix mDestination;
+ @Nullable private final InetAddress mGateway;
+ @Nullable private final String mInterface;
+
+ RouteKey(@NonNull IpPrefix destination, @Nullable InetAddress gateway,
+ @Nullable String iface) {
+ mDestination = destination;
+ mGateway = gateway;
+ mInterface = iface;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof RouteKey)) {
+ return false;
+ }
+ RouteKey p = (RouteKey) o;
+ // No need to do anything special for scoped addresses. Inet6Address#equals does not
+ // consider the scope ID, but the netd route IPCs (e.g., INetd#networkAddRouteParcel)
+ // and the kernel ignore scoped addresses both in the prefix and in the nexthop and only
+ // look at RTA_OIF.
+ return Objects.equals(p.mDestination, mDestination)
+ && Objects.equals(p.mGateway, mGateway)
+ && Objects.equals(p.mInterface, mInterface);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDestination, mGateway, mInterface);
}
}
@@ -574,7 +603,7 @@
*/
@NonNull
public RouteKey getRouteKey() {
- return new RouteKey(mDestination, mGateway);
+ return new RouteKey(mDestination, mGateway, mInterface);
}
/**
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 8331105..52cc8f6 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -1699,6 +1699,12 @@
return newNc;
}
+ // Allow VPNs to see ownership of their own VPN networks - not location sensitive.
+ if (nc.hasTransport(TRANSPORT_VPN)) {
+ // Owner UIDs already checked above. No need to re-check.
+ return newNc;
+ }
+
Binder.withCleanCallingIdentity(
() -> {
if (!mLocationPermissionChecker.checkLocationPermission(
@@ -2474,10 +2480,12 @@
final List<NetworkDiagnostics> netDiags = new ArrayList<NetworkDiagnostics>();
final long DIAG_TIME_MS = 5000;
for (NetworkAgentInfo nai : networksSortedById()) {
+ PrivateDnsConfig privateDnsCfg = mDnsManager.getPrivateDnsConfig(nai.network);
// Start gathering diagnostic information.
netDiags.add(new NetworkDiagnostics(
nai.network,
new LinkProperties(nai.linkProperties), // Must be a copy.
+ privateDnsCfg,
DIAG_TIME_MS));
}
@@ -6444,7 +6452,7 @@
final boolean shouldFilter = requiresVpnIsolation(nai, newNc, nai.linkProperties);
final String iface = nai.linkProperties.getInterfaceName();
// For VPN uid interface filtering, old ranges need to be removed before new ranges can
- // be added, due to the range being expanded and stored as invidiual UIDs. For example
+ // be added, due to the range being expanded and stored as individual UIDs. For example
// the UIDs might be updated from [0, 99999] to ([0, 10012], [10014, 99999]) which means
// prevRanges = [0, 99999] while newRanges = [0, 10012], [10014, 99999]. If prevRanges
// were added first and then newRanges got removed later, there would be only one uid
diff --git a/services/core/java/com/android/server/connectivity/DnsManager.java b/services/core/java/com/android/server/connectivity/DnsManager.java
index 506c8e3..cf6a7f6 100644
--- a/services/core/java/com/android/server/connectivity/DnsManager.java
+++ b/services/core/java/com/android/server/connectivity/DnsManager.java
@@ -57,6 +57,7 @@
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@@ -64,7 +65,9 @@
* Encapsulate the management of DNS settings for networks.
*
* This class it NOT designed for concurrent access. Furthermore, all non-static
- * methods MUST be called from ConnectivityService's thread.
+ * methods MUST be called from ConnectivityService's thread. However, an exceptional
+ * case is getPrivateDnsConfig(Network) which is exclusively for
+ * ConnectivityService#dumpNetworkDiagnostics() on a random binder thread.
*
* [ Private DNS ]
* The code handling Private DNS is spread across several components, but this
@@ -236,8 +239,8 @@
private final ContentResolver mContentResolver;
private final IDnsResolver mDnsResolver;
private final MockableSystemProperties mSystemProperties;
- // TODO: Replace these Maps with SparseArrays.
- private final Map<Integer, PrivateDnsConfig> mPrivateDnsMap;
+ private final ConcurrentHashMap<Integer, PrivateDnsConfig> mPrivateDnsMap;
+ // TODO: Replace the Map with SparseArrays.
private final Map<Integer, PrivateDnsValidationStatuses> mPrivateDnsValidationMap;
private final Map<Integer, LinkProperties> mLinkPropertiesMap;
private final Map<Integer, int[]> mTransportsMap;
@@ -247,15 +250,13 @@
private int mSuccessThreshold;
private int mMinSamples;
private int mMaxSamples;
- private String mPrivateDnsMode;
- private String mPrivateDnsSpecifier;
public DnsManager(Context ctx, IDnsResolver dnsResolver, MockableSystemProperties sp) {
mContext = ctx;
mContentResolver = mContext.getContentResolver();
mDnsResolver = dnsResolver;
mSystemProperties = sp;
- mPrivateDnsMap = new HashMap<>();
+ mPrivateDnsMap = new ConcurrentHashMap<>();
mPrivateDnsValidationMap = new HashMap<>();
mLinkPropertiesMap = new HashMap<>();
mTransportsMap = new HashMap<>();
@@ -275,6 +276,12 @@
mLinkPropertiesMap.remove(network.netId);
}
+ // This is exclusively called by ConnectivityService#dumpNetworkDiagnostics() which
+ // is not on the ConnectivityService handler thread.
+ public PrivateDnsConfig getPrivateDnsConfig(@NonNull Network network) {
+ return mPrivateDnsMap.getOrDefault(network.netId, PRIVATE_DNS_OFF);
+ }
+
public PrivateDnsConfig updatePrivateDns(Network network, PrivateDnsConfig cfg) {
Slog.w(TAG, "updatePrivateDns(" + network + ", " + cfg + ")");
return (cfg != null)
diff --git a/services/core/java/com/android/server/connectivity/NetworkDiagnostics.java b/services/core/java/com/android/server/connectivity/NetworkDiagnostics.java
index a1a8e35..49c16ad 100644
--- a/services/core/java/com/android/server/connectivity/NetworkDiagnostics.java
+++ b/services/core/java/com/android/server/connectivity/NetworkDiagnostics.java
@@ -18,12 +18,15 @@
import static android.system.OsConstants.*;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkUtils;
import android.net.RouteInfo;
import android.net.TrafficStats;
+import android.net.shared.PrivateDnsConfig;
import android.net.util.NetworkConstants;
import android.os.SystemClock;
import android.system.ErrnoException;
@@ -38,6 +41,8 @@
import libcore.io.IoUtils;
import java.io.Closeable;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InterruptedIOException;
@@ -52,6 +57,7 @@
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -59,6 +65,12 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
/**
* NetworkDiagnostics
*
@@ -100,6 +112,7 @@
private final Network mNetwork;
private final LinkProperties mLinkProperties;
+ private final PrivateDnsConfig mPrivateDnsCfg;
private final Integer mInterfaceIndex;
private final long mTimeoutMs;
@@ -163,12 +176,15 @@
private final Map<Pair<InetAddress, InetAddress>, Measurement> mExplicitSourceIcmpChecks =
new HashMap<>();
private final Map<InetAddress, Measurement> mDnsUdpChecks = new HashMap<>();
+ private final Map<InetAddress, Measurement> mDnsTlsChecks = new HashMap<>();
private final String mDescription;
- public NetworkDiagnostics(Network network, LinkProperties lp, long timeoutMs) {
+ public NetworkDiagnostics(Network network, LinkProperties lp,
+ @NonNull PrivateDnsConfig privateDnsCfg, long timeoutMs) {
mNetwork = network;
mLinkProperties = lp;
+ mPrivateDnsCfg = privateDnsCfg;
mInterfaceIndex = getInterfaceIndex(mLinkProperties.getInterfaceName());
mTimeoutMs = timeoutMs;
mStartTime = now();
@@ -199,8 +215,22 @@
}
}
for (InetAddress nameserver : mLinkProperties.getDnsServers()) {
- prepareIcmpMeasurement(nameserver);
- prepareDnsMeasurement(nameserver);
+ prepareIcmpMeasurement(nameserver);
+ prepareDnsMeasurement(nameserver);
+
+ // Unlike the DnsResolver which doesn't do certificate validation in opportunistic mode,
+ // DoT probes to the DNS servers will fail if certificate validation fails.
+ prepareDnsTlsMeasurement(null /* hostname */, nameserver);
+ }
+
+ for (InetAddress tlsNameserver : mPrivateDnsCfg.ips) {
+ // Reachability check is necessary since when resolving the strict mode hostname,
+ // NetworkMonitor always queries for both A and AAAA records, even if the network
+ // is IPv4-only or IPv6-only.
+ if (mLinkProperties.isReachable(tlsNameserver)) {
+ // If there are IPs, there must have been a name that resolved to them.
+ prepareDnsTlsMeasurement(mPrivateDnsCfg.hostname, tlsNameserver);
+ }
}
mCountDownLatch = new CountDownLatch(totalMeasurementCount());
@@ -222,6 +252,15 @@
}
}
+ private static String socketAddressToString(@NonNull SocketAddress sockAddr) {
+ // The default toString() implementation is not the prettiest.
+ InetSocketAddress inetSockAddr = (InetSocketAddress) sockAddr;
+ InetAddress localAddr = inetSockAddr.getAddress();
+ return String.format(
+ (localAddr instanceof Inet6Address ? "[%s]:%d" : "%s:%d"),
+ localAddr.getHostAddress(), inetSockAddr.getPort());
+ }
+
private void prepareIcmpMeasurement(InetAddress target) {
if (!mIcmpChecks.containsKey(target)) {
Measurement measurement = new Measurement();
@@ -252,8 +291,19 @@
}
}
+ private void prepareDnsTlsMeasurement(@Nullable String hostname, @NonNull InetAddress target) {
+ // This might overwrite an existing entry in mDnsTlsChecks, because |target| can be an IP
+ // address configured by the network as well as an IP address learned by resolving the
+ // strict mode DNS hostname. If the entry is overwritten, the overwritten measurement
+ // thread will not execute.
+ Measurement measurement = new Measurement();
+ measurement.thread = new Thread(new DnsTlsCheck(hostname, target, measurement));
+ mDnsTlsChecks.put(target, measurement);
+ }
+
private int totalMeasurementCount() {
- return mIcmpChecks.size() + mExplicitSourceIcmpChecks.size() + mDnsUdpChecks.size();
+ return mIcmpChecks.size() + mExplicitSourceIcmpChecks.size() + mDnsUdpChecks.size()
+ + mDnsTlsChecks.size();
}
private void startMeasurements() {
@@ -266,6 +316,9 @@
for (Measurement measurement : mDnsUdpChecks.values()) {
measurement.thread.start();
}
+ for (Measurement measurement : mDnsTlsChecks.values()) {
+ measurement.thread.start();
+ }
}
public void waitForMeasurements() {
@@ -297,6 +350,11 @@
measurements.add(entry.getValue());
}
}
+ for (Map.Entry<InetAddress, Measurement> entry : mDnsTlsChecks.entrySet()) {
+ if (entry.getKey() instanceof Inet4Address) {
+ measurements.add(entry.getValue());
+ }
+ }
// IPv6 measurements second.
for (Map.Entry<InetAddress, Measurement> entry : mIcmpChecks.entrySet()) {
@@ -315,6 +373,11 @@
measurements.add(entry.getValue());
}
}
+ for (Map.Entry<InetAddress, Measurement> entry : mDnsTlsChecks.entrySet()) {
+ if (entry.getKey() instanceof Inet6Address) {
+ measurements.add(entry.getValue());
+ }
+ }
return measurements;
}
@@ -387,6 +450,8 @@
try {
mFileDescriptor = Os.socket(mAddressFamily, sockType, protocol);
} finally {
+ // TODO: The tag should remain set until all traffic is sent and received.
+ // Consider tagging the socket after the measurement thread is started.
TrafficStats.setThreadStatsTag(oldTag);
}
// Setting SNDTIMEO is purely for defensive purposes.
@@ -403,13 +468,12 @@
mSocketAddress = Os.getsockname(mFileDescriptor);
}
- protected String getSocketAddressString() {
- // The default toString() implementation is not the prettiest.
- InetSocketAddress inetSockAddr = (InetSocketAddress) mSocketAddress;
- InetAddress localAddr = inetSockAddr.getAddress();
- return String.format(
- (localAddr instanceof Inet6Address ? "[%s]:%d" : "%s:%d"),
- localAddr.getHostAddress(), inetSockAddr.getPort());
+ protected boolean ensureMeasurementNecessary() {
+ if (mMeasurement.finishTime == 0) return false;
+
+ // Countdown latch was not decremented when the measurement failed during setup.
+ mCountDownLatch.countDown();
+ return true;
}
@Override
@@ -448,13 +512,7 @@
@Override
public void run() {
- // Check if this measurement has already failed during setup.
- if (mMeasurement.finishTime > 0) {
- // If the measurement failed during construction it didn't
- // decrement the countdown latch; do so here.
- mCountDownLatch.countDown();
- return;
- }
+ if (ensureMeasurementNecessary()) return;
try {
setupSocket(SOCK_DGRAM, mProtocol, TIMEOUT_SEND, TIMEOUT_RECV, 0);
@@ -462,7 +520,7 @@
mMeasurement.recordFailure(e.toString());
return;
}
- mMeasurement.description += " src{" + getSocketAddressString() + "}";
+ mMeasurement.description += " src{" + socketAddressToString(mSocketAddress) + "}";
// Build a trivial ICMP packet.
final byte[] icmpPacket = {
@@ -507,10 +565,10 @@
private static final int RR_TYPE_AAAA = 28;
private static final int PACKET_BUFSIZE = 512;
- private final Random mRandom = new Random();
+ protected final Random mRandom = new Random();
// Should be static, but the compiler mocks our puny, human attempts at reason.
- private String responseCodeStr(int rcode) {
+ protected String responseCodeStr(int rcode) {
try {
return DnsResponseCode.values()[rcode].toString();
} catch (IndexOutOfBoundsException e) {
@@ -518,7 +576,7 @@
}
}
- private final int mQueryType;
+ protected final int mQueryType;
public DnsUdpCheck(InetAddress target, Measurement measurement) {
super(target, measurement);
@@ -535,13 +593,7 @@
@Override
public void run() {
- // Check if this measurement has already failed during setup.
- if (mMeasurement.finishTime > 0) {
- // If the measurement failed during construction it didn't
- // decrement the countdown latch; do so here.
- mCountDownLatch.countDown();
- return;
- }
+ if (ensureMeasurementNecessary()) return;
try {
setupSocket(SOCK_DGRAM, IPPROTO_UDP, TIMEOUT_SEND, TIMEOUT_RECV,
@@ -550,12 +602,10 @@
mMeasurement.recordFailure(e.toString());
return;
}
- mMeasurement.description += " src{" + getSocketAddressString() + "}";
// This needs to be fixed length so it can be dropped into the pre-canned packet.
final String sixRandomDigits = String.valueOf(mRandom.nextInt(900000) + 100000);
- mMeasurement.description += " qtype{" + mQueryType + "}"
- + " qname{" + sixRandomDigits + "-android-ds.metric.gstatic.com}";
+ appendDnsToMeasurementDescription(sixRandomDigits, mSocketAddress);
// Build a trivial DNS packet.
final byte[] dnsPacket = getDnsQueryPacket(sixRandomDigits);
@@ -592,7 +642,7 @@
close();
}
- private byte[] getDnsQueryPacket(String sixRandomDigits) {
+ protected byte[] getDnsQueryPacket(String sixRandomDigits) {
byte[] rnd = sixRandomDigits.getBytes(StandardCharsets.US_ASCII);
return new byte[] {
(byte) mRandom.nextInt(), (byte) mRandom.nextInt(), // [0-1] query ID
@@ -611,5 +661,97 @@
0, 1 // QCLASS, set to 1 = IN (Internet)
};
}
+
+ protected void appendDnsToMeasurementDescription(
+ String sixRandomDigits, SocketAddress sockAddr) {
+ mMeasurement.description += " src{" + socketAddressToString(sockAddr) + "}"
+ + " qtype{" + mQueryType + "}"
+ + " qname{" + sixRandomDigits + "-android-ds.metric.gstatic.com}";
+ }
+ }
+
+ // TODO: Have it inherited from SimpleSocketCheck, and separate common DNS helpers out of
+ // DnsUdpCheck.
+ private class DnsTlsCheck extends DnsUdpCheck {
+ private static final int TCP_CONNECT_TIMEOUT_MS = 2500;
+ private static final int TCP_TIMEOUT_MS = 2000;
+ private static final int DNS_TLS_PORT = 853;
+ private static final int DNS_HEADER_SIZE = 12;
+
+ private final String mHostname;
+
+ public DnsTlsCheck(@Nullable String hostname, @NonNull InetAddress target,
+ @NonNull Measurement measurement) {
+ super(target, measurement);
+
+ mHostname = hostname;
+ mMeasurement.description = "DNS TLS dst{" + mTarget.getHostAddress() + "} hostname{"
+ + TextUtils.emptyIfNull(mHostname) + "}";
+ }
+
+ private SSLSocket setupSSLSocket() throws IOException {
+ // A TrustManager will be created and initialized with a KeyStore containing system
+ // CaCerts. During SSL handshake, it will be used to validate the certificates from
+ // the server.
+ SSLSocket sslSocket = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
+ sslSocket.setSoTimeout(TCP_TIMEOUT_MS);
+
+ if (!TextUtils.isEmpty(mHostname)) {
+ // Set SNI.
+ final List<SNIServerName> names =
+ Collections.singletonList(new SNIHostName(mHostname));
+ SSLParameters params = sslSocket.getSSLParameters();
+ params.setServerNames(names);
+ sslSocket.setSSLParameters(params);
+ }
+
+ mNetwork.bindSocket(sslSocket);
+ return sslSocket;
+ }
+
+ private void sendDoTProbe(@Nullable SSLSocket sslSocket) throws IOException {
+ final String sixRandomDigits = String.valueOf(mRandom.nextInt(900000) + 100000);
+ final byte[] dnsPacket = getDnsQueryPacket(sixRandomDigits);
+
+ mMeasurement.startTime = now();
+ sslSocket.connect(new InetSocketAddress(mTarget, DNS_TLS_PORT), TCP_CONNECT_TIMEOUT_MS);
+
+ // Synchronous call waiting for the TLS handshake complete.
+ sslSocket.startHandshake();
+ appendDnsToMeasurementDescription(sixRandomDigits, sslSocket.getLocalSocketAddress());
+
+ final DataOutputStream output = new DataOutputStream(sslSocket.getOutputStream());
+ output.writeShort(dnsPacket.length);
+ output.write(dnsPacket, 0, dnsPacket.length);
+
+ final DataInputStream input = new DataInputStream(sslSocket.getInputStream());
+ final int replyLength = Short.toUnsignedInt(input.readShort());
+ final byte[] reply = new byte[replyLength];
+ int bytesRead = 0;
+ while (bytesRead < replyLength) {
+ bytesRead += input.read(reply, bytesRead, replyLength - bytesRead);
+ }
+
+ if (bytesRead > DNS_HEADER_SIZE && bytesRead == replyLength) {
+ mMeasurement.recordSuccess("1/1 " + responseCodeStr((int) (reply[3]) & 0x0f));
+ } else {
+ mMeasurement.recordFailure("1/1 Read " + bytesRead + " bytes while expected to be "
+ + replyLength + " bytes");
+ }
+ }
+
+ @Override
+ public void run() {
+ if (ensureMeasurementNecessary()) return;
+
+ // No need to restore the tag, since this thread is only used for this measurement.
+ TrafficStats.getAndSetThreadStatsTag(TrafficStatsConstants.TAG_SYSTEM_PROBE);
+
+ try (SSLSocket sslSocket = setupSSLSocket()) {
+ sendDoTProbe(sslSocket);
+ } catch (IOException e) {
+ mMeasurement.recordFailure(e.toString());
+ }
+ }
}
}
diff --git a/tests/net/common/java/android/net/LinkPropertiesTest.java b/tests/net/common/java/android/net/LinkPropertiesTest.java
index 0fc9be3..6eba62e 100644
--- a/tests/net/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/net/common/java/android/net/LinkPropertiesTest.java
@@ -16,6 +16,8 @@
package android.net;
+import static android.net.RouteInfo.RTN_THROW;
+import static android.net.RouteInfo.RTN_UNICAST;
import static android.net.RouteInfo.RTN_UNREACHABLE;
import static com.android.testutils.ParcelUtilsKt.assertParcelSane;
@@ -1282,4 +1284,20 @@
assertTrue(lp.hasIpv6UnreachableDefaultRoute());
assertFalse(lp.hasIpv4UnreachableDefaultRoute());
}
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testRouteAddWithSameKey() throws Exception {
+ LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName("wlan0");
+ final IpPrefix v6 = new IpPrefix("64:ff9b::/96");
+ lp.addRoute(new RouteInfo(v6, address("fe80::1"), "wlan0", RTN_UNICAST, 1280));
+ assertEquals(1, lp.getRoutes().size());
+ lp.addRoute(new RouteInfo(v6, address("fe80::1"), "wlan0", RTN_UNICAST, 1500));
+ assertEquals(1, lp.getRoutes().size());
+ final IpPrefix v4 = new IpPrefix("192.0.2.128/25");
+ lp.addRoute(new RouteInfo(v4, address("192.0.2.1"), "wlan0", RTN_UNICAST, 1460));
+ assertEquals(2, lp.getRoutes().size());
+ lp.addRoute(new RouteInfo(v4, address("192.0.2.1"), "wlan0", RTN_THROW, 1460));
+ assertEquals(2, lp.getRoutes().size());
+ }
}
diff --git a/tests/net/common/java/android/net/RouteInfoTest.java b/tests/net/common/java/android/net/RouteInfoTest.java
index 8204b49..60cac0b 100644
--- a/tests/net/common/java/android/net/RouteInfoTest.java
+++ b/tests/net/common/java/android/net/RouteInfoTest.java
@@ -25,6 +25,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -56,7 +57,7 @@
private static final int INVALID_ROUTE_TYPE = -1;
private InetAddress Address(String addr) {
- return InetAddress.parseNumericAddress(addr);
+ return InetAddresses.parseNumericAddress(addr);
}
private IpPrefix Prefix(String prefix) {
@@ -391,4 +392,43 @@
r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0");
assertEquals(0, r.getMtu());
}
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testRouteKey() {
+ RouteInfo.RouteKey k1, k2;
+ // Only prefix, null gateway and null interface
+ k1 = new RouteInfo(Prefix("2001:db8::/128"), null).getRouteKey();
+ k2 = new RouteInfo(Prefix("2001:db8::/128"), null).getRouteKey();
+ assertEquals(k1, k2);
+ assertEquals(k1.hashCode(), k2.hashCode());
+
+ // With prefix, gateway and interface. Type and MTU does not affect RouteKey equality
+ k1 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0",
+ RTN_UNREACHABLE, 1450).getRouteKey();
+ k2 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0",
+ RouteInfo.RTN_UNICAST, 1400).getRouteKey();
+ assertEquals(k1, k2);
+ assertEquals(k1.hashCode(), k2.hashCode());
+
+ // Different scope IDs are ignored by the kernel, so we consider them equal here too.
+ k1 = new RouteInfo(Prefix("2001:db8::/64"), Address("fe80::1%1"), "wlan0").getRouteKey();
+ k2 = new RouteInfo(Prefix("2001:db8::/64"), Address("fe80::1%2"), "wlan0").getRouteKey();
+ assertEquals(k1, k2);
+ assertEquals(k1.hashCode(), k2.hashCode());
+
+ // Different prefix
+ k1 = new RouteInfo(Prefix("192.0.2.0/24"), null).getRouteKey();
+ k2 = new RouteInfo(Prefix("192.0.3.0/24"), null).getRouteKey();
+ assertNotEquals(k1, k2);
+
+ // Different gateway
+ k1 = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::1"), null).getRouteKey();
+ k2 = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::2"), null).getRouteKey();
+ assertNotEquals(k1, k2);
+
+ // Different interface
+ k1 = new RouteInfo(Prefix("ff02::1/128"), null, "tun0").getRouteKey();
+ k2 = new RouteInfo(Prefix("ff02::1/128"), null, "tun1").getRouteKey();
+ assertNotEquals(k1, k2);
+ }
}
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index ea4982e..bc85374 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -100,6 +100,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.any;
@@ -164,6 +165,8 @@
import android.net.LinkProperties;
import android.net.MatchAllNetworkSpecifier;
import android.net.Network;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
import android.net.NetworkCapabilities;
import android.net.NetworkFactory;
import android.net.NetworkInfo;
@@ -6808,6 +6811,30 @@
assertEquals(wifiLp, mService.getActiveLinkProperties());
}
+ @Test
+ public void testLegacyExtraInfoSentToNetworkMonitor() throws Exception {
+ class TestNetworkAgent extends NetworkAgent {
+ TestNetworkAgent(Context context, Looper looper, NetworkAgentConfig config) {
+ super(context, looper, "MockAgent", new NetworkCapabilities(),
+ new LinkProperties(), 40 , config, null /* provider */);
+ }
+ }
+ final NetworkAgent naNoExtraInfo = new TestNetworkAgent(
+ mServiceContext, mCsHandlerThread.getLooper(), new NetworkAgentConfig());
+ naNoExtraInfo.register();
+ verify(mNetworkStack).makeNetworkMonitor(any(), isNull(String.class), any());
+ naNoExtraInfo.unregister();
+
+ reset(mNetworkStack);
+ final NetworkAgentConfig config =
+ new NetworkAgentConfig.Builder().setLegacyExtraInfo("legacyinfo").build();
+ final NetworkAgent naExtraInfo = new TestNetworkAgent(
+ mServiceContext, mCsHandlerThread.getLooper(), config);
+ naExtraInfo.register();
+ verify(mNetworkStack).makeNetworkMonitor(any(), eq("legacyinfo"), any());
+ naExtraInfo.unregister();
+ }
+
private void setupLocationPermissions(
int targetSdk, boolean locationToggle, String op, String perm) throws Exception {
final ApplicationInfo applicationInfo = new ApplicationInfo();
diff --git a/tests/net/java/com/android/server/connectivity/DnsManagerTest.java b/tests/net/java/com/android/server/connectivity/DnsManagerTest.java
index 0a603b8..508b5cd 100644
--- a/tests/net/java/com/android/server/connectivity/DnsManagerTest.java
+++ b/tests/net/java/com/android/server/connectivity/DnsManagerTest.java
@@ -62,6 +62,8 @@
import com.android.internal.util.MessageUtils;
import com.android.internal.util.test.FakeSettingsProvider;
+import libcore.net.InetAddressUtils;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -103,7 +105,8 @@
@NonNull ResolverOptionsParcel expected) {
assertEquals(actual.hosts, expected.hosts);
assertEquals(actual.tcMode, expected.tcMode);
- assertFieldCountEquals(2, ResolverOptionsParcel.class);
+ assertEquals(actual.enforceDnsUid, expected.enforceDnsUid);
+ assertFieldCountEquals(3, ResolverOptionsParcel.class);
}
private void assertResolverParamsEquals(@NonNull ResolverParamsParcel actual,
@@ -379,4 +382,49 @@
assertEquals(name, dnsTransTypes.get(i));
}
}
+
+ @Test
+ public void testGetPrivateDnsConfigForNetwork() throws Exception {
+ final Network network = new Network(TEST_NETID);
+ final InetAddress dnsAddr = InetAddressUtils.parseNumericAddress("3.3.3.3");
+ final InetAddress[] tlsAddrs = new InetAddress[]{
+ InetAddressUtils.parseNumericAddress("6.6.6.6"),
+ InetAddressUtils.parseNumericAddress("2001:db8:66:66::1")
+ };
+ final String tlsName = "strictmode.com";
+ LinkProperties lp = new LinkProperties();
+ lp.addDnsServer(dnsAddr);
+
+ // The PrivateDnsConfig map is empty, so the default PRIVATE_DNS_OFF is returned.
+ PrivateDnsConfig privateDnsCfg = mDnsManager.getPrivateDnsConfig(network);
+ assertFalse(privateDnsCfg.useTls);
+ assertEquals("", privateDnsCfg.hostname);
+ assertEquals(new InetAddress[0], privateDnsCfg.ips);
+
+ // An entry with default PrivateDnsConfig is added to the PrivateDnsConfig map.
+ mDnsManager.updatePrivateDns(network, mDnsManager.getPrivateDnsConfig());
+ mDnsManager.noteDnsServersForNetwork(TEST_NETID, lp);
+ mDnsManager.updatePrivateDnsValidation(
+ new DnsManager.PrivateDnsValidationUpdate(TEST_NETID, dnsAddr, "", true));
+ mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp);
+ privateDnsCfg = mDnsManager.getPrivateDnsConfig(network);
+ assertTrue(privateDnsCfg.useTls);
+ assertEquals("", privateDnsCfg.hostname);
+ assertEquals(new InetAddress[0], privateDnsCfg.ips);
+
+ // The original entry is overwritten by a new PrivateDnsConfig.
+ mDnsManager.updatePrivateDns(network, new PrivateDnsConfig(tlsName, tlsAddrs));
+ mDnsManager.updatePrivateDnsStatus(TEST_NETID, lp);
+ privateDnsCfg = mDnsManager.getPrivateDnsConfig(network);
+ assertTrue(privateDnsCfg.useTls);
+ assertEquals(tlsName, privateDnsCfg.hostname);
+ assertEquals(tlsAddrs, privateDnsCfg.ips);
+
+ // The network is removed, so the PrivateDnsConfig map becomes empty again.
+ mDnsManager.removeNetwork(network);
+ privateDnsCfg = mDnsManager.getPrivateDnsConfig(network);
+ assertFalse(privateDnsCfg.useTls);
+ assertEquals("", privateDnsCfg.hostname);
+ assertEquals(new InetAddress[0], privateDnsCfg.ips);
+ }
}
diff --git a/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java
index a1bb0d5..1307a84 100644
--- a/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/net/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -41,6 +41,7 @@
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStats.UID_ALL;
import static android.net.NetworkStatsHistory.FIELD_ALL;
+import static android.net.NetworkTemplate.NETWORK_TYPE_ALL;
import static android.net.NetworkTemplate.buildTemplateMobileAll;
import static android.net.NetworkTemplate.buildTemplateMobileWithRatType;
import static android.net.NetworkTemplate.buildTemplateWifiWildcard;
@@ -62,6 +63,7 @@
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -71,6 +73,7 @@
import android.app.usage.NetworkStatsManager;
import android.content.Context;
import android.content.Intent;
+import android.database.ContentObserver;
import android.net.DataUsageRequest;
import android.net.INetworkManagementEventObserver;
import android.net.INetworkStatsSession;
@@ -94,6 +97,7 @@
import android.os.Messenger;
import android.os.PowerManager;
import android.os.SimpleClock;
+import android.provider.Settings;
import android.telephony.TelephonyManager;
import androidx.test.InstrumentationRegistry;
@@ -173,6 +177,8 @@
private NetworkStatsService mService;
private INetworkStatsSession mSession;
private INetworkManagementEventObserver mNetworkObserver;
+ private ContentObserver mContentObserver;
+ private Handler mHandler;
private final Clock mClock = new SimpleClock(ZoneOffset.UTC) {
@Override
@@ -212,6 +218,12 @@
mService.systemReady();
// Verify that system ready fetches realtime stats
verify(mStatsFactory).readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL);
+ // Wait for posting onChange() event to handler thread and verify that when system ready,
+ // start monitoring data usage per RAT type because the settings value is mock as false
+ // by default in expectSettings().
+ waitForIdle();
+ verify(mNetworkStatsSubscriptionsMonitor).start();
+ reset(mNetworkStatsSubscriptionsMonitor);
mSession = mService.openSession();
assertNotNull("openSession() failed", mSession);
@@ -233,11 +245,19 @@
@Override
public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(
- @NonNull Context context, @NonNull Executor executor,
+ @NonNull Context context, @NonNull Looper looper, @NonNull Executor executor,
@NonNull NetworkStatsService service) {
return mNetworkStatsSubscriptionsMonitor;
}
+
+ @Override
+ public ContentObserver makeContentObserver(Handler handler,
+ NetworkStatsSettings settings, NetworkStatsSubscriptionsMonitor monitor) {
+ mHandler = handler;
+ return mContentObserver = super.makeContentObserver(handler, settings, monitor);
+ }
+
};
}
@@ -1191,6 +1211,99 @@
provider.expectOnSetAlert(MB_IN_BYTES);
}
+ private void setCombineSubtypeEnabled(boolean enable) {
+ when(mSettings.getCombineSubtypeEnabled()).thenReturn(enable);
+ mHandler.post(() -> mContentObserver.onChange(false, Settings.Global
+ .getUriFor(Settings.Global.NETSTATS_COMBINE_SUBTYPE_ENABLED)));
+ waitForIdle();
+ if (enable) {
+ verify(mNetworkStatsSubscriptionsMonitor).stop();
+ } else {
+ verify(mNetworkStatsSubscriptionsMonitor).start();
+ }
+ }
+
+ @Test
+ public void testDynamicWatchForNetworkRatTypeChanges() throws Exception {
+ // Build 3G template, type unknown template to get stats while network type is unknown
+ // and type all template to get the sum of all network type stats.
+ final NetworkTemplate template3g =
+ buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UMTS);
+ final NetworkTemplate templateUnknown =
+ buildTemplateMobileWithRatType(null, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ final NetworkTemplate templateAll =
+ buildTemplateMobileWithRatType(null, NETWORK_TYPE_ALL);
+ final NetworkState[] states = new NetworkState[]{buildMobile3gState(IMSI_1)};
+
+ expectNetworkStatsSummary(buildEmptyStats());
+ expectNetworkStatsUidDetail(buildEmptyStats());
+
+ // 3G network comes online.
+ setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS);
+ mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states),
+ new VpnInfo[0]);
+
+ // Create some traffic.
+ incrementCurrentTime(MINUTE_IN_MILLIS);
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+ .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
+ 12L, 18L, 14L, 1L, 0L)));
+ forcePollAndWaitForIdle();
+
+ // Since CombineSubtypeEnabled is false by default in unit test, the generated traffic
+ // will be split by RAT type. Verify 3G templates gets stats, while template with unknown
+ // RAT type gets nothing, and template with NETWORK_TYPE_ALL gets all stats.
+ assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0);
+ assertUidTotal(templateUnknown, UID_RED, 0L, 0L, 0L, 0L, 0);
+ assertUidTotal(templateAll, UID_RED, 12L, 18L, 14L, 1L, 0);
+
+ // Stop monitoring data usage per RAT type changes NetworkStatsService records data
+ // to {@link TelephonyManager#NETWORK_TYPE_UNKNOWN}.
+ setCombineSubtypeEnabled(true);
+
+ // Call handleOnCollapsedRatTypeChanged manually to simulate the callback fired
+ // when stopping monitor, this is needed by NetworkStatsService to trigger updateIfaces.
+ mService.handleOnCollapsedRatTypeChanged();
+ HandlerUtilsKt.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
+ // Create some traffic.
+ incrementCurrentTime(MINUTE_IN_MILLIS);
+ // Append more traffic on existing snapshot.
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+ .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
+ 12L + 4L, 18L + 4L, 14L + 3L, 1L + 1L, 0L))
+ .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE,
+ 35L, 29L, 7L, 11L, 1L)));
+ forcePollAndWaitForIdle();
+
+ // Verify 3G counters do not increase, while template with unknown RAT type gets new
+ // traffic and template with NETWORK_TYPE_ALL gets all stats.
+ assertUidTotal(template3g, UID_RED, 12L, 18L, 14L, 1L, 0);
+ assertUidTotal(templateUnknown, UID_RED, 4L + 35L, 4L + 29L, 3L + 7L, 1L + 11L, 1);
+ assertUidTotal(templateAll, UID_RED, 16L + 35L, 22L + 29L, 17L + 7L, 2L + 11L, 1);
+
+ // Start monitoring data usage per RAT type changes and NetworkStatsService records data
+ // by a granular subtype representative of the actual subtype
+ setCombineSubtypeEnabled(false);
+
+ mService.handleOnCollapsedRatTypeChanged();
+ HandlerUtilsKt.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
+ // Create some traffic.
+ incrementCurrentTime(MINUTE_IN_MILLIS);
+ // Append more traffic on existing snapshot.
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+ .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE,
+ 22L, 26L, 19L, 5L, 0L))
+ .addEntry(new NetworkStats.Entry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE,
+ 35L, 29L, 7L, 11L, 1L)));
+ forcePollAndWaitForIdle();
+
+ // Verify traffic is split by RAT type, no increase on template with unknown RAT type
+ // and template with NETWORK_TYPE_ALL gets all stats.
+ assertUidTotal(template3g, UID_RED, 6L + 12L , 4L + 18L, 2L + 14L, 3L + 1L, 0);
+ assertUidTotal(templateUnknown, UID_RED, 4L + 35L, 4L + 29L, 3L + 7L, 1L + 11L, 1);
+ assertUidTotal(templateAll, UID_RED, 22L + 35L, 26L + 29L, 19L + 7L, 5L + 11L, 1);
+ }
+
private static File getBaseDir(File statsDir) {
File baseDir = new File(statsDir, "netstats");
baseDir.mkdirs();
@@ -1403,6 +1516,10 @@
private void forcePollAndWaitForIdle() {
mServiceContext.sendBroadcast(new Intent(ACTION_NETWORK_STATS_POLL));
+ waitForIdle();
+ }
+
+ private void waitForIdle() {
HandlerUtilsKt.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
}
diff --git a/tests/net/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java b/tests/net/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
new file mode 100644
index 0000000..c813269
--- /dev/null
+++ b/tests/net/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.test.TestLooper;
+import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.util.CollectionUtils;
+import com.android.server.net.NetworkStatsSubscriptionsMonitor.RatTypeListener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+@RunWith(JUnit4.class)
+public final class NetworkStatsSubscriptionsMonitorTest {
+ private static final int TEST_SUBID1 = 3;
+ private static final int TEST_SUBID2 = 5;
+ private static final String TEST_IMSI1 = "466921234567890";
+ private static final String TEST_IMSI2 = "466920987654321";
+ private static final String TEST_IMSI3 = "466929999999999";
+
+ @Mock private Context mContext;
+ @Mock private PhoneStateListener mPhoneStateListener;
+ @Mock private SubscriptionManager mSubscriptionManager;
+ @Mock private TelephonyManager mTelephonyManager;
+ @Mock private NetworkStatsSubscriptionsMonitor.Delegate mDelegate;
+ private final List<Integer> mTestSubList = new ArrayList<>();
+
+ private final Executor mExecutor = Executors.newSingleThreadExecutor();
+ private NetworkStatsSubscriptionsMonitor mMonitor;
+ private TestLooper mTestLooper = new TestLooper();
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager);
+
+ when(mContext.getSystemService(eq(Context.TELEPHONY_SUBSCRIPTION_SERVICE)))
+ .thenReturn(mSubscriptionManager);
+ when(mContext.getSystemService(eq(Context.TELEPHONY_SERVICE)))
+ .thenReturn(mTelephonyManager);
+
+ mMonitor = new NetworkStatsSubscriptionsMonitor(mContext, mTestLooper.getLooper(),
+ mExecutor, mDelegate);
+ }
+
+ @Test
+ public void testStartStop() {
+ // Verify that addOnSubscriptionsChangedListener() is never called before start().
+ verify(mSubscriptionManager, never())
+ .addOnSubscriptionsChangedListener(mExecutor, mMonitor);
+ mMonitor.start();
+ verify(mSubscriptionManager).addOnSubscriptionsChangedListener(mExecutor, mMonitor);
+
+ // Verify that removeOnSubscriptionsChangedListener() is never called before stop()
+ verify(mSubscriptionManager, never()).removeOnSubscriptionsChangedListener(mMonitor);
+ mMonitor.stop();
+ verify(mSubscriptionManager).removeOnSubscriptionsChangedListener(mMonitor);
+ }
+
+ @NonNull
+ private static int[] convertArrayListToIntArray(@NonNull List<Integer> arrayList) {
+ final int[] list = new int[arrayList.size()];
+ for (int i = 0; i < arrayList.size(); i++) {
+ list[i] = arrayList.get(i);
+ }
+ return list;
+ }
+
+ private void setRatTypeForSub(List<RatTypeListener> listeners,
+ int subId, int type) {
+ final ServiceState serviceState = mock(ServiceState.class);
+ when(serviceState.getDataNetworkType()).thenReturn(type);
+ final RatTypeListener match = CollectionUtils
+ .find(listeners, it -> it.getSubId() == subId);
+ if (match == null) {
+ fail("Could not find listener with subId: " + subId);
+ }
+ match.onServiceStateChanged(serviceState);
+ }
+
+ private void addTestSub(int subId, String subscriberId) {
+ // add SubId to TestSubList.
+ if (mTestSubList.contains(subId)) fail("The subscriber list already contains this ID");
+
+ mTestSubList.add(subId);
+
+ final int[] subList = convertArrayListToIntArray(mTestSubList);
+ when(mSubscriptionManager.getActiveAndHiddenSubscriptionIdList()).thenReturn(subList);
+ when(mTelephonyManager.getSubscriberId(subId)).thenReturn(subscriberId);
+ mMonitor.onSubscriptionsChanged();
+ }
+
+ private void removeTestSub(int subId) {
+ // Remove subId from TestSubList.
+ mTestSubList.removeIf(it -> it == subId);
+ final int[] subList = convertArrayListToIntArray(mTestSubList);
+ when(mSubscriptionManager.getActiveAndHiddenSubscriptionIdList()).thenReturn(subList);
+ mMonitor.onSubscriptionsChanged();
+ }
+
+ private void assertRatTypeChangedForSub(String subscriberId, int ratType) {
+ assertEquals(mMonitor.getRatTypeForSubscriberId(subscriberId), ratType);
+ final ArgumentCaptor<Integer> typeCaptor = ArgumentCaptor.forClass(Integer.class);
+ // Verify callback with the subscriberId and the RAT type should be as expected.
+ // It will fail if get a callback with an unexpected RAT type.
+ verify(mDelegate).onCollapsedRatTypeChanged(eq(subscriberId), typeCaptor.capture());
+ final int type = typeCaptor.getValue();
+ assertEquals(ratType, type);
+ }
+
+ private void assertRatTypeNotChangedForSub(String subscriberId, int ratType) {
+ assertEquals(mMonitor.getRatTypeForSubscriberId(subscriberId), ratType);
+ // Should never get callback with any RAT type.
+ verify(mDelegate, never()).onCollapsedRatTypeChanged(eq(subscriberId), anyInt());
+ }
+
+ @Test
+ public void testSubChangedAndRatTypeChanged() {
+ final ArgumentCaptor<RatTypeListener> ratTypeListenerCaptor =
+ ArgumentCaptor.forClass(RatTypeListener.class);
+
+ mMonitor.start();
+ // Insert sim1, verify RAT type is NETWORK_TYPE_UNKNOWN, and never get any callback
+ // before changing RAT type.
+ addTestSub(TEST_SUBID1, TEST_IMSI1);
+ assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+
+ // Insert sim2.
+ addTestSub(TEST_SUBID2, TEST_IMSI2);
+ assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ verify(mTelephonyManager, times(2)).listen(ratTypeListenerCaptor.capture(),
+ eq(PhoneStateListener.LISTEN_SERVICE_STATE));
+ reset(mDelegate);
+
+ // Set RAT type of sim1 to UMTS.
+ // Verify RAT type of sim1 after subscription gets onCollapsedRatTypeChanged() callback
+ // and others remain untouched.
+ setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1,
+ TelephonyManager.NETWORK_TYPE_UMTS);
+ assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS);
+ assertRatTypeNotChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ reset(mDelegate);
+
+ // Set RAT type of sim2 to LTE.
+ // Verify RAT type of sim2 after subscription gets onCollapsedRatTypeChanged() callback
+ // and others remain untouched.
+ setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID2,
+ TelephonyManager.NETWORK_TYPE_LTE);
+ assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS);
+ assertRatTypeChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_LTE);
+ assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ reset(mDelegate);
+
+ // Remove sim2 and verify that callbacks are fired and RAT type is correct for sim2.
+ // while the other two remain untouched.
+ removeTestSub(TEST_SUBID2);
+ verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_NONE));
+ assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS);
+ assertRatTypeChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ reset(mDelegate);
+
+ // Set RAT type of sim1 to UNKNOWN. Then stop monitoring subscription changes
+ // and verify that the listener for sim1 is removed.
+ setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1,
+ TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ reset(mDelegate);
+
+ mMonitor.stop();
+ verify(mTelephonyManager, times(2)).listen(any(), eq(PhoneStateListener.LISTEN_NONE));
+ assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ }
+}