Merge "Move to renamed NDK symbol AFileDescriptor_getFd"
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index b219375..6c454bc 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -48,6 +48,7 @@
public class ConnectivitySettingsManager {
method public static void clearGlobalProxy(@NonNull android.content.Context);
+ method @NonNull public static java.util.Set<java.lang.String> getAppsAllowedOnRestrictedNetworks(@NonNull android.content.Context);
method @Nullable public static String getCaptivePortalHttpUrl(@NonNull android.content.Context);
method public static int getCaptivePortalMode(@NonNull android.content.Context, int);
method @NonNull public static java.time.Duration getConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
@@ -65,9 +66,9 @@
method @NonNull public static String getPrivateDnsDefaultMode(@NonNull android.content.Context);
method @Nullable public static String getPrivateDnsHostname(@NonNull android.content.Context);
method public static int getPrivateDnsMode(@NonNull android.content.Context);
- method @NonNull public static java.util.Set<java.lang.String> getRestrictedAllowedApps(@NonNull android.content.Context);
method public static boolean getWifiAlwaysRequested(@NonNull android.content.Context, boolean);
method @NonNull public static java.time.Duration getWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
+ method public static void setAppsAllowedOnRestrictedNetworks(@NonNull android.content.Context, @NonNull java.util.Set<java.lang.String>);
method public static void setCaptivePortalHttpUrl(@NonNull android.content.Context, @Nullable String);
method public static void setCaptivePortalMode(@NonNull android.content.Context, int);
method public static void setConnectivityKeepPendingIntentDuration(@NonNull android.content.Context, @NonNull java.time.Duration);
@@ -85,7 +86,6 @@
method public static void setPrivateDnsDefaultMode(@NonNull android.content.Context, @NonNull int);
method public static void setPrivateDnsHostname(@NonNull android.content.Context, @Nullable String);
method public static void setPrivateDnsMode(@NonNull android.content.Context, int);
- method public static void setRestrictedAllowedApps(@NonNull android.content.Context, @NonNull java.util.Set<java.lang.String>);
method public static void setWifiAlwaysRequested(@NonNull android.content.Context, boolean);
method public static void setWifiDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration);
field public static final int CAPTIVE_PORTAL_MODE_AVOID = 2; // 0x2
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 5750845..27bf114 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -294,7 +294,6 @@
method @NonNull public android.net.NetworkCapabilities.Builder addCapability(int);
method @NonNull public android.net.NetworkCapabilities.Builder addTransportType(int);
method @NonNull public android.net.NetworkCapabilities build();
- method @NonNull public android.net.NetworkCapabilities.Builder clearAll();
method @NonNull public android.net.NetworkCapabilities.Builder removeCapability(int);
method @NonNull public android.net.NetworkCapabilities.Builder removeTransportType(int);
method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setAdministratorUids(@NonNull int[]);
@@ -308,6 +307,7 @@
method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.NetworkCapabilities.Builder setSsid(@Nullable String);
method @NonNull public android.net.NetworkCapabilities.Builder setSubscriptionIds(@NonNull java.util.Set<java.lang.Integer>);
method @NonNull public android.net.NetworkCapabilities.Builder setTransportInfo(@Nullable android.net.TransportInfo);
+ method @NonNull public static android.net.NetworkCapabilities.Builder withoutDefaultCapabilities();
}
public class NetworkProvider {
@@ -381,6 +381,7 @@
public abstract class QosFilter {
method @NonNull public abstract android.net.Network getNetwork();
method public abstract boolean matchesLocalAddress(@NonNull java.net.InetAddress, int, int);
+ method public abstract boolean matchesRemoteAddress(@NonNull java.net.InetAddress, int, int);
}
public final class QosSession implements android.os.Parcelable {
@@ -403,6 +404,7 @@
method public int describeContents();
method @NonNull public java.net.InetSocketAddress getLocalSocketAddress();
method @NonNull public android.net.Network getNetwork();
+ method @Nullable public java.net.InetSocketAddress getRemoteSocketAddress();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.QosSocketInfo> CREATOR;
}
diff --git a/framework/src/android/net/ConnectivityDiagnosticsManager.java b/framework/src/android/net/ConnectivityDiagnosticsManager.java
index 3598ebc..dcc8a5e 100644
--- a/framework/src/android/net/ConnectivityDiagnosticsManager.java
+++ b/framework/src/android/net/ConnectivityDiagnosticsManager.java
@@ -713,7 +713,9 @@
* <p>Callbacks registered by apps not meeting the above criteria will not be invoked.
*
* <p>If a registering app loses its relevant permissions, any callbacks it registered will
- * silently stop receiving callbacks.
+ * silently stop receiving callbacks. Note that registering apps must also have location
+ * permissions to receive callbacks as some Networks may be location-bound (such as WiFi
+ * networks).
*
* <p>Each register() call <b>MUST</b> use a ConnectivityDiagnosticsCallback instance that is
* not currently registered. If a ConnectivityDiagnosticsCallback instance is registered with
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 0a3e231..1a6b37b 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -3337,7 +3337,60 @@
provider.setProviderId(NetworkProvider.ID_NONE);
}
+ /**
+ * Register or update a network offer with ConnectivityService.
+ *
+ * ConnectivityService keeps track of offers made by the various providers and matches
+ * them to networking requests made by apps or the system. The provider supplies a score
+ * and the capabilities of the network it might be able to bring up ; these act as filters
+ * used by ConnectivityService to only send those requests that can be fulfilled by the
+ * provider.
+ *
+ * The provider is under no obligation to be able to bring up the network it offers at any
+ * given time. Instead, this mechanism is meant to limit requests received by providers
+ * to those they actually have a chance to fulfill, as providers don't have a way to compare
+ * the quality of the network satisfying a given request to their own offer.
+ *
+ * An offer can be updated by calling this again with the same callback object. This is
+ * similar to calling unofferNetwork and offerNetwork again, but will only update the
+ * provider with the changes caused by the changes in the offer.
+ *
+ * @param provider The provider making this offer.
+ * @param score The prospective score of the network.
+ * @param caps The prospective capabilities of the network.
+ * @param callback The callback to call when this offer is needed or unneeded.
+ * @hide
+ */
+ @RequiresPermission(anyOf = {
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ android.Manifest.permission.NETWORK_FACTORY})
+ public void offerNetwork(@NonNull final NetworkProvider provider,
+ @NonNull final NetworkScore score, @NonNull final NetworkCapabilities caps,
+ @NonNull final INetworkOfferCallback callback) {
+ try {
+ mService.offerNetwork(Objects.requireNonNull(provider.getMessenger(), "null messenger"),
+ Objects.requireNonNull(score, "null score"),
+ Objects.requireNonNull(caps, "null caps"),
+ Objects.requireNonNull(callback, "null callback"));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ /**
+ * Withdraw a network offer made with {@link #offerNetwork}.
+ *
+ * @param callback The callback passed at registration time. This must be the same object
+ * that was passed to {@link #offerNetwork}
+ * @hide
+ */
+ public void unofferNetwork(@NonNull final INetworkOfferCallback callback) {
+ try {
+ mService.unofferNetwork(Objects.requireNonNull(callback));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
/** @hide exposed via the NetworkProvider class. */
@RequiresPermission(anyOf = {
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
diff --git a/framework/src/android/net/ConnectivitySettingsManager.java b/framework/src/android/net/ConnectivitySettingsManager.java
index 07754e4..762f24f 100644
--- a/framework/src/android/net/ConnectivitySettingsManager.java
+++ b/framework/src/android/net/ConnectivitySettingsManager.java
@@ -43,7 +43,6 @@
import java.util.List;
import java.util.Set;
import java.util.StringJoiner;
-import java.util.regex.Pattern;
/**
* A manager class for connectivity module settings.
@@ -375,11 +374,12 @@
private static final String PRIVATE_DNS_MODE_PROVIDER_HOSTNAME_STRING = "hostname";
/**
- * A list of apps that should be granted netd system permission for using restricted networks.
+ * A list of apps that is allowed on restricted networks.
*
* @hide
*/
- public static final String RESTRICTED_ALLOWED_APPS = "restricted_allowed_apps";
+ public static final String APPS_ALLOWED_ON_RESTRICTED_NETWORKS =
+ "apps_allowed_on_restricted_networks";
/**
* Get mobile data activity timeout from {@link Settings}.
@@ -1047,17 +1047,16 @@
}
/**
- * Get the list of apps(from {@link Settings}) that should be granted netd system permission for
- * using restricted networks.
+ * Get the list of apps(from {@link Settings}) that is allowed on restricted networks.
*
* @param context The {@link Context} to query the setting.
- * @return A list of apps that should be granted netd system permission for using restricted
- * networks or null if no setting value.
+ * @return A list of apps that is allowed on restricted networks or null if no setting
+ * value.
*/
@NonNull
- public static Set<String> getRestrictedAllowedApps(@NonNull Context context) {
+ public static Set<String> getAppsAllowedOnRestrictedNetworks(@NonNull Context context) {
final String appList = Settings.Secure.getString(
- context.getContentResolver(), RESTRICTED_ALLOWED_APPS);
+ context.getContentResolver(), APPS_ALLOWED_ON_RESTRICTED_NETWORKS);
if (TextUtils.isEmpty(appList)) {
return new ArraySet<>();
}
@@ -1065,27 +1064,24 @@
}
/**
- * Set the list of apps(from {@link Settings}) that should be granted netd system permission for
- * using restricted networks.
+ * Set the list of apps(from {@link Settings}) that is allowed on restricted networks.
*
* Note: Please refer to android developer guidelines for valid app(package name).
* https://developer.android.com/guide/topics/manifest/manifest-element.html#package
*
* @param context The {@link Context} to set the setting.
- * @param list A list of apps that should be granted netd system permission for using
- * restricted networks.
+ * @param list A list of apps that is allowed on restricted networks.
*/
- public static void setRestrictedAllowedApps(@NonNull Context context,
+ public static void setAppsAllowedOnRestrictedNetworks(@NonNull Context context,
@NonNull Set<String> list) {
- final Pattern appPattern = Pattern.compile("[a-zA-Z_0-9]+([.][a-zA-Z_0-9]+)*");
final StringJoiner joiner = new StringJoiner(";");
for (String app : list) {
- if (!appPattern.matcher(app).matches()) {
+ if (app == null || app.contains(";")) {
throw new IllegalArgumentException("Invalid app(package name)");
}
joiner.add(app);
}
- Settings.Secure.putString(
- context.getContentResolver(), RESTRICTED_ALLOWED_APPS, joiner.toString());
+ Settings.Secure.putString(context.getContentResolver(), APPS_ALLOWED_ON_RESTRICTED_NETWORKS,
+ joiner.toString());
}
}
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index a7cb618..d937c9c 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -23,6 +23,7 @@
import android.net.INetworkAgent;
import android.net.IOnCompleteListener;
import android.net.INetworkActivityListener;
+import android.net.INetworkOfferCallback;
import android.net.IQosCallback;
import android.net.ISocketKeepaliveCallback;
import android.net.LinkProperties;
@@ -221,4 +222,8 @@
in IOnCompleteListener listener);
int getRestrictBackgroundStatusByCaller();
+
+ void offerNetwork(in Messenger messenger, in NetworkScore score,
+ in NetworkCapabilities caps, in INetworkOfferCallback callback);
+ void unofferNetwork(in INetworkOfferCallback callback);
}
diff --git a/framework/src/android/net/INetworkOfferCallback.aidl b/framework/src/android/net/INetworkOfferCallback.aidl
new file mode 100644
index 0000000..67d2d40
--- /dev/null
+++ b/framework/src/android/net/INetworkOfferCallback.aidl
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 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 android.net;
+
+import android.net.NetworkRequest;
+
+/**
+ * A callback registered with connectivity by network providers together with
+ * a NetworkOffer.
+ *
+ * When the offer is needed to satisfy some application or system component,
+ * connectivity will call onOfferNeeded on this callback. When this happens,
+ * the provider should try and bring up the network.
+ *
+ * When the offer is no longer needed, for example because the application has
+ * withdrawn the request or if the request is being satisfied by a network
+ * that this offer will never be able to beat, connectivity calls
+ * onOfferUnneeded. When this happens, the provider should stop trying to
+ * bring up the network, or tear it down if it has already been brought up.
+ *
+ * When NetworkProvider#offerNetwork is called, the provider can expect to
+ * immediately receive all requests that can be fulfilled by that offer and
+ * are not already satisfied by a better network. It is possible no such
+ * request is currently outstanding, because no requests have been made that
+ * can be satisfied by this offer, or because all such requests are already
+ * satisfied by a better network.
+ * onOfferNeeded can be called at any time after registration and until the
+ * offer is withdrawn with NetworkProvider#unofferNetwork is called. This
+ * typically happens when a new network request is filed by an application,
+ * or when the network satisfying a request disconnects and this offer now
+ * stands a chance to be the best network for it.
+ *
+ * @hide
+ */
+oneway interface INetworkOfferCallback {
+ /**
+ * Informs the registrant that the offer is needed to fulfill this request.
+ * @param networkRequest the request to satisfy
+ * @param providerId the ID of the provider currently satisfying
+ * this request, or NetworkProvider.ID_NONE if none.
+ */
+ void onOfferNeeded(in NetworkRequest networkRequest, int providerId);
+
+ /**
+ * Informs the registrant that the offer is no longer needed to fulfill this request.
+ */
+ void onOfferUnneeded(in NetworkRequest networkRequest);
+}
diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java
index 4a99d29..90d821b 100644
--- a/framework/src/android/net/NetworkCapabilities.java
+++ b/framework/src/android/net/NetworkCapabilities.java
@@ -139,19 +139,13 @@
*/
private String mRequestorPackageName;
- /**
- * Indicates what fields should be redacted from this instance.
- */
- private final @RedactionType long mRedactions;
-
public NetworkCapabilities() {
- mRedactions = REDACT_ALL;
clearAll();
mNetworkCapabilities = DEFAULT_CAPABILITIES;
}
public NetworkCapabilities(NetworkCapabilities nc) {
- this(nc, REDACT_ALL);
+ this(nc, REDACT_NONE);
}
/**
@@ -163,10 +157,12 @@
* @hide
*/
public NetworkCapabilities(@Nullable NetworkCapabilities nc, @RedactionType long redactions) {
- mRedactions = redactions;
if (nc != null) {
set(nc);
}
+ if (mTransportInfo != null) {
+ mTransportInfo = nc.mTransportInfo.makeCopy(redactions);
+ }
}
/**
@@ -175,14 +171,6 @@
* @hide
*/
public void clearAll() {
- // Ensures that the internal copies maintained by the connectivity stack does not set it to
- // anything other than |REDACT_ALL|.
- if (mRedactions != REDACT_ALL) {
- // This is needed because the current redaction mechanism relies on redaction while
- // parceling.
- throw new UnsupportedOperationException(
- "Cannot clear NetworkCapabilities when mRedactions is set");
- }
mNetworkCapabilities = mTransportTypes = mForbiddenNetworkCapabilities = 0;
mLinkUpBandwidthKbps = mLinkDownBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED;
mNetworkSpecifier = null;
@@ -211,7 +199,7 @@
mLinkDownBandwidthKbps = nc.mLinkDownBandwidthKbps;
mNetworkSpecifier = nc.mNetworkSpecifier;
if (nc.getTransportInfo() != null) {
- setTransportInfo(nc.getTransportInfo().makeCopy(mRedactions));
+ setTransportInfo(nc.getTransportInfo());
} else {
setTransportInfo(null);
}
@@ -2411,6 +2399,11 @@
return mTransportInfo.getApplicableRedactions();
}
+ private NetworkCapabilities removeDefaultCapabilites() {
+ mNetworkCapabilities &= ~DEFAULT_CAPABILITIES;
+ return this;
+ }
+
/**
* Builder class for NetworkCapabilities.
*
@@ -2447,6 +2440,16 @@
}
/**
+ * Creates a new Builder without the default capabilities.
+ */
+ @NonNull
+ public static Builder withoutDefaultCapabilities() {
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ nc.removeDefaultCapabilites();
+ return new Builder(nc);
+ }
+
+ /**
* Adds the given transport type.
*
* Multiple transports may be added. Note that when searching for a network to satisfy a
@@ -2507,17 +2510,6 @@
}
/**
- * Completely clears the contents of this object, removing even the capabilities that are
- * set by default when the object is constructed.
- * @return this builder
- */
- @NonNull
- public Builder clearAll() {
- mCaps.clearAll();
- return this;
- }
-
- /**
* Sets the owner UID.
*
* The default value is {@link Process#INVALID_UID}. Pass this value to reset.
diff --git a/framework/src/android/net/NetworkProvider.java b/framework/src/android/net/NetworkProvider.java
index 14cb51c..8f93047 100644
--- a/framework/src/android/net/NetworkProvider.java
+++ b/framework/src/android/net/NetworkProvider.java
@@ -28,6 +28,11 @@
import android.os.Messenger;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.concurrent.Executor;
+
/**
* Base class for network providers such as telephony or Wi-Fi. NetworkProviders connect the device
* to networks and makes them available to the core network stack by creating
@@ -78,7 +83,9 @@
*/
@SystemApi
public NetworkProvider(@NonNull Context context, @NonNull Looper looper, @NonNull String name) {
- Handler handler = new Handler(looper) {
+ // TODO (b/174636568) : this class should be able to cache an instance of
+ // ConnectivityManager so it doesn't have to fetch it again every time.
+ final Handler handler = new Handler(looper) {
@Override
public void handleMessage(Message m) {
switch (m.what) {
@@ -159,4 +166,148 @@
public void declareNetworkRequestUnfulfillable(@NonNull NetworkRequest request) {
ConnectivityManager.from(mContext).declareNetworkRequestUnfulfillable(request);
}
+
+ /** @hide */
+ // TODO : make @SystemApi when the impl is complete
+ public interface NetworkOfferCallback {
+ /** Called by the system when this offer is needed to satisfy some networking request. */
+ void onOfferNeeded(@NonNull NetworkRequest request, int providerId);
+ /** Called by the system when this offer is no longer needed. */
+ void onOfferUnneeded(@NonNull NetworkRequest request);
+ }
+
+ private class NetworkOfferCallbackProxy extends INetworkOfferCallback.Stub {
+ @NonNull public final NetworkOfferCallback callback;
+ @NonNull private final Executor mExecutor;
+
+ NetworkOfferCallbackProxy(@NonNull final NetworkOfferCallback callback,
+ @NonNull final Executor executor) {
+ this.callback = callback;
+ this.mExecutor = executor;
+ }
+
+ @Override
+ public void onOfferNeeded(final @NonNull NetworkRequest request,
+ final int providerId) {
+ mExecutor.execute(() -> callback.onOfferNeeded(request, providerId));
+ }
+
+ @Override
+ public void onOfferUnneeded(final @NonNull NetworkRequest request) {
+ mExecutor.execute(() -> callback.onOfferUnneeded(request));
+ }
+ }
+
+ @GuardedBy("mProxies")
+ @NonNull private final ArrayList<NetworkOfferCallbackProxy> mProxies = new ArrayList<>();
+
+ // Returns the proxy associated with this callback, or null if none.
+ @Nullable
+ private NetworkOfferCallbackProxy findProxyForCallback(@NonNull final NetworkOfferCallback cb) {
+ synchronized (mProxies) {
+ for (final NetworkOfferCallbackProxy p : mProxies) {
+ if (p.callback == cb) return p;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Register or update an offer for network with the passed caps and score.
+ *
+ * A NetworkProvider's job is to provide networks. This function is how a provider tells the
+ * connectivity stack what kind of network it may provide. The score and caps arguments act
+ * as filters that the connectivity stack uses to tell when the offer is necessary. When an
+ * offer might be advantageous over existing networks, the provider will receive a call to
+ * the associated callback's {@link NetworkOfferCallback#onOfferNeeded} method. The provider
+ * should then try to bring up this network. When an offer is no longer needed, the stack
+ * will inform the provider by calling {@link NetworkOfferCallback#onOfferUnneeded}. The
+ * provider should stop trying to bring up such a network, or disconnect it if it already has
+ * one.
+ *
+ * The stack determines what offers are needed according to what networks are currently
+ * available to the system, and what networking requests are made by applications. If an
+ * offer looks like it could be a better choice than any existing network for any particular
+ * request, that's when the stack decides the offer is needed. If the current networking
+ * requests are all satisfied by networks that this offer can't possibly be a better match
+ * for, that's when the offer is unneeded. An offer starts off as unneeded ; the provider
+ * should not try to bring up the network until {@link NetworkOfferCallback#onOfferNeeded}
+ * is called.
+ *
+ * Note that the offers are non-binding to the providers, in particular because providers
+ * often don't know if they will be able to bring up such a network at any given time. For
+ * example, no wireless network may be in range when the offer is needed. This is fine and
+ * expected ; the provider should simply continue to try to bring up the network and do so
+ * if/when it becomes possible. In the mean time, the stack will continue to satisfy requests
+ * with the best network currently available, or if none, keep the apps informed that no
+ * network can currently satisfy this request. When/if the provider can bring up the network,
+ * the connectivity stack will match it against requests, and inform interested apps of the
+ * availability of this network. This may, in turn, render the offer of some other provider
+ * unneeded if all requests it used to satisfy are now better served by this network.
+ *
+ * A network can become unneeded for a reason like the above : whether the provider managed
+ * to bring up the offered network after it became needed or not, some other provider may
+ * bring up a better network than this one, making this offer unneeded. A network may also
+ * become unneeded if the application making the request withdrew it (for example, after it
+ * is done transferring data, or if the user canceled an operation).
+ *
+ * The capabilities and score act as filters as to what requests the provider will see.
+ * They are not promises, but for best performance, the providers should strive to put
+ * as much known information as possible in the offer. For capabilities in particular, it
+ * should put all NetworkAgent-managed capabilities a network may have, even if it doesn't
+ * have them at first. This applies to INTERNET, for example ; if a provider thinks the
+ * network it can bring up for this offer may offer Internet access it should include the
+ * INTERNET bit. It's fine if the brought up network ends up not actually having INTERNET.
+ *
+ * TODO : in the future, to avoid possible infinite loops, there should be constraints on
+ * what can be put in capabilities of networks brought up for an offer. If a provider might
+ * bring up a network with or without INTERNET, then it should file two offers : this will
+ * let it know precisely what networks are needed, so it can avoid bringing up networks that
+ * won't actually satisfy requests and remove the risk for bring-up-bring-down loops.
+ *
+ * @hide
+ */
+ // TODO : make @SystemApi when the impl is complete
+ @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
+ public void offerNetwork(@NonNull final NetworkScore score,
+ @NonNull final NetworkCapabilities caps, @NonNull final Executor executor,
+ @NonNull final NetworkOfferCallback callback) {
+ NetworkOfferCallbackProxy proxy = null;
+ synchronized (mProxies) {
+ for (final NetworkOfferCallbackProxy existingProxy : mProxies) {
+ if (existingProxy.callback == callback) {
+ proxy = existingProxy;
+ break;
+ }
+ }
+ if (null == proxy) {
+ proxy = new NetworkOfferCallbackProxy(callback, executor);
+ mProxies.add(proxy);
+ }
+ }
+ mContext.getSystemService(ConnectivityManager.class).offerNetwork(this, score, caps, proxy);
+ }
+
+ /**
+ * Withdraw a network offer previously made to the networking stack.
+ *
+ * If a provider can no longer provide a network they offered, it should call this method.
+ * An example of usage could be if the hardware necessary to bring up the network was turned
+ * off in UI by the user. Note that because offers are never binding, the provider might
+ * alternatively decide not to withdraw this offer and simply refuse to bring up the network
+ * even when it's needed. However, withdrawing the request is slightly more resource-efficient
+ * because the networking stack won't have to compare this offer to exiting networks to see
+ * if it could beat any of them, and may be advantageous to the provider's implementation that
+ * can rely on no longer receiving callbacks for a network that they can't bring up anyways.
+ *
+ * @hide
+ */
+ // TODO : make @SystemApi when the impl is complete
+ @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY)
+ public void unofferNetwork(final @NonNull NetworkOfferCallback callback) {
+ final NetworkOfferCallbackProxy proxy = findProxyForCallback(callback);
+ if (null == proxy) return;
+ mProxies.remove(proxy);
+ mContext.getSystemService(ConnectivityManager.class).unofferNetwork(proxy);
+ }
}
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index dd88c5a..e6a96ef 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -200,8 +200,9 @@
private final NetworkCapabilities mNetworkCapabilities;
- // A boolean that represents the user modified NOT_VCN_MANAGED capability.
- private boolean mModifiedNotVcnManaged = false;
+ // A boolean that represents whether the NOT_VCN_MANAGED capability should be deduced when
+ // the NetworkRequest object is built.
+ private boolean mShouldDeduceNotVcnManaged = true;
/**
* Default constructor for Builder.
@@ -223,7 +224,7 @@
// If the caller constructed the builder from a request, it means the user
// might explicitly want the capabilities from the request. Thus, the NOT_VCN_MANAGED
// capabilities should not be touched later.
- mModifiedNotVcnManaged = true;
+ mShouldDeduceNotVcnManaged = false;
}
/**
@@ -254,7 +255,7 @@
public Builder addCapability(@NetworkCapabilities.NetCapability int capability) {
mNetworkCapabilities.addCapability(capability);
if (capability == NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) {
- mModifiedNotVcnManaged = true;
+ mShouldDeduceNotVcnManaged = false;
}
return this;
}
@@ -268,7 +269,7 @@
public Builder removeCapability(@NetworkCapabilities.NetCapability int capability) {
mNetworkCapabilities.removeCapability(capability);
if (capability == NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED) {
- mModifiedNotVcnManaged = true;
+ mShouldDeduceNotVcnManaged = false;
}
return this;
}
@@ -352,7 +353,7 @@
mNetworkCapabilities.clearAll();
// If the caller explicitly clear all capabilities, the NOT_VCN_MANAGED capabilities
// should not be add back later.
- mModifiedNotVcnManaged = true;
+ mShouldDeduceNotVcnManaged = false;
return this;
}
@@ -453,6 +454,9 @@
throw new IllegalArgumentException("A MatchAllNetworkSpecifier is not permitted");
}
mNetworkCapabilities.setNetworkSpecifier(networkSpecifier);
+ // Do not touch NOT_VCN_MANAGED if the caller needs to access to a very specific
+ // Network.
+ mShouldDeduceNotVcnManaged = false;
return this;
}
@@ -486,12 +490,13 @@
* {@link #VCN_SUPPORTED_CAPABILITIES}, add the NET_CAPABILITY_NOT_VCN_MANAGED to
* allow the callers automatically utilize VCN networks if available.
* 2. For the requests that explicitly add or remove NET_CAPABILITY_NOT_VCN_MANAGED,
+ * or has clear intention of tracking specific network,
* do not alter them to allow user fire request that suits their need.
*
* @hide
*/
private void deduceNotVcnManagedCapability(final NetworkCapabilities nc) {
- if (mModifiedNotVcnManaged) return;
+ if (!mShouldDeduceNotVcnManaged) return;
for (final int cap : nc.getCapabilities()) {
if (!VCN_SUPPORTED_CAPABILITIES.contains(cap)) return;
}
diff --git a/framework/src/android/net/QosFilter.java b/framework/src/android/net/QosFilter.java
index ab55002..957c867 100644
--- a/framework/src/android/net/QosFilter.java
+++ b/framework/src/android/net/QosFilter.java
@@ -71,5 +71,16 @@
*/
public abstract boolean matchesLocalAddress(@NonNull InetAddress address,
int startPort, int endPort);
+
+ /**
+ * Determines whether or not the parameters is a match for the filter.
+ *
+ * @param address the remote address
+ * @param startPort the start of the port range
+ * @param endPort the end of the port range
+ * @return whether the parameters match the remote address of the filter
+ */
+ public abstract boolean matchesRemoteAddress(@NonNull InetAddress address,
+ int startPort, int endPort);
}
diff --git a/framework/src/android/net/QosSocketFilter.java b/framework/src/android/net/QosSocketFilter.java
index 2080e68..69da7f4 100644
--- a/framework/src/android/net/QosSocketFilter.java
+++ b/framework/src/android/net/QosSocketFilter.java
@@ -138,13 +138,26 @@
if (mQosSocketInfo.getLocalSocketAddress() == null) {
return false;
}
-
- return matchesLocalAddress(mQosSocketInfo.getLocalSocketAddress(), address, startPort,
+ return matchesAddress(mQosSocketInfo.getLocalSocketAddress(), address, startPort,
endPort);
}
/**
- * Called from {@link QosSocketFilter#matchesLocalAddress(InetAddress, int, int)} with the
+ * @inheritDoc
+ */
+ @Override
+ public boolean matchesRemoteAddress(@NonNull final InetAddress address, final int startPort,
+ final int endPort) {
+ if (mQosSocketInfo.getRemoteSocketAddress() == null) {
+ return false;
+ }
+ return matchesAddress(mQosSocketInfo.getRemoteSocketAddress(), address, startPort,
+ endPort);
+ }
+
+ /**
+ * Called from {@link QosSocketFilter#matchesLocalAddress(InetAddress, int, int)}
+ * and {@link QosSocketFilter#matchesRemoteAddress(InetAddress, int, int)} with the
* filterSocketAddress coming from {@link QosSocketInfo#getLocalSocketAddress()}.
* <p>
* This method exists for testing purposes since {@link QosSocketInfo} couldn't be mocked
@@ -156,7 +169,7 @@
* @param endPort the end of the port range to check
*/
@VisibleForTesting
- public static boolean matchesLocalAddress(@NonNull final InetSocketAddress filterSocketAddress,
+ public static boolean matchesAddress(@NonNull final InetSocketAddress filterSocketAddress,
@NonNull final InetAddress address,
final int startPort, final int endPort) {
return startPort <= filterSocketAddress.getPort()
diff --git a/framework/src/android/net/QosSocketInfo.java b/framework/src/android/net/QosSocketInfo.java
index 53d9669..a45d507 100644
--- a/framework/src/android/net/QosSocketInfo.java
+++ b/framework/src/android/net/QosSocketInfo.java
@@ -17,6 +17,7 @@
package android.net;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
@@ -32,7 +33,8 @@
/**
* Used in conjunction with
* {@link ConnectivityManager#registerQosCallback}
- * in order to receive Qos Sessions related to the local address and port of a bound {@link Socket}.
+ * in order to receive Qos Sessions related to the local address and port of a bound {@link Socket}
+ * and/or remote address and port of a connected {@link Socket}.
*
* @hide
*/
@@ -48,6 +50,9 @@
@NonNull
private final InetSocketAddress mLocalSocketAddress;
+ @Nullable
+ private final InetSocketAddress mRemoteSocketAddress;
+
/**
* The {@link Network} the socket is on.
*
@@ -81,6 +86,18 @@
}
/**
+ * The remote address of the socket passed into {@link QosSocketInfo(Network, Socket)}.
+ * The value does not reflect any changes that occur to the socket after it is first set
+ * in the constructor.
+ *
+ * @return the remote address of the socket if socket is connected, null otherwise
+ */
+ @Nullable
+ public InetSocketAddress getRemoteSocketAddress() {
+ return mRemoteSocketAddress;
+ }
+
+ /**
* Creates a {@link QosSocketInfo} given a {@link Network} and bound {@link Socket}. The
* {@link Socket} must remain bound in order to receive {@link QosSession}s.
*
@@ -95,6 +112,12 @@
mParcelFileDescriptor = ParcelFileDescriptor.fromSocket(socket);
mLocalSocketAddress =
new InetSocketAddress(socket.getLocalAddress(), socket.getLocalPort());
+
+ if (socket.isConnected()) {
+ mRemoteSocketAddress = (InetSocketAddress) socket.getRemoteSocketAddress();
+ } else {
+ mRemoteSocketAddress = null;
+ }
}
/* Parcelable methods */
@@ -102,11 +125,15 @@
mNetwork = Objects.requireNonNull(Network.CREATOR.createFromParcel(in));
mParcelFileDescriptor = ParcelFileDescriptor.CREATOR.createFromParcel(in);
- final int addressLength = in.readInt();
- mLocalSocketAddress = readSocketAddress(in, addressLength);
+ final int localAddressLength = in.readInt();
+ mLocalSocketAddress = readSocketAddress(in, localAddressLength);
+
+ final int remoteAddressLength = in.readInt();
+ mRemoteSocketAddress = remoteAddressLength == 0 ? null
+ : readSocketAddress(in, remoteAddressLength);
}
- private InetSocketAddress readSocketAddress(final Parcel in, final int addressLength) {
+ private @NonNull InetSocketAddress readSocketAddress(final Parcel in, final int addressLength) {
final byte[] address = new byte[addressLength];
in.readByteArray(address);
final int port = in.readInt();
@@ -130,10 +157,19 @@
mNetwork.writeToParcel(dest, 0);
mParcelFileDescriptor.writeToParcel(dest, 0);
- final byte[] address = mLocalSocketAddress.getAddress().getAddress();
- dest.writeInt(address.length);
- dest.writeByteArray(address);
+ final byte[] localAddress = mLocalSocketAddress.getAddress().getAddress();
+ dest.writeInt(localAddress.length);
+ dest.writeByteArray(localAddress);
dest.writeInt(mLocalSocketAddress.getPort());
+
+ if (mRemoteSocketAddress == null) {
+ dest.writeInt(0);
+ } else {
+ final byte[] remoteAddress = mRemoteSocketAddress.getAddress().getAddress();
+ dest.writeInt(remoteAddress.length);
+ dest.writeByteArray(remoteAddress);
+ dest.writeInt(mRemoteSocketAddress.getPort());
+ }
}
@NonNull
diff --git a/framework/src/android/net/apf/ApfCapabilities.java b/framework/src/android/net/apf/ApfCapabilities.java
index 85b2471..663c1b3 100644
--- a/framework/src/android/net/apf/ApfCapabilities.java
+++ b/framework/src/android/net/apf/ApfCapabilities.java
@@ -131,43 +131,21 @@
* @return Whether the APF Filter in the device should filter out IEEE 802.3 Frames.
*/
public static boolean getApfDrop8023Frames() {
- // TODO(b/183076074): remove reading resources from system resources
+ // TODO: deprecate/remove this method (now unused in the platform), as the resource was
+ // moved to NetworkStack.
final Resources systemRes = Resources.getSystem();
final int id = systemRes.getIdentifier("config_apfDrop802_3Frames", "bool", "android");
return systemRes.getBoolean(id);
}
/**
- * @return Whether the APF Filter in the device should filter out IEEE 802.3 Frames.
- * @hide
- */
- public static boolean getApfDrop8023Frames(@NonNull Context context) {
- final ConnectivityResources res = getResources(context);
- // TODO(b/183076074): use R.bool.config_apfDrop802_3Frames directly
- final int id = res.get().getIdentifier("config_apfDrop802_3Frames", "bool",
- res.getResourcesContext().getPackageName());
- return res.get().getBoolean(id);
- }
-
- /**
* @return An array of denylisted EtherType, packets with EtherTypes within it will be dropped.
*/
public static @NonNull int[] getApfEtherTypeBlackList() {
- // TODO(b/183076074): remove reading resources from system resources
+ // TODO: deprecate/remove this method (now unused in the platform), as the resource was
+ // moved to NetworkStack.
final Resources systemRes = Resources.getSystem();
final int id = systemRes.getIdentifier("config_apfEthTypeBlackList", "array", "android");
return systemRes.getIntArray(id);
}
-
- /**
- * @return An array of denylisted EtherType, packets with EtherTypes within it will be dropped.
- * @hide
- */
- public static @NonNull int[] getApfEtherTypeDenyList(@NonNull Context context) {
- final ConnectivityResources res = getResources(context);
- // TODO(b/183076074): use R.array.config_apfEthTypeDenyList directly
- final int id = res.get().getIdentifier("config_apfEthTypeDenyList", "array",
- res.getResourcesContext().getPackageName());
- return res.get().getIntArray(id);
- }
}
diff --git a/service/Android.bp b/service/Android.bp
index 1330e71..513de19 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -52,8 +52,8 @@
java_library {
name: "service-connectivity-pre-jarjar",
srcs: [
+ "src/**/*.java",
":framework-connectivity-shared-srcs",
- ":connectivity-service-srcs",
],
libs: [
"android.net.ipsec.ike",
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index 9ff2a22..078a9eb 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -52,22 +52,6 @@
<item>12,60000</item><!-- mobile_cbs -->
</string-array>
- <!-- Whether the APF Filter in the device should filter out IEEE 802.3 Frames
- Those frames are identified by the field Eth-type having values
- less than 0x600 -->
- <bool translatable="false" name="config_apfDrop802_3Frames">true</bool>
-
- <!-- An array of Denylisted EtherType, packets with EtherTypes within this array
- will be dropped
- TODO: need to put proper values, these are for testing purposes only -->
- <integer-array translatable="false" name="config_apfEthTypeDenyList">
- <item>0x88A2</item>
- <item>0x88A4</item>
- <item>0x88B8</item>
- <item>0x88CD</item>
- <item>0x88E3</item>
- </integer-array>
-
<!-- Default supported concurrent socket keepalive slots per transport type, used by
ConnectivityManager.createSocketKeepalive() for calculating the number of keepalive
offload slots that should be reserved for privileged access. This string array should be
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 717d08e..f0f4ae8 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -21,8 +21,6 @@
<item type="string" name="config_networkCaptivePortalServerUrl"/>
<item type="integer" name="config_networkTransitionTimeout"/>
<item type="array" name="config_wakeonlan_supported_interfaces"/>
- <item type="bool" name="config_apfDrop802_3Frames"/>
- <item type="array" name="config_apfEthTypeDenyList"/>
<item type="integer" name="config_networkMeteredMultipathPreference"/>
<item type="array" name="config_networkSupportedKeepaliveCount"/>
<item type="integer" name="config_networkAvoidBadWifi"/>
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
new file mode 100644
index 0000000..842ad62
--- /dev/null
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -0,0 +1,10090 @@
+/*
+ * Copyright (C) 2008 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;
+import static android.Manifest.permission.RECEIVE_DATA_ACTIVITY_CHANGE;
+import static android.content.pm.PackageManager.FEATURE_BLUETOOTH;
+import static android.content.pm.PackageManager.FEATURE_WATCH;
+import static android.content.pm.PackageManager.FEATURE_WIFI;
+import static android.content.pm.PackageManager.FEATURE_WIFI_DIRECT;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_ATTEMPTED_BITMASK;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_SUCCEEDED_BITMASK;
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_VALIDATION_RESULT;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_DNS_EVENTS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_TCP_METRICS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_DNS_CONSECUTIVE_TIMEOUTS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS;
+import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_PACKET_FAIL_RATE;
+import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
+import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN;
+import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
+import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
+import static android.net.ConnectivityManager.TYPE_ETHERNET;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_MOBILE_CBS;
+import static android.net.ConnectivityManager.TYPE_MOBILE_DUN;
+import static android.net.ConnectivityManager.TYPE_MOBILE_EMERGENCY;
+import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA;
+import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
+import static android.net.ConnectivityManager.TYPE_MOBILE_IA;
+import static android.net.ConnectivityManager.TYPE_MOBILE_IMS;
+import static android.net.ConnectivityManager.TYPE_MOBILE_MMS;
+import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL;
+import static android.net.ConnectivityManager.TYPE_NONE;
+import static android.net.ConnectivityManager.TYPE_PROXY;
+import static android.net.ConnectivityManager.TYPE_VPN;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
+import static android.net.ConnectivityManager.getNetworkTypeName;
+import static android.net.ConnectivityManager.isNetworkTypeValid;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS;
+import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL;
+import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_ENTERPRISE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
+import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS;
+import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkRequest.Type.LISTEN_FOR_BEST;
+import static android.net.shared.NetworkMonitorUtils.isPrivateDnsValidationRequired;
+import static android.os.Process.INVALID_UID;
+import static android.os.Process.VPN_UID;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static java.util.Map.Entry;
+
+import android.Manifest;
+import android.annotation.BoolRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.app.BroadcastOptions;
+import android.app.PendingIntent;
+import android.app.usage.NetworkStatsManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.net.CaptivePortal;
+import android.net.CaptivePortalData;
+import android.net.ConnectionInfo;
+import android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
+import android.net.ConnectivityDiagnosticsManager.DataStallReport;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.BlockedReason;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.ConnectivityManager.RestrictBackgroundStatus;
+import android.net.ConnectivityResources;
+import android.net.ConnectivitySettingsManager;
+import android.net.DataStallReportParcelable;
+import android.net.DnsResolverServiceManager;
+import android.net.ICaptivePortal;
+import android.net.IConnectivityDiagnosticsCallback;
+import android.net.IConnectivityManager;
+import android.net.IDnsResolver;
+import android.net.INetd;
+import android.net.INetworkActivityListener;
+import android.net.INetworkAgent;
+import android.net.INetworkMonitor;
+import android.net.INetworkMonitorCallbacks;
+import android.net.INetworkOfferCallback;
+import android.net.IOnCompleteListener;
+import android.net.IQosCallback;
+import android.net.ISocketKeepaliveCallback;
+import android.net.InetAddresses;
+import android.net.IpMemoryStore;
+import android.net.IpPrefix;
+import android.net.LinkProperties;
+import android.net.MatchAllNetworkSpecifier;
+import android.net.NativeNetworkConfig;
+import android.net.NativeNetworkType;
+import android.net.NattSocketKeepalive;
+import android.net.Network;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkMonitorManager;
+import android.net.NetworkPolicyManager;
+import android.net.NetworkPolicyManager.NetworkPolicyCallback;
+import android.net.NetworkProvider;
+import android.net.NetworkRequest;
+import android.net.NetworkScore;
+import android.net.NetworkSpecifier;
+import android.net.NetworkStack;
+import android.net.NetworkStackClient;
+import android.net.NetworkState;
+import android.net.NetworkStateSnapshot;
+import android.net.NetworkTestResultParcelable;
+import android.net.NetworkUtils;
+import android.net.NetworkWatchlistManager;
+import android.net.OemNetworkPreferences;
+import android.net.PrivateDnsConfigParcel;
+import android.net.ProxyInfo;
+import android.net.QosCallbackException;
+import android.net.QosFilter;
+import android.net.QosSocketFilter;
+import android.net.QosSocketInfo;
+import android.net.RouteInfo;
+import android.net.RouteInfoParcel;
+import android.net.SocketKeepalive;
+import android.net.TetheringManager;
+import android.net.TransportInfo;
+import android.net.UidRange;
+import android.net.UidRangeParcel;
+import android.net.UnderlyingNetworkInfo;
+import android.net.Uri;
+import android.net.VpnManager;
+import android.net.VpnTransportInfo;
+import android.net.metrics.IpConnectivityLog;
+import android.net.metrics.NetworkEvent;
+import android.net.netlink.InetDiagMessage;
+import android.net.resolv.aidl.DnsHealthEventParcel;
+import android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener;
+import android.net.resolv.aidl.Nat64PrefixEventParcel;
+import android.net.resolv.aidl.PrivateDnsValidationEventParcel;
+import android.net.shared.PrivateDnsConfig;
+import android.net.util.MultinetworkPolicyTracker;
+import android.net.util.NetdService;
+import android.os.BatteryStatsManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.os.PowerManager;
+import android.os.Process;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.sysprop.NetworkProperties;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.LocalLog;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.MessageUtils;
+import com.android.modules.utils.BasicShellCommandHandler;
+import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult;
+import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
+import com.android.net.module.util.LocationPermissionChecker;
+import com.android.net.module.util.NetworkCapabilitiesUtils;
+import com.android.net.module.util.PermissionUtils;
+import com.android.server.connectivity.AutodestructReference;
+import com.android.server.connectivity.DnsManager;
+import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
+import com.android.server.connectivity.FullScore;
+import com.android.server.connectivity.KeepaliveTracker;
+import com.android.server.connectivity.LingerMonitor;
+import com.android.server.connectivity.MockableSystemProperties;
+import com.android.server.connectivity.NetworkAgentInfo;
+import com.android.server.connectivity.NetworkDiagnostics;
+import com.android.server.connectivity.NetworkNotificationManager;
+import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+import com.android.server.connectivity.NetworkOffer;
+import com.android.server.connectivity.NetworkRanker;
+import com.android.server.connectivity.PermissionMonitor;
+import com.android.server.connectivity.ProfileNetworkPreferences;
+import com.android.server.connectivity.ProxyTracker;
+import com.android.server.connectivity.QosCallbackTracker;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.ConcurrentModificationException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.StringJoiner;
+import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @hide
+ */
+public class ConnectivityService extends IConnectivityManager.Stub
+ implements PendingIntent.OnFinished {
+ private static final String TAG = ConnectivityService.class.getSimpleName();
+
+ private static final String DIAG_ARG = "--diag";
+ public static final String SHORT_ARG = "--short";
+ private static final String NETWORK_ARG = "networks";
+ private static final String REQUEST_ARG = "requests";
+
+ private static final boolean DBG = true;
+ private static final boolean DDBG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
+
+ private static final boolean LOGD_BLOCKED_NETWORKINFO = true;
+
+ /**
+ * Default URL to use for {@link #getCaptivePortalServerUrl()}. This should not be changed
+ * by OEMs for configuration purposes, as this value is overridden by
+ * ConnectivitySettingsManager.CAPTIVE_PORTAL_HTTP_URL.
+ * R.string.config_networkCaptivePortalServerUrl should be overridden instead for this purpose
+ * (preferably via runtime resource overlays).
+ */
+ private static final String DEFAULT_CAPTIVE_PORTAL_HTTP_URL =
+ "http://connectivitycheck.gstatic.com/generate_204";
+
+ // TODO: create better separation between radio types and network types
+
+ // how long to wait before switching back to a radio's default network
+ private static final int RESTORE_DEFAULT_NETWORK_DELAY = 1 * 60 * 1000;
+ // system property that can override the above value
+ private static final String NETWORK_RESTORE_DELAY_PROP_NAME =
+ "android.telephony.apn-restore";
+
+ // How long to wait before putting up a "This network doesn't have an Internet connection,
+ // connect anyway?" dialog after the user selects a network that doesn't validate.
+ private static final int PROMPT_UNVALIDATED_DELAY_MS = 8 * 1000;
+
+ // Default to 30s linger time-out, and 5s for nascent network. Modifiable only for testing.
+ private static final String LINGER_DELAY_PROPERTY = "persist.netmon.linger";
+ private static final int DEFAULT_LINGER_DELAY_MS = 30_000;
+ private static final int DEFAULT_NASCENT_DELAY_MS = 5_000;
+
+ // The maximum number of network request allowed per uid before an exception is thrown.
+ private static final int MAX_NETWORK_REQUESTS_PER_UID = 100;
+
+ // The maximum number of network request allowed for system UIDs before an exception is thrown.
+ @VisibleForTesting
+ static final int MAX_NETWORK_REQUESTS_PER_SYSTEM_UID = 250;
+
+ @VisibleForTesting
+ protected int mLingerDelayMs; // Can't be final, or test subclass constructors can't change it.
+ @VisibleForTesting
+ protected int mNascentDelayMs;
+
+ // How long to delay to removal of a pending intent based request.
+ // See ConnectivitySettingsManager.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS
+ private final int mReleasePendingIntentDelayMs;
+
+ private MockableSystemProperties mSystemProperties;
+
+ @VisibleForTesting
+ protected final PermissionMonitor mPermissionMonitor;
+
+ private final PerUidCounter mNetworkRequestCounter;
+ @VisibleForTesting
+ final PerUidCounter mSystemNetworkRequestCounter;
+
+ private volatile boolean mLockdownEnabled;
+
+ /**
+ * Stale copy of uid blocked reasons provided by NPMS. As long as they are accessed only in
+ * internal handler thread, they don't need a lock.
+ */
+ private SparseIntArray mUidBlockedReasons = new SparseIntArray();
+
+ private final Context mContext;
+ private final ConnectivityResources mResources;
+ // The Context is created for UserHandle.ALL.
+ private final Context mUserAllContext;
+ private final Dependencies mDeps;
+ // 0 is full bad, 100 is full good
+ private int mDefaultInetConditionPublished = 0;
+
+ @VisibleForTesting
+ protected IDnsResolver mDnsResolver;
+ @VisibleForTesting
+ protected INetd mNetd;
+ private NetworkStatsManager mStatsManager;
+ private NetworkPolicyManager mPolicyManager;
+ private final NetdCallback mNetdCallback;
+
+ /**
+ * TestNetworkService (lazily) created upon first usage. Locked to prevent creation of multiple
+ * instances.
+ */
+ @GuardedBy("mTNSLock")
+ private TestNetworkService mTNS;
+
+ private final Object mTNSLock = new Object();
+
+ private String mCurrentTcpBufferSizes;
+
+ private static final SparseArray<String> sMagicDecoderRing = MessageUtils.findMessageNames(
+ new Class[] { ConnectivityService.class, NetworkAgent.class, NetworkAgentInfo.class });
+
+ private enum ReapUnvalidatedNetworks {
+ // Tear down networks that have no chance (e.g. even if validated) of becoming
+ // the highest scoring network satisfying a NetworkRequest. This should be passed when
+ // all networks have been rematched against all NetworkRequests.
+ REAP,
+ // Don't reap networks. This should be passed when some networks have not yet been
+ // rematched against all NetworkRequests.
+ DONT_REAP
+ }
+
+ private enum UnneededFor {
+ LINGER, // Determine whether this network is unneeded and should be lingered.
+ TEARDOWN, // Determine whether this network is unneeded and should be torn down.
+ }
+
+ /**
+ * used internally to clear a wakelock when transitioning
+ * from one net to another. Clear happens when we get a new
+ * network - EVENT_EXPIRE_NET_TRANSITION_WAKELOCK happens
+ * after a timeout if no network is found (typically 1 min).
+ */
+ private static final int EVENT_CLEAR_NET_TRANSITION_WAKELOCK = 8;
+
+ /**
+ * used internally to reload global proxy settings
+ */
+ private static final int EVENT_APPLY_GLOBAL_HTTP_PROXY = 9;
+
+ /**
+ * PAC manager has received new port.
+ */
+ private static final int EVENT_PROXY_HAS_CHANGED = 16;
+
+ /**
+ * used internally when registering NetworkProviders
+ * obj = NetworkProviderInfo
+ */
+ private static final int EVENT_REGISTER_NETWORK_PROVIDER = 17;
+
+ /**
+ * used internally when registering NetworkAgents
+ * obj = Messenger
+ */
+ private static final int EVENT_REGISTER_NETWORK_AGENT = 18;
+
+ /**
+ * used to add a network request
+ * includes a NetworkRequestInfo
+ */
+ private static final int EVENT_REGISTER_NETWORK_REQUEST = 19;
+
+ /**
+ * indicates a timeout period is over - check if we had a network yet or not
+ * and if not, call the timeout callback (but leave the request live until they
+ * cancel it.
+ * includes a NetworkRequestInfo
+ */
+ private static final int EVENT_TIMEOUT_NETWORK_REQUEST = 20;
+
+ /**
+ * used to add a network listener - no request
+ * includes a NetworkRequestInfo
+ */
+ private static final int EVENT_REGISTER_NETWORK_LISTENER = 21;
+
+ /**
+ * used to remove a network request, either a listener or a real request
+ * arg1 = UID of caller
+ * obj = NetworkRequest
+ */
+ private static final int EVENT_RELEASE_NETWORK_REQUEST = 22;
+
+ /**
+ * used internally when registering NetworkProviders
+ * obj = Messenger
+ */
+ private static final int EVENT_UNREGISTER_NETWORK_PROVIDER = 23;
+
+ /**
+ * used internally to expire a wakelock when transitioning
+ * from one net to another. Expire happens when we fail to find
+ * a new network (typically after 1 minute) -
+ * EVENT_CLEAR_NET_TRANSITION_WAKELOCK happens if we had found
+ * a replacement network.
+ */
+ private static final int EVENT_EXPIRE_NET_TRANSITION_WAKELOCK = 24;
+
+ /**
+ * used to add a network request with a pending intent
+ * obj = NetworkRequestInfo
+ */
+ private static final int EVENT_REGISTER_NETWORK_REQUEST_WITH_INTENT = 26;
+
+ /**
+ * used to remove a pending intent and its associated network request.
+ * arg1 = UID of caller
+ * obj = PendingIntent
+ */
+ private static final int EVENT_RELEASE_NETWORK_REQUEST_WITH_INTENT = 27;
+
+ /**
+ * used to specify whether a network should be used even if unvalidated.
+ * arg1 = whether to accept the network if it's unvalidated (1 or 0)
+ * arg2 = whether to remember this choice in the future (1 or 0)
+ * obj = network
+ */
+ private static final int EVENT_SET_ACCEPT_UNVALIDATED = 28;
+
+ /**
+ * used to ask the user to confirm a connection to an unvalidated network.
+ * obj = network
+ */
+ private static final int EVENT_PROMPT_UNVALIDATED = 29;
+
+ /**
+ * used internally to (re)configure always-on networks.
+ */
+ private static final int EVENT_CONFIGURE_ALWAYS_ON_NETWORKS = 30;
+
+ /**
+ * used to add a network listener with a pending intent
+ * obj = NetworkRequestInfo
+ */
+ private static final int EVENT_REGISTER_NETWORK_LISTENER_WITH_INTENT = 31;
+
+ /**
+ * used to specify whether a network should not be penalized when it becomes unvalidated.
+ */
+ private static final int EVENT_SET_AVOID_UNVALIDATED = 35;
+
+ /**
+ * used to trigger revalidation of a network.
+ */
+ private static final int EVENT_REVALIDATE_NETWORK = 36;
+
+ // Handle changes in Private DNS settings.
+ private static final int EVENT_PRIVATE_DNS_SETTINGS_CHANGED = 37;
+
+ // Handle private DNS validation status updates.
+ private static final int EVENT_PRIVATE_DNS_VALIDATION_UPDATE = 38;
+
+ /**
+ * Event for NetworkMonitor/NetworkAgentInfo to inform ConnectivityService that the network has
+ * been tested.
+ * obj = {@link NetworkTestedResults} representing information sent from NetworkMonitor.
+ * data = PersistableBundle of extras passed from NetworkMonitor. If {@link
+ * NetworkMonitorCallbacks#notifyNetworkTested} is called, this will be null.
+ */
+ private static final int EVENT_NETWORK_TESTED = 41;
+
+ /**
+ * Event for NetworkMonitor/NetworkAgentInfo to inform ConnectivityService that the private DNS
+ * config was resolved.
+ * obj = PrivateDnsConfig
+ * arg2 = netid
+ */
+ private static final int EVENT_PRIVATE_DNS_CONFIG_RESOLVED = 42;
+
+ /**
+ * Request ConnectivityService display provisioning notification.
+ * arg1 = Whether to make the notification visible.
+ * arg2 = NetID.
+ * obj = Intent to be launched when notification selected by user, null if !arg1.
+ */
+ private static final int EVENT_PROVISIONING_NOTIFICATION = 43;
+
+ /**
+ * Used to specify whether a network should be used even if connectivity is partial.
+ * arg1 = whether to accept the network if its connectivity is partial (1 for true or 0 for
+ * false)
+ * arg2 = whether to remember this choice in the future (1 for true or 0 for false)
+ * obj = network
+ */
+ private static final int EVENT_SET_ACCEPT_PARTIAL_CONNECTIVITY = 44;
+
+ /**
+ * Event for NetworkMonitor to inform ConnectivityService that the probe status has changed.
+ * Both of the arguments are bitmasks, and the value of bits come from
+ * INetworkMonitor.NETWORK_VALIDATION_PROBE_*.
+ * arg1 = A bitmask to describe which probes are completed.
+ * arg2 = A bitmask to describe which probes are successful.
+ */
+ public static final int EVENT_PROBE_STATUS_CHANGED = 45;
+
+ /**
+ * Event for NetworkMonitor to inform ConnectivityService that captive portal data has changed.
+ * arg1 = unused
+ * arg2 = netId
+ * obj = captive portal data
+ */
+ private static final int EVENT_CAPPORT_DATA_CHANGED = 46;
+
+ /**
+ * Used by setRequireVpnForUids.
+ * arg1 = whether the specified UID ranges are required to use a VPN.
+ * obj = Array of UidRange objects.
+ */
+ private static final int EVENT_SET_REQUIRE_VPN_FOR_UIDS = 47;
+
+ /**
+ * Used internally when setting the default networks for OemNetworkPreferences.
+ * obj = Pair<OemNetworkPreferences, listener>
+ */
+ private static final int EVENT_SET_OEM_NETWORK_PREFERENCE = 48;
+
+ /**
+ * Used to indicate the system default network becomes active.
+ */
+ private static final int EVENT_REPORT_NETWORK_ACTIVITY = 49;
+
+ /**
+ * Used internally when setting a network preference for a user profile.
+ * obj = Pair<ProfileNetworkPreference, Listener>
+ */
+ private static final int EVENT_SET_PROFILE_NETWORK_PREFERENCE = 50;
+
+ /**
+ * Event to specify that reasons for why an uid is blocked changed.
+ * arg1 = uid
+ * arg2 = blockedReasons
+ */
+ private static final int EVENT_UID_BLOCKED_REASON_CHANGED = 51;
+
+ /**
+ * Event to register a new network offer
+ * obj = NetworkOffer
+ */
+ private static final int EVENT_REGISTER_NETWORK_OFFER = 52;
+
+ /**
+ * Event to unregister an existing network offer
+ * obj = INetworkOfferCallback
+ */
+ private static final int EVENT_UNREGISTER_NETWORK_OFFER = 53;
+
+ /**
+ * Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification
+ * should be shown.
+ */
+ private static final int PROVISIONING_NOTIFICATION_SHOW = 1;
+
+ /**
+ * Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification
+ * should be hidden.
+ */
+ private static final int PROVISIONING_NOTIFICATION_HIDE = 0;
+
+ private static String eventName(int what) {
+ return sMagicDecoderRing.get(what, Integer.toString(what));
+ }
+
+ private static IDnsResolver getDnsResolver(Context context) {
+ final DnsResolverServiceManager dsm = context.getSystemService(
+ DnsResolverServiceManager.class);
+ return IDnsResolver.Stub.asInterface(dsm.getService());
+ }
+
+ /** Handler thread used for all of the handlers below. */
+ @VisibleForTesting
+ protected final HandlerThread mHandlerThread;
+ /** Handler used for internal events. */
+ final private InternalHandler mHandler;
+ /** Handler used for incoming {@link NetworkStateTracker} events. */
+ final private NetworkStateTrackerHandler mTrackerHandler;
+ /** Handler used for processing {@link android.net.ConnectivityDiagnosticsManager} events */
+ @VisibleForTesting
+ final ConnectivityDiagnosticsHandler mConnectivityDiagnosticsHandler;
+
+ private final DnsManager mDnsManager;
+ private final NetworkRanker mNetworkRanker;
+
+ private boolean mSystemReady;
+ private Intent mInitialBroadcast;
+
+ private PowerManager.WakeLock mNetTransitionWakeLock;
+ private final PowerManager.WakeLock mPendingIntentWakeLock;
+
+ // A helper object to track the current default HTTP proxy. ConnectivityService needs to tell
+ // the world when it changes.
+ @VisibleForTesting
+ protected final ProxyTracker mProxyTracker;
+
+ final private SettingsObserver mSettingsObserver;
+
+ private UserManager mUserManager;
+
+ // the set of network types that can only be enabled by system/sig apps
+ private List<Integer> mProtectedNetworks;
+
+ private Set<String> mWolSupportedInterfaces;
+
+ private final TelephonyManager mTelephonyManager;
+ private final AppOpsManager mAppOpsManager;
+
+ private final LocationPermissionChecker mLocationPermissionChecker;
+
+ private KeepaliveTracker mKeepaliveTracker;
+ private QosCallbackTracker mQosCallbackTracker;
+ private NetworkNotificationManager mNotifier;
+ private LingerMonitor mLingerMonitor;
+
+ // sequence number of NetworkRequests
+ private int mNextNetworkRequestId = NetworkRequest.FIRST_REQUEST_ID;
+
+ // Sequence number for NetworkProvider IDs.
+ private final AtomicInteger mNextNetworkProviderId = new AtomicInteger(
+ NetworkProvider.FIRST_PROVIDER_ID);
+
+ // NetworkRequest activity String log entries.
+ private static final int MAX_NETWORK_REQUEST_LOGS = 20;
+ private final LocalLog mNetworkRequestInfoLogs = new LocalLog(MAX_NETWORK_REQUEST_LOGS);
+
+ // NetworkInfo blocked and unblocked String log entries
+ private static final int MAX_NETWORK_INFO_LOGS = 40;
+ private final LocalLog mNetworkInfoBlockingLogs = new LocalLog(MAX_NETWORK_INFO_LOGS);
+
+ private static final int MAX_WAKELOCK_LOGS = 20;
+ private final LocalLog mWakelockLogs = new LocalLog(MAX_WAKELOCK_LOGS);
+ private int mTotalWakelockAcquisitions = 0;
+ private int mTotalWakelockReleases = 0;
+ private long mTotalWakelockDurationMs = 0;
+ private long mMaxWakelockDurationMs = 0;
+ private long mLastWakeLockAcquireTimestamp = 0;
+
+ private final IpConnectivityLog mMetricsLog;
+
+ @GuardedBy("mBandwidthRequests")
+ private final SparseArray<Integer> mBandwidthRequests = new SparseArray(10);
+
+ @VisibleForTesting
+ final MultinetworkPolicyTracker mMultinetworkPolicyTracker;
+
+ @VisibleForTesting
+ final Map<IBinder, ConnectivityDiagnosticsCallbackInfo> mConnectivityDiagnosticsCallbacks =
+ new HashMap<>();
+
+ /**
+ * Implements support for the legacy "one network per network type" model.
+ *
+ * We used to have a static array of NetworkStateTrackers, one for each
+ * network type, but that doesn't work any more now that we can have,
+ * for example, more that one wifi network. This class stores all the
+ * NetworkAgentInfo objects that support a given type, but the legacy
+ * API will only see the first one.
+ *
+ * It serves two main purposes:
+ *
+ * 1. Provide information about "the network for a given type" (since this
+ * API only supports one).
+ * 2. Send legacy connectivity change broadcasts. Broadcasts are sent if
+ * the first network for a given type changes, or if the default network
+ * changes.
+ */
+ @VisibleForTesting
+ static class LegacyTypeTracker {
+
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ /**
+ * Array of lists, one per legacy network type (e.g., TYPE_MOBILE_MMS).
+ * Each list holds references to all NetworkAgentInfos that are used to
+ * satisfy requests for that network type.
+ *
+ * This array is built out at startup such that an unsupported network
+ * doesn't get an ArrayList instance, making this a tristate:
+ * unsupported, supported but not active and active.
+ *
+ * The actual lists are populated when we scan the network types that
+ * are supported on this device.
+ *
+ * Threading model:
+ * - addSupportedType() is only called in the constructor
+ * - add(), update(), remove() are only called from the ConnectivityService handler thread.
+ * They are therefore not thread-safe with respect to each other.
+ * - getNetworkForType() can be called at any time on binder threads. It is synchronized
+ * on mTypeLists to be thread-safe with respect to a concurrent remove call.
+ * - getRestoreTimerForType(type) is also synchronized on mTypeLists.
+ * - dump is thread-safe with respect to concurrent add and remove calls.
+ */
+ private final ArrayList<NetworkAgentInfo> mTypeLists[];
+ @NonNull
+ private final ConnectivityService mService;
+
+ // Restore timers for requestNetworkForFeature (network type -> timer in ms). Types without
+ // an entry have no timer (equivalent to -1). Lazily loaded.
+ @NonNull
+ private ArrayMap<Integer, Integer> mRestoreTimers = new ArrayMap<>();
+
+ LegacyTypeTracker(@NonNull ConnectivityService service) {
+ mService = service;
+ mTypeLists = new ArrayList[ConnectivityManager.MAX_NETWORK_TYPE + 1];
+ }
+
+ public void loadSupportedTypes(@NonNull Context ctx, @NonNull TelephonyManager tm) {
+ final PackageManager pm = ctx.getPackageManager();
+ if (pm.hasSystemFeature(FEATURE_WIFI)) {
+ addSupportedType(TYPE_WIFI);
+ }
+ if (pm.hasSystemFeature(FEATURE_WIFI_DIRECT)) {
+ addSupportedType(TYPE_WIFI_P2P);
+ }
+ if (tm.isDataCapable()) {
+ // Telephony does not have granular support for these types: they are either all
+ // supported, or none is supported
+ addSupportedType(TYPE_MOBILE);
+ addSupportedType(TYPE_MOBILE_MMS);
+ addSupportedType(TYPE_MOBILE_SUPL);
+ addSupportedType(TYPE_MOBILE_DUN);
+ addSupportedType(TYPE_MOBILE_HIPRI);
+ addSupportedType(TYPE_MOBILE_FOTA);
+ addSupportedType(TYPE_MOBILE_IMS);
+ addSupportedType(TYPE_MOBILE_CBS);
+ addSupportedType(TYPE_MOBILE_IA);
+ addSupportedType(TYPE_MOBILE_EMERGENCY);
+ }
+ if (pm.hasSystemFeature(FEATURE_BLUETOOTH)) {
+ addSupportedType(TYPE_BLUETOOTH);
+ }
+ if (pm.hasSystemFeature(FEATURE_WATCH)) {
+ // TYPE_PROXY is only used on Wear
+ addSupportedType(TYPE_PROXY);
+ }
+ // Ethernet is often not specified in the configs, although many devices can use it via
+ // USB host adapters. Add it as long as the ethernet service is here.
+ if (ctx.getSystemService(Context.ETHERNET_SERVICE) != null) {
+ addSupportedType(TYPE_ETHERNET);
+ }
+
+ // Always add TYPE_VPN as a supported type
+ addSupportedType(TYPE_VPN);
+ }
+
+ private void addSupportedType(int type) {
+ if (mTypeLists[type] != null) {
+ throw new IllegalStateException(
+ "legacy list for type " + type + "already initialized");
+ }
+ mTypeLists[type] = new ArrayList<>();
+ }
+
+ public boolean isTypeSupported(int type) {
+ return isNetworkTypeValid(type) && mTypeLists[type] != null;
+ }
+
+ public NetworkAgentInfo getNetworkForType(int type) {
+ synchronized (mTypeLists) {
+ if (isTypeSupported(type) && !mTypeLists[type].isEmpty()) {
+ return mTypeLists[type].get(0);
+ }
+ }
+ return null;
+ }
+
+ public int getRestoreTimerForType(int type) {
+ synchronized (mTypeLists) {
+ if (mRestoreTimers == null) {
+ mRestoreTimers = loadRestoreTimers();
+ }
+ return mRestoreTimers.getOrDefault(type, -1);
+ }
+ }
+
+ private ArrayMap<Integer, Integer> loadRestoreTimers() {
+ final String[] configs = mService.mResources.get().getStringArray(
+ R.array.config_legacy_networktype_restore_timers);
+ final ArrayMap<Integer, Integer> ret = new ArrayMap<>(configs.length);
+ for (final String config : configs) {
+ final String[] splits = TextUtils.split(config, ",");
+ if (splits.length != 2) {
+ logwtf("Invalid restore timer token count: " + config);
+ continue;
+ }
+ try {
+ ret.put(Integer.parseInt(splits[0]), Integer.parseInt(splits[1]));
+ } catch (NumberFormatException e) {
+ logwtf("Invalid restore timer number format: " + config, e);
+ }
+ }
+ return ret;
+ }
+
+ private void maybeLogBroadcast(NetworkAgentInfo nai, DetailedState state, int type,
+ boolean isDefaultNetwork) {
+ if (DBG) {
+ log("Sending " + state
+ + " broadcast for type " + type + " " + nai.toShortString()
+ + " isDefaultNetwork=" + isDefaultNetwork);
+ }
+ }
+
+ // When a lockdown VPN connects, send another CONNECTED broadcast for the underlying
+ // network type, to preserve previous behaviour.
+ private void maybeSendLegacyLockdownBroadcast(@NonNull NetworkAgentInfo vpnNai) {
+ if (vpnNai != mService.getLegacyLockdownNai()) return;
+
+ if (vpnNai.declaredUnderlyingNetworks == null
+ || vpnNai.declaredUnderlyingNetworks.length != 1) {
+ Log.wtf(TAG, "Legacy lockdown VPN must have exactly one underlying network: "
+ + Arrays.toString(vpnNai.declaredUnderlyingNetworks));
+ return;
+ }
+ final NetworkAgentInfo underlyingNai = mService.getNetworkAgentInfoForNetwork(
+ vpnNai.declaredUnderlyingNetworks[0]);
+ if (underlyingNai == null) return;
+
+ final int type = underlyingNai.networkInfo.getType();
+ final DetailedState state = DetailedState.CONNECTED;
+ maybeLogBroadcast(underlyingNai, state, type, true /* isDefaultNetwork */);
+ mService.sendLegacyNetworkBroadcast(underlyingNai, state, type);
+ }
+
+ /** Adds the given network to the specified legacy type list. */
+ public void add(int type, NetworkAgentInfo nai) {
+ if (!isTypeSupported(type)) {
+ return; // Invalid network type.
+ }
+ if (VDBG) log("Adding agent " + nai + " for legacy network type " + type);
+
+ ArrayList<NetworkAgentInfo> list = mTypeLists[type];
+ if (list.contains(nai)) {
+ return;
+ }
+ synchronized (mTypeLists) {
+ list.add(nai);
+ }
+
+ // Send a broadcast if this is the first network of its type or if it's the default.
+ final boolean isDefaultNetwork = mService.isDefaultNetwork(nai);
+
+ // If a legacy lockdown VPN is active, override the NetworkInfo state in all broadcasts
+ // to preserve previous behaviour.
+ final DetailedState state = mService.getLegacyLockdownState(DetailedState.CONNECTED);
+ if ((list.size() == 1) || isDefaultNetwork) {
+ maybeLogBroadcast(nai, state, type, isDefaultNetwork);
+ mService.sendLegacyNetworkBroadcast(nai, state, type);
+ }
+
+ if (type == TYPE_VPN && state == DetailedState.CONNECTED) {
+ maybeSendLegacyLockdownBroadcast(nai);
+ }
+ }
+
+ /** Removes the given network from the specified legacy type list. */
+ public void remove(int type, NetworkAgentInfo nai, boolean wasDefault) {
+ ArrayList<NetworkAgentInfo> list = mTypeLists[type];
+ if (list == null || list.isEmpty()) {
+ return;
+ }
+ final boolean wasFirstNetwork = list.get(0).equals(nai);
+
+ synchronized (mTypeLists) {
+ if (!list.remove(nai)) {
+ return;
+ }
+ }
+
+ if (wasFirstNetwork || wasDefault) {
+ maybeLogBroadcast(nai, DetailedState.DISCONNECTED, type, wasDefault);
+ mService.sendLegacyNetworkBroadcast(nai, DetailedState.DISCONNECTED, type);
+ }
+
+ if (!list.isEmpty() && wasFirstNetwork) {
+ if (DBG) log("Other network available for type " + type +
+ ", sending connected broadcast");
+ final NetworkAgentInfo replacement = list.get(0);
+ maybeLogBroadcast(replacement, DetailedState.CONNECTED, type,
+ mService.isDefaultNetwork(replacement));
+ mService.sendLegacyNetworkBroadcast(replacement, DetailedState.CONNECTED, type);
+ }
+ }
+
+ /** Removes the given network from all legacy type lists. */
+ public void remove(NetworkAgentInfo nai, boolean wasDefault) {
+ if (VDBG) log("Removing agent " + nai + " wasDefault=" + wasDefault);
+ for (int type = 0; type < mTypeLists.length; type++) {
+ remove(type, nai, wasDefault);
+ }
+ }
+
+ // send out another legacy broadcast - currently only used for suspend/unsuspend
+ // toggle
+ public void update(NetworkAgentInfo nai) {
+ final boolean isDefault = mService.isDefaultNetwork(nai);
+ final DetailedState state = nai.networkInfo.getDetailedState();
+ for (int type = 0; type < mTypeLists.length; type++) {
+ final ArrayList<NetworkAgentInfo> list = mTypeLists[type];
+ final boolean contains = (list != null && list.contains(nai));
+ final boolean isFirst = contains && (nai == list.get(0));
+ if (isFirst || contains && isDefault) {
+ maybeLogBroadcast(nai, state, type, isDefault);
+ mService.sendLegacyNetworkBroadcast(nai, state, type);
+ }
+ }
+ }
+
+ public void dump(IndentingPrintWriter pw) {
+ pw.println("mLegacyTypeTracker:");
+ pw.increaseIndent();
+ pw.print("Supported types:");
+ for (int type = 0; type < mTypeLists.length; type++) {
+ if (mTypeLists[type] != null) pw.print(" " + type);
+ }
+ pw.println();
+ pw.println("Current state:");
+ pw.increaseIndent();
+ synchronized (mTypeLists) {
+ for (int type = 0; type < mTypeLists.length; type++) {
+ if (mTypeLists[type] == null || mTypeLists[type].isEmpty()) continue;
+ for (NetworkAgentInfo nai : mTypeLists[type]) {
+ pw.println(type + " " + nai.toShortString());
+ }
+ }
+ }
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+ pw.println();
+ }
+ }
+ private final LegacyTypeTracker mLegacyTypeTracker = new LegacyTypeTracker(this);
+
+ final LocalPriorityDump mPriorityDumper = new LocalPriorityDump();
+ /**
+ * Helper class which parses out priority arguments and dumps sections according to their
+ * priority. If priority arguments are omitted, function calls the legacy dump command.
+ */
+ private class LocalPriorityDump {
+ private static final String PRIORITY_ARG = "--dump-priority";
+ private static final String PRIORITY_ARG_HIGH = "HIGH";
+ private static final String PRIORITY_ARG_NORMAL = "NORMAL";
+
+ LocalPriorityDump() {}
+
+ private void dumpHigh(FileDescriptor fd, PrintWriter pw) {
+ doDump(fd, pw, new String[] {DIAG_ARG});
+ doDump(fd, pw, new String[] {SHORT_ARG});
+ }
+
+ private void dumpNormal(FileDescriptor fd, PrintWriter pw, String[] args) {
+ doDump(fd, pw, args);
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (args == null) {
+ dumpNormal(fd, pw, args);
+ return;
+ }
+
+ String priority = null;
+ for (int argIndex = 0; argIndex < args.length; argIndex++) {
+ if (args[argIndex].equals(PRIORITY_ARG) && argIndex + 1 < args.length) {
+ argIndex++;
+ priority = args[argIndex];
+ }
+ }
+
+ if (PRIORITY_ARG_HIGH.equals(priority)) {
+ dumpHigh(fd, pw);
+ } else if (PRIORITY_ARG_NORMAL.equals(priority)) {
+ dumpNormal(fd, pw, args);
+ } else {
+ // ConnectivityService publishes binder service using publishBinderService() with
+ // no priority assigned will be treated as NORMAL priority. Dumpsys does not send
+ // "--dump-priority" arguments to the service. Thus, dump NORMAL only to align the
+ // legacy output for dumpsys connectivity.
+ // TODO: Integrate into signal dump.
+ dumpNormal(fd, pw, args);
+ }
+ }
+ }
+
+ /**
+ * Keeps track of the number of requests made under different uids.
+ */
+ public static class PerUidCounter {
+ private final int mMaxCountPerUid;
+
+ // Map from UID to number of NetworkRequests that UID has filed.
+ @VisibleForTesting
+ @GuardedBy("mUidToNetworkRequestCount")
+ final SparseIntArray mUidToNetworkRequestCount = new SparseIntArray();
+
+ /**
+ * Constructor
+ *
+ * @param maxCountPerUid the maximum count per uid allowed
+ */
+ public PerUidCounter(final int maxCountPerUid) {
+ mMaxCountPerUid = maxCountPerUid;
+ }
+
+ /**
+ * Increments the request count of the given uid. Throws an exception if the number
+ * of open requests for the uid exceeds the value of maxCounterPerUid which is the value
+ * passed into the constructor. see: {@link #PerUidCounter(int)}.
+ *
+ * @throws ServiceSpecificException with
+ * {@link ConnectivityManager.Errors.TOO_MANY_REQUESTS} if the number of requests for
+ * the uid exceed the allowed number.
+ *
+ * @param uid the uid that the request was made under
+ */
+ public void incrementCountOrThrow(final int uid) {
+ synchronized (mUidToNetworkRequestCount) {
+ incrementCountOrThrow(uid, 1 /* numToIncrement */);
+ }
+ }
+
+ private void incrementCountOrThrow(final int uid, final int numToIncrement) {
+ final int newRequestCount =
+ mUidToNetworkRequestCount.get(uid, 0) + numToIncrement;
+ if (newRequestCount >= mMaxCountPerUid) {
+ throw new ServiceSpecificException(
+ ConnectivityManager.Errors.TOO_MANY_REQUESTS);
+ }
+ mUidToNetworkRequestCount.put(uid, newRequestCount);
+ }
+
+ /**
+ * Decrements the request count of the given uid.
+ *
+ * @param uid the uid that the request was made under
+ */
+ public void decrementCount(final int uid) {
+ synchronized (mUidToNetworkRequestCount) {
+ decrementCount(uid, 1 /* numToDecrement */);
+ }
+ }
+
+ private void decrementCount(final int uid, final int numToDecrement) {
+ final int newRequestCount =
+ mUidToNetworkRequestCount.get(uid, 0) - numToDecrement;
+ if (newRequestCount < 0) {
+ logwtf("BUG: too small request count " + newRequestCount + " for UID " + uid);
+ } else if (newRequestCount == 0) {
+ mUidToNetworkRequestCount.delete(uid);
+ } else {
+ mUidToNetworkRequestCount.put(uid, newRequestCount);
+ }
+ }
+
+ /**
+ * Used to adjust the request counter for the per-app API flows. Directly adjusting the
+ * counter is not ideal however in the per-app flows, the nris can't be removed until they
+ * are used to create the new nris upon set. Therefore the request count limit can be
+ * artificially hit. This method is used as a workaround for this particular case so that
+ * the request counts are accounted for correctly.
+ * @param uid the uid to adjust counts for
+ * @param numOfNewRequests the new request count to account for
+ * @param r the runnable to execute
+ */
+ public void transact(final int uid, final int numOfNewRequests, @NonNull final Runnable r) {
+ // This should only be used on the handler thread as per all current and foreseen
+ // use-cases. ensureRunningOnConnectivityServiceThread() can't be used because there is
+ // no ref to the outer ConnectivityService.
+ synchronized (mUidToNetworkRequestCount) {
+ final int reqCountOverage = getCallingUidRequestCountOverage(uid, numOfNewRequests);
+ decrementCount(uid, reqCountOverage);
+ r.run();
+ incrementCountOrThrow(uid, reqCountOverage);
+ }
+ }
+
+ private int getCallingUidRequestCountOverage(final int uid, final int numOfNewRequests) {
+ final int newUidRequestCount = mUidToNetworkRequestCount.get(uid, 0)
+ + numOfNewRequests;
+ return newUidRequestCount >= MAX_NETWORK_REQUESTS_PER_SYSTEM_UID
+ ? newUidRequestCount - (MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - 1) : 0;
+ }
+ }
+
+ /**
+ * Dependencies of ConnectivityService, for injection in tests.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ public int getCallingUid() {
+ return Binder.getCallingUid();
+ }
+
+ /**
+ * Get system properties to use in ConnectivityService.
+ */
+ public MockableSystemProperties getSystemProperties() {
+ return new MockableSystemProperties();
+ }
+
+ /**
+ * Get the {@link ConnectivityResources} to use in ConnectivityService.
+ */
+ public ConnectivityResources getResources(@NonNull Context ctx) {
+ return new ConnectivityResources(ctx);
+ }
+
+ /**
+ * Create a HandlerThread to use in ConnectivityService.
+ */
+ public HandlerThread makeHandlerThread() {
+ return new HandlerThread("ConnectivityServiceThread");
+ }
+
+ /**
+ * Get a reference to the NetworkStackClient.
+ */
+ public NetworkStackClient getNetworkStack() {
+ return NetworkStackClient.getInstance();
+ }
+
+ /**
+ * @see ProxyTracker
+ */
+ public ProxyTracker makeProxyTracker(@NonNull Context context,
+ @NonNull Handler connServiceHandler) {
+ return new ProxyTracker(context, connServiceHandler, EVENT_PROXY_HAS_CHANGED);
+ }
+
+ /**
+ * @see NetIdManager
+ */
+ public NetIdManager makeNetIdManager() {
+ return new NetIdManager();
+ }
+
+ /**
+ * @see NetworkUtils#queryUserAccess(int, int)
+ */
+ public boolean queryUserAccess(int uid, Network network, ConnectivityService cs) {
+ return cs.queryUserAccess(uid, network);
+ }
+
+ /**
+ * Gets the UID that owns a socket connection. Needed because opening SOCK_DIAG sockets
+ * requires CAP_NET_ADMIN, which the unit tests do not have.
+ */
+ public int getConnectionOwnerUid(int protocol, InetSocketAddress local,
+ InetSocketAddress remote) {
+ return InetDiagMessage.getConnectionOwnerUid(protocol, local, remote);
+ }
+
+ /**
+ * @see MultinetworkPolicyTracker
+ */
+ public MultinetworkPolicyTracker makeMultinetworkPolicyTracker(
+ @NonNull Context c, @NonNull Handler h, @NonNull Runnable r) {
+ return new MultinetworkPolicyTracker(c, h, r);
+ }
+
+ /**
+ * @see BatteryStatsManager
+ */
+ public void reportNetworkInterfaceForTransports(Context context, String iface,
+ int[] transportTypes) {
+ final BatteryStatsManager batteryStats =
+ context.getSystemService(BatteryStatsManager.class);
+ batteryStats.reportNetworkInterfaceForTransports(iface, transportTypes);
+ }
+
+ public boolean getCellular464XlatEnabled() {
+ return NetworkProperties.isCellular464XlatEnabled().orElse(true);
+ }
+ }
+
+ public ConnectivityService(Context context) {
+ this(context, getDnsResolver(context), new IpConnectivityLog(),
+ NetdService.getInstance(), new Dependencies());
+ }
+
+ @VisibleForTesting
+ protected ConnectivityService(Context context, IDnsResolver dnsresolver,
+ IpConnectivityLog logger, INetd netd, Dependencies deps) {
+ if (DBG) log("ConnectivityService starting up");
+
+ mDeps = Objects.requireNonNull(deps, "missing Dependencies");
+ mSystemProperties = mDeps.getSystemProperties();
+ mNetIdManager = mDeps.makeNetIdManager();
+ mContext = Objects.requireNonNull(context, "missing Context");
+ mResources = deps.getResources(mContext);
+ mNetworkRequestCounter = new PerUidCounter(MAX_NETWORK_REQUESTS_PER_UID);
+ mSystemNetworkRequestCounter = new PerUidCounter(MAX_NETWORK_REQUESTS_PER_SYSTEM_UID);
+
+ mMetricsLog = logger;
+ mNetworkRanker = new NetworkRanker();
+ final NetworkRequest defaultInternetRequest = createDefaultRequest();
+ mDefaultRequest = new NetworkRequestInfo(
+ Process.myUid(), defaultInternetRequest, null,
+ new Binder(), NetworkCallback.FLAG_INCLUDE_LOCATION_INFO,
+ null /* attributionTags */);
+ mNetworkRequests.put(defaultInternetRequest, mDefaultRequest);
+ mDefaultNetworkRequests.add(mDefaultRequest);
+ mNetworkRequestInfoLogs.log("REGISTER " + mDefaultRequest);
+
+ mDefaultMobileDataRequest = createDefaultInternetRequestForTransport(
+ NetworkCapabilities.TRANSPORT_CELLULAR, NetworkRequest.Type.BACKGROUND_REQUEST);
+
+ // The default WiFi request is a background request so that apps using WiFi are
+ // migrated to a better network (typically ethernet) when one comes up, instead
+ // of staying on WiFi forever.
+ mDefaultWifiRequest = createDefaultInternetRequestForTransport(
+ NetworkCapabilities.TRANSPORT_WIFI, NetworkRequest.Type.BACKGROUND_REQUEST);
+
+ mDefaultVehicleRequest = createAlwaysOnRequestForCapability(
+ NetworkCapabilities.NET_CAPABILITY_VEHICLE_INTERNAL,
+ NetworkRequest.Type.BACKGROUND_REQUEST);
+
+ mHandlerThread = mDeps.makeHandlerThread();
+ mHandlerThread.start();
+ mHandler = new InternalHandler(mHandlerThread.getLooper());
+ mTrackerHandler = new NetworkStateTrackerHandler(mHandlerThread.getLooper());
+ mConnectivityDiagnosticsHandler =
+ new ConnectivityDiagnosticsHandler(mHandlerThread.getLooper());
+
+ mReleasePendingIntentDelayMs = Settings.Secure.getInt(context.getContentResolver(),
+ ConnectivitySettingsManager.CONNECTIVITY_RELEASE_PENDING_INTENT_DELAY_MS, 5_000);
+
+ mLingerDelayMs = mSystemProperties.getInt(LINGER_DELAY_PROPERTY, DEFAULT_LINGER_DELAY_MS);
+ // TODO: Consider making the timer customizable.
+ mNascentDelayMs = DEFAULT_NASCENT_DELAY_MS;
+
+ mStatsManager = mContext.getSystemService(NetworkStatsManager.class);
+ mPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);
+ mDnsResolver = Objects.requireNonNull(dnsresolver, "missing IDnsResolver");
+ mProxyTracker = mDeps.makeProxyTracker(mContext, mHandler);
+
+ mNetd = netd;
+ mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
+ mLocationPermissionChecker = new LocationPermissionChecker(mContext);
+
+ // To ensure uid state is synchronized with Network Policy, register for
+ // NetworkPolicyManagerService events must happen prior to NetworkPolicyManagerService
+ // reading existing policy from disk.
+ mPolicyManager.registerNetworkPolicyCallback(null, mPolicyCallback);
+
+ final PowerManager powerManager = (PowerManager) context.getSystemService(
+ Context.POWER_SERVICE);
+ mNetTransitionWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+ mPendingIntentWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+
+ mLegacyTypeTracker.loadSupportedTypes(mContext, mTelephonyManager);
+ mProtectedNetworks = new ArrayList<>();
+ int[] protectedNetworks = mResources.get().getIntArray(R.array.config_protectedNetworks);
+ for (int p : protectedNetworks) {
+ if (mLegacyTypeTracker.isTypeSupported(p) && !mProtectedNetworks.contains(p)) {
+ mProtectedNetworks.add(p);
+ } else {
+ if (DBG) loge("Ignoring protectedNetwork " + p);
+ }
+ }
+
+ mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+
+ mPermissionMonitor = new PermissionMonitor(mContext, mNetd);
+
+ mUserAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */);
+ // Listen for user add/removes to inform PermissionMonitor.
+ // Should run on mHandler to avoid any races.
+ final IntentFilter userIntentFilter = new IntentFilter();
+ userIntentFilter.addAction(Intent.ACTION_USER_ADDED);
+ userIntentFilter.addAction(Intent.ACTION_USER_REMOVED);
+ mUserAllContext.registerReceiver(mUserIntentReceiver, userIntentFilter,
+ null /* broadcastPermission */, mHandler);
+
+ // Listen to package add/removes for netd
+ final IntentFilter packageIntentFilter = new IntentFilter();
+ packageIntentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ packageIntentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+ packageIntentFilter.addDataScheme("package");
+ mUserAllContext.registerReceiver(mPackageIntentReceiver, packageIntentFilter,
+ null /* broadcastPermission */, mHandler);
+
+ mNetworkActivityTracker = new LegacyNetworkActivityTracker(mContext, mHandler, mNetd);
+
+ mNetdCallback = new NetdCallback();
+ try {
+ mNetd.registerUnsolicitedEventListener(mNetdCallback);
+ } catch (RemoteException | ServiceSpecificException e) {
+ loge("Error registering event listener :" + e);
+ }
+
+ mSettingsObserver = new SettingsObserver(mContext, mHandler);
+ registerSettingsCallbacks();
+
+ mKeepaliveTracker = new KeepaliveTracker(mContext, mHandler);
+ mNotifier = new NetworkNotificationManager(mContext, mTelephonyManager);
+ mQosCallbackTracker = new QosCallbackTracker(mHandler, mNetworkRequestCounter);
+
+ final int dailyLimit = Settings.Global.getInt(mContext.getContentResolver(),
+ ConnectivitySettingsManager.NETWORK_SWITCH_NOTIFICATION_DAILY_LIMIT,
+ LingerMonitor.DEFAULT_NOTIFICATION_DAILY_LIMIT);
+ final long rateLimit = Settings.Global.getLong(mContext.getContentResolver(),
+ ConnectivitySettingsManager.NETWORK_SWITCH_NOTIFICATION_RATE_LIMIT_MILLIS,
+ LingerMonitor.DEFAULT_NOTIFICATION_RATE_LIMIT_MILLIS);
+ mLingerMonitor = new LingerMonitor(mContext, mNotifier, dailyLimit, rateLimit);
+
+ mMultinetworkPolicyTracker = mDeps.makeMultinetworkPolicyTracker(
+ mContext, mHandler, () -> rematchForAvoidBadWifiUpdate());
+ mMultinetworkPolicyTracker.start();
+
+ mDnsManager = new DnsManager(mContext, mDnsResolver);
+ registerPrivateDnsSettingsCallbacks();
+
+ // This NAI is a sentinel used to offer no service to apps that are on a multi-layer
+ // request that doesn't allow fallback to the default network. It should never be visible
+ // to apps. As such, it's not in the list of NAIs and doesn't need many of the normal
+ // arguments like the handler or the DnsResolver.
+ // TODO : remove this ; it is probably better handled with a sentinel request.
+ mNoServiceNetwork = new NetworkAgentInfo(null,
+ new Network(INetd.UNREACHABLE_NET_ID),
+ new NetworkInfo(TYPE_NONE, 0, "", ""),
+ new LinkProperties(), new NetworkCapabilities(),
+ new NetworkScore.Builder().setLegacyInt(0).build(), mContext, null,
+ new NetworkAgentConfig(), this, null, null, 0, INVALID_UID, mQosCallbackTracker,
+ mDeps);
+ }
+
+ private static NetworkCapabilities createDefaultNetworkCapabilitiesForUid(int uid) {
+ return createDefaultNetworkCapabilitiesForUidRange(new UidRange(uid, uid));
+ }
+
+ private static NetworkCapabilities createDefaultNetworkCapabilitiesForUidRange(
+ @NonNull final UidRange uids) {
+ final NetworkCapabilities netCap = new NetworkCapabilities();
+ netCap.addCapability(NET_CAPABILITY_INTERNET);
+ netCap.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+ netCap.removeCapability(NET_CAPABILITY_NOT_VPN);
+ netCap.setUids(UidRange.toIntRanges(Collections.singleton(uids)));
+ return netCap;
+ }
+
+ private NetworkRequest createDefaultRequest() {
+ return createDefaultInternetRequestForTransport(
+ TYPE_NONE, NetworkRequest.Type.REQUEST);
+ }
+
+ private NetworkRequest createDefaultInternetRequestForTransport(
+ int transportType, NetworkRequest.Type type) {
+ final NetworkCapabilities netCap = new NetworkCapabilities();
+ netCap.addCapability(NET_CAPABILITY_INTERNET);
+ netCap.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
+ netCap.setRequestorUidAndPackageName(Process.myUid(), mContext.getPackageName());
+ if (transportType > TYPE_NONE) {
+ netCap.addTransportType(transportType);
+ }
+ return createNetworkRequest(type, netCap);
+ }
+
+ private NetworkRequest createNetworkRequest(
+ NetworkRequest.Type type, NetworkCapabilities netCap) {
+ return new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId(), type);
+ }
+
+ private NetworkRequest createAlwaysOnRequestForCapability(int capability,
+ NetworkRequest.Type type) {
+ final NetworkCapabilities netCap = new NetworkCapabilities();
+ netCap.clearAll();
+ netCap.addCapability(capability);
+ netCap.setRequestorUidAndPackageName(Process.myUid(), mContext.getPackageName());
+ return new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId(), type);
+ }
+
+ // Used only for testing.
+ // TODO: Delete this and either:
+ // 1. Give FakeSettingsProvider the ability to send settings change notifications (requires
+ // changing ContentResolver to make registerContentObserver non-final).
+ // 2. Give FakeSettingsProvider an alternative notification mechanism and have the test use it
+ // by subclassing SettingsObserver.
+ @VisibleForTesting
+ void updateAlwaysOnNetworks() {
+ mHandler.sendEmptyMessage(EVENT_CONFIGURE_ALWAYS_ON_NETWORKS);
+ }
+
+ // See FakeSettingsProvider comment above.
+ @VisibleForTesting
+ void updatePrivateDnsSettings() {
+ mHandler.sendEmptyMessage(EVENT_PRIVATE_DNS_SETTINGS_CHANGED);
+ }
+
+ private void handleAlwaysOnNetworkRequest(NetworkRequest networkRequest, @BoolRes int id) {
+ final boolean enable = mContext.getResources().getBoolean(id);
+ handleAlwaysOnNetworkRequest(networkRequest, enable);
+ }
+
+ private void handleAlwaysOnNetworkRequest(
+ NetworkRequest networkRequest, String settingName, boolean defaultValue) {
+ final boolean enable = toBool(Settings.Global.getInt(
+ mContext.getContentResolver(), settingName, encodeBool(defaultValue)));
+ handleAlwaysOnNetworkRequest(networkRequest, enable);
+ }
+
+ private void handleAlwaysOnNetworkRequest(NetworkRequest networkRequest, boolean enable) {
+ final boolean isEnabled = (mNetworkRequests.get(networkRequest) != null);
+ if (enable == isEnabled) {
+ return; // Nothing to do.
+ }
+
+ if (enable) {
+ handleRegisterNetworkRequest(new NetworkRequestInfo(
+ Process.myUid(), networkRequest, null, new Binder(),
+ NetworkCallback.FLAG_INCLUDE_LOCATION_INFO,
+ null /* attributionTags */));
+ } else {
+ handleReleaseNetworkRequest(networkRequest, Process.SYSTEM_UID,
+ /* callOnUnavailable */ false);
+ }
+ }
+
+ private void handleConfigureAlwaysOnNetworks() {
+ handleAlwaysOnNetworkRequest(mDefaultMobileDataRequest,
+ ConnectivitySettingsManager.MOBILE_DATA_ALWAYS_ON, true /* defaultValue */);
+ handleAlwaysOnNetworkRequest(mDefaultWifiRequest,
+ ConnectivitySettingsManager.WIFI_ALWAYS_REQUESTED, false /* defaultValue */);
+ final boolean vehicleAlwaysRequested = mResources.get().getBoolean(
+ R.bool.config_vehicleInternalNetworkAlwaysRequested);
+ // TODO (b/183076074): remove legacy fallback after migrating overlays
+ final boolean legacyAlwaysRequested = mContext.getResources().getBoolean(
+ mContext.getResources().getIdentifier(
+ "config_vehicleInternalNetworkAlwaysRequested", "bool", "android"));
+ handleAlwaysOnNetworkRequest(mDefaultVehicleRequest,
+ vehicleAlwaysRequested || legacyAlwaysRequested);
+ }
+
+ private void registerSettingsCallbacks() {
+ // Watch for global HTTP proxy changes.
+ mSettingsObserver.observe(
+ Settings.Global.getUriFor(Settings.Global.HTTP_PROXY),
+ EVENT_APPLY_GLOBAL_HTTP_PROXY);
+
+ // Watch for whether or not to keep mobile data always on.
+ mSettingsObserver.observe(
+ Settings.Global.getUriFor(ConnectivitySettingsManager.MOBILE_DATA_ALWAYS_ON),
+ EVENT_CONFIGURE_ALWAYS_ON_NETWORKS);
+
+ // Watch for whether or not to keep wifi always on.
+ mSettingsObserver.observe(
+ Settings.Global.getUriFor(ConnectivitySettingsManager.WIFI_ALWAYS_REQUESTED),
+ EVENT_CONFIGURE_ALWAYS_ON_NETWORKS);
+ }
+
+ private void registerPrivateDnsSettingsCallbacks() {
+ for (Uri uri : DnsManager.getPrivateDnsSettingsUris()) {
+ mSettingsObserver.observe(uri, EVENT_PRIVATE_DNS_SETTINGS_CHANGED);
+ }
+ }
+
+ private synchronized int nextNetworkRequestId() {
+ // TODO: Consider handle wrapping and exclude {@link NetworkRequest#REQUEST_ID_NONE} if
+ // doing that.
+ return mNextNetworkRequestId++;
+ }
+
+ @VisibleForTesting
+ protected NetworkAgentInfo getNetworkAgentInfoForNetwork(Network network) {
+ if (network == null) {
+ return null;
+ }
+ return getNetworkAgentInfoForNetId(network.getNetId());
+ }
+
+ private NetworkAgentInfo getNetworkAgentInfoForNetId(int netId) {
+ synchronized (mNetworkForNetId) {
+ return mNetworkForNetId.get(netId);
+ }
+ }
+
+ // TODO: determine what to do when more than one VPN applies to |uid|.
+ private NetworkAgentInfo getVpnForUid(int uid) {
+ synchronized (mNetworkForNetId) {
+ for (int i = 0; i < mNetworkForNetId.size(); i++) {
+ final NetworkAgentInfo nai = mNetworkForNetId.valueAt(i);
+ if (nai.isVPN() && nai.everConnected && nai.networkCapabilities.appliesToUid(uid)) {
+ return nai;
+ }
+ }
+ }
+ return null;
+ }
+
+ private Network[] getVpnUnderlyingNetworks(int uid) {
+ if (mLockdownEnabled) return null;
+ final NetworkAgentInfo nai = getVpnForUid(uid);
+ if (nai != null) return nai.declaredUnderlyingNetworks;
+ return null;
+ }
+
+ private NetworkAgentInfo getNetworkAgentInfoForUid(int uid) {
+ NetworkAgentInfo nai = getDefaultNetworkForUid(uid);
+
+ final Network[] networks = getVpnUnderlyingNetworks(uid);
+ if (networks != null) {
+ // getUnderlyingNetworks() returns:
+ // null => there was no VPN, or the VPN didn't specify anything, so we use the default.
+ // empty array => the VPN explicitly said "no default network".
+ // non-empty array => the VPN specified one or more default networks; we use the
+ // first one.
+ if (networks.length > 0) {
+ nai = getNetworkAgentInfoForNetwork(networks[0]);
+ } else {
+ nai = null;
+ }
+ }
+ return nai;
+ }
+
+ /**
+ * Check if UID should be blocked from using the specified network.
+ */
+ private boolean isNetworkWithCapabilitiesBlocked(@Nullable final NetworkCapabilities nc,
+ final int uid, final boolean ignoreBlocked) {
+ // Networks aren't blocked when ignoring blocked status
+ if (ignoreBlocked) {
+ return false;
+ }
+ if (isUidBlockedByVpn(uid, mVpnBlockedUidRanges)) return true;
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ final boolean metered = nc == null ? true : nc.isMetered();
+ return mPolicyManager.isUidNetworkingBlocked(uid, metered);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private void maybeLogBlockedNetworkInfo(NetworkInfo ni, int uid) {
+ if (ni == null || !LOGD_BLOCKED_NETWORKINFO) {
+ return;
+ }
+ final boolean blocked;
+ synchronized (mBlockedAppUids) {
+ if (ni.getDetailedState() == DetailedState.BLOCKED && mBlockedAppUids.add(uid)) {
+ blocked = true;
+ } else if (ni.isConnected() && mBlockedAppUids.remove(uid)) {
+ blocked = false;
+ } else {
+ return;
+ }
+ }
+ String action = blocked ? "BLOCKED" : "UNBLOCKED";
+ log(String.format("Returning %s NetworkInfo to uid=%d", action, uid));
+ mNetworkInfoBlockingLogs.log(action + " " + uid);
+ }
+
+ private void maybeLogBlockedStatusChanged(NetworkRequestInfo nri, Network net, int blocked) {
+ if (nri == null || net == null || !LOGD_BLOCKED_NETWORKINFO) {
+ return;
+ }
+ final String action = (blocked != 0) ? "BLOCKED" : "UNBLOCKED";
+ final int requestId = nri.getActiveRequest() != null
+ ? nri.getActiveRequest().requestId : nri.mRequests.get(0).requestId;
+ mNetworkInfoBlockingLogs.log(String.format(
+ "%s %d(%d) on netId %d: %s", action, nri.mAsUid, requestId, net.getNetId(),
+ Integer.toHexString(blocked)));
+ }
+
+ /**
+ * Apply any relevant filters to the specified {@link NetworkInfo} for the given UID. For
+ * example, this may mark the network as {@link DetailedState#BLOCKED} based
+ * on {@link #isNetworkWithCapabilitiesBlocked}.
+ */
+ @NonNull
+ private NetworkInfo filterNetworkInfo(@NonNull NetworkInfo networkInfo, int type,
+ @NonNull NetworkCapabilities nc, int uid, boolean ignoreBlocked) {
+ final NetworkInfo filtered = new NetworkInfo(networkInfo);
+ // Many legacy types (e.g,. TYPE_MOBILE_HIPRI) are not actually a property of the network
+ // but only exists if an app asks about them or requests them. Ensure the requesting app
+ // gets the type it asks for.
+ filtered.setType(type);
+ if (isNetworkWithCapabilitiesBlocked(nc, uid, ignoreBlocked)) {
+ filtered.setDetailedState(DetailedState.BLOCKED, null /* reason */,
+ null /* extraInfo */);
+ }
+ filterForLegacyLockdown(filtered);
+ return filtered;
+ }
+
+ private NetworkInfo getFilteredNetworkInfo(NetworkAgentInfo nai, int uid,
+ boolean ignoreBlocked) {
+ return filterNetworkInfo(nai.networkInfo, nai.networkInfo.getType(),
+ nai.networkCapabilities, uid, ignoreBlocked);
+ }
+
+ /**
+ * Return NetworkInfo for the active (i.e., connected) network interface.
+ * It is assumed that at most one network is active at a time. If more
+ * than one is active, it is indeterminate which will be returned.
+ * @return the info for the active network, or {@code null} if none is
+ * active
+ */
+ @Override
+ public NetworkInfo getActiveNetworkInfo() {
+ enforceAccessPermission();
+ final int uid = mDeps.getCallingUid();
+ final NetworkAgentInfo nai = getNetworkAgentInfoForUid(uid);
+ if (nai == null) return null;
+ final NetworkInfo networkInfo = getFilteredNetworkInfo(nai, uid, false);
+ maybeLogBlockedNetworkInfo(networkInfo, uid);
+ return networkInfo;
+ }
+
+ @Override
+ public Network getActiveNetwork() {
+ enforceAccessPermission();
+ return getActiveNetworkForUidInternal(mDeps.getCallingUid(), false);
+ }
+
+ @Override
+ public Network getActiveNetworkForUid(int uid, boolean ignoreBlocked) {
+ PermissionUtils.enforceNetworkStackPermission(mContext);
+ return getActiveNetworkForUidInternal(uid, ignoreBlocked);
+ }
+
+ private Network getActiveNetworkForUidInternal(final int uid, boolean ignoreBlocked) {
+ final NetworkAgentInfo vpnNai = getVpnForUid(uid);
+ if (vpnNai != null) {
+ final NetworkCapabilities requiredCaps = createDefaultNetworkCapabilitiesForUid(uid);
+ if (requiredCaps.satisfiedByNetworkCapabilities(vpnNai.networkCapabilities)) {
+ return vpnNai.network;
+ }
+ }
+
+ NetworkAgentInfo nai = getDefaultNetworkForUid(uid);
+ if (nai == null || isNetworkWithCapabilitiesBlocked(nai.networkCapabilities, uid,
+ ignoreBlocked)) {
+ return null;
+ }
+ return nai.network;
+ }
+
+ @Override
+ public NetworkInfo getActiveNetworkInfoForUid(int uid, boolean ignoreBlocked) {
+ PermissionUtils.enforceNetworkStackPermission(mContext);
+ final NetworkAgentInfo nai = getNetworkAgentInfoForUid(uid);
+ if (nai == null) return null;
+ return getFilteredNetworkInfo(nai, uid, ignoreBlocked);
+ }
+
+ /** Returns a NetworkInfo object for a network that doesn't exist. */
+ private NetworkInfo makeFakeNetworkInfo(int networkType, int uid) {
+ final NetworkInfo info = new NetworkInfo(networkType, 0 /* subtype */,
+ getNetworkTypeName(networkType), "" /* subtypeName */);
+ info.setIsAvailable(true);
+ // For compatibility with legacy code, return BLOCKED instead of DISCONNECTED when
+ // background data is restricted.
+ final NetworkCapabilities nc = new NetworkCapabilities(); // Metered.
+ final DetailedState state = isNetworkWithCapabilitiesBlocked(nc, uid, false)
+ ? DetailedState.BLOCKED
+ : DetailedState.DISCONNECTED;
+ info.setDetailedState(state, null /* reason */, null /* extraInfo */);
+ filterForLegacyLockdown(info);
+ return info;
+ }
+
+ private NetworkInfo getFilteredNetworkInfoForType(int networkType, int uid) {
+ if (!mLegacyTypeTracker.isTypeSupported(networkType)) {
+ return null;
+ }
+ final NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType);
+ if (nai == null) {
+ return makeFakeNetworkInfo(networkType, uid);
+ }
+ return filterNetworkInfo(nai.networkInfo, networkType, nai.networkCapabilities, uid,
+ false);
+ }
+
+ @Override
+ public NetworkInfo getNetworkInfo(int networkType) {
+ enforceAccessPermission();
+ final int uid = mDeps.getCallingUid();
+ if (getVpnUnderlyingNetworks(uid) != null) {
+ // A VPN is active, so we may need to return one of its underlying networks. This
+ // information is not available in LegacyTypeTracker, so we have to get it from
+ // getNetworkAgentInfoForUid.
+ final NetworkAgentInfo nai = getNetworkAgentInfoForUid(uid);
+ if (nai == null) return null;
+ final NetworkInfo networkInfo = getFilteredNetworkInfo(nai, uid, false);
+ if (networkInfo.getType() == networkType) {
+ return networkInfo;
+ }
+ }
+ return getFilteredNetworkInfoForType(networkType, uid);
+ }
+
+ @Override
+ public NetworkInfo getNetworkInfoForUid(Network network, int uid, boolean ignoreBlocked) {
+ enforceAccessPermission();
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+ if (nai == null) return null;
+ return getFilteredNetworkInfo(nai, uid, ignoreBlocked);
+ }
+
+ @Override
+ public NetworkInfo[] getAllNetworkInfo() {
+ enforceAccessPermission();
+ final ArrayList<NetworkInfo> result = new ArrayList<>();
+ for (int networkType = 0; networkType <= ConnectivityManager.MAX_NETWORK_TYPE;
+ networkType++) {
+ NetworkInfo info = getNetworkInfo(networkType);
+ if (info != null) {
+ result.add(info);
+ }
+ }
+ return result.toArray(new NetworkInfo[result.size()]);
+ }
+
+ @Override
+ public Network getNetworkForType(int networkType) {
+ enforceAccessPermission();
+ if (!mLegacyTypeTracker.isTypeSupported(networkType)) {
+ return null;
+ }
+ final NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType);
+ if (nai == null) {
+ return null;
+ }
+ final int uid = mDeps.getCallingUid();
+ if (isNetworkWithCapabilitiesBlocked(nai.networkCapabilities, uid, false)) {
+ return null;
+ }
+ return nai.network;
+ }
+
+ @Override
+ public Network[] getAllNetworks() {
+ enforceAccessPermission();
+ synchronized (mNetworkForNetId) {
+ final Network[] result = new Network[mNetworkForNetId.size()];
+ for (int i = 0; i < mNetworkForNetId.size(); i++) {
+ result[i] = mNetworkForNetId.valueAt(i).network;
+ }
+ return result;
+ }
+ }
+
+ @Override
+ public NetworkCapabilities[] getDefaultNetworkCapabilitiesForUser(
+ int userId, String callingPackageName, @Nullable String callingAttributionTag) {
+ // The basic principle is: if an app's traffic could possibly go over a
+ // network, without the app doing anything multinetwork-specific,
+ // (hence, by "default"), then include that network's capabilities in
+ // the array.
+ //
+ // In the normal case, app traffic only goes over the system's default
+ // network connection, so that's the only network returned.
+ //
+ // With a VPN in force, some app traffic may go into the VPN, and thus
+ // over whatever underlying networks the VPN specifies, while other app
+ // traffic may go over the system default network (e.g.: a split-tunnel
+ // VPN, or an app disallowed by the VPN), so the set of networks
+ // returned includes the VPN's underlying networks and the system
+ // default.
+ enforceAccessPermission();
+
+ HashMap<Network, NetworkCapabilities> result = new HashMap<>();
+
+ for (final NetworkRequestInfo nri : mDefaultNetworkRequests) {
+ if (!nri.isBeingSatisfied()) {
+ continue;
+ }
+ final NetworkAgentInfo nai = nri.getSatisfier();
+ final NetworkCapabilities nc = getNetworkCapabilitiesInternal(nai);
+ if (null != nc
+ && nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
+ && !result.containsKey(nai.network)) {
+ result.put(
+ nai.network,
+ createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+ nc, false /* includeLocationSensitiveInfo */,
+ getCallingPid(), mDeps.getCallingUid(), callingPackageName,
+ callingAttributionTag));
+ }
+ }
+
+ // No need to check mLockdownEnabled. If it's true, getVpnUnderlyingNetworks returns null.
+ final Network[] networks = getVpnUnderlyingNetworks(mDeps.getCallingUid());
+ if (null != networks) {
+ for (final Network network : networks) {
+ final NetworkCapabilities nc = getNetworkCapabilitiesInternal(network);
+ if (null != nc) {
+ result.put(
+ network,
+ createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+ nc,
+ false /* includeLocationSensitiveInfo */,
+ getCallingPid(), mDeps.getCallingUid(), callingPackageName,
+ callingAttributionTag));
+ }
+ }
+ }
+
+ NetworkCapabilities[] out = new NetworkCapabilities[result.size()];
+ out = result.values().toArray(out);
+ return out;
+ }
+
+ @Override
+ public boolean isNetworkSupported(int networkType) {
+ enforceAccessPermission();
+ return mLegacyTypeTracker.isTypeSupported(networkType);
+ }
+
+ /**
+ * Return LinkProperties for the active (i.e., connected) default
+ * network interface for the calling uid.
+ * @return the ip properties for the active network, or {@code null} if
+ * none is active
+ */
+ @Override
+ public LinkProperties getActiveLinkProperties() {
+ enforceAccessPermission();
+ final int uid = mDeps.getCallingUid();
+ NetworkAgentInfo nai = getNetworkAgentInfoForUid(uid);
+ if (nai == null) return null;
+ return linkPropertiesRestrictedForCallerPermissions(nai.linkProperties,
+ Binder.getCallingPid(), uid);
+ }
+
+ @Override
+ public LinkProperties getLinkPropertiesForType(int networkType) {
+ enforceAccessPermission();
+ NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType);
+ final LinkProperties lp = getLinkProperties(nai);
+ if (lp == null) return null;
+ return linkPropertiesRestrictedForCallerPermissions(
+ lp, Binder.getCallingPid(), mDeps.getCallingUid());
+ }
+
+ // TODO - this should be ALL networks
+ @Override
+ public LinkProperties getLinkProperties(Network network) {
+ enforceAccessPermission();
+ final LinkProperties lp = getLinkProperties(getNetworkAgentInfoForNetwork(network));
+ if (lp == null) return null;
+ return linkPropertiesRestrictedForCallerPermissions(
+ lp, Binder.getCallingPid(), mDeps.getCallingUid());
+ }
+
+ @Nullable
+ private LinkProperties getLinkProperties(@Nullable NetworkAgentInfo nai) {
+ if (nai == null) {
+ return null;
+ }
+ synchronized (nai) {
+ return nai.linkProperties;
+ }
+ }
+
+ private NetworkCapabilities getNetworkCapabilitiesInternal(Network network) {
+ return getNetworkCapabilitiesInternal(getNetworkAgentInfoForNetwork(network));
+ }
+
+ private NetworkCapabilities getNetworkCapabilitiesInternal(NetworkAgentInfo nai) {
+ if (nai == null) return null;
+ synchronized (nai) {
+ return networkCapabilitiesRestrictedForCallerPermissions(
+ nai.networkCapabilities, Binder.getCallingPid(), mDeps.getCallingUid());
+ }
+ }
+
+ @Override
+ public NetworkCapabilities getNetworkCapabilities(Network network, String callingPackageName,
+ @Nullable String callingAttributionTag) {
+ mAppOpsManager.checkPackage(mDeps.getCallingUid(), callingPackageName);
+ enforceAccessPermission();
+ return createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+ getNetworkCapabilitiesInternal(network),
+ false /* includeLocationSensitiveInfo */,
+ getCallingPid(), mDeps.getCallingUid(), callingPackageName, callingAttributionTag);
+ }
+
+ @VisibleForTesting
+ NetworkCapabilities networkCapabilitiesRestrictedForCallerPermissions(
+ NetworkCapabilities nc, int callerPid, int callerUid) {
+ final NetworkCapabilities newNc = new NetworkCapabilities(nc);
+ if (!checkSettingsPermission(callerPid, callerUid)) {
+ newNc.setUids(null);
+ newNc.setSSID(null);
+ }
+ if (newNc.getNetworkSpecifier() != null) {
+ newNc.setNetworkSpecifier(newNc.getNetworkSpecifier().redact());
+ }
+ newNc.setAdministratorUids(new int[0]);
+ if (!checkAnyPermissionOf(
+ callerPid, callerUid, android.Manifest.permission.NETWORK_FACTORY)) {
+ newNc.setSubscriptionIds(Collections.emptySet());
+ }
+
+ return newNc;
+ }
+
+ /**
+ * Wrapper used to cache the permission check results performed for the corresponding
+ * app. This avoid performing multiple permission checks for different fields in
+ * NetworkCapabilities.
+ * Note: This wrapper does not support any sort of invalidation and thus must not be
+ * persistent or long-lived. It may only be used for the time necessary to
+ * compute the redactions required by one particular NetworkCallback or
+ * synchronous call.
+ */
+ private class RedactionPermissionChecker {
+ private final int mCallingPid;
+ private final int mCallingUid;
+ @NonNull private final String mCallingPackageName;
+ @Nullable private final String mCallingAttributionTag;
+
+ private Boolean mHasLocationPermission = null;
+ private Boolean mHasLocalMacAddressPermission = null;
+ private Boolean mHasSettingsPermission = null;
+
+ RedactionPermissionChecker(int callingPid, int callingUid,
+ @NonNull String callingPackageName, @Nullable String callingAttributionTag) {
+ mCallingPid = callingPid;
+ mCallingUid = callingUid;
+ mCallingPackageName = callingPackageName;
+ mCallingAttributionTag = callingAttributionTag;
+ }
+
+ private boolean hasLocationPermissionInternal() {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return mLocationPermissionChecker.checkLocationPermission(
+ mCallingPackageName, mCallingAttributionTag, mCallingUid,
+ null /* message */);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ /**
+ * Returns whether the app holds location permission or not (might return cached result
+ * if the permission was already checked before).
+ */
+ public boolean hasLocationPermission() {
+ if (mHasLocationPermission == null) {
+ // If there is no cached result, perform the check now.
+ mHasLocationPermission = hasLocationPermissionInternal();
+ }
+ return mHasLocationPermission;
+ }
+
+ /**
+ * Returns whether the app holds local mac address permission or not (might return cached
+ * result if the permission was already checked before).
+ */
+ public boolean hasLocalMacAddressPermission() {
+ if (mHasLocalMacAddressPermission == null) {
+ // If there is no cached result, perform the check now.
+ mHasLocalMacAddressPermission =
+ checkLocalMacAddressPermission(mCallingPid, mCallingUid);
+ }
+ return mHasLocalMacAddressPermission;
+ }
+
+ /**
+ * Returns whether the app holds settings permission or not (might return cached
+ * result if the permission was already checked before).
+ */
+ public boolean hasSettingsPermission() {
+ if (mHasSettingsPermission == null) {
+ // If there is no cached result, perform the check now.
+ mHasSettingsPermission = checkSettingsPermission(mCallingPid, mCallingUid);
+ }
+ return mHasSettingsPermission;
+ }
+ }
+
+ private static boolean shouldRedact(@NetworkCapabilities.RedactionType long redactions,
+ @NetworkCapabilities.NetCapability long redaction) {
+ return (redactions & redaction) != 0;
+ }
+
+ /**
+ * Use the provided |applicableRedactions| to check the receiving app's
+ * permissions and clear/set the corresponding bit in the returned bitmask. The bitmask
+ * returned will be used to ensure the necessary redactions are performed by NetworkCapabilities
+ * before being sent to the corresponding app.
+ */
+ private @NetworkCapabilities.RedactionType long retrieveRequiredRedactions(
+ @NetworkCapabilities.RedactionType long applicableRedactions,
+ @NonNull RedactionPermissionChecker redactionPermissionChecker,
+ boolean includeLocationSensitiveInfo) {
+ long redactions = applicableRedactions;
+ if (shouldRedact(redactions, REDACT_FOR_ACCESS_FINE_LOCATION)) {
+ if (includeLocationSensitiveInfo
+ && redactionPermissionChecker.hasLocationPermission()) {
+ redactions &= ~REDACT_FOR_ACCESS_FINE_LOCATION;
+ }
+ }
+ if (shouldRedact(redactions, REDACT_FOR_LOCAL_MAC_ADDRESS)) {
+ if (redactionPermissionChecker.hasLocalMacAddressPermission()) {
+ redactions &= ~REDACT_FOR_LOCAL_MAC_ADDRESS;
+ }
+ }
+ if (shouldRedact(redactions, REDACT_FOR_NETWORK_SETTINGS)) {
+ if (redactionPermissionChecker.hasSettingsPermission()) {
+ redactions &= ~REDACT_FOR_NETWORK_SETTINGS;
+ }
+ }
+ return redactions;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ NetworkCapabilities createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+ @Nullable NetworkCapabilities nc, boolean includeLocationSensitiveInfo,
+ int callingPid, int callingUid, @NonNull String callingPkgName,
+ @Nullable String callingAttributionTag) {
+ if (nc == null) {
+ return null;
+ }
+ // Avoid doing location permission check if the transport info has no location sensitive
+ // data.
+ final RedactionPermissionChecker redactionPermissionChecker =
+ new RedactionPermissionChecker(callingPid, callingUid, callingPkgName,
+ callingAttributionTag);
+ final long redactions = retrieveRequiredRedactions(
+ nc.getApplicableRedactions(), redactionPermissionChecker,
+ includeLocationSensitiveInfo);
+ final NetworkCapabilities newNc = new NetworkCapabilities(nc, redactions);
+ // Reset owner uid if not destined for the owner app.
+ if (callingUid != nc.getOwnerUid()) {
+ newNc.setOwnerUid(INVALID_UID);
+ 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;
+ }
+ // If the calling does not want location sensitive data & target SDK >= S, then mask info.
+ // Else include the owner UID iff the calling has location permission to provide backwards
+ // compatibility for older apps.
+ if (!includeLocationSensitiveInfo
+ && isTargetSdkAtleast(
+ Build.VERSION_CODES.S, callingUid, callingPkgName)) {
+ newNc.setOwnerUid(INVALID_UID);
+ return newNc;
+ }
+ // Reset owner uid if the app has no location permission.
+ if (!redactionPermissionChecker.hasLocationPermission()) {
+ newNc.setOwnerUid(INVALID_UID);
+ }
+ return newNc;
+ }
+
+ private LinkProperties linkPropertiesRestrictedForCallerPermissions(
+ LinkProperties lp, int callerPid, int callerUid) {
+ if (lp == null) return new LinkProperties();
+
+ // Only do a permission check if sanitization is needed, to avoid unnecessary binder calls.
+ final boolean needsSanitization =
+ (lp.getCaptivePortalApiUrl() != null || lp.getCaptivePortalData() != null);
+ if (!needsSanitization) {
+ return new LinkProperties(lp);
+ }
+
+ if (checkSettingsPermission(callerPid, callerUid)) {
+ return new LinkProperties(lp, true /* parcelSensitiveFields */);
+ }
+
+ final LinkProperties newLp = new LinkProperties(lp);
+ // Sensitive fields would not be parceled anyway, but sanitize for consistency before the
+ // object gets parceled.
+ newLp.setCaptivePortalApiUrl(null);
+ newLp.setCaptivePortalData(null);
+ return newLp;
+ }
+
+ private void restrictRequestUidsForCallerAndSetRequestorInfo(NetworkCapabilities nc,
+ int callerUid, String callerPackageName) {
+ if (!checkSettingsPermission()) {
+ // There is no need to track the effective UID of the request here. If the caller lacks
+ // the settings permission, the effective UID is the same as the calling ID.
+ nc.setSingleUid(callerUid);
+ }
+ nc.setRequestorUidAndPackageName(callerUid, callerPackageName);
+ nc.setAdministratorUids(new int[0]);
+
+ // Clear owner UID; this can never come from an app.
+ nc.setOwnerUid(INVALID_UID);
+ }
+
+ private void restrictBackgroundRequestForCaller(NetworkCapabilities nc) {
+ if (!mPermissionMonitor.hasUseBackgroundNetworksPermission(mDeps.getCallingUid())) {
+ nc.addCapability(NET_CAPABILITY_FOREGROUND);
+ }
+ }
+
+ @Override
+ public @RestrictBackgroundStatus int getRestrictBackgroundStatusByCaller() {
+ enforceAccessPermission();
+ final int callerUid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return mPolicyManager.getRestrictBackgroundStatus(callerUid);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ // TODO: Consider delete this function or turn it into a no-op method.
+ @Override
+ public NetworkState[] getAllNetworkState() {
+ // This contains IMSI details, so make sure the caller is privileged.
+ PermissionUtils.enforceNetworkStackPermission(mContext);
+
+ final ArrayList<NetworkState> result = new ArrayList<>();
+ for (NetworkStateSnapshot snapshot : getAllNetworkStateSnapshots()) {
+ // NetworkStateSnapshot doesn't contain NetworkInfo, so need to fetch it from the
+ // NetworkAgentInfo.
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(snapshot.getNetwork());
+ if (nai != null && nai.networkInfo.isConnected()) {
+ result.add(new NetworkState(new NetworkInfo(nai.networkInfo),
+ snapshot.getLinkProperties(), snapshot.getNetworkCapabilities(),
+ snapshot.getNetwork(), snapshot.getSubscriberId()));
+ }
+ }
+ return result.toArray(new NetworkState[result.size()]);
+ }
+
+ @Override
+ @NonNull
+ public List<NetworkStateSnapshot> getAllNetworkStateSnapshots() {
+ // This contains IMSI details, so make sure the caller is privileged.
+ PermissionUtils.enforceNetworkStackPermission(mContext);
+
+ final ArrayList<NetworkStateSnapshot> result = new ArrayList<>();
+ for (Network network : getAllNetworks()) {
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+ // TODO: Consider include SUSPENDED networks, which should be considered as
+ // temporary shortage of connectivity of a connected network.
+ if (nai != null && nai.networkInfo.isConnected()) {
+ // TODO (b/73321673) : NetworkStateSnapshot contains a copy of the
+ // NetworkCapabilities, which may contain UIDs of apps to which the
+ // network applies. Should the UIDs be cleared so as not to leak or
+ // interfere ?
+ result.add(nai.getNetworkStateSnapshot());
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public boolean isActiveNetworkMetered() {
+ enforceAccessPermission();
+
+ final NetworkCapabilities caps = getNetworkCapabilitiesInternal(getActiveNetwork());
+ if (caps != null) {
+ return !caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+ } else {
+ // Always return the most conservative value
+ return true;
+ }
+ }
+
+ /**
+ * Ensures that the system cannot call a particular method.
+ */
+ private boolean disallowedBecauseSystemCaller() {
+ // TODO: start throwing a SecurityException when GnssLocationProvider stops calling
+ // requestRouteToHost. In Q, GnssLocationProvider is changed to not call requestRouteToHost
+ // for devices launched with Q and above. However, existing devices upgrading to Q and
+ // above must continued to be supported for few more releases.
+ if (isSystem(mDeps.getCallingUid()) && SystemProperties.getInt(
+ "ro.product.first_api_level", 0) > Build.VERSION_CODES.P) {
+ log("This method exists only for app backwards compatibility"
+ + " and must not be called by system services.");
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Ensure that a network route exists to deliver traffic to the specified
+ * host via the specified network interface.
+ * @param networkType the type of the network over which traffic to the
+ * specified host is to be routed
+ * @param hostAddress the IP address of the host to which the route is
+ * desired
+ * @return {@code true} on success, {@code false} on failure
+ */
+ @Override
+ public boolean requestRouteToHostAddress(int networkType, byte[] hostAddress,
+ String callingPackageName, String callingAttributionTag) {
+ if (disallowedBecauseSystemCaller()) {
+ return false;
+ }
+ enforceChangePermission(callingPackageName, callingAttributionTag);
+ if (mProtectedNetworks.contains(networkType)) {
+ enforceConnectivityRestrictedNetworksPermission();
+ }
+
+ InetAddress addr;
+ try {
+ addr = InetAddress.getByAddress(hostAddress);
+ } catch (UnknownHostException e) {
+ if (DBG) log("requestRouteToHostAddress got " + e.toString());
+ return false;
+ }
+
+ if (!ConnectivityManager.isNetworkTypeValid(networkType)) {
+ if (DBG) log("requestRouteToHostAddress on invalid network: " + networkType);
+ return false;
+ }
+
+ NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType);
+ if (nai == null) {
+ if (mLegacyTypeTracker.isTypeSupported(networkType) == false) {
+ if (DBG) log("requestRouteToHostAddress on unsupported network: " + networkType);
+ } else {
+ if (DBG) log("requestRouteToHostAddress on down network: " + networkType);
+ }
+ return false;
+ }
+
+ DetailedState netState;
+ synchronized (nai) {
+ netState = nai.networkInfo.getDetailedState();
+ }
+
+ if (netState != DetailedState.CONNECTED && netState != DetailedState.CAPTIVE_PORTAL_CHECK) {
+ if (VDBG) {
+ log("requestRouteToHostAddress on down network "
+ + "(" + networkType + ") - dropped"
+ + " netState=" + netState);
+ }
+ return false;
+ }
+
+ final int uid = mDeps.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ LinkProperties lp;
+ int netId;
+ synchronized (nai) {
+ lp = nai.linkProperties;
+ netId = nai.network.getNetId();
+ }
+ boolean ok = addLegacyRouteToHost(lp, addr, netId, uid);
+ if (DBG) {
+ log("requestRouteToHostAddress " + addr + nai.toShortString() + " ok=" + ok);
+ }
+ return ok;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ private boolean addLegacyRouteToHost(LinkProperties lp, InetAddress addr, int netId, int uid) {
+ RouteInfo bestRoute = RouteInfo.selectBestRoute(lp.getAllRoutes(), addr);
+ if (bestRoute == null) {
+ bestRoute = RouteInfo.makeHostRoute(addr, lp.getInterfaceName());
+ } else {
+ String iface = bestRoute.getInterface();
+ if (bestRoute.getGateway().equals(addr)) {
+ // if there is no better route, add the implied hostroute for our gateway
+ bestRoute = RouteInfo.makeHostRoute(addr, iface);
+ } else {
+ // if we will connect to this through another route, add a direct route
+ // to it's gateway
+ bestRoute = RouteInfo.makeHostRoute(addr, bestRoute.getGateway(), iface);
+ }
+ }
+ if (DBG) log("Adding legacy route " + bestRoute +
+ " for UID/PID " + uid + "/" + Binder.getCallingPid());
+
+ final String dst = bestRoute.getDestinationLinkAddress().toString();
+ final String nextHop = bestRoute.hasGateway()
+ ? bestRoute.getGateway().getHostAddress() : "";
+ try {
+ mNetd.networkAddLegacyRoute(netId, bestRoute.getInterface(), dst, nextHop , uid);
+ } catch (RemoteException | ServiceSpecificException e) {
+ if (DBG) loge("Exception trying to add a route: " + e);
+ return false;
+ }
+ return true;
+ }
+
+ class DnsResolverUnsolicitedEventCallback extends
+ IDnsResolverUnsolicitedEventListener.Stub {
+ @Override
+ public void onPrivateDnsValidationEvent(final PrivateDnsValidationEventParcel event) {
+ try {
+ mHandler.sendMessage(mHandler.obtainMessage(
+ EVENT_PRIVATE_DNS_VALIDATION_UPDATE,
+ new PrivateDnsValidationUpdate(event.netId,
+ InetAddresses.parseNumericAddress(event.ipAddress),
+ event.hostname, event.validation)));
+ } catch (IllegalArgumentException e) {
+ loge("Error parsing ip address in validation event");
+ }
+ }
+
+ @Override
+ public void onDnsHealthEvent(final DnsHealthEventParcel event) {
+ NetworkAgentInfo nai = getNetworkAgentInfoForNetId(event.netId);
+ // Netd event only allow registrants from system. Each NetworkMonitor thread is under
+ // the caller thread of registerNetworkAgent. Thus, it's not allowed to register netd
+ // event callback for certain nai. e.g. cellular. Register here to pass to
+ // NetworkMonitor instead.
+ // TODO: Move the Dns Event to NetworkMonitor. NetdEventListenerService only allow one
+ // callback from each caller type. Need to re-factor NetdEventListenerService to allow
+ // multiple NetworkMonitor registrants.
+ if (nai != null && nai.satisfies(mDefaultRequest.mRequests.get(0))) {
+ nai.networkMonitor().notifyDnsResponse(event.healthResult);
+ }
+ }
+
+ @Override
+ public void onNat64PrefixEvent(final Nat64PrefixEventParcel event) {
+ mHandler.post(() -> handleNat64PrefixEvent(event.netId, event.prefixOperation,
+ event.prefixAddress, event.prefixLength));
+ }
+
+ @Override
+ public int getInterfaceVersion() {
+ return this.VERSION;
+ }
+
+ @Override
+ public String getInterfaceHash() {
+ return this.HASH;
+ }
+ }
+
+ @VisibleForTesting
+ protected final DnsResolverUnsolicitedEventCallback mResolverUnsolEventCallback =
+ new DnsResolverUnsolicitedEventCallback();
+
+ private void registerDnsResolverUnsolicitedEventListener() {
+ try {
+ mDnsResolver.registerUnsolicitedEventListener(mResolverUnsolEventCallback);
+ } catch (Exception e) {
+ loge("Error registering DnsResolver unsolicited event callback: " + e);
+ }
+ }
+
+ private final NetworkPolicyCallback mPolicyCallback = new NetworkPolicyCallback() {
+ @Override
+ public void onUidBlockedReasonChanged(int uid, @BlockedReason int blockedReasons) {
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_UID_BLOCKED_REASON_CHANGED,
+ uid, blockedReasons));
+ }
+ };
+
+ private void handleUidBlockedReasonChanged(int uid, @BlockedReason int blockedReasons) {
+ maybeNotifyNetworkBlockedForNewState(uid, blockedReasons);
+ setUidBlockedReasons(uid, blockedReasons);
+ }
+
+ private boolean checkAnyPermissionOf(String... permissions) {
+ for (String permission : permissions) {
+ if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkAnyPermissionOf(int pid, int uid, String... permissions) {
+ for (String permission : permissions) {
+ if (mContext.checkPermission(permission, pid, uid) == PERMISSION_GRANTED) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void enforceAnyPermissionOf(String... permissions) {
+ if (!checkAnyPermissionOf(permissions)) {
+ throw new SecurityException("Requires one of the following permissions: "
+ + String.join(", ", permissions) + ".");
+ }
+ }
+
+ private void enforceInternetPermission() {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.INTERNET,
+ "ConnectivityService");
+ }
+
+ private void enforceAccessPermission() {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_NETWORK_STATE,
+ "ConnectivityService");
+ }
+
+ /**
+ * Performs a strict and comprehensive check of whether a calling package is allowed to
+ * change the state of network, as the condition differs for pre-M, M+, and
+ * privileged/preinstalled apps. The caller is expected to have either the
+ * CHANGE_NETWORK_STATE or the WRITE_SETTINGS permission declared. Either of these
+ * permissions allow changing network state; WRITE_SETTINGS is a runtime permission and
+ * can be revoked, but (except in M, excluding M MRs), CHANGE_NETWORK_STATE is a normal
+ * permission and cannot be revoked. See http://b/23597341
+ *
+ * Note: if the check succeeds because the application holds WRITE_SETTINGS, the operation
+ * of this app will be updated to the current time.
+ */
+ private void enforceChangePermission(String callingPkg, String callingAttributionTag) {
+ if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.CHANGE_NETWORK_STATE)
+ == PackageManager.PERMISSION_GRANTED) {
+ return;
+ }
+
+ if (callingPkg == null) {
+ throw new SecurityException("Calling package name is null.");
+ }
+
+ final AppOpsManager appOpsMgr = mContext.getSystemService(AppOpsManager.class);
+ final int uid = mDeps.getCallingUid();
+ final int mode = appOpsMgr.noteOpNoThrow(AppOpsManager.OPSTR_WRITE_SETTINGS, uid,
+ callingPkg, callingAttributionTag, null /* message */);
+
+ if (mode == AppOpsManager.MODE_ALLOWED) {
+ return;
+ }
+
+ if ((mode == AppOpsManager.MODE_DEFAULT) && (mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.WRITE_SETTINGS) == PackageManager.PERMISSION_GRANTED)) {
+ return;
+ }
+
+ throw new SecurityException(callingPkg + " was not granted either of these permissions:"
+ + android.Manifest.permission.CHANGE_NETWORK_STATE + ","
+ + android.Manifest.permission.WRITE_SETTINGS + ".");
+ }
+
+ private void enforceSettingsPermission() {
+ enforceAnyPermissionOf(
+ android.Manifest.permission.NETWORK_SETTINGS,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+ }
+
+ private void enforceNetworkFactoryPermission() {
+ enforceAnyPermissionOf(
+ android.Manifest.permission.NETWORK_FACTORY,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+ }
+
+ private void enforceNetworkFactoryOrSettingsPermission() {
+ enforceAnyPermissionOf(
+ android.Manifest.permission.NETWORK_SETTINGS,
+ android.Manifest.permission.NETWORK_FACTORY,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+ }
+
+ private void enforceNetworkFactoryOrTestNetworksPermission() {
+ enforceAnyPermissionOf(
+ android.Manifest.permission.MANAGE_TEST_NETWORKS,
+ android.Manifest.permission.NETWORK_FACTORY,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+ }
+
+ private boolean checkSettingsPermission() {
+ return checkAnyPermissionOf(
+ android.Manifest.permission.NETWORK_SETTINGS,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+ }
+
+ private boolean checkSettingsPermission(int pid, int uid) {
+ return PERMISSION_GRANTED == mContext.checkPermission(
+ android.Manifest.permission.NETWORK_SETTINGS, pid, uid)
+ || PERMISSION_GRANTED == mContext.checkPermission(
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, pid, uid);
+ }
+
+ private void enforceNetworkStackOrSettingsPermission() {
+ enforceAnyPermissionOf(
+ android.Manifest.permission.NETWORK_SETTINGS,
+ android.Manifest.permission.NETWORK_STACK,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+ }
+
+ private void enforceNetworkStackSettingsOrSetup() {
+ enforceAnyPermissionOf(
+ android.Manifest.permission.NETWORK_SETTINGS,
+ android.Manifest.permission.NETWORK_SETUP_WIZARD,
+ android.Manifest.permission.NETWORK_STACK,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+ }
+
+ private void enforceAirplaneModePermission() {
+ enforceAnyPermissionOf(
+ android.Manifest.permission.NETWORK_AIRPLANE_MODE,
+ android.Manifest.permission.NETWORK_SETTINGS,
+ android.Manifest.permission.NETWORK_SETUP_WIZARD,
+ android.Manifest.permission.NETWORK_STACK,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+ }
+
+ private void enforceOemNetworkPreferencesPermission() {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE,
+ "ConnectivityService");
+ }
+
+ private boolean checkNetworkStackPermission() {
+ return checkAnyPermissionOf(
+ android.Manifest.permission.NETWORK_STACK,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+ }
+
+ private boolean checkNetworkStackPermission(int pid, int uid) {
+ return checkAnyPermissionOf(pid, uid,
+ android.Manifest.permission.NETWORK_STACK,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+ }
+
+ private boolean checkNetworkSignalStrengthWakeupPermission(int pid, int uid) {
+ return checkAnyPermissionOf(pid, uid,
+ android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ android.Manifest.permission.NETWORK_SETTINGS);
+ }
+
+ private void enforceConnectivityRestrictedNetworksPermission() {
+ try {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS,
+ "ConnectivityService");
+ return;
+ } catch (SecurityException e) { /* fallback to ConnectivityInternalPermission */ }
+ // TODO: Remove this fallback check after all apps have declared
+ // CONNECTIVITY_USE_RESTRICTED_NETWORKS.
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.CONNECTIVITY_INTERNAL,
+ "ConnectivityService");
+ }
+
+ private void enforceKeepalivePermission() {
+ mContext.enforceCallingOrSelfPermission(KeepaliveTracker.PERMISSION, "ConnectivityService");
+ }
+
+ private boolean checkLocalMacAddressPermission(int pid, int uid) {
+ return PERMISSION_GRANTED == mContext.checkPermission(
+ Manifest.permission.LOCAL_MAC_ADDRESS, pid, uid);
+ }
+
+ private void sendConnectedBroadcast(NetworkInfo info) {
+ sendGeneralBroadcast(info, CONNECTIVITY_ACTION);
+ }
+
+ private void sendInetConditionBroadcast(NetworkInfo info) {
+ sendGeneralBroadcast(info, ConnectivityManager.INET_CONDITION_ACTION);
+ }
+
+ private Intent makeGeneralIntent(NetworkInfo info, String bcastType) {
+ Intent intent = new Intent(bcastType);
+ intent.putExtra(ConnectivityManager.EXTRA_NETWORK_INFO, new NetworkInfo(info));
+ intent.putExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, info.getType());
+ if (info.isFailover()) {
+ intent.putExtra(ConnectivityManager.EXTRA_IS_FAILOVER, true);
+ info.setFailover(false);
+ }
+ if (info.getReason() != null) {
+ intent.putExtra(ConnectivityManager.EXTRA_REASON, info.getReason());
+ }
+ if (info.getExtraInfo() != null) {
+ intent.putExtra(ConnectivityManager.EXTRA_EXTRA_INFO,
+ info.getExtraInfo());
+ }
+ intent.putExtra(ConnectivityManager.EXTRA_INET_CONDITION, mDefaultInetConditionPublished);
+ return intent;
+ }
+
+ private void sendGeneralBroadcast(NetworkInfo info, String bcastType) {
+ sendStickyBroadcast(makeGeneralIntent(info, bcastType));
+ }
+
+ private void sendStickyBroadcast(Intent intent) {
+ synchronized (this) {
+ if (!mSystemReady
+ && intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
+ mInitialBroadcast = new Intent(intent);
+ }
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ if (VDBG) {
+ log("sendStickyBroadcast: action=" + intent.getAction());
+ }
+
+ Bundle options = null;
+ final long ident = Binder.clearCallingIdentity();
+ if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
+ final NetworkInfo ni = intent.getParcelableExtra(
+ ConnectivityManager.EXTRA_NETWORK_INFO);
+ final BroadcastOptions opts = BroadcastOptions.makeBasic();
+ opts.setMaxManifestReceiverApiLevel(Build.VERSION_CODES.M);
+ options = opts.toBundle();
+ intent.addFlags(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
+ }
+ try {
+ mUserAllContext.sendStickyBroadcast(intent, options);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ }
+
+ /**
+ * Called by SystemServer through ConnectivityManager when the system is ready.
+ */
+ @Override
+ public void systemReady() {
+ if (mDeps.getCallingUid() != Process.SYSTEM_UID) {
+ throw new SecurityException("Calling Uid is not system uid.");
+ }
+ systemReadyInternal();
+ }
+
+ /**
+ * Called when ConnectivityService can initialize remaining components.
+ */
+ @VisibleForTesting
+ public void systemReadyInternal() {
+ // Since mApps in PermissionMonitor needs to be populated first to ensure that
+ // listening network request which is sent by MultipathPolicyTracker won't be added
+ // NET_CAPABILITY_FOREGROUND capability. Thus, MultipathPolicyTracker.start() must
+ // be called after PermissionMonitor#startMonitoring().
+ // Calling PermissionMonitor#startMonitoring() in systemReadyInternal() and the
+ // MultipathPolicyTracker.start() is called in NetworkPolicyManagerService#systemReady()
+ // to ensure the tracking will be initialized correctly.
+ mPermissionMonitor.startMonitoring();
+ mProxyTracker.loadGlobalProxy();
+ registerDnsResolverUnsolicitedEventListener();
+
+ synchronized (this) {
+ mSystemReady = true;
+ if (mInitialBroadcast != null) {
+ mContext.sendStickyBroadcastAsUser(mInitialBroadcast, UserHandle.ALL);
+ mInitialBroadcast = null;
+ }
+ }
+
+ // Create network requests for always-on networks.
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_CONFIGURE_ALWAYS_ON_NETWORKS));
+ }
+
+ /**
+ * Start listening for default data network activity state changes.
+ */
+ @Override
+ public void registerNetworkActivityListener(@NonNull INetworkActivityListener l) {
+ mNetworkActivityTracker.registerNetworkActivityListener(l);
+ }
+
+ /**
+ * Stop listening for default data network activity state changes.
+ */
+ @Override
+ public void unregisterNetworkActivityListener(@NonNull INetworkActivityListener l) {
+ mNetworkActivityTracker.unregisterNetworkActivityListener(l);
+ }
+
+ /**
+ * Check whether the default network radio is currently active.
+ */
+ @Override
+ public boolean isDefaultNetworkActive() {
+ return mNetworkActivityTracker.isDefaultNetworkActive();
+ }
+
+ /**
+ * Reads the network specific MTU size from resources.
+ * and set it on it's iface.
+ */
+ private void updateMtu(LinkProperties newLp, LinkProperties oldLp) {
+ final String iface = newLp.getInterfaceName();
+ final int mtu = newLp.getMtu();
+ if (oldLp == null && mtu == 0) {
+ // Silently ignore unset MTU value.
+ return;
+ }
+ if (oldLp != null && newLp.isIdenticalMtu(oldLp)) {
+ if (VDBG) log("identical MTU - not setting");
+ return;
+ }
+ if (!LinkProperties.isValidMtu(mtu, newLp.hasGlobalIpv6Address())) {
+ if (mtu != 0) loge("Unexpected mtu value: " + mtu + ", " + iface);
+ return;
+ }
+
+ // Cannot set MTU without interface name
+ if (TextUtils.isEmpty(iface)) {
+ loge("Setting MTU size with null iface.");
+ return;
+ }
+
+ try {
+ if (VDBG || DDBG) log("Setting MTU size: " + iface + ", " + mtu);
+ mNetd.interfaceSetMtu(iface, mtu);
+ } catch (RemoteException | ServiceSpecificException e) {
+ loge("exception in interfaceSetMtu()" + e);
+ }
+ }
+
+ @VisibleForTesting
+ protected static final String DEFAULT_TCP_BUFFER_SIZES = "4096,87380,110208,4096,16384,110208";
+
+ private void updateTcpBufferSizes(String tcpBufferSizes) {
+ String[] values = null;
+ if (tcpBufferSizes != null) {
+ values = tcpBufferSizes.split(",");
+ }
+
+ if (values == null || values.length != 6) {
+ if (DBG) log("Invalid tcpBufferSizes string: " + tcpBufferSizes +", using defaults");
+ tcpBufferSizes = DEFAULT_TCP_BUFFER_SIZES;
+ values = tcpBufferSizes.split(",");
+ }
+
+ if (tcpBufferSizes.equals(mCurrentTcpBufferSizes)) return;
+
+ try {
+ if (VDBG || DDBG) log("Setting tx/rx TCP buffers to " + tcpBufferSizes);
+
+ String rmemValues = String.join(" ", values[0], values[1], values[2]);
+ String wmemValues = String.join(" ", values[3], values[4], values[5]);
+ mNetd.setTcpRWmemorySize(rmemValues, wmemValues);
+ mCurrentTcpBufferSizes = tcpBufferSizes;
+ } catch (RemoteException | ServiceSpecificException e) {
+ loge("Can't set TCP buffer sizes:" + e);
+ }
+ }
+
+ @Override
+ public int getRestoreDefaultNetworkDelay(int networkType) {
+ String restoreDefaultNetworkDelayStr = mSystemProperties.get(
+ NETWORK_RESTORE_DELAY_PROP_NAME);
+ if(restoreDefaultNetworkDelayStr != null &&
+ restoreDefaultNetworkDelayStr.length() != 0) {
+ try {
+ return Integer.parseInt(restoreDefaultNetworkDelayStr);
+ } catch (NumberFormatException e) {
+ }
+ }
+ // if the system property isn't set, use the value for the apn type
+ int ret = RESTORE_DEFAULT_NETWORK_DELAY;
+
+ if (mLegacyTypeTracker.isTypeSupported(networkType)) {
+ ret = mLegacyTypeTracker.getRestoreTimerForType(networkType);
+ }
+ return ret;
+ }
+
+ private void dumpNetworkDiagnostics(IndentingPrintWriter pw) {
+ 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));
+ }
+
+ for (NetworkDiagnostics netDiag : netDiags) {
+ pw.println();
+ netDiag.waitForMeasurements();
+ netDiag.dump(pw);
+ }
+ }
+
+ @Override
+ protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
+ @Nullable String[] args) {
+ if (!checkDumpPermission(mContext, TAG, writer)) return;
+
+ mPriorityDumper.dump(fd, writer, args);
+ }
+
+ private boolean checkDumpPermission(Context context, String tag, PrintWriter pw) {
+ if (context.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+ != PackageManager.PERMISSION_GRANTED) {
+ pw.println("Permission Denial: can't dump " + tag + " from from pid="
+ + Binder.getCallingPid() + ", uid=" + mDeps.getCallingUid()
+ + " due to missing android.permission.DUMP permission");
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ private void doDump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
+
+ if (CollectionUtils.contains(args, DIAG_ARG)) {
+ dumpNetworkDiagnostics(pw);
+ return;
+ } else if (CollectionUtils.contains(args, NETWORK_ARG)) {
+ dumpNetworks(pw);
+ return;
+ } else if (CollectionUtils.contains(args, REQUEST_ARG)) {
+ dumpNetworkRequests(pw);
+ return;
+ }
+
+ pw.print("NetworkProviders for:");
+ for (NetworkProviderInfo npi : mNetworkProviderInfos.values()) {
+ pw.print(" " + npi.name);
+ }
+ pw.println();
+ pw.println();
+
+ final NetworkAgentInfo defaultNai = getDefaultNetwork();
+ pw.print("Active default network: ");
+ if (defaultNai == null) {
+ pw.println("none");
+ } else {
+ pw.println(defaultNai.network.getNetId());
+ }
+ pw.println();
+
+ pw.print("Current per-app default networks: ");
+ pw.increaseIndent();
+ dumpPerAppNetworkPreferences(pw);
+ pw.decreaseIndent();
+ pw.println();
+
+ pw.println("Current Networks:");
+ pw.increaseIndent();
+ dumpNetworks(pw);
+ pw.decreaseIndent();
+ pw.println();
+
+ pw.println("Status for known UIDs:");
+ pw.increaseIndent();
+ final int size = mUidBlockedReasons.size();
+ for (int i = 0; i < size; i++) {
+ // Don't crash if the array is modified while dumping in bugreports.
+ try {
+ final int uid = mUidBlockedReasons.keyAt(i);
+ final int blockedReasons = mUidBlockedReasons.valueAt(i);
+ pw.println("UID=" + uid + " blockedReasons="
+ + Integer.toHexString(blockedReasons));
+ } catch (ArrayIndexOutOfBoundsException e) {
+ pw.println(" ArrayIndexOutOfBoundsException");
+ } catch (ConcurrentModificationException e) {
+ pw.println(" ConcurrentModificationException");
+ }
+ }
+ pw.println();
+ pw.decreaseIndent();
+
+ pw.println("Network Requests:");
+ pw.increaseIndent();
+ dumpNetworkRequests(pw);
+ pw.decreaseIndent();
+ pw.println();
+
+ mLegacyTypeTracker.dump(pw);
+
+ pw.println();
+ mKeepaliveTracker.dump(pw);
+
+ pw.println();
+ dumpAvoidBadWifiSettings(pw);
+
+ pw.println();
+
+ if (!CollectionUtils.contains(args, SHORT_ARG)) {
+ pw.println();
+ pw.println("mNetworkRequestInfoLogs (most recent first):");
+ pw.increaseIndent();
+ mNetworkRequestInfoLogs.reverseDump(pw);
+ pw.decreaseIndent();
+
+ pw.println();
+ pw.println("mNetworkInfoBlockingLogs (most recent first):");
+ pw.increaseIndent();
+ mNetworkInfoBlockingLogs.reverseDump(pw);
+ pw.decreaseIndent();
+
+ pw.println();
+ pw.println("NetTransition WakeLock activity (most recent first):");
+ pw.increaseIndent();
+ pw.println("total acquisitions: " + mTotalWakelockAcquisitions);
+ pw.println("total releases: " + mTotalWakelockReleases);
+ pw.println("cumulative duration: " + (mTotalWakelockDurationMs / 1000) + "s");
+ pw.println("longest duration: " + (mMaxWakelockDurationMs / 1000) + "s");
+ if (mTotalWakelockAcquisitions > mTotalWakelockReleases) {
+ long duration = SystemClock.elapsedRealtime() - mLastWakeLockAcquireTimestamp;
+ pw.println("currently holding WakeLock for: " + (duration / 1000) + "s");
+ }
+ mWakelockLogs.reverseDump(pw);
+
+ pw.println();
+ pw.println("bandwidth update requests (by uid):");
+ pw.increaseIndent();
+ synchronized (mBandwidthRequests) {
+ for (int i = 0; i < mBandwidthRequests.size(); i++) {
+ pw.println("[" + mBandwidthRequests.keyAt(i)
+ + "]: " + mBandwidthRequests.valueAt(i));
+ }
+ }
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+
+ pw.println();
+ pw.println("mOemNetworkPreferencesLogs (most recent first):");
+ pw.increaseIndent();
+ mOemNetworkPreferencesLogs.reverseDump(pw);
+ pw.decreaseIndent();
+ }
+
+ pw.println();
+ pw.println("NetworkStackClient logs:");
+ pw.increaseIndent();
+ NetworkStackClient.getInstance().dump(pw);
+ pw.decreaseIndent();
+
+ pw.println();
+ pw.println("Permission Monitor:");
+ pw.increaseIndent();
+ mPermissionMonitor.dump(pw);
+ pw.decreaseIndent();
+
+ pw.println();
+ pw.println("Legacy network activity:");
+ pw.increaseIndent();
+ mNetworkActivityTracker.dump(pw);
+ pw.decreaseIndent();
+ }
+
+ private void dumpNetworks(IndentingPrintWriter pw) {
+ for (NetworkAgentInfo nai : networksSortedById()) {
+ pw.println(nai.toString());
+ pw.increaseIndent();
+ pw.println(String.format(
+ "Requests: REQUEST:%d LISTEN:%d BACKGROUND_REQUEST:%d total:%d",
+ nai.numForegroundNetworkRequests(),
+ nai.numNetworkRequests() - nai.numRequestNetworkRequests(),
+ nai.numBackgroundNetworkRequests(),
+ nai.numNetworkRequests()));
+ pw.increaseIndent();
+ for (int i = 0; i < nai.numNetworkRequests(); i++) {
+ pw.println(nai.requestAt(i).toString());
+ }
+ pw.decreaseIndent();
+ pw.println("Inactivity Timers:");
+ pw.increaseIndent();
+ nai.dumpInactivityTimers(pw);
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+ }
+ }
+
+ private void dumpPerAppNetworkPreferences(IndentingPrintWriter pw) {
+ pw.println("Per-App Network Preference:");
+ pw.increaseIndent();
+ if (0 == mOemNetworkPreferences.getNetworkPreferences().size()) {
+ pw.println("none");
+ } else {
+ pw.println(mOemNetworkPreferences.toString());
+ }
+ pw.decreaseIndent();
+
+ for (final NetworkRequestInfo defaultRequest : mDefaultNetworkRequests) {
+ if (mDefaultRequest == defaultRequest) {
+ continue;
+ }
+
+ final boolean isActive = null != defaultRequest.getSatisfier();
+ pw.println("Is per-app network active:");
+ pw.increaseIndent();
+ pw.println(isActive);
+ if (isActive) {
+ pw.println("Active network: " + defaultRequest.getSatisfier().network.netId);
+ }
+ pw.println("Tracked UIDs:");
+ pw.increaseIndent();
+ if (0 == defaultRequest.mRequests.size()) {
+ pw.println("none, this should never occur.");
+ } else {
+ pw.println(defaultRequest.mRequests.get(0).networkCapabilities.getUidRanges());
+ }
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+ }
+ }
+
+ private void dumpNetworkRequests(IndentingPrintWriter pw) {
+ for (NetworkRequestInfo nri : requestsSortedById()) {
+ pw.println(nri.toString());
+ }
+ }
+
+ /**
+ * Return an array of all current NetworkAgentInfos sorted by network id.
+ */
+ private NetworkAgentInfo[] networksSortedById() {
+ NetworkAgentInfo[] networks = new NetworkAgentInfo[0];
+ networks = mNetworkAgentInfos.toArray(networks);
+ Arrays.sort(networks, Comparator.comparingInt(nai -> nai.network.getNetId()));
+ return networks;
+ }
+
+ /**
+ * Return an array of all current NetworkRequest sorted by request id.
+ */
+ @VisibleForTesting
+ NetworkRequestInfo[] requestsSortedById() {
+ NetworkRequestInfo[] requests = new NetworkRequestInfo[0];
+ requests = getNrisFromGlobalRequests().toArray(requests);
+ // Sort the array based off the NRI containing the min requestId in its requests.
+ Arrays.sort(requests,
+ Comparator.comparingInt(nri -> Collections.min(nri.mRequests,
+ Comparator.comparingInt(req -> req.requestId)).requestId
+ )
+ );
+ return requests;
+ }
+
+ private boolean isLiveNetworkAgent(NetworkAgentInfo nai, int what) {
+ final NetworkAgentInfo officialNai = getNetworkAgentInfoForNetwork(nai.network);
+ if (officialNai != null && officialNai.equals(nai)) return true;
+ if (officialNai != null || VDBG) {
+ loge(eventName(what) + " - isLiveNetworkAgent found mismatched netId: " + officialNai +
+ " - " + nai);
+ }
+ return false;
+ }
+
+ // must be stateless - things change under us.
+ private class NetworkStateTrackerHandler extends Handler {
+ public NetworkStateTrackerHandler(Looper looper) {
+ super(looper);
+ }
+
+ private void maybeHandleNetworkAgentMessage(Message msg) {
+ final Pair<NetworkAgentInfo, Object> arg = (Pair<NetworkAgentInfo, Object>) msg.obj;
+ final NetworkAgentInfo nai = arg.first;
+ if (!mNetworkAgentInfos.contains(nai)) {
+ if (VDBG) {
+ log(String.format("%s from unknown NetworkAgent", eventName(msg.what)));
+ }
+ return;
+ }
+
+ switch (msg.what) {
+ case NetworkAgent.EVENT_NETWORK_CAPABILITIES_CHANGED: {
+ NetworkCapabilities networkCapabilities = (NetworkCapabilities) arg.second;
+ if (networkCapabilities.hasConnectivityManagedCapability()) {
+ Log.wtf(TAG, "BUG: " + nai + " has CS-managed capability.");
+ }
+ if (networkCapabilities.hasTransport(TRANSPORT_TEST)) {
+ // Make sure the original object is not mutated. NetworkAgent normally
+ // makes a copy of the capabilities when sending the message through
+ // the Messenger, but if this ever changes, not making a defensive copy
+ // here will give attack vectors to clients using this code path.
+ networkCapabilities = new NetworkCapabilities(networkCapabilities);
+ networkCapabilities.restrictCapabilitesForTestNetwork(nai.creatorUid);
+ }
+ processCapabilitiesFromAgent(nai, networkCapabilities);
+ updateCapabilities(nai.getCurrentScore(), nai, networkCapabilities);
+ break;
+ }
+ case NetworkAgent.EVENT_NETWORK_PROPERTIES_CHANGED: {
+ LinkProperties newLp = (LinkProperties) arg.second;
+ processLinkPropertiesFromAgent(nai, newLp);
+ handleUpdateLinkProperties(nai, newLp);
+ break;
+ }
+ case NetworkAgent.EVENT_NETWORK_INFO_CHANGED: {
+ NetworkInfo info = (NetworkInfo) arg.second;
+ updateNetworkInfo(nai, info);
+ break;
+ }
+ case NetworkAgent.EVENT_NETWORK_SCORE_CHANGED: {
+ updateNetworkScore(nai, (NetworkScore) arg.second);
+ break;
+ }
+ case NetworkAgent.EVENT_SET_EXPLICITLY_SELECTED: {
+ if (nai.everConnected) {
+ loge("ERROR: cannot call explicitlySelected on already-connected network");
+ // Note that if the NAI had been connected, this would affect the
+ // score, and therefore would require re-mixing the score and performing
+ // a rematch.
+ }
+ nai.networkAgentConfig.explicitlySelected = toBool(msg.arg1);
+ nai.networkAgentConfig.acceptUnvalidated = toBool(msg.arg1) && toBool(msg.arg2);
+ // Mark the network as temporarily accepting partial connectivity so that it
+ // will be validated (and possibly become default) even if it only provides
+ // partial internet access. Note that if user connects to partial connectivity
+ // and choose "don't ask again", then wifi disconnected by some reasons(maybe
+ // out of wifi coverage) and if the same wifi is available again, the device
+ // will auto connect to this wifi even though the wifi has "no internet".
+ // TODO: Evaluate using a separate setting in IpMemoryStore.
+ nai.networkAgentConfig.acceptPartialConnectivity = toBool(msg.arg2);
+ break;
+ }
+ case NetworkAgent.EVENT_SOCKET_KEEPALIVE: {
+ mKeepaliveTracker.handleEventSocketKeepalive(nai, msg.arg1, msg.arg2);
+ break;
+ }
+ case NetworkAgent.EVENT_UNDERLYING_NETWORKS_CHANGED: {
+ // TODO: prevent loops, e.g., if a network declares itself as underlying.
+ if (!nai.supportsUnderlyingNetworks()) {
+ Log.wtf(TAG, "Non-virtual networks cannot have underlying networks");
+ break;
+ }
+
+ final List<Network> underlying = (List<Network>) arg.second;
+
+ if (isLegacyLockdownNai(nai)
+ && (underlying == null || underlying.size() != 1)) {
+ Log.wtf(TAG, "Legacy lockdown VPN " + nai.toShortString()
+ + " must have exactly one underlying network: " + underlying);
+ }
+
+ final Network[] oldUnderlying = nai.declaredUnderlyingNetworks;
+ nai.declaredUnderlyingNetworks = (underlying != null)
+ ? underlying.toArray(new Network[0]) : null;
+
+ if (!Arrays.equals(oldUnderlying, nai.declaredUnderlyingNetworks)) {
+ if (DBG) {
+ log(nai.toShortString() + " changed underlying networks to "
+ + Arrays.toString(nai.declaredUnderlyingNetworks));
+ }
+ updateCapabilitiesForNetwork(nai);
+ notifyIfacesChangedForNetworkStats();
+ }
+ break;
+ }
+ case NetworkAgent.EVENT_TEARDOWN_DELAY_CHANGED: {
+ if (msg.arg1 >= 0 && msg.arg1 <= NetworkAgent.MAX_TEARDOWN_DELAY_MS) {
+ nai.teardownDelayMs = msg.arg1;
+ } else {
+ logwtf(nai.toShortString() + " set invalid teardown delay " + msg.arg1);
+ }
+ }
+ }
+ }
+
+ private boolean maybeHandleNetworkMonitorMessage(Message msg) {
+ switch (msg.what) {
+ default:
+ return false;
+ case EVENT_PROBE_STATUS_CHANGED: {
+ final Integer netId = (Integer) msg.obj;
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
+ if (nai == null) {
+ break;
+ }
+ final boolean probePrivateDnsCompleted =
+ ((msg.arg1 & NETWORK_VALIDATION_PROBE_PRIVDNS) != 0);
+ final boolean privateDnsBroken =
+ ((msg.arg2 & NETWORK_VALIDATION_PROBE_PRIVDNS) == 0);
+ if (probePrivateDnsCompleted) {
+ if (nai.networkCapabilities.isPrivateDnsBroken() != privateDnsBroken) {
+ nai.networkCapabilities.setPrivateDnsBroken(privateDnsBroken);
+ updateCapabilitiesForNetwork(nai);
+ }
+ // Only show the notification when the private DNS is broken and the
+ // PRIVATE_DNS_BROKEN notification hasn't shown since last valid.
+ if (privateDnsBroken && !nai.networkAgentConfig.hasShownBroken) {
+ showNetworkNotification(nai, NotificationType.PRIVATE_DNS_BROKEN);
+ }
+ nai.networkAgentConfig.hasShownBroken = privateDnsBroken;
+ } else if (nai.networkCapabilities.isPrivateDnsBroken()) {
+ // If probePrivateDnsCompleted is false but nai.networkCapabilities says
+ // private DNS is broken, it means this network is being reevaluated.
+ // Either probing private DNS is not necessary any more or it hasn't been
+ // done yet. In either case, the networkCapabilities should be updated to
+ // reflect the new status.
+ nai.networkCapabilities.setPrivateDnsBroken(false);
+ updateCapabilitiesForNetwork(nai);
+ nai.networkAgentConfig.hasShownBroken = false;
+ }
+ break;
+ }
+ case EVENT_NETWORK_TESTED: {
+ final NetworkTestedResults results = (NetworkTestedResults) msg.obj;
+
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(results.mNetId);
+ if (nai == null) break;
+
+ handleNetworkTested(nai, results.mTestResult,
+ (results.mRedirectUrl == null) ? "" : results.mRedirectUrl);
+ break;
+ }
+ case EVENT_PROVISIONING_NOTIFICATION: {
+ final int netId = msg.arg2;
+ final boolean visible = toBool(msg.arg1);
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
+ // If captive portal status has changed, update capabilities or disconnect.
+ if (nai != null && (visible != nai.lastCaptivePortalDetected)) {
+ nai.lastCaptivePortalDetected = visible;
+ nai.everCaptivePortalDetected |= visible;
+ if (nai.lastCaptivePortalDetected &&
+ ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_AVOID
+ == getCaptivePortalMode()) {
+ if (DBG) log("Avoiding captive portal network: " + nai.toShortString());
+ nai.onPreventAutomaticReconnect();
+ teardownUnneededNetwork(nai);
+ break;
+ }
+ updateCapabilitiesForNetwork(nai);
+ }
+ if (!visible) {
+ // Only clear SIGN_IN and NETWORK_SWITCH notifications here, or else other
+ // notifications belong to the same network may be cleared unexpectedly.
+ mNotifier.clearNotification(netId, NotificationType.SIGN_IN);
+ mNotifier.clearNotification(netId, NotificationType.NETWORK_SWITCH);
+ } else {
+ if (nai == null) {
+ loge("EVENT_PROVISIONING_NOTIFICATION from unknown NetworkMonitor");
+ break;
+ }
+ if (!nai.networkAgentConfig.provisioningNotificationDisabled) {
+ mNotifier.showNotification(netId, NotificationType.SIGN_IN, nai, null,
+ (PendingIntent) msg.obj,
+ nai.networkAgentConfig.explicitlySelected);
+ }
+ }
+ break;
+ }
+ case EVENT_PRIVATE_DNS_CONFIG_RESOLVED: {
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2);
+ if (nai == null) break;
+
+ updatePrivateDns(nai, (PrivateDnsConfig) msg.obj);
+ break;
+ }
+ case EVENT_CAPPORT_DATA_CHANGED: {
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2);
+ if (nai == null) break;
+ handleCapportApiDataUpdate(nai, (CaptivePortalData) msg.obj);
+ break;
+ }
+ }
+ return true;
+ }
+
+ private void handleNetworkTested(
+ @NonNull NetworkAgentInfo nai, int testResult, @NonNull String redirectUrl) {
+ final boolean wasPartial = nai.partialConnectivity;
+ nai.partialConnectivity = ((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0);
+ final boolean partialConnectivityChanged =
+ (wasPartial != nai.partialConnectivity);
+
+ final boolean valid = ((testResult & NETWORK_VALIDATION_RESULT_VALID) != 0);
+ final boolean wasValidated = nai.lastValidated;
+ final boolean wasDefault = isDefaultNetwork(nai);
+
+ if (DBG) {
+ final String logMsg = !TextUtils.isEmpty(redirectUrl)
+ ? " with redirect to " + redirectUrl
+ : "";
+ log(nai.toShortString() + " validation " + (valid ? "passed" : "failed") + logMsg);
+ }
+ if (valid != nai.lastValidated) {
+ final int oldScore = nai.getCurrentScore();
+ nai.lastValidated = valid;
+ nai.everValidated |= valid;
+ updateCapabilities(oldScore, nai, nai.networkCapabilities);
+ // If score has changed, rebroadcast to NetworkProviders. b/17726566
+ if (oldScore != nai.getCurrentScore()) sendUpdatedScoreToFactories(nai);
+ if (valid) {
+ handleFreshlyValidatedNetwork(nai);
+ // Clear NO_INTERNET, PRIVATE_DNS_BROKEN, PARTIAL_CONNECTIVITY and
+ // LOST_INTERNET notifications if network becomes valid.
+ mNotifier.clearNotification(nai.network.getNetId(),
+ NotificationType.NO_INTERNET);
+ mNotifier.clearNotification(nai.network.getNetId(),
+ NotificationType.LOST_INTERNET);
+ mNotifier.clearNotification(nai.network.getNetId(),
+ NotificationType.PARTIAL_CONNECTIVITY);
+ mNotifier.clearNotification(nai.network.getNetId(),
+ NotificationType.PRIVATE_DNS_BROKEN);
+ // If network becomes valid, the hasShownBroken should be reset for
+ // that network so that the notification will be fired when the private
+ // DNS is broken again.
+ nai.networkAgentConfig.hasShownBroken = false;
+ }
+ } else if (partialConnectivityChanged) {
+ updateCapabilitiesForNetwork(nai);
+ }
+ updateInetCondition(nai);
+ // Let the NetworkAgent know the state of its network
+ // TODO: Evaluate to update partial connectivity to status to NetworkAgent.
+ nai.onValidationStatusChanged(
+ valid ? NetworkAgent.VALID_NETWORK : NetworkAgent.INVALID_NETWORK,
+ redirectUrl);
+
+ // If NetworkMonitor detects partial connectivity before
+ // EVENT_PROMPT_UNVALIDATED arrives, show the partial connectivity notification
+ // immediately. Re-notify partial connectivity silently if no internet
+ // notification already there.
+ if (!wasPartial && nai.partialConnectivity) {
+ // Remove delayed message if there is a pending message.
+ mHandler.removeMessages(EVENT_PROMPT_UNVALIDATED, nai.network);
+ handlePromptUnvalidated(nai.network);
+ }
+
+ if (wasValidated && !nai.lastValidated) {
+ handleNetworkUnvalidated(nai);
+ }
+ }
+
+ private int getCaptivePortalMode() {
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE,
+ ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_PROMPT);
+ }
+
+ private boolean maybeHandleNetworkAgentInfoMessage(Message msg) {
+ switch (msg.what) {
+ default:
+ return false;
+ case NetworkAgentInfo.EVENT_NETWORK_LINGER_COMPLETE: {
+ NetworkAgentInfo nai = (NetworkAgentInfo) msg.obj;
+ if (nai != null && isLiveNetworkAgent(nai, msg.what)) {
+ handleLingerComplete(nai);
+ }
+ break;
+ }
+ case NetworkAgentInfo.EVENT_AGENT_REGISTERED: {
+ handleNetworkAgentRegistered(msg);
+ break;
+ }
+ case NetworkAgentInfo.EVENT_AGENT_DISCONNECTED: {
+ handleNetworkAgentDisconnected(msg);
+ break;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (!maybeHandleNetworkMonitorMessage(msg)
+ && !maybeHandleNetworkAgentInfoMessage(msg)) {
+ maybeHandleNetworkAgentMessage(msg);
+ }
+ }
+ }
+
+ private class NetworkMonitorCallbacks extends INetworkMonitorCallbacks.Stub {
+ private final int mNetId;
+ private final AutodestructReference<NetworkAgentInfo> mNai;
+
+ private NetworkMonitorCallbacks(NetworkAgentInfo nai) {
+ mNetId = nai.network.getNetId();
+ mNai = new AutodestructReference<>(nai);
+ }
+
+ @Override
+ public void onNetworkMonitorCreated(INetworkMonitor networkMonitor) {
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_AGENT,
+ new Pair<>(mNai.getAndDestroy(), networkMonitor)));
+ }
+
+ @Override
+ public void notifyNetworkTested(int testResult, @Nullable String redirectUrl) {
+ // Legacy version of notifyNetworkTestedWithExtras.
+ // Would only be called if the system has a NetworkStack module older than the
+ // framework, which does not happen in practice.
+ Log.wtf(TAG, "Deprecated notifyNetworkTested called: no action taken");
+ }
+
+ @Override
+ public void notifyNetworkTestedWithExtras(NetworkTestResultParcelable p) {
+ // Notify mTrackerHandler and mConnectivityDiagnosticsHandler of the event. Both use
+ // the same looper so messages will be processed in sequence.
+ final Message msg = mTrackerHandler.obtainMessage(
+ EVENT_NETWORK_TESTED,
+ new NetworkTestedResults(
+ mNetId, p.result, p.timestampMillis, p.redirectUrl));
+ mTrackerHandler.sendMessage(msg);
+
+ // Invoke ConnectivityReport generation for this Network test event.
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(mNetId);
+ if (nai == null) return;
+
+ final PersistableBundle extras = new PersistableBundle();
+ extras.putInt(KEY_NETWORK_VALIDATION_RESULT, p.result);
+ extras.putInt(KEY_NETWORK_PROBES_SUCCEEDED_BITMASK, p.probesSucceeded);
+ extras.putInt(KEY_NETWORK_PROBES_ATTEMPTED_BITMASK, p.probesAttempted);
+
+ ConnectivityReportEvent reportEvent =
+ new ConnectivityReportEvent(p.timestampMillis, nai, extras);
+ final Message m = mConnectivityDiagnosticsHandler.obtainMessage(
+ ConnectivityDiagnosticsHandler.EVENT_NETWORK_TESTED, reportEvent);
+ mConnectivityDiagnosticsHandler.sendMessage(m);
+ }
+
+ @Override
+ public void notifyPrivateDnsConfigResolved(PrivateDnsConfigParcel config) {
+ mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
+ EVENT_PRIVATE_DNS_CONFIG_RESOLVED,
+ 0, mNetId, PrivateDnsConfig.fromParcel(config)));
+ }
+
+ @Override
+ public void notifyProbeStatusChanged(int probesCompleted, int probesSucceeded) {
+ mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
+ EVENT_PROBE_STATUS_CHANGED,
+ probesCompleted, probesSucceeded, new Integer(mNetId)));
+ }
+
+ @Override
+ public void notifyCaptivePortalDataChanged(CaptivePortalData data) {
+ mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
+ EVENT_CAPPORT_DATA_CHANGED,
+ 0, mNetId, data));
+ }
+
+ @Override
+ public void showProvisioningNotification(String action, String packageName) {
+ final Intent intent = new Intent(action);
+ intent.setPackage(packageName);
+
+ final PendingIntent pendingIntent;
+ // Only the system server can register notifications with package "android"
+ final long token = Binder.clearCallingIdentity();
+ try {
+ pendingIntent = PendingIntent.getBroadcast(
+ mContext,
+ 0 /* requestCode */,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
+ EVENT_PROVISIONING_NOTIFICATION, PROVISIONING_NOTIFICATION_SHOW,
+ mNetId, pendingIntent));
+ }
+
+ @Override
+ public void hideProvisioningNotification() {
+ mTrackerHandler.sendMessage(mTrackerHandler.obtainMessage(
+ EVENT_PROVISIONING_NOTIFICATION, PROVISIONING_NOTIFICATION_HIDE, mNetId));
+ }
+
+ @Override
+ public void notifyDataStallSuspected(DataStallReportParcelable p) {
+ ConnectivityService.this.notifyDataStallSuspected(p, mNetId);
+ }
+
+ @Override
+ public int getInterfaceVersion() {
+ return this.VERSION;
+ }
+
+ @Override
+ public String getInterfaceHash() {
+ return this.HASH;
+ }
+ }
+
+ private void notifyDataStallSuspected(DataStallReportParcelable p, int netId) {
+ log("Data stall detected with methods: " + p.detectionMethod);
+
+ final PersistableBundle extras = new PersistableBundle();
+ int detectionMethod = 0;
+ if (hasDataStallDetectionMethod(p, DETECTION_METHOD_DNS_EVENTS)) {
+ extras.putInt(KEY_DNS_CONSECUTIVE_TIMEOUTS, p.dnsConsecutiveTimeouts);
+ detectionMethod |= DETECTION_METHOD_DNS_EVENTS;
+ }
+ if (hasDataStallDetectionMethod(p, DETECTION_METHOD_TCP_METRICS)) {
+ extras.putInt(KEY_TCP_PACKET_FAIL_RATE, p.tcpPacketFailRate);
+ extras.putInt(KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS,
+ p.tcpMetricsCollectionPeriodMillis);
+ detectionMethod |= DETECTION_METHOD_TCP_METRICS;
+ }
+
+ final Message msg = mConnectivityDiagnosticsHandler.obtainMessage(
+ ConnectivityDiagnosticsHandler.EVENT_DATA_STALL_SUSPECTED, detectionMethod, netId,
+ new Pair<>(p.timestampMillis, extras));
+
+ // NetworkStateTrackerHandler currently doesn't take any actions based on data
+ // stalls so send the message directly to ConnectivityDiagnosticsHandler and avoid
+ // the cost of going through two handlers.
+ mConnectivityDiagnosticsHandler.sendMessage(msg);
+ }
+
+ private boolean hasDataStallDetectionMethod(DataStallReportParcelable p, int detectionMethod) {
+ return (p.detectionMethod & detectionMethod) != 0;
+ }
+
+ private boolean networkRequiresPrivateDnsValidation(NetworkAgentInfo nai) {
+ return isPrivateDnsValidationRequired(nai.networkCapabilities);
+ }
+
+ private void handleFreshlyValidatedNetwork(NetworkAgentInfo nai) {
+ if (nai == null) return;
+ // If the Private DNS mode is opportunistic, reprogram the DNS servers
+ // in order to restart a validation pass from within netd.
+ final PrivateDnsConfig cfg = mDnsManager.getPrivateDnsConfig();
+ if (cfg.useTls && TextUtils.isEmpty(cfg.hostname)) {
+ updateDnses(nai.linkProperties, null, nai.network.getNetId());
+ }
+ }
+
+ private void handlePrivateDnsSettingsChanged() {
+ final PrivateDnsConfig cfg = mDnsManager.getPrivateDnsConfig();
+
+ for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+ handlePerNetworkPrivateDnsConfig(nai, cfg);
+ if (networkRequiresPrivateDnsValidation(nai)) {
+ handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties));
+ }
+ }
+ }
+
+ private void handlePerNetworkPrivateDnsConfig(NetworkAgentInfo nai, PrivateDnsConfig cfg) {
+ // Private DNS only ever applies to networks that might provide
+ // Internet access and therefore also require validation.
+ if (!networkRequiresPrivateDnsValidation(nai)) return;
+
+ // Notify the NetworkAgentInfo/NetworkMonitor in case NetworkMonitor needs to cancel or
+ // schedule DNS resolutions. If a DNS resolution is required the
+ // result will be sent back to us.
+ nai.networkMonitor().notifyPrivateDnsChanged(cfg.toParcel());
+
+ // With Private DNS bypass support, we can proceed to update the
+ // Private DNS config immediately, even if we're in strict mode
+ // and have not yet resolved the provider name into a set of IPs.
+ updatePrivateDns(nai, cfg);
+ }
+
+ private void updatePrivateDns(NetworkAgentInfo nai, PrivateDnsConfig newCfg) {
+ mDnsManager.updatePrivateDns(nai.network, newCfg);
+ updateDnses(nai.linkProperties, null, nai.network.getNetId());
+ }
+
+ private void handlePrivateDnsValidationUpdate(PrivateDnsValidationUpdate update) {
+ NetworkAgentInfo nai = getNetworkAgentInfoForNetId(update.netId);
+ if (nai == null) {
+ return;
+ }
+ mDnsManager.updatePrivateDnsValidation(update);
+ handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties));
+ }
+
+ private void handleNat64PrefixEvent(int netId, int operation, String prefixAddress,
+ int prefixLength) {
+ NetworkAgentInfo nai = mNetworkForNetId.get(netId);
+ if (nai == null) return;
+
+ log(String.format("NAT64 prefix changed on netId %d: operation=%d, %s/%d",
+ netId, operation, prefixAddress, prefixLength));
+
+ IpPrefix prefix = null;
+ if (operation == IDnsResolverUnsolicitedEventListener.PREFIX_OPERATION_ADDED) {
+ try {
+ prefix = new IpPrefix(InetAddresses.parseNumericAddress(prefixAddress),
+ prefixLength);
+ } catch (IllegalArgumentException e) {
+ loge("Invalid NAT64 prefix " + prefixAddress + "/" + prefixLength);
+ return;
+ }
+ }
+
+ nai.clatd.setNat64PrefixFromDns(prefix);
+ handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties));
+ }
+
+ private void handleCapportApiDataUpdate(@NonNull final NetworkAgentInfo nai,
+ @Nullable final CaptivePortalData data) {
+ nai.capportApiData = data;
+ // CaptivePortalData will be merged into LinkProperties from NetworkAgentInfo
+ handleUpdateLinkProperties(nai, new LinkProperties(nai.linkProperties));
+ }
+
+ /**
+ * Updates the inactivity state from the network requests inside the NAI.
+ * @param nai the agent info to update
+ * @param now the timestamp of the event causing this update
+ * @return whether the network was inactive as a result of this update
+ */
+ private boolean updateInactivityState(@NonNull final NetworkAgentInfo nai, final long now) {
+ // 1. Update the inactivity timer. If it's changed, reschedule or cancel the alarm.
+ // 2. If the network was inactive and there are now requests, unset inactive.
+ // 3. If this network is unneeded (which implies it is not lingering), and there is at least
+ // one lingered request, set inactive.
+ nai.updateInactivityTimer();
+ if (nai.isInactive() && nai.numForegroundNetworkRequests() > 0) {
+ if (DBG) log("Unsetting inactive " + nai.toShortString());
+ nai.unsetInactive();
+ logNetworkEvent(nai, NetworkEvent.NETWORK_UNLINGER);
+ } else if (unneeded(nai, UnneededFor.LINGER) && nai.getInactivityExpiry() > 0) {
+ if (DBG) {
+ final int lingerTime = (int) (nai.getInactivityExpiry() - now);
+ log("Setting inactive " + nai.toShortString() + " for " + lingerTime + "ms");
+ }
+ nai.setInactive();
+ logNetworkEvent(nai, NetworkEvent.NETWORK_LINGER);
+ return true;
+ }
+ return false;
+ }
+
+ private void handleNetworkAgentRegistered(Message msg) {
+ final NetworkAgentInfo nai = (NetworkAgentInfo) msg.obj;
+ if (!mNetworkAgentInfos.contains(nai)) {
+ return;
+ }
+
+ if (msg.arg1 == NetworkAgentInfo.ARG_AGENT_SUCCESS) {
+ if (VDBG) log("NetworkAgent registered");
+ } else {
+ loge("Error connecting NetworkAgent");
+ mNetworkAgentInfos.remove(nai);
+ if (nai != null) {
+ final boolean wasDefault = isDefaultNetwork(nai);
+ synchronized (mNetworkForNetId) {
+ mNetworkForNetId.remove(nai.network.getNetId());
+ }
+ mNetIdManager.releaseNetId(nai.network.getNetId());
+ // Just in case.
+ mLegacyTypeTracker.remove(nai, wasDefault);
+ }
+ }
+ }
+
+ private void handleNetworkAgentDisconnected(Message msg) {
+ NetworkAgentInfo nai = (NetworkAgentInfo) msg.obj;
+ if (mNetworkAgentInfos.contains(nai)) {
+ disconnectAndDestroyNetwork(nai);
+ }
+ }
+
+ // Destroys a network, remove references to it from the internal state managed by
+ // ConnectivityService, free its interfaces and clean up.
+ // Must be called on the Handler thread.
+ private void disconnectAndDestroyNetwork(NetworkAgentInfo nai) {
+ ensureRunningOnConnectivityServiceThread();
+ if (DBG) {
+ log(nai.toShortString() + " disconnected, was satisfying " + nai.numNetworkRequests());
+ }
+ // Clear all notifications of this network.
+ mNotifier.clearNotification(nai.network.getNetId());
+ // A network agent has disconnected.
+ // TODO - if we move the logic to the network agent (have them disconnect
+ // because they lost all their requests or because their score isn't good)
+ // then they would disconnect organically, report their new state and then
+ // disconnect the channel.
+ if (nai.networkInfo.isConnected()) {
+ nai.networkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED,
+ null, null);
+ }
+ final boolean wasDefault = isDefaultNetwork(nai);
+ if (wasDefault) {
+ mDefaultInetConditionPublished = 0;
+ }
+ notifyIfacesChangedForNetworkStats();
+ // TODO - we shouldn't send CALLBACK_LOST to requests that can be satisfied
+ // by other networks that are already connected. Perhaps that can be done by
+ // sending all CALLBACK_LOST messages (for requests, not listens) at the end
+ // of rematchAllNetworksAndRequests
+ notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOST);
+ mKeepaliveTracker.handleStopAllKeepalives(nai, SocketKeepalive.ERROR_INVALID_NETWORK);
+
+ mQosCallbackTracker.handleNetworkReleased(nai.network);
+ for (String iface : nai.linkProperties.getAllInterfaceNames()) {
+ // Disable wakeup packet monitoring for each interface.
+ wakeupModifyInterface(iface, nai.networkCapabilities, false);
+ }
+ nai.networkMonitor().notifyNetworkDisconnected();
+ mNetworkAgentInfos.remove(nai);
+ nai.clatd.update();
+ synchronized (mNetworkForNetId) {
+ // Remove the NetworkAgent, but don't mark the netId as
+ // available until we've told netd to delete it below.
+ mNetworkForNetId.remove(nai.network.getNetId());
+ }
+ propagateUnderlyingNetworkCapabilities(nai.network);
+ // Remove all previously satisfied requests.
+ for (int i = 0; i < nai.numNetworkRequests(); i++) {
+ final NetworkRequest request = nai.requestAt(i);
+ final NetworkRequestInfo nri = mNetworkRequests.get(request);
+ final NetworkAgentInfo currentNetwork = nri.getSatisfier();
+ if (currentNetwork != null
+ && currentNetwork.network.getNetId() == nai.network.getNetId()) {
+ // uid rules for this network will be removed in destroyNativeNetwork(nai).
+ nri.setSatisfier(null, null);
+ if (request.isRequest()) {
+ sendUpdatedScoreToFactories(request, null);
+ }
+
+ if (mDefaultRequest == nri) {
+ // TODO : make battery stats aware that since 2013 multiple interfaces may be
+ // active at the same time. For now keep calling this with the default
+ // network, because while incorrect this is the closest to the old (also
+ // incorrect) behavior.
+ mNetworkActivityTracker.updateDataActivityTracking(
+ null /* newNetwork */, nai);
+ ensureNetworkTransitionWakelock(nai.toShortString());
+ }
+ }
+ }
+ nai.clearInactivityState();
+ // TODO: mLegacyTypeTracker.remove seems redundant given there's a full rematch right after.
+ // Currently, deleting it breaks tests that check for the default network disconnecting.
+ // Find out why, fix the rematch code, and delete this.
+ mLegacyTypeTracker.remove(nai, wasDefault);
+ rematchAllNetworksAndRequests();
+ mLingerMonitor.noteDisconnect(nai);
+
+ // Immediate teardown.
+ if (nai.teardownDelayMs == 0) {
+ destroyNetwork(nai);
+ return;
+ }
+
+ // Delayed teardown.
+ try {
+ mNetd.networkSetPermissionForNetwork(nai.network.netId, INetd.PERMISSION_SYSTEM);
+ } catch (RemoteException e) {
+ Log.d(TAG, "Error marking network restricted during teardown: " + e);
+ }
+ mHandler.postDelayed(() -> destroyNetwork(nai), nai.teardownDelayMs);
+ }
+
+ private void destroyNetwork(NetworkAgentInfo nai) {
+ if (nai.created) {
+ // Tell netd to clean up the configuration for this network
+ // (routing rules, DNS, etc).
+ // This may be slow as it requires a lot of netd shelling out to ip and
+ // ip[6]tables to flush routes and remove the incoming packet mark rule, so do it
+ // after we've rematched networks with requests (which might change the default
+ // network or service a new request from an app), so network traffic isn't interrupted
+ // for an unnecessarily long time.
+ destroyNativeNetwork(nai);
+ mDnsManager.removeNetwork(nai.network);
+ }
+ mNetIdManager.releaseNetId(nai.network.getNetId());
+ nai.onNetworkDestroyed();
+ }
+
+ private boolean createNativeNetwork(@NonNull NetworkAgentInfo nai) {
+ try {
+ // This should never fail. Specifying an already in use NetID will cause failure.
+ final NativeNetworkConfig config;
+ if (nai.isVPN()) {
+ if (getVpnType(nai) == VpnManager.TYPE_VPN_NONE) {
+ Log.wtf(TAG, "Unable to get VPN type from network " + nai.toShortString());
+ return false;
+ }
+ config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.VIRTUAL,
+ INetd.PERMISSION_NONE,
+ (nai.networkAgentConfig == null || !nai.networkAgentConfig.allowBypass),
+ getVpnType(nai));
+ } else {
+ config = new NativeNetworkConfig(nai.network.getNetId(), NativeNetworkType.PHYSICAL,
+ getNetworkPermission(nai.networkCapabilities), /*secure=*/ false,
+ VpnManager.TYPE_VPN_NONE);
+ }
+ mNetd.networkCreate(config);
+ mDnsResolver.createNetworkCache(nai.network.getNetId());
+ mDnsManager.updateTransportsForNetwork(nai.network.getNetId(),
+ nai.networkCapabilities.getTransportTypes());
+ return true;
+ } catch (RemoteException | ServiceSpecificException e) {
+ loge("Error creating network " + nai.toShortString() + ": " + e.getMessage());
+ return false;
+ }
+ }
+
+ private void destroyNativeNetwork(@NonNull NetworkAgentInfo nai) {
+ try {
+ mNetd.networkDestroy(nai.network.getNetId());
+ } catch (RemoteException | ServiceSpecificException e) {
+ loge("Exception destroying network(networkDestroy): " + e);
+ }
+ try {
+ mDnsResolver.destroyNetworkCache(nai.network.getNetId());
+ } catch (RemoteException | ServiceSpecificException e) {
+ loge("Exception destroying network: " + e);
+ }
+ }
+
+ // If this method proves to be too slow then we can maintain a separate
+ // pendingIntent => NetworkRequestInfo map.
+ // This method assumes that every non-null PendingIntent maps to exactly 1 NetworkRequestInfo.
+ private NetworkRequestInfo findExistingNetworkRequestInfo(PendingIntent pendingIntent) {
+ for (Map.Entry<NetworkRequest, NetworkRequestInfo> entry : mNetworkRequests.entrySet()) {
+ PendingIntent existingPendingIntent = entry.getValue().mPendingIntent;
+ if (existingPendingIntent != null &&
+ existingPendingIntent.intentFilterEquals(pendingIntent)) {
+ return entry.getValue();
+ }
+ }
+ return null;
+ }
+
+ private void handleRegisterNetworkRequestWithIntent(@NonNull final Message msg) {
+ final NetworkRequestInfo nri = (NetworkRequestInfo) (msg.obj);
+ // handleRegisterNetworkRequestWithIntent() doesn't apply to multilayer requests.
+ ensureNotMultilayerRequest(nri, "handleRegisterNetworkRequestWithIntent");
+ final NetworkRequestInfo existingRequest =
+ findExistingNetworkRequestInfo(nri.mPendingIntent);
+ if (existingRequest != null) { // remove the existing request.
+ if (DBG) {
+ log("Replacing " + existingRequest.mRequests.get(0) + " with "
+ + nri.mRequests.get(0) + " because their intents matched.");
+ }
+ handleReleaseNetworkRequest(existingRequest.mRequests.get(0), mDeps.getCallingUid(),
+ /* callOnUnavailable */ false);
+ }
+ handleRegisterNetworkRequest(nri);
+ }
+
+ private void handleRegisterNetworkRequest(@NonNull final NetworkRequestInfo nri) {
+ handleRegisterNetworkRequests(Collections.singleton(nri));
+ }
+
+ private void handleRegisterNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris) {
+ ensureRunningOnConnectivityServiceThread();
+ for (final NetworkRequestInfo nri : nris) {
+ mNetworkRequestInfoLogs.log("REGISTER " + nri);
+ for (final NetworkRequest req : nri.mRequests) {
+ mNetworkRequests.put(req, nri);
+ // TODO: Consider update signal strength for other types.
+ if (req.isListen()) {
+ for (final NetworkAgentInfo network : mNetworkAgentInfos) {
+ if (req.networkCapabilities.hasSignalStrength()
+ && network.satisfiesImmutableCapabilitiesOf(req)) {
+ updateSignalStrengthThresholds(network, "REGISTER", req);
+ }
+ }
+ }
+ }
+ // If this NRI has a satisfier already, it is replacing an older request that
+ // has been removed. Track it.
+ final NetworkRequest activeRequest = nri.getActiveRequest();
+ if (null != activeRequest) {
+ // If there is an active request, then for sure there is a satisfier.
+ nri.getSatisfier().addRequest(activeRequest);
+ }
+ }
+
+ rematchAllNetworksAndRequests();
+ for (final NetworkRequestInfo nri : nris) {
+ // If the nri is satisfied, return as its score has already been sent if needed.
+ if (nri.isBeingSatisfied()) {
+ return;
+ }
+
+ // As this request was not satisfied on rematch and thus never had any scores sent to
+ // the factories, send null now for each request of type REQUEST.
+ for (final NetworkRequest req : nri.mRequests) {
+ if (req.isRequest()) sendUpdatedScoreToFactories(req, null);
+ }
+ }
+ }
+
+ private void handleReleaseNetworkRequestWithIntent(@NonNull final PendingIntent pendingIntent,
+ final int callingUid) {
+ final NetworkRequestInfo nri = findExistingNetworkRequestInfo(pendingIntent);
+ if (nri != null) {
+ // handleReleaseNetworkRequestWithIntent() paths don't apply to multilayer requests.
+ ensureNotMultilayerRequest(nri, "handleReleaseNetworkRequestWithIntent");
+ handleReleaseNetworkRequest(
+ nri.mRequests.get(0),
+ callingUid,
+ /* callOnUnavailable */ false);
+ }
+ }
+
+ // Determines whether the network is the best (or could become the best, if it validated), for
+ // none of a particular type of NetworkRequests. The type of NetworkRequests considered depends
+ // on the value of reason:
+ //
+ // - UnneededFor.TEARDOWN: non-listen NetworkRequests. If a network is unneeded for this reason,
+ // then it should be torn down.
+ // - UnneededFor.LINGER: foreground NetworkRequests. If a network is unneeded for this reason,
+ // then it should be lingered.
+ private boolean unneeded(NetworkAgentInfo nai, UnneededFor reason) {
+ ensureRunningOnConnectivityServiceThread();
+ final int numRequests;
+ switch (reason) {
+ case TEARDOWN:
+ numRequests = nai.numRequestNetworkRequests();
+ break;
+ case LINGER:
+ numRequests = nai.numForegroundNetworkRequests();
+ break;
+ default:
+ Log.wtf(TAG, "Invalid reason. Cannot happen.");
+ return true;
+ }
+
+ if (!nai.everConnected || nai.isVPN() || nai.isInactive() || numRequests > 0) {
+ return false;
+ }
+ for (NetworkRequestInfo nri : mNetworkRequests.values()) {
+ if (reason == UnneededFor.LINGER
+ && !nri.isMultilayerRequest()
+ && nri.mRequests.get(0).isBackgroundRequest()) {
+ // Background requests don't affect lingering.
+ continue;
+ }
+
+ if (isNetworkPotentialSatisfier(nai, nri)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean isNetworkPotentialSatisfier(
+ @NonNull final NetworkAgentInfo candidate, @NonNull final NetworkRequestInfo nri) {
+ // listen requests won't keep up a network satisfying it. If this is not a multilayer
+ // request, return immediately. For multilayer requests, check to see if any of the
+ // multilayer requests may have a potential satisfier.
+ if (!nri.isMultilayerRequest() && (nri.mRequests.get(0).isListen()
+ || nri.mRequests.get(0).isListenForBest())) {
+ return false;
+ }
+ for (final NetworkRequest req : nri.mRequests) {
+ // This multilayer listen request is satisfied therefore no further requests need to be
+ // evaluated deeming this network not a potential satisfier.
+ if ((req.isListen() || req.isListenForBest()) && nri.getActiveRequest() == req) {
+ return false;
+ }
+ // As non-multilayer listen requests have already returned, the below would only happen
+ // for a multilayer request therefore continue to the next request if available.
+ if (req.isListen() || req.isListenForBest()) {
+ continue;
+ }
+ // If this Network is already the highest scoring Network for a request, or if
+ // there is hope for it to become one if it validated, then it is needed.
+ if (candidate.satisfies(req)) {
+ // As soon as a network is found that satisfies a request, return. Specifically for
+ // multilayer requests, returning as soon as a NetworkAgentInfo satisfies a request
+ // is important so as to not evaluate lower priority requests further in
+ // nri.mRequests.
+ final boolean isNetworkNeeded = candidate.isSatisfyingRequest(req.requestId)
+ // Note that this catches two important cases:
+ // 1. Unvalidated cellular will not be reaped when unvalidated WiFi
+ // is currently satisfying the request. This is desirable when
+ // cellular ends up validating but WiFi does not.
+ // 2. Unvalidated WiFi will not be reaped when validated cellular
+ // is currently satisfying the request. This is desirable when
+ // WiFi ends up validating and out scoring cellular.
+ || nri.getSatisfier().getCurrentScore()
+ < candidate.getCurrentScoreAsValidated();
+ return isNetworkNeeded;
+ }
+ }
+
+ return false;
+ }
+
+ private NetworkRequestInfo getNriForAppRequest(
+ NetworkRequest request, int callingUid, String requestedOperation) {
+ // Looking up the app passed param request in mRequests isn't possible since it may return
+ // null for a request managed by a per-app default. Therefore use getNriForAppRequest() to
+ // do the lookup since that will also find per-app default managed requests.
+ // Additionally, this lookup needs to be relatively fast (hence the lookup optimization)
+ // to avoid potential race conditions when validating a package->uid mapping when sending
+ // the callback on the very low-chance that an application shuts down prior to the callback
+ // being sent.
+ final NetworkRequestInfo nri = mNetworkRequests.get(request) != null
+ ? mNetworkRequests.get(request) : getNriForAppRequest(request);
+
+ if (nri != null) {
+ if (Process.SYSTEM_UID != callingUid && nri.mUid != callingUid) {
+ log(String.format("UID %d attempted to %s for unowned request %s",
+ callingUid, requestedOperation, nri));
+ return null;
+ }
+ }
+
+ return nri;
+ }
+
+ private void ensureNotMultilayerRequest(@NonNull final NetworkRequestInfo nri,
+ final String callingMethod) {
+ if (nri.isMultilayerRequest()) {
+ throw new IllegalStateException(
+ callingMethod + " does not support multilayer requests.");
+ }
+ }
+
+ private void handleTimedOutNetworkRequest(@NonNull final NetworkRequestInfo nri) {
+ ensureRunningOnConnectivityServiceThread();
+ // handleTimedOutNetworkRequest() is part of the requestNetwork() flow which works off of a
+ // single NetworkRequest and thus does not apply to multilayer requests.
+ ensureNotMultilayerRequest(nri, "handleTimedOutNetworkRequest");
+ if (mNetworkRequests.get(nri.mRequests.get(0)) == null) {
+ return;
+ }
+ if (nri.isBeingSatisfied()) {
+ return;
+ }
+ if (VDBG || (DBG && nri.mRequests.get(0).isRequest())) {
+ log("releasing " + nri.mRequests.get(0) + " (timeout)");
+ }
+ handleRemoveNetworkRequest(nri);
+ callCallbackForRequest(
+ nri, null, ConnectivityManager.CALLBACK_UNAVAIL, 0);
+ }
+
+ private void handleReleaseNetworkRequest(@NonNull final NetworkRequest request,
+ final int callingUid,
+ final boolean callOnUnavailable) {
+ final NetworkRequestInfo nri =
+ getNriForAppRequest(request, callingUid, "release NetworkRequest");
+ if (nri == null) {
+ return;
+ }
+ if (VDBG || (DBG && request.isRequest())) {
+ log("releasing " + request + " (release request)");
+ }
+ handleRemoveNetworkRequest(nri);
+ if (callOnUnavailable) {
+ callCallbackForRequest(nri, null, ConnectivityManager.CALLBACK_UNAVAIL, 0);
+ }
+ }
+
+ private void handleRemoveNetworkRequest(@NonNull final NetworkRequestInfo nri) {
+ ensureRunningOnConnectivityServiceThread();
+ nri.unlinkDeathRecipient();
+ for (final NetworkRequest req : nri.mRequests) {
+ mNetworkRequests.remove(req);
+ if (req.isListen()) {
+ removeListenRequestFromNetworks(req);
+ }
+ }
+ if (mDefaultNetworkRequests.remove(nri)) {
+ // If this request was one of the defaults, then the UID rules need to be updated
+ // WARNING : if the app(s) for which this network request is the default are doing
+ // traffic, this will kill their connected sockets, even if an equivalent request
+ // is going to be reinstated right away ; unconnected traffic will go on the default
+ // until the new default is set, which will happen very soon.
+ // TODO : The only way out of this is to diff old defaults and new defaults, and only
+ // remove ranges for those requests that won't have a replacement
+ final NetworkAgentInfo satisfier = nri.getSatisfier();
+ if (null != satisfier) {
+ try {
+ mNetd.networkRemoveUidRanges(satisfier.network.getNetId(),
+ toUidRangeStableParcels(nri.getUids()));
+ } catch (RemoteException e) {
+ loge("Exception setting network preference default network", e);
+ }
+ }
+ }
+ nri.decrementRequestCount();
+ mNetworkRequestInfoLogs.log("RELEASE " + nri);
+
+ if (null != nri.getActiveRequest()) {
+ if (!nri.getActiveRequest().isListen()) {
+ removeSatisfiedNetworkRequestFromNetwork(nri);
+ } else {
+ nri.setSatisfier(null, null);
+ }
+ }
+
+ cancelNpiRequests(nri);
+ }
+
+ private void handleRemoveNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris) {
+ for (final NetworkRequestInfo nri : nris) {
+ if (mDefaultRequest == nri) {
+ // Make sure we never remove the default request.
+ continue;
+ }
+ handleRemoveNetworkRequest(nri);
+ }
+ }
+
+ private void cancelNpiRequests(@NonNull final NetworkRequestInfo nri) {
+ for (final NetworkRequest req : nri.mRequests) {
+ cancelNpiRequest(req);
+ }
+ }
+
+ private void cancelNpiRequest(@NonNull final NetworkRequest req) {
+ if (req.isRequest()) {
+ for (final NetworkProviderInfo npi : mNetworkProviderInfos.values()) {
+ npi.cancelRequest(req);
+ }
+ }
+ }
+
+ private void removeListenRequestFromNetworks(@NonNull final NetworkRequest req) {
+ // listens don't have a singular affected Network. Check all networks to see
+ // if this listen request applies and remove it.
+ for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+ nai.removeRequest(req.requestId);
+ if (req.networkCapabilities.hasSignalStrength()
+ && nai.satisfiesImmutableCapabilitiesOf(req)) {
+ updateSignalStrengthThresholds(nai, "RELEASE", req);
+ }
+ }
+ }
+
+ /**
+ * Remove a NetworkRequestInfo's satisfied request from its 'satisfier' (NetworkAgentInfo) and
+ * manage the necessary upkeep (linger, teardown networks, etc.) when doing so.
+ * @param nri the NetworkRequestInfo to disassociate from its current NetworkAgentInfo
+ */
+ private void removeSatisfiedNetworkRequestFromNetwork(@NonNull final NetworkRequestInfo nri) {
+ boolean wasKept = false;
+ final NetworkAgentInfo nai = nri.getSatisfier();
+ if (nai != null) {
+ final int requestLegacyType = nri.getActiveRequest().legacyType;
+ final boolean wasBackgroundNetwork = nai.isBackgroundNetwork();
+ nai.removeRequest(nri.getActiveRequest().requestId);
+ if (VDBG || DDBG) {
+ log(" Removing from current network " + nai.toShortString()
+ + ", leaving " + nai.numNetworkRequests() + " requests.");
+ }
+ // If there are still lingered requests on this network, don't tear it down,
+ // but resume lingering instead.
+ final long now = SystemClock.elapsedRealtime();
+ if (updateInactivityState(nai, now)) {
+ notifyNetworkLosing(nai, now);
+ }
+ if (unneeded(nai, UnneededFor.TEARDOWN)) {
+ if (DBG) log("no live requests for " + nai.toShortString() + "; disconnecting");
+ teardownUnneededNetwork(nai);
+ } else {
+ wasKept = true;
+ }
+ nri.setSatisfier(null, null);
+ if (!wasBackgroundNetwork && nai.isBackgroundNetwork()) {
+ // Went from foreground to background.
+ updateCapabilitiesForNetwork(nai);
+ }
+
+ // Maintain the illusion. When this request arrived, we might have pretended
+ // that a network connected to serve it, even though the network was already
+ // connected. Now that this request has gone away, we might have to pretend
+ // that the network disconnected. LegacyTypeTracker will generate that
+ // phantom disconnect for this type.
+ if (requestLegacyType != TYPE_NONE) {
+ boolean doRemove = true;
+ if (wasKept) {
+ // check if any of the remaining requests for this network are for the
+ // same legacy type - if so, don't remove the nai
+ for (int i = 0; i < nai.numNetworkRequests(); i++) {
+ NetworkRequest otherRequest = nai.requestAt(i);
+ if (otherRequest.legacyType == requestLegacyType
+ && otherRequest.isRequest()) {
+ if (DBG) log(" still have other legacy request - leaving");
+ doRemove = false;
+ }
+ }
+ }
+
+ if (doRemove) {
+ mLegacyTypeTracker.remove(requestLegacyType, nai, false);
+ }
+ }
+ }
+ }
+
+ private PerUidCounter getRequestCounter(NetworkRequestInfo nri) {
+ return checkAnyPermissionOf(
+ nri.mPid, nri.mUid, NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
+ ? mSystemNetworkRequestCounter : mNetworkRequestCounter;
+ }
+
+ @Override
+ public void setAcceptUnvalidated(Network network, boolean accept, boolean always) {
+ enforceNetworkStackSettingsOrSetup();
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_ACCEPT_UNVALIDATED,
+ encodeBool(accept), encodeBool(always), network));
+ }
+
+ @Override
+ public void setAcceptPartialConnectivity(Network network, boolean accept, boolean always) {
+ enforceNetworkStackSettingsOrSetup();
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_ACCEPT_PARTIAL_CONNECTIVITY,
+ encodeBool(accept), encodeBool(always), network));
+ }
+
+ @Override
+ public void setAvoidUnvalidated(Network network) {
+ enforceNetworkStackSettingsOrSetup();
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_AVOID_UNVALIDATED, network));
+ }
+
+ private void handleSetAcceptUnvalidated(Network network, boolean accept, boolean always) {
+ if (DBG) log("handleSetAcceptUnvalidated network=" + network +
+ " accept=" + accept + " always=" + always);
+
+ NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+ if (nai == null) {
+ // Nothing to do.
+ return;
+ }
+
+ if (nai.everValidated) {
+ // The network validated while the dialog box was up. Take no action.
+ return;
+ }
+
+ if (!nai.networkAgentConfig.explicitlySelected) {
+ Log.wtf(TAG, "BUG: setAcceptUnvalidated non non-explicitly selected network");
+ }
+
+ if (accept != nai.networkAgentConfig.acceptUnvalidated) {
+ nai.networkAgentConfig.acceptUnvalidated = accept;
+ // If network becomes partial connectivity and user already accepted to use this
+ // network, we should respect the user's option and don't need to popup the
+ // PARTIAL_CONNECTIVITY notification to user again.
+ nai.networkAgentConfig.acceptPartialConnectivity = accept;
+ nai.updateScoreForNetworkAgentConfigUpdate();
+ rematchAllNetworksAndRequests();
+ sendUpdatedScoreToFactories(nai);
+ }
+
+ if (always) {
+ nai.onSaveAcceptUnvalidated(accept);
+ }
+
+ if (!accept) {
+ // Tell the NetworkAgent to not automatically reconnect to the network.
+ nai.onPreventAutomaticReconnect();
+ // Teardown the network.
+ teardownUnneededNetwork(nai);
+ }
+
+ }
+
+ private void handleSetAcceptPartialConnectivity(Network network, boolean accept,
+ boolean always) {
+ if (DBG) {
+ log("handleSetAcceptPartialConnectivity network=" + network + " accept=" + accept
+ + " always=" + always);
+ }
+
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+ if (nai == null) {
+ // Nothing to do.
+ return;
+ }
+
+ if (nai.lastValidated) {
+ // The network validated while the dialog box was up. Take no action.
+ return;
+ }
+
+ if (accept != nai.networkAgentConfig.acceptPartialConnectivity) {
+ nai.networkAgentConfig.acceptPartialConnectivity = accept;
+ }
+
+ // TODO: Use the current design or save the user choice into IpMemoryStore.
+ if (always) {
+ nai.onSaveAcceptUnvalidated(accept);
+ }
+
+ if (!accept) {
+ // Tell the NetworkAgent to not automatically reconnect to the network.
+ nai.onPreventAutomaticReconnect();
+ // Tear down the network.
+ teardownUnneededNetwork(nai);
+ } else {
+ // Inform NetworkMonitor that partial connectivity is acceptable. This will likely
+ // result in a partial connectivity result which will be processed by
+ // maybeHandleNetworkMonitorMessage.
+ //
+ // TODO: NetworkMonitor does not refer to the "never ask again" bit. The bit is stored
+ // per network. Therefore, NetworkMonitor may still do https probe.
+ nai.networkMonitor().setAcceptPartialConnectivity();
+ }
+ }
+
+ private void handleSetAvoidUnvalidated(Network network) {
+ NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+ if (nai == null || nai.lastValidated) {
+ // Nothing to do. The network either disconnected or revalidated.
+ return;
+ }
+ if (!nai.avoidUnvalidated) {
+ nai.avoidUnvalidated = true;
+ rematchAllNetworksAndRequests();
+ sendUpdatedScoreToFactories(nai);
+ }
+ }
+
+ private void scheduleUnvalidatedPrompt(NetworkAgentInfo nai) {
+ if (VDBG) log("scheduleUnvalidatedPrompt " + nai.network);
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(EVENT_PROMPT_UNVALIDATED, nai.network),
+ PROMPT_UNVALIDATED_DELAY_MS);
+ }
+
+ @Override
+ public void startCaptivePortalApp(Network network) {
+ enforceNetworkStackOrSettingsPermission();
+ mHandler.post(() -> {
+ NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+ if (nai == null) return;
+ if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) return;
+ nai.networkMonitor().launchCaptivePortalApp();
+ });
+ }
+
+ /**
+ * NetworkStack endpoint to start the captive portal app. The NetworkStack needs to use this
+ * endpoint as it does not have INTERACT_ACROSS_USERS_FULL itself.
+ * @param network Network on which the captive portal was detected.
+ * @param appExtras Bundle to use as intent extras for the captive portal application.
+ * Must be treated as opaque to avoid preventing the captive portal app to
+ * update its arguments.
+ */
+ @Override
+ public void startCaptivePortalAppInternal(Network network, Bundle appExtras) {
+ mContext.enforceCallingOrSelfPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+ "ConnectivityService");
+
+ final Intent appIntent = new Intent(ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
+ appIntent.putExtras(appExtras);
+ appIntent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL,
+ new CaptivePortal(new CaptivePortalImpl(network).asBinder()));
+ appIntent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mContext.startActivityAsUser(appIntent, UserHandle.CURRENT);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ private class CaptivePortalImpl extends ICaptivePortal.Stub {
+ private final Network mNetwork;
+
+ private CaptivePortalImpl(Network network) {
+ mNetwork = network;
+ }
+
+ @Override
+ public void appResponse(final int response) {
+ if (response == CaptivePortal.APP_RETURN_WANTED_AS_IS) {
+ enforceSettingsPermission();
+ }
+
+ final NetworkMonitorManager nm = getNetworkMonitorManager(mNetwork);
+ if (nm == null) return;
+ nm.notifyCaptivePortalAppFinished(response);
+ }
+
+ @Override
+ public void appRequest(final int request) {
+ final NetworkMonitorManager nm = getNetworkMonitorManager(mNetwork);
+ if (nm == null) return;
+
+ if (request == CaptivePortal.APP_REQUEST_REEVALUATION_REQUIRED) {
+ checkNetworkStackPermission();
+ nm.forceReevaluation(mDeps.getCallingUid());
+ }
+ }
+
+ @Nullable
+ private NetworkMonitorManager getNetworkMonitorManager(final Network network) {
+ // getNetworkAgentInfoForNetwork is thread-safe
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+ if (nai == null) return null;
+
+ // nai.networkMonitor() is thread-safe
+ return nai.networkMonitor();
+ }
+ }
+
+ public boolean avoidBadWifi() {
+ return mMultinetworkPolicyTracker.getAvoidBadWifi();
+ }
+
+ /**
+ * Return whether the device should maintain continuous, working connectivity by switching away
+ * from WiFi networks having no connectivity.
+ * @see MultinetworkPolicyTracker#getAvoidBadWifi()
+ */
+ public boolean shouldAvoidBadWifi() {
+ if (!checkNetworkStackPermission()) {
+ throw new SecurityException("avoidBadWifi requires NETWORK_STACK permission");
+ }
+ return avoidBadWifi();
+ }
+
+
+ private void rematchForAvoidBadWifiUpdate() {
+ rematchAllNetworksAndRequests();
+ for (NetworkAgentInfo nai: mNetworkAgentInfos) {
+ if (nai.networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+ sendUpdatedScoreToFactories(nai);
+ }
+ }
+ }
+
+ // TODO: Evaluate whether this is of interest to other consumers of
+ // MultinetworkPolicyTracker and worth moving out of here.
+ private void dumpAvoidBadWifiSettings(IndentingPrintWriter pw) {
+ final boolean configRestrict = mMultinetworkPolicyTracker.configRestrictsAvoidBadWifi();
+ if (!configRestrict) {
+ pw.println("Bad Wi-Fi avoidance: unrestricted");
+ return;
+ }
+
+ pw.println("Bad Wi-Fi avoidance: " + avoidBadWifi());
+ pw.increaseIndent();
+ pw.println("Config restrict: " + configRestrict);
+
+ final String value = mMultinetworkPolicyTracker.getAvoidBadWifiSetting();
+ String description;
+ // Can't use a switch statement because strings are legal case labels, but null is not.
+ if ("0".equals(value)) {
+ description = "get stuck";
+ } else if (value == null) {
+ description = "prompt";
+ } else if ("1".equals(value)) {
+ description = "avoid";
+ } else {
+ description = value + " (?)";
+ }
+ pw.println("User setting: " + description);
+ pw.println("Network overrides:");
+ pw.increaseIndent();
+ for (NetworkAgentInfo nai : networksSortedById()) {
+ if (nai.avoidUnvalidated) {
+ pw.println(nai.toShortString());
+ }
+ }
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+ }
+
+ // TODO: This method is copied from TetheringNotificationUpdater. Should have a utility class to
+ // unify the method.
+ private static @NonNull String getSettingsPackageName(@NonNull final PackageManager pm) {
+ final Intent settingsIntent = new Intent(Settings.ACTION_SETTINGS);
+ final ComponentName settingsComponent = settingsIntent.resolveActivity(pm);
+ return settingsComponent != null
+ ? settingsComponent.getPackageName() : "com.android.settings";
+ }
+
+ private void showNetworkNotification(NetworkAgentInfo nai, NotificationType type) {
+ final String action;
+ final boolean highPriority;
+ switch (type) {
+ case NO_INTERNET:
+ action = ConnectivityManager.ACTION_PROMPT_UNVALIDATED;
+ // High priority because it is only displayed for explicitly selected networks.
+ highPriority = true;
+ break;
+ case PRIVATE_DNS_BROKEN:
+ action = Settings.ACTION_WIRELESS_SETTINGS;
+ // High priority because we should let user know why there is no internet.
+ highPriority = true;
+ break;
+ case LOST_INTERNET:
+ action = ConnectivityManager.ACTION_PROMPT_LOST_VALIDATION;
+ // High priority because it could help the user avoid unexpected data usage.
+ highPriority = true;
+ break;
+ case PARTIAL_CONNECTIVITY:
+ action = ConnectivityManager.ACTION_PROMPT_PARTIAL_CONNECTIVITY;
+ // Don't bother the user with a high-priority notification if the network was not
+ // explicitly selected by the user.
+ highPriority = nai.networkAgentConfig.explicitlySelected;
+ break;
+ default:
+ Log.wtf(TAG, "Unknown notification type " + type);
+ return;
+ }
+
+ Intent intent = new Intent(action);
+ if (type != NotificationType.PRIVATE_DNS_BROKEN) {
+ intent.putExtra(ConnectivityManager.EXTRA_NETWORK, nai.network);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // Some OEMs have their own Settings package. Thus, need to get the current using
+ // Settings package name instead of just use default name "com.android.settings".
+ final String settingsPkgName = getSettingsPackageName(mContext.getPackageManager());
+ intent.setClassName(settingsPkgName,
+ settingsPkgName + ".wifi.WifiNoInternetDialog");
+ }
+
+ PendingIntent pendingIntent = PendingIntent.getActivity(
+ mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
+ 0 /* requestCode */,
+ intent,
+ PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+ mNotifier.showNotification(
+ nai.network.getNetId(), type, nai, null, pendingIntent, highPriority);
+ }
+
+ private boolean shouldPromptUnvalidated(NetworkAgentInfo nai) {
+ // Don't prompt if the network is validated, and don't prompt on captive portals
+ // because we're already prompting the user to sign in.
+ if (nai.everValidated || nai.everCaptivePortalDetected) {
+ return false;
+ }
+
+ // If a network has partial connectivity, always prompt unless the user has already accepted
+ // partial connectivity and selected don't ask again. This ensures that if the device
+ // automatically connects to a network that has partial Internet access, the user will
+ // always be able to use it, either because they've already chosen "don't ask again" or
+ // because we have prompt them.
+ if (nai.partialConnectivity && !nai.networkAgentConfig.acceptPartialConnectivity) {
+ return true;
+ }
+
+ // If a network has no Internet access, only prompt if the network was explicitly selected
+ // and if the user has not already told us to use the network regardless of whether it
+ // validated or not.
+ if (nai.networkAgentConfig.explicitlySelected
+ && !nai.networkAgentConfig.acceptUnvalidated) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private void handlePromptUnvalidated(Network network) {
+ if (VDBG || DDBG) log("handlePromptUnvalidated " + network);
+ NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+
+ if (nai == null || !shouldPromptUnvalidated(nai)) {
+ return;
+ }
+
+ // Stop automatically reconnecting to this network in the future. Automatically connecting
+ // to a network that provides no or limited connectivity is not useful, because the user
+ // cannot use that network except through the notification shown by this method, and the
+ // notification is only shown if the network is explicitly selected by the user.
+ nai.onPreventAutomaticReconnect();
+
+ // TODO: Evaluate if it's needed to wait 8 seconds for triggering notification when
+ // NetworkMonitor detects the network is partial connectivity. Need to change the design to
+ // popup the notification immediately when the network is partial connectivity.
+ if (nai.partialConnectivity) {
+ showNetworkNotification(nai, NotificationType.PARTIAL_CONNECTIVITY);
+ } else {
+ showNetworkNotification(nai, NotificationType.NO_INTERNET);
+ }
+ }
+
+ private void handleNetworkUnvalidated(NetworkAgentInfo nai) {
+ NetworkCapabilities nc = nai.networkCapabilities;
+ if (DBG) log("handleNetworkUnvalidated " + nai.toShortString() + " cap=" + nc);
+
+ if (!nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+ return;
+ }
+
+ if (mMultinetworkPolicyTracker.shouldNotifyWifiUnvalidated()) {
+ showNetworkNotification(nai, NotificationType.LOST_INTERNET);
+ }
+ }
+
+ @Override
+ public int getMultipathPreference(Network network) {
+ enforceAccessPermission();
+
+ NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+ if (nai != null && nai.networkCapabilities
+ .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) {
+ return ConnectivityManager.MULTIPATH_PREFERENCE_UNMETERED;
+ }
+
+ final NetworkPolicyManager netPolicyManager =
+ mContext.getSystemService(NetworkPolicyManager.class);
+
+ final long token = Binder.clearCallingIdentity();
+ final int networkPreference;
+ try {
+ networkPreference = netPolicyManager.getMultipathPreference(network);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ if (networkPreference != 0) {
+ return networkPreference;
+ }
+ return mMultinetworkPolicyTracker.getMeteredMultipathPreference();
+ }
+
+ @Override
+ public NetworkRequest getDefaultRequest() {
+ return mDefaultRequest.mRequests.get(0);
+ }
+
+ private class InternalHandler extends Handler {
+ public InternalHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_EXPIRE_NET_TRANSITION_WAKELOCK:
+ case EVENT_CLEAR_NET_TRANSITION_WAKELOCK: {
+ handleReleaseNetworkTransitionWakelock(msg.what);
+ break;
+ }
+ case EVENT_APPLY_GLOBAL_HTTP_PROXY: {
+ mProxyTracker.loadDeprecatedGlobalHttpProxy();
+ break;
+ }
+ case EVENT_PROXY_HAS_CHANGED: {
+ final Pair<Network, ProxyInfo> arg = (Pair<Network, ProxyInfo>) msg.obj;
+ handleApplyDefaultProxy(arg.second);
+ break;
+ }
+ case EVENT_REGISTER_NETWORK_PROVIDER: {
+ handleRegisterNetworkProvider((NetworkProviderInfo) msg.obj);
+ break;
+ }
+ case EVENT_UNREGISTER_NETWORK_PROVIDER: {
+ handleUnregisterNetworkProvider((Messenger) msg.obj);
+ break;
+ }
+ case EVENT_REGISTER_NETWORK_OFFER: {
+ handleRegisterNetworkOffer((NetworkOffer) msg.obj);
+ break;
+ }
+ case EVENT_UNREGISTER_NETWORK_OFFER: {
+ final NetworkOfferInfo offer =
+ findNetworkOfferInfoByCallback((INetworkOfferCallback) msg.obj);
+ if (null != offer) {
+ handleUnregisterNetworkOffer(offer);
+ }
+ break;
+ }
+ case EVENT_REGISTER_NETWORK_AGENT: {
+ final Pair<NetworkAgentInfo, INetworkMonitor> arg =
+ (Pair<NetworkAgentInfo, INetworkMonitor>) msg.obj;
+ handleRegisterNetworkAgent(arg.first, arg.second);
+ break;
+ }
+ case EVENT_REGISTER_NETWORK_REQUEST:
+ case EVENT_REGISTER_NETWORK_LISTENER: {
+ handleRegisterNetworkRequest((NetworkRequestInfo) msg.obj);
+ break;
+ }
+ case EVENT_REGISTER_NETWORK_REQUEST_WITH_INTENT:
+ case EVENT_REGISTER_NETWORK_LISTENER_WITH_INTENT: {
+ handleRegisterNetworkRequestWithIntent(msg);
+ break;
+ }
+ case EVENT_TIMEOUT_NETWORK_REQUEST: {
+ NetworkRequestInfo nri = (NetworkRequestInfo) msg.obj;
+ handleTimedOutNetworkRequest(nri);
+ break;
+ }
+ case EVENT_RELEASE_NETWORK_REQUEST_WITH_INTENT: {
+ handleReleaseNetworkRequestWithIntent((PendingIntent) msg.obj, msg.arg1);
+ break;
+ }
+ case EVENT_RELEASE_NETWORK_REQUEST: {
+ handleReleaseNetworkRequest((NetworkRequest) msg.obj, msg.arg1,
+ /* callOnUnavailable */ false);
+ break;
+ }
+ case EVENT_SET_ACCEPT_UNVALIDATED: {
+ Network network = (Network) msg.obj;
+ handleSetAcceptUnvalidated(network, toBool(msg.arg1), toBool(msg.arg2));
+ break;
+ }
+ case EVENT_SET_ACCEPT_PARTIAL_CONNECTIVITY: {
+ Network network = (Network) msg.obj;
+ handleSetAcceptPartialConnectivity(network, toBool(msg.arg1),
+ toBool(msg.arg2));
+ break;
+ }
+ case EVENT_SET_AVOID_UNVALIDATED: {
+ handleSetAvoidUnvalidated((Network) msg.obj);
+ break;
+ }
+ case EVENT_PROMPT_UNVALIDATED: {
+ handlePromptUnvalidated((Network) msg.obj);
+ break;
+ }
+ case EVENT_CONFIGURE_ALWAYS_ON_NETWORKS: {
+ handleConfigureAlwaysOnNetworks();
+ break;
+ }
+ // Sent by KeepaliveTracker to process an app request on the state machine thread.
+ case NetworkAgent.CMD_START_SOCKET_KEEPALIVE: {
+ mKeepaliveTracker.handleStartKeepalive(msg);
+ break;
+ }
+ // Sent by KeepaliveTracker to process an app request on the state machine thread.
+ case NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE: {
+ NetworkAgentInfo nai = getNetworkAgentInfoForNetwork((Network) msg.obj);
+ int slot = msg.arg1;
+ int reason = msg.arg2;
+ mKeepaliveTracker.handleStopKeepalive(nai, slot, reason);
+ break;
+ }
+ case EVENT_REVALIDATE_NETWORK: {
+ handleReportNetworkConnectivity((Network) msg.obj, msg.arg1, toBool(msg.arg2));
+ break;
+ }
+ case EVENT_PRIVATE_DNS_SETTINGS_CHANGED:
+ handlePrivateDnsSettingsChanged();
+ break;
+ case EVENT_PRIVATE_DNS_VALIDATION_UPDATE:
+ handlePrivateDnsValidationUpdate(
+ (PrivateDnsValidationUpdate) msg.obj);
+ break;
+ case EVENT_UID_BLOCKED_REASON_CHANGED:
+ handleUidBlockedReasonChanged(msg.arg1, msg.arg2);
+ break;
+ case EVENT_SET_REQUIRE_VPN_FOR_UIDS:
+ handleSetRequireVpnForUids(toBool(msg.arg1), (UidRange[]) msg.obj);
+ break;
+ case EVENT_SET_OEM_NETWORK_PREFERENCE: {
+ final Pair<OemNetworkPreferences, IOnCompleteListener> arg =
+ (Pair<OemNetworkPreferences, IOnCompleteListener>) msg.obj;
+ handleSetOemNetworkPreference(arg.first, arg.second);
+ break;
+ }
+ case EVENT_SET_PROFILE_NETWORK_PREFERENCE: {
+ final Pair<ProfileNetworkPreferences.Preference, IOnCompleteListener> arg =
+ (Pair<ProfileNetworkPreferences.Preference, IOnCompleteListener>)
+ msg.obj;
+ handleSetProfileNetworkPreference(arg.first, arg.second);
+ break;
+ }
+ case EVENT_REPORT_NETWORK_ACTIVITY:
+ mNetworkActivityTracker.handleReportNetworkActivity();
+ break;
+ }
+ }
+ }
+
+ @Override
+ @Deprecated
+ public int getLastTetherError(String iface) {
+ final TetheringManager tm = (TetheringManager) mContext.getSystemService(
+ Context.TETHERING_SERVICE);
+ return tm.getLastTetherError(iface);
+ }
+
+ @Override
+ @Deprecated
+ public String[] getTetherableIfaces() {
+ final TetheringManager tm = (TetheringManager) mContext.getSystemService(
+ Context.TETHERING_SERVICE);
+ return tm.getTetherableIfaces();
+ }
+
+ @Override
+ @Deprecated
+ public String[] getTetheredIfaces() {
+ final TetheringManager tm = (TetheringManager) mContext.getSystemService(
+ Context.TETHERING_SERVICE);
+ return tm.getTetheredIfaces();
+ }
+
+
+ @Override
+ @Deprecated
+ public String[] getTetheringErroredIfaces() {
+ final TetheringManager tm = (TetheringManager) mContext.getSystemService(
+ Context.TETHERING_SERVICE);
+
+ return tm.getTetheringErroredIfaces();
+ }
+
+ @Override
+ @Deprecated
+ public String[] getTetherableUsbRegexs() {
+ final TetheringManager tm = (TetheringManager) mContext.getSystemService(
+ Context.TETHERING_SERVICE);
+
+ return tm.getTetherableUsbRegexs();
+ }
+
+ @Override
+ @Deprecated
+ public String[] getTetherableWifiRegexs() {
+ final TetheringManager tm = (TetheringManager) mContext.getSystemService(
+ Context.TETHERING_SERVICE);
+ return tm.getTetherableWifiRegexs();
+ }
+
+ // Called when we lose the default network and have no replacement yet.
+ // This will automatically be cleared after X seconds or a new default network
+ // becomes CONNECTED, whichever happens first. The timer is started by the
+ // first caller and not restarted by subsequent callers.
+ private void ensureNetworkTransitionWakelock(String forWhom) {
+ synchronized (this) {
+ if (mNetTransitionWakeLock.isHeld()) {
+ return;
+ }
+ mNetTransitionWakeLock.acquire();
+ mLastWakeLockAcquireTimestamp = SystemClock.elapsedRealtime();
+ mTotalWakelockAcquisitions++;
+ }
+ mWakelockLogs.log("ACQUIRE for " + forWhom);
+ Message msg = mHandler.obtainMessage(EVENT_EXPIRE_NET_TRANSITION_WAKELOCK);
+ final int lockTimeout = mResources.get().getInteger(
+ R.integer.config_networkTransitionTimeout);
+ mHandler.sendMessageDelayed(msg, lockTimeout);
+ }
+
+ // Called when we gain a new default network to release the network transition wakelock in a
+ // second, to allow a grace period for apps to reconnect over the new network. Pending expiry
+ // message is cancelled.
+ private void scheduleReleaseNetworkTransitionWakelock() {
+ synchronized (this) {
+ if (!mNetTransitionWakeLock.isHeld()) {
+ return; // expiry message released the lock first.
+ }
+ }
+ // Cancel self timeout on wakelock hold.
+ mHandler.removeMessages(EVENT_EXPIRE_NET_TRANSITION_WAKELOCK);
+ Message msg = mHandler.obtainMessage(EVENT_CLEAR_NET_TRANSITION_WAKELOCK);
+ mHandler.sendMessageDelayed(msg, 1000);
+ }
+
+ // Called when either message of ensureNetworkTransitionWakelock or
+ // scheduleReleaseNetworkTransitionWakelock is processed.
+ private void handleReleaseNetworkTransitionWakelock(int eventId) {
+ String event = eventName(eventId);
+ synchronized (this) {
+ if (!mNetTransitionWakeLock.isHeld()) {
+ mWakelockLogs.log(String.format("RELEASE: already released (%s)", event));
+ Log.w(TAG, "expected Net Transition WakeLock to be held");
+ return;
+ }
+ mNetTransitionWakeLock.release();
+ long lockDuration = SystemClock.elapsedRealtime() - mLastWakeLockAcquireTimestamp;
+ mTotalWakelockDurationMs += lockDuration;
+ mMaxWakelockDurationMs = Math.max(mMaxWakelockDurationMs, lockDuration);
+ mTotalWakelockReleases++;
+ }
+ mWakelockLogs.log(String.format("RELEASE (%s)", event));
+ }
+
+ // 100 percent is full good, 0 is full bad.
+ @Override
+ public void reportInetCondition(int networkType, int percentage) {
+ NetworkAgentInfo nai = mLegacyTypeTracker.getNetworkForType(networkType);
+ if (nai == null) return;
+ reportNetworkConnectivity(nai.network, percentage > 50);
+ }
+
+ @Override
+ public void reportNetworkConnectivity(Network network, boolean hasConnectivity) {
+ enforceAccessPermission();
+ enforceInternetPermission();
+ final int uid = mDeps.getCallingUid();
+ final int connectivityInfo = encodeBool(hasConnectivity);
+
+ // Handle ConnectivityDiagnostics event before attempting to revalidate the network. This
+ // forces an ordering of ConnectivityDiagnostics events in the case where hasConnectivity
+ // does not match the known connectivity of the network - this causes NetworkMonitor to
+ // revalidate the network and generate a ConnectivityDiagnostics ConnectivityReport event.
+ final NetworkAgentInfo nai;
+ if (network == null) {
+ nai = getDefaultNetwork();
+ } else {
+ nai = getNetworkAgentInfoForNetwork(network);
+ }
+ if (nai != null) {
+ mConnectivityDiagnosticsHandler.sendMessage(
+ mConnectivityDiagnosticsHandler.obtainMessage(
+ ConnectivityDiagnosticsHandler.EVENT_NETWORK_CONNECTIVITY_REPORTED,
+ connectivityInfo, 0, nai));
+ }
+
+ mHandler.sendMessage(
+ mHandler.obtainMessage(EVENT_REVALIDATE_NETWORK, uid, connectivityInfo, network));
+ }
+
+ private void handleReportNetworkConnectivity(
+ Network network, int uid, boolean hasConnectivity) {
+ final NetworkAgentInfo nai;
+ if (network == null) {
+ nai = getDefaultNetwork();
+ } else {
+ nai = getNetworkAgentInfoForNetwork(network);
+ }
+ if (nai == null || nai.networkInfo.getState() == NetworkInfo.State.DISCONNECTING ||
+ nai.networkInfo.getState() == NetworkInfo.State.DISCONNECTED) {
+ return;
+ }
+ // Revalidate if the app report does not match our current validated state.
+ if (hasConnectivity == nai.lastValidated) {
+ return;
+ }
+ if (DBG) {
+ int netid = nai.network.getNetId();
+ log("reportNetworkConnectivity(" + netid + ", " + hasConnectivity + ") by " + uid);
+ }
+ // Validating a network that has not yet connected could result in a call to
+ // rematchNetworkAndRequests() which is not meant to work on such networks.
+ if (!nai.everConnected) {
+ return;
+ }
+ final NetworkCapabilities nc = getNetworkCapabilitiesInternal(nai);
+ if (isNetworkWithCapabilitiesBlocked(nc, uid, false)) {
+ return;
+ }
+ nai.networkMonitor().forceReevaluation(uid);
+ }
+
+ // TODO: call into netd.
+ private boolean queryUserAccess(int uid, Network network) {
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+ if (nai == null) return false;
+
+ // Any UID can use its default network.
+ if (nai == getDefaultNetworkForUid(uid)) return true;
+
+ // Privileged apps can use any network.
+ if (mPermissionMonitor.hasRestrictedNetworksPermission(uid)) {
+ return true;
+ }
+
+ // An unprivileged UID can use a VPN iff the VPN applies to it.
+ if (nai.isVPN()) {
+ return nai.networkCapabilities.appliesToUid(uid);
+ }
+
+ // An unprivileged UID can bypass the VPN that applies to it only if it can protect its
+ // sockets, i.e., if it is the owner.
+ final NetworkAgentInfo vpn = getVpnForUid(uid);
+ if (vpn != null && !vpn.networkAgentConfig.allowBypass
+ && uid != vpn.networkCapabilities.getOwnerUid()) {
+ return false;
+ }
+
+ // The UID's permission must be at least sufficient for the network. Since the restricted
+ // permission was already checked above, that just leaves background networks.
+ if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_FOREGROUND)) {
+ return mPermissionMonitor.hasUseBackgroundNetworksPermission(uid);
+ }
+
+ // Unrestricted network. Anyone gets to use it.
+ return true;
+ }
+
+ /**
+ * Returns information about the proxy a certain network is using. If given a null network, it
+ * it will return the proxy for the bound network for the caller app or the default proxy if
+ * none.
+ *
+ * @param network the network we want to get the proxy information for.
+ * @return Proxy information if a network has a proxy configured, or otherwise null.
+ */
+ @Override
+ public ProxyInfo getProxyForNetwork(Network network) {
+ final ProxyInfo globalProxy = mProxyTracker.getGlobalProxy();
+ if (globalProxy != null) return globalProxy;
+ if (network == null) {
+ // Get the network associated with the calling UID.
+ final Network activeNetwork = getActiveNetworkForUidInternal(mDeps.getCallingUid(),
+ true);
+ if (activeNetwork == null) {
+ return null;
+ }
+ return getLinkPropertiesProxyInfo(activeNetwork);
+ } else if (mDeps.queryUserAccess(mDeps.getCallingUid(), network, this)) {
+ // Don't call getLinkProperties() as it requires ACCESS_NETWORK_STATE permission, which
+ // caller may not have.
+ return getLinkPropertiesProxyInfo(network);
+ }
+ // No proxy info available if the calling UID does not have network access.
+ return null;
+ }
+
+
+ private ProxyInfo getLinkPropertiesProxyInfo(Network network) {
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+ if (nai == null) return null;
+ synchronized (nai) {
+ final ProxyInfo linkHttpProxy = nai.linkProperties.getHttpProxy();
+ return linkHttpProxy == null ? null : new ProxyInfo(linkHttpProxy);
+ }
+ }
+
+ @Override
+ public void setGlobalProxy(@Nullable final ProxyInfo proxyProperties) {
+ PermissionUtils.enforceNetworkStackPermission(mContext);
+ mProxyTracker.setGlobalProxy(proxyProperties);
+ }
+
+ @Override
+ @Nullable
+ public ProxyInfo getGlobalProxy() {
+ return mProxyTracker.getGlobalProxy();
+ }
+
+ private void handleApplyDefaultProxy(ProxyInfo proxy) {
+ if (proxy != null && TextUtils.isEmpty(proxy.getHost())
+ && Uri.EMPTY.equals(proxy.getPacFileUrl())) {
+ proxy = null;
+ }
+ mProxyTracker.setDefaultProxy(proxy);
+ }
+
+ // If the proxy has changed from oldLp to newLp, resend proxy broadcast. This method gets called
+ // when any network changes proxy.
+ // TODO: Remove usage of broadcast extras as they are deprecated and not applicable in a
+ // multi-network world where an app might be bound to a non-default network.
+ private void updateProxy(LinkProperties newLp, LinkProperties oldLp) {
+ ProxyInfo newProxyInfo = newLp == null ? null : newLp.getHttpProxy();
+ ProxyInfo oldProxyInfo = oldLp == null ? null : oldLp.getHttpProxy();
+
+ if (!ProxyTracker.proxyInfoEqual(newProxyInfo, oldProxyInfo)) {
+ mProxyTracker.sendProxyBroadcast();
+ }
+ }
+
+ private static class SettingsObserver extends ContentObserver {
+ final private HashMap<Uri, Integer> mUriEventMap;
+ final private Context mContext;
+ final private Handler mHandler;
+
+ SettingsObserver(Context context, Handler handler) {
+ super(null);
+ mUriEventMap = new HashMap<>();
+ mContext = context;
+ mHandler = handler;
+ }
+
+ void observe(Uri uri, int what) {
+ mUriEventMap.put(uri, what);
+ final ContentResolver resolver = mContext.getContentResolver();
+ resolver.registerContentObserver(uri, false, this);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ Log.wtf(TAG, "Should never be reached.");
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ final Integer what = mUriEventMap.get(uri);
+ if (what != null) {
+ mHandler.obtainMessage(what).sendToTarget();
+ } else {
+ loge("No matching event to send for URI=" + uri);
+ }
+ }
+ }
+
+ private static void log(String s) {
+ Log.d(TAG, s);
+ }
+
+ private static void logw(String s) {
+ Log.w(TAG, s);
+ }
+
+ private static void logwtf(String s) {
+ Log.wtf(TAG, s);
+ }
+
+ private static void logwtf(String s, Throwable t) {
+ Log.wtf(TAG, s, t);
+ }
+
+ private static void loge(String s) {
+ Log.e(TAG, s);
+ }
+
+ private static void loge(String s, Throwable t) {
+ Log.e(TAG, s, t);
+ }
+
+ /**
+ * Return the information of all ongoing VPNs.
+ *
+ * <p>This method is used to update NetworkStatsService.
+ *
+ * <p>Must be called on the handler thread.
+ */
+ private UnderlyingNetworkInfo[] getAllVpnInfo() {
+ ensureRunningOnConnectivityServiceThread();
+ if (mLockdownEnabled) {
+ return new UnderlyingNetworkInfo[0];
+ }
+ List<UnderlyingNetworkInfo> infoList = new ArrayList<>();
+ for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+ UnderlyingNetworkInfo info = createVpnInfo(nai);
+ if (info != null) {
+ infoList.add(info);
+ }
+ }
+ return infoList.toArray(new UnderlyingNetworkInfo[infoList.size()]);
+ }
+
+ /**
+ * @return VPN information for accounting, or null if we can't retrieve all required
+ * information, e.g underlying ifaces.
+ */
+ private UnderlyingNetworkInfo createVpnInfo(NetworkAgentInfo nai) {
+ if (!nai.isVPN()) return null;
+
+ Network[] underlyingNetworks = nai.declaredUnderlyingNetworks;
+ // see VpnService.setUnderlyingNetworks()'s javadoc about how to interpret
+ // the underlyingNetworks list.
+ if (underlyingNetworks == null) {
+ final NetworkAgentInfo defaultNai = getDefaultNetworkForUid(
+ nai.networkCapabilities.getOwnerUid());
+ if (defaultNai != null) {
+ underlyingNetworks = new Network[] { defaultNai.network };
+ }
+ }
+
+ if (CollectionUtils.isEmpty(underlyingNetworks)) return null;
+
+ List<String> interfaces = new ArrayList<>();
+ for (Network network : underlyingNetworks) {
+ NetworkAgentInfo underlyingNai = getNetworkAgentInfoForNetwork(network);
+ if (underlyingNai == null) continue;
+ LinkProperties lp = underlyingNai.linkProperties;
+ for (String iface : lp.getAllInterfaceNames()) {
+ if (!TextUtils.isEmpty(iface)) {
+ interfaces.add(iface);
+ }
+ }
+ }
+
+ if (interfaces.isEmpty()) return null;
+
+ // Must be non-null or NetworkStatsService will crash.
+ // Cannot happen in production code because Vpn only registers the NetworkAgent after the
+ // tun or ipsec interface is created.
+ // TODO: Remove this check.
+ if (nai.linkProperties.getInterfaceName() == null) return null;
+
+ return new UnderlyingNetworkInfo(nai.networkCapabilities.getOwnerUid(),
+ nai.linkProperties.getInterfaceName(), interfaces);
+ }
+
+ // TODO This needs to be the default network that applies to the NAI.
+ private Network[] underlyingNetworksOrDefault(final int ownerUid,
+ Network[] underlyingNetworks) {
+ final Network defaultNetwork = getNetwork(getDefaultNetworkForUid(ownerUid));
+ if (underlyingNetworks == null && defaultNetwork != null) {
+ // null underlying networks means to track the default.
+ underlyingNetworks = new Network[] { defaultNetwork };
+ }
+ return underlyingNetworks;
+ }
+
+ // Returns true iff |network| is an underlying network of |nai|.
+ private boolean hasUnderlyingNetwork(NetworkAgentInfo nai, Network network) {
+ // TODO: support more than one level of underlying networks, either via a fixed-depth search
+ // (e.g., 2 levels of underlying networks), or via loop detection, or....
+ if (!nai.supportsUnderlyingNetworks()) return false;
+ final Network[] underlying = underlyingNetworksOrDefault(
+ nai.networkCapabilities.getOwnerUid(), nai.declaredUnderlyingNetworks);
+ return CollectionUtils.contains(underlying, network);
+ }
+
+ /**
+ * Recompute the capabilities for any networks that had a specific network as underlying.
+ *
+ * When underlying networks change, such networks may have to update capabilities to reflect
+ * things like the metered bit, their transports, and so on. The capabilities are calculated
+ * immediately. This method runs on the ConnectivityService thread.
+ */
+ private void propagateUnderlyingNetworkCapabilities(Network updatedNetwork) {
+ ensureRunningOnConnectivityServiceThread();
+ for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+ if (updatedNetwork == null || hasUnderlyingNetwork(nai, updatedNetwork)) {
+ updateCapabilitiesForNetwork(nai);
+ }
+ }
+ }
+
+ private boolean isUidBlockedByVpn(int uid, List<UidRange> blockedUidRanges) {
+ // Determine whether this UID is blocked because of always-on VPN lockdown. If a VPN applies
+ // to the UID, then the UID is not blocked because always-on VPN lockdown applies only when
+ // a VPN is not up.
+ final NetworkAgentInfo vpnNai = getVpnForUid(uid);
+ if (vpnNai != null && !vpnNai.networkAgentConfig.allowBypass) return false;
+ for (UidRange range : blockedUidRanges) {
+ if (range.contains(uid)) return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void setRequireVpnForUids(boolean requireVpn, UidRange[] ranges) {
+ enforceNetworkStackOrSettingsPermission();
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_REQUIRE_VPN_FOR_UIDS,
+ encodeBool(requireVpn), 0 /* arg2 */, ranges));
+ }
+
+ private void handleSetRequireVpnForUids(boolean requireVpn, UidRange[] ranges) {
+ if (DBG) {
+ Log.d(TAG, "Setting VPN " + (requireVpn ? "" : "not ") + "required for UIDs: "
+ + Arrays.toString(ranges));
+ }
+ // Cannot use a Set since the list of UID ranges might contain duplicates.
+ final List<UidRange> newVpnBlockedUidRanges = new ArrayList(mVpnBlockedUidRanges);
+ for (int i = 0; i < ranges.length; i++) {
+ if (requireVpn) {
+ newVpnBlockedUidRanges.add(ranges[i]);
+ } else {
+ newVpnBlockedUidRanges.remove(ranges[i]);
+ }
+ }
+
+ try {
+ mNetd.networkRejectNonSecureVpn(requireVpn, toUidRangeStableParcels(ranges));
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "setRequireVpnForUids(" + requireVpn + ", "
+ + Arrays.toString(ranges) + "): netd command failed: " + e);
+ }
+
+ for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+ final boolean curMetered = nai.networkCapabilities.isMetered();
+ maybeNotifyNetworkBlocked(nai, curMetered, curMetered,
+ mVpnBlockedUidRanges, newVpnBlockedUidRanges);
+ }
+
+ mVpnBlockedUidRanges = newVpnBlockedUidRanges;
+ }
+
+ @Override
+ public void setLegacyLockdownVpnEnabled(boolean enabled) {
+ enforceNetworkStackOrSettingsPermission();
+ mHandler.post(() -> mLockdownEnabled = enabled);
+ }
+
+ private boolean isLegacyLockdownNai(NetworkAgentInfo nai) {
+ return mLockdownEnabled
+ && getVpnType(nai) == VpnManager.TYPE_VPN_LEGACY
+ && nai.networkCapabilities.appliesToUid(Process.FIRST_APPLICATION_UID);
+ }
+
+ private NetworkAgentInfo getLegacyLockdownNai() {
+ if (!mLockdownEnabled) {
+ return null;
+ }
+ // The legacy lockdown VPN always only applies to userId 0.
+ final NetworkAgentInfo nai = getVpnForUid(Process.FIRST_APPLICATION_UID);
+ if (nai == null || !isLegacyLockdownNai(nai)) return null;
+
+ // The legacy lockdown VPN must always have exactly one underlying network.
+ // This code may run on any thread and declaredUnderlyingNetworks may change, so store it in
+ // a local variable. There is no need to make a copy because its contents cannot change.
+ final Network[] underlying = nai.declaredUnderlyingNetworks;
+ if (underlying == null || underlying.length != 1) {
+ return null;
+ }
+
+ // The legacy lockdown VPN always uses the default network.
+ // If the VPN's underlying network is no longer the current default network, it means that
+ // the default network has just switched, and the VPN is about to disconnect.
+ // Report that the VPN is not connected, so the state of NetworkInfo objects overwritten
+ // by filterForLegacyLockdown will be set to CONNECTING and not CONNECTED.
+ final NetworkAgentInfo defaultNetwork = getDefaultNetwork();
+ if (defaultNetwork == null || !defaultNetwork.network.equals(underlying[0])) {
+ return null;
+ }
+
+ return nai;
+ };
+
+ // TODO: move all callers to filterForLegacyLockdown and delete this method.
+ // This likely requires making sendLegacyNetworkBroadcast take a NetworkInfo object instead of
+ // just a DetailedState object.
+ private DetailedState getLegacyLockdownState(DetailedState origState) {
+ if (origState != DetailedState.CONNECTED) {
+ return origState;
+ }
+ return (mLockdownEnabled && getLegacyLockdownNai() == null)
+ ? DetailedState.CONNECTING
+ : DetailedState.CONNECTED;
+ }
+
+ private void filterForLegacyLockdown(NetworkInfo ni) {
+ if (!mLockdownEnabled || !ni.isConnected()) return;
+ // The legacy lockdown VPN replaces the state of every network in CONNECTED state with the
+ // state of its VPN. This is to ensure that when an underlying network connects, apps will
+ // not see a CONNECTIVITY_ACTION broadcast for a network in state CONNECTED until the VPN
+ // comes up, at which point there is a new CONNECTIVITY_ACTION broadcast for the underlying
+ // network, this time with a state of CONNECTED.
+ //
+ // Now that the legacy lockdown code lives in ConnectivityService, and no longer has access
+ // to the internal state of the Vpn object, always replace the state with CONNECTING. This
+ // is not too far off the truth, since an always-on VPN, when not connected, is always
+ // trying to reconnect.
+ if (getLegacyLockdownNai() == null) {
+ ni.setDetailedState(DetailedState.CONNECTING, "", null);
+ }
+ }
+
+ @Override
+ public void setProvisioningNotificationVisible(boolean visible, int networkType,
+ String action) {
+ enforceSettingsPermission();
+ if (!ConnectivityManager.isNetworkTypeValid(networkType)) {
+ return;
+ }
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ // Concatenate the range of types onto the range of NetIDs.
+ int id = NetIdManager.MAX_NET_ID + 1 + (networkType - ConnectivityManager.TYPE_NONE);
+ mNotifier.setProvNotificationVisible(visible, id, action);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public void setAirplaneMode(boolean enable) {
+ enforceAirplaneModePermission();
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ final ContentResolver cr = mContext.getContentResolver();
+ Settings.Global.putInt(cr, Settings.Global.AIRPLANE_MODE_ON, encodeBool(enable));
+ Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ intent.putExtra("state", enable);
+ mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private void onUserAdded(@NonNull final UserHandle user) {
+ mPermissionMonitor.onUserAdded(user);
+ if (mOemNetworkPreferences.getNetworkPreferences().size() > 0) {
+ handleSetOemNetworkPreference(mOemNetworkPreferences, null);
+ }
+ }
+
+ private void onUserRemoved(@NonNull final UserHandle user) {
+ mPermissionMonitor.onUserRemoved(user);
+ // If there was a network preference for this user, remove it.
+ handleSetProfileNetworkPreference(new ProfileNetworkPreferences.Preference(user, null),
+ null /* listener */);
+ if (mOemNetworkPreferences.getNetworkPreferences().size() > 0) {
+ handleSetOemNetworkPreference(mOemNetworkPreferences, null);
+ }
+ }
+
+ private void onPackageChanged(@NonNull final String packageName) {
+ // This is necessary in case a package is added or removed, but also when it's replaced to
+ // run as a new UID by its manifest rules. Also, if a separate package shares the same UID
+ // as one in the preferences, then it should follow the same routing as that other package,
+ // which means updating the rules is never to be needed in this case (whether it joins or
+ // leaves a UID with a preference).
+ if (isMappedInOemNetworkPreference(packageName)) {
+ handleSetOemNetworkPreference(mOemNetworkPreferences, null);
+ }
+ }
+
+ private final BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ ensureRunningOnConnectivityServiceThread();
+ final String action = intent.getAction();
+ final UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER);
+
+ // User should be filled for below intents, check the existence.
+ if (user == null) {
+ Log.wtf(TAG, intent.getAction() + " broadcast without EXTRA_USER");
+ return;
+ }
+
+ if (Intent.ACTION_USER_ADDED.equals(action)) {
+ onUserAdded(user);
+ } else if (Intent.ACTION_USER_REMOVED.equals(action)) {
+ onUserRemoved(user);
+ } else {
+ Log.wtf(TAG, "received unexpected intent: " + action);
+ }
+ }
+ };
+
+ private final BroadcastReceiver mPackageIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ ensureRunningOnConnectivityServiceThread();
+ switch (intent.getAction()) {
+ case Intent.ACTION_PACKAGE_ADDED:
+ case Intent.ACTION_PACKAGE_REMOVED:
+ case Intent.ACTION_PACKAGE_REPLACED:
+ onPackageChanged(intent.getData().getSchemeSpecificPart());
+ break;
+ default:
+ Log.wtf(TAG, "received unexpected intent: " + intent.getAction());
+ }
+ }
+ };
+
+ private final HashMap<Messenger, NetworkProviderInfo> mNetworkProviderInfos = new HashMap<>();
+ private final HashMap<NetworkRequest, NetworkRequestInfo> mNetworkRequests = new HashMap<>();
+
+ private static class NetworkProviderInfo {
+ public final String name;
+ public final Messenger messenger;
+ private final IBinder.DeathRecipient mDeathRecipient;
+ public final int providerId;
+
+ NetworkProviderInfo(String name, Messenger messenger, int providerId,
+ @NonNull IBinder.DeathRecipient deathRecipient) {
+ this.name = name;
+ this.messenger = messenger;
+ this.providerId = providerId;
+ mDeathRecipient = deathRecipient;
+
+ if (mDeathRecipient == null) {
+ throw new AssertionError("Must pass a deathRecipient");
+ }
+ }
+
+ void sendMessageToNetworkProvider(int what, int arg1, int arg2, Object obj) {
+ try {
+ messenger.send(Message.obtain(null /* handler */, what, arg1, arg2, obj));
+ } catch (RemoteException e) {
+ // Remote process died. Ignore; the death recipient will remove this
+ // NetworkProviderInfo from mNetworkProviderInfos.
+ }
+ }
+
+ void requestNetwork(NetworkRequest request, int score, int servingProviderId) {
+ sendMessageToNetworkProvider(NetworkProvider.CMD_REQUEST_NETWORK, score,
+ servingProviderId, request);
+ }
+
+ void cancelRequest(NetworkRequest request) {
+ sendMessageToNetworkProvider(NetworkProvider.CMD_CANCEL_REQUEST, 0, 0, request);
+ }
+
+ void connect(Context context, Handler handler) {
+ try {
+ messenger.getBinder().linkToDeath(mDeathRecipient, 0);
+ } catch (RemoteException e) {
+ mDeathRecipient.binderDied();
+ }
+ }
+ }
+
+ private void ensureAllNetworkRequestsHaveType(List<NetworkRequest> requests) {
+ for (int i = 0; i < requests.size(); i++) {
+ ensureNetworkRequestHasType(requests.get(i));
+ }
+ }
+
+ private void ensureNetworkRequestHasType(NetworkRequest request) {
+ if (request.type == NetworkRequest.Type.NONE) {
+ throw new IllegalArgumentException(
+ "All NetworkRequests in ConnectivityService must have a type");
+ }
+ }
+
+ /**
+ * Tracks info about the requester.
+ * Also used to notice when the calling process dies so as to self-expire
+ */
+ @VisibleForTesting
+ protected class NetworkRequestInfo implements IBinder.DeathRecipient {
+ // The requests to be satisfied in priority order. Non-multilayer requests will only have a
+ // single NetworkRequest in mRequests.
+ final List<NetworkRequest> mRequests;
+
+ // mSatisfier and mActiveRequest rely on one another therefore set them together.
+ void setSatisfier(
+ @Nullable final NetworkAgentInfo satisfier,
+ @Nullable final NetworkRequest activeRequest) {
+ mSatisfier = satisfier;
+ mActiveRequest = activeRequest;
+ }
+
+ // The network currently satisfying this NRI. Only one request in an NRI can have a
+ // satisfier. For non-multilayer requests, only non-listen requests can have a satisfier.
+ @Nullable
+ private NetworkAgentInfo mSatisfier;
+ NetworkAgentInfo getSatisfier() {
+ return mSatisfier;
+ }
+
+ // The request in mRequests assigned to a network agent. This is null if none of the
+ // requests in mRequests can be satisfied. This member has the constraint of only being
+ // accessible on the handler thread.
+ @Nullable
+ private NetworkRequest mActiveRequest;
+ NetworkRequest getActiveRequest() {
+ return mActiveRequest;
+ }
+
+ final PendingIntent mPendingIntent;
+ boolean mPendingIntentSent;
+ @Nullable
+ final Messenger mMessenger;
+
+ // Information about the caller that caused this object to be created.
+ @Nullable
+ private final IBinder mBinder;
+ final int mPid;
+ final int mUid;
+ final @NetworkCallback.Flag int mCallbackFlags;
+ @Nullable
+ final String mCallingAttributionTag;
+
+ // Counter keeping track of this NRI.
+ final PerUidCounter mPerUidCounter;
+
+ // Effective UID of this request. This is different from mUid when a privileged process
+ // files a request on behalf of another UID. This UID is used to determine blocked status,
+ // UID matching, and so on. mUid above is used for permission checks and to enforce the
+ // maximum limit of registered callbacks per UID.
+ final int mAsUid;
+
+ // In order to preserve the mapping of NetworkRequest-to-callback when apps register
+ // callbacks using a returned NetworkRequest, the original NetworkRequest needs to be
+ // maintained for keying off of. This is only a concern when the original nri
+ // mNetworkRequests changes which happens currently for apps that register callbacks to
+ // track the default network. In those cases, the nri is updated to have mNetworkRequests
+ // that match the per-app default nri that currently tracks the calling app's uid so that
+ // callbacks are fired at the appropriate time. When the callbacks fire,
+ // mNetworkRequestForCallback will be used so as to preserve the caller's mapping. When
+ // callbacks are updated to key off of an nri vs NetworkRequest, this stops being an issue.
+ // TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects.
+ @NonNull
+ private final NetworkRequest mNetworkRequestForCallback;
+ NetworkRequest getNetworkRequestForCallback() {
+ return mNetworkRequestForCallback;
+ }
+
+ /**
+ * Get the list of UIDs this nri applies to.
+ */
+ @NonNull
+ private Set<UidRange> getUids() {
+ // networkCapabilities.getUids() returns a defensive copy.
+ // multilayer requests will all have the same uids so return the first one.
+ final Set<UidRange> uids = mRequests.get(0).networkCapabilities.getUidRanges();
+ return (null == uids) ? new ArraySet<>() : uids;
+ }
+
+ NetworkRequestInfo(int asUid, @NonNull final NetworkRequest r,
+ @Nullable final PendingIntent pi, @Nullable String callingAttributionTag) {
+ this(asUid, Collections.singletonList(r), r, pi, callingAttributionTag);
+ }
+
+ NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r,
+ @NonNull final NetworkRequest requestForCallback, @Nullable final PendingIntent pi,
+ @Nullable String callingAttributionTag) {
+ ensureAllNetworkRequestsHaveType(r);
+ mRequests = initializeRequests(r);
+ mNetworkRequestForCallback = requestForCallback;
+ mPendingIntent = pi;
+ mMessenger = null;
+ mBinder = null;
+ mPid = getCallingPid();
+ mUid = mDeps.getCallingUid();
+ mAsUid = asUid;
+ mPerUidCounter = getRequestCounter(this);
+ mPerUidCounter.incrementCountOrThrow(mUid);
+ /**
+ * Location sensitive data not included in pending intent. Only included in
+ * {@link NetworkCallback}.
+ */
+ mCallbackFlags = NetworkCallback.FLAG_NONE;
+ mCallingAttributionTag = callingAttributionTag;
+ }
+
+ NetworkRequestInfo(int asUid, @NonNull final NetworkRequest r, @Nullable final Messenger m,
+ @Nullable final IBinder binder,
+ @NetworkCallback.Flag int callbackFlags,
+ @Nullable String callingAttributionTag) {
+ this(asUid, Collections.singletonList(r), r, m, binder, callbackFlags,
+ callingAttributionTag);
+ }
+
+ NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r,
+ @NonNull final NetworkRequest requestForCallback, @Nullable final Messenger m,
+ @Nullable final IBinder binder,
+ @NetworkCallback.Flag int callbackFlags,
+ @Nullable String callingAttributionTag) {
+ super();
+ ensureAllNetworkRequestsHaveType(r);
+ mRequests = initializeRequests(r);
+ mNetworkRequestForCallback = requestForCallback;
+ mMessenger = m;
+ mBinder = binder;
+ mPid = getCallingPid();
+ mUid = mDeps.getCallingUid();
+ mAsUid = asUid;
+ mPendingIntent = null;
+ mPerUidCounter = getRequestCounter(this);
+ mPerUidCounter.incrementCountOrThrow(mUid);
+ mCallbackFlags = callbackFlags;
+ mCallingAttributionTag = callingAttributionTag;
+ linkDeathRecipient();
+ }
+
+ NetworkRequestInfo(@NonNull final NetworkRequestInfo nri,
+ @NonNull final List<NetworkRequest> r) {
+ super();
+ ensureAllNetworkRequestsHaveType(r);
+ mRequests = initializeRequests(r);
+ mNetworkRequestForCallback = nri.getNetworkRequestForCallback();
+ final NetworkAgentInfo satisfier = nri.getSatisfier();
+ if (null != satisfier) {
+ // If the old NRI was satisfied by an NAI, then it may have had an active request.
+ // The active request is necessary to figure out what callbacks to send, in
+ // particular then a network updates its capabilities.
+ // As this code creates a new NRI with a new set of requests, figure out which of
+ // the list of requests should be the active request. It is always the first
+ // request of the list that can be satisfied by the satisfier since the order of
+ // requests is a priority order.
+ // Note even in the presence of a satisfier there may not be an active request,
+ // when the satisfier is the no-service network.
+ NetworkRequest activeRequest = null;
+ for (final NetworkRequest candidate : r) {
+ if (candidate.canBeSatisfiedBy(satisfier.networkCapabilities)) {
+ activeRequest = candidate;
+ break;
+ }
+ }
+ setSatisfier(satisfier, activeRequest);
+ }
+ mMessenger = nri.mMessenger;
+ mBinder = nri.mBinder;
+ mPid = nri.mPid;
+ mUid = nri.mUid;
+ mAsUid = nri.mAsUid;
+ mPendingIntent = nri.mPendingIntent;
+ mPerUidCounter = getRequestCounter(this);
+ mPerUidCounter.incrementCountOrThrow(mUid);
+ mCallbackFlags = nri.mCallbackFlags;
+ mCallingAttributionTag = nri.mCallingAttributionTag;
+ linkDeathRecipient();
+ }
+
+ NetworkRequestInfo(int asUid, @NonNull final NetworkRequest r) {
+ this(asUid, Collections.singletonList(r));
+ }
+
+ NetworkRequestInfo(int asUid, @NonNull final List<NetworkRequest> r) {
+ this(asUid, r, r.get(0), null /* pi */, null /* callingAttributionTag */);
+ }
+
+ // True if this NRI is being satisfied. It also accounts for if the nri has its satisifer
+ // set to the mNoServiceNetwork in which case mActiveRequest will be null thus returning
+ // false.
+ boolean isBeingSatisfied() {
+ return (null != mSatisfier && null != mActiveRequest);
+ }
+
+ boolean isMultilayerRequest() {
+ return mRequests.size() > 1;
+ }
+
+ private List<NetworkRequest> initializeRequests(List<NetworkRequest> r) {
+ // Creating a defensive copy to prevent the sender from modifying the list being
+ // reflected in the return value of this method.
+ final List<NetworkRequest> tempRequests = new ArrayList<>(r);
+ return Collections.unmodifiableList(tempRequests);
+ }
+
+ void decrementRequestCount() {
+ mPerUidCounter.decrementCount(mUid);
+ }
+
+ void linkDeathRecipient() {
+ if (null != mBinder) {
+ try {
+ mBinder.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ binderDied();
+ }
+ }
+ }
+
+ void unlinkDeathRecipient() {
+ if (null != mBinder) {
+ mBinder.unlinkToDeath(this, 0);
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ log("ConnectivityService NetworkRequestInfo binderDied(" +
+ mRequests + ", " + mBinder + ")");
+ releaseNetworkRequests(mRequests);
+ }
+
+ @Override
+ public String toString() {
+ final String asUidString = (mAsUid == mUid) ? "" : " asUid: " + mAsUid;
+ return "uid/pid:" + mUid + "/" + mPid + asUidString + " activeRequest: "
+ + (mActiveRequest == null ? null : mActiveRequest.requestId)
+ + " callbackRequest: "
+ + mNetworkRequestForCallback.requestId
+ + " " + mRequests
+ + (mPendingIntent == null ? "" : " to trigger " + mPendingIntent)
+ + " callback flags: " + mCallbackFlags;
+ }
+ }
+
+ private void ensureRequestableCapabilities(NetworkCapabilities networkCapabilities) {
+ final String badCapability = networkCapabilities.describeFirstNonRequestableCapability();
+ if (badCapability != null) {
+ throw new IllegalArgumentException("Cannot request network with " + badCapability);
+ }
+ }
+
+ // This checks that the passed capabilities either do not request a
+ // specific SSID/SignalStrength, or the calling app has permission to do so.
+ private void ensureSufficientPermissionsForRequest(NetworkCapabilities nc,
+ int callerPid, int callerUid, String callerPackageName) {
+ if (null != nc.getSsid() && !checkSettingsPermission(callerPid, callerUid)) {
+ throw new SecurityException("Insufficient permissions to request a specific SSID");
+ }
+
+ if (nc.hasSignalStrength()
+ && !checkNetworkSignalStrengthWakeupPermission(callerPid, callerUid)) {
+ throw new SecurityException(
+ "Insufficient permissions to request a specific signal strength");
+ }
+ mAppOpsManager.checkPackage(callerUid, callerPackageName);
+
+ if (!nc.getSubscriptionIds().isEmpty()) {
+ enforceNetworkFactoryPermission();
+ }
+ }
+
+ private int[] getSignalStrengthThresholds(@NonNull final NetworkAgentInfo nai) {
+ final SortedSet<Integer> thresholds = new TreeSet<>();
+ synchronized (nai) {
+ // mNetworkRequests may contain the same value multiple times in case of
+ // multilayer requests. It won't matter in this case because the thresholds
+ // will then be the same and be deduplicated as they enter the `thresholds` set.
+ // TODO : have mNetworkRequests be a Set<NetworkRequestInfo> or the like.
+ for (final NetworkRequestInfo nri : mNetworkRequests.values()) {
+ for (final NetworkRequest req : nri.mRequests) {
+ if (req.networkCapabilities.hasSignalStrength()
+ && nai.satisfiesImmutableCapabilitiesOf(req)) {
+ thresholds.add(req.networkCapabilities.getSignalStrength());
+ }
+ }
+ }
+ }
+ return CollectionUtils.toIntArray(new ArrayList<>(thresholds));
+ }
+
+ private void updateSignalStrengthThresholds(
+ NetworkAgentInfo nai, String reason, NetworkRequest request) {
+ final int[] thresholdsArray = getSignalStrengthThresholds(nai);
+
+ if (VDBG || (DBG && !"CONNECT".equals(reason))) {
+ String detail;
+ if (request != null && request.networkCapabilities.hasSignalStrength()) {
+ detail = reason + " " + request.networkCapabilities.getSignalStrength();
+ } else {
+ detail = reason;
+ }
+ log(String.format("updateSignalStrengthThresholds: %s, sending %s to %s",
+ detail, Arrays.toString(thresholdsArray), nai.toShortString()));
+ }
+
+ nai.onSignalStrengthThresholdsUpdated(thresholdsArray);
+ }
+
+ private void ensureValidNetworkSpecifier(NetworkCapabilities nc) {
+ if (nc == null) {
+ return;
+ }
+ NetworkSpecifier ns = nc.getNetworkSpecifier();
+ if (ns == null) {
+ return;
+ }
+ if (ns instanceof MatchAllNetworkSpecifier) {
+ throw new IllegalArgumentException("A MatchAllNetworkSpecifier is not permitted");
+ }
+ }
+
+ private void ensureValid(NetworkCapabilities nc) {
+ ensureValidNetworkSpecifier(nc);
+ if (nc.isPrivateDnsBroken()) {
+ throw new IllegalArgumentException("Can't request broken private DNS");
+ }
+ }
+
+ private boolean isTargetSdkAtleast(int version, int callingUid,
+ @NonNull String callingPackageName) {
+ final UserHandle user = UserHandle.getUserHandleForUid(callingUid);
+ final PackageManager pm =
+ mContext.createContextAsUser(user, 0 /* flags */).getPackageManager();
+ try {
+ final int callingVersion = pm.getTargetSdkVersion(callingPackageName);
+ if (callingVersion < version) return false;
+ } catch (PackageManager.NameNotFoundException e) { }
+ return true;
+ }
+
+ @Override
+ public NetworkRequest requestNetwork(int asUid, NetworkCapabilities networkCapabilities,
+ int reqTypeInt, Messenger messenger, int timeoutMs, IBinder binder,
+ int legacyType, int callbackFlags, @NonNull String callingPackageName,
+ @Nullable String callingAttributionTag) {
+ if (legacyType != TYPE_NONE && !checkNetworkStackPermission()) {
+ if (isTargetSdkAtleast(Build.VERSION_CODES.M, mDeps.getCallingUid(),
+ callingPackageName)) {
+ throw new SecurityException("Insufficient permissions to specify legacy type");
+ }
+ }
+ final NetworkCapabilities defaultNc = mDefaultRequest.mRequests.get(0).networkCapabilities;
+ final int callingUid = mDeps.getCallingUid();
+ // Privileged callers can track the default network of another UID by passing in a UID.
+ if (asUid != Process.INVALID_UID) {
+ enforceSettingsPermission();
+ } else {
+ asUid = callingUid;
+ }
+ final NetworkRequest.Type reqType;
+ try {
+ reqType = NetworkRequest.Type.values()[reqTypeInt];
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new IllegalArgumentException("Unsupported request type " + reqTypeInt);
+ }
+ switch (reqType) {
+ case TRACK_DEFAULT:
+ // If the request type is TRACK_DEFAULT, the passed {@code networkCapabilities}
+ // is unused and will be replaced by ones appropriate for the UID (usually, the
+ // calling app). This allows callers to keep track of the default network.
+ networkCapabilities = copyDefaultNetworkCapabilitiesForUid(
+ defaultNc, asUid, callingUid, callingPackageName);
+ enforceAccessPermission();
+ break;
+ case TRACK_SYSTEM_DEFAULT:
+ enforceSettingsPermission();
+ networkCapabilities = new NetworkCapabilities(defaultNc);
+ break;
+ case BACKGROUND_REQUEST:
+ enforceNetworkStackOrSettingsPermission();
+ // Fall-through since other checks are the same with normal requests.
+ case REQUEST:
+ networkCapabilities = new NetworkCapabilities(networkCapabilities);
+ enforceNetworkRequestPermissions(networkCapabilities, callingPackageName,
+ callingAttributionTag);
+ // TODO: this is incorrect. We mark the request as metered or not depending on
+ // the state of the app when the request is filed, but we never change the
+ // request if the app changes network state. http://b/29964605
+ enforceMeteredApnPolicy(networkCapabilities);
+ break;
+ case LISTEN_FOR_BEST:
+ enforceAccessPermission();
+ networkCapabilities = new NetworkCapabilities(networkCapabilities);
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported request type " + reqType);
+ }
+ ensureRequestableCapabilities(networkCapabilities);
+ ensureSufficientPermissionsForRequest(networkCapabilities,
+ Binder.getCallingPid(), callingUid, callingPackageName);
+
+ // Enforce FOREGROUND if the caller does not have permission to use background network.
+ if (reqType == LISTEN_FOR_BEST) {
+ restrictBackgroundRequestForCaller(networkCapabilities);
+ }
+
+ // Set the UID range for this request to the single UID of the requester, unless the
+ // requester has the permission to specify other UIDs.
+ // This will overwrite any allowed UIDs in the requested capabilities. Though there
+ // are no visible methods to set the UIDs, an app could use reflection to try and get
+ // networks for other apps so it's essential that the UIDs are overwritten.
+ // Also set the requester UID and package name in the request.
+ restrictRequestUidsForCallerAndSetRequestorInfo(networkCapabilities,
+ callingUid, callingPackageName);
+
+ if (timeoutMs < 0) {
+ throw new IllegalArgumentException("Bad timeout specified");
+ }
+ ensureValid(networkCapabilities);
+
+ final NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, legacyType,
+ nextNetworkRequestId(), reqType);
+ final NetworkRequestInfo nri = getNriToRegister(
+ asUid, networkRequest, messenger, binder, callbackFlags,
+ callingAttributionTag);
+ if (DBG) log("requestNetwork for " + nri);
+
+ // For TRACK_SYSTEM_DEFAULT callbacks, the capabilities have been modified since they were
+ // copied from the default request above. (This is necessary to ensure, for example, that
+ // the callback does not leak sensitive information to unprivileged apps.) Check that the
+ // changes don't alter request matching.
+ if (reqType == NetworkRequest.Type.TRACK_SYSTEM_DEFAULT &&
+ (!networkCapabilities.equalRequestableCapabilities(defaultNc))) {
+ throw new IllegalStateException(
+ "TRACK_SYSTEM_DEFAULT capabilities don't match default request: "
+ + networkCapabilities + " vs. " + defaultNc);
+ }
+
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_REQUEST, nri));
+ if (timeoutMs > 0) {
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_TIMEOUT_NETWORK_REQUEST,
+ nri), timeoutMs);
+ }
+ return networkRequest;
+ }
+
+ /**
+ * Return the nri to be used when registering a network request. Specifically, this is used with
+ * requests registered to track the default request. If there is currently a per-app default
+ * tracking the app requestor, then we need to create a version of this nri that mirrors that of
+ * the tracking per-app default so that callbacks are sent to the app requestor appropriately.
+ * @param asUid the uid on behalf of which to file the request. Different from requestorUid
+ * when a privileged caller is tracking the default network for another uid.
+ * @param nr the network request for the nri.
+ * @param msgr the messenger for the nri.
+ * @param binder the binder for the nri.
+ * @param callingAttributionTag the calling attribution tag for the nri.
+ * @return the nri to register.
+ */
+ private NetworkRequestInfo getNriToRegister(final int asUid, @NonNull final NetworkRequest nr,
+ @Nullable final Messenger msgr, @Nullable final IBinder binder,
+ @NetworkCallback.Flag int callbackFlags,
+ @Nullable String callingAttributionTag) {
+ final List<NetworkRequest> requests;
+ if (NetworkRequest.Type.TRACK_DEFAULT == nr.type) {
+ requests = copyDefaultNetworkRequestsForUid(
+ asUid, nr.getRequestorUid(), nr.getRequestorPackageName());
+ } else {
+ requests = Collections.singletonList(nr);
+ }
+ return new NetworkRequestInfo(
+ asUid, requests, nr, msgr, binder, callbackFlags, callingAttributionTag);
+ }
+
+ private void enforceNetworkRequestPermissions(NetworkCapabilities networkCapabilities,
+ String callingPackageName, String callingAttributionTag) {
+ if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) == false) {
+ enforceConnectivityRestrictedNetworksPermission();
+ } else {
+ enforceChangePermission(callingPackageName, callingAttributionTag);
+ }
+ }
+
+ @Override
+ public boolean requestBandwidthUpdate(Network network) {
+ enforceAccessPermission();
+ NetworkAgentInfo nai = null;
+ if (network == null) {
+ return false;
+ }
+ synchronized (mNetworkForNetId) {
+ nai = mNetworkForNetId.get(network.getNetId());
+ }
+ if (nai != null) {
+ nai.onBandwidthUpdateRequested();
+ synchronized (mBandwidthRequests) {
+ final int uid = mDeps.getCallingUid();
+ Integer uidReqs = mBandwidthRequests.get(uid);
+ if (uidReqs == null) {
+ uidReqs = 0;
+ }
+ mBandwidthRequests.put(uid, ++uidReqs);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isSystem(int uid) {
+ return uid < Process.FIRST_APPLICATION_UID;
+ }
+
+ private void enforceMeteredApnPolicy(NetworkCapabilities networkCapabilities) {
+ final int uid = mDeps.getCallingUid();
+ if (isSystem(uid)) {
+ // Exemption for system uid.
+ return;
+ }
+ if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED)) {
+ // Policy already enforced.
+ return;
+ }
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ if (mPolicyManager.isUidRestrictedOnMeteredNetworks(uid)) {
+ // If UID is restricted, don't allow them to bring up metered APNs.
+ networkCapabilities.addCapability(NET_CAPABILITY_NOT_METERED);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public NetworkRequest pendingRequestForNetwork(NetworkCapabilities networkCapabilities,
+ PendingIntent operation, @NonNull String callingPackageName,
+ @Nullable String callingAttributionTag) {
+ Objects.requireNonNull(operation, "PendingIntent cannot be null.");
+ final int callingUid = mDeps.getCallingUid();
+ networkCapabilities = new NetworkCapabilities(networkCapabilities);
+ enforceNetworkRequestPermissions(networkCapabilities, callingPackageName,
+ callingAttributionTag);
+ enforceMeteredApnPolicy(networkCapabilities);
+ ensureRequestableCapabilities(networkCapabilities);
+ ensureSufficientPermissionsForRequest(networkCapabilities,
+ Binder.getCallingPid(), callingUid, callingPackageName);
+ ensureValidNetworkSpecifier(networkCapabilities);
+ restrictRequestUidsForCallerAndSetRequestorInfo(networkCapabilities,
+ callingUid, callingPackageName);
+
+ NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, TYPE_NONE,
+ nextNetworkRequestId(), NetworkRequest.Type.REQUEST);
+ NetworkRequestInfo nri = new NetworkRequestInfo(callingUid, networkRequest, operation,
+ callingAttributionTag);
+ if (DBG) log("pendingRequest for " + nri);
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_REQUEST_WITH_INTENT,
+ nri));
+ return networkRequest;
+ }
+
+ private void releasePendingNetworkRequestWithDelay(PendingIntent operation) {
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(EVENT_RELEASE_NETWORK_REQUEST_WITH_INTENT,
+ mDeps.getCallingUid(), 0, operation), mReleasePendingIntentDelayMs);
+ }
+
+ @Override
+ public void releasePendingNetworkRequest(PendingIntent operation) {
+ Objects.requireNonNull(operation, "PendingIntent cannot be null.");
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_RELEASE_NETWORK_REQUEST_WITH_INTENT,
+ mDeps.getCallingUid(), 0, operation));
+ }
+
+ // In order to implement the compatibility measure for pre-M apps that call
+ // WifiManager.enableNetwork(..., true) without also binding to that network explicitly,
+ // WifiManager registers a network listen for the purpose of calling setProcessDefaultNetwork.
+ // This ensures it has permission to do so.
+ private boolean hasWifiNetworkListenPermission(NetworkCapabilities nc) {
+ if (nc == null) {
+ return false;
+ }
+ int[] transportTypes = nc.getTransportTypes();
+ if (transportTypes.length != 1 || transportTypes[0] != NetworkCapabilities.TRANSPORT_WIFI) {
+ return false;
+ }
+ try {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_WIFI_STATE,
+ "ConnectivityService");
+ } catch (SecurityException e) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public NetworkRequest listenForNetwork(NetworkCapabilities networkCapabilities,
+ Messenger messenger, IBinder binder,
+ @NetworkCallback.Flag int callbackFlags,
+ @NonNull String callingPackageName, @NonNull String callingAttributionTag) {
+ final int callingUid = mDeps.getCallingUid();
+ if (!hasWifiNetworkListenPermission(networkCapabilities)) {
+ enforceAccessPermission();
+ }
+
+ NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
+ ensureSufficientPermissionsForRequest(networkCapabilities,
+ Binder.getCallingPid(), callingUid, callingPackageName);
+ restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName);
+ // Apps without the CHANGE_NETWORK_STATE permission can't use background networks, so
+ // make all their listens include NET_CAPABILITY_FOREGROUND. That way, they will get
+ // onLost and onAvailable callbacks when networks move in and out of the background.
+ // There is no need to do this for requests because an app without CHANGE_NETWORK_STATE
+ // can't request networks.
+ restrictBackgroundRequestForCaller(nc);
+ ensureValid(nc);
+
+ NetworkRequest networkRequest = new NetworkRequest(nc, TYPE_NONE, nextNetworkRequestId(),
+ NetworkRequest.Type.LISTEN);
+ NetworkRequestInfo nri =
+ new NetworkRequestInfo(callingUid, networkRequest, messenger, binder, callbackFlags,
+ callingAttributionTag);
+ if (VDBG) log("listenForNetwork for " + nri);
+
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_LISTENER, nri));
+ return networkRequest;
+ }
+
+ @Override
+ public void pendingListenForNetwork(NetworkCapabilities networkCapabilities,
+ PendingIntent operation, @NonNull String callingPackageName,
+ @Nullable String callingAttributionTag) {
+ Objects.requireNonNull(operation, "PendingIntent cannot be null.");
+ final int callingUid = mDeps.getCallingUid();
+ if (!hasWifiNetworkListenPermission(networkCapabilities)) {
+ enforceAccessPermission();
+ }
+ ensureValid(networkCapabilities);
+ ensureSufficientPermissionsForRequest(networkCapabilities,
+ Binder.getCallingPid(), callingUid, callingPackageName);
+ final NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
+ restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName);
+
+ NetworkRequest networkRequest = new NetworkRequest(nc, TYPE_NONE, nextNetworkRequestId(),
+ NetworkRequest.Type.LISTEN);
+ NetworkRequestInfo nri = new NetworkRequestInfo(callingUid, networkRequest, operation,
+ callingAttributionTag);
+ if (VDBG) log("pendingListenForNetwork for " + nri);
+
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_LISTENER, nri));
+ }
+
+ /** Returns the next Network provider ID. */
+ public final int nextNetworkProviderId() {
+ return mNextNetworkProviderId.getAndIncrement();
+ }
+
+ private void releaseNetworkRequests(List<NetworkRequest> networkRequests) {
+ for (int i = 0; i < networkRequests.size(); i++) {
+ releaseNetworkRequest(networkRequests.get(i));
+ }
+ }
+
+ @Override
+ public void releaseNetworkRequest(NetworkRequest networkRequest) {
+ ensureNetworkRequestHasType(networkRequest);
+ mHandler.sendMessage(mHandler.obtainMessage(
+ EVENT_RELEASE_NETWORK_REQUEST, mDeps.getCallingUid(), 0, networkRequest));
+ }
+
+ private void handleRegisterNetworkProvider(NetworkProviderInfo npi) {
+ if (mNetworkProviderInfos.containsKey(npi.messenger)) {
+ // Avoid creating duplicates. even if an app makes a direct AIDL call.
+ // This will never happen if an app calls ConnectivityManager#registerNetworkProvider,
+ // as that will throw if a duplicate provider is registered.
+ loge("Attempt to register existing NetworkProviderInfo "
+ + mNetworkProviderInfos.get(npi.messenger).name);
+ return;
+ }
+
+ if (DBG) log("Got NetworkProvider Messenger for " + npi.name);
+ mNetworkProviderInfos.put(npi.messenger, npi);
+ npi.connect(mContext, mTrackerHandler);
+ sendAllRequestsToProvider(npi);
+ }
+
+ @Override
+ public int registerNetworkProvider(Messenger messenger, String name) {
+ enforceNetworkFactoryOrSettingsPermission();
+ Objects.requireNonNull(messenger, "messenger must be non-null");
+ NetworkProviderInfo npi = new NetworkProviderInfo(name, messenger,
+ nextNetworkProviderId(), () -> unregisterNetworkProvider(messenger));
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_PROVIDER, npi));
+ return npi.providerId;
+ }
+
+ @Override
+ public void unregisterNetworkProvider(Messenger messenger) {
+ enforceNetworkFactoryOrSettingsPermission();
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_UNREGISTER_NETWORK_PROVIDER, messenger));
+ }
+
+ @Override
+ public void offerNetwork(@NonNull final Messenger providerMessenger,
+ @NonNull final NetworkScore score, @NonNull final NetworkCapabilities caps,
+ @NonNull final INetworkOfferCallback callback) {
+ final NetworkOffer offer = new NetworkOffer(
+ FullScore.makeProspectiveScore(score, caps), caps, callback, providerMessenger);
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_OFFER, offer));
+ }
+
+ @Override
+ public void unofferNetwork(@NonNull final INetworkOfferCallback callback) {
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_UNREGISTER_NETWORK_OFFER, callback));
+ }
+
+ private void handleUnregisterNetworkProvider(Messenger messenger) {
+ NetworkProviderInfo npi = mNetworkProviderInfos.remove(messenger);
+ if (npi == null) {
+ loge("Failed to find Messenger in unregisterNetworkProvider");
+ return;
+ }
+ // Unregister all the offers from this provider
+ final ArrayList<NetworkOfferInfo> toRemove = new ArrayList<>();
+ for (final NetworkOfferInfo noi : mNetworkOffers) {
+ if (noi.offer.provider == messenger) {
+ // Can't call handleUnregisterNetworkOffer here because iteration is in progress
+ toRemove.add(noi);
+ }
+ }
+ for (NetworkOfferInfo noi : toRemove) {
+ handleUnregisterNetworkOffer(noi);
+ }
+ if (DBG) log("unregisterNetworkProvider for " + npi.name);
+ }
+
+ @Override
+ public void declareNetworkRequestUnfulfillable(@NonNull final NetworkRequest request) {
+ if (request.hasTransport(TRANSPORT_TEST)) {
+ enforceNetworkFactoryOrTestNetworksPermission();
+ } else {
+ enforceNetworkFactoryPermission();
+ }
+ final NetworkRequestInfo nri = mNetworkRequests.get(request);
+ if (nri != null) {
+ // declareNetworkRequestUnfulfillable() paths don't apply to multilayer requests.
+ ensureNotMultilayerRequest(nri, "declareNetworkRequestUnfulfillable");
+ mHandler.post(() -> handleReleaseNetworkRequest(
+ nri.mRequests.get(0), mDeps.getCallingUid(), true));
+ }
+ }
+
+ // NOTE: Accessed on multiple threads, must be synchronized on itself.
+ @GuardedBy("mNetworkForNetId")
+ private final SparseArray<NetworkAgentInfo> mNetworkForNetId = new SparseArray<>();
+ // NOTE: Accessed on multiple threads, synchronized with mNetworkForNetId.
+ // An entry is first reserved with NetIdManager, prior to being added to mNetworkForNetId, so
+ // there may not be a strict 1:1 correlation between the two.
+ private final NetIdManager mNetIdManager;
+
+ // NetworkAgentInfo keyed off its connecting messenger
+ // TODO - eval if we can reduce the number of lists/hashmaps/sparsearrays
+ // NOTE: Only should be accessed on ConnectivityServiceThread, except dump().
+ private final ArraySet<NetworkAgentInfo> mNetworkAgentInfos = new ArraySet<>();
+
+ // UID ranges for users that are currently blocked by VPNs.
+ // This array is accessed and iterated on multiple threads without holding locks, so its
+ // contents must never be mutated. When the ranges change, the array is replaced with a new one
+ // (on the handler thread).
+ private volatile List<UidRange> mVpnBlockedUidRanges = new ArrayList<>();
+
+ // Must only be accessed on the handler thread
+ @NonNull
+ private final ArrayList<NetworkOfferInfo> mNetworkOffers = new ArrayList<>();
+
+ @GuardedBy("mBlockedAppUids")
+ private final HashSet<Integer> mBlockedAppUids = new HashSet<>();
+
+ // Current OEM network preferences. This object must only be written to on the handler thread.
+ // Since it is immutable and always non-null, other threads may read it if they only care
+ // about seeing a consistent object but not that it is current.
+ @NonNull
+ private OemNetworkPreferences mOemNetworkPreferences =
+ new OemNetworkPreferences.Builder().build();
+ // Current per-profile network preferences. This object follows the same threading rules as
+ // the OEM network preferences above.
+ @NonNull
+ private ProfileNetworkPreferences mProfileNetworkPreferences = new ProfileNetworkPreferences();
+
+ // OemNetworkPreferences activity String log entries.
+ private static final int MAX_OEM_NETWORK_PREFERENCE_LOGS = 20;
+ @NonNull
+ private final LocalLog mOemNetworkPreferencesLogs =
+ new LocalLog(MAX_OEM_NETWORK_PREFERENCE_LOGS);
+
+ /**
+ * Determine whether a given package has a mapping in the current OemNetworkPreferences.
+ * @param packageName the package name to check existence of a mapping for.
+ * @return true if a mapping exists, false otherwise
+ */
+ private boolean isMappedInOemNetworkPreference(@NonNull final String packageName) {
+ return mOemNetworkPreferences.getNetworkPreferences().containsKey(packageName);
+ }
+
+ // The always-on request for an Internet-capable network that apps without a specific default
+ // fall back to.
+ @VisibleForTesting
+ @NonNull
+ final NetworkRequestInfo mDefaultRequest;
+ // Collection of NetworkRequestInfo's used for default networks.
+ @VisibleForTesting
+ @NonNull
+ final ArraySet<NetworkRequestInfo> mDefaultNetworkRequests = new ArraySet<>();
+
+ private boolean isPerAppDefaultRequest(@NonNull final NetworkRequestInfo nri) {
+ return (mDefaultNetworkRequests.contains(nri) && mDefaultRequest != nri);
+ }
+
+ /**
+ * Return the default network request currently tracking the given uid.
+ * @param uid the uid to check.
+ * @return the NetworkRequestInfo tracking the given uid.
+ */
+ @NonNull
+ private NetworkRequestInfo getDefaultRequestTrackingUid(final int uid) {
+ for (final NetworkRequestInfo nri : mDefaultNetworkRequests) {
+ if (nri == mDefaultRequest) {
+ continue;
+ }
+ // Checking the first request is sufficient as only multilayer requests will have more
+ // than one request and for multilayer, all requests will track the same uids.
+ if (nri.mRequests.get(0).networkCapabilities.appliesToUid(uid)) {
+ return nri;
+ }
+ }
+ return mDefaultRequest;
+ }
+
+ /**
+ * Get a copy of the network requests of the default request that is currently tracking the
+ * given uid.
+ * @param asUid the uid on behalf of which to file the request. Different from requestorUid
+ * when a privileged caller is tracking the default network for another uid.
+ * @param requestorUid the uid to check the default for.
+ * @param requestorPackageName the requestor's package name.
+ * @return a copy of the default's NetworkRequest that is tracking the given uid.
+ */
+ @NonNull
+ private List<NetworkRequest> copyDefaultNetworkRequestsForUid(
+ final int asUid, final int requestorUid, @NonNull final String requestorPackageName) {
+ return copyNetworkRequestsForUid(
+ getDefaultRequestTrackingUid(asUid).mRequests,
+ asUid, requestorUid, requestorPackageName);
+ }
+
+ /**
+ * Copy the given nri's NetworkRequest collection.
+ * @param requestsToCopy the NetworkRequest collection to be copied.
+ * @param asUid the uid on behalf of which to file the request. Different from requestorUid
+ * when a privileged caller is tracking the default network for another uid.
+ * @param requestorUid the uid to set on the copied collection.
+ * @param requestorPackageName the package name to set on the copied collection.
+ * @return the copied NetworkRequest collection.
+ */
+ @NonNull
+ private List<NetworkRequest> copyNetworkRequestsForUid(
+ @NonNull final List<NetworkRequest> requestsToCopy, final int asUid,
+ final int requestorUid, @NonNull final String requestorPackageName) {
+ final List<NetworkRequest> requests = new ArrayList<>();
+ for (final NetworkRequest nr : requestsToCopy) {
+ requests.add(new NetworkRequest(copyDefaultNetworkCapabilitiesForUid(
+ nr.networkCapabilities, asUid, requestorUid, requestorPackageName),
+ nr.legacyType, nextNetworkRequestId(), nr.type));
+ }
+ return requests;
+ }
+
+ @NonNull
+ private NetworkCapabilities copyDefaultNetworkCapabilitiesForUid(
+ @NonNull final NetworkCapabilities netCapToCopy, final int asUid,
+ final int requestorUid, @NonNull final String requestorPackageName) {
+ // These capabilities are for a TRACK_DEFAULT callback, so:
+ // 1. Remove NET_CAPABILITY_VPN, because it's (currently!) the only difference between
+ // mDefaultRequest and a per-UID default request.
+ // TODO: stop depending on the fact that these two unrelated things happen to be the same
+ // 2. Always set the UIDs to asUid. restrictRequestUidsForCallerAndSetRequestorInfo will
+ // not do this in the case of a privileged application.
+ final NetworkCapabilities netCap = new NetworkCapabilities(netCapToCopy);
+ netCap.removeCapability(NET_CAPABILITY_NOT_VPN);
+ netCap.setSingleUid(asUid);
+ restrictRequestUidsForCallerAndSetRequestorInfo(
+ netCap, requestorUid, requestorPackageName);
+ return netCap;
+ }
+
+ /**
+ * Get the nri that is currently being tracked for callbacks by per-app defaults.
+ * @param nr the network request to check for equality against.
+ * @return the nri if one exists, null otherwise.
+ */
+ @Nullable
+ private NetworkRequestInfo getNriForAppRequest(@NonNull final NetworkRequest nr) {
+ for (final NetworkRequestInfo nri : mNetworkRequests.values()) {
+ if (nri.getNetworkRequestForCallback().equals(nr)) {
+ return nri;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Check if an nri is currently being managed by per-app default networking.
+ * @param nri the nri to check.
+ * @return true if this nri is currently being managed by per-app default networking.
+ */
+ private boolean isPerAppTrackedNri(@NonNull final NetworkRequestInfo nri) {
+ // nri.mRequests.get(0) is only different from the original request filed in
+ // nri.getNetworkRequestForCallback() if nri.mRequests was changed by per-app default
+ // functionality therefore if these two don't match, it means this particular nri is
+ // currently being managed by a per-app default.
+ return nri.getNetworkRequestForCallback() != nri.mRequests.get(0);
+ }
+
+ /**
+ * Determine if an nri is a managed default request that disallows default networking.
+ * @param nri the request to evaluate
+ * @return true if device-default networking is disallowed
+ */
+ private boolean isDefaultBlocked(@NonNull final NetworkRequestInfo nri) {
+ // Check if this nri is a managed default that supports the default network at its
+ // lowest priority request.
+ final NetworkRequest defaultNetworkRequest = mDefaultRequest.mRequests.get(0);
+ final NetworkCapabilities lowestPriorityNetCap =
+ nri.mRequests.get(nri.mRequests.size() - 1).networkCapabilities;
+ return isPerAppDefaultRequest(nri)
+ && !(defaultNetworkRequest.networkCapabilities.equalRequestableCapabilities(
+ lowestPriorityNetCap));
+ }
+
+ // Request used to optionally keep mobile data active even when higher
+ // priority networks like Wi-Fi are active.
+ private final NetworkRequest mDefaultMobileDataRequest;
+
+ // Request used to optionally keep wifi data active even when higher
+ // priority networks like ethernet are active.
+ private final NetworkRequest mDefaultWifiRequest;
+
+ // Request used to optionally keep vehicle internal network always active
+ private final NetworkRequest mDefaultVehicleRequest;
+
+ // Sentinel NAI used to direct apps with default networks that should have no connectivity to a
+ // network with no service. This NAI should never be matched against, nor should any public API
+ // ever return the associated network. For this reason, this NAI is not in the list of available
+ // NAIs. It is used in computeNetworkReassignment() to be set as the satisfier for non-device
+ // default requests that don't support using the device default network which will ultimately
+ // allow ConnectivityService to use this no-service network when calling makeDefaultForApps().
+ @VisibleForTesting
+ final NetworkAgentInfo mNoServiceNetwork;
+
+ // The NetworkAgentInfo currently satisfying the default request, if any.
+ private NetworkAgentInfo getDefaultNetwork() {
+ return mDefaultRequest.mSatisfier;
+ }
+
+ private NetworkAgentInfo getDefaultNetworkForUid(final int uid) {
+ for (final NetworkRequestInfo nri : mDefaultNetworkRequests) {
+ // Currently, all network requests will have the same uids therefore checking the first
+ // one is sufficient. If/when uids are tracked at the nri level, this can change.
+ final Set<UidRange> uids = nri.mRequests.get(0).networkCapabilities.getUidRanges();
+ if (null == uids) {
+ continue;
+ }
+ for (final UidRange range : uids) {
+ if (range.contains(uid)) {
+ return nri.getSatisfier();
+ }
+ }
+ }
+ return getDefaultNetwork();
+ }
+
+ @Nullable
+ private Network getNetwork(@Nullable NetworkAgentInfo nai) {
+ return nai != null ? nai.network : null;
+ }
+
+ private void ensureRunningOnConnectivityServiceThread() {
+ if (mHandler.getLooper().getThread() != Thread.currentThread()) {
+ throw new IllegalStateException(
+ "Not running on ConnectivityService thread: "
+ + Thread.currentThread().getName());
+ }
+ }
+
+ @VisibleForTesting
+ protected boolean isDefaultNetwork(NetworkAgentInfo nai) {
+ return nai == getDefaultNetwork();
+ }
+
+ /**
+ * Register a new agent with ConnectivityService to handle a network.
+ *
+ * @param na a reference for ConnectivityService to contact the agent asynchronously.
+ * @param networkInfo the initial info associated with this network. It can be updated later :
+ * see {@link #updateNetworkInfo}.
+ * @param linkProperties the initial link properties of this network. They can be updated
+ * later : see {@link #updateLinkProperties}.
+ * @param networkCapabilities the initial capabilites of this network. They can be updated
+ * later : see {@link #updateCapabilities}.
+ * @param initialScore the initial score of the network. See
+ * {@link NetworkAgentInfo#getCurrentScore}.
+ * @param networkAgentConfig metadata about the network. This is never updated.
+ * @param providerId the ID of the provider owning this NetworkAgent.
+ * @return the network created for this agent.
+ */
+ public Network registerNetworkAgent(INetworkAgent na, NetworkInfo networkInfo,
+ LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
+ @NonNull NetworkScore initialScore, NetworkAgentConfig networkAgentConfig,
+ int providerId) {
+ Objects.requireNonNull(networkInfo, "networkInfo must not be null");
+ Objects.requireNonNull(linkProperties, "linkProperties must not be null");
+ Objects.requireNonNull(networkCapabilities, "networkCapabilities must not be null");
+ Objects.requireNonNull(initialScore, "initialScore must not be null");
+ Objects.requireNonNull(networkAgentConfig, "networkAgentConfig must not be null");
+ if (networkCapabilities.hasTransport(TRANSPORT_TEST)) {
+ enforceAnyPermissionOf(Manifest.permission.MANAGE_TEST_NETWORKS);
+ } else {
+ enforceNetworkFactoryPermission();
+ }
+
+ final int uid = mDeps.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return registerNetworkAgentInternal(na, networkInfo, linkProperties,
+ networkCapabilities, initialScore, networkAgentConfig, providerId, uid);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ private Network registerNetworkAgentInternal(INetworkAgent na, NetworkInfo networkInfo,
+ LinkProperties linkProperties, NetworkCapabilities networkCapabilities,
+ NetworkScore currentScore, NetworkAgentConfig networkAgentConfig, int providerId,
+ int uid) {
+ if (networkCapabilities.hasTransport(TRANSPORT_TEST)) {
+ // Strictly, sanitizing here is unnecessary as the capabilities will be sanitized in
+ // the call to mixInCapabilities below anyway, but sanitizing here means the NAI never
+ // sees capabilities that may be malicious, which might prevent mistakes in the future.
+ networkCapabilities = new NetworkCapabilities(networkCapabilities);
+ networkCapabilities.restrictCapabilitesForTestNetwork(uid);
+ }
+
+ LinkProperties lp = new LinkProperties(linkProperties);
+
+ final NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
+ final NetworkAgentInfo nai = new NetworkAgentInfo(na,
+ new Network(mNetIdManager.reserveNetId()), new NetworkInfo(networkInfo), lp, nc,
+ currentScore, mContext, mTrackerHandler, new NetworkAgentConfig(networkAgentConfig),
+ this, mNetd, mDnsResolver, providerId, uid, mQosCallbackTracker, mDeps);
+
+ // Make sure the LinkProperties and NetworkCapabilities reflect what the agent info says.
+ processCapabilitiesFromAgent(nai, nc);
+ nai.getAndSetNetworkCapabilities(mixInCapabilities(nai, nc));
+ processLinkPropertiesFromAgent(nai, nai.linkProperties);
+
+ final String extraInfo = networkInfo.getExtraInfo();
+ final String name = TextUtils.isEmpty(extraInfo)
+ ? nai.networkCapabilities.getSsid() : extraInfo;
+ if (DBG) log("registerNetworkAgent " + nai);
+ mDeps.getNetworkStack().makeNetworkMonitor(
+ nai.network, name, new NetworkMonitorCallbacks(nai));
+ // NetworkAgentInfo registration will finish when the NetworkMonitor is created.
+ // If the network disconnects or sends any other event before that, messages are deferred by
+ // NetworkAgent until nai.connect(), which will be called when finalizing the
+ // registration.
+ return nai.network;
+ }
+
+ private void handleRegisterNetworkAgent(NetworkAgentInfo nai, INetworkMonitor networkMonitor) {
+ nai.onNetworkMonitorCreated(networkMonitor);
+ if (VDBG) log("Got NetworkAgent Messenger");
+ mNetworkAgentInfos.add(nai);
+ synchronized (mNetworkForNetId) {
+ mNetworkForNetId.put(nai.network.getNetId(), nai);
+ }
+
+ try {
+ networkMonitor.start();
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ nai.notifyRegistered();
+ NetworkInfo networkInfo = nai.networkInfo;
+ updateNetworkInfo(nai, networkInfo);
+ updateUids(nai, null, nai.networkCapabilities);
+ }
+
+ private class NetworkOfferInfo implements IBinder.DeathRecipient {
+ @NonNull public final NetworkOffer offer;
+
+ NetworkOfferInfo(@NonNull final NetworkOffer offer) {
+ this.offer = offer;
+ }
+
+ @Override
+ public void binderDied() {
+ mHandler.post(() -> handleUnregisterNetworkOffer(this));
+ }
+ }
+
+ /**
+ * Register or update a network offer.
+ * @param newOffer The new offer. If the callback member is the same as an existing
+ * offer, it is an update of that offer.
+ */
+ private void handleRegisterNetworkOffer(@NonNull final NetworkOffer newOffer) {
+ ensureRunningOnConnectivityServiceThread();
+ if (null == mNetworkProviderInfos.get(newOffer.provider)) {
+ // This may actually happen if a provider updates its score or registers and then
+ // immediately unregisters. The offer would still be in the handler queue, but the
+ // provider would have been removed.
+ if (DBG) log("Received offer from an unregistered provider");
+ return;
+ }
+
+ final NetworkOfferInfo existingOffer = findNetworkOfferInfoByCallback(newOffer.callback);
+ if (null != existingOffer) {
+ handleUnregisterNetworkOffer(existingOffer);
+ newOffer.migrateFrom(existingOffer.offer);
+ }
+ final NetworkOfferInfo noi = new NetworkOfferInfo(newOffer);
+ try {
+ noi.offer.provider.getBinder().linkToDeath(noi, 0 /* flags */);
+ } catch (RemoteException e) {
+ noi.binderDied();
+ return;
+ }
+ mNetworkOffers.add(noi);
+ // TODO : send requests to the provider.
+ }
+
+ private void handleUnregisterNetworkOffer(@NonNull final NetworkOfferInfo noi) {
+ ensureRunningOnConnectivityServiceThread();
+ mNetworkOffers.remove(noi);
+ noi.offer.provider.getBinder().unlinkToDeath(noi, 0 /* flags */);
+ }
+
+ @Nullable private NetworkOfferInfo findNetworkOfferInfoByCallback(
+ @NonNull final INetworkOfferCallback callback) {
+ ensureRunningOnConnectivityServiceThread();
+ for (final NetworkOfferInfo noi : mNetworkOffers) {
+ if (noi.offer.callback.equals(callback)) return noi;
+ }
+ return null;
+ }
+
+ /**
+ * Called when receiving LinkProperties directly from a NetworkAgent.
+ * Stores into |nai| any data coming from the agent that might also be written to the network's
+ * LinkProperties by ConnectivityService itself. This ensures that the data provided by the
+ * agent is not lost when updateLinkProperties is called.
+ * This method should never alter the agent's LinkProperties, only store data in |nai|.
+ */
+ private void processLinkPropertiesFromAgent(NetworkAgentInfo nai, LinkProperties lp) {
+ lp.ensureDirectlyConnectedRoutes();
+ nai.clatd.setNat64PrefixFromRa(lp.getNat64Prefix());
+ nai.networkAgentPortalData = lp.getCaptivePortalData();
+ }
+
+ private void updateLinkProperties(NetworkAgentInfo networkAgent, @NonNull LinkProperties newLp,
+ @NonNull LinkProperties oldLp) {
+ int netId = networkAgent.network.getNetId();
+
+ // The NetworkAgent does not know whether clatd is running on its network or not, or whether
+ // a NAT64 prefix was discovered by the DNS resolver. Before we do anything else, make sure
+ // the LinkProperties for the network are accurate.
+ networkAgent.clatd.fixupLinkProperties(oldLp, newLp);
+
+ updateInterfaces(newLp, oldLp, netId, networkAgent.networkCapabilities);
+
+ // update filtering rules, need to happen after the interface update so netd knows about the
+ // new interface (the interface name -> index map becomes initialized)
+ updateVpnFiltering(newLp, oldLp, networkAgent);
+
+ updateMtu(newLp, oldLp);
+ // TODO - figure out what to do for clat
+// for (LinkProperties lp : newLp.getStackedLinks()) {
+// updateMtu(lp, null);
+// }
+ if (isDefaultNetwork(networkAgent)) {
+ updateTcpBufferSizes(newLp.getTcpBufferSizes());
+ }
+
+ updateRoutes(newLp, oldLp, netId);
+ updateDnses(newLp, oldLp, netId);
+ // Make sure LinkProperties represents the latest private DNS status.
+ // This does not need to be done before updateDnses because the
+ // LinkProperties are not the source of the private DNS configuration.
+ // updateDnses will fetch the private DNS configuration from DnsManager.
+ mDnsManager.updatePrivateDnsStatus(netId, newLp);
+
+ if (isDefaultNetwork(networkAgent)) {
+ handleApplyDefaultProxy(newLp.getHttpProxy());
+ } else {
+ updateProxy(newLp, oldLp);
+ }
+
+ updateWakeOnLan(newLp);
+
+ // Captive portal data is obtained from NetworkMonitor and stored in NetworkAgentInfo.
+ // It is not always contained in the LinkProperties sent from NetworkAgents, and if it
+ // does, it needs to be merged here.
+ newLp.setCaptivePortalData(mergeCaptivePortalData(networkAgent.networkAgentPortalData,
+ networkAgent.capportApiData));
+
+ // TODO - move this check to cover the whole function
+ if (!Objects.equals(newLp, oldLp)) {
+ synchronized (networkAgent) {
+ networkAgent.linkProperties = newLp;
+ }
+ // Start or stop DNS64 detection and 464xlat according to network state.
+ networkAgent.clatd.update();
+ notifyIfacesChangedForNetworkStats();
+ networkAgent.networkMonitor().notifyLinkPropertiesChanged(
+ new LinkProperties(newLp, true /* parcelSensitiveFields */));
+ if (networkAgent.everConnected) {
+ notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_IP_CHANGED);
+ }
+ }
+
+ mKeepaliveTracker.handleCheckKeepalivesStillValid(networkAgent);
+ }
+
+ /**
+ * @param naData captive portal data from NetworkAgent
+ * @param apiData captive portal data from capport API
+ */
+ @Nullable
+ private CaptivePortalData mergeCaptivePortalData(CaptivePortalData naData,
+ CaptivePortalData apiData) {
+ if (naData == null || apiData == null) {
+ return naData == null ? apiData : naData;
+ }
+ final CaptivePortalData.Builder captivePortalBuilder =
+ new CaptivePortalData.Builder(naData);
+
+ if (apiData.isCaptive()) {
+ captivePortalBuilder.setCaptive(true);
+ }
+ if (apiData.isSessionExtendable()) {
+ captivePortalBuilder.setSessionExtendable(true);
+ }
+ if (apiData.getExpiryTimeMillis() >= 0 || apiData.getByteLimit() >= 0) {
+ // Expiry time, bytes remaining, refresh time all need to come from the same source,
+ // otherwise data would be inconsistent. Prefer the capport API info if present,
+ // as it can generally be refreshed more often.
+ captivePortalBuilder.setExpiryTime(apiData.getExpiryTimeMillis());
+ captivePortalBuilder.setBytesRemaining(apiData.getByteLimit());
+ captivePortalBuilder.setRefreshTime(apiData.getRefreshTimeMillis());
+ } else if (naData.getExpiryTimeMillis() < 0 && naData.getByteLimit() < 0) {
+ // No source has time / bytes remaining information: surface the newest refresh time
+ // for other fields
+ captivePortalBuilder.setRefreshTime(
+ Math.max(naData.getRefreshTimeMillis(), apiData.getRefreshTimeMillis()));
+ }
+
+ // Prioritize the user portal URL from the network agent if the source is authenticated.
+ if (apiData.getUserPortalUrl() != null && naData.getUserPortalUrlSource()
+ != CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) {
+ captivePortalBuilder.setUserPortalUrl(apiData.getUserPortalUrl(),
+ apiData.getUserPortalUrlSource());
+ }
+ // Prioritize the venue information URL from the network agent if the source is
+ // authenticated.
+ if (apiData.getVenueInfoUrl() != null && naData.getVenueInfoUrlSource()
+ != CaptivePortalData.CAPTIVE_PORTAL_DATA_SOURCE_PASSPOINT) {
+ captivePortalBuilder.setVenueInfoUrl(apiData.getVenueInfoUrl(),
+ apiData.getVenueInfoUrlSource());
+ }
+ return captivePortalBuilder.build();
+ }
+
+ private void wakeupModifyInterface(String iface, NetworkCapabilities caps, boolean add) {
+ // Marks are only available on WiFi interfaces. Checking for
+ // marks on unsupported interfaces is harmless.
+ if (!caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+ return;
+ }
+
+ int mark = mResources.get().getInteger(R.integer.config_networkWakeupPacketMark);
+ int mask = mResources.get().getInteger(R.integer.config_networkWakeupPacketMask);
+
+ // TODO (b/183076074): remove legacy fallback after migrating overlays
+ final int legacyMark = mContext.getResources().getInteger(mContext.getResources()
+ .getIdentifier("config_networkWakeupPacketMark", "integer", "android"));
+ final int legacyMask = mContext.getResources().getInteger(mContext.getResources()
+ .getIdentifier("config_networkWakeupPacketMask", "integer", "android"));
+ mark = mark == 0 ? legacyMark : mark;
+ mask = mask == 0 ? legacyMask : mask;
+
+ // Mask/mark of zero will not detect anything interesting.
+ // Don't install rules unless both values are nonzero.
+ if (mark == 0 || mask == 0) {
+ return;
+ }
+
+ final String prefix = "iface:" + iface;
+ try {
+ if (add) {
+ mNetd.wakeupAddInterface(iface, prefix, mark, mask);
+ } else {
+ mNetd.wakeupDelInterface(iface, prefix, mark, mask);
+ }
+ } catch (Exception e) {
+ loge("Exception modifying wakeup packet monitoring: " + e);
+ }
+
+ }
+
+ private void updateInterfaces(final @Nullable LinkProperties newLp,
+ final @Nullable LinkProperties oldLp, final int netId,
+ final @NonNull NetworkCapabilities caps) {
+ final CompareResult<String> interfaceDiff = new CompareResult<>(
+ oldLp != null ? oldLp.getAllInterfaceNames() : null,
+ newLp != null ? newLp.getAllInterfaceNames() : null);
+ if (!interfaceDiff.added.isEmpty()) {
+ for (final String iface : interfaceDiff.added) {
+ try {
+ if (DBG) log("Adding iface " + iface + " to network " + netId);
+ mNetd.networkAddInterface(netId, iface);
+ wakeupModifyInterface(iface, caps, true);
+ mDeps.reportNetworkInterfaceForTransports(mContext, iface,
+ caps.getTransportTypes());
+ } catch (Exception e) {
+ logw("Exception adding interface: " + e);
+ }
+ }
+ }
+ for (final String iface : interfaceDiff.removed) {
+ try {
+ if (DBG) log("Removing iface " + iface + " from network " + netId);
+ wakeupModifyInterface(iface, caps, false);
+ mNetd.networkRemoveInterface(netId, iface);
+ } catch (Exception e) {
+ loge("Exception removing interface: " + e);
+ }
+ }
+ }
+
+ // TODO: move to frameworks/libs/net.
+ private RouteInfoParcel convertRouteInfo(RouteInfo route) {
+ final String nextHop;
+
+ switch (route.getType()) {
+ case RouteInfo.RTN_UNICAST:
+ if (route.hasGateway()) {
+ nextHop = route.getGateway().getHostAddress();
+ } else {
+ nextHop = INetd.NEXTHOP_NONE;
+ }
+ break;
+ case RouteInfo.RTN_UNREACHABLE:
+ nextHop = INetd.NEXTHOP_UNREACHABLE;
+ break;
+ case RouteInfo.RTN_THROW:
+ nextHop = INetd.NEXTHOP_THROW;
+ break;
+ default:
+ nextHop = INetd.NEXTHOP_NONE;
+ break;
+ }
+
+ final RouteInfoParcel rip = new RouteInfoParcel();
+ rip.ifName = route.getInterface();
+ rip.destination = route.getDestination().toString();
+ rip.nextHop = nextHop;
+ rip.mtu = route.getMtu();
+
+ return rip;
+ }
+
+ /**
+ * Have netd update routes from oldLp to newLp.
+ * @return true if routes changed between oldLp and newLp
+ */
+ private boolean updateRoutes(LinkProperties newLp, LinkProperties oldLp, int netId) {
+ // compare the route diff to determine which routes have been updated
+ final CompareOrUpdateResult<RouteInfo.RouteKey, RouteInfo> routeDiff =
+ new CompareOrUpdateResult<>(
+ oldLp != null ? oldLp.getAllRoutes() : null,
+ newLp != null ? newLp.getAllRoutes() : null,
+ (r) -> r.getRouteKey());
+
+ // add routes before removing old in case it helps with continuous connectivity
+
+ // do this twice, adding non-next-hop routes first, then routes they are dependent on
+ for (RouteInfo route : routeDiff.added) {
+ if (route.hasGateway()) continue;
+ if (VDBG || DDBG) log("Adding Route [" + route + "] to network " + netId);
+ try {
+ mNetd.networkAddRouteParcel(netId, convertRouteInfo(route));
+ } catch (Exception e) {
+ if ((route.getDestination().getAddress() instanceof Inet4Address) || VDBG) {
+ loge("Exception in networkAddRouteParcel for non-gateway: " + e);
+ }
+ }
+ }
+ for (RouteInfo route : routeDiff.added) {
+ if (!route.hasGateway()) continue;
+ if (VDBG || DDBG) log("Adding Route [" + route + "] to network " + netId);
+ try {
+ mNetd.networkAddRouteParcel(netId, convertRouteInfo(route));
+ } catch (Exception e) {
+ if ((route.getGateway() instanceof Inet4Address) || VDBG) {
+ loge("Exception in networkAddRouteParcel for gateway: " + e);
+ }
+ }
+ }
+
+ for (RouteInfo route : routeDiff.removed) {
+ if (VDBG || DDBG) log("Removing Route [" + route + "] from network " + netId);
+ try {
+ mNetd.networkRemoveRouteParcel(netId, convertRouteInfo(route));
+ } catch (Exception e) {
+ loge("Exception in networkRemoveRouteParcel: " + e);
+ }
+ }
+
+ for (RouteInfo route : routeDiff.updated) {
+ if (VDBG || DDBG) log("Updating Route [" + route + "] from network " + netId);
+ try {
+ mNetd.networkUpdateRouteParcel(netId, convertRouteInfo(route));
+ } catch (Exception e) {
+ loge("Exception in networkUpdateRouteParcel: " + e);
+ }
+ }
+ return !routeDiff.added.isEmpty() || !routeDiff.removed.isEmpty()
+ || !routeDiff.updated.isEmpty();
+ }
+
+ private void updateDnses(LinkProperties newLp, LinkProperties oldLp, int netId) {
+ if (oldLp != null && newLp.isIdenticalDnses(oldLp)) {
+ return; // no updating necessary
+ }
+
+ if (DBG) {
+ final Collection<InetAddress> dnses = newLp.getDnsServers();
+ log("Setting DNS servers for network " + netId + " to " + dnses);
+ }
+ try {
+ mDnsManager.noteDnsServersForNetwork(netId, newLp);
+ mDnsManager.flushVmDnsCache();
+ } catch (Exception e) {
+ loge("Exception in setDnsConfigurationForNetwork: " + e);
+ }
+ }
+
+ private void updateVpnFiltering(LinkProperties newLp, LinkProperties oldLp,
+ NetworkAgentInfo nai) {
+ final String oldIface = oldLp != null ? oldLp.getInterfaceName() : null;
+ final String newIface = newLp != null ? newLp.getInterfaceName() : null;
+ final boolean wasFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, oldLp);
+ final boolean needsFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, newLp);
+
+ if (!wasFiltering && !needsFiltering) {
+ // Nothing to do.
+ return;
+ }
+
+ if (Objects.equals(oldIface, newIface) && (wasFiltering == needsFiltering)) {
+ // Nothing changed.
+ return;
+ }
+
+ final Set<UidRange> ranges = nai.networkCapabilities.getUidRanges();
+ final int vpnAppUid = nai.networkCapabilities.getOwnerUid();
+ // TODO: this create a window of opportunity for apps to receive traffic between the time
+ // when the old rules are removed and the time when new rules are added. To fix this,
+ // make eBPF support two allowlisted interfaces so here new rules can be added before the
+ // old rules are being removed.
+ if (wasFiltering) {
+ mPermissionMonitor.onVpnUidRangesRemoved(oldIface, ranges, vpnAppUid);
+ }
+ if (needsFiltering) {
+ mPermissionMonitor.onVpnUidRangesAdded(newIface, ranges, vpnAppUid);
+ }
+ }
+
+ private void updateWakeOnLan(@NonNull LinkProperties lp) {
+ if (mWolSupportedInterfaces == null) {
+ mWolSupportedInterfaces = new ArraySet<>(mResources.get().getStringArray(
+ R.array.config_wakeonlan_supported_interfaces));
+ }
+ lp.setWakeOnLanSupported(mWolSupportedInterfaces.contains(lp.getInterfaceName()));
+ }
+
+ private int getNetworkPermission(NetworkCapabilities nc) {
+ if (!nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
+ return INetd.PERMISSION_SYSTEM;
+ }
+ if (!nc.hasCapability(NET_CAPABILITY_FOREGROUND)) {
+ return INetd.PERMISSION_NETWORK;
+ }
+ return INetd.PERMISSION_NONE;
+ }
+
+ private void updateNetworkPermissions(@NonNull final NetworkAgentInfo nai,
+ @NonNull final NetworkCapabilities newNc) {
+ final int oldPermission = getNetworkPermission(nai.networkCapabilities);
+ final int newPermission = getNetworkPermission(newNc);
+ if (oldPermission != newPermission && nai.created && !nai.isVPN()) {
+ try {
+ mNetd.networkSetPermissionForNetwork(nai.network.getNetId(), newPermission);
+ } catch (RemoteException | ServiceSpecificException e) {
+ loge("Exception in networkSetPermissionForNetwork: " + e);
+ }
+ }
+ }
+
+ /**
+ * Called when receiving NetworkCapabilities directly from a NetworkAgent.
+ * Stores into |nai| any data coming from the agent that might also be written to the network's
+ * NetworkCapabilities by ConnectivityService itself. This ensures that the data provided by the
+ * agent is not lost when updateCapabilities is called.
+ * This method should never alter the agent's NetworkCapabilities, only store data in |nai|.
+ */
+ private void processCapabilitiesFromAgent(NetworkAgentInfo nai, NetworkCapabilities nc) {
+ // Note: resetting the owner UID before storing the agent capabilities in NAI means that if
+ // the agent attempts to change the owner UID, then nai.declaredCapabilities will not
+ // actually be the same as the capabilities sent by the agent. Still, it is safer to reset
+ // the owner UID here and behave as if the agent had never tried to change it.
+ if (nai.networkCapabilities.getOwnerUid() != nc.getOwnerUid()) {
+ Log.e(TAG, nai.toShortString() + ": ignoring attempt to change owner from "
+ + nai.networkCapabilities.getOwnerUid() + " to " + nc.getOwnerUid());
+ nc.setOwnerUid(nai.networkCapabilities.getOwnerUid());
+ }
+ nai.declaredCapabilities = new NetworkCapabilities(nc);
+ }
+
+ /** Modifies |newNc| based on the capabilities of |underlyingNetworks| and |agentCaps|. */
+ @VisibleForTesting
+ void applyUnderlyingCapabilities(@Nullable Network[] underlyingNetworks,
+ @NonNull NetworkCapabilities agentCaps, @NonNull NetworkCapabilities newNc) {
+ underlyingNetworks = underlyingNetworksOrDefault(
+ agentCaps.getOwnerUid(), underlyingNetworks);
+ long transportTypes = NetworkCapabilitiesUtils.packBits(agentCaps.getTransportTypes());
+ int downKbps = NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED;
+ int upKbps = NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED;
+ // metered if any underlying is metered, or originally declared metered by the agent.
+ boolean metered = !agentCaps.hasCapability(NET_CAPABILITY_NOT_METERED);
+ boolean roaming = false; // roaming if any underlying is roaming
+ boolean congested = false; // congested if any underlying is congested
+ boolean suspended = true; // suspended if all underlying are suspended
+
+ boolean hadUnderlyingNetworks = false;
+ if (null != underlyingNetworks) {
+ for (Network underlyingNetwork : underlyingNetworks) {
+ final NetworkAgentInfo underlying =
+ getNetworkAgentInfoForNetwork(underlyingNetwork);
+ if (underlying == null) continue;
+
+ final NetworkCapabilities underlyingCaps = underlying.networkCapabilities;
+ hadUnderlyingNetworks = true;
+ for (int underlyingType : underlyingCaps.getTransportTypes()) {
+ transportTypes |= 1L << underlyingType;
+ }
+
+ // Merge capabilities of this underlying network. For bandwidth, assume the
+ // worst case.
+ downKbps = NetworkCapabilities.minBandwidth(downKbps,
+ underlyingCaps.getLinkDownstreamBandwidthKbps());
+ upKbps = NetworkCapabilities.minBandwidth(upKbps,
+ underlyingCaps.getLinkUpstreamBandwidthKbps());
+ // If this underlying network is metered, the VPN is metered (it may cost money
+ // to send packets on this network).
+ metered |= !underlyingCaps.hasCapability(NET_CAPABILITY_NOT_METERED);
+ // If this underlying network is roaming, the VPN is roaming (the billing structure
+ // is different than the usual, local one).
+ roaming |= !underlyingCaps.hasCapability(NET_CAPABILITY_NOT_ROAMING);
+ // If this underlying network is congested, the VPN is congested (the current
+ // condition of the network affects the performance of this network).
+ congested |= !underlyingCaps.hasCapability(NET_CAPABILITY_NOT_CONGESTED);
+ // If this network is not suspended, the VPN is not suspended (the VPN
+ // is able to transfer some data).
+ suspended &= !underlyingCaps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED);
+ }
+ }
+ if (!hadUnderlyingNetworks) {
+ // No idea what the underlying networks are; assume reasonable defaults
+ metered = true;
+ roaming = false;
+ congested = false;
+ suspended = false;
+ }
+
+ newNc.setTransportTypes(NetworkCapabilitiesUtils.unpackBits(transportTypes));
+ newNc.setLinkDownstreamBandwidthKbps(downKbps);
+ newNc.setLinkUpstreamBandwidthKbps(upKbps);
+ newNc.setCapability(NET_CAPABILITY_NOT_METERED, !metered);
+ newNc.setCapability(NET_CAPABILITY_NOT_ROAMING, !roaming);
+ newNc.setCapability(NET_CAPABILITY_NOT_CONGESTED, !congested);
+ newNc.setCapability(NET_CAPABILITY_NOT_SUSPENDED, !suspended);
+ }
+
+ /**
+ * Augments the NetworkCapabilities passed in by a NetworkAgent with capabilities that are
+ * maintained here that the NetworkAgent is not aware of (e.g., validated, captive portal,
+ * and foreground status).
+ */
+ @NonNull
+ private NetworkCapabilities mixInCapabilities(NetworkAgentInfo nai, NetworkCapabilities nc) {
+ // Once a NetworkAgent is connected, complain if some immutable capabilities are removed.
+ // Don't complain for VPNs since they're not driven by requests and there is no risk of
+ // causing a connect/teardown loop.
+ // TODO: remove this altogether and make it the responsibility of the NetworkProviders to
+ // avoid connect/teardown loops.
+ if (nai.everConnected &&
+ !nai.isVPN() &&
+ !nai.networkCapabilities.satisfiedByImmutableNetworkCapabilities(nc)) {
+ // TODO: consider not complaining when a network agent degrades its capabilities if this
+ // does not cause any request (that is not a listen) currently matching that agent to
+ // stop being matched by the updated agent.
+ String diff = nai.networkCapabilities.describeImmutableDifferences(nc);
+ if (!TextUtils.isEmpty(diff)) {
+ Log.wtf(TAG, "BUG: " + nai + " lost immutable capabilities:" + diff);
+ }
+ }
+
+ // Don't modify caller's NetworkCapabilities.
+ final NetworkCapabilities newNc = new NetworkCapabilities(nc);
+ if (nai.lastValidated) {
+ newNc.addCapability(NET_CAPABILITY_VALIDATED);
+ } else {
+ newNc.removeCapability(NET_CAPABILITY_VALIDATED);
+ }
+ if (nai.lastCaptivePortalDetected) {
+ newNc.addCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
+ } else {
+ newNc.removeCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
+ }
+ if (nai.isBackgroundNetwork()) {
+ newNc.removeCapability(NET_CAPABILITY_FOREGROUND);
+ } else {
+ newNc.addCapability(NET_CAPABILITY_FOREGROUND);
+ }
+ if (nai.partialConnectivity) {
+ newNc.addCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY);
+ } else {
+ newNc.removeCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY);
+ }
+ newNc.setPrivateDnsBroken(nai.networkCapabilities.isPrivateDnsBroken());
+
+ // TODO : remove this once all factories are updated to send NOT_SUSPENDED and NOT_ROAMING
+ if (!newNc.hasTransport(TRANSPORT_CELLULAR)) {
+ newNc.addCapability(NET_CAPABILITY_NOT_SUSPENDED);
+ newNc.addCapability(NET_CAPABILITY_NOT_ROAMING);
+ }
+
+ if (nai.supportsUnderlyingNetworks()) {
+ applyUnderlyingCapabilities(nai.declaredUnderlyingNetworks, nai.declaredCapabilities,
+ newNc);
+ }
+
+ return newNc;
+ }
+
+ private void updateNetworkInfoForRoamingAndSuspended(NetworkAgentInfo nai,
+ NetworkCapabilities prevNc, NetworkCapabilities newNc) {
+ final boolean prevSuspended = !prevNc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED);
+ final boolean suspended = !newNc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED);
+ final boolean prevRoaming = !prevNc.hasCapability(NET_CAPABILITY_NOT_ROAMING);
+ final boolean roaming = !newNc.hasCapability(NET_CAPABILITY_NOT_ROAMING);
+ if (prevSuspended != suspended) {
+ // TODO (b/73132094) : remove this call once the few users of onSuspended and
+ // onResumed have been removed.
+ notifyNetworkCallbacks(nai, suspended ? ConnectivityManager.CALLBACK_SUSPENDED
+ : ConnectivityManager.CALLBACK_RESUMED);
+ }
+ if (prevSuspended != suspended || prevRoaming != roaming) {
+ // updateNetworkInfo will mix in the suspended info from the capabilities and
+ // take appropriate action for the network having possibly changed state.
+ updateNetworkInfo(nai, nai.networkInfo);
+ }
+ }
+
+ /**
+ * Update the NetworkCapabilities for {@code nai} to {@code nc}. Specifically:
+ *
+ * 1. Calls mixInCapabilities to merge the passed-in NetworkCapabilities {@code nc} with the
+ * capabilities we manage and store in {@code nai}, such as validated status and captive
+ * portal status)
+ * 2. Takes action on the result: changes network permissions, sends CAP_CHANGED callbacks, and
+ * potentially triggers rematches.
+ * 3. Directly informs other network stack components (NetworkStatsService, VPNs, etc. of the
+ * change.)
+ *
+ * @param oldScore score of the network before any of the changes that prompted us
+ * to call this function.
+ * @param nai the network having its capabilities updated.
+ * @param nc the new network capabilities.
+ */
+ private void updateCapabilities(final int oldScore, @NonNull final NetworkAgentInfo nai,
+ @NonNull final NetworkCapabilities nc) {
+ NetworkCapabilities newNc = mixInCapabilities(nai, nc);
+ if (Objects.equals(nai.networkCapabilities, newNc)) return;
+ updateNetworkPermissions(nai, newNc);
+ final NetworkCapabilities prevNc = nai.getAndSetNetworkCapabilities(newNc);
+
+ updateUids(nai, prevNc, newNc);
+
+ if (nai.getCurrentScore() == oldScore && newNc.equalRequestableCapabilities(prevNc)) {
+ // If the requestable capabilities haven't changed, and the score hasn't changed, then
+ // the change we're processing can't affect any requests, it can only affect the listens
+ // on this network. We might have been called by rematchNetworkAndRequests when a
+ // network changed foreground state.
+ processListenRequests(nai);
+ } else {
+ // If the requestable capabilities have changed or the score changed, we can't have been
+ // called by rematchNetworkAndRequests, so it's safe to start a rematch.
+ rematchAllNetworksAndRequests();
+ notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
+ }
+ updateNetworkInfoForRoamingAndSuspended(nai, prevNc, newNc);
+
+ final boolean oldMetered = prevNc.isMetered();
+ final boolean newMetered = newNc.isMetered();
+ final boolean meteredChanged = oldMetered != newMetered;
+
+ if (meteredChanged) {
+ maybeNotifyNetworkBlocked(nai, oldMetered, newMetered,
+ mVpnBlockedUidRanges, mVpnBlockedUidRanges);
+ }
+
+ final boolean roamingChanged = prevNc.hasCapability(NET_CAPABILITY_NOT_ROAMING)
+ != newNc.hasCapability(NET_CAPABILITY_NOT_ROAMING);
+
+ // Report changes that are interesting for network statistics tracking.
+ if (meteredChanged || roamingChanged) {
+ notifyIfacesChangedForNetworkStats();
+ }
+
+ // This network might have been underlying another network. Propagate its capabilities.
+ propagateUnderlyingNetworkCapabilities(nai.network);
+
+ if (!newNc.equalsTransportTypes(prevNc)) {
+ mDnsManager.updateTransportsForNetwork(
+ nai.network.getNetId(), newNc.getTransportTypes());
+ }
+ }
+
+ /** Convenience method to update the capabilities for a given network. */
+ private void updateCapabilitiesForNetwork(NetworkAgentInfo nai) {
+ updateCapabilities(nai.getCurrentScore(), nai, nai.networkCapabilities);
+ }
+
+ /**
+ * Returns whether VPN isolation (ingress interface filtering) should be applied on the given
+ * network.
+ *
+ * Ingress interface filtering enforces that all apps under the given network can only receive
+ * packets from the network's interface (and loopback). This is important for VPNs because
+ * apps that cannot bypass a fully-routed VPN shouldn't be able to receive packets from any
+ * non-VPN interfaces.
+ *
+ * As a result, this method should return true iff
+ * 1. the network is an app VPN (not legacy VPN)
+ * 2. the VPN does not allow bypass
+ * 3. the VPN is fully-routed
+ * 4. the VPN interface is non-null
+ *
+ * @see INetd#firewallAddUidInterfaceRules
+ * @see INetd#firewallRemoveUidInterfaceRules
+ */
+ private boolean requiresVpnIsolation(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
+ LinkProperties lp) {
+ if (nc == null || lp == null) return false;
+ return nai.isVPN()
+ && !nai.networkAgentConfig.allowBypass
+ && nc.getOwnerUid() != Process.SYSTEM_UID
+ && lp.getInterfaceName() != null
+ && (lp.hasIpv4DefaultRoute() || lp.hasIpv4UnreachableDefaultRoute())
+ && (lp.hasIpv6DefaultRoute() || lp.hasIpv6UnreachableDefaultRoute());
+ }
+
+ private static UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
+ final UidRangeParcel[] stableRanges = new UidRangeParcel[ranges.size()];
+ int index = 0;
+ for (UidRange range : ranges) {
+ stableRanges[index] = new UidRangeParcel(range.start, range.stop);
+ index++;
+ }
+ return stableRanges;
+ }
+
+ private static UidRangeParcel[] toUidRangeStableParcels(UidRange[] ranges) {
+ final UidRangeParcel[] stableRanges = new UidRangeParcel[ranges.length];
+ for (int i = 0; i < ranges.length; i++) {
+ stableRanges[i] = new UidRangeParcel(ranges[i].start, ranges[i].stop);
+ }
+ return stableRanges;
+ }
+
+ private void maybeCloseSockets(NetworkAgentInfo nai, UidRangeParcel[] ranges,
+ int[] exemptUids) {
+ if (nai.isVPN() && !nai.networkAgentConfig.allowBypass) {
+ try {
+ mNetd.socketDestroy(ranges, exemptUids);
+ } catch (Exception e) {
+ loge("Exception in socket destroy: ", e);
+ }
+ }
+ }
+
+ private void updateUidRanges(boolean add, NetworkAgentInfo nai, Set<UidRange> uidRanges) {
+ int[] exemptUids = new int[2];
+ // TODO: Excluding VPN_UID is necessary in order to not to kill the TCP connection used
+ // by PPTP. Fix this by making Vpn set the owner UID to VPN_UID instead of system when
+ // starting a legacy VPN, and remove VPN_UID here. (b/176542831)
+ exemptUids[0] = VPN_UID;
+ exemptUids[1] = nai.networkCapabilities.getOwnerUid();
+ UidRangeParcel[] ranges = toUidRangeStableParcels(uidRanges);
+
+ maybeCloseSockets(nai, ranges, exemptUids);
+ try {
+ if (add) {
+ mNetd.networkAddUidRanges(nai.network.netId, ranges);
+ } else {
+ mNetd.networkRemoveUidRanges(nai.network.netId, ranges);
+ }
+ } catch (Exception e) {
+ loge("Exception while " + (add ? "adding" : "removing") + " uid ranges " + uidRanges +
+ " on netId " + nai.network.netId + ". " + e);
+ }
+ maybeCloseSockets(nai, ranges, exemptUids);
+ }
+
+ private void updateUids(NetworkAgentInfo nai, NetworkCapabilities prevNc,
+ NetworkCapabilities newNc) {
+ Set<UidRange> prevRanges = null == prevNc ? null : prevNc.getUidRanges();
+ Set<UidRange> newRanges = null == newNc ? null : newNc.getUidRanges();
+ if (null == prevRanges) prevRanges = new ArraySet<>();
+ if (null == newRanges) newRanges = new ArraySet<>();
+ final Set<UidRange> prevRangesCopy = new ArraySet<>(prevRanges);
+
+ prevRanges.removeAll(newRanges);
+ newRanges.removeAll(prevRangesCopy);
+
+ try {
+ // When updating the VPN uid routing rules, add the new range first then remove the old
+ // range. If old range were removed first, there would be a window between the old
+ // range being removed and the new range being added, during which UIDs contained
+ // in both ranges are not subject to any VPN routing rules. Adding new range before
+ // removing old range works because, unlike the filtering rules below, it's possible to
+ // add duplicate UID routing rules.
+ // TODO: calculate the intersection of add & remove. Imagining that we are trying to
+ // remove uid 3 from a set containing 1-5. Intersection of the prev and new sets is:
+ // [1-5] & [1-2],[4-5] == [3]
+ // Then we can do:
+ // maybeCloseSockets([3])
+ // mNetd.networkAddUidRanges([1-2],[4-5])
+ // mNetd.networkRemoveUidRanges([1-5])
+ // maybeCloseSockets([3])
+ // This can prevent the sockets of uid 1-2, 4-5 from being closed. It also reduce the
+ // number of binder calls from 6 to 4.
+ if (!newRanges.isEmpty()) {
+ updateUidRanges(true, nai, newRanges);
+ }
+ if (!prevRanges.isEmpty()) {
+ updateUidRanges(false, nai, prevRanges);
+ }
+ final boolean wasFiltering = requiresVpnIsolation(nai, prevNc, nai.linkProperties);
+ 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 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
+ // 10013 left. A consequence of removing old ranges before adding new ranges is that
+ // there is now a window of opportunity when the UIDs are not subject to any filtering.
+ // Note that this is in contrast with the (more robust) update of VPN routing rules
+ // above, where the addition of new ranges happens before the removal of old ranges.
+ // TODO Fix this window by computing an accurate diff on Set<UidRange>, so the old range
+ // to be removed will never overlap with the new range to be added.
+ if (wasFiltering && !prevRanges.isEmpty()) {
+ mPermissionMonitor.onVpnUidRangesRemoved(iface, prevRanges, prevNc.getOwnerUid());
+ }
+ if (shouldFilter && !newRanges.isEmpty()) {
+ mPermissionMonitor.onVpnUidRangesAdded(iface, newRanges, newNc.getOwnerUid());
+ }
+ } catch (Exception e) {
+ // Never crash!
+ loge("Exception in updateUids: ", e);
+ }
+ }
+
+ public void handleUpdateLinkProperties(NetworkAgentInfo nai, LinkProperties newLp) {
+ ensureRunningOnConnectivityServiceThread();
+
+ if (getNetworkAgentInfoForNetId(nai.network.getNetId()) != nai) {
+ // Ignore updates for disconnected networks
+ return;
+ }
+ if (VDBG || DDBG) {
+ log("Update of LinkProperties for " + nai.toShortString()
+ + "; created=" + nai.created
+ + "; everConnected=" + nai.everConnected);
+ }
+ // TODO: eliminate this defensive copy after confirming that updateLinkProperties does not
+ // modify its oldLp parameter.
+ updateLinkProperties(nai, newLp, new LinkProperties(nai.linkProperties));
+ }
+
+ private void sendUpdatedScoreToFactories(NetworkAgentInfo nai) {
+ for (int i = 0; i < nai.numNetworkRequests(); i++) {
+ NetworkRequest nr = nai.requestAt(i);
+ // Don't send listening or track default request to factories. b/17393458
+ if (!nr.isRequest()) continue;
+ sendUpdatedScoreToFactories(nr, nai);
+ }
+ }
+
+ private void sendUpdatedScoreToFactories(
+ @NonNull final NetworkReassignment.RequestReassignment event) {
+ // If a request of type REQUEST is now being satisfied by a new network.
+ if (null != event.mNewNetworkRequest && event.mNewNetworkRequest.isRequest()) {
+ sendUpdatedScoreToFactories(event.mNewNetworkRequest, event.mNewNetwork);
+ }
+
+ // If a previously satisfied request of type REQUEST is no longer being satisfied.
+ if (null != event.mOldNetworkRequest && event.mOldNetworkRequest.isRequest()
+ && event.mOldNetworkRequest != event.mNewNetworkRequest) {
+ sendUpdatedScoreToFactories(event.mOldNetworkRequest, null);
+ }
+
+ cancelMultilayerLowerPriorityNpiRequests(event.mNetworkRequestInfo);
+ }
+
+ /**
+ * Cancel with all NPIs the given NRI's multilayer requests that are a lower priority than
+ * its currently satisfied active request.
+ * @param nri the NRI to cancel lower priority requests for.
+ */
+ private void cancelMultilayerLowerPriorityNpiRequests(
+ @NonNull final NetworkRequestInfo nri) {
+ if (!nri.isMultilayerRequest() || null == nri.mActiveRequest) {
+ return;
+ }
+
+ final int indexOfNewRequest = nri.mRequests.indexOf(nri.mActiveRequest);
+ for (int i = indexOfNewRequest + 1; i < nri.mRequests.size(); i++) {
+ cancelNpiRequest(nri.mRequests.get(i));
+ }
+ }
+
+ private void sendUpdatedScoreToFactories(@NonNull NetworkRequest networkRequest,
+ @Nullable NetworkAgentInfo nai) {
+ final int score;
+ final int serial;
+ if (nai != null) {
+ score = nai.getCurrentScore();
+ serial = nai.factorySerialNumber;
+ } else {
+ score = 0;
+ serial = 0;
+ }
+ if (VDBG || DDBG){
+ log("sending new Min Network Score(" + score + "): " + networkRequest.toString());
+ }
+ for (NetworkProviderInfo npi : mNetworkProviderInfos.values()) {
+ npi.requestNetwork(networkRequest, score, serial);
+ }
+ }
+
+ /** Sends all current NetworkRequests to the specified factory. */
+ private void sendAllRequestsToProvider(@NonNull final NetworkProviderInfo npi) {
+ ensureRunningOnConnectivityServiceThread();
+ for (final NetworkRequestInfo nri : getNrisFromGlobalRequests()) {
+ for (final NetworkRequest req : nri.mRequests) {
+ if (!req.isRequest() && nri.getActiveRequest() == req) {
+ break;
+ }
+ if (!req.isRequest()) {
+ continue;
+ }
+ // Only set the nai for the request it is satisfying.
+ final NetworkAgentInfo nai =
+ nri.getActiveRequest() == req ? nri.getSatisfier() : null;
+ final int score;
+ final int serial;
+ if (null != nai) {
+ score = nai.getCurrentScore();
+ serial = nai.factorySerialNumber;
+ } else {
+ score = 0;
+ serial = NetworkProvider.ID_NONE;
+ }
+ npi.requestNetwork(req, score, serial);
+ // For multilayer requests, don't send lower priority requests if a higher priority
+ // request is already satisfied.
+ if (null != nai) {
+ break;
+ }
+ }
+ }
+ }
+
+ private void sendPendingIntentForRequest(NetworkRequestInfo nri, NetworkAgentInfo networkAgent,
+ int notificationType) {
+ if (notificationType == ConnectivityManager.CALLBACK_AVAILABLE && !nri.mPendingIntentSent) {
+ Intent intent = new Intent();
+ intent.putExtra(ConnectivityManager.EXTRA_NETWORK, networkAgent.network);
+ // If apps could file multi-layer requests with PendingIntents, they'd need to know
+ // which of the layer is satisfied alongside with some ID for the request. Hence, if
+ // such an API is ever implemented, there is no doubt the right request to send in
+ // EXTRA_NETWORK_REQUEST is mActiveRequest, and whatever ID would be added would need to
+ // be sent as a separate extra.
+ intent.putExtra(ConnectivityManager.EXTRA_NETWORK_REQUEST, nri.getActiveRequest());
+ nri.mPendingIntentSent = true;
+ sendIntent(nri.mPendingIntent, intent);
+ }
+ // else not handled
+ }
+
+ private void sendIntent(PendingIntent pendingIntent, Intent intent) {
+ mPendingIntentWakeLock.acquire();
+ try {
+ if (DBG) log("Sending " + pendingIntent);
+ pendingIntent.send(mContext, 0, intent, this /* onFinished */, null /* Handler */);
+ } catch (PendingIntent.CanceledException e) {
+ if (DBG) log(pendingIntent + " was not sent, it had been canceled.");
+ mPendingIntentWakeLock.release();
+ releasePendingNetworkRequest(pendingIntent);
+ }
+ // ...otherwise, mPendingIntentWakeLock.release() gets called by onSendFinished()
+ }
+
+ @Override
+ public void onSendFinished(PendingIntent pendingIntent, Intent intent, int resultCode,
+ String resultData, Bundle resultExtras) {
+ if (DBG) log("Finished sending " + pendingIntent);
+ mPendingIntentWakeLock.release();
+ // Release with a delay so the receiving client has an opportunity to put in its
+ // own request.
+ releasePendingNetworkRequestWithDelay(pendingIntent);
+ }
+
+ private void callCallbackForRequest(@NonNull final NetworkRequestInfo nri,
+ @NonNull final NetworkAgentInfo networkAgent, final int notificationType,
+ final int arg1) {
+ if (nri.mMessenger == null) {
+ // Default request has no msgr. Also prevents callbacks from being invoked for
+ // NetworkRequestInfos registered with ConnectivityDiagnostics requests. Those callbacks
+ // are Type.LISTEN, but should not have NetworkCallbacks invoked.
+ return;
+ }
+ Bundle bundle = new Bundle();
+ // TODO b/177608132: make sure callbacks are indexed by NRIs and not NetworkRequest objects.
+ // TODO: check if defensive copies of data is needed.
+ final NetworkRequest nrForCallback = nri.getNetworkRequestForCallback();
+ putParcelable(bundle, nrForCallback);
+ Message msg = Message.obtain();
+ if (notificationType != ConnectivityManager.CALLBACK_UNAVAIL) {
+ putParcelable(bundle, networkAgent.network);
+ }
+ final boolean includeLocationSensitiveInfo =
+ (nri.mCallbackFlags & NetworkCallback.FLAG_INCLUDE_LOCATION_INFO) != 0;
+ switch (notificationType) {
+ case ConnectivityManager.CALLBACK_AVAILABLE: {
+ final NetworkCapabilities nc =
+ networkCapabilitiesRestrictedForCallerPermissions(
+ networkAgent.networkCapabilities, nri.mPid, nri.mUid);
+ putParcelable(
+ bundle,
+ createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+ nc, includeLocationSensitiveInfo, nri.mPid, nri.mUid,
+ nrForCallback.getRequestorPackageName(),
+ nri.mCallingAttributionTag));
+ putParcelable(bundle, linkPropertiesRestrictedForCallerPermissions(
+ networkAgent.linkProperties, nri.mPid, nri.mUid));
+ // For this notification, arg1 contains the blocked status.
+ msg.arg1 = arg1;
+ break;
+ }
+ case ConnectivityManager.CALLBACK_LOSING: {
+ msg.arg1 = arg1;
+ break;
+ }
+ case ConnectivityManager.CALLBACK_CAP_CHANGED: {
+ // networkAgent can't be null as it has been accessed a few lines above.
+ final NetworkCapabilities netCap =
+ networkCapabilitiesRestrictedForCallerPermissions(
+ networkAgent.networkCapabilities, nri.mPid, nri.mUid);
+ putParcelable(
+ bundle,
+ createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+ netCap, includeLocationSensitiveInfo, nri.mPid, nri.mUid,
+ nrForCallback.getRequestorPackageName(),
+ nri.mCallingAttributionTag));
+ break;
+ }
+ case ConnectivityManager.CALLBACK_IP_CHANGED: {
+ putParcelable(bundle, linkPropertiesRestrictedForCallerPermissions(
+ networkAgent.linkProperties, nri.mPid, nri.mUid));
+ break;
+ }
+ case ConnectivityManager.CALLBACK_BLK_CHANGED: {
+ maybeLogBlockedStatusChanged(nri, networkAgent.network, arg1);
+ msg.arg1 = arg1;
+ break;
+ }
+ }
+ msg.what = notificationType;
+ msg.setData(bundle);
+ try {
+ if (VDBG) {
+ String notification = ConnectivityManager.getCallbackName(notificationType);
+ log("sending notification " + notification + " for " + nrForCallback);
+ }
+ nri.mMessenger.send(msg);
+ } catch (RemoteException e) {
+ // may occur naturally in the race of binder death.
+ loge("RemoteException caught trying to send a callback msg for " + nrForCallback);
+ }
+ }
+
+ private static <T extends Parcelable> void putParcelable(Bundle bundle, T t) {
+ bundle.putParcelable(t.getClass().getSimpleName(), t);
+ }
+
+ private void teardownUnneededNetwork(NetworkAgentInfo nai) {
+ if (nai.numRequestNetworkRequests() != 0) {
+ for (int i = 0; i < nai.numNetworkRequests(); i++) {
+ NetworkRequest nr = nai.requestAt(i);
+ // Ignore listening and track default requests.
+ if (!nr.isRequest()) continue;
+ loge("Dead network still had at least " + nr);
+ break;
+ }
+ }
+ nai.disconnect();
+ }
+
+ private void handleLingerComplete(NetworkAgentInfo oldNetwork) {
+ if (oldNetwork == null) {
+ loge("Unknown NetworkAgentInfo in handleLingerComplete");
+ return;
+ }
+ if (DBG) log("handleLingerComplete for " + oldNetwork.toShortString());
+
+ // If we get here it means that the last linger timeout for this network expired. So there
+ // must be no other active linger timers, and we must stop lingering.
+ oldNetwork.clearInactivityState();
+
+ if (unneeded(oldNetwork, UnneededFor.TEARDOWN)) {
+ // Tear the network down.
+ teardownUnneededNetwork(oldNetwork);
+ } else {
+ // Put the network in the background if it doesn't satisfy any foreground request.
+ updateCapabilitiesForNetwork(oldNetwork);
+ }
+ }
+
+ private void processDefaultNetworkChanges(@NonNull final NetworkReassignment changes) {
+ boolean isDefaultChanged = false;
+ for (final NetworkRequestInfo defaultRequestInfo : mDefaultNetworkRequests) {
+ final NetworkReassignment.RequestReassignment reassignment =
+ changes.getReassignment(defaultRequestInfo);
+ if (null == reassignment) {
+ continue;
+ }
+ // reassignment only contains those instances where the satisfying network changed.
+ isDefaultChanged = true;
+ // Notify system services of the new default.
+ makeDefault(defaultRequestInfo, reassignment.mOldNetwork, reassignment.mNewNetwork);
+ }
+
+ if (isDefaultChanged) {
+ // Hold a wakelock for a short time to help apps in migrating to a new default.
+ scheduleReleaseNetworkTransitionWakelock();
+ }
+ }
+
+ private void makeDefault(@NonNull final NetworkRequestInfo nri,
+ @Nullable final NetworkAgentInfo oldDefaultNetwork,
+ @Nullable final NetworkAgentInfo newDefaultNetwork) {
+ if (DBG) {
+ log("Switching to new default network for: " + nri + " using " + newDefaultNetwork);
+ }
+
+ // Fix up the NetworkCapabilities of any networks that have this network as underlying.
+ if (newDefaultNetwork != null) {
+ propagateUnderlyingNetworkCapabilities(newDefaultNetwork.network);
+ }
+
+ // Set an app level managed default and return since further processing only applies to the
+ // default network.
+ if (mDefaultRequest != nri) {
+ makeDefaultForApps(nri, oldDefaultNetwork, newDefaultNetwork);
+ return;
+ }
+
+ makeDefaultNetwork(newDefaultNetwork);
+
+ if (oldDefaultNetwork != null) {
+ mLingerMonitor.noteLingerDefaultNetwork(oldDefaultNetwork, newDefaultNetwork);
+ }
+ mNetworkActivityTracker.updateDataActivityTracking(newDefaultNetwork, oldDefaultNetwork);
+ handleApplyDefaultProxy(null != newDefaultNetwork
+ ? newDefaultNetwork.linkProperties.getHttpProxy() : null);
+ updateTcpBufferSizes(null != newDefaultNetwork
+ ? newDefaultNetwork.linkProperties.getTcpBufferSizes() : null);
+ notifyIfacesChangedForNetworkStats();
+ }
+
+ private void makeDefaultForApps(@NonNull final NetworkRequestInfo nri,
+ @Nullable final NetworkAgentInfo oldDefaultNetwork,
+ @Nullable final NetworkAgentInfo newDefaultNetwork) {
+ try {
+ if (VDBG) {
+ log("Setting default network for " + nri
+ + " using UIDs " + nri.getUids()
+ + " with old network " + (oldDefaultNetwork != null
+ ? oldDefaultNetwork.network().getNetId() : "null")
+ + " and new network " + (newDefaultNetwork != null
+ ? newDefaultNetwork.network().getNetId() : "null"));
+ }
+ if (nri.getUids().isEmpty()) {
+ throw new IllegalStateException("makeDefaultForApps called without specifying"
+ + " any applications to set as the default." + nri);
+ }
+ if (null != newDefaultNetwork) {
+ mNetd.networkAddUidRanges(
+ newDefaultNetwork.network.getNetId(),
+ toUidRangeStableParcels(nri.getUids()));
+ }
+ if (null != oldDefaultNetwork) {
+ mNetd.networkRemoveUidRanges(
+ oldDefaultNetwork.network.getNetId(),
+ toUidRangeStableParcels(nri.getUids()));
+ }
+ } catch (RemoteException | ServiceSpecificException e) {
+ loge("Exception setting app default network", e);
+ }
+ }
+
+ private void makeDefaultNetwork(@Nullable final NetworkAgentInfo newDefaultNetwork) {
+ try {
+ if (null != newDefaultNetwork) {
+ mNetd.networkSetDefault(newDefaultNetwork.network.getNetId());
+ } else {
+ mNetd.networkClearDefault();
+ }
+ } catch (RemoteException | ServiceSpecificException e) {
+ loge("Exception setting default network :" + e);
+ }
+ }
+
+ private void processListenRequests(@NonNull final NetworkAgentInfo nai) {
+ // For consistency with previous behaviour, send onLost callbacks before onAvailable.
+ processNewlyLostListenRequests(nai);
+ notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
+ processNewlySatisfiedListenRequests(nai);
+ }
+
+ private void processNewlyLostListenRequests(@NonNull final NetworkAgentInfo nai) {
+ for (final NetworkRequestInfo nri : mNetworkRequests.values()) {
+ if (nri.isMultilayerRequest()) {
+ continue;
+ }
+ final NetworkRequest nr = nri.mRequests.get(0);
+ if (!nr.isListen()) continue;
+ if (nai.isSatisfyingRequest(nr.requestId) && !nai.satisfies(nr)) {
+ nai.removeRequest(nr.requestId);
+ callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_LOST, 0);
+ }
+ }
+ }
+
+ private void processNewlySatisfiedListenRequests(@NonNull final NetworkAgentInfo nai) {
+ for (final NetworkRequestInfo nri : mNetworkRequests.values()) {
+ if (nri.isMultilayerRequest()) {
+ continue;
+ }
+ final NetworkRequest nr = nri.mRequests.get(0);
+ if (!nr.isListen()) continue;
+ if (nai.satisfies(nr) && !nai.isSatisfyingRequest(nr.requestId)) {
+ nai.addRequest(nr);
+ notifyNetworkAvailable(nai, nri);
+ }
+ }
+ }
+
+ // An accumulator class to gather the list of changes that result from a rematch.
+ private static class NetworkReassignment {
+ static class RequestReassignment {
+ @NonNull public final NetworkRequestInfo mNetworkRequestInfo;
+ @Nullable public final NetworkRequest mOldNetworkRequest;
+ @Nullable public final NetworkRequest mNewNetworkRequest;
+ @Nullable public final NetworkAgentInfo mOldNetwork;
+ @Nullable public final NetworkAgentInfo mNewNetwork;
+ RequestReassignment(@NonNull final NetworkRequestInfo networkRequestInfo,
+ @Nullable final NetworkRequest oldNetworkRequest,
+ @Nullable final NetworkRequest newNetworkRequest,
+ @Nullable final NetworkAgentInfo oldNetwork,
+ @Nullable final NetworkAgentInfo newNetwork) {
+ mNetworkRequestInfo = networkRequestInfo;
+ mOldNetworkRequest = oldNetworkRequest;
+ mNewNetworkRequest = newNetworkRequest;
+ mOldNetwork = oldNetwork;
+ mNewNetwork = newNetwork;
+ }
+
+ public String toString() {
+ final NetworkRequest requestToShow = null != mNewNetworkRequest
+ ? mNewNetworkRequest : mNetworkRequestInfo.mRequests.get(0);
+ return requestToShow.requestId + " : "
+ + (null != mOldNetwork ? mOldNetwork.network.getNetId() : "null")
+ + " → " + (null != mNewNetwork ? mNewNetwork.network.getNetId() : "null");
+ }
+ }
+
+ @NonNull private final ArrayList<RequestReassignment> mReassignments = new ArrayList<>();
+
+ @NonNull Iterable<RequestReassignment> getRequestReassignments() {
+ return mReassignments;
+ }
+
+ void addRequestReassignment(@NonNull final RequestReassignment reassignment) {
+ if (Build.isDebuggable()) {
+ // The code is never supposed to add two reassignments of the same request. Make
+ // sure this stays true, but without imposing this expensive check on all
+ // reassignments on all user devices.
+ for (final RequestReassignment existing : mReassignments) {
+ if (existing.mNetworkRequestInfo.equals(reassignment.mNetworkRequestInfo)) {
+ throw new IllegalStateException("Trying to reassign ["
+ + reassignment + "] but already have ["
+ + existing + "]");
+ }
+ }
+ }
+ mReassignments.add(reassignment);
+ }
+
+ // Will return null if this reassignment does not change the network assigned to
+ // the passed request.
+ @Nullable
+ private RequestReassignment getReassignment(@NonNull final NetworkRequestInfo nri) {
+ for (final RequestReassignment event : getRequestReassignments()) {
+ if (nri == event.mNetworkRequestInfo) return event;
+ }
+ return null;
+ }
+
+ public String toString() {
+ final StringJoiner sj = new StringJoiner(", " /* delimiter */,
+ "NetReassign [" /* prefix */, "]" /* suffix */);
+ if (mReassignments.isEmpty()) return sj.add("no changes").toString();
+ for (final RequestReassignment rr : getRequestReassignments()) {
+ sj.add(rr.toString());
+ }
+ return sj.toString();
+ }
+
+ public String debugString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("NetworkReassignment :");
+ if (mReassignments.isEmpty()) return sb.append(" no changes").toString();
+ for (final RequestReassignment rr : getRequestReassignments()) {
+ sb.append("\n ").append(rr);
+ }
+ return sb.append("\n").toString();
+ }
+ }
+
+ private void updateSatisfiersForRematchRequest(@NonNull final NetworkRequestInfo nri,
+ @Nullable final NetworkRequest previousRequest,
+ @Nullable final NetworkRequest newRequest,
+ @Nullable final NetworkAgentInfo previousSatisfier,
+ @Nullable final NetworkAgentInfo newSatisfier,
+ final long now) {
+ if (null != newSatisfier && mNoServiceNetwork != newSatisfier) {
+ if (VDBG) log("rematch for " + newSatisfier.toShortString());
+ if (null != previousRequest && null != previousSatisfier) {
+ if (VDBG || DDBG) {
+ log(" accepting network in place of " + previousSatisfier.toShortString());
+ }
+ previousSatisfier.removeRequest(previousRequest.requestId);
+ previousSatisfier.lingerRequest(previousRequest.requestId, now, mLingerDelayMs);
+ } else {
+ if (VDBG || DDBG) log(" accepting network in place of null");
+ }
+
+ // To prevent constantly CPU wake up for nascent timer, if a network comes up
+ // and immediately satisfies a request then remove the timer. This will happen for
+ // all networks except in the case of an underlying network for a VCN.
+ if (newSatisfier.isNascent()) {
+ newSatisfier.unlingerRequest(NetworkRequest.REQUEST_ID_NONE);
+ }
+
+ // if newSatisfier is not null, then newRequest may not be null.
+ newSatisfier.unlingerRequest(newRequest.requestId);
+ if (!newSatisfier.addRequest(newRequest)) {
+ Log.wtf(TAG, "BUG: " + newSatisfier.toShortString() + " already has "
+ + newRequest);
+ }
+ } else if (null != previousRequest && null != previousSatisfier) {
+ if (DBG) {
+ log("Network " + previousSatisfier.toShortString() + " stopped satisfying"
+ + " request " + previousRequest.requestId);
+ }
+ previousSatisfier.removeRequest(previousRequest.requestId);
+ }
+ nri.setSatisfier(newSatisfier, newRequest);
+ }
+
+ /**
+ * This function is triggered when something can affect what network should satisfy what
+ * request, and it computes the network reassignment from the passed collection of requests to
+ * network match to the one that the system should now have. That data is encoded in an
+ * object that is a list of changes, each of them having an NRI, and old satisfier, and a new
+ * satisfier.
+ *
+ * After the reassignment is computed, it is applied to the state objects.
+ *
+ * @param networkRequests the nri objects to evaluate for possible network reassignment
+ * @return NetworkReassignment listing of proposed network assignment changes
+ */
+ @NonNull
+ private NetworkReassignment computeNetworkReassignment(
+ @NonNull final Collection<NetworkRequestInfo> networkRequests) {
+ final NetworkReassignment changes = new NetworkReassignment();
+
+ // Gather the list of all relevant agents and sort them by score.
+ final ArrayList<NetworkAgentInfo> nais = new ArrayList<>();
+ for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+ if (!nai.everConnected) {
+ continue;
+ }
+ nais.add(nai);
+ }
+
+ for (final NetworkRequestInfo nri : networkRequests) {
+ // Non-multilayer listen requests can be ignored.
+ if (!nri.isMultilayerRequest() && nri.mRequests.get(0).isListen()) {
+ continue;
+ }
+ NetworkAgentInfo bestNetwork = null;
+ NetworkRequest bestRequest = null;
+ for (final NetworkRequest req : nri.mRequests) {
+ bestNetwork = mNetworkRanker.getBestNetwork(req, nais);
+ // Stop evaluating as the highest possible priority request is satisfied.
+ if (null != bestNetwork) {
+ bestRequest = req;
+ break;
+ }
+ }
+ if (null == bestNetwork && isDefaultBlocked(nri)) {
+ // Remove default networking if disallowed for managed default requests.
+ bestNetwork = mNoServiceNetwork;
+ }
+ if (nri.getSatisfier() != bestNetwork) {
+ // bestNetwork may be null if no network can satisfy this request.
+ changes.addRequestReassignment(new NetworkReassignment.RequestReassignment(
+ nri, nri.mActiveRequest, bestRequest, nri.getSatisfier(), bestNetwork));
+ }
+ }
+ return changes;
+ }
+
+ private Set<NetworkRequestInfo> getNrisFromGlobalRequests() {
+ return new HashSet<>(mNetworkRequests.values());
+ }
+
+ /**
+ * Attempt to rematch all Networks with all NetworkRequests. This may result in Networks
+ * being disconnected.
+ */
+ private void rematchAllNetworksAndRequests() {
+ rematchNetworksAndRequests(getNrisFromGlobalRequests());
+ }
+
+ /**
+ * Attempt to rematch all Networks with given NetworkRequests. This may result in Networks
+ * being disconnected.
+ */
+ private void rematchNetworksAndRequests(
+ @NonNull final Set<NetworkRequestInfo> networkRequests) {
+ ensureRunningOnConnectivityServiceThread();
+ // TODO: This may be slow, and should be optimized.
+ final long now = SystemClock.elapsedRealtime();
+ final NetworkReassignment changes = computeNetworkReassignment(networkRequests);
+ if (VDBG || DDBG) {
+ log(changes.debugString());
+ } else if (DBG) {
+ log(changes.toString()); // Shorter form, only one line of log
+ }
+ applyNetworkReassignment(changes, now);
+ }
+
+ private void applyNetworkReassignment(@NonNull final NetworkReassignment changes,
+ final long now) {
+ final Collection<NetworkAgentInfo> nais = mNetworkAgentInfos;
+
+ // Since most of the time there are only 0 or 1 background networks, it would probably
+ // be more efficient to just use an ArrayList here. TODO : measure performance
+ final ArraySet<NetworkAgentInfo> oldBgNetworks = new ArraySet<>();
+ for (final NetworkAgentInfo nai : nais) {
+ if (nai.isBackgroundNetwork()) oldBgNetworks.add(nai);
+ }
+
+ // First, update the lists of satisfied requests in the network agents. This is necessary
+ // because some code later depends on this state to be correct, most prominently computing
+ // the linger status.
+ for (final NetworkReassignment.RequestReassignment event :
+ changes.getRequestReassignments()) {
+ updateSatisfiersForRematchRequest(event.mNetworkRequestInfo,
+ event.mOldNetworkRequest, event.mNewNetworkRequest,
+ event.mOldNetwork, event.mNewNetwork,
+ now);
+ }
+
+ // Process default network changes if applicable.
+ processDefaultNetworkChanges(changes);
+
+ // Notify requested networks are available after the default net is switched, but
+ // before LegacyTypeTracker sends legacy broadcasts
+ for (final NetworkReassignment.RequestReassignment event :
+ changes.getRequestReassignments()) {
+ // Tell NetworkProviders about the new score, so they can stop
+ // trying to connect if they know they cannot match it.
+ // TODO - this could get expensive if there are a lot of outstanding requests for this
+ // network. Think of a way to reduce this. Push netid->request mapping to each factory?
+ sendUpdatedScoreToFactories(event);
+
+ if (null != event.mNewNetwork) {
+ notifyNetworkAvailable(event.mNewNetwork, event.mNetworkRequestInfo);
+ } else {
+ callCallbackForRequest(event.mNetworkRequestInfo, event.mOldNetwork,
+ ConnectivityManager.CALLBACK_LOST, 0);
+ }
+ }
+
+ // Update the inactivity state before processing listen callbacks, because the background
+ // computation depends on whether the network is inactive. Don't send the LOSING callbacks
+ // just yet though, because they have to be sent after the listens are processed to keep
+ // backward compatibility.
+ final ArrayList<NetworkAgentInfo> inactiveNetworks = new ArrayList<>();
+ for (final NetworkAgentInfo nai : nais) {
+ // Rematching may have altered the inactivity state of some networks, so update all
+ // inactivity timers. updateInactivityState reads the state from the network agent
+ // and does nothing if the state has not changed : the source of truth is controlled
+ // with NetworkAgentInfo#lingerRequest and NetworkAgentInfo#unlingerRequest, which
+ // have been called while rematching the individual networks above.
+ if (updateInactivityState(nai, now)) {
+ inactiveNetworks.add(nai);
+ }
+ }
+
+ for (final NetworkAgentInfo nai : nais) {
+ if (!nai.everConnected) continue;
+ final boolean oldBackground = oldBgNetworks.contains(nai);
+ // Process listen requests and update capabilities if the background state has
+ // changed for this network. For consistency with previous behavior, send onLost
+ // callbacks before onAvailable.
+ processNewlyLostListenRequests(nai);
+ if (oldBackground != nai.isBackgroundNetwork()) {
+ applyBackgroundChangeForRematch(nai);
+ }
+ processNewlySatisfiedListenRequests(nai);
+ }
+
+ for (final NetworkAgentInfo nai : inactiveNetworks) {
+ // For nascent networks, if connecting with no foreground request, skip broadcasting
+ // LOSING for backward compatibility. This is typical when mobile data connected while
+ // wifi connected with mobile data always-on enabled.
+ if (nai.isNascent()) continue;
+ notifyNetworkLosing(nai, now);
+ }
+
+ updateLegacyTypeTrackerAndVpnLockdownForRematch(changes, nais);
+
+ // Tear down all unneeded networks.
+ for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+ if (unneeded(nai, UnneededFor.TEARDOWN)) {
+ if (nai.getInactivityExpiry() > 0) {
+ // This network has active linger timers and no requests, but is not
+ // lingering. Linger it.
+ //
+ // One way (the only way?) this can happen if this network is unvalidated
+ // and became unneeded due to another network improving its score to the
+ // point where this network will no longer be able to satisfy any requests
+ // even if it validates.
+ if (updateInactivityState(nai, now)) {
+ notifyNetworkLosing(nai, now);
+ }
+ } else {
+ if (DBG) log("Reaping " + nai.toShortString());
+ teardownUnneededNetwork(nai);
+ }
+ }
+ }
+ }
+
+ /**
+ * Apply a change in background state resulting from rematching networks with requests.
+ *
+ * During rematch, a network may change background states by starting to satisfy or stopping
+ * to satisfy a foreground request. Listens don't count for this. When a network changes
+ * background states, its capabilities need to be updated and callbacks fired for the
+ * capability change.
+ *
+ * @param nai The network that changed background states
+ */
+ private void applyBackgroundChangeForRematch(@NonNull final NetworkAgentInfo nai) {
+ final NetworkCapabilities newNc = mixInCapabilities(nai, nai.networkCapabilities);
+ if (Objects.equals(nai.networkCapabilities, newNc)) return;
+ updateNetworkPermissions(nai, newNc);
+ nai.getAndSetNetworkCapabilities(newNc);
+ notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_CAP_CHANGED);
+ }
+
+ private void updateLegacyTypeTrackerAndVpnLockdownForRematch(
+ @NonNull final NetworkReassignment changes,
+ @NonNull final Collection<NetworkAgentInfo> nais) {
+ final NetworkReassignment.RequestReassignment reassignmentOfDefault =
+ changes.getReassignment(mDefaultRequest);
+ final NetworkAgentInfo oldDefaultNetwork =
+ null != reassignmentOfDefault ? reassignmentOfDefault.mOldNetwork : null;
+ final NetworkAgentInfo newDefaultNetwork =
+ null != reassignmentOfDefault ? reassignmentOfDefault.mNewNetwork : null;
+
+ if (oldDefaultNetwork != newDefaultNetwork) {
+ // Maintain the illusion : since the legacy API only understands one network at a time,
+ // if the default network changed, apps should see a disconnected broadcast for the
+ // old default network before they see a connected broadcast for the new one.
+ if (oldDefaultNetwork != null) {
+ mLegacyTypeTracker.remove(oldDefaultNetwork.networkInfo.getType(),
+ oldDefaultNetwork, true);
+ }
+ if (newDefaultNetwork != null) {
+ // The new default network can be newly null if and only if the old default
+ // network doesn't satisfy the default request any more because it lost a
+ // capability.
+ mDefaultInetConditionPublished = newDefaultNetwork.lastValidated ? 100 : 0;
+ mLegacyTypeTracker.add(
+ newDefaultNetwork.networkInfo.getType(), newDefaultNetwork);
+ }
+ }
+
+ // Now that all the callbacks have been sent, send the legacy network broadcasts
+ // as needed. This is necessary so that legacy requests correctly bind dns
+ // requests to this network. The legacy users are listening for this broadcast
+ // and will generally do a dns request so they can ensureRouteToHost and if
+ // they do that before the callbacks happen they'll use the default network.
+ //
+ // TODO: Is there still a race here? The legacy broadcast will be sent after sending
+ // callbacks, but if apps can receive the broadcast before the callback, they still might
+ // have an inconsistent view of networking.
+ //
+ // This *does* introduce a race where if the user uses the new api
+ // (notification callbacks) and then uses the old api (getNetworkInfo(type))
+ // they may get old info. Reverse this after the old startUsing api is removed.
+ // This is on top of the multiple intent sequencing referenced in the todo above.
+ for (NetworkAgentInfo nai : nais) {
+ if (nai.everConnected) {
+ addNetworkToLegacyTypeTracker(nai);
+ }
+ }
+ }
+
+ private void addNetworkToLegacyTypeTracker(@NonNull final NetworkAgentInfo nai) {
+ for (int i = 0; i < nai.numNetworkRequests(); i++) {
+ NetworkRequest nr = nai.requestAt(i);
+ if (nr.legacyType != TYPE_NONE && nr.isRequest()) {
+ // legacy type tracker filters out repeat adds
+ mLegacyTypeTracker.add(nr.legacyType, nai);
+ }
+ }
+
+ // A VPN generally won't get added to the legacy tracker in the "for (nri)" loop above,
+ // because usually there are no NetworkRequests it satisfies (e.g., mDefaultRequest
+ // wants the NOT_VPN capability, so it will never be satisfied by a VPN). So, add the
+ // newNetwork to the tracker explicitly (it's a no-op if it has already been added).
+ if (nai.isVPN()) {
+ mLegacyTypeTracker.add(TYPE_VPN, nai);
+ }
+ }
+
+ private void updateInetCondition(NetworkAgentInfo nai) {
+ // Don't bother updating until we've graduated to validated at least once.
+ if (!nai.everValidated) return;
+ // For now only update icons for the default connection.
+ // TODO: Update WiFi and cellular icons separately. b/17237507
+ if (!isDefaultNetwork(nai)) return;
+
+ int newInetCondition = nai.lastValidated ? 100 : 0;
+ // Don't repeat publish.
+ if (newInetCondition == mDefaultInetConditionPublished) return;
+
+ mDefaultInetConditionPublished = newInetCondition;
+ sendInetConditionBroadcast(nai.networkInfo);
+ }
+
+ @NonNull
+ private NetworkInfo mixInInfo(@NonNull final NetworkAgentInfo nai, @NonNull NetworkInfo info) {
+ final NetworkInfo newInfo = new NetworkInfo(info);
+ // The suspended and roaming bits are managed in NetworkCapabilities.
+ final boolean suspended =
+ !nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_SUSPENDED);
+ if (suspended && info.getDetailedState() == NetworkInfo.DetailedState.CONNECTED) {
+ // Only override the state with SUSPENDED if the network is currently in CONNECTED
+ // state. This is because the network could have been suspended before connecting,
+ // or it could be disconnecting while being suspended, and in both these cases
+ // the state should not be overridden. Note that the only detailed state that
+ // maps to State.CONNECTED is DetailedState.CONNECTED, so there is also no need to
+ // worry about multiple different substates of CONNECTED.
+ newInfo.setDetailedState(NetworkInfo.DetailedState.SUSPENDED, info.getReason(),
+ info.getExtraInfo());
+ } else if (!suspended && info.getDetailedState() == NetworkInfo.DetailedState.SUSPENDED) {
+ // SUSPENDED state is currently only overridden from CONNECTED state. In the case the
+ // network agent is created, then goes to suspended, then goes out of suspended without
+ // ever setting connected. Check if network agent is ever connected to update the state.
+ newInfo.setDetailedState(nai.everConnected
+ ? NetworkInfo.DetailedState.CONNECTED
+ : NetworkInfo.DetailedState.CONNECTING,
+ info.getReason(),
+ info.getExtraInfo());
+ }
+ newInfo.setRoaming(!nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_ROAMING));
+ return newInfo;
+ }
+
+ private void updateNetworkInfo(NetworkAgentInfo networkAgent, NetworkInfo info) {
+ final NetworkInfo newInfo = mixInInfo(networkAgent, info);
+
+ final NetworkInfo.State state = newInfo.getState();
+ NetworkInfo oldInfo = null;
+ synchronized (networkAgent) {
+ oldInfo = networkAgent.networkInfo;
+ networkAgent.networkInfo = newInfo;
+ }
+
+ if (DBG) {
+ log(networkAgent.toShortString() + " EVENT_NETWORK_INFO_CHANGED, going from "
+ + oldInfo.getState() + " to " + state);
+ }
+
+ if (!networkAgent.created
+ && (state == NetworkInfo.State.CONNECTED
+ || (state == NetworkInfo.State.CONNECTING && networkAgent.isVPN()))) {
+
+ // A network that has just connected has zero requests and is thus a foreground network.
+ networkAgent.networkCapabilities.addCapability(NET_CAPABILITY_FOREGROUND);
+
+ if (!createNativeNetwork(networkAgent)) return;
+ if (networkAgent.supportsUnderlyingNetworks()) {
+ // Initialize the network's capabilities to their starting values according to the
+ // underlying networks. This ensures that the capabilities are correct before
+ // anything happens to the network.
+ updateCapabilitiesForNetwork(networkAgent);
+ }
+ networkAgent.created = true;
+ networkAgent.onNetworkCreated();
+ }
+
+ if (!networkAgent.everConnected && state == NetworkInfo.State.CONNECTED) {
+ networkAgent.everConnected = true;
+
+ // NetworkCapabilities need to be set before sending the private DNS config to
+ // NetworkMonitor, otherwise NetworkMonitor cannot determine if validation is required.
+ networkAgent.getAndSetNetworkCapabilities(networkAgent.networkCapabilities);
+
+ handlePerNetworkPrivateDnsConfig(networkAgent, mDnsManager.getPrivateDnsConfig());
+ updateLinkProperties(networkAgent, new LinkProperties(networkAgent.linkProperties),
+ null);
+
+ // Until parceled LinkProperties are sent directly to NetworkMonitor, the connect
+ // command must be sent after updating LinkProperties to maximize chances of
+ // NetworkMonitor seeing the correct LinkProperties when starting.
+ // TODO: pass LinkProperties to the NetworkMonitor in the notifyNetworkConnected call.
+ if (networkAgent.networkAgentConfig.acceptPartialConnectivity) {
+ networkAgent.networkMonitor().setAcceptPartialConnectivity();
+ }
+ networkAgent.networkMonitor().notifyNetworkConnected(
+ new LinkProperties(networkAgent.linkProperties,
+ true /* parcelSensitiveFields */),
+ networkAgent.networkCapabilities);
+ scheduleUnvalidatedPrompt(networkAgent);
+
+ // Whether a particular NetworkRequest listen should cause signal strength thresholds to
+ // be communicated to a particular NetworkAgent depends only on the network's immutable,
+ // capabilities, so it only needs to be done once on initial connect, not every time the
+ // network's capabilities change. Note that we do this before rematching the network,
+ // so we could decide to tear it down immediately afterwards. That's fine though - on
+ // disconnection NetworkAgents should stop any signal strength monitoring they have been
+ // doing.
+ updateSignalStrengthThresholds(networkAgent, "CONNECT", null);
+
+ // Before first rematching networks, put an inactivity timer without any request, this
+ // allows {@code updateInactivityState} to update the state accordingly and prevent
+ // tearing down for any {@code unneeded} evaluation in this period.
+ // Note that the timer will not be rescheduled since the expiry time is
+ // fixed after connection regardless of the network satisfying other requests or not.
+ // But it will be removed as soon as the network satisfies a request for the first time.
+ networkAgent.lingerRequest(NetworkRequest.REQUEST_ID_NONE,
+ SystemClock.elapsedRealtime(), mNascentDelayMs);
+
+ // Consider network even though it is not yet validated.
+ rematchAllNetworksAndRequests();
+
+ // This has to happen after matching the requests, because callbacks are just requests.
+ notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_PRECHECK);
+ } else if (state == NetworkInfo.State.DISCONNECTED) {
+ networkAgent.disconnect();
+ if (networkAgent.isVPN()) {
+ updateUids(networkAgent, networkAgent.networkCapabilities, null);
+ }
+ disconnectAndDestroyNetwork(networkAgent);
+ if (networkAgent.isVPN()) {
+ // As the active or bound network changes for apps, broadcast the default proxy, as
+ // apps may need to update their proxy data. This is called after disconnecting from
+ // VPN to make sure we do not broadcast the old proxy data.
+ // TODO(b/122649188): send the broadcast only to VPN users.
+ mProxyTracker.sendProxyBroadcast();
+ }
+ } else if (networkAgent.created && (oldInfo.getState() == NetworkInfo.State.SUSPENDED ||
+ state == NetworkInfo.State.SUSPENDED)) {
+ mLegacyTypeTracker.update(networkAgent);
+ }
+ }
+
+ private void updateNetworkScore(@NonNull final NetworkAgentInfo nai, final NetworkScore score) {
+ if (VDBG || DDBG) log("updateNetworkScore for " + nai.toShortString() + " to " + score);
+ nai.setScore(score);
+ rematchAllNetworksAndRequests();
+ sendUpdatedScoreToFactories(nai);
+ }
+
+ // Notify only this one new request of the current state. Transfer all the
+ // current state by calling NetworkCapabilities and LinkProperties callbacks
+ // so that callers can be guaranteed to have as close to atomicity in state
+ // transfer as can be supported by this current API.
+ protected void notifyNetworkAvailable(NetworkAgentInfo nai, NetworkRequestInfo nri) {
+ mHandler.removeMessages(EVENT_TIMEOUT_NETWORK_REQUEST, nri);
+ if (nri.mPendingIntent != null) {
+ sendPendingIntentForRequest(nri, nai, ConnectivityManager.CALLBACK_AVAILABLE);
+ // Attempt no subsequent state pushes where intents are involved.
+ return;
+ }
+
+ final int blockedReasons = mUidBlockedReasons.get(nri.mAsUid, BLOCKED_REASON_NONE);
+ final boolean metered = nai.networkCapabilities.isMetered();
+ final boolean vpnBlocked = isUidBlockedByVpn(nri.mAsUid, mVpnBlockedUidRanges);
+ callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_AVAILABLE,
+ getBlockedState(blockedReasons, metered, vpnBlocked));
+ }
+
+ // Notify the requests on this NAI that the network is now lingered.
+ private void notifyNetworkLosing(@NonNull final NetworkAgentInfo nai, final long now) {
+ final int lingerTime = (int) (nai.getInactivityExpiry() - now);
+ notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOSING, lingerTime);
+ }
+
+ private static int getBlockedState(int reasons, boolean metered, boolean vpnBlocked) {
+ if (!metered) reasons &= ~BLOCKED_METERED_REASON_MASK;
+ return vpnBlocked
+ ? reasons | BLOCKED_REASON_LOCKDOWN_VPN
+ : reasons & ~BLOCKED_REASON_LOCKDOWN_VPN;
+ }
+
+ private void setUidBlockedReasons(int uid, @BlockedReason int blockedReasons) {
+ if (blockedReasons == BLOCKED_REASON_NONE) {
+ mUidBlockedReasons.delete(uid);
+ } else {
+ mUidBlockedReasons.put(uid, blockedReasons);
+ }
+ }
+
+ /**
+ * Notify of the blocked state apps with a registered callback matching a given NAI.
+ *
+ * Unlike other callbacks, blocked status is different between each individual uid. So for
+ * any given nai, all requests need to be considered according to the uid who filed it.
+ *
+ * @param nai The target NetworkAgentInfo.
+ * @param oldMetered True if the previous network capabilities were metered.
+ * @param newMetered True if the current network capabilities are metered.
+ * @param oldBlockedUidRanges list of UID ranges previously blocked by lockdown VPN.
+ * @param newBlockedUidRanges list of UID ranges blocked by lockdown VPN.
+ */
+ private void maybeNotifyNetworkBlocked(NetworkAgentInfo nai, boolean oldMetered,
+ boolean newMetered, List<UidRange> oldBlockedUidRanges,
+ List<UidRange> newBlockedUidRanges) {
+
+ for (int i = 0; i < nai.numNetworkRequests(); i++) {
+ NetworkRequest nr = nai.requestAt(i);
+ NetworkRequestInfo nri = mNetworkRequests.get(nr);
+
+ final int blockedReasons = mUidBlockedReasons.get(nri.mAsUid, BLOCKED_REASON_NONE);
+ final boolean oldVpnBlocked = isUidBlockedByVpn(nri.mAsUid, oldBlockedUidRanges);
+ final boolean newVpnBlocked = (oldBlockedUidRanges != newBlockedUidRanges)
+ ? isUidBlockedByVpn(nri.mAsUid, newBlockedUidRanges)
+ : oldVpnBlocked;
+
+ final int oldBlockedState = getBlockedState(blockedReasons, oldMetered, oldVpnBlocked);
+ final int newBlockedState = getBlockedState(blockedReasons, newMetered, newVpnBlocked);
+ if (oldBlockedState != newBlockedState) {
+ callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_BLK_CHANGED,
+ newBlockedState);
+ }
+ }
+ }
+
+ /**
+ * Notify apps with a given UID of the new blocked state according to new uid state.
+ * @param uid The uid for which the rules changed.
+ * @param blockedReasons The reasons for why an uid is blocked.
+ */
+ private void maybeNotifyNetworkBlockedForNewState(int uid, @BlockedReason int blockedReasons) {
+ for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
+ final boolean metered = nai.networkCapabilities.isMetered();
+ final boolean vpnBlocked = isUidBlockedByVpn(uid, mVpnBlockedUidRanges);
+
+ final int oldBlockedState = getBlockedState(
+ mUidBlockedReasons.get(uid, BLOCKED_REASON_NONE), metered, vpnBlocked);
+ final int newBlockedState = getBlockedState(blockedReasons, metered, vpnBlocked);
+ if (oldBlockedState == newBlockedState) {
+ continue;
+ }
+ for (int i = 0; i < nai.numNetworkRequests(); i++) {
+ NetworkRequest nr = nai.requestAt(i);
+ NetworkRequestInfo nri = mNetworkRequests.get(nr);
+ if (nri != null && nri.mAsUid == uid) {
+ callCallbackForRequest(nri, nai, ConnectivityManager.CALLBACK_BLK_CHANGED,
+ newBlockedState);
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ protected void sendLegacyNetworkBroadcast(NetworkAgentInfo nai, DetailedState state, int type) {
+ // The NetworkInfo we actually send out has no bearing on the real
+ // state of affairs. For example, if the default connection is mobile,
+ // and a request for HIPRI has just gone away, we need to pretend that
+ // HIPRI has just disconnected. So we need to set the type to HIPRI and
+ // the state to DISCONNECTED, even though the network is of type MOBILE
+ // and is still connected.
+ NetworkInfo info = new NetworkInfo(nai.networkInfo);
+ info.setType(type);
+ filterForLegacyLockdown(info);
+ if (state != DetailedState.DISCONNECTED) {
+ info.setDetailedState(state, null, info.getExtraInfo());
+ sendConnectedBroadcast(info);
+ } else {
+ info.setDetailedState(state, info.getReason(), info.getExtraInfo());
+ Intent intent = new Intent(ConnectivityManager.CONNECTIVITY_ACTION);
+ intent.putExtra(ConnectivityManager.EXTRA_NETWORK_INFO, info);
+ intent.putExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, info.getType());
+ if (info.isFailover()) {
+ intent.putExtra(ConnectivityManager.EXTRA_IS_FAILOVER, true);
+ nai.networkInfo.setFailover(false);
+ }
+ if (info.getReason() != null) {
+ intent.putExtra(ConnectivityManager.EXTRA_REASON, info.getReason());
+ }
+ if (info.getExtraInfo() != null) {
+ intent.putExtra(ConnectivityManager.EXTRA_EXTRA_INFO, info.getExtraInfo());
+ }
+ NetworkAgentInfo newDefaultAgent = null;
+ if (nai.isSatisfyingRequest(mDefaultRequest.mRequests.get(0).requestId)) {
+ newDefaultAgent = mDefaultRequest.getSatisfier();
+ if (newDefaultAgent != null) {
+ intent.putExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO,
+ newDefaultAgent.networkInfo);
+ } else {
+ intent.putExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, true);
+ }
+ }
+ intent.putExtra(ConnectivityManager.EXTRA_INET_CONDITION,
+ mDefaultInetConditionPublished);
+ sendStickyBroadcast(intent);
+ if (newDefaultAgent != null) {
+ sendConnectedBroadcast(newDefaultAgent.networkInfo);
+ }
+ }
+ }
+
+ protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType, int arg1) {
+ if (VDBG || DDBG) {
+ String notification = ConnectivityManager.getCallbackName(notifyType);
+ log("notifyType " + notification + " for " + networkAgent.toShortString());
+ }
+ for (int i = 0; i < networkAgent.numNetworkRequests(); i++) {
+ NetworkRequest nr = networkAgent.requestAt(i);
+ NetworkRequestInfo nri = mNetworkRequests.get(nr);
+ if (VDBG) log(" sending notification for " + nr);
+ if (nri.mPendingIntent == null) {
+ callCallbackForRequest(nri, networkAgent, notifyType, arg1);
+ } else {
+ sendPendingIntentForRequest(nri, networkAgent, notifyType);
+ }
+ }
+ }
+
+ protected void notifyNetworkCallbacks(NetworkAgentInfo networkAgent, int notifyType) {
+ notifyNetworkCallbacks(networkAgent, notifyType, 0);
+ }
+
+ /**
+ * Returns the list of all interfaces that could be used by network traffic that does not
+ * explicitly specify a network. This includes the default network, but also all VPNs that are
+ * currently connected.
+ *
+ * Must be called on the handler thread.
+ */
+ @NonNull
+ private ArrayList<Network> getDefaultNetworks() {
+ ensureRunningOnConnectivityServiceThread();
+ final ArrayList<Network> defaultNetworks = new ArrayList<>();
+ final Set<Integer> activeNetIds = new ArraySet<>();
+ for (final NetworkRequestInfo nri : mDefaultNetworkRequests) {
+ if (nri.isBeingSatisfied()) {
+ activeNetIds.add(nri.getSatisfier().network().netId);
+ }
+ }
+ for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+ if (nai.everConnected && (activeNetIds.contains(nai.network().netId) || nai.isVPN())) {
+ defaultNetworks.add(nai.network);
+ }
+ }
+ return defaultNetworks;
+ }
+
+ /**
+ * Notify NetworkStatsService that the set of active ifaces has changed, or that one of the
+ * active iface's tracked properties has changed.
+ */
+ private void notifyIfacesChangedForNetworkStats() {
+ ensureRunningOnConnectivityServiceThread();
+ String activeIface = null;
+ LinkProperties activeLinkProperties = getActiveLinkProperties();
+ if (activeLinkProperties != null) {
+ activeIface = activeLinkProperties.getInterfaceName();
+ }
+
+ final UnderlyingNetworkInfo[] underlyingNetworkInfos = getAllVpnInfo();
+ try {
+ final ArrayList<NetworkStateSnapshot> snapshots = new ArrayList<>();
+ for (final NetworkStateSnapshot snapshot : getAllNetworkStateSnapshots()) {
+ snapshots.add(snapshot);
+ }
+ mStatsManager.notifyNetworkStatus(getDefaultNetworks(),
+ snapshots, activeIface, Arrays.asList(underlyingNetworkInfos));
+ } catch (Exception ignored) {
+ }
+ }
+
+ @Override
+ public String getCaptivePortalServerUrl() {
+ enforceNetworkStackOrSettingsPermission();
+ String settingUrl = mResources.get().getString(
+ R.string.config_networkCaptivePortalServerUrl);
+
+ if (!TextUtils.isEmpty(settingUrl)) {
+ return settingUrl;
+ }
+
+ settingUrl = Settings.Global.getString(mContext.getContentResolver(),
+ ConnectivitySettingsManager.CAPTIVE_PORTAL_HTTP_URL);
+ if (!TextUtils.isEmpty(settingUrl)) {
+ return settingUrl;
+ }
+
+ return DEFAULT_CAPTIVE_PORTAL_HTTP_URL;
+ }
+
+ @Override
+ public void startNattKeepalive(Network network, int intervalSeconds,
+ ISocketKeepaliveCallback cb, String srcAddr, int srcPort, String dstAddr) {
+ enforceKeepalivePermission();
+ mKeepaliveTracker.startNattKeepalive(
+ getNetworkAgentInfoForNetwork(network), null /* fd */,
+ intervalSeconds, cb,
+ srcAddr, srcPort, dstAddr, NattSocketKeepalive.NATT_PORT);
+ }
+
+ @Override
+ public void startNattKeepaliveWithFd(Network network, ParcelFileDescriptor pfd, int resourceId,
+ int intervalSeconds, ISocketKeepaliveCallback cb, String srcAddr,
+ String dstAddr) {
+ try {
+ final FileDescriptor fd = pfd.getFileDescriptor();
+ mKeepaliveTracker.startNattKeepalive(
+ getNetworkAgentInfoForNetwork(network), fd, resourceId,
+ intervalSeconds, cb,
+ srcAddr, dstAddr, NattSocketKeepalive.NATT_PORT);
+ } finally {
+ // FileDescriptors coming from AIDL calls must be manually closed to prevent leaks.
+ // startNattKeepalive calls Os.dup(fd) before returning, so we can close immediately.
+ if (pfd != null && Binder.getCallingPid() != Process.myPid()) {
+ IoUtils.closeQuietly(pfd);
+ }
+ }
+ }
+
+ @Override
+ public void startTcpKeepalive(Network network, ParcelFileDescriptor pfd, int intervalSeconds,
+ ISocketKeepaliveCallback cb) {
+ try {
+ enforceKeepalivePermission();
+ final FileDescriptor fd = pfd.getFileDescriptor();
+ mKeepaliveTracker.startTcpKeepalive(
+ getNetworkAgentInfoForNetwork(network), fd, intervalSeconds, cb);
+ } finally {
+ // FileDescriptors coming from AIDL calls must be manually closed to prevent leaks.
+ // startTcpKeepalive calls Os.dup(fd) before returning, so we can close immediately.
+ if (pfd != null && Binder.getCallingPid() != Process.myPid()) {
+ IoUtils.closeQuietly(pfd);
+ }
+ }
+ }
+
+ @Override
+ public void stopKeepalive(Network network, int slot) {
+ mHandler.sendMessage(mHandler.obtainMessage(
+ NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE, slot, SocketKeepalive.SUCCESS, network));
+ }
+
+ @Override
+ public void factoryReset() {
+ enforceSettingsPermission();
+
+ final int uid = mDeps.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ if (mUserManager.hasUserRestrictionForUser(UserManager.DISALLOW_NETWORK_RESET,
+ UserHandle.getUserHandleForUid(uid))) {
+ return;
+ }
+
+ final IpMemoryStore ipMemoryStore = IpMemoryStore.getMemoryStore(mContext);
+ ipMemoryStore.factoryReset();
+
+ // Turn airplane mode off
+ setAirplaneMode(false);
+
+ // restore private DNS settings to default mode (opportunistic)
+ if (!mUserManager.hasUserRestrictionForUser(UserManager.DISALLOW_CONFIG_PRIVATE_DNS,
+ UserHandle.getUserHandleForUid(uid))) {
+ ConnectivitySettingsManager.setPrivateDnsMode(mContext,
+ PRIVATE_DNS_MODE_OPPORTUNISTIC);
+ }
+
+ Settings.Global.putString(mContext.getContentResolver(),
+ ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, null);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public byte[] getNetworkWatchlistConfigHash() {
+ NetworkWatchlistManager nwm = mContext.getSystemService(NetworkWatchlistManager.class);
+ if (nwm == null) {
+ loge("Unable to get NetworkWatchlistManager");
+ return null;
+ }
+ // Redirect it to network watchlist service to access watchlist file and calculate hash.
+ return nwm.getWatchlistConfigHash();
+ }
+
+ private void logNetworkEvent(NetworkAgentInfo nai, int evtype) {
+ int[] transports = nai.networkCapabilities.getTransportTypes();
+ mMetricsLog.log(nai.network.getNetId(), transports, new NetworkEvent(evtype));
+ }
+
+ private static boolean toBool(int encodedBoolean) {
+ return encodedBoolean != 0; // Only 0 means false.
+ }
+
+ private static int encodeBool(boolean b) {
+ return b ? 1 : 0;
+ }
+
+ @Override
+ public int handleShellCommand(@NonNull ParcelFileDescriptor in,
+ @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err,
+ @NonNull String[] args) {
+ return new ShellCmd().exec(this, in.getFileDescriptor(), out.getFileDescriptor(),
+ err.getFileDescriptor(), args);
+ }
+
+ private class ShellCmd extends BasicShellCommandHandler {
+ @Override
+ public int onCommand(String cmd) {
+ if (cmd == null) {
+ return handleDefaultCommands(cmd);
+ }
+ final PrintWriter pw = getOutPrintWriter();
+ try {
+ switch (cmd) {
+ case "airplane-mode":
+ final String action = getNextArg();
+ if ("enable".equals(action)) {
+ setAirplaneMode(true);
+ return 0;
+ } else if ("disable".equals(action)) {
+ setAirplaneMode(false);
+ return 0;
+ } else if (action == null) {
+ final ContentResolver cr = mContext.getContentResolver();
+ final int enabled = Settings.Global.getInt(cr,
+ Settings.Global.AIRPLANE_MODE_ON);
+ pw.println(enabled == 0 ? "disabled" : "enabled");
+ return 0;
+ } else {
+ onHelp();
+ return -1;
+ }
+ default:
+ return handleDefaultCommands(cmd);
+ }
+ } catch (Exception e) {
+ pw.println(e);
+ }
+ return -1;
+ }
+
+ @Override
+ public void onHelp() {
+ PrintWriter pw = getOutPrintWriter();
+ pw.println("Connectivity service commands:");
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println(" airplane-mode [enable|disable]");
+ pw.println(" Turn airplane mode on or off.");
+ pw.println(" airplane-mode");
+ pw.println(" Get airplane mode.");
+ }
+ }
+
+ private @VpnManager.VpnType int getVpnType(@Nullable NetworkAgentInfo vpn) {
+ if (vpn == null) return VpnManager.TYPE_VPN_NONE;
+ final TransportInfo ti = vpn.networkCapabilities.getTransportInfo();
+ if (!(ti instanceof VpnTransportInfo)) return VpnManager.TYPE_VPN_NONE;
+ return ((VpnTransportInfo) ti).getType();
+ }
+
+ /**
+ * @param connectionInfo the connection to resolve.
+ * @return {@code uid} if the connection is found and the app has permission to observe it
+ * (e.g., if it is associated with the calling VPN app's tunnel) or {@code INVALID_UID} if the
+ * connection is not found.
+ */
+ public int getConnectionOwnerUid(ConnectionInfo connectionInfo) {
+ if (connectionInfo.protocol != IPPROTO_TCP && connectionInfo.protocol != IPPROTO_UDP) {
+ throw new IllegalArgumentException("Unsupported protocol " + connectionInfo.protocol);
+ }
+
+ final int uid = mDeps.getConnectionOwnerUid(connectionInfo.protocol,
+ connectionInfo.local, connectionInfo.remote);
+
+ if (uid == INVALID_UID) return uid; // Not found.
+
+ // Connection owner UIDs are visible only to the network stack and to the VpnService-based
+ // VPN, if any, that applies to the UID that owns the connection.
+ if (checkNetworkStackPermission()) return uid;
+
+ final NetworkAgentInfo vpn = getVpnForUid(uid);
+ if (vpn == null || getVpnType(vpn) != VpnManager.TYPE_VPN_SERVICE
+ || vpn.networkCapabilities.getOwnerUid() != mDeps.getCallingUid()) {
+ return INVALID_UID;
+ }
+
+ return uid;
+ }
+
+ /**
+ * Returns a IBinder to a TestNetworkService. Will be lazily created as needed.
+ *
+ * <p>The TestNetworkService must be run in the system server due to TUN creation.
+ */
+ @Override
+ public IBinder startOrGetTestNetworkService() {
+ synchronized (mTNSLock) {
+ TestNetworkService.enforceTestNetworkPermissions(mContext);
+
+ if (mTNS == null) {
+ mTNS = new TestNetworkService(mContext);
+ }
+
+ return mTNS;
+ }
+ }
+
+ /**
+ * Handler used for managing all Connectivity Diagnostics related functions.
+ *
+ * @see android.net.ConnectivityDiagnosticsManager
+ *
+ * TODO(b/147816404): Explore moving ConnectivityDiagnosticsHandler to a separate file
+ */
+ @VisibleForTesting
+ class ConnectivityDiagnosticsHandler extends Handler {
+ private final String mTag = ConnectivityDiagnosticsHandler.class.getSimpleName();
+
+ /**
+ * Used to handle ConnectivityDiagnosticsCallback registration events from {@link
+ * android.net.ConnectivityDiagnosticsManager}.
+ * obj = ConnectivityDiagnosticsCallbackInfo with IConnectivityDiagnosticsCallback and
+ * NetworkRequestInfo to be registered
+ */
+ private static final int EVENT_REGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK = 1;
+
+ /**
+ * Used to handle ConnectivityDiagnosticsCallback unregister events from {@link
+ * android.net.ConnectivityDiagnosticsManager}.
+ * obj = the IConnectivityDiagnosticsCallback to be unregistered
+ * arg1 = the uid of the caller
+ */
+ private static final int EVENT_UNREGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK = 2;
+
+ /**
+ * Event for {@link NetworkStateTrackerHandler} to trigger ConnectivityReport callbacks
+ * after processing {@link #EVENT_NETWORK_TESTED} events.
+ * obj = {@link ConnectivityReportEvent} representing ConnectivityReport info reported from
+ * NetworkMonitor.
+ * data = PersistableBundle of extras passed from NetworkMonitor.
+ *
+ * <p>See {@link ConnectivityService#EVENT_NETWORK_TESTED}.
+ */
+ private static final int EVENT_NETWORK_TESTED = ConnectivityService.EVENT_NETWORK_TESTED;
+
+ /**
+ * Event for NetworkMonitor to inform ConnectivityService that a potential data stall has
+ * been detected on the network.
+ * obj = Long the timestamp (in millis) for when the suspected data stall was detected.
+ * arg1 = {@link DataStallReport#DetectionMethod} indicating the detection method.
+ * arg2 = NetID.
+ * data = PersistableBundle of extras passed from NetworkMonitor.
+ */
+ private static final int EVENT_DATA_STALL_SUSPECTED = 4;
+
+ /**
+ * Event for ConnectivityDiagnosticsHandler to handle network connectivity being reported to
+ * the platform. This event will invoke {@link
+ * IConnectivityDiagnosticsCallback#onNetworkConnectivityReported} for permissioned
+ * callbacks.
+ * obj = Network that was reported on
+ * arg1 = boolint for the quality reported
+ */
+ private static final int EVENT_NETWORK_CONNECTIVITY_REPORTED = 5;
+
+ private ConnectivityDiagnosticsHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_REGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK: {
+ handleRegisterConnectivityDiagnosticsCallback(
+ (ConnectivityDiagnosticsCallbackInfo) msg.obj);
+ break;
+ }
+ case EVENT_UNREGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK: {
+ handleUnregisterConnectivityDiagnosticsCallback(
+ (IConnectivityDiagnosticsCallback) msg.obj, msg.arg1);
+ break;
+ }
+ case EVENT_NETWORK_TESTED: {
+ final ConnectivityReportEvent reportEvent =
+ (ConnectivityReportEvent) msg.obj;
+
+ handleNetworkTestedWithExtras(reportEvent, reportEvent.mExtras);
+ break;
+ }
+ case EVENT_DATA_STALL_SUSPECTED: {
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(msg.arg2);
+ final Pair<Long, PersistableBundle> arg =
+ (Pair<Long, PersistableBundle>) msg.obj;
+ if (nai == null) break;
+
+ handleDataStallSuspected(nai, arg.first, msg.arg1, arg.second);
+ break;
+ }
+ case EVENT_NETWORK_CONNECTIVITY_REPORTED: {
+ handleNetworkConnectivityReported((NetworkAgentInfo) msg.obj, toBool(msg.arg1));
+ break;
+ }
+ default: {
+ Log.e(mTag, "Unrecognized event in ConnectivityDiagnostics: " + msg.what);
+ }
+ }
+ }
+ }
+
+ /** Class used for cleaning up IConnectivityDiagnosticsCallback instances after their death. */
+ @VisibleForTesting
+ class ConnectivityDiagnosticsCallbackInfo implements Binder.DeathRecipient {
+ @NonNull private final IConnectivityDiagnosticsCallback mCb;
+ @NonNull private final NetworkRequestInfo mRequestInfo;
+ @NonNull private final String mCallingPackageName;
+
+ @VisibleForTesting
+ ConnectivityDiagnosticsCallbackInfo(
+ @NonNull IConnectivityDiagnosticsCallback cb,
+ @NonNull NetworkRequestInfo nri,
+ @NonNull String callingPackageName) {
+ mCb = cb;
+ mRequestInfo = nri;
+ mCallingPackageName = callingPackageName;
+ }
+
+ @Override
+ public void binderDied() {
+ log("ConnectivityDiagnosticsCallback IBinder died.");
+ unregisterConnectivityDiagnosticsCallback(mCb);
+ }
+ }
+
+ /**
+ * Class used for sending information from {@link
+ * NetworkMonitorCallbacks#notifyNetworkTestedWithExtras} to the handler for processing it.
+ */
+ private static class NetworkTestedResults {
+ private final int mNetId;
+ private final int mTestResult;
+ private final long mTimestampMillis;
+ @Nullable private final String mRedirectUrl;
+
+ private NetworkTestedResults(
+ int netId, int testResult, long timestampMillis, @Nullable String redirectUrl) {
+ mNetId = netId;
+ mTestResult = testResult;
+ mTimestampMillis = timestampMillis;
+ mRedirectUrl = redirectUrl;
+ }
+ }
+
+ /**
+ * Class used for sending information from {@link NetworkStateTrackerHandler} to {@link
+ * ConnectivityDiagnosticsHandler}.
+ */
+ private static class ConnectivityReportEvent {
+ private final long mTimestampMillis;
+ @NonNull private final NetworkAgentInfo mNai;
+ private final PersistableBundle mExtras;
+
+ private ConnectivityReportEvent(long timestampMillis, @NonNull NetworkAgentInfo nai,
+ PersistableBundle p) {
+ mTimestampMillis = timestampMillis;
+ mNai = nai;
+ mExtras = p;
+ }
+ }
+
+ private void handleRegisterConnectivityDiagnosticsCallback(
+ @NonNull ConnectivityDiagnosticsCallbackInfo cbInfo) {
+ ensureRunningOnConnectivityServiceThread();
+
+ final IConnectivityDiagnosticsCallback cb = cbInfo.mCb;
+ final IBinder iCb = cb.asBinder();
+ final NetworkRequestInfo nri = cbInfo.mRequestInfo;
+
+ // Connectivity Diagnostics are meant to be used with a single network request. It would be
+ // confusing for these networks to change when an NRI is satisfied in another layer.
+ if (nri.isMultilayerRequest()) {
+ throw new IllegalArgumentException("Connectivity Diagnostics do not support multilayer "
+ + "network requests.");
+ }
+
+ // This means that the client registered the same callback multiple times. Do
+ // not override the previous entry, and exit silently.
+ if (mConnectivityDiagnosticsCallbacks.containsKey(iCb)) {
+ if (VDBG) log("Diagnostics callback is already registered");
+
+ // Decrement the reference count for this NetworkRequestInfo. The reference count is
+ // incremented when the NetworkRequestInfo is created as part of
+ // enforceRequestCountLimit().
+ nri.decrementRequestCount();
+ return;
+ }
+
+ mConnectivityDiagnosticsCallbacks.put(iCb, cbInfo);
+
+ try {
+ iCb.linkToDeath(cbInfo, 0);
+ } catch (RemoteException e) {
+ cbInfo.binderDied();
+ return;
+ }
+
+ // Once registered, provide ConnectivityReports for matching Networks
+ final List<NetworkAgentInfo> matchingNetworks = new ArrayList<>();
+ synchronized (mNetworkForNetId) {
+ for (int i = 0; i < mNetworkForNetId.size(); i++) {
+ final NetworkAgentInfo nai = mNetworkForNetId.valueAt(i);
+ // Connectivity Diagnostics rejects multilayer requests at registration hence get(0)
+ if (nai.satisfies(nri.mRequests.get(0))) {
+ matchingNetworks.add(nai);
+ }
+ }
+ }
+ for (final NetworkAgentInfo nai : matchingNetworks) {
+ final ConnectivityReport report = nai.getConnectivityReport();
+ if (report == null) {
+ continue;
+ }
+ if (!checkConnectivityDiagnosticsPermissions(
+ nri.mPid, nri.mUid, nai, cbInfo.mCallingPackageName)) {
+ continue;
+ }
+
+ try {
+ cb.onConnectivityReportAvailable(report);
+ } catch (RemoteException e) {
+ // Exception while sending the ConnectivityReport. Move on to the next network.
+ }
+ }
+ }
+
+ private void handleUnregisterConnectivityDiagnosticsCallback(
+ @NonNull IConnectivityDiagnosticsCallback cb, int uid) {
+ ensureRunningOnConnectivityServiceThread();
+ final IBinder iCb = cb.asBinder();
+
+ final ConnectivityDiagnosticsCallbackInfo cbInfo =
+ mConnectivityDiagnosticsCallbacks.remove(iCb);
+ if (cbInfo == null) {
+ if (VDBG) log("Removing diagnostics callback that is not currently registered");
+ return;
+ }
+
+ final NetworkRequestInfo nri = cbInfo.mRequestInfo;
+
+ // Caller's UID must either be the registrants (if they are unregistering) or the System's
+ // (if the Binder died)
+ if (uid != nri.mUid && uid != Process.SYSTEM_UID) {
+ if (DBG) loge("Uid(" + uid + ") not registrant's (" + nri.mUid + ") or System's");
+ return;
+ }
+
+ // Decrement the reference count for this NetworkRequestInfo. The reference count is
+ // incremented when the NetworkRequestInfo is created as part of
+ // enforceRequestCountLimit().
+ nri.decrementRequestCount();
+
+ iCb.unlinkToDeath(cbInfo, 0);
+ }
+
+ private void handleNetworkTestedWithExtras(
+ @NonNull ConnectivityReportEvent reportEvent, @NonNull PersistableBundle extras) {
+ final NetworkAgentInfo nai = reportEvent.mNai;
+ final NetworkCapabilities networkCapabilities =
+ getNetworkCapabilitiesWithoutUids(nai.networkCapabilities);
+ final ConnectivityReport report =
+ new ConnectivityReport(
+ reportEvent.mNai.network,
+ reportEvent.mTimestampMillis,
+ nai.linkProperties,
+ networkCapabilities,
+ extras);
+ nai.setConnectivityReport(report);
+ final List<IConnectivityDiagnosticsCallback> results =
+ getMatchingPermissionedCallbacks(nai);
+ for (final IConnectivityDiagnosticsCallback cb : results) {
+ try {
+ cb.onConnectivityReportAvailable(report);
+ } catch (RemoteException ex) {
+ loge("Error invoking onConnectivityReport", ex);
+ }
+ }
+ }
+
+ private void handleDataStallSuspected(
+ @NonNull NetworkAgentInfo nai, long timestampMillis, int detectionMethod,
+ @NonNull PersistableBundle extras) {
+ final NetworkCapabilities networkCapabilities =
+ getNetworkCapabilitiesWithoutUids(nai.networkCapabilities);
+ final DataStallReport report =
+ new DataStallReport(
+ nai.network,
+ timestampMillis,
+ detectionMethod,
+ nai.linkProperties,
+ networkCapabilities,
+ extras);
+ final List<IConnectivityDiagnosticsCallback> results =
+ getMatchingPermissionedCallbacks(nai);
+ for (final IConnectivityDiagnosticsCallback cb : results) {
+ try {
+ cb.onDataStallSuspected(report);
+ } catch (RemoteException ex) {
+ loge("Error invoking onDataStallSuspected", ex);
+ }
+ }
+ }
+
+ private void handleNetworkConnectivityReported(
+ @NonNull NetworkAgentInfo nai, boolean connectivity) {
+ final List<IConnectivityDiagnosticsCallback> results =
+ getMatchingPermissionedCallbacks(nai);
+ for (final IConnectivityDiagnosticsCallback cb : results) {
+ try {
+ cb.onNetworkConnectivityReported(nai.network, connectivity);
+ } catch (RemoteException ex) {
+ loge("Error invoking onNetworkConnectivityReported", ex);
+ }
+ }
+ }
+
+ private NetworkCapabilities getNetworkCapabilitiesWithoutUids(@NonNull NetworkCapabilities nc) {
+ final NetworkCapabilities sanitized = new NetworkCapabilities(nc,
+ NetworkCapabilities.REDACT_ALL);
+ sanitized.setUids(null);
+ sanitized.setAdministratorUids(new int[0]);
+ sanitized.setOwnerUid(Process.INVALID_UID);
+ return sanitized;
+ }
+
+ private List<IConnectivityDiagnosticsCallback> getMatchingPermissionedCallbacks(
+ @NonNull NetworkAgentInfo nai) {
+ final List<IConnectivityDiagnosticsCallback> results = new ArrayList<>();
+ for (Entry<IBinder, ConnectivityDiagnosticsCallbackInfo> entry :
+ mConnectivityDiagnosticsCallbacks.entrySet()) {
+ final ConnectivityDiagnosticsCallbackInfo cbInfo = entry.getValue();
+ final NetworkRequestInfo nri = cbInfo.mRequestInfo;
+ // Connectivity Diagnostics rejects multilayer requests at registration hence get(0).
+ if (nai.satisfies(nri.mRequests.get(0))) {
+ if (checkConnectivityDiagnosticsPermissions(
+ nri.mPid, nri.mUid, nai, cbInfo.mCallingPackageName)) {
+ results.add(entry.getValue().mCb);
+ }
+ }
+ }
+ return results;
+ }
+
+ private boolean hasLocationPermission(String packageName, int uid) {
+ // LocationPermissionChecker#checkLocationPermission can throw SecurityException if the uid
+ // and package name don't match. Throwing on the CS thread is not acceptable, so wrap the
+ // call in a try-catch.
+ try {
+ if (!mLocationPermissionChecker.checkLocationPermission(
+ packageName, null /* featureId */, uid, null /* message */)) {
+ return false;
+ }
+ } catch (SecurityException e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean ownsVpnRunningOverNetwork(int uid, Network network) {
+ for (NetworkAgentInfo virtual : mNetworkAgentInfos) {
+ if (virtual.supportsUnderlyingNetworks()
+ && virtual.networkCapabilities.getOwnerUid() == uid
+ && CollectionUtils.contains(virtual.declaredUnderlyingNetworks, network)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @VisibleForTesting
+ boolean checkConnectivityDiagnosticsPermissions(
+ int callbackPid, int callbackUid, NetworkAgentInfo nai, String callbackPackageName) {
+ if (checkNetworkStackPermission(callbackPid, callbackUid)) {
+ return true;
+ }
+
+ // Administrator UIDs also contains the Owner UID
+ final int[] administratorUids = nai.networkCapabilities.getAdministratorUids();
+ if (!CollectionUtils.contains(administratorUids, callbackUid)
+ && !ownsVpnRunningOverNetwork(callbackUid, nai.network)) {
+ return false;
+ }
+
+ return hasLocationPermission(callbackPackageName, callbackUid);
+ }
+
+ @Override
+ public void registerConnectivityDiagnosticsCallback(
+ @NonNull IConnectivityDiagnosticsCallback callback,
+ @NonNull NetworkRequest request,
+ @NonNull String callingPackageName) {
+ if (request.legacyType != TYPE_NONE) {
+ throw new IllegalArgumentException("ConnectivityManager.TYPE_* are deprecated."
+ + " Please use NetworkCapabilities instead.");
+ }
+ final int callingUid = mDeps.getCallingUid();
+ mAppOpsManager.checkPackage(callingUid, callingPackageName);
+
+ // This NetworkCapabilities is only used for matching to Networks. Clear out its owner uid
+ // and administrator uids to be safe.
+ final NetworkCapabilities nc = new NetworkCapabilities(request.networkCapabilities);
+ restrictRequestUidsForCallerAndSetRequestorInfo(nc, callingUid, callingPackageName);
+
+ final NetworkRequest requestWithId =
+ new NetworkRequest(
+ nc, TYPE_NONE, nextNetworkRequestId(), NetworkRequest.Type.LISTEN);
+
+ // NetworkRequestInfos created here count towards MAX_NETWORK_REQUESTS_PER_UID limit.
+ //
+ // nri is not bound to the death of callback. Instead, callback.bindToDeath() is set in
+ // handleRegisterConnectivityDiagnosticsCallback(). nri will be cleaned up as part of the
+ // callback's binder death.
+ final NetworkRequestInfo nri = new NetworkRequestInfo(callingUid, requestWithId);
+ final ConnectivityDiagnosticsCallbackInfo cbInfo =
+ new ConnectivityDiagnosticsCallbackInfo(callback, nri, callingPackageName);
+
+ mConnectivityDiagnosticsHandler.sendMessage(
+ mConnectivityDiagnosticsHandler.obtainMessage(
+ ConnectivityDiagnosticsHandler
+ .EVENT_REGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK,
+ cbInfo));
+ }
+
+ @Override
+ public void unregisterConnectivityDiagnosticsCallback(
+ @NonNull IConnectivityDiagnosticsCallback callback) {
+ Objects.requireNonNull(callback, "callback must be non-null");
+ mConnectivityDiagnosticsHandler.sendMessage(
+ mConnectivityDiagnosticsHandler.obtainMessage(
+ ConnectivityDiagnosticsHandler
+ .EVENT_UNREGISTER_CONNECTIVITY_DIAGNOSTICS_CALLBACK,
+ mDeps.getCallingUid(),
+ 0,
+ callback));
+ }
+
+ @Override
+ public void simulateDataStall(int detectionMethod, long timestampMillis,
+ @NonNull Network network, @NonNull PersistableBundle extras) {
+ enforceAnyPermissionOf(android.Manifest.permission.MANAGE_TEST_NETWORKS,
+ android.Manifest.permission.NETWORK_STACK);
+ final NetworkCapabilities nc = getNetworkCapabilitiesInternal(network);
+ if (!nc.hasTransport(TRANSPORT_TEST)) {
+ throw new SecurityException("Data Stall simluation is only possible for test networks");
+ }
+
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+ if (nai == null || nai.creatorUid != mDeps.getCallingUid()) {
+ throw new SecurityException("Data Stall simulation is only possible for network "
+ + "creators");
+ }
+
+ // Instead of passing the data stall directly to the ConnectivityDiagnostics handler, treat
+ // this as a Data Stall received directly from NetworkMonitor. This requires wrapping the
+ // Data Stall information as a DataStallReportParcelable and passing to
+ // #notifyDataStallSuspected. This ensures that unknown Data Stall detection methods are
+ // still passed to ConnectivityDiagnostics (with new detection methods masked).
+ final DataStallReportParcelable p = new DataStallReportParcelable();
+ p.timestampMillis = timestampMillis;
+ p.detectionMethod = detectionMethod;
+
+ if (hasDataStallDetectionMethod(p, DETECTION_METHOD_DNS_EVENTS)) {
+ p.dnsConsecutiveTimeouts = extras.getInt(KEY_DNS_CONSECUTIVE_TIMEOUTS);
+ }
+ if (hasDataStallDetectionMethod(p, DETECTION_METHOD_TCP_METRICS)) {
+ p.tcpPacketFailRate = extras.getInt(KEY_TCP_PACKET_FAIL_RATE);
+ p.tcpMetricsCollectionPeriodMillis = extras.getInt(
+ KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS);
+ }
+
+ notifyDataStallSuspected(p, network.getNetId());
+ }
+
+ private class NetdCallback extends BaseNetdUnsolicitedEventListener {
+ @Override
+ public void onInterfaceClassActivityChanged(boolean isActive, int transportType,
+ long timestampNs, int uid) {
+ mNetworkActivityTracker.setAndReportNetworkActive(isActive, transportType, timestampNs);
+ }
+
+ @Override
+ public void onInterfaceLinkStateChanged(String iface, boolean up) {
+ for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+ nai.clatd.interfaceLinkStateChanged(iface, up);
+ }
+ }
+
+ @Override
+ public void onInterfaceRemoved(String iface) {
+ for (NetworkAgentInfo nai : mNetworkAgentInfos) {
+ nai.clatd.interfaceRemoved(iface);
+ }
+ }
+ }
+
+ private final LegacyNetworkActivityTracker mNetworkActivityTracker;
+
+ /**
+ * Class used for updating network activity tracking with netd and notify network activity
+ * changes.
+ */
+ private static final class LegacyNetworkActivityTracker {
+ private static final int NO_UID = -1;
+ private final Context mContext;
+ private final INetd mNetd;
+ private final RemoteCallbackList<INetworkActivityListener> mNetworkActivityListeners =
+ new RemoteCallbackList<>();
+ // Indicate the current system default network activity is active or not.
+ @GuardedBy("mActiveIdleTimers")
+ private boolean mNetworkActive;
+ @GuardedBy("mActiveIdleTimers")
+ private final ArrayMap<String, IdleTimerParams> mActiveIdleTimers = new ArrayMap();
+ private final Handler mHandler;
+
+ private class IdleTimerParams {
+ public final int timeout;
+ public final int transportType;
+
+ IdleTimerParams(int timeout, int transport) {
+ this.timeout = timeout;
+ this.transportType = transport;
+ }
+ }
+
+ LegacyNetworkActivityTracker(@NonNull Context context, @NonNull Handler handler,
+ @NonNull INetd netd) {
+ mContext = context;
+ mNetd = netd;
+ mHandler = handler;
+ }
+
+ public void setAndReportNetworkActive(boolean active, int transportType, long tsNanos) {
+ sendDataActivityBroadcast(transportTypeToLegacyType(transportType), active, tsNanos);
+ synchronized (mActiveIdleTimers) {
+ mNetworkActive = active;
+ // If there are no idle timers, it means that system is not monitoring
+ // activity, so the system default network for those default network
+ // unspecified apps is always considered active.
+ //
+ // TODO: If the mActiveIdleTimers is empty, netd will actually not send
+ // any network activity change event. Whenever this event is received,
+ // the mActiveIdleTimers should be always not empty. The legacy behavior
+ // is no-op. Remove to refer to mNetworkActive only.
+ if (mNetworkActive || mActiveIdleTimers.isEmpty()) {
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_REPORT_NETWORK_ACTIVITY));
+ }
+ }
+ }
+
+ // The network activity should only be updated from ConnectivityService handler thread
+ // when mActiveIdleTimers lock is held.
+ @GuardedBy("mActiveIdleTimers")
+ private void reportNetworkActive() {
+ final int length = mNetworkActivityListeners.beginBroadcast();
+ if (DDBG) log("reportNetworkActive, notify " + length + " listeners");
+ try {
+ for (int i = 0; i < length; i++) {
+ try {
+ mNetworkActivityListeners.getBroadcastItem(i).onNetworkActive();
+ } catch (RemoteException | RuntimeException e) {
+ loge("Fail to send network activie to listener " + e);
+ }
+ }
+ } finally {
+ mNetworkActivityListeners.finishBroadcast();
+ }
+ }
+
+ @GuardedBy("mActiveIdleTimers")
+ public void handleReportNetworkActivity() {
+ synchronized (mActiveIdleTimers) {
+ reportNetworkActive();
+ }
+ }
+
+ // This is deprecated and only to support legacy use cases.
+ private int transportTypeToLegacyType(int type) {
+ switch (type) {
+ case NetworkCapabilities.TRANSPORT_CELLULAR:
+ return TYPE_MOBILE;
+ case NetworkCapabilities.TRANSPORT_WIFI:
+ return TYPE_WIFI;
+ case NetworkCapabilities.TRANSPORT_BLUETOOTH:
+ return TYPE_BLUETOOTH;
+ case NetworkCapabilities.TRANSPORT_ETHERNET:
+ return TYPE_ETHERNET;
+ default:
+ loge("Unexpected transport in transportTypeToLegacyType: " + type);
+ }
+ return ConnectivityManager.TYPE_NONE;
+ }
+
+ public void sendDataActivityBroadcast(int deviceType, boolean active, long tsNanos) {
+ final Intent intent = new Intent(ConnectivityManager.ACTION_DATA_ACTIVITY_CHANGE);
+ intent.putExtra(ConnectivityManager.EXTRA_DEVICE_TYPE, deviceType);
+ intent.putExtra(ConnectivityManager.EXTRA_IS_ACTIVE, active);
+ intent.putExtra(ConnectivityManager.EXTRA_REALTIME_NS, tsNanos);
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL,
+ RECEIVE_DATA_ACTIVITY_CHANGE,
+ null /* resultReceiver */,
+ null /* scheduler */,
+ 0 /* initialCode */,
+ null /* initialData */,
+ null /* initialExtra */);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ /**
+ * Setup data activity tracking for the given network.
+ *
+ * Every {@code setupDataActivityTracking} should be paired with a
+ * {@link #removeDataActivityTracking} for cleanup.
+ */
+ private void setupDataActivityTracking(NetworkAgentInfo networkAgent) {
+ final String iface = networkAgent.linkProperties.getInterfaceName();
+
+ final int timeout;
+ final int type;
+
+ if (networkAgent.networkCapabilities.hasTransport(
+ NetworkCapabilities.TRANSPORT_CELLULAR)) {
+ timeout = Settings.Global.getInt(mContext.getContentResolver(),
+ ConnectivitySettingsManager.DATA_ACTIVITY_TIMEOUT_MOBILE,
+ 10);
+ type = NetworkCapabilities.TRANSPORT_CELLULAR;
+ } else if (networkAgent.networkCapabilities.hasTransport(
+ NetworkCapabilities.TRANSPORT_WIFI)) {
+ timeout = Settings.Global.getInt(mContext.getContentResolver(),
+ ConnectivitySettingsManager.DATA_ACTIVITY_TIMEOUT_WIFI,
+ 15);
+ type = NetworkCapabilities.TRANSPORT_WIFI;
+ } else {
+ return; // do not track any other networks
+ }
+
+ updateRadioPowerState(true /* isActive */, type);
+
+ if (timeout > 0 && iface != null) {
+ try {
+ synchronized (mActiveIdleTimers) {
+ // Networks start up.
+ mNetworkActive = true;
+ mActiveIdleTimers.put(iface, new IdleTimerParams(timeout, type));
+ mNetd.idletimerAddInterface(iface, timeout, Integer.toString(type));
+ reportNetworkActive();
+ }
+ } catch (Exception e) {
+ // You shall not crash!
+ loge("Exception in setupDataActivityTracking " + e);
+ }
+ }
+ }
+
+ /**
+ * Remove data activity tracking when network disconnects.
+ */
+ private void removeDataActivityTracking(NetworkAgentInfo networkAgent) {
+ final String iface = networkAgent.linkProperties.getInterfaceName();
+ final NetworkCapabilities caps = networkAgent.networkCapabilities;
+
+ if (iface == null) return;
+
+ final int type;
+ if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+ type = NetworkCapabilities.TRANSPORT_CELLULAR;
+ } else if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
+ type = NetworkCapabilities.TRANSPORT_WIFI;
+ } else {
+ return; // do not track any other networks
+ }
+
+ try {
+ updateRadioPowerState(false /* isActive */, type);
+ synchronized (mActiveIdleTimers) {
+ final IdleTimerParams params = mActiveIdleTimers.remove(iface);
+ // The call fails silently if no idle timer setup for this interface
+ mNetd.idletimerRemoveInterface(iface, params.timeout,
+ Integer.toString(params.transportType));
+ }
+ } catch (Exception e) {
+ // You shall not crash!
+ loge("Exception in removeDataActivityTracking " + e);
+ }
+ }
+
+ /**
+ * Update data activity tracking when network state is updated.
+ */
+ public void updateDataActivityTracking(NetworkAgentInfo newNetwork,
+ NetworkAgentInfo oldNetwork) {
+ if (newNetwork != null) {
+ setupDataActivityTracking(newNetwork);
+ }
+ if (oldNetwork != null) {
+ removeDataActivityTracking(oldNetwork);
+ }
+ }
+
+ private void updateRadioPowerState(boolean isActive, int transportType) {
+ final BatteryStatsManager bs = mContext.getSystemService(BatteryStatsManager.class);
+ switch (transportType) {
+ case NetworkCapabilities.TRANSPORT_CELLULAR:
+ bs.reportMobileRadioPowerState(isActive, NO_UID);
+ break;
+ case NetworkCapabilities.TRANSPORT_WIFI:
+ bs.reportWifiRadioPowerState(isActive, NO_UID);
+ break;
+ default:
+ logw("Untracked transport type:" + transportType);
+ }
+ }
+
+ public boolean isDefaultNetworkActive() {
+ synchronized (mActiveIdleTimers) {
+ // If there are no idle timers, it means that system is not monitoring activity,
+ // so the default network is always considered active.
+ //
+ // TODO : Distinguish between the cases where mActiveIdleTimers is empty because
+ // tracking is disabled (negative idle timer value configured), or no active default
+ // network. In the latter case, this reports active but it should report inactive.
+ return mNetworkActive || mActiveIdleTimers.isEmpty();
+ }
+ }
+
+ public void registerNetworkActivityListener(@NonNull INetworkActivityListener l) {
+ mNetworkActivityListeners.register(l);
+ }
+
+ public void unregisterNetworkActivityListener(@NonNull INetworkActivityListener l) {
+ mNetworkActivityListeners.unregister(l);
+ }
+
+ public void dump(IndentingPrintWriter pw) {
+ synchronized (mActiveIdleTimers) {
+ pw.print("mNetworkActive="); pw.println(mNetworkActive);
+ pw.println("Idle timers:");
+ for (HashMap.Entry<String, IdleTimerParams> ent : mActiveIdleTimers.entrySet()) {
+ pw.print(" "); pw.print(ent.getKey()); pw.println(":");
+ final IdleTimerParams params = ent.getValue();
+ pw.print(" timeout="); pw.print(params.timeout);
+ pw.print(" type="); pw.println(params.transportType);
+ }
+ }
+ }
+ }
+
+ /**
+ * Registers {@link QosSocketFilter} with {@link IQosCallback}.
+ *
+ * @param socketInfo the socket information
+ * @param callback the callback to register
+ */
+ @Override
+ public void registerQosSocketCallback(@NonNull final QosSocketInfo socketInfo,
+ @NonNull final IQosCallback callback) {
+ final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(socketInfo.getNetwork());
+ if (nai == null || nai.networkCapabilities == null) {
+ try {
+ callback.onError(QosCallbackException.EX_TYPE_FILTER_NETWORK_RELEASED);
+ } catch (final RemoteException ex) {
+ loge("registerQosCallbackInternal: RemoteException", ex);
+ }
+ return;
+ }
+ registerQosCallbackInternal(new QosSocketFilter(socketInfo), callback, nai);
+ }
+
+ /**
+ * Register a {@link IQosCallback} with base {@link QosFilter}.
+ *
+ * @param filter the filter to register
+ * @param callback the callback to register
+ * @param nai the agent information related to the filter's network
+ */
+ @VisibleForTesting
+ public void registerQosCallbackInternal(@NonNull final QosFilter filter,
+ @NonNull final IQosCallback callback, @NonNull final NetworkAgentInfo nai) {
+ if (filter == null) throw new IllegalArgumentException("filter must be non-null");
+ if (callback == null) throw new IllegalArgumentException("callback must be non-null");
+
+ if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
+ enforceConnectivityRestrictedNetworksPermission();
+ }
+ mQosCallbackTracker.registerCallback(callback, filter, nai);
+ }
+
+ /**
+ * Unregisters the given callback.
+ *
+ * @param callback the callback to unregister
+ */
+ @Override
+ public void unregisterQosCallback(@NonNull final IQosCallback callback) {
+ Objects.requireNonNull(callback, "callback must be non-null");
+ mQosCallbackTracker.unregisterCallback(callback);
+ }
+
+ // Network preference per-profile and OEM network preferences can't be set at the same
+ // time, because it is unclear what should happen if both preferences are active for
+ // one given UID. To make it possible, the stack would have to clarify what would happen
+ // in case both are active at the same time. The implementation may have to be adjusted
+ // to implement the resulting rules. For example, a priority could be defined between them,
+ // where the OEM preference would be considered less or more important than the enterprise
+ // preference ; this would entail implementing the priorities somehow, e.g. by doing
+ // UID arithmetic with UID ranges or passing a priority to netd so that the routing rules
+ // are set at the right level. Other solutions are possible, e.g. merging of the
+ // preferences for the relevant UIDs.
+ private static void throwConcurrentPreferenceException() {
+ throw new IllegalStateException("Can't set NetworkPreferenceForUser and "
+ + "set OemNetworkPreference at the same time");
+ }
+
+ /**
+ * Request that a user profile is put by default on a network matching a given preference.
+ *
+ * See the documentation for the individual preferences for a description of the supported
+ * behaviors.
+ *
+ * @param profile the profile concerned.
+ * @param preference the preference for this profile, as one of the PROFILE_NETWORK_PREFERENCE_*
+ * constants.
+ * @param listener an optional listener to listen for completion of the operation.
+ */
+ @Override
+ public void setProfileNetworkPreference(@NonNull final UserHandle profile,
+ @ConnectivityManager.ProfileNetworkPreference final int preference,
+ @Nullable final IOnCompleteListener listener) {
+ Objects.requireNonNull(profile);
+ PermissionUtils.enforceNetworkStackPermission(mContext);
+ if (DBG) {
+ log("setProfileNetworkPreference " + profile + " to " + preference);
+ }
+ if (profile.getIdentifier() < 0) {
+ throw new IllegalArgumentException("Must explicitly specify a user handle ("
+ + "UserHandle.CURRENT not supported)");
+ }
+ final UserManager um = mContext.getSystemService(UserManager.class);
+ if (!um.isManagedProfile(profile.getIdentifier())) {
+ throw new IllegalArgumentException("Profile must be a managed profile");
+ }
+ // Strictly speaking, mOemNetworkPreferences should only be touched on the
+ // handler thread. However it is an immutable object, so reading the reference is
+ // safe - it's just possible the value is slightly outdated. For the final check,
+ // see #handleSetProfileNetworkPreference. But if this can be caught here it is a
+ // lot easier to understand, so opportunistically check it.
+ if (!mOemNetworkPreferences.isEmpty()) {
+ throwConcurrentPreferenceException();
+ }
+ final NetworkCapabilities nc;
+ switch (preference) {
+ case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT:
+ nc = null;
+ break;
+ case ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE:
+ final UidRange uids = UidRange.createForUser(profile);
+ nc = createDefaultNetworkCapabilitiesForUidRange(uids);
+ nc.addCapability(NET_CAPABILITY_ENTERPRISE);
+ nc.removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Invalid preference in setProfileNetworkPreference");
+ }
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_PROFILE_NETWORK_PREFERENCE,
+ new Pair<>(new ProfileNetworkPreferences.Preference(profile, nc), listener)));
+ }
+
+ private void validateNetworkCapabilitiesOfProfileNetworkPreference(
+ @Nullable final NetworkCapabilities nc) {
+ if (null == nc) return; // Null caps are always allowed. It means to remove the setting.
+ ensureRequestableCapabilities(nc);
+ }
+
+ private ArraySet<NetworkRequestInfo> createNrisFromProfileNetworkPreferences(
+ @NonNull final ProfileNetworkPreferences prefs) {
+ final ArraySet<NetworkRequestInfo> result = new ArraySet<>();
+ for (final ProfileNetworkPreferences.Preference pref : prefs.preferences) {
+ // The NRI for a user should be comprised of two layers:
+ // - The request for the capabilities
+ // - The request for the default network, for fallback. Create an image of it to
+ // have the correct UIDs in it (also a request can only be part of one NRI, because
+ // of lookups in 1:1 associations like mNetworkRequests).
+ // Note that denying a fallback can be implemented simply by not adding the second
+ // request.
+ final ArrayList<NetworkRequest> nrs = new ArrayList<>();
+ nrs.add(createNetworkRequest(NetworkRequest.Type.REQUEST, pref.capabilities));
+ nrs.add(createDefaultInternetRequestForTransport(
+ TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
+ setNetworkRequestUids(nrs, UidRange.fromIntRanges(pref.capabilities.getUids()));
+ final NetworkRequestInfo nri = new NetworkRequestInfo(Process.myUid(), nrs);
+ result.add(nri);
+ }
+ return result;
+ }
+
+ private void handleSetProfileNetworkPreference(
+ @NonNull final ProfileNetworkPreferences.Preference preference,
+ @Nullable final IOnCompleteListener listener) {
+ // setProfileNetworkPreference and setOemNetworkPreference are mutually exclusive, in
+ // particular because it's not clear what preference should win in case both apply
+ // to the same app.
+ // The binder call has already checked this, but as mOemNetworkPreferences is only
+ // touched on the handler thread, it's theoretically not impossible that it has changed
+ // since.
+ if (!mOemNetworkPreferences.isEmpty()) {
+ // This may happen on a device with an OEM preference set when a user is removed.
+ // In this case, it's safe to ignore. In particular this happens in the tests.
+ loge("handleSetProfileNetworkPreference, but OEM network preferences not empty");
+ return;
+ }
+
+ validateNetworkCapabilitiesOfProfileNetworkPreference(preference.capabilities);
+
+ mProfileNetworkPreferences = mProfileNetworkPreferences.plus(preference);
+ mSystemNetworkRequestCounter.transact(
+ mDeps.getCallingUid(), mProfileNetworkPreferences.preferences.size(),
+ () -> {
+ final ArraySet<NetworkRequestInfo> nris =
+ createNrisFromProfileNetworkPreferences(mProfileNetworkPreferences);
+ replaceDefaultNetworkRequestsForPreference(nris);
+ });
+ // Finally, rematch.
+ rematchAllNetworksAndRequests();
+
+ if (null != listener) {
+ try {
+ listener.onComplete();
+ } catch (RemoteException e) {
+ loge("Listener for setProfileNetworkPreference has died");
+ }
+ }
+ }
+
+ private void enforceAutomotiveDevice() {
+ final boolean isAutomotiveDevice =
+ mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+ if (!isAutomotiveDevice) {
+ throw new UnsupportedOperationException(
+ "setOemNetworkPreference() is only available on automotive devices.");
+ }
+ }
+
+ /**
+ * Used by automotive devices to set the network preferences used to direct traffic at an
+ * application level as per the given OemNetworkPreferences. An example use-case would be an
+ * automotive OEM wanting to provide connectivity for applications critical to the usage of a
+ * vehicle via a particular network.
+ *
+ * Calling this will overwrite the existing preference.
+ *
+ * @param preference {@link OemNetworkPreferences} The application network preference to be set.
+ * @param listener {@link ConnectivityManager.OnCompleteListener} Listener used
+ * to communicate completion of setOemNetworkPreference();
+ */
+ @Override
+ public void setOemNetworkPreference(
+ @NonNull final OemNetworkPreferences preference,
+ @Nullable final IOnCompleteListener listener) {
+
+ enforceAutomotiveDevice();
+ enforceOemNetworkPreferencesPermission();
+
+ if (!mProfileNetworkPreferences.isEmpty()) {
+ // Strictly speaking, mProfileNetworkPreferences should only be touched on the
+ // handler thread. However it is an immutable object, so reading the reference is
+ // safe - it's just possible the value is slightly outdated. For the final check,
+ // see #handleSetOemPreference. But if this can be caught here it is a
+ // lot easier to understand, so opportunistically check it.
+ throwConcurrentPreferenceException();
+ }
+
+ Objects.requireNonNull(preference, "OemNetworkPreferences must be non-null");
+ validateOemNetworkPreferences(preference);
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_OEM_NETWORK_PREFERENCE,
+ new Pair<>(preference, listener)));
+ }
+
+ private void validateOemNetworkPreferences(@NonNull OemNetworkPreferences preference) {
+ for (@OemNetworkPreferences.OemNetworkPreference final int pref
+ : preference.getNetworkPreferences().values()) {
+ if (OemNetworkPreferences.OEM_NETWORK_PREFERENCE_UNINITIALIZED == pref) {
+ final String msg = "OEM_NETWORK_PREFERENCE_UNINITIALIZED is an invalid value.";
+ throw new IllegalArgumentException(msg);
+ }
+ }
+ }
+
+ private void handleSetOemNetworkPreference(
+ @NonNull final OemNetworkPreferences preference,
+ @Nullable final IOnCompleteListener listener) {
+ Objects.requireNonNull(preference, "OemNetworkPreferences must be non-null");
+ if (DBG) {
+ log("set OEM network preferences :" + preference.toString());
+ }
+ // setProfileNetworkPreference and setOemNetworkPreference are mutually exclusive, in
+ // particular because it's not clear what preference should win in case both apply
+ // to the same app.
+ // The binder call has already checked this, but as mOemNetworkPreferences is only
+ // touched on the handler thread, it's theoretically not impossible that it has changed
+ // since.
+ if (!mProfileNetworkPreferences.isEmpty()) {
+ logwtf("handleSetOemPreference, but per-profile network preferences not empty");
+ return;
+ }
+
+ mOemNetworkPreferencesLogs.log("UPDATE INITIATED: " + preference);
+ final int uniquePreferenceCount = new ArraySet<>(
+ preference.getNetworkPreferences().values()).size();
+ mSystemNetworkRequestCounter.transact(
+ mDeps.getCallingUid(), uniquePreferenceCount,
+ () -> {
+ final ArraySet<NetworkRequestInfo> nris =
+ new OemNetworkRequestFactory()
+ .createNrisFromOemNetworkPreferences(preference);
+ replaceDefaultNetworkRequestsForPreference(nris);
+ });
+ mOemNetworkPreferences = preference;
+
+ if (null != listener) {
+ try {
+ listener.onComplete();
+ } catch (RemoteException e) {
+ loge("Can't send onComplete in handleSetOemNetworkPreference", e);
+ }
+ }
+ }
+
+ private void replaceDefaultNetworkRequestsForPreference(
+ @NonNull final Set<NetworkRequestInfo> nris) {
+ // Pass in a defensive copy as this collection will be updated on remove.
+ handleRemoveNetworkRequests(new ArraySet<>(mDefaultNetworkRequests));
+ addPerAppDefaultNetworkRequests(nris);
+ }
+
+ private void addPerAppDefaultNetworkRequests(@NonNull final Set<NetworkRequestInfo> nris) {
+ ensureRunningOnConnectivityServiceThread();
+ mDefaultNetworkRequests.addAll(nris);
+ final ArraySet<NetworkRequestInfo> perAppCallbackRequestsToUpdate =
+ getPerAppCallbackRequestsToUpdate();
+ final ArraySet<NetworkRequestInfo> nrisToRegister = new ArraySet<>(nris);
+ mSystemNetworkRequestCounter.transact(
+ mDeps.getCallingUid(), perAppCallbackRequestsToUpdate.size(),
+ () -> {
+ nrisToRegister.addAll(
+ createPerAppCallbackRequestsToRegister(perAppCallbackRequestsToUpdate));
+ handleRemoveNetworkRequests(perAppCallbackRequestsToUpdate);
+ handleRegisterNetworkRequests(nrisToRegister);
+ });
+ }
+
+ /**
+ * All current requests that are tracking the default network need to be assessed as to whether
+ * or not the current set of per-application default requests will be changing their default
+ * network. If so, those requests will need to be updated so that they will send callbacks for
+ * default network changes at the appropriate time. Additionally, those requests tracking the
+ * default that were previously updated by this flow will need to be reassessed.
+ * @return the nris which will need to be updated.
+ */
+ private ArraySet<NetworkRequestInfo> getPerAppCallbackRequestsToUpdate() {
+ final ArraySet<NetworkRequestInfo> defaultCallbackRequests = new ArraySet<>();
+ // Get the distinct nris to check since for multilayer requests, it is possible to have the
+ // same nri in the map's values for each of its NetworkRequest objects.
+ final ArraySet<NetworkRequestInfo> nris = new ArraySet<>(mNetworkRequests.values());
+ for (final NetworkRequestInfo nri : nris) {
+ // Include this nri if it is currently being tracked.
+ if (isPerAppTrackedNri(nri)) {
+ defaultCallbackRequests.add(nri);
+ continue;
+ }
+ // We only track callbacks for requests tracking the default.
+ if (NetworkRequest.Type.TRACK_DEFAULT != nri.mRequests.get(0).type) {
+ continue;
+ }
+ // Include this nri if it will be tracked by the new per-app default requests.
+ final boolean isNriGoingToBeTracked =
+ getDefaultRequestTrackingUid(nri.mAsUid) != mDefaultRequest;
+ if (isNriGoingToBeTracked) {
+ defaultCallbackRequests.add(nri);
+ }
+ }
+ return defaultCallbackRequests;
+ }
+
+ /**
+ * Create nris for those network requests that are currently tracking the default network that
+ * are being controlled by a per-application default.
+ * @param perAppCallbackRequestsForUpdate the baseline network requests to be used as the
+ * foundation when creating the nri. Important items include the calling uid's original
+ * NetworkRequest to be used when mapping callbacks as well as the caller's uid and name. These
+ * requests are assumed to have already been validated as needing to be updated.
+ * @return the Set of nris to use when registering network requests.
+ */
+ private ArraySet<NetworkRequestInfo> createPerAppCallbackRequestsToRegister(
+ @NonNull final ArraySet<NetworkRequestInfo> perAppCallbackRequestsForUpdate) {
+ final ArraySet<NetworkRequestInfo> callbackRequestsToRegister = new ArraySet<>();
+ for (final NetworkRequestInfo callbackRequest : perAppCallbackRequestsForUpdate) {
+ final NetworkRequestInfo trackingNri =
+ getDefaultRequestTrackingUid(callbackRequest.mAsUid);
+
+ // If this nri is not being tracked, the change it back to an untracked nri.
+ if (trackingNri == mDefaultRequest) {
+ callbackRequestsToRegister.add(new NetworkRequestInfo(
+ callbackRequest,
+ Collections.singletonList(callbackRequest.getNetworkRequestForCallback())));
+ continue;
+ }
+
+ final NetworkRequest request = callbackRequest.mRequests.get(0);
+ callbackRequestsToRegister.add(new NetworkRequestInfo(
+ callbackRequest,
+ copyNetworkRequestsForUid(
+ trackingNri.mRequests, callbackRequest.mAsUid,
+ callbackRequest.mUid, request.getRequestorPackageName())));
+ }
+ return callbackRequestsToRegister;
+ }
+
+ private static void setNetworkRequestUids(@NonNull final List<NetworkRequest> requests,
+ @NonNull final Set<UidRange> uids) {
+ for (final NetworkRequest req : requests) {
+ req.networkCapabilities.setUids(UidRange.toIntRanges(uids));
+ }
+ }
+
+ /**
+ * Class used to generate {@link NetworkRequestInfo} based off of {@link OemNetworkPreferences}.
+ */
+ @VisibleForTesting
+ final class OemNetworkRequestFactory {
+ ArraySet<NetworkRequestInfo> createNrisFromOemNetworkPreferences(
+ @NonNull final OemNetworkPreferences preference) {
+ final ArraySet<NetworkRequestInfo> nris = new ArraySet<>();
+ final SparseArray<Set<Integer>> uids =
+ createUidsFromOemNetworkPreferences(preference);
+ for (int i = 0; i < uids.size(); i++) {
+ final int key = uids.keyAt(i);
+ final Set<Integer> value = uids.valueAt(i);
+ final NetworkRequestInfo nri = createNriFromOemNetworkPreferences(key, value);
+ // No need to add an nri without any requests.
+ if (0 == nri.mRequests.size()) {
+ continue;
+ }
+ nris.add(nri);
+ }
+
+ return nris;
+ }
+
+ private SparseArray<Set<Integer>> createUidsFromOemNetworkPreferences(
+ @NonNull final OemNetworkPreferences preference) {
+ final SparseArray<Set<Integer>> uids = new SparseArray<>();
+ final PackageManager pm = mContext.getPackageManager();
+ final List<UserHandle> users =
+ mContext.getSystemService(UserManager.class).getUserHandles(true);
+ if (null == users || users.size() == 0) {
+ if (VDBG || DDBG) {
+ log("No users currently available for setting the OEM network preference.");
+ }
+ return uids;
+ }
+ for (final Map.Entry<String, Integer> entry :
+ preference.getNetworkPreferences().entrySet()) {
+ @OemNetworkPreferences.OemNetworkPreference final int pref = entry.getValue();
+ try {
+ final int uid = pm.getApplicationInfo(entry.getKey(), 0).uid;
+ if (!uids.contains(pref)) {
+ uids.put(pref, new ArraySet<>());
+ }
+ for (final UserHandle ui : users) {
+ // Add the rules for all users as this policy is device wide.
+ uids.get(pref).add(ui.getUid(uid));
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // Although this may seem like an error scenario, it is ok that uninstalled
+ // packages are sent on a network preference as the system will watch for
+ // package installations associated with this network preference and update
+ // accordingly. This is done so as to minimize race conditions on app install.
+ continue;
+ }
+ }
+ return uids;
+ }
+
+ private NetworkRequestInfo createNriFromOemNetworkPreferences(
+ @OemNetworkPreferences.OemNetworkPreference final int preference,
+ @NonNull final Set<Integer> uids) {
+ final List<NetworkRequest> requests = new ArrayList<>();
+ // Requests will ultimately be evaluated by order of insertion therefore it matters.
+ switch (preference) {
+ case OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID:
+ requests.add(createUnmeteredNetworkRequest());
+ requests.add(createOemPaidNetworkRequest());
+ requests.add(createDefaultInternetRequestForTransport(
+ TYPE_NONE, NetworkRequest.Type.TRACK_DEFAULT));
+ break;
+ case OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_NO_FALLBACK:
+ requests.add(createUnmeteredNetworkRequest());
+ requests.add(createOemPaidNetworkRequest());
+ break;
+ case OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PAID_ONLY:
+ requests.add(createOemPaidNetworkRequest());
+ break;
+ case OemNetworkPreferences.OEM_NETWORK_PREFERENCE_OEM_PRIVATE_ONLY:
+ requests.add(createOemPrivateNetworkRequest());
+ break;
+ default:
+ // This should never happen.
+ throw new IllegalArgumentException("createNriFromOemNetworkPreferences()"
+ + " called with invalid preference of " + preference);
+ }
+
+ final ArraySet ranges = new ArraySet<Integer>();
+ for (final int uid : uids) {
+ ranges.add(new UidRange(uid, uid));
+ }
+ setNetworkRequestUids(requests, ranges);
+ return new NetworkRequestInfo(Process.myUid(), requests);
+ }
+
+ private NetworkRequest createUnmeteredNetworkRequest() {
+ final NetworkCapabilities netcap = createDefaultPerAppNetCap()
+ .addCapability(NET_CAPABILITY_NOT_METERED)
+ .addCapability(NET_CAPABILITY_VALIDATED);
+ return createNetworkRequest(NetworkRequest.Type.LISTEN, netcap);
+ }
+
+ private NetworkRequest createOemPaidNetworkRequest() {
+ // NET_CAPABILITY_OEM_PAID is a restricted capability.
+ final NetworkCapabilities netcap = createDefaultPerAppNetCap()
+ .addCapability(NET_CAPABILITY_OEM_PAID)
+ .removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+ return createNetworkRequest(NetworkRequest.Type.REQUEST, netcap);
+ }
+
+ private NetworkRequest createOemPrivateNetworkRequest() {
+ // NET_CAPABILITY_OEM_PRIVATE is a restricted capability.
+ final NetworkCapabilities netcap = createDefaultPerAppNetCap()
+ .addCapability(NET_CAPABILITY_OEM_PRIVATE)
+ .removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+ return createNetworkRequest(NetworkRequest.Type.REQUEST, netcap);
+ }
+
+ private NetworkCapabilities createDefaultPerAppNetCap() {
+ final NetworkCapabilities netCap = new NetworkCapabilities();
+ netCap.addCapability(NET_CAPABILITY_INTERNET);
+ netCap.setRequestorUidAndPackageName(Process.myUid(), mContext.getPackageName());
+ return netCap;
+ }
+ }
+}
diff --git a/service/src/com/android/server/ConnectivityServiceInitializer.java b/service/src/com/android/server/ConnectivityServiceInitializer.java
new file mode 100644
index 0000000..2465479
--- /dev/null
+++ b/service/src/com/android/server/ConnectivityServiceInitializer.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.util.Log;
+
+/**
+ * Connectivity service initializer for core networking. This is called by system server to create
+ * a new instance of ConnectivityService.
+ */
+public final class ConnectivityServiceInitializer extends SystemService {
+ private static final String TAG = ConnectivityServiceInitializer.class.getSimpleName();
+ private final ConnectivityService mConnectivity;
+
+ public ConnectivityServiceInitializer(Context context) {
+ super(context);
+ // Load JNI libraries used by ConnectivityService and its dependencies
+ System.loadLibrary("service-connectivity");
+ // TODO: Define formal APIs to get the needed services.
+ mConnectivity = new ConnectivityService(context);
+ }
+
+ @Override
+ public void onStart() {
+ Log.i(TAG, "Registering " + Context.CONNECTIVITY_SERVICE);
+ publishBinderService(Context.CONNECTIVITY_SERVICE, mConnectivity,
+ /* allowIsolated= */ false);
+ }
+}
diff --git a/service/src/com/android/server/NetIdManager.java b/service/src/com/android/server/NetIdManager.java
new file mode 100644
index 0000000..61925c8
--- /dev/null
+++ b/service/src/com/android/server/NetIdManager.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2019 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;
+
+import android.annotation.NonNull;
+import android.net.ConnectivityManager;
+import android.util.SparseBooleanArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Class used to reserve and release net IDs.
+ *
+ * <p>Instances of this class are thread-safe.
+ */
+public class NetIdManager {
+ // Sequence number for Networks; keep in sync with system/netd/NetworkController.cpp
+ public static final int MIN_NET_ID = 100; // some reserved marks
+ // Top IDs reserved by IpSecService
+ public static final int MAX_NET_ID = ConnectivityManager.getIpSecNetIdRange().getLower() - 1;
+
+ @GuardedBy("mNetIdInUse")
+ private final SparseBooleanArray mNetIdInUse = new SparseBooleanArray();
+
+ @GuardedBy("mNetIdInUse")
+ private int mLastNetId = MIN_NET_ID - 1;
+
+ private final int mMaxNetId;
+
+ public NetIdManager() {
+ this(MAX_NET_ID);
+ }
+
+ @VisibleForTesting
+ NetIdManager(int maxNetId) {
+ mMaxNetId = maxNetId;
+ }
+
+ /**
+ * Get the first netId that follows the provided lastId and is available.
+ */
+ private int getNextAvailableNetIdLocked(
+ int lastId, @NonNull SparseBooleanArray netIdInUse) {
+ int netId = lastId;
+ for (int i = MIN_NET_ID; i <= mMaxNetId; i++) {
+ netId = netId < mMaxNetId ? netId + 1 : MIN_NET_ID;
+ if (!netIdInUse.get(netId)) {
+ return netId;
+ }
+ }
+ throw new IllegalStateException("No free netIds");
+ }
+
+ /**
+ * Reserve a new ID for a network.
+ */
+ public int reserveNetId() {
+ synchronized (mNetIdInUse) {
+ mLastNetId = getNextAvailableNetIdLocked(mLastNetId, mNetIdInUse);
+ // Make sure NetID unused. http://b/16815182
+ mNetIdInUse.put(mLastNetId, true);
+ return mLastNetId;
+ }
+ }
+
+ /**
+ * Clear a previously reserved ID for a network.
+ */
+ public void releaseNetId(int id) {
+ synchronized (mNetIdInUse) {
+ mNetIdInUse.delete(id);
+ }
+ }
+}
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
new file mode 100644
index 0000000..f566277
--- /dev/null
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright (C) 2018 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;
+
+import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
+import static android.net.TestNetworkManager.TEST_TUN_PREFIX;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.INetd;
+import android.net.ITestNetworkManager;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.RouteInfo;
+import android.net.TestNetworkInterface;
+import android.net.TestNetworkSpecifier;
+import android.net.util.NetdService;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.NetdUtils;
+import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.PermissionUtils;
+
+import java.io.UncheckedIOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** @hide */
+class TestNetworkService extends ITestNetworkManager.Stub {
+ @NonNull private static final String TEST_NETWORK_LOGTAG = "TestNetworkAgent";
+ @NonNull private static final String TEST_NETWORK_PROVIDER_NAME = "TestNetworkProvider";
+ @NonNull private static final AtomicInteger sTestTunIndex = new AtomicInteger();
+
+ @NonNull private final Context mContext;
+ @NonNull private final INetd mNetd;
+
+ @NonNull private final HandlerThread mHandlerThread;
+ @NonNull private final Handler mHandler;
+
+ @NonNull private final ConnectivityManager mCm;
+ @NonNull private final NetworkProvider mNetworkProvider;
+
+ // Native method stubs
+ private static native int jniCreateTunTap(boolean isTun, @NonNull String iface);
+
+ @VisibleForTesting
+ protected TestNetworkService(@NonNull Context context) {
+ mHandlerThread = new HandlerThread("TestNetworkServiceThread");
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+
+ mContext = Objects.requireNonNull(context, "missing Context");
+ mNetd = Objects.requireNonNull(NetdService.getInstance(), "could not get netd instance");
+ mCm = mContext.getSystemService(ConnectivityManager.class);
+ mNetworkProvider = new NetworkProvider(mContext, mHandler.getLooper(),
+ TEST_NETWORK_PROVIDER_NAME);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mCm.registerNetworkProvider(mNetworkProvider);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ /**
+ * Create a TUN or TAP interface with the given interface name and link addresses
+ *
+ * <p>This method will return the FileDescriptor to the interface. Close it to tear down the
+ * interface.
+ */
+ private TestNetworkInterface createInterface(boolean isTun, LinkAddress[] linkAddrs) {
+ enforceTestNetworkPermissions(mContext);
+
+ Objects.requireNonNull(linkAddrs, "missing linkAddrs");
+
+ String ifacePrefix = isTun ? TEST_TUN_PREFIX : TEST_TAP_PREFIX;
+ String iface = ifacePrefix + sTestTunIndex.getAndIncrement();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ ParcelFileDescriptor tunIntf =
+ ParcelFileDescriptor.adoptFd(jniCreateTunTap(isTun, iface));
+ for (LinkAddress addr : linkAddrs) {
+ mNetd.interfaceAddAddress(
+ iface,
+ addr.getAddress().getHostAddress(),
+ addr.getPrefixLength());
+ }
+
+ return new TestNetworkInterface(tunIntf, iface);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ /**
+ * Create a TUN interface with the given interface name and link addresses
+ *
+ * <p>This method will return the FileDescriptor to the TUN interface. Close it to tear down the
+ * TUN interface.
+ */
+ @Override
+ public TestNetworkInterface createTunInterface(@NonNull LinkAddress[] linkAddrs) {
+ return createInterface(true, linkAddrs);
+ }
+
+ /**
+ * Create a TAP interface with the given interface name
+ *
+ * <p>This method will return the FileDescriptor to the TAP interface. Close it to tear down the
+ * TAP interface.
+ */
+ @Override
+ public TestNetworkInterface createTapInterface() {
+ return createInterface(false, new LinkAddress[0]);
+ }
+
+ // Tracker for TestNetworkAgents
+ @GuardedBy("mTestNetworkTracker")
+ @NonNull
+ private final SparseArray<TestNetworkAgent> mTestNetworkTracker = new SparseArray<>();
+
+ public class TestNetworkAgent extends NetworkAgent implements IBinder.DeathRecipient {
+ private static final int NETWORK_SCORE = 1; // Use a low, non-zero score.
+
+ private final int mUid;
+
+ @GuardedBy("mBinderLock")
+ @NonNull
+ private IBinder mBinder;
+
+ @NonNull private final Object mBinderLock = new Object();
+
+ private TestNetworkAgent(
+ @NonNull Context context,
+ @NonNull Looper looper,
+ @NonNull NetworkCapabilities nc,
+ @NonNull LinkProperties lp,
+ @NonNull NetworkAgentConfig config,
+ int uid,
+ @NonNull IBinder binder,
+ @NonNull NetworkProvider np)
+ throws RemoteException {
+ super(context, looper, TEST_NETWORK_LOGTAG, nc, lp, NETWORK_SCORE, config, np);
+ mUid = uid;
+ synchronized (mBinderLock) {
+ mBinder = binder; // Binder null-checks in create()
+
+ try {
+ mBinder.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ binderDied();
+ throw e; // Abort, signal failure up the stack.
+ }
+ }
+ }
+
+ /**
+ * If the Binder object dies, this function is called to free the resources of this
+ * TestNetworkAgent
+ */
+ @Override
+ public void binderDied() {
+ teardown();
+ }
+
+ @Override
+ protected void unwanted() {
+ teardown();
+ }
+
+ private void teardown() {
+ unregister();
+
+ // Synchronize on mBinderLock to ensure that unlinkToDeath is never called more than
+ // once (otherwise it could throw an exception)
+ synchronized (mBinderLock) {
+ // If mBinder is null, this Test Network has already been cleaned up.
+ if (mBinder == null) return;
+ mBinder.unlinkToDeath(this, 0);
+ mBinder = null;
+ }
+
+ // Has to be in TestNetworkAgent to ensure all teardown codepaths properly clean up
+ // resources, even for binder death or unwanted calls.
+ synchronized (mTestNetworkTracker) {
+ mTestNetworkTracker.remove(getNetwork().getNetId());
+ }
+ }
+ }
+
+ private TestNetworkAgent registerTestNetworkAgent(
+ @NonNull Looper looper,
+ @NonNull Context context,
+ @NonNull String iface,
+ @Nullable LinkProperties lp,
+ boolean isMetered,
+ int callingUid,
+ @NonNull int[] administratorUids,
+ @NonNull IBinder binder)
+ throws RemoteException, SocketException {
+ Objects.requireNonNull(looper, "missing Looper");
+ Objects.requireNonNull(context, "missing Context");
+ // iface and binder validity checked by caller
+
+ // Build narrow set of NetworkCapabilities, useful only for testing
+ NetworkCapabilities nc = new NetworkCapabilities();
+ nc.clearAll(); // Remove default capabilities.
+ nc.addTransportType(NetworkCapabilities.TRANSPORT_TEST);
+ nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED);
+ nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
+ nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+ nc.setNetworkSpecifier(new TestNetworkSpecifier(iface));
+ nc.setAdministratorUids(administratorUids);
+ if (!isMetered) {
+ nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+ }
+
+ // Build LinkProperties
+ if (lp == null) {
+ lp = new LinkProperties();
+ } else {
+ lp = new LinkProperties(lp);
+ // Use LinkAddress(es) from the interface itself to minimize how much the caller
+ // is trusted.
+ lp.setLinkAddresses(new ArrayList<>());
+ }
+ lp.setInterfaceName(iface);
+
+ // Find the currently assigned addresses, and add them to LinkProperties
+ boolean allowIPv4 = false, allowIPv6 = false;
+ NetworkInterface netIntf = NetworkInterface.getByName(iface);
+ Objects.requireNonNull(netIntf, "No such network interface found: " + netIntf);
+
+ for (InterfaceAddress intfAddr : netIntf.getInterfaceAddresses()) {
+ lp.addLinkAddress(
+ new LinkAddress(intfAddr.getAddress(), intfAddr.getNetworkPrefixLength()));
+
+ if (intfAddr.getAddress() instanceof Inet6Address) {
+ allowIPv6 |= !intfAddr.getAddress().isLinkLocalAddress();
+ } else if (intfAddr.getAddress() instanceof Inet4Address) {
+ allowIPv4 = true;
+ }
+ }
+
+ // Add global routes (but as non-default, non-internet providing network)
+ if (allowIPv4) {
+ lp.addRoute(new RouteInfo(new IpPrefix(
+ NetworkStackConstants.IPV4_ADDR_ANY, 0), null, iface));
+ }
+ if (allowIPv6) {
+ lp.addRoute(new RouteInfo(new IpPrefix(
+ NetworkStackConstants.IPV6_ADDR_ANY, 0), null, iface));
+ }
+
+ final TestNetworkAgent agent = new TestNetworkAgent(context, looper, nc, lp,
+ new NetworkAgentConfig.Builder().build(), callingUid, binder,
+ mNetworkProvider);
+ agent.register();
+ agent.markConnected();
+ return agent;
+ }
+
+ /**
+ * Sets up a Network with extremely limited privileges, guarded by the MANAGE_TEST_NETWORKS
+ * permission.
+ *
+ * <p>This method provides a Network that is useful only for testing.
+ */
+ @Override
+ public void setupTestNetwork(
+ @NonNull String iface,
+ @Nullable LinkProperties lp,
+ boolean isMetered,
+ @NonNull int[] administratorUids,
+ @NonNull IBinder binder) {
+ enforceTestNetworkPermissions(mContext);
+
+ Objects.requireNonNull(iface, "missing Iface");
+ Objects.requireNonNull(binder, "missing IBinder");
+
+ if (!(iface.startsWith(INetd.IPSEC_INTERFACE_PREFIX)
+ || iface.startsWith(TEST_TUN_PREFIX))) {
+ throw new IllegalArgumentException(
+ "Cannot create network for non ipsec, non-testtun interface");
+ }
+
+ try {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ PermissionUtils.enforceNetworkStackPermission(mContext);
+ NetdUtils.setInterfaceUp(mNetd, iface);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+
+ // Synchronize all accesses to mTestNetworkTracker to prevent the case where:
+ // 1. TestNetworkAgent successfully binds to death of binder
+ // 2. Before it is added to the mTestNetworkTracker, binder dies, binderDied() is called
+ // (on a different thread)
+ // 3. This thread is pre-empted, put() is called after remove()
+ synchronized (mTestNetworkTracker) {
+ TestNetworkAgent agent =
+ registerTestNetworkAgent(
+ mHandler.getLooper(),
+ mContext,
+ iface,
+ lp,
+ isMetered,
+ Binder.getCallingUid(),
+ administratorUids,
+ binder);
+
+ mTestNetworkTracker.put(agent.getNetwork().getNetId(), agent);
+ }
+ } catch (SocketException e) {
+ throw new UncheckedIOException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** Teardown a test network */
+ @Override
+ public void teardownTestNetwork(int netId) {
+ enforceTestNetworkPermissions(mContext);
+
+ final TestNetworkAgent agent;
+ synchronized (mTestNetworkTracker) {
+ agent = mTestNetworkTracker.get(netId);
+ }
+
+ if (agent == null) {
+ return; // Already torn down
+ } else if (agent.mUid != Binder.getCallingUid()) {
+ throw new SecurityException("Attempted to modify other user's test networks");
+ }
+
+ // Safe to be called multiple times.
+ agent.teardown();
+ }
+
+ private static final String PERMISSION_NAME =
+ android.Manifest.permission.MANAGE_TEST_NETWORKS;
+
+ public static void enforceTestNetworkPermissions(@NonNull Context context) {
+ context.enforceCallingOrSelfPermission(PERMISSION_NAME, "TestNetworkService");
+ }
+}
diff --git a/service/src/com/android/server/connectivity/AutodestructReference.java b/service/src/com/android/server/connectivity/AutodestructReference.java
new file mode 100644
index 0000000..009a43e
--- /dev/null
+++ b/service/src/com/android/server/connectivity/AutodestructReference.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2019 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.connectivity;
+
+import android.annotation.NonNull;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A ref that autodestructs at the first usage of it.
+ * @param <T> The type of the held object
+ * @hide
+ */
+public class AutodestructReference<T> {
+ private final AtomicReference<T> mHeld;
+ public AutodestructReference(@NonNull T obj) {
+ if (null == obj) throw new NullPointerException("Autodestruct reference to null");
+ mHeld = new AtomicReference<>(obj);
+ }
+
+ /** Get the ref and destruct it. NPE if already destructed. */
+ @NonNull
+ public T getAndDestroy() {
+ final T obj = mHeld.getAndSet(null);
+ if (null == obj) throw new NullPointerException("Already autodestructed");
+ return obj;
+ }
+}
diff --git a/service/src/com/android/server/connectivity/ConnectivityConstants.java b/service/src/com/android/server/connectivity/ConnectivityConstants.java
new file mode 100644
index 0000000..325a2cd
--- /dev/null
+++ b/service/src/com/android/server/connectivity/ConnectivityConstants.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018 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.connectivity;
+
+/**
+ * A class encapsulating various constants used by Connectivity.
+ * TODO : remove this class.
+ * @hide
+ */
+public class ConnectivityConstants {
+ // VPNs typically have priority over other networks. Give them a score that will
+ // let them win every single time.
+ public static final int VPN_DEFAULT_SCORE = 101;
+}
diff --git a/service/src/com/android/server/connectivity/DnsManager.java b/service/src/com/android/server/connectivity/DnsManager.java
new file mode 100644
index 0000000..05b12ba
--- /dev/null
+++ b/service/src/com/android/server/connectivity/DnsManager.java
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) 2018 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.connectivity;
+
+import static android.net.ConnectivitySettingsManager.DNS_RESOLVER_MAX_SAMPLES;
+import static android.net.ConnectivitySettingsManager.DNS_RESOLVER_MIN_SAMPLES;
+import static android.net.ConnectivitySettingsManager.DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS;
+import static android.net.ConnectivitySettingsManager.DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_DEFAULT_MODE;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_SPECIFIER;
+import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_FAILURE;
+import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS;
+
+import android.annotation.NonNull;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.ConnectivitySettingsManager;
+import android.net.IDnsResolver;
+import android.net.InetAddresses;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.ResolverOptionsParcel;
+import android.net.ResolverParamsParcel;
+import android.net.Uri;
+import android.net.shared.PrivateDnsConfig;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import java.net.InetAddress;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * 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. 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
+ * seems like the least bad place to collect all the observations.
+ *
+ * Private DNS handling and updating occurs in response to several different
+ * events. Each is described here with its corresponding intended handling.
+ *
+ * [A] Event: A new network comes up.
+ * Mechanics:
+ * [1] ConnectivityService gets notifications from NetworkAgents.
+ * [2] in updateNetworkInfo(), the first time the NetworkAgent goes into
+ * into CONNECTED state, the Private DNS configuration is retrieved,
+ * programmed, and strict mode hostname resolution (if applicable) is
+ * enqueued in NetworkAgent's NetworkMonitor, via a call to
+ * handlePerNetworkPrivateDnsConfig().
+ * [3] Re-resolution of strict mode hostnames that fail to return any
+ * IP addresses happens inside NetworkMonitor; it sends itself a
+ * delayed CMD_EVALUATE_PRIVATE_DNS message in a simple backoff
+ * schedule.
+ * [4] Successfully resolved hostnames are sent to ConnectivityService
+ * inside an EVENT_PRIVATE_DNS_CONFIG_RESOLVED message. The resolved
+ * IP addresses are programmed into netd via:
+ *
+ * updatePrivateDns() -> updateDnses()
+ *
+ * both of which make calls into DnsManager.
+ * [5] Upon a successful hostname resolution NetworkMonitor initiates a
+ * validation attempt in the form of a lookup for a one-time hostname
+ * that uses Private DNS.
+ *
+ * [B] Event: Private DNS settings are changed.
+ * Mechanics:
+ * [1] ConnectivityService gets notifications from its SettingsObserver.
+ * [2] handlePrivateDnsSettingsChanged() is called, which calls
+ * handlePerNetworkPrivateDnsConfig() and the process proceeds
+ * as if from A.3 above.
+ *
+ * [C] Event: An application calls ConnectivityManager#reportBadNetwork().
+ * Mechanics:
+ * [1] NetworkMonitor is notified and initiates a reevaluation, which
+ * always bypasses Private DNS.
+ * [2] Once completed, NetworkMonitor checks if strict mode is in operation
+ * and if so enqueues another evaluation of Private DNS, as if from
+ * step A.5 above.
+ *
+ * @hide
+ */
+public class DnsManager {
+ private static final String TAG = DnsManager.class.getSimpleName();
+ private static final PrivateDnsConfig PRIVATE_DNS_OFF = new PrivateDnsConfig();
+
+ /* Defaults for resolver parameters. */
+ private static final int DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS = 1800;
+ private static final int DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT = 25;
+ private static final int DNS_RESOLVER_DEFAULT_MIN_SAMPLES = 8;
+ private static final int DNS_RESOLVER_DEFAULT_MAX_SAMPLES = 64;
+
+ /**
+ * Get PrivateDnsConfig.
+ */
+ public static PrivateDnsConfig getPrivateDnsConfig(Context context) {
+ final int mode = ConnectivitySettingsManager.getPrivateDnsMode(context);
+
+ final boolean useTls = mode != PRIVATE_DNS_MODE_OFF;
+
+ if (PRIVATE_DNS_MODE_PROVIDER_HOSTNAME == mode) {
+ final String specifier = getStringSetting(context.getContentResolver(),
+ PRIVATE_DNS_SPECIFIER);
+ return new PrivateDnsConfig(specifier, null);
+ }
+
+ return new PrivateDnsConfig(useTls);
+ }
+
+ public static Uri[] getPrivateDnsSettingsUris() {
+ return new Uri[]{
+ Settings.Global.getUriFor(PRIVATE_DNS_DEFAULT_MODE),
+ Settings.Global.getUriFor(PRIVATE_DNS_MODE),
+ Settings.Global.getUriFor(PRIVATE_DNS_SPECIFIER),
+ };
+ }
+
+ public static class PrivateDnsValidationUpdate {
+ public final int netId;
+ public final InetAddress ipAddress;
+ public final String hostname;
+ // Refer to IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_*.
+ public final int validationResult;
+
+ public PrivateDnsValidationUpdate(int netId, InetAddress ipAddress,
+ String hostname, int validationResult) {
+ this.netId = netId;
+ this.ipAddress = ipAddress;
+ this.hostname = hostname;
+ this.validationResult = validationResult;
+ }
+ }
+
+ private static class PrivateDnsValidationStatuses {
+ enum ValidationStatus {
+ IN_PROGRESS,
+ FAILED,
+ SUCCEEDED
+ }
+
+ // Validation statuses of <hostname, ipAddress> pairs for a single netId
+ // Caution : not thread-safe. As mentioned in the top file comment, all
+ // methods of this class must only be called on ConnectivityService's thread.
+ private Map<Pair<String, InetAddress>, ValidationStatus> mValidationMap;
+
+ private PrivateDnsValidationStatuses() {
+ mValidationMap = new HashMap<>();
+ }
+
+ private boolean hasValidatedServer() {
+ for (ValidationStatus status : mValidationMap.values()) {
+ if (status == ValidationStatus.SUCCEEDED) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void updateTrackedDnses(String[] ipAddresses, String hostname) {
+ Set<Pair<String, InetAddress>> latestDnses = new HashSet<>();
+ for (String ipAddress : ipAddresses) {
+ try {
+ latestDnses.add(new Pair(hostname,
+ InetAddresses.parseNumericAddress(ipAddress)));
+ } catch (IllegalArgumentException e) {}
+ }
+ // Remove <hostname, ipAddress> pairs that should not be tracked.
+ for (Iterator<Map.Entry<Pair<String, InetAddress>, ValidationStatus>> it =
+ mValidationMap.entrySet().iterator(); it.hasNext(); ) {
+ Map.Entry<Pair<String, InetAddress>, ValidationStatus> entry = it.next();
+ if (!latestDnses.contains(entry.getKey())) {
+ it.remove();
+ }
+ }
+ // Add new <hostname, ipAddress> pairs that should be tracked.
+ for (Pair<String, InetAddress> p : latestDnses) {
+ if (!mValidationMap.containsKey(p)) {
+ mValidationMap.put(p, ValidationStatus.IN_PROGRESS);
+ }
+ }
+ }
+
+ private void updateStatus(PrivateDnsValidationUpdate update) {
+ Pair<String, InetAddress> p = new Pair(update.hostname,
+ update.ipAddress);
+ if (!mValidationMap.containsKey(p)) {
+ return;
+ }
+ if (update.validationResult == VALIDATION_RESULT_SUCCESS) {
+ mValidationMap.put(p, ValidationStatus.SUCCEEDED);
+ } else if (update.validationResult == VALIDATION_RESULT_FAILURE) {
+ mValidationMap.put(p, ValidationStatus.FAILED);
+ } else {
+ Log.e(TAG, "Unknown private dns validation operation="
+ + update.validationResult);
+ }
+ }
+
+ private LinkProperties fillInValidatedPrivateDns(LinkProperties lp) {
+ lp.setValidatedPrivateDnsServers(Collections.EMPTY_LIST);
+ mValidationMap.forEach((key, value) -> {
+ if (value == ValidationStatus.SUCCEEDED) {
+ lp.addValidatedPrivateDnsServer(key.second);
+ }
+ });
+ return lp;
+ }
+ }
+
+ private final Context mContext;
+ private final ContentResolver mContentResolver;
+ private final IDnsResolver mDnsResolver;
+ 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;
+
+ private int mSampleValidity;
+ private int mSuccessThreshold;
+ private int mMinSamples;
+ private int mMaxSamples;
+
+ public DnsManager(Context ctx, IDnsResolver dnsResolver) {
+ mContext = ctx;
+ mContentResolver = mContext.getContentResolver();
+ mDnsResolver = dnsResolver;
+ mPrivateDnsMap = new ConcurrentHashMap<>();
+ mPrivateDnsValidationMap = new HashMap<>();
+ mLinkPropertiesMap = new HashMap<>();
+ mTransportsMap = new HashMap<>();
+
+ // TODO: Create and register ContentObservers to track every setting
+ // used herein, posting messages to respond to changes.
+ }
+
+ public PrivateDnsConfig getPrivateDnsConfig() {
+ return getPrivateDnsConfig(mContext);
+ }
+
+ public void removeNetwork(Network network) {
+ mPrivateDnsMap.remove(network.getNetId());
+ mPrivateDnsValidationMap.remove(network.getNetId());
+ mTransportsMap.remove(network.getNetId());
+ mLinkPropertiesMap.remove(network.getNetId());
+ }
+
+ // 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.getNetId(), PRIVATE_DNS_OFF);
+ }
+
+ public PrivateDnsConfig updatePrivateDns(Network network, PrivateDnsConfig cfg) {
+ Log.w(TAG, "updatePrivateDns(" + network + ", " + cfg + ")");
+ return (cfg != null)
+ ? mPrivateDnsMap.put(network.getNetId(), cfg)
+ : mPrivateDnsMap.remove(network.getNetId());
+ }
+
+ public void updatePrivateDnsStatus(int netId, LinkProperties lp) {
+ // Use the PrivateDnsConfig data pushed to this class instance
+ // from ConnectivityService.
+ final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId,
+ PRIVATE_DNS_OFF);
+
+ final boolean useTls = privateDnsCfg.useTls;
+ final PrivateDnsValidationStatuses statuses =
+ useTls ? mPrivateDnsValidationMap.get(netId) : null;
+ final boolean validated = (null != statuses) && statuses.hasValidatedServer();
+ final boolean strictMode = privateDnsCfg.inStrictMode();
+ final String tlsHostname = strictMode ? privateDnsCfg.hostname : null;
+ final boolean usingPrivateDns = strictMode || validated;
+
+ lp.setUsePrivateDns(usingPrivateDns);
+ lp.setPrivateDnsServerName(tlsHostname);
+ if (usingPrivateDns && null != statuses) {
+ statuses.fillInValidatedPrivateDns(lp);
+ } else {
+ lp.setValidatedPrivateDnsServers(Collections.EMPTY_LIST);
+ }
+ }
+
+ public void updatePrivateDnsValidation(PrivateDnsValidationUpdate update) {
+ final PrivateDnsValidationStatuses statuses = mPrivateDnsValidationMap.get(update.netId);
+ if (statuses == null) return;
+ statuses.updateStatus(update);
+ }
+
+ /**
+ * When creating a new network or transport types are changed in a specific network,
+ * transport types are always saved to a hashMap before update dns config.
+ * When destroying network, the specific network will be removed from the hashMap.
+ * The hashMap is always accessed on the same thread.
+ */
+ public void updateTransportsForNetwork(int netId, @NonNull int[] transportTypes) {
+ mTransportsMap.put(netId, transportTypes);
+ sendDnsConfigurationForNetwork(netId);
+ }
+
+ /**
+ * When {@link LinkProperties} are changed in a specific network, they are
+ * always saved to a hashMap before update dns config.
+ * When destroying network, the specific network will be removed from the hashMap.
+ * The hashMap is always accessed on the same thread.
+ */
+ public void noteDnsServersForNetwork(int netId, @NonNull LinkProperties lp) {
+ mLinkPropertiesMap.put(netId, lp);
+ sendDnsConfigurationForNetwork(netId);
+ }
+
+ /**
+ * Send dns configuration parameters to resolver for a given network.
+ */
+ public void sendDnsConfigurationForNetwork(int netId) {
+ final LinkProperties lp = mLinkPropertiesMap.get(netId);
+ final int[] transportTypes = mTransportsMap.get(netId);
+ if (lp == null || transportTypes == null) return;
+ updateParametersSettings();
+ final ResolverParamsParcel paramsParcel = new ResolverParamsParcel();
+
+ // We only use the PrivateDnsConfig data pushed to this class instance
+ // from ConnectivityService because it works in coordination with
+ // NetworkMonitor to decide which networks need validation and runs the
+ // blocking calls to resolve Private DNS strict mode hostnames.
+ //
+ // At this time we do not attempt to enable Private DNS on non-Internet
+ // networks like IMS.
+ final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId,
+ PRIVATE_DNS_OFF);
+ final boolean useTls = privateDnsCfg.useTls;
+ final boolean strictMode = privateDnsCfg.inStrictMode();
+
+ paramsParcel.netId = netId;
+ paramsParcel.sampleValiditySeconds = mSampleValidity;
+ paramsParcel.successThreshold = mSuccessThreshold;
+ paramsParcel.minSamples = mMinSamples;
+ paramsParcel.maxSamples = mMaxSamples;
+ paramsParcel.servers = makeStrings(lp.getDnsServers());
+ paramsParcel.domains = getDomainStrings(lp.getDomains());
+ paramsParcel.tlsName = strictMode ? privateDnsCfg.hostname : "";
+ paramsParcel.tlsServers =
+ strictMode ? makeStrings(
+ Arrays.stream(privateDnsCfg.ips)
+ .filter((ip) -> lp.isReachable(ip))
+ .collect(Collectors.toList()))
+ : useTls ? paramsParcel.servers // Opportunistic
+ : new String[0]; // Off
+ paramsParcel.resolverOptions = new ResolverOptionsParcel();
+ paramsParcel.transportTypes = transportTypes;
+ // Prepare to track the validation status of the DNS servers in the
+ // resolver config when private DNS is in opportunistic or strict mode.
+ if (useTls) {
+ if (!mPrivateDnsValidationMap.containsKey(netId)) {
+ mPrivateDnsValidationMap.put(netId, new PrivateDnsValidationStatuses());
+ }
+ mPrivateDnsValidationMap.get(netId).updateTrackedDnses(paramsParcel.tlsServers,
+ paramsParcel.tlsName);
+ } else {
+ mPrivateDnsValidationMap.remove(netId);
+ }
+
+ Log.d(TAG, String.format("sendDnsConfigurationForNetwork(%d, %s, %s, %d, %d, %d, %d, "
+ + "%d, %d, %s, %s)", paramsParcel.netId, Arrays.toString(paramsParcel.servers),
+ Arrays.toString(paramsParcel.domains), paramsParcel.sampleValiditySeconds,
+ paramsParcel.successThreshold, paramsParcel.minSamples,
+ paramsParcel.maxSamples, paramsParcel.baseTimeoutMsec,
+ paramsParcel.retryCount, paramsParcel.tlsName,
+ Arrays.toString(paramsParcel.tlsServers)));
+
+ try {
+ mDnsResolver.setResolverConfiguration(paramsParcel);
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Error setting DNS configuration: " + e);
+ return;
+ }
+ }
+
+ /**
+ * Flush DNS caches and events work before boot has completed.
+ */
+ public void flushVmDnsCache() {
+ /*
+ * Tell the VMs to toss their DNS caches
+ */
+ final Intent intent = new Intent(ConnectivityManager.ACTION_CLEAR_DNS_CACHE);
+ intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+ /*
+ * Connectivity events can happen before boot has completed ...
+ */
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private void updateParametersSettings() {
+ mSampleValidity = getIntSetting(
+ DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS,
+ DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS);
+ if (mSampleValidity < 0 || mSampleValidity > 65535) {
+ Log.w(TAG, "Invalid sampleValidity=" + mSampleValidity + ", using default="
+ + DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS);
+ mSampleValidity = DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS;
+ }
+
+ mSuccessThreshold = getIntSetting(
+ DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT,
+ DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT);
+ if (mSuccessThreshold < 0 || mSuccessThreshold > 100) {
+ Log.w(TAG, "Invalid successThreshold=" + mSuccessThreshold + ", using default="
+ + DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT);
+ mSuccessThreshold = DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT;
+ }
+
+ mMinSamples = getIntSetting(DNS_RESOLVER_MIN_SAMPLES, DNS_RESOLVER_DEFAULT_MIN_SAMPLES);
+ mMaxSamples = getIntSetting(DNS_RESOLVER_MAX_SAMPLES, DNS_RESOLVER_DEFAULT_MAX_SAMPLES);
+ if (mMinSamples < 0 || mMinSamples > mMaxSamples || mMaxSamples > 64) {
+ Log.w(TAG, "Invalid sample count (min, max)=(" + mMinSamples + ", " + mMaxSamples
+ + "), using default=(" + DNS_RESOLVER_DEFAULT_MIN_SAMPLES + ", "
+ + DNS_RESOLVER_DEFAULT_MAX_SAMPLES + ")");
+ mMinSamples = DNS_RESOLVER_DEFAULT_MIN_SAMPLES;
+ mMaxSamples = DNS_RESOLVER_DEFAULT_MAX_SAMPLES;
+ }
+ }
+
+ private int getIntSetting(String which, int dflt) {
+ return Settings.Global.getInt(mContentResolver, which, dflt);
+ }
+
+ /**
+ * Create a string array of host addresses from a collection of InetAddresses
+ *
+ * @param addrs a Collection of InetAddresses
+ * @return an array of Strings containing their host addresses
+ */
+ private String[] makeStrings(Collection<InetAddress> addrs) {
+ String[] result = new String[addrs.size()];
+ int i = 0;
+ for (InetAddress addr : addrs) {
+ result[i++] = addr.getHostAddress();
+ }
+ return result;
+ }
+
+ private static String getStringSetting(ContentResolver cr, String which) {
+ return Settings.Global.getString(cr, which);
+ }
+
+ private static String[] getDomainStrings(String domains) {
+ return (TextUtils.isEmpty(domains)) ? new String[0] : domains.split(" ");
+ }
+}
diff --git a/service/src/com/android/server/connectivity/FullScore.java b/service/src/com/android/server/connectivity/FullScore.java
new file mode 100644
index 0000000..9326d69
--- /dev/null
+++ b/service/src/com/android/server/connectivity/FullScore.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2021 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.connectivity;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkScore;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.StringJoiner;
+
+/**
+ * This class represents how desirable a network is.
+ *
+ * FullScore is very similar to NetworkScore, but it contains the bits that are managed
+ * by ConnectivityService. This provides static guarantee that all users must know whether
+ * they are handling a score that had the CS-managed bits set.
+ */
+public class FullScore {
+ // This will be removed soon. Do *NOT* depend on it for any new code that is not part of
+ // a migration.
+ private final int mLegacyInt;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"POLICY_"}, value = {
+ POLICY_IS_VALIDATED,
+ POLICY_IS_VPN,
+ POLICY_EVER_USER_SELECTED,
+ POLICY_ACCEPT_UNVALIDATED
+ })
+ public @interface Policy {
+ }
+
+ // Agent-managed policies are in NetworkScore. They start from 1.
+ // CS-managed policies, counting from 63 downward
+ // This network is validated. CS-managed because the source of truth is in NetworkCapabilities.
+ /** @hide */
+ public static final int POLICY_IS_VALIDATED = 63;
+
+ // This is a VPN and behaves as one for scoring purposes.
+ /** @hide */
+ public static final int POLICY_IS_VPN = 62;
+
+ // This network has been selected by the user manually from settings or a 3rd party app
+ // at least once. {@see NetworkAgentConfig#explicitlySelected}.
+ /** @hide */
+ public static final int POLICY_EVER_USER_SELECTED = 61;
+
+ // The user has indicated in UI that this network should be used even if it doesn't
+ // validate. {@see NetworkAgentConfig#acceptUnvalidated}.
+ /** @hide */
+ public static final int POLICY_ACCEPT_UNVALIDATED = 60;
+
+ // To help iterate when printing
+ @VisibleForTesting
+ static final int MIN_CS_MANAGED_POLICY = POLICY_ACCEPT_UNVALIDATED;
+ @VisibleForTesting
+ static final int MAX_CS_MANAGED_POLICY = POLICY_IS_VALIDATED;
+
+ @VisibleForTesting
+ static @NonNull String policyNameOf(final int policy) {
+ switch (policy) {
+ case POLICY_IS_VALIDATED: return "IS_VALIDATED";
+ case POLICY_IS_VPN: return "IS_VPN";
+ case POLICY_EVER_USER_SELECTED: return "EVER_USER_SELECTED";
+ case POLICY_ACCEPT_UNVALIDATED: return "ACCEPT_UNVALIDATED";
+ }
+ throw new IllegalArgumentException("Unknown policy : " + policy);
+ }
+
+ // Bitmask of all the policies applied to this score.
+ private final long mPolicies;
+
+ FullScore(final int legacyInt, final long policies) {
+ mLegacyInt = legacyInt;
+ mPolicies = policies;
+ }
+
+ /**
+ * Given a score supplied by the NetworkAgent and CS-managed objects, produce a full score.
+ *
+ * @param score the score supplied by the agent
+ * @param caps the NetworkCapabilities of the network
+ * @param config the NetworkAgentConfig of the network
+ * @return an FullScore that is appropriate to use for ranking.
+ */
+ public static FullScore fromNetworkScore(@NonNull final NetworkScore score,
+ @NonNull final NetworkCapabilities caps, @NonNull final NetworkAgentConfig config) {
+ return withPolicies(score.getLegacyInt(), caps.hasCapability(NET_CAPABILITY_VALIDATED),
+ caps.hasTransport(TRANSPORT_VPN),
+ config.explicitlySelected,
+ config.acceptUnvalidated);
+ }
+
+ /**
+ * Given a score supplied by the NetworkAgent, produce a prospective score for an offer.
+ *
+ * NetworkOffers have score filters that are compared to the scores of actual networks
+ * to see if they could possibly beat the current satisfier. Some things the agent can't
+ * know in advance ; a good example is the validation bit – some networks will validate,
+ * others won't. For comparison purposes, assume the best, so all possibly beneficial
+ * networks will be brought up.
+ *
+ * @param score the score supplied by the agent for this offer
+ * @param caps the capabilities supplied by the agent for this offer
+ * @return a FullScore appropriate for comparing to actual network's scores.
+ */
+ public static FullScore makeProspectiveScore(@NonNull final NetworkScore score,
+ @NonNull final NetworkCapabilities caps) {
+ // If the network offers Internet access, it may validate.
+ final boolean mayValidate = caps.hasCapability(NET_CAPABILITY_INTERNET);
+ // VPN transports are known in advance.
+ final boolean vpn = caps.hasTransport(TRANSPORT_VPN);
+ // The network hasn't been chosen by the user (yet, at least).
+ final boolean everUserSelected = false;
+ // Don't assume the user will accept unvalidated connectivity.
+ final boolean acceptUnvalidated = false;
+ return withPolicies(score.getLegacyInt(), mayValidate, vpn, everUserSelected,
+ acceptUnvalidated);
+ }
+
+ /**
+ * Return a new score given updated caps and config.
+ *
+ * @param caps the NetworkCapabilities of the network
+ * @param config the NetworkAgentConfig of the network
+ * @return a score with the policies from the arguments reset
+ */
+ public FullScore mixInScore(@NonNull final NetworkCapabilities caps,
+ @NonNull final NetworkAgentConfig config) {
+ return withPolicies(mLegacyInt, caps.hasCapability(NET_CAPABILITY_VALIDATED),
+ caps.hasTransport(TRANSPORT_VPN),
+ config.explicitlySelected,
+ config.acceptUnvalidated);
+ }
+
+ private static FullScore withPolicies(@NonNull final int legacyInt,
+ final boolean isValidated,
+ final boolean isVpn,
+ final boolean everUserSelected,
+ final boolean acceptUnvalidated) {
+ return new FullScore(legacyInt,
+ (isValidated ? 1L << POLICY_IS_VALIDATED : 0)
+ | (isVpn ? 1L << POLICY_IS_VPN : 0)
+ | (everUserSelected ? 1L << POLICY_EVER_USER_SELECTED : 0)
+ | (acceptUnvalidated ? 1L << POLICY_ACCEPT_UNVALIDATED : 0));
+ }
+
+ /**
+ * For backward compatibility, get the legacy int.
+ * This will be removed before S is published.
+ */
+ public int getLegacyInt() {
+ return getLegacyInt(false /* pretendValidated */);
+ }
+
+ public int getLegacyIntAsValidated() {
+ return getLegacyInt(true /* pretendValidated */);
+ }
+
+ // TODO : remove these two constants
+ // Penalty applied to scores of Networks that have not been validated.
+ private static final int UNVALIDATED_SCORE_PENALTY = 40;
+
+ // Score for a network that can be used unvalidated
+ private static final int ACCEPT_UNVALIDATED_NETWORK_SCORE = 100;
+
+ private int getLegacyInt(boolean pretendValidated) {
+ // If the user has chosen this network at least once, give it the maximum score when
+ // checking to pretend it's validated, or if it doesn't need to validate because the
+ // user said to use it even if it doesn't validate.
+ // This ensures that networks that have been selected in UI are not torn down before the
+ // user gets a chance to prefer it when a higher-scoring network (e.g., Ethernet) is
+ // available.
+ if (hasPolicy(POLICY_EVER_USER_SELECTED)
+ && (hasPolicy(POLICY_ACCEPT_UNVALIDATED) || pretendValidated)) {
+ return ACCEPT_UNVALIDATED_NETWORK_SCORE;
+ }
+
+ int score = mLegacyInt;
+ // Except for VPNs, networks are subject to a penalty for not being validated.
+ // Apply the penalty unless the network is a VPN, or it's validated or pretending to be.
+ if (!hasPolicy(POLICY_IS_VALIDATED) && !pretendValidated && !hasPolicy(POLICY_IS_VPN)) {
+ score -= UNVALIDATED_SCORE_PENALTY;
+ }
+ if (score < 0) score = 0;
+ return score;
+ }
+
+ /**
+ * @return whether this score has a particular policy.
+ */
+ @VisibleForTesting
+ public boolean hasPolicy(final int policy) {
+ return 0 != (mPolicies & (1L << policy));
+ }
+
+ // Example output :
+ // Score(50 ; Policies : EVER_USER_SELECTED&IS_VALIDATED)
+ @Override
+ public String toString() {
+ final StringJoiner sj = new StringJoiner(
+ "&", // delimiter
+ "Score(" + mLegacyInt + " ; Policies : ", // prefix
+ ")"); // suffix
+ for (int i = NetworkScore.MIN_AGENT_MANAGED_POLICY;
+ i <= NetworkScore.MAX_AGENT_MANAGED_POLICY; ++i) {
+ if (hasPolicy(i)) sj.add(policyNameOf(i));
+ }
+ for (int i = MIN_CS_MANAGED_POLICY; i <= MAX_CS_MANAGED_POLICY; ++i) {
+ if (hasPolicy(i)) sj.add(policyNameOf(i));
+ }
+ return sj.toString();
+ }
+}
diff --git a/service/src/com/android/server/connectivity/KeepaliveTracker.java b/service/src/com/android/server/connectivity/KeepaliveTracker.java
new file mode 100644
index 0000000..acf39f0
--- /dev/null
+++ b/service/src/com/android/server/connectivity/KeepaliveTracker.java
@@ -0,0 +1,768 @@
+/*
+ * Copyright (C) 2015 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.connectivity;
+
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.NattSocketKeepalive.NATT_PORT;
+import static android.net.NetworkAgent.CMD_START_SOCKET_KEEPALIVE;
+import static android.net.SocketKeepalive.BINDER_DIED;
+import static android.net.SocketKeepalive.DATA_RECEIVED;
+import static android.net.SocketKeepalive.ERROR_INSUFFICIENT_RESOURCES;
+import static android.net.SocketKeepalive.ERROR_INVALID_INTERVAL;
+import static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS;
+import static android.net.SocketKeepalive.ERROR_INVALID_NETWORK;
+import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET;
+import static android.net.SocketKeepalive.ERROR_NO_SUCH_SLOT;
+import static android.net.SocketKeepalive.ERROR_STOP_REASON_UNINITIALIZED;
+import static android.net.SocketKeepalive.ERROR_UNSUPPORTED;
+import static android.net.SocketKeepalive.MAX_INTERVAL_SEC;
+import static android.net.SocketKeepalive.MIN_INTERVAL_SEC;
+import static android.net.SocketKeepalive.NO_KEEPALIVE;
+import static android.net.SocketKeepalive.SUCCESS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.ConnectivityResources;
+import android.net.ISocketKeepaliveCallback;
+import android.net.InetAddresses;
+import android.net.InvalidPacketException;
+import android.net.KeepalivePacketData;
+import android.net.NattKeepalivePacketData;
+import android.net.NetworkAgent;
+import android.net.SocketKeepalive.InvalidSocketException;
+import android.net.TcpKeepalivePacketData;
+import android.net.util.KeepaliveUtils;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.HexDump;
+import com.android.net.module.util.IpUtils;
+
+import java.io.FileDescriptor;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+
+/**
+ * Manages socket keepalive requests.
+ *
+ * Provides methods to stop and start keepalive requests, and keeps track of keepalives across all
+ * networks. This class is tightly coupled to ConnectivityService. It is not thread-safe and its
+ * handle* methods must be called only from the ConnectivityService handler thread.
+ */
+public class KeepaliveTracker {
+
+ private static final String TAG = "KeepaliveTracker";
+ private static final boolean DBG = false;
+
+ public static final String PERMISSION = android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
+
+ /** Keeps track of keepalive requests. */
+ private final HashMap <NetworkAgentInfo, HashMap<Integer, KeepaliveInfo>> mKeepalives =
+ new HashMap<> ();
+ private final Handler mConnectivityServiceHandler;
+ @NonNull
+ private final TcpKeepaliveController mTcpController;
+ @NonNull
+ private final Context mContext;
+
+ // Supported keepalive count for each transport type, can be configured through
+ // config_networkSupportedKeepaliveCount. For better error handling, use
+ // {@link getSupportedKeepalivesForNetworkCapabilities} instead of direct access.
+ @NonNull
+ private final int[] mSupportedKeepalives;
+
+ // Reserved privileged keepalive slots per transport. Caller's permission will be enforced if
+ // the number of remaining keepalive slots is less than or equal to the threshold.
+ private final int mReservedPrivilegedSlots;
+
+ // Allowed unprivileged keepalive slots per uid. Caller's permission will be enforced if
+ // the number of remaining keepalive slots is less than or equal to the threshold.
+ private final int mAllowedUnprivilegedSlotsForUid;
+
+ public KeepaliveTracker(Context context, Handler handler) {
+ mConnectivityServiceHandler = handler;
+ mTcpController = new TcpKeepaliveController(handler);
+ mContext = context;
+ mSupportedKeepalives = KeepaliveUtils.getSupportedKeepalives(mContext);
+
+ // TODO (b/183076074): stop reading legacy resources after migrating overlays
+ final int legacyReservedSlots = mContext.getResources().getInteger(
+ mContext.getResources().getIdentifier(
+ "config_reservedPrivilegedKeepaliveSlots", "integer", "android"));
+ final int legacyAllowedSlots = mContext.getResources().getInteger(
+ mContext.getResources().getIdentifier(
+ "config_allowedUnprivilegedKeepalivePerUid", "integer", "android"));
+ final ConnectivityResources res = new ConnectivityResources(mContext);
+ mReservedPrivilegedSlots = Math.min(legacyReservedSlots, res.get().getInteger(
+ R.integer.config_reservedPrivilegedKeepaliveSlots));
+ mAllowedUnprivilegedSlotsForUid = Math.min(legacyAllowedSlots, res.get().getInteger(
+ R.integer.config_allowedUnprivilegedKeepalivePerUid));
+ }
+
+ /**
+ * Tracks information about a socket keepalive.
+ *
+ * All information about this keepalive is known at construction time except the slot number,
+ * which is only returned when the hardware has successfully started the keepalive.
+ */
+ class KeepaliveInfo implements IBinder.DeathRecipient {
+ // Bookkeeping data.
+ private final ISocketKeepaliveCallback mCallback;
+ private final int mUid;
+ private final int mPid;
+ private final boolean mPrivileged;
+ private final NetworkAgentInfo mNai;
+ private final int mType;
+ private final FileDescriptor mFd;
+
+ public static final int TYPE_NATT = 1;
+ public static final int TYPE_TCP = 2;
+
+ // Keepalive slot. A small integer that identifies this keepalive among the ones handled
+ // by this network.
+ private int mSlot = NO_KEEPALIVE;
+
+ // Packet data.
+ private final KeepalivePacketData mPacket;
+ private final int mInterval;
+
+ // Whether the keepalive is started or not. The initial state is NOT_STARTED.
+ private static final int NOT_STARTED = 1;
+ private static final int STARTING = 2;
+ private static final int STARTED = 3;
+ private static final int STOPPING = 4;
+ private int mStartedState = NOT_STARTED;
+ private int mStopReason = ERROR_STOP_REASON_UNINITIALIZED;
+
+ KeepaliveInfo(@NonNull ISocketKeepaliveCallback callback,
+ @NonNull NetworkAgentInfo nai,
+ @NonNull KeepalivePacketData packet,
+ int interval,
+ int type,
+ @Nullable FileDescriptor fd) throws InvalidSocketException {
+ mCallback = callback;
+ mPid = Binder.getCallingPid();
+ mUid = Binder.getCallingUid();
+ mPrivileged = (PERMISSION_GRANTED == mContext.checkPermission(PERMISSION, mPid, mUid));
+
+ mNai = nai;
+ mPacket = packet;
+ mInterval = interval;
+ mType = type;
+
+ // For SocketKeepalive, a dup of fd is kept in mFd so the source port from which the
+ // keepalives are sent cannot be reused by another app even if the fd gets closed by
+ // the user. A null is acceptable here for backward compatibility of PacketKeepalive
+ // API.
+ try {
+ if (fd != null) {
+ mFd = Os.dup(fd);
+ } else {
+ Log.d(TAG, toString() + " calls with null fd");
+ if (!mPrivileged) {
+ throw new SecurityException(
+ "null fd is not allowed for unprivileged access.");
+ }
+ if (mType == TYPE_TCP) {
+ throw new IllegalArgumentException(
+ "null fd is not allowed for tcp socket keepalives.");
+ }
+ mFd = null;
+ }
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot dup fd: ", e);
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+ }
+
+ try {
+ mCallback.asBinder().linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ binderDied();
+ }
+ }
+
+ public NetworkAgentInfo getNai() {
+ return mNai;
+ }
+
+ private String startedStateString(final int state) {
+ switch (state) {
+ case NOT_STARTED : return "NOT_STARTED";
+ case STARTING : return "STARTING";
+ case STARTED : return "STARTED";
+ case STOPPING : return "STOPPING";
+ }
+ throw new IllegalArgumentException("Unknown state");
+ }
+
+ public String toString() {
+ return "KeepaliveInfo ["
+ + " type=" + mType
+ + " network=" + mNai.network
+ + " startedState=" + startedStateString(mStartedState)
+ + " "
+ + IpUtils.addressAndPortToString(mPacket.getSrcAddress(), mPacket.getSrcPort())
+ + "->"
+ + IpUtils.addressAndPortToString(mPacket.getDstAddress(), mPacket.getDstPort())
+ + " interval=" + mInterval
+ + " uid=" + mUid + " pid=" + mPid + " privileged=" + mPrivileged
+ + " packetData=" + HexDump.toHexString(mPacket.getPacket())
+ + " ]";
+ }
+
+ /** Called when the application process is killed. */
+ public void binderDied() {
+ stop(BINDER_DIED);
+ }
+
+ void unlinkDeathRecipient() {
+ if (mCallback != null) {
+ mCallback.asBinder().unlinkToDeath(this, 0);
+ }
+ }
+
+ private int checkNetworkConnected() {
+ if (!mNai.networkInfo.isConnectedOrConnecting()) {
+ return ERROR_INVALID_NETWORK;
+ }
+ return SUCCESS;
+ }
+
+ private int checkSourceAddress() {
+ // Check that we have the source address.
+ for (InetAddress address : mNai.linkProperties.getAddresses()) {
+ if (address.equals(mPacket.getSrcAddress())) {
+ return SUCCESS;
+ }
+ }
+ return ERROR_INVALID_IP_ADDRESS;
+ }
+
+ private int checkInterval() {
+ if (mInterval < MIN_INTERVAL_SEC || mInterval > MAX_INTERVAL_SEC) {
+ return ERROR_INVALID_INTERVAL;
+ }
+ return SUCCESS;
+ }
+
+ private int checkPermission() {
+ final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(mNai);
+ if (networkKeepalives == null) {
+ return ERROR_INVALID_NETWORK;
+ }
+
+ if (mPrivileged) return SUCCESS;
+
+ final int supported = KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(
+ mSupportedKeepalives, mNai.networkCapabilities);
+
+ int takenUnprivilegedSlots = 0;
+ for (final KeepaliveInfo ki : networkKeepalives.values()) {
+ if (!ki.mPrivileged) ++takenUnprivilegedSlots;
+ }
+ if (takenUnprivilegedSlots > supported - mReservedPrivilegedSlots) {
+ return ERROR_INSUFFICIENT_RESOURCES;
+ }
+
+ // Count unprivileged keepalives for the same uid across networks.
+ int unprivilegedCountSameUid = 0;
+ for (final HashMap<Integer, KeepaliveInfo> kaForNetwork : mKeepalives.values()) {
+ for (final KeepaliveInfo ki : kaForNetwork.values()) {
+ if (ki.mUid == mUid) {
+ unprivilegedCountSameUid++;
+ }
+ }
+ }
+ if (unprivilegedCountSameUid > mAllowedUnprivilegedSlotsForUid) {
+ return ERROR_INSUFFICIENT_RESOURCES;
+ }
+ return SUCCESS;
+ }
+
+ private int checkLimit() {
+ final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(mNai);
+ if (networkKeepalives == null) {
+ return ERROR_INVALID_NETWORK;
+ }
+ final int supported = KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(
+ mSupportedKeepalives, mNai.networkCapabilities);
+ if (supported == 0) return ERROR_UNSUPPORTED;
+ if (networkKeepalives.size() > supported) return ERROR_INSUFFICIENT_RESOURCES;
+ return SUCCESS;
+ }
+
+ private int isValid() {
+ synchronized (mNai) {
+ int error = checkInterval();
+ if (error == SUCCESS) error = checkLimit();
+ if (error == SUCCESS) error = checkPermission();
+ if (error == SUCCESS) error = checkNetworkConnected();
+ if (error == SUCCESS) error = checkSourceAddress();
+ return error;
+ }
+ }
+
+ void start(int slot) {
+ mSlot = slot;
+ int error = isValid();
+ if (error == SUCCESS) {
+ Log.d(TAG, "Starting keepalive " + mSlot + " on " + mNai.toShortString());
+ switch (mType) {
+ case TYPE_NATT:
+ final NattKeepalivePacketData nattData = (NattKeepalivePacketData) mPacket;
+ mNai.onAddNattKeepalivePacketFilter(slot, nattData);
+ mNai.onStartNattSocketKeepalive(slot, mInterval, nattData);
+ break;
+ case TYPE_TCP:
+ try {
+ mTcpController.startSocketMonitor(mFd, this, mSlot);
+ } catch (InvalidSocketException e) {
+ handleStopKeepalive(mNai, mSlot, ERROR_INVALID_SOCKET);
+ return;
+ }
+ final TcpKeepalivePacketData tcpData = (TcpKeepalivePacketData) mPacket;
+ mNai.onAddTcpKeepalivePacketFilter(slot, tcpData);
+ // TODO: check result from apf and notify of failure as needed.
+ mNai.onStartTcpSocketKeepalive(slot, mInterval, tcpData);
+ break;
+ default:
+ Log.wtf(TAG, "Starting keepalive with unknown type: " + mType);
+ handleStopKeepalive(mNai, mSlot, error);
+ return;
+ }
+ mStartedState = STARTING;
+ } else {
+ handleStopKeepalive(mNai, mSlot, error);
+ return;
+ }
+ }
+
+ void stop(int reason) {
+ int uid = Binder.getCallingUid();
+ if (uid != mUid && uid != Process.SYSTEM_UID) {
+ if (DBG) {
+ Log.e(TAG, "Cannot stop unowned keepalive " + mSlot + " on " + mNai.network);
+ }
+ }
+ // Ignore the case when the network disconnects immediately after stop() has been
+ // called and the keepalive code is waiting for the response from the modem. This
+ // might happen when the caller listens for a lower-layer network disconnect
+ // callback and stop the keepalive at that time. But the stop() races with the
+ // stop() generated in ConnectivityService network disconnection code path.
+ if (mStartedState == STOPPING && reason == ERROR_INVALID_NETWORK) return;
+
+ // Store the reason of stopping, and report it after the keepalive is fully stopped.
+ if (mStopReason != ERROR_STOP_REASON_UNINITIALIZED) {
+ throw new IllegalStateException("Unexpected stop reason: " + mStopReason);
+ }
+ mStopReason = reason;
+ Log.d(TAG, "Stopping keepalive " + mSlot + " on " + mNai.toShortString()
+ + ": " + reason);
+ switch (mStartedState) {
+ case NOT_STARTED:
+ // Remove the reference of the keepalive that meet error before starting,
+ // e.g. invalid parameter.
+ cleanupStoppedKeepalive(mNai, mSlot);
+ break;
+ default:
+ mStartedState = STOPPING;
+ switch (mType) {
+ case TYPE_TCP:
+ mTcpController.stopSocketMonitor(mSlot);
+ // fall through
+ case TYPE_NATT:
+ mNai.onStopSocketKeepalive(mSlot);
+ mNai.onRemoveKeepalivePacketFilter(mSlot);
+ break;
+ default:
+ Log.wtf(TAG, "Stopping keepalive with unknown type: " + mType);
+ }
+ }
+
+ // Close the duplicated fd that maintains the lifecycle of socket whenever
+ // keepalive is running.
+ if (mFd != null) {
+ try {
+ Os.close(mFd);
+ } catch (ErrnoException e) {
+ // This should not happen since system server controls the lifecycle of fd when
+ // keepalive offload is running.
+ Log.wtf(TAG, "Error closing fd for keepalive " + mSlot + ": " + e);
+ }
+ }
+ }
+
+ void onFileDescriptorInitiatedStop(final int socketKeepaliveReason) {
+ handleStopKeepalive(mNai, mSlot, socketKeepaliveReason);
+ }
+ }
+
+ void notifyErrorCallback(ISocketKeepaliveCallback cb, int error) {
+ if (DBG) Log.w(TAG, "Sending onError(" + error + ") callback");
+ try {
+ cb.onError(error);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Discarded onError(" + error + ") callback");
+ }
+ }
+
+ private int findFirstFreeSlot(NetworkAgentInfo nai) {
+ HashMap networkKeepalives = mKeepalives.get(nai);
+ if (networkKeepalives == null) {
+ networkKeepalives = new HashMap<Integer, KeepaliveInfo>();
+ mKeepalives.put(nai, networkKeepalives);
+ }
+
+ // Find the lowest-numbered free slot. Slot numbers start from 1, because that's what two
+ // separate chipset implementations independently came up with.
+ int slot;
+ for (slot = 1; slot <= networkKeepalives.size(); slot++) {
+ if (networkKeepalives.get(slot) == null) {
+ return slot;
+ }
+ }
+ return slot;
+ }
+
+ public void handleStartKeepalive(Message message) {
+ KeepaliveInfo ki = (KeepaliveInfo) message.obj;
+ NetworkAgentInfo nai = ki.getNai();
+ int slot = findFirstFreeSlot(nai);
+ mKeepalives.get(nai).put(slot, ki);
+ ki.start(slot);
+ }
+
+ public void handleStopAllKeepalives(NetworkAgentInfo nai, int reason) {
+ final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
+ if (networkKeepalives != null) {
+ final ArrayList<KeepaliveInfo> kalist = new ArrayList(networkKeepalives.values());
+ for (KeepaliveInfo ki : kalist) {
+ ki.stop(reason);
+ // Clean up keepalives since the network agent is disconnected and unable to pass
+ // back asynchronous result of stop().
+ cleanupStoppedKeepalive(nai, ki.mSlot);
+ }
+ }
+ }
+
+ public void handleStopKeepalive(NetworkAgentInfo nai, int slot, int reason) {
+ final String networkName = NetworkAgentInfo.toShortString(nai);
+ HashMap <Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
+ if (networkKeepalives == null) {
+ Log.e(TAG, "Attempt to stop keepalive on nonexistent network " + networkName);
+ return;
+ }
+ KeepaliveInfo ki = networkKeepalives.get(slot);
+ if (ki == null) {
+ Log.e(TAG, "Attempt to stop nonexistent keepalive " + slot + " on " + networkName);
+ return;
+ }
+ ki.stop(reason);
+ // Clean up keepalives will be done as a result of calling ki.stop() after the slots are
+ // freed.
+ }
+
+ private void cleanupStoppedKeepalive(NetworkAgentInfo nai, int slot) {
+ final String networkName = NetworkAgentInfo.toShortString(nai);
+ HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
+ if (networkKeepalives == null) {
+ Log.e(TAG, "Attempt to remove keepalive on nonexistent network " + networkName);
+ return;
+ }
+ KeepaliveInfo ki = networkKeepalives.get(slot);
+ if (ki == null) {
+ Log.e(TAG, "Attempt to remove nonexistent keepalive " + slot + " on " + networkName);
+ return;
+ }
+
+ // Remove the keepalive from hash table so the slot can be considered available when reusing
+ // it.
+ networkKeepalives.remove(slot);
+ Log.d(TAG, "Remove keepalive " + slot + " on " + networkName + ", "
+ + networkKeepalives.size() + " remains.");
+ if (networkKeepalives.isEmpty()) {
+ mKeepalives.remove(nai);
+ }
+
+ // Notify app that the keepalive is stopped.
+ final int reason = ki.mStopReason;
+ if (reason == SUCCESS) {
+ try {
+ ki.mCallback.onStopped();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Discarded onStop callback: " + reason);
+ }
+ } else if (reason == DATA_RECEIVED) {
+ try {
+ ki.mCallback.onDataReceived();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Discarded onDataReceived callback: " + reason);
+ }
+ } else if (reason == ERROR_STOP_REASON_UNINITIALIZED) {
+ throw new IllegalStateException("Unexpected stop reason: " + reason);
+ } else if (reason == ERROR_NO_SUCH_SLOT) {
+ throw new IllegalStateException("No such slot: " + reason);
+ } else {
+ notifyErrorCallback(ki.mCallback, reason);
+ }
+
+ ki.unlinkDeathRecipient();
+ }
+
+ public void handleCheckKeepalivesStillValid(NetworkAgentInfo nai) {
+ HashMap <Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
+ if (networkKeepalives != null) {
+ ArrayList<Pair<Integer, Integer>> invalidKeepalives = new ArrayList<>();
+ for (int slot : networkKeepalives.keySet()) {
+ int error = networkKeepalives.get(slot).isValid();
+ if (error != SUCCESS) {
+ invalidKeepalives.add(Pair.create(slot, error));
+ }
+ }
+ for (Pair<Integer, Integer> slotAndError: invalidKeepalives) {
+ handleStopKeepalive(nai, slotAndError.first, slotAndError.second);
+ }
+ }
+ }
+
+ /** Handle keepalive events from lower layer. */
+ public void handleEventSocketKeepalive(@NonNull NetworkAgentInfo nai, int slot, int reason) {
+ KeepaliveInfo ki = null;
+ try {
+ ki = mKeepalives.get(nai).get(slot);
+ } catch(NullPointerException e) {}
+ if (ki == null) {
+ Log.e(TAG, "Event " + NetworkAgent.EVENT_SOCKET_KEEPALIVE + "," + slot + "," + reason
+ + " for unknown keepalive " + slot + " on " + nai.toShortString());
+ return;
+ }
+
+ // This can be called in a number of situations :
+ // - startedState is STARTING.
+ // - reason is SUCCESS => go to STARTED.
+ // - reason isn't SUCCESS => it's an error starting. Go to NOT_STARTED and stop keepalive.
+ // - startedState is STARTED.
+ // - reason is SUCCESS => it's a success stopping. Go to NOT_STARTED and stop keepalive.
+ // - reason isn't SUCCESS => it's an error in exec. Go to NOT_STARTED and stop keepalive.
+ // The control is not supposed to ever come here if the state is NOT_STARTED. This is
+ // because in NOT_STARTED state, the code will switch to STARTING before sending messages
+ // to start, and the only way to NOT_STARTED is this function, through the edges outlined
+ // above : in all cases, keepalive gets stopped and can't restart without going into
+ // STARTING as messages are ordered. This also depends on the hardware processing the
+ // messages in order.
+ // TODO : clarify this code and get rid of mStartedState. Using a StateMachine is an
+ // option.
+ if (KeepaliveInfo.STARTING == ki.mStartedState) {
+ if (SUCCESS == reason) {
+ // Keepalive successfully started.
+ Log.d(TAG, "Started keepalive " + slot + " on " + nai.toShortString());
+ ki.mStartedState = KeepaliveInfo.STARTED;
+ try {
+ ki.mCallback.onStarted(slot);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Discarded onStarted(" + slot + ") callback");
+ }
+ } else {
+ Log.d(TAG, "Failed to start keepalive " + slot + " on " + nai.toShortString()
+ + ": " + reason);
+ // The message indicated some error trying to start: do call handleStopKeepalive.
+ handleStopKeepalive(nai, slot, reason);
+ }
+ } else if (KeepaliveInfo.STOPPING == ki.mStartedState) {
+ // The message indicated result of stopping : clean up keepalive slots.
+ Log.d(TAG, "Stopped keepalive " + slot + " on " + nai.toShortString()
+ + " stopped: " + reason);
+ ki.mStartedState = KeepaliveInfo.NOT_STARTED;
+ cleanupStoppedKeepalive(nai, slot);
+ } else {
+ Log.wtf(TAG, "Event " + NetworkAgent.EVENT_SOCKET_KEEPALIVE + "," + slot + "," + reason
+ + " for keepalive in wrong state: " + ki.toString());
+ }
+ }
+
+ /**
+ * Called when requesting that keepalives be started on a IPsec NAT-T socket. See
+ * {@link android.net.SocketKeepalive}.
+ **/
+ public void startNattKeepalive(@Nullable NetworkAgentInfo nai,
+ @Nullable FileDescriptor fd,
+ int intervalSeconds,
+ @NonNull ISocketKeepaliveCallback cb,
+ @NonNull String srcAddrString,
+ int srcPort,
+ @NonNull String dstAddrString,
+ int dstPort) {
+ if (nai == null) {
+ notifyErrorCallback(cb, ERROR_INVALID_NETWORK);
+ return;
+ }
+
+ InetAddress srcAddress, dstAddress;
+ try {
+ srcAddress = InetAddresses.parseNumericAddress(srcAddrString);
+ dstAddress = InetAddresses.parseNumericAddress(dstAddrString);
+ } catch (IllegalArgumentException e) {
+ notifyErrorCallback(cb, ERROR_INVALID_IP_ADDRESS);
+ return;
+ }
+
+ KeepalivePacketData packet;
+ try {
+ packet = NattKeepalivePacketData.nattKeepalivePacket(
+ srcAddress, srcPort, dstAddress, NATT_PORT);
+ } catch (InvalidPacketException e) {
+ notifyErrorCallback(cb, e.getError());
+ return;
+ }
+ KeepaliveInfo ki = null;
+ try {
+ ki = new KeepaliveInfo(cb, nai, packet, intervalSeconds,
+ KeepaliveInfo.TYPE_NATT, fd);
+ } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) {
+ Log.e(TAG, "Fail to construct keepalive", e);
+ notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
+ return;
+ }
+ Log.d(TAG, "Created keepalive: " + ki.toString());
+ mConnectivityServiceHandler.obtainMessage(
+ NetworkAgent.CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget();
+ }
+
+ /**
+ * Called by ConnectivityService to start TCP keepalive on a file descriptor.
+ *
+ * In order to offload keepalive for application correctly, sequence number, ack number and
+ * other fields are needed to form the keepalive packet. Thus, this function synchronously
+ * puts the socket into repair mode to get the necessary information. After the socket has been
+ * put into repair mode, the application cannot access the socket until reverted to normal.
+ *
+ * See {@link android.net.SocketKeepalive}.
+ **/
+ public void startTcpKeepalive(@Nullable NetworkAgentInfo nai,
+ @NonNull FileDescriptor fd,
+ int intervalSeconds,
+ @NonNull ISocketKeepaliveCallback cb) {
+ if (nai == null) {
+ notifyErrorCallback(cb, ERROR_INVALID_NETWORK);
+ return;
+ }
+
+ final TcpKeepalivePacketData packet;
+ try {
+ packet = TcpKeepaliveController.getTcpKeepalivePacket(fd);
+ } catch (InvalidSocketException e) {
+ notifyErrorCallback(cb, e.error);
+ return;
+ } catch (InvalidPacketException e) {
+ notifyErrorCallback(cb, e.getError());
+ return;
+ }
+ KeepaliveInfo ki = null;
+ try {
+ ki = new KeepaliveInfo(cb, nai, packet, intervalSeconds,
+ KeepaliveInfo.TYPE_TCP, fd);
+ } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) {
+ Log.e(TAG, "Fail to construct keepalive e=" + e);
+ notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
+ return;
+ }
+ Log.d(TAG, "Created keepalive: " + ki.toString());
+ mConnectivityServiceHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget();
+ }
+
+ /**
+ * Called when requesting that keepalives be started on a IPsec NAT-T socket. This function is
+ * identical to {@link #startNattKeepalive}, but also takes a {@code resourceId}, which is the
+ * resource index bound to the {@link UdpEncapsulationSocket} when creating by
+ * {@link com.android.server.IpSecService} to verify whether the given
+ * {@link UdpEncapsulationSocket} is legitimate.
+ **/
+ public void startNattKeepalive(@Nullable NetworkAgentInfo nai,
+ @Nullable FileDescriptor fd,
+ int resourceId,
+ int intervalSeconds,
+ @NonNull ISocketKeepaliveCallback cb,
+ @NonNull String srcAddrString,
+ @NonNull String dstAddrString,
+ int dstPort) {
+ // Ensure that the socket is created by IpSecService.
+ if (!isNattKeepaliveSocketValid(fd, resourceId)) {
+ notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
+ }
+
+ // Get src port to adopt old API.
+ int srcPort = 0;
+ try {
+ final SocketAddress srcSockAddr = Os.getsockname(fd);
+ srcPort = ((InetSocketAddress) srcSockAddr).getPort();
+ } catch (ErrnoException e) {
+ notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
+ }
+
+ // Forward request to old API.
+ startNattKeepalive(nai, fd, intervalSeconds, cb, srcAddrString, srcPort,
+ dstAddrString, dstPort);
+ }
+
+ /**
+ * Verify if the IPsec NAT-T file descriptor and resource Id hold for IPsec keepalive is valid.
+ **/
+ public static boolean isNattKeepaliveSocketValid(@Nullable FileDescriptor fd, int resourceId) {
+ // TODO: 1. confirm whether the fd is called from system api or created by IpSecService.
+ // 2. If the fd is created from the system api, check that it's bounded. And
+ // call dup to keep the fd open.
+ // 3. If the fd is created from IpSecService, check if the resource ID is valid. And
+ // hold the resource needed in IpSecService.
+ if (null == fd) {
+ return false;
+ }
+ return true;
+ }
+
+ public void dump(IndentingPrintWriter pw) {
+ pw.println("Supported Socket keepalives: " + Arrays.toString(mSupportedKeepalives));
+ pw.println("Reserved Privileged keepalives: " + mReservedPrivilegedSlots);
+ pw.println("Allowed Unprivileged keepalives per uid: " + mAllowedUnprivilegedSlotsForUid);
+ pw.println("Socket keepalives:");
+ pw.increaseIndent();
+ for (NetworkAgentInfo nai : mKeepalives.keySet()) {
+ pw.println(nai.toShortString());
+ pw.increaseIndent();
+ for (int slot : mKeepalives.get(nai).keySet()) {
+ KeepaliveInfo ki = mKeepalives.get(nai).get(slot);
+ pw.println(slot + ": " + ki.toString());
+ }
+ pw.decreaseIndent();
+ }
+ pw.decreaseIndent();
+ }
+}
diff --git a/service/src/com/android/server/connectivity/LingerMonitor.java b/service/src/com/android/server/connectivity/LingerMonitor.java
new file mode 100644
index 0000000..032612c
--- /dev/null
+++ b/service/src/com/android/server/connectivity/LingerMonitor.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2016 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.connectivity;
+
+import static android.net.ConnectivityManager.NETID_UNSET;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.ConnectivityResources;
+import android.net.NetworkCapabilities;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.MessageUtils;
+import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
+
+import java.util.Arrays;
+import java.util.HashMap;
+
+/**
+ * Class that monitors default network linger events and possibly notifies the user of network
+ * switches.
+ *
+ * This class is not thread-safe and all its methods must be called on the ConnectivityService
+ * handler thread.
+ */
+public class LingerMonitor {
+
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+ private static final String TAG = LingerMonitor.class.getSimpleName();
+
+ public static final int DEFAULT_NOTIFICATION_DAILY_LIMIT = 3;
+ public static final long DEFAULT_NOTIFICATION_RATE_LIMIT_MILLIS = DateUtils.MINUTE_IN_MILLIS;
+
+ private static final HashMap<String, Integer> TRANSPORT_NAMES = makeTransportToNameMap();
+ @VisibleForTesting
+ public static final Intent CELLULAR_SETTINGS = new Intent().setComponent(new ComponentName(
+ "com.android.settings", "com.android.settings.Settings$DataUsageSummaryActivity"));
+
+ @VisibleForTesting
+ public static final int NOTIFY_TYPE_NONE = 0;
+ public static final int NOTIFY_TYPE_NOTIFICATION = 1;
+ public static final int NOTIFY_TYPE_TOAST = 2;
+
+ private static SparseArray<String> sNotifyTypeNames = MessageUtils.findMessageNames(
+ new Class[] { LingerMonitor.class }, new String[]{ "NOTIFY_TYPE_" });
+
+ private final Context mContext;
+ final Resources mResources;
+ private final NetworkNotificationManager mNotifier;
+ private final int mDailyLimit;
+ private final long mRateLimitMillis;
+
+ private long mFirstNotificationMillis;
+ private long mLastNotificationMillis;
+ private int mNotificationCounter;
+
+ /** Current notifications. Maps the netId we switched away from to the netId we switched to. */
+ private final SparseIntArray mNotifications = new SparseIntArray();
+
+ /** Whether we ever notified that we switched away from a particular network. */
+ private final SparseBooleanArray mEverNotified = new SparseBooleanArray();
+
+ public LingerMonitor(Context context, NetworkNotificationManager notifier,
+ int dailyLimit, long rateLimitMillis) {
+ mContext = context;
+ mResources = new ConnectivityResources(mContext).get();
+ mNotifier = notifier;
+ mDailyLimit = dailyLimit;
+ mRateLimitMillis = rateLimitMillis;
+ // Ensure that (now - mLastNotificationMillis) >= rateLimitMillis at first
+ mLastNotificationMillis = -rateLimitMillis;
+ }
+
+ private static HashMap<String, Integer> makeTransportToNameMap() {
+ SparseArray<String> numberToName = MessageUtils.findMessageNames(
+ new Class[] { NetworkCapabilities.class }, new String[]{ "TRANSPORT_" });
+ HashMap<String, Integer> nameToNumber = new HashMap<>();
+ for (int i = 0; i < numberToName.size(); i++) {
+ // MessageUtils will fail to initialize if there are duplicate constant values, so there
+ // are no duplicates here.
+ nameToNumber.put(numberToName.valueAt(i), numberToName.keyAt(i));
+ }
+ return nameToNumber;
+ }
+
+ private static boolean hasTransport(NetworkAgentInfo nai, int transport) {
+ return nai.networkCapabilities.hasTransport(transport);
+ }
+
+ private int getNotificationSource(NetworkAgentInfo toNai) {
+ for (int i = 0; i < mNotifications.size(); i++) {
+ if (mNotifications.valueAt(i) == toNai.network.getNetId()) {
+ return mNotifications.keyAt(i);
+ }
+ }
+ return NETID_UNSET;
+ }
+
+ private boolean everNotified(NetworkAgentInfo nai) {
+ return mEverNotified.get(nai.network.getNetId(), false);
+ }
+
+ @VisibleForTesting
+ public boolean isNotificationEnabled(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+ // TODO: Evaluate moving to CarrierConfigManager.
+ String[] notifySwitches = mResources.getStringArray(R.array.config_networkNotifySwitches);
+
+ if (VDBG) {
+ Log.d(TAG, "Notify on network switches: " + Arrays.toString(notifySwitches));
+ }
+
+ for (String notifySwitch : notifySwitches) {
+ if (TextUtils.isEmpty(notifySwitch)) continue;
+ String[] transports = notifySwitch.split("-", 2);
+ if (transports.length != 2) {
+ Log.e(TAG, "Invalid network switch notification configuration: " + notifySwitch);
+ continue;
+ }
+ int fromTransport = TRANSPORT_NAMES.get("TRANSPORT_" + transports[0]);
+ int toTransport = TRANSPORT_NAMES.get("TRANSPORT_" + transports[1]);
+ if (hasTransport(fromNai, fromTransport) && hasTransport(toNai, toTransport)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void showNotification(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+ mNotifier.showNotification(fromNai.network.getNetId(), NotificationType.NETWORK_SWITCH,
+ fromNai, toNai, createNotificationIntent(), true);
+ }
+
+ @VisibleForTesting
+ protected PendingIntent createNotificationIntent() {
+ return PendingIntent.getActivity(
+ mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
+ 0 /* requestCode */,
+ CELLULAR_SETTINGS,
+ PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+ }
+
+ // Removes any notification that was put up as a result of switching to nai.
+ private void maybeStopNotifying(NetworkAgentInfo nai) {
+ int fromNetId = getNotificationSource(nai);
+ if (fromNetId != NETID_UNSET) {
+ mNotifications.delete(fromNetId);
+ mNotifier.clearNotification(fromNetId);
+ // Toasts can't be deleted.
+ }
+ }
+
+ // Notify the user of a network switch using a notification or a toast.
+ private void notify(NetworkAgentInfo fromNai, NetworkAgentInfo toNai, boolean forceToast) {
+ int notifyType = mResources.getInteger(R.integer.config_networkNotifySwitchType);
+ if (notifyType == NOTIFY_TYPE_NOTIFICATION && forceToast) {
+ notifyType = NOTIFY_TYPE_TOAST;
+ }
+
+ if (VDBG) {
+ Log.d(TAG, "Notify type: " + sNotifyTypeNames.get(notifyType, "" + notifyType));
+ }
+
+ switch (notifyType) {
+ case NOTIFY_TYPE_NONE:
+ return;
+ case NOTIFY_TYPE_NOTIFICATION:
+ showNotification(fromNai, toNai);
+ break;
+ case NOTIFY_TYPE_TOAST:
+ mNotifier.showToast(fromNai, toNai);
+ break;
+ default:
+ Log.e(TAG, "Unknown notify type " + notifyType);
+ return;
+ }
+
+ if (DBG) {
+ Log.d(TAG, "Notifying switch from=" + fromNai.toShortString()
+ + " to=" + toNai.toShortString()
+ + " type=" + sNotifyTypeNames.get(notifyType, "unknown(" + notifyType + ")"));
+ }
+
+ mNotifications.put(fromNai.network.getNetId(), toNai.network.getNetId());
+ mEverNotified.put(fromNai.network.getNetId(), true);
+ }
+
+ /**
+ * Put up or dismiss a notification or toast for of a change in the default network if needed.
+ *
+ * Putting up a notification when switching from no network to some network is not supported
+ * and as such this method can't be called with a null |fromNai|. It can be called with a
+ * null |toNai| if there isn't a default network any more.
+ *
+ * @param fromNai switching from this NAI
+ * @param toNai switching to this NAI
+ */
+ // The default network changed from fromNai to toNai due to a change in score.
+ public void noteLingerDefaultNetwork(@NonNull final NetworkAgentInfo fromNai,
+ @Nullable final NetworkAgentInfo toNai) {
+ if (VDBG) {
+ Log.d(TAG, "noteLingerDefaultNetwork from=" + fromNai.toShortString()
+ + " everValidated=" + fromNai.everValidated
+ + " lastValidated=" + fromNai.lastValidated
+ + " to=" + toNai.toShortString());
+ }
+
+ // If we are currently notifying the user because the device switched to fromNai, now that
+ // we are switching away from it we should remove the notification. This includes the case
+ // where we switch back to toNai because its score improved again (e.g., because it regained
+ // Internet access).
+ maybeStopNotifying(fromNai);
+
+ // If the network was simply lost (either because it disconnected or because it stopped
+ // being the default with no replacement), then don't show a notification.
+ if (null == toNai) return;
+
+ // If this network never validated, don't notify. Otherwise, we could do things like:
+ //
+ // 1. Unvalidated wifi connects.
+ // 2. Unvalidated mobile data connects.
+ // 3. Cell validates, and we show a notification.
+ // or:
+ // 1. User connects to wireless printer.
+ // 2. User turns on cellular data.
+ // 3. We show a notification.
+ if (!fromNai.everValidated) return;
+
+ // If this network is a captive portal, don't notify. This cannot happen on initial connect
+ // to a captive portal, because the everValidated check above will fail. However, it can
+ // happen if the captive portal reasserts itself (e.g., because its timeout fires). In that
+ // case, as soon as the captive portal reasserts itself, we'll show a sign-in notification.
+ // We don't want to overwrite that notification with this one; the user has already been
+ // notified, and of the two, the captive portal notification is the more useful one because
+ // it allows the user to sign in to the captive portal. In this case, display a toast
+ // in addition to the captive portal notification.
+ //
+ // Note that if the network we switch to is already up when the captive portal reappears,
+ // this won't work because NetworkMonitor tells ConnectivityService that the network is
+ // unvalidated (causing a switch) before asking it to show the sign in notification. In this
+ // case, the toast won't show and we'll only display the sign in notification. This is the
+ // best we can do at this time.
+ boolean forceToast = fromNai.networkCapabilities.hasCapability(
+ NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL);
+
+ // Only show the notification once, in order to avoid irritating the user every time.
+ // TODO: should we do this?
+ if (everNotified(fromNai)) {
+ if (VDBG) {
+ Log.d(TAG, "Not notifying handover from " + fromNai.toShortString()
+ + ", already notified");
+ }
+ return;
+ }
+
+ // Only show the notification if we switched away because a network became unvalidated, not
+ // because its score changed.
+ // TODO: instead of just skipping notification, keep a note of it, and show it if it becomes
+ // unvalidated.
+ if (fromNai.lastValidated) return;
+
+ if (!isNotificationEnabled(fromNai, toNai)) return;
+
+ final long now = SystemClock.elapsedRealtime();
+ if (isRateLimited(now) || isAboveDailyLimit(now)) return;
+
+ notify(fromNai, toNai, forceToast);
+ }
+
+ public void noteDisconnect(NetworkAgentInfo nai) {
+ mNotifications.delete(nai.network.getNetId());
+ mEverNotified.delete(nai.network.getNetId());
+ maybeStopNotifying(nai);
+ // No need to cancel notifications on nai: NetworkMonitor does that on disconnect.
+ }
+
+ private boolean isRateLimited(long now) {
+ final long millisSinceLast = now - mLastNotificationMillis;
+ if (millisSinceLast < mRateLimitMillis) {
+ return true;
+ }
+ mLastNotificationMillis = now;
+ return false;
+ }
+
+ private boolean isAboveDailyLimit(long now) {
+ if (mFirstNotificationMillis == 0) {
+ mFirstNotificationMillis = now;
+ }
+ final long millisSinceFirst = now - mFirstNotificationMillis;
+ if (millisSinceFirst > DateUtils.DAY_IN_MILLIS) {
+ mNotificationCounter = 0;
+ mFirstNotificationMillis = 0;
+ }
+ if (mNotificationCounter >= mDailyLimit) {
+ return true;
+ }
+ mNotificationCounter++;
+ return false;
+ }
+}
diff --git a/service/src/com/android/server/connectivity/MockableSystemProperties.java b/service/src/com/android/server/connectivity/MockableSystemProperties.java
new file mode 100644
index 0000000..a25b89a
--- /dev/null
+++ b/service/src/com/android/server/connectivity/MockableSystemProperties.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2016 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.connectivity;
+
+import android.os.SystemProperties;
+
+public class MockableSystemProperties {
+
+ public String get(String key) {
+ return SystemProperties.get(key);
+ }
+
+ public int getInt(String key, int def) {
+ return SystemProperties.getInt(key, def);
+ }
+
+ public boolean getBoolean(String key, boolean def) {
+ return SystemProperties.getBoolean(key, def);
+ }
+}
diff --git a/service/src/com/android/server/connectivity/Nat464Xlat.java b/service/src/com/android/server/connectivity/Nat464Xlat.java
new file mode 100644
index 0000000..c66a280
--- /dev/null
+++ b/service/src/com/android/server/connectivity/Nat464Xlat.java
@@ -0,0 +1,523 @@
+/*
+ * Copyright (C) 2012 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.connectivity;
+
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+
+import static com.android.net.module.util.CollectionUtils.contains;
+
+import android.annotation.NonNull;
+import android.net.ConnectivityManager;
+import android.net.IDnsResolver;
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.InterfaceConfigurationParcel;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkInfo;
+import android.net.RouteInfo;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.NetworkStackConstants;
+import com.android.server.ConnectivityService;
+
+import java.net.Inet6Address;
+import java.util.Objects;
+
+/**
+ * Class to manage a 464xlat CLAT daemon. Nat464Xlat is not thread safe and should be manipulated
+ * from a consistent and unique thread context. It is the responsibility of ConnectivityService to
+ * call into this class from its own Handler thread.
+ *
+ * @hide
+ */
+public class Nat464Xlat {
+ private static final String TAG = Nat464Xlat.class.getSimpleName();
+
+ // This must match the interface prefix in clatd.c.
+ private static final String CLAT_PREFIX = "v4-";
+
+ // The network types on which we will start clatd,
+ // allowing clat only on networks for which we can support IPv6-only.
+ private static final int[] NETWORK_TYPES = {
+ ConnectivityManager.TYPE_MOBILE,
+ ConnectivityManager.TYPE_WIFI,
+ ConnectivityManager.TYPE_ETHERNET,
+ };
+
+ // The network states in which running clatd is supported.
+ private static final NetworkInfo.State[] NETWORK_STATES = {
+ NetworkInfo.State.CONNECTED,
+ NetworkInfo.State.SUSPENDED,
+ };
+
+ private final IDnsResolver mDnsResolver;
+ private final INetd mNetd;
+
+ // The network we're running on, and its type.
+ private final NetworkAgentInfo mNetwork;
+
+ private enum State {
+ IDLE, // start() not called. Base iface and stacked iface names are null.
+ DISCOVERING, // same as IDLE, except prefix discovery in progress.
+ STARTING, // start() called. Base iface and stacked iface names are known.
+ RUNNING, // start() called, and the stacked iface is known to be up.
+ }
+
+ /**
+ * NAT64 prefix currently in use. Only valid in STARTING or RUNNING states.
+ * Used, among other things, to avoid updates when switching from a prefix learned from one
+ * source (e.g., RA) to the same prefix learned from another source (e.g., RA).
+ */
+ private IpPrefix mNat64PrefixInUse;
+ /** NAT64 prefix (if any) discovered from DNS via RFC 7050. */
+ private IpPrefix mNat64PrefixFromDns;
+ /** NAT64 prefix (if any) learned from the network via RA. */
+ private IpPrefix mNat64PrefixFromRa;
+ private String mBaseIface;
+ private String mIface;
+ private Inet6Address mIPv6Address;
+ private State mState = State.IDLE;
+
+ private boolean mEnableClatOnCellular;
+ private boolean mPrefixDiscoveryRunning;
+
+ public Nat464Xlat(NetworkAgentInfo nai, INetd netd, IDnsResolver dnsResolver,
+ ConnectivityService.Dependencies deps) {
+ mDnsResolver = dnsResolver;
+ mNetd = netd;
+ mNetwork = nai;
+ mEnableClatOnCellular = deps.getCellular464XlatEnabled();
+ }
+
+ /**
+ * Whether to attempt 464xlat on this network. This is true for an IPv6-only network that is
+ * currently connected and where the NetworkAgent has not disabled 464xlat. It is the signal to
+ * enable NAT64 prefix discovery.
+ *
+ * @param nai the NetworkAgentInfo corresponding to the network.
+ * @return true if the network requires clat, false otherwise.
+ */
+ @VisibleForTesting
+ protected boolean requiresClat(NetworkAgentInfo nai) {
+ // TODO: migrate to NetworkCapabilities.TRANSPORT_*.
+ final boolean supported = contains(NETWORK_TYPES, nai.networkInfo.getType());
+ final boolean connected = contains(NETWORK_STATES, nai.networkInfo.getState());
+
+ // Only run clat on networks that have a global IPv6 address and don't have a native IPv4
+ // address.
+ LinkProperties lp = nai.linkProperties;
+ final boolean isIpv6OnlyNetwork = (lp != null) && lp.hasGlobalIpv6Address()
+ && !lp.hasIpv4Address();
+
+ // If the network tells us it doesn't use clat, respect that.
+ final boolean skip464xlat = (nai.netAgentConfig() != null)
+ && nai.netAgentConfig().skip464xlat;
+
+ return supported && connected && isIpv6OnlyNetwork && !skip464xlat
+ && (nai.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)
+ ? isCellular464XlatEnabled() : true);
+ }
+
+ /**
+ * Whether the clat demon should be started on this network now. This is true if requiresClat is
+ * true and a NAT64 prefix has been discovered.
+ *
+ * @param nai the NetworkAgentInfo corresponding to the network.
+ * @return true if the network should start clat, false otherwise.
+ */
+ @VisibleForTesting
+ protected boolean shouldStartClat(NetworkAgentInfo nai) {
+ LinkProperties lp = nai.linkProperties;
+ return requiresClat(nai) && lp != null && lp.getNat64Prefix() != null;
+ }
+
+ /**
+ * @return true if clatd has been started and has not yet stopped.
+ * A true result corresponds to internal states STARTING and RUNNING.
+ */
+ public boolean isStarted() {
+ return (mState == State.STARTING || mState == State.RUNNING);
+ }
+
+ /**
+ * @return true if clatd has been started but the stacked interface is not yet up.
+ */
+ public boolean isStarting() {
+ return mState == State.STARTING;
+ }
+
+ /**
+ * @return true if clatd has been started and the stacked interface is up.
+ */
+ public boolean isRunning() {
+ return mState == State.RUNNING;
+ }
+
+ /**
+ * Start clatd, register this Nat464Xlat as a network observer for the stacked interface,
+ * and set internal state.
+ */
+ private void enterStartingState(String baseIface) {
+ mNat64PrefixInUse = selectNat64Prefix();
+ String addrStr = null;
+ try {
+ addrStr = mNetd.clatdStart(baseIface, mNat64PrefixInUse.toString());
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Error starting clatd on " + baseIface + ": " + e);
+ }
+ mIface = CLAT_PREFIX + baseIface;
+ mBaseIface = baseIface;
+ mState = State.STARTING;
+ try {
+ mIPv6Address = (Inet6Address) InetAddresses.parseNumericAddress(addrStr);
+ } catch (ClassCastException | IllegalArgumentException | NullPointerException e) {
+ Log.e(TAG, "Invalid IPv6 address " + addrStr);
+ }
+ if (mPrefixDiscoveryRunning && !isPrefixDiscoveryNeeded()) {
+ stopPrefixDiscovery();
+ }
+ if (!mPrefixDiscoveryRunning) {
+ setPrefix64(mNat64PrefixInUse);
+ }
+ }
+
+ /**
+ * Enter running state just after getting confirmation that the stacked interface is up, and
+ * turn ND offload off if on WiFi.
+ */
+ private void enterRunningState() {
+ mState = State.RUNNING;
+ }
+
+ /**
+ * Unregister as a base observer for the stacked interface, and clear internal state.
+ */
+ private void leaveStartedState() {
+ mNat64PrefixInUse = null;
+ mIface = null;
+ mBaseIface = null;
+
+ if (!mPrefixDiscoveryRunning) {
+ setPrefix64(null);
+ }
+
+ if (isPrefixDiscoveryNeeded()) {
+ if (!mPrefixDiscoveryRunning) {
+ startPrefixDiscovery();
+ }
+ mState = State.DISCOVERING;
+ } else {
+ stopPrefixDiscovery();
+ mState = State.IDLE;
+ }
+ }
+
+ @VisibleForTesting
+ protected void start() {
+ if (isStarted()) {
+ Log.e(TAG, "startClat: already started");
+ return;
+ }
+
+ String baseIface = mNetwork.linkProperties.getInterfaceName();
+ if (baseIface == null) {
+ Log.e(TAG, "startClat: Can't start clat on null interface");
+ return;
+ }
+ // TODO: should we only do this if mNetd.clatdStart() succeeds?
+ Log.i(TAG, "Starting clatd on " + baseIface);
+ enterStartingState(baseIface);
+ }
+
+ @VisibleForTesting
+ protected void stop() {
+ if (!isStarted()) {
+ Log.e(TAG, "stopClat: already stopped");
+ return;
+ }
+
+ Log.i(TAG, "Stopping clatd on " + mBaseIface);
+ try {
+ mNetd.clatdStop(mBaseIface);
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Error stopping clatd on " + mBaseIface + ": " + e);
+ }
+
+ String iface = mIface;
+ boolean wasRunning = isRunning();
+
+ // Change state before updating LinkProperties. handleUpdateLinkProperties ends up calling
+ // fixupLinkProperties, and if at that time the state is still RUNNING, fixupLinkProperties
+ // would wrongly inform ConnectivityService that there is still a stacked interface.
+ leaveStartedState();
+
+ if (wasRunning) {
+ LinkProperties lp = new LinkProperties(mNetwork.linkProperties);
+ lp.removeStackedLink(iface);
+ mNetwork.connService().handleUpdateLinkProperties(mNetwork, lp);
+ }
+ }
+
+ private void startPrefixDiscovery() {
+ try {
+ mDnsResolver.startPrefix64Discovery(getNetId());
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Error starting prefix discovery on netId " + getNetId() + ": " + e);
+ }
+ mPrefixDiscoveryRunning = true;
+ }
+
+ private void stopPrefixDiscovery() {
+ try {
+ mDnsResolver.stopPrefix64Discovery(getNetId());
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Error stopping prefix discovery on netId " + getNetId() + ": " + e);
+ }
+ mPrefixDiscoveryRunning = false;
+ }
+
+ private boolean isPrefixDiscoveryNeeded() {
+ // If there is no NAT64 prefix in the RA, prefix discovery is always needed. It cannot be
+ // stopped after it succeeds, because stopping it will cause netd to report that the prefix
+ // has been removed, and that will cause us to stop clatd.
+ return requiresClat(mNetwork) && mNat64PrefixFromRa == null;
+ }
+
+ private void setPrefix64(IpPrefix prefix) {
+ final String prefixString = (prefix != null) ? prefix.toString() : "";
+ try {
+ mDnsResolver.setPrefix64(getNetId(), prefixString);
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Error setting NAT64 prefix on netId " + getNetId() + " to "
+ + prefix + ": " + e);
+ }
+ }
+
+ private void maybeHandleNat64PrefixChange() {
+ final IpPrefix newPrefix = selectNat64Prefix();
+ if (!Objects.equals(mNat64PrefixInUse, newPrefix)) {
+ Log.d(TAG, "NAT64 prefix changed from " + mNat64PrefixInUse + " to "
+ + newPrefix);
+ stop();
+ // It's safe to call update here, even though this method is called from update, because
+ // stop() is guaranteed to have moved out of STARTING and RUNNING, which are the only
+ // states in which this method can be called.
+ update();
+ }
+ }
+
+ /**
+ * Starts/stops NAT64 prefix discovery and clatd as necessary.
+ */
+ public void update() {
+ // TODO: turn this class into a proper StateMachine. http://b/126113090
+ switch (mState) {
+ case IDLE:
+ if (isPrefixDiscoveryNeeded()) {
+ startPrefixDiscovery(); // Enters DISCOVERING state.
+ mState = State.DISCOVERING;
+ } else if (requiresClat(mNetwork)) {
+ start(); // Enters STARTING state.
+ }
+ break;
+
+ case DISCOVERING:
+ if (shouldStartClat(mNetwork)) {
+ // NAT64 prefix detected. Start clatd.
+ start(); // Enters STARTING state.
+ return;
+ }
+ if (!requiresClat(mNetwork)) {
+ // IPv4 address added. Go back to IDLE state.
+ stopPrefixDiscovery();
+ mState = State.IDLE;
+ return;
+ }
+ break;
+
+ case STARTING:
+ case RUNNING:
+ // NAT64 prefix removed, or IPv4 address added.
+ // Stop clatd and go back into DISCOVERING or idle.
+ if (!shouldStartClat(mNetwork)) {
+ stop();
+ break;
+ }
+ // Only necessary while clat is actually started.
+ maybeHandleNat64PrefixChange();
+ break;
+ }
+ }
+
+ /**
+ * Picks a NAT64 prefix to use. Always prefers the prefix from the RA if one is received from
+ * both RA and DNS, because the prefix in the RA has better security and updatability, and will
+ * almost always be received first anyway.
+ *
+ * Any network that supports legacy hosts will support discovering the DNS64 prefix via DNS as
+ * well. If the prefix from the RA is withdrawn, fall back to that for reliability purposes.
+ */
+ private IpPrefix selectNat64Prefix() {
+ return mNat64PrefixFromRa != null ? mNat64PrefixFromRa : mNat64PrefixFromDns;
+ }
+
+ public void setNat64PrefixFromRa(IpPrefix prefix) {
+ mNat64PrefixFromRa = prefix;
+ }
+
+ public void setNat64PrefixFromDns(IpPrefix prefix) {
+ mNat64PrefixFromDns = prefix;
+ }
+
+ /**
+ * Copies the stacked clat link in oldLp, if any, to the passed LinkProperties.
+ * This is necessary because the LinkProperties in mNetwork come from the transport layer, which
+ * has no idea that 464xlat is running on top of it.
+ */
+ public void fixupLinkProperties(@NonNull LinkProperties oldLp, @NonNull LinkProperties lp) {
+ // This must be done even if clatd is not running, because otherwise shouldStartClat would
+ // never return true.
+ lp.setNat64Prefix(selectNat64Prefix());
+
+ if (!isRunning()) {
+ return;
+ }
+ if (lp.getAllInterfaceNames().contains(mIface)) {
+ return;
+ }
+
+ Log.d(TAG, "clatd running, updating NAI for " + mIface);
+ for (LinkProperties stacked: oldLp.getStackedLinks()) {
+ if (Objects.equals(mIface, stacked.getInterfaceName())) {
+ lp.addStackedLink(stacked);
+ return;
+ }
+ }
+ }
+
+ private LinkProperties makeLinkProperties(LinkAddress clatAddress) {
+ LinkProperties stacked = new LinkProperties();
+ stacked.setInterfaceName(mIface);
+
+ // Although the clat interface is a point-to-point tunnel, we don't
+ // point the route directly at the interface because some apps don't
+ // understand routes without gateways (see, e.g., http://b/9597256
+ // http://b/9597516). Instead, set the next hop of the route to the
+ // clat IPv4 address itself (for those apps, it doesn't matter what
+ // the IP of the gateway is, only that there is one).
+ RouteInfo ipv4Default = new RouteInfo(
+ new LinkAddress(NetworkStackConstants.IPV4_ADDR_ANY, 0),
+ clatAddress.getAddress(), mIface);
+ stacked.addRoute(ipv4Default);
+ stacked.addLinkAddress(clatAddress);
+ return stacked;
+ }
+
+ private LinkAddress getLinkAddress(String iface) {
+ try {
+ final InterfaceConfigurationParcel config = mNetd.interfaceGetCfg(iface);
+ return new LinkAddress(
+ InetAddresses.parseNumericAddress(config.ipv4Addr), config.prefixLength);
+ } catch (IllegalArgumentException | RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Error getting link properties: " + e);
+ return null;
+ }
+ }
+
+ /**
+ * Adds stacked link on base link and transitions to RUNNING state.
+ */
+ private void handleInterfaceLinkStateChanged(String iface, boolean up) {
+ // TODO: if we call start(), then stop(), then start() again, and the
+ // interfaceLinkStateChanged notification for the first start is delayed past the first
+ // stop, then the code becomes out of sync with system state and will behave incorrectly.
+ //
+ // This is not trivial to fix because:
+ // 1. It is not guaranteed that start() will eventually result in the interface coming up,
+ // because there could be an error starting clat (e.g., if the interface goes down before
+ // the packet socket can be bound).
+ // 2. If start is called multiple times, there is nothing in the interfaceLinkStateChanged
+ // notification that says which start() call the interface was created by.
+ //
+ // Once this code is converted to StateMachine, it will be possible to use deferMessage to
+ // ensure it stays in STARTING state until the interfaceLinkStateChanged notification fires,
+ // and possibly use a timeout (or provide some guarantees at the lower layer) to address #1.
+ if (!isStarting() || !up || !Objects.equals(mIface, iface)) {
+ return;
+ }
+
+ LinkAddress clatAddress = getLinkAddress(iface);
+ if (clatAddress == null) {
+ Log.e(TAG, "clatAddress was null for stacked iface " + iface);
+ return;
+ }
+
+ Log.i(TAG, String.format("interface %s is up, adding stacked link %s on top of %s",
+ mIface, mIface, mBaseIface));
+ enterRunningState();
+ LinkProperties lp = new LinkProperties(mNetwork.linkProperties);
+ lp.addStackedLink(makeLinkProperties(clatAddress));
+ mNetwork.connService().handleUpdateLinkProperties(mNetwork, lp);
+ }
+
+ /**
+ * Removes stacked link on base link and transitions to IDLE state.
+ */
+ private void handleInterfaceRemoved(String iface) {
+ if (!Objects.equals(mIface, iface)) {
+ return;
+ }
+ if (!isRunning()) {
+ return;
+ }
+
+ Log.i(TAG, "interface " + iface + " removed");
+ // If we're running, and the interface was removed, then we didn't call stop(), and it's
+ // likely that clatd crashed. Ensure we call stop() so we can start clatd again. Calling
+ // stop() will also update LinkProperties, and if clatd crashed, the LinkProperties update
+ // will cause ConnectivityService to call start() again.
+ stop();
+ }
+
+ public void interfaceLinkStateChanged(String iface, boolean up) {
+ mNetwork.handler().post(() -> { handleInterfaceLinkStateChanged(iface, up); });
+ }
+
+ public void interfaceRemoved(String iface) {
+ mNetwork.handler().post(() -> handleInterfaceRemoved(iface));
+ }
+
+ @Override
+ public String toString() {
+ return "mBaseIface: " + mBaseIface + ", mIface: " + mIface + ", mState: " + mState;
+ }
+
+ @VisibleForTesting
+ protected int getNetId() {
+ return mNetwork.network.getNetId();
+ }
+
+ @VisibleForTesting
+ protected boolean isCellular464XlatEnabled() {
+ return mEnableClatOnCellular;
+ }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
new file mode 100644
index 0000000..ee32fbf
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -0,0 +1,1127 @@
+/*
+ * Copyright (C) 2014 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.connectivity;
+
+import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport;
+import static android.net.NetworkCapabilities.transportNamesOf;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.CaptivePortalData;
+import android.net.IDnsResolver;
+import android.net.INetd;
+import android.net.INetworkAgent;
+import android.net.INetworkAgentRegistry;
+import android.net.INetworkMonitor;
+import android.net.LinkProperties;
+import android.net.NattKeepalivePacketData;
+import android.net.Network;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkMonitorManager;
+import android.net.NetworkRequest;
+import android.net.NetworkScore;
+import android.net.NetworkStateSnapshot;
+import android.net.QosCallbackException;
+import android.net.QosFilter;
+import android.net.QosFilterParcelable;
+import android.net.QosSession;
+import android.net.TcpKeepalivePacketData;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.telephony.data.EpsBearerQosSessionAttributes;
+import android.telephony.data.NrQosSessionAttributes;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+
+import com.android.internal.util.WakeupMessage;
+import com.android.server.ConnectivityService;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * A bag class used by ConnectivityService for holding a collection of most recent
+ * information published by a particular NetworkAgent as well as the
+ * AsyncChannel/messenger for reaching that NetworkAgent and lists of NetworkRequests
+ * interested in using it. Default sort order is descending by score.
+ */
+// States of a network:
+// --------------------
+// 1. registered, uncreated, disconnected, unvalidated
+// This state is entered when a NetworkFactory registers a NetworkAgent in any state except
+// the CONNECTED state.
+// 2. registered, uncreated, connecting, unvalidated
+// This state is entered when a registered NetworkAgent for a VPN network transitions to the
+// CONNECTING state (TODO: go through this state for every network, not just VPNs).
+// ConnectivityService will tell netd to create the network early in order to add extra UID
+// routing rules referencing the netID. These rules need to be in place before the network is
+// connected to avoid racing against client apps trying to connect to a half-setup network.
+// 3. registered, uncreated, connected, unvalidated
+// This state is entered when a registered NetworkAgent transitions to the CONNECTED state.
+// ConnectivityService will tell netd to create the network if it was not already created, and
+// immediately transition to state #4.
+// 4. registered, created, connected, unvalidated
+// If this network can satisfy the default NetworkRequest, then NetworkMonitor will
+// probe for Internet connectivity.
+// If this network cannot satisfy the default NetworkRequest, it will immediately be
+// transitioned to state #5.
+// A network may remain in this state if NetworkMonitor fails to find Internet connectivity,
+// for example:
+// a. a captive portal is present, or
+// b. a WiFi router whose Internet backhaul is down, or
+// c. a wireless connection stops transfering packets temporarily (e.g. device is in elevator
+// or tunnel) but does not disconnect from the AP/cell tower, or
+// d. a stand-alone device offering a WiFi AP without an uplink for configuration purposes.
+// 5. registered, created, connected, validated
+//
+// The device's default network connection:
+// ----------------------------------------
+// Networks in states #4 and #5 may be used as a device's default network connection if they
+// satisfy the default NetworkRequest.
+// A network, that satisfies the default NetworkRequest, in state #5 should always be chosen
+// in favor of a network, that satisfies the default NetworkRequest, in state #4.
+// When deciding between two networks, that both satisfy the default NetworkRequest, to select
+// for the default network connection, the one with the higher score should be chosen.
+//
+// When a network disconnects:
+// ---------------------------
+// If a network's transport disappears, for example:
+// a. WiFi turned off, or
+// b. cellular data turned off, or
+// c. airplane mode is turned on, or
+// d. a wireless connection disconnects from AP/cell tower entirely (e.g. device is out of range
+// of AP for an extended period of time, or switches to another AP without roaming)
+// then that network can transition from any state (#1-#5) to unregistered. This happens by
+// the transport disconnecting their NetworkAgent's AsyncChannel with ConnectivityManager.
+// ConnectivityService also tells netd to destroy the network.
+//
+// When ConnectivityService disconnects a network:
+// -----------------------------------------------
+// If a network is just connected, ConnectivityService will think it will be used soon, but might
+// not be used. Thus, a 5s timer will be held to prevent the network being torn down immediately.
+// This "nascent" state is implemented by the "lingering" logic below without relating to any
+// request, and is used in some cases where network requests race with network establishment. The
+// nascent state ends when the 5-second timer fires, or as soon as the network satisfies a
+// request, whichever is earlier. In this state, the network is considered in the background.
+//
+// If a network has no chance of satisfying any requests (even if it were to become validated
+// and enter state #5), ConnectivityService will disconnect the NetworkAgent's AsyncChannel.
+//
+// If the network was satisfying a foreground NetworkRequest (i.e. had been the highest scoring that
+// satisfied the NetworkRequest's constraints), but is no longer the highest scoring network for any
+// foreground NetworkRequest, then there will be a 30s pause to allow network communication to be
+// wrapped up rather than abruptly terminated. During this pause the network is said to be
+// "lingering". During this pause if the network begins satisfying a foreground NetworkRequest,
+// ConnectivityService will cancel the future disconnection of the NetworkAgent's AsyncChannel, and
+// the network is no longer considered "lingering". After the linger timer expires, if the network
+// is satisfying one or more background NetworkRequests it is kept up in the background. If it is
+// not, ConnectivityService disconnects the NetworkAgent's AsyncChannel.
+public class NetworkAgentInfo implements Comparable<NetworkAgentInfo> {
+
+ @NonNull public NetworkInfo networkInfo;
+ // This Network object should always be used if possible, so as to encourage reuse of the
+ // enclosed socket factory and connection pool. Avoid creating other Network objects.
+ // This Network object is always valid.
+ @NonNull public final Network network;
+ @NonNull public LinkProperties linkProperties;
+ // This should only be modified by ConnectivityService, via setNetworkCapabilities().
+ // TODO: make this private with a getter.
+ @NonNull public NetworkCapabilities networkCapabilities;
+ @NonNull public final NetworkAgentConfig networkAgentConfig;
+
+ // Underlying networks declared by the agent. Only set if supportsUnderlyingNetworks is true.
+ // The networks in this list might be declared by a VPN app using setUnderlyingNetworks and are
+ // not guaranteed to be current or correct, or even to exist.
+ //
+ // This array is read and iterated on multiple threads with no locking so its contents must
+ // never be modified. When the list of networks changes, replace with a new array, on the
+ // handler thread.
+ public @Nullable volatile Network[] declaredUnderlyingNetworks;
+
+ // The capabilities originally announced by the NetworkAgent, regardless of any capabilities
+ // that were added or removed due to this network's underlying networks.
+ // Only set if #supportsUnderlyingNetworks is true.
+ public @Nullable NetworkCapabilities declaredCapabilities;
+
+ // Indicates if netd has been told to create this Network. From this point on the appropriate
+ // routing rules are setup and routes are added so packets can begin flowing over the Network.
+ // This is a sticky bit; once set it is never cleared.
+ public boolean created;
+ // Set to true after the first time this network is marked as CONNECTED. Once set, the network
+ // shows up in API calls, is able to satisfy NetworkRequests and can become the default network.
+ // This is a sticky bit; once set it is never cleared.
+ public boolean everConnected;
+ // Set to true if this Network successfully passed validation or if it did not satisfy the
+ // default NetworkRequest in which case validation will not be attempted.
+ // This is a sticky bit; once set it is never cleared even if future validation attempts fail.
+ public boolean everValidated;
+
+ // The result of the last validation attempt on this network (true if validated, false if not).
+ public boolean lastValidated;
+
+ // If true, becoming unvalidated will lower the network's score. This is only meaningful if the
+ // system is configured not to do this for certain networks, e.g., if the
+ // config_networkAvoidBadWifi option is set to 0 and the user has not overridden that via
+ // Settings.Global.NETWORK_AVOID_BAD_WIFI.
+ public boolean avoidUnvalidated;
+
+ // Whether a captive portal was ever detected on this network.
+ // This is a sticky bit; once set it is never cleared.
+ public boolean everCaptivePortalDetected;
+
+ // Whether a captive portal was found during the last network validation attempt.
+ public boolean lastCaptivePortalDetected;
+
+ // Set to true when partial connectivity was detected.
+ public boolean partialConnectivity;
+
+ // Delay between when the network is disconnected and when the native network is destroyed.
+ public int teardownDelayMs;
+
+ // Captive portal info of the network from RFC8908, if any.
+ // Obtained by ConnectivityService and merged into NetworkAgent-provided information.
+ public CaptivePortalData capportApiData;
+
+ // The UID of the remote entity that created this Network.
+ public final int creatorUid;
+
+ // Network agent portal info of the network, if any. This information is provided from
+ // non-RFC8908 sources, such as Wi-Fi Passpoint, which can provide information such as Venue
+ // URL, Terms & Conditions URL, and network friendly name.
+ public CaptivePortalData networkAgentPortalData;
+
+ // Networks are lingered when they become unneeded as a result of their NetworkRequests being
+ // satisfied by a higher-scoring network. so as to allow communication to wrap up before the
+ // network is taken down. This usually only happens to the default network. Lingering ends with
+ // either the linger timeout expiring and the network being taken down, or the network
+ // satisfying a request again.
+ public static class InactivityTimer implements Comparable<InactivityTimer> {
+ public final int requestId;
+ public final long expiryMs;
+
+ public InactivityTimer(int requestId, long expiryMs) {
+ this.requestId = requestId;
+ this.expiryMs = expiryMs;
+ }
+ public boolean equals(Object o) {
+ if (!(o instanceof InactivityTimer)) return false;
+ InactivityTimer other = (InactivityTimer) o;
+ return (requestId == other.requestId) && (expiryMs == other.expiryMs);
+ }
+ public int hashCode() {
+ return Objects.hash(requestId, expiryMs);
+ }
+ public int compareTo(InactivityTimer other) {
+ return (expiryMs != other.expiryMs) ?
+ Long.compare(expiryMs, other.expiryMs) :
+ Integer.compare(requestId, other.requestId);
+ }
+ public String toString() {
+ return String.format("%s, expires %dms", requestId,
+ expiryMs - SystemClock.elapsedRealtime());
+ }
+ }
+
+ /**
+ * Inform ConnectivityService that the network LINGER period has
+ * expired.
+ * obj = this NetworkAgentInfo
+ */
+ public static final int EVENT_NETWORK_LINGER_COMPLETE = 1001;
+
+ /**
+ * Inform ConnectivityService that the agent is half-connected.
+ * arg1 = ARG_AGENT_SUCCESS or ARG_AGENT_FAILURE
+ * obj = NetworkAgentInfo
+ * @hide
+ */
+ public static final int EVENT_AGENT_REGISTERED = 1002;
+
+ /**
+ * Inform ConnectivityService that the agent was disconnected.
+ * obj = NetworkAgentInfo
+ * @hide
+ */
+ public static final int EVENT_AGENT_DISCONNECTED = 1003;
+
+ /**
+ * Argument for EVENT_AGENT_HALF_CONNECTED indicating failure.
+ */
+ public static final int ARG_AGENT_FAILURE = 0;
+
+ /**
+ * Argument for EVENT_AGENT_HALF_CONNECTED indicating success.
+ */
+ public static final int ARG_AGENT_SUCCESS = 1;
+
+ // All inactivity timers for this network, sorted by expiry time. A timer is added whenever
+ // a request is moved to a network with a better score, regardless of whether the network is or
+ // was lingering or not. An inactivity timer is also added when a network connects
+ // without immediately satisfying any requests.
+ // TODO: determine if we can replace this with a smaller or unsorted data structure. (e.g.,
+ // SparseLongArray) combined with the timestamp of when the last timer is scheduled to fire.
+ private final SortedSet<InactivityTimer> mInactivityTimers = new TreeSet<>();
+
+ // For fast lookups. Indexes into mInactivityTimers by request ID.
+ private final SparseArray<InactivityTimer> mInactivityTimerForRequest = new SparseArray<>();
+
+ // Inactivity expiry timer. Armed whenever mInactivityTimers is non-empty, regardless of
+ // whether the network is inactive or not. Always set to the expiry of the mInactivityTimers
+ // that expires last. When the timer fires, all inactivity state is cleared, and if the network
+ // has no requests, it is torn down.
+ private WakeupMessage mInactivityMessage;
+
+ // Inactivity expiry. Holds the expiry time of the inactivity timer, or 0 if the timer is not
+ // armed.
+ private long mInactivityExpiryMs;
+
+ // Whether the network is inactive or not. Must be maintained separately from the above because
+ // it depends on the state of other networks and requests, which only ConnectivityService knows.
+ // (Example: we don't linger a network if it would become the best for a NetworkRequest if it
+ // validated).
+ private boolean mInactive;
+
+ // This represents the quality of the network. As opposed to NetworkScore, FullScore includes
+ // the ConnectivityService-managed bits.
+ private FullScore mScore;
+
+ // The list of NetworkRequests being satisfied by this Network.
+ private final SparseArray<NetworkRequest> mNetworkRequests = new SparseArray<>();
+
+ // How many of the satisfied requests are actual requests and not listens.
+ private int mNumRequestNetworkRequests = 0;
+
+ // How many of the satisfied requests are of type BACKGROUND_REQUEST.
+ private int mNumBackgroundNetworkRequests = 0;
+
+ // The last ConnectivityReport made available for this network. This value is only null before a
+ // report is generated. Once non-null, it will never be null again.
+ @Nullable private ConnectivityReport mConnectivityReport;
+
+ public final INetworkAgent networkAgent;
+ // Only accessed from ConnectivityService handler thread
+ private final AgentDeathMonitor mDeathMonitor = new AgentDeathMonitor();
+
+ public final int factorySerialNumber;
+
+ // Used by ConnectivityService to keep track of 464xlat.
+ public final Nat464Xlat clatd;
+
+ // Set after asynchronous creation of the NetworkMonitor.
+ private volatile NetworkMonitorManager mNetworkMonitor;
+
+ private static final String TAG = ConnectivityService.class.getSimpleName();
+ private static final boolean VDBG = false;
+ private final ConnectivityService mConnService;
+ private final Context mContext;
+ private final Handler mHandler;
+ private final QosCallbackTracker mQosCallbackTracker;
+
+ public NetworkAgentInfo(INetworkAgent na, Network net, NetworkInfo info,
+ @NonNull LinkProperties lp, @NonNull NetworkCapabilities nc,
+ @NonNull NetworkScore score, Context context,
+ Handler handler, NetworkAgentConfig config, ConnectivityService connService, INetd netd,
+ IDnsResolver dnsResolver, int factorySerialNumber, int creatorUid,
+ QosCallbackTracker qosCallbackTracker, ConnectivityService.Dependencies deps) {
+ Objects.requireNonNull(net);
+ Objects.requireNonNull(info);
+ Objects.requireNonNull(lp);
+ Objects.requireNonNull(nc);
+ Objects.requireNonNull(context);
+ Objects.requireNonNull(config);
+ Objects.requireNonNull(qosCallbackTracker);
+ networkAgent = na;
+ network = net;
+ networkInfo = info;
+ linkProperties = lp;
+ networkCapabilities = nc;
+ networkAgentConfig = config;
+ setScore(score); // uses members networkCapabilities and networkAgentConfig
+ clatd = new Nat464Xlat(this, netd, dnsResolver, deps);
+ mConnService = connService;
+ mContext = context;
+ mHandler = handler;
+ this.factorySerialNumber = factorySerialNumber;
+ this.creatorUid = creatorUid;
+ mQosCallbackTracker = qosCallbackTracker;
+ }
+
+ private class AgentDeathMonitor implements IBinder.DeathRecipient {
+ @Override
+ public void binderDied() {
+ notifyDisconnected();
+ }
+ }
+
+ /**
+ * Notify the NetworkAgent that it was registered, and should be unregistered if it dies.
+ *
+ * Must be called from the ConnectivityService handler thread. A NetworkAgent can only be
+ * registered once.
+ */
+ public void notifyRegistered() {
+ try {
+ networkAgent.asBinder().linkToDeath(mDeathMonitor, 0);
+ networkAgent.onRegistered(new NetworkAgentMessageHandler(mHandler));
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error registering NetworkAgent", e);
+ maybeUnlinkDeathMonitor();
+ mHandler.obtainMessage(EVENT_AGENT_REGISTERED, ARG_AGENT_FAILURE, 0, this)
+ .sendToTarget();
+ return;
+ }
+
+ mHandler.obtainMessage(EVENT_AGENT_REGISTERED, ARG_AGENT_SUCCESS, 0, this).sendToTarget();
+ }
+
+ /**
+ * Disconnect the NetworkAgent. Must be called from the ConnectivityService handler thread.
+ */
+ public void disconnect() {
+ try {
+ networkAgent.onDisconnected();
+ } catch (RemoteException e) {
+ Log.i(TAG, "Error disconnecting NetworkAgent", e);
+ // Fall through: it's fine if the remote has died
+ }
+
+ notifyDisconnected();
+ maybeUnlinkDeathMonitor();
+ }
+
+ private void maybeUnlinkDeathMonitor() {
+ try {
+ networkAgent.asBinder().unlinkToDeath(mDeathMonitor, 0);
+ } catch (NoSuchElementException e) {
+ // Was not linked: ignore
+ }
+ }
+
+ private void notifyDisconnected() {
+ // Note this may be called multiple times if ConnectivityService disconnects while the
+ // NetworkAgent also dies. ConnectivityService ignores disconnects of already disconnected
+ // agents.
+ mHandler.obtainMessage(EVENT_AGENT_DISCONNECTED, this).sendToTarget();
+ }
+
+ /**
+ * Notify the NetworkAgent that bandwidth update was requested.
+ */
+ public void onBandwidthUpdateRequested() {
+ try {
+ networkAgent.onBandwidthUpdateRequested();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending bandwidth update request event", e);
+ }
+ }
+
+ /**
+ * Notify the NetworkAgent that validation status has changed.
+ */
+ public void onValidationStatusChanged(int validationStatus, @Nullable String captivePortalUrl) {
+ try {
+ networkAgent.onValidationStatusChanged(validationStatus, captivePortalUrl);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending validation status change event", e);
+ }
+ }
+
+ /**
+ * Notify the NetworkAgent that the acceptUnvalidated setting should be saved.
+ */
+ public void onSaveAcceptUnvalidated(boolean acceptUnvalidated) {
+ try {
+ networkAgent.onSaveAcceptUnvalidated(acceptUnvalidated);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending accept unvalidated event", e);
+ }
+ }
+
+ /**
+ * Notify the NetworkAgent that NATT socket keepalive should be started.
+ */
+ public void onStartNattSocketKeepalive(int slot, int intervalDurationMs,
+ @NonNull NattKeepalivePacketData packetData) {
+ try {
+ networkAgent.onStartNattSocketKeepalive(slot, intervalDurationMs, packetData);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending NATT socket keepalive start event", e);
+ }
+ }
+
+ /**
+ * Notify the NetworkAgent that TCP socket keepalive should be started.
+ */
+ public void onStartTcpSocketKeepalive(int slot, int intervalDurationMs,
+ @NonNull TcpKeepalivePacketData packetData) {
+ try {
+ networkAgent.onStartTcpSocketKeepalive(slot, intervalDurationMs, packetData);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending TCP socket keepalive start event", e);
+ }
+ }
+
+ /**
+ * Notify the NetworkAgent that socket keepalive should be stopped.
+ */
+ public void onStopSocketKeepalive(int slot) {
+ try {
+ networkAgent.onStopSocketKeepalive(slot);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending TCP socket keepalive stop event", e);
+ }
+ }
+
+ /**
+ * Notify the NetworkAgent that signal strength thresholds should be updated.
+ */
+ public void onSignalStrengthThresholdsUpdated(@NonNull int[] thresholds) {
+ try {
+ networkAgent.onSignalStrengthThresholdsUpdated(thresholds);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending signal strength thresholds event", e);
+ }
+ }
+
+ /**
+ * Notify the NetworkAgent that automatic reconnect should be prevented.
+ */
+ public void onPreventAutomaticReconnect() {
+ try {
+ networkAgent.onPreventAutomaticReconnect();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending prevent automatic reconnect event", e);
+ }
+ }
+
+ /**
+ * Notify the NetworkAgent that a NATT keepalive packet filter should be added.
+ */
+ public void onAddNattKeepalivePacketFilter(int slot,
+ @NonNull NattKeepalivePacketData packetData) {
+ try {
+ networkAgent.onAddNattKeepalivePacketFilter(slot, packetData);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending add NATT keepalive packet filter event", e);
+ }
+ }
+
+ /**
+ * Notify the NetworkAgent that a TCP keepalive packet filter should be added.
+ */
+ public void onAddTcpKeepalivePacketFilter(int slot,
+ @NonNull TcpKeepalivePacketData packetData) {
+ try {
+ networkAgent.onAddTcpKeepalivePacketFilter(slot, packetData);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending add TCP keepalive packet filter event", e);
+ }
+ }
+
+ /**
+ * Notify the NetworkAgent that a keepalive packet filter should be removed.
+ */
+ public void onRemoveKeepalivePacketFilter(int slot) {
+ try {
+ networkAgent.onRemoveKeepalivePacketFilter(slot);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending remove keepalive packet filter event", e);
+ }
+ }
+
+ /**
+ * Notify the NetworkAgent that the qos filter should be registered against the given qos
+ * callback id.
+ */
+ public void onQosFilterCallbackRegistered(final int qosCallbackId,
+ final QosFilter qosFilter) {
+ try {
+ networkAgent.onQosFilterCallbackRegistered(qosCallbackId,
+ new QosFilterParcelable(qosFilter));
+ } catch (final RemoteException e) {
+ Log.e(TAG, "Error registering a qos callback id against a qos filter", e);
+ }
+ }
+
+ /**
+ * Notify the NetworkAgent that the given qos callback id should be unregistered.
+ */
+ public void onQosCallbackUnregistered(final int qosCallbackId) {
+ try {
+ networkAgent.onQosCallbackUnregistered(qosCallbackId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error unregistering a qos callback id", e);
+ }
+ }
+
+ /**
+ * Notify the NetworkAgent that the network is successfully connected.
+ */
+ public void onNetworkCreated() {
+ try {
+ networkAgent.onNetworkCreated();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending network created event", e);
+ }
+ }
+
+ /**
+ * Notify the NetworkAgent that the native network has been destroyed.
+ */
+ public void onNetworkDestroyed() {
+ try {
+ networkAgent.onNetworkDestroyed();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error sending network destroyed event", e);
+ }
+ }
+
+ // TODO: consider moving out of NetworkAgentInfo into its own class
+ private class NetworkAgentMessageHandler extends INetworkAgentRegistry.Stub {
+ private final Handler mHandler;
+
+ private NetworkAgentMessageHandler(Handler handler) {
+ mHandler = handler;
+ }
+
+ @Override
+ public void sendNetworkCapabilities(@NonNull NetworkCapabilities nc) {
+ Objects.requireNonNull(nc);
+ mHandler.obtainMessage(NetworkAgent.EVENT_NETWORK_CAPABILITIES_CHANGED,
+ new Pair<>(NetworkAgentInfo.this, nc)).sendToTarget();
+ }
+
+ @Override
+ public void sendLinkProperties(@NonNull LinkProperties lp) {
+ Objects.requireNonNull(lp);
+ mHandler.obtainMessage(NetworkAgent.EVENT_NETWORK_PROPERTIES_CHANGED,
+ new Pair<>(NetworkAgentInfo.this, lp)).sendToTarget();
+ }
+
+ @Override
+ public void sendNetworkInfo(@NonNull NetworkInfo info) {
+ Objects.requireNonNull(info);
+ mHandler.obtainMessage(NetworkAgent.EVENT_NETWORK_INFO_CHANGED,
+ new Pair<>(NetworkAgentInfo.this, info)).sendToTarget();
+ }
+
+ @Override
+ public void sendScore(@NonNull final NetworkScore score) {
+ mHandler.obtainMessage(NetworkAgent.EVENT_NETWORK_SCORE_CHANGED,
+ new Pair<>(NetworkAgentInfo.this, score)).sendToTarget();
+ }
+
+ @Override
+ public void sendExplicitlySelected(boolean explicitlySelected, boolean acceptPartial) {
+ mHandler.obtainMessage(NetworkAgent.EVENT_SET_EXPLICITLY_SELECTED,
+ explicitlySelected ? 1 : 0, acceptPartial ? 1 : 0,
+ new Pair<>(NetworkAgentInfo.this, null)).sendToTarget();
+ }
+
+ @Override
+ public void sendSocketKeepaliveEvent(int slot, int reason) {
+ mHandler.obtainMessage(NetworkAgent.EVENT_SOCKET_KEEPALIVE,
+ slot, reason, new Pair<>(NetworkAgentInfo.this, null)).sendToTarget();
+ }
+
+ @Override
+ public void sendUnderlyingNetworks(@Nullable List<Network> networks) {
+ mHandler.obtainMessage(NetworkAgent.EVENT_UNDERLYING_NETWORKS_CHANGED,
+ new Pair<>(NetworkAgentInfo.this, networks)).sendToTarget();
+ }
+
+ @Override
+ public void sendEpsQosSessionAvailable(final int qosCallbackId, final QosSession session,
+ final EpsBearerQosSessionAttributes attributes) {
+ mQosCallbackTracker.sendEventEpsQosSessionAvailable(qosCallbackId, session, attributes);
+ }
+
+ @Override
+ public void sendNrQosSessionAvailable(final int qosCallbackId, final QosSession session,
+ final NrQosSessionAttributes attributes) {
+ mQosCallbackTracker.sendEventNrQosSessionAvailable(qosCallbackId, session, attributes);
+ }
+
+ @Override
+ public void sendQosSessionLost(final int qosCallbackId, final QosSession session) {
+ mQosCallbackTracker.sendEventQosSessionLost(qosCallbackId, session);
+ }
+
+ @Override
+ public void sendQosCallbackError(final int qosCallbackId,
+ @QosCallbackException.ExceptionType final int exceptionType) {
+ mQosCallbackTracker.sendEventQosCallbackError(qosCallbackId, exceptionType);
+ }
+
+ @Override
+ public void sendTeardownDelayMs(int teardownDelayMs) {
+ mHandler.obtainMessage(NetworkAgent.EVENT_TEARDOWN_DELAY_CHANGED,
+ teardownDelayMs, 0, new Pair<>(NetworkAgentInfo.this, null)).sendToTarget();
+ }
+ }
+
+ /**
+ * Inform NetworkAgentInfo that a new NetworkMonitor was created.
+ */
+ public void onNetworkMonitorCreated(INetworkMonitor networkMonitor) {
+ mNetworkMonitor = new NetworkMonitorManager(networkMonitor);
+ }
+
+ /**
+ * Set the NetworkCapabilities on this NetworkAgentInfo. Also attempts to notify NetworkMonitor
+ * of the new capabilities, if NetworkMonitor has been created.
+ *
+ * <p>If {@link NetworkMonitor#notifyNetworkCapabilitiesChanged(NetworkCapabilities)} fails,
+ * the exception is logged but not reported to callers.
+ *
+ * @return the old capabilities of this network.
+ */
+ @NonNull public synchronized NetworkCapabilities getAndSetNetworkCapabilities(
+ @NonNull final NetworkCapabilities nc) {
+ final NetworkCapabilities oldNc = networkCapabilities;
+ networkCapabilities = nc;
+ mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig);
+ final NetworkMonitorManager nm = mNetworkMonitor;
+ if (nm != null) {
+ nm.notifyNetworkCapabilitiesChanged(nc);
+ }
+ return oldNc;
+ }
+
+ public ConnectivityService connService() {
+ return mConnService;
+ }
+
+ public NetworkAgentConfig netAgentConfig() {
+ return networkAgentConfig;
+ }
+
+ public Handler handler() {
+ return mHandler;
+ }
+
+ public Network network() {
+ return network;
+ }
+
+ /**
+ * Get the NetworkMonitorManager in this NetworkAgentInfo.
+ *
+ * <p>This will be null before {@link #onNetworkMonitorCreated(INetworkMonitor)} is called.
+ */
+ public NetworkMonitorManager networkMonitor() {
+ return mNetworkMonitor;
+ }
+
+ // Functions for manipulating the requests satisfied by this network.
+ //
+ // These functions must only called on ConnectivityService's main thread.
+
+ private static final boolean ADD = true;
+ private static final boolean REMOVE = false;
+
+ private void updateRequestCounts(boolean add, NetworkRequest request) {
+ int delta = add ? +1 : -1;
+ switch (request.type) {
+ case REQUEST:
+ mNumRequestNetworkRequests += delta;
+ break;
+
+ case BACKGROUND_REQUEST:
+ mNumRequestNetworkRequests += delta;
+ mNumBackgroundNetworkRequests += delta;
+ break;
+
+ case LISTEN:
+ case LISTEN_FOR_BEST:
+ case TRACK_DEFAULT:
+ case TRACK_SYSTEM_DEFAULT:
+ break;
+
+ case NONE:
+ default:
+ Log.wtf(TAG, "Unhandled request type " + request.type);
+ break;
+ }
+ }
+
+ /**
+ * Add {@code networkRequest} to this network as it's satisfied by this network.
+ * @return true if {@code networkRequest} was added or false if {@code networkRequest} was
+ * already present.
+ */
+ public boolean addRequest(NetworkRequest networkRequest) {
+ NetworkRequest existing = mNetworkRequests.get(networkRequest.requestId);
+ if (existing == networkRequest) return false;
+ if (existing != null) {
+ // Should only happen if the requestId wraps. If that happens lots of other things will
+ // be broken as well.
+ Log.wtf(TAG, String.format("Duplicate requestId for %s and %s on %s",
+ networkRequest, existing, toShortString()));
+ updateRequestCounts(REMOVE, existing);
+ }
+ mNetworkRequests.put(networkRequest.requestId, networkRequest);
+ updateRequestCounts(ADD, networkRequest);
+ return true;
+ }
+
+ /**
+ * Remove the specified request from this network.
+ */
+ public void removeRequest(int requestId) {
+ NetworkRequest existing = mNetworkRequests.get(requestId);
+ if (existing == null) return;
+ updateRequestCounts(REMOVE, existing);
+ mNetworkRequests.remove(requestId);
+ if (existing.isRequest()) {
+ unlingerRequest(existing.requestId);
+ }
+ }
+
+ /**
+ * Returns whether this network is currently satisfying the request with the specified ID.
+ */
+ public boolean isSatisfyingRequest(int id) {
+ return mNetworkRequests.get(id) != null;
+ }
+
+ /**
+ * Returns the request at the specified position in the list of requests satisfied by this
+ * network.
+ */
+ public NetworkRequest requestAt(int index) {
+ return mNetworkRequests.valueAt(index);
+ }
+
+ /**
+ * Returns the number of requests currently satisfied by this network for which
+ * {@link android.net.NetworkRequest#isRequest} returns {@code true}.
+ */
+ public int numRequestNetworkRequests() {
+ return mNumRequestNetworkRequests;
+ }
+
+ /**
+ * Returns the number of requests currently satisfied by this network of type
+ * {@link android.net.NetworkRequest.Type.BACKGROUND_REQUEST}.
+ */
+ public int numBackgroundNetworkRequests() {
+ return mNumBackgroundNetworkRequests;
+ }
+
+ /**
+ * Returns the number of foreground requests currently satisfied by this network.
+ */
+ public int numForegroundNetworkRequests() {
+ return mNumRequestNetworkRequests - mNumBackgroundNetworkRequests;
+ }
+
+ /**
+ * Returns the number of requests of any type currently satisfied by this network.
+ */
+ public int numNetworkRequests() {
+ return mNetworkRequests.size();
+ }
+
+ /**
+ * Returns whether the network is a background network. A network is a background network if it
+ * does not have the NET_CAPABILITY_FOREGROUND capability, which implies it is satisfying no
+ * foreground request, is not lingering (i.e. kept for a while after being outscored), and is
+ * not a speculative network (i.e. kept pending validation when validation would have it
+ * outscore another foreground network). That implies it is being kept up by some background
+ * request (otherwise it would be torn down), maybe the mobile always-on request.
+ */
+ public boolean isBackgroundNetwork() {
+ return !isVPN() && numForegroundNetworkRequests() == 0 && mNumBackgroundNetworkRequests > 0
+ && !isLingering();
+ }
+
+ // Does this network satisfy request?
+ public boolean satisfies(NetworkRequest request) {
+ return created &&
+ request.networkCapabilities.satisfiedByNetworkCapabilities(networkCapabilities);
+ }
+
+ public boolean satisfiesImmutableCapabilitiesOf(NetworkRequest request) {
+ return created &&
+ request.networkCapabilities.satisfiedByImmutableNetworkCapabilities(
+ networkCapabilities);
+ }
+
+ /** Whether this network is a VPN. */
+ public boolean isVPN() {
+ return networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
+ }
+
+ /** Whether this network might have underlying networks. Currently only true for VPNs. */
+ public boolean supportsUnderlyingNetworks() {
+ return isVPN();
+ }
+
+ // Return true on devices configured to ignore score penalty for wifi networks
+ // that become unvalidated (b/31075769).
+ private boolean ignoreWifiUnvalidationPenalty() {
+ boolean isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
+ networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ boolean avoidBadWifi = mConnService.avoidBadWifi() || avoidUnvalidated;
+ return isWifi && !avoidBadWifi && everValidated;
+ }
+
+ // Get the current score for this Network. This may be modified from what the
+ // NetworkAgent sent, as it has modifiers applied to it.
+ public int getCurrentScore() {
+ return mScore.getLegacyInt();
+ }
+
+ // Get the current score for this Network as if it was validated. This may be modified from
+ // what the NetworkAgent sent, as it has modifiers applied to it.
+ public int getCurrentScoreAsValidated() {
+ return mScore.getLegacyIntAsValidated();
+ }
+
+ /**
+ * Mix-in the ConnectivityService-managed bits in the score.
+ */
+ public void setScore(final NetworkScore score) {
+ mScore = FullScore.fromNetworkScore(score, networkCapabilities, networkAgentConfig);
+ }
+
+ /**
+ * Update the ConnectivityService-managed bits in the score.
+ *
+ * Call this after updating the network agent config.
+ */
+ public void updateScoreForNetworkAgentConfigUpdate() {
+ mScore = mScore.mixInScore(networkCapabilities, networkAgentConfig);
+ }
+
+ /**
+ * Return a {@link NetworkStateSnapshot} for this network.
+ */
+ @NonNull
+ public NetworkStateSnapshot getNetworkStateSnapshot() {
+ synchronized (this) {
+ // Network objects are outwardly immutable so there is no point in duplicating.
+ // Duplicating also precludes sharing socket factories and connection pools.
+ final String subscriberId = (networkAgentConfig != null)
+ ? networkAgentConfig.subscriberId : null;
+ return new NetworkStateSnapshot(network, new NetworkCapabilities(networkCapabilities),
+ new LinkProperties(linkProperties), subscriberId, networkInfo.getType());
+ }
+ }
+
+ /**
+ * Sets the specified requestId to linger on this network for the specified time. Called by
+ * ConnectivityService when the request is moved to another network with a higher score, or
+ * when a network is newly created.
+ *
+ * @param requestId The requestId of the request that no longer need to be served by this
+ * network. Or {@link NetworkRequest.REQUEST_ID_NONE} if this is the
+ * {@code LingerTimer} for a newly created network.
+ */
+ public void lingerRequest(int requestId, long now, long duration) {
+ if (mInactivityTimerForRequest.get(requestId) != null) {
+ // Cannot happen. Once a request is lingering on a particular network, we cannot
+ // re-linger it unless that network becomes the best for that request again, in which
+ // case we should have unlingered it.
+ Log.wtf(TAG, toShortString() + ": request " + requestId + " already lingered");
+ }
+ final long expiryMs = now + duration;
+ InactivityTimer timer = new InactivityTimer(requestId, expiryMs);
+ if (VDBG) Log.d(TAG, "Adding InactivityTimer " + timer + " to " + toShortString());
+ mInactivityTimers.add(timer);
+ mInactivityTimerForRequest.put(requestId, timer);
+ }
+
+ /**
+ * Cancel lingering. Called by ConnectivityService when a request is added to this network.
+ * Returns true if the given requestId was lingering on this network, false otherwise.
+ */
+ public boolean unlingerRequest(int requestId) {
+ InactivityTimer timer = mInactivityTimerForRequest.get(requestId);
+ if (timer != null) {
+ if (VDBG) {
+ Log.d(TAG, "Removing InactivityTimer " + timer + " from " + toShortString());
+ }
+ mInactivityTimers.remove(timer);
+ mInactivityTimerForRequest.remove(requestId);
+ return true;
+ }
+ return false;
+ }
+
+ public long getInactivityExpiry() {
+ return mInactivityExpiryMs;
+ }
+
+ public void updateInactivityTimer() {
+ long newExpiry = mInactivityTimers.isEmpty() ? 0 : mInactivityTimers.last().expiryMs;
+ if (newExpiry == mInactivityExpiryMs) return;
+
+ // Even if we're going to reschedule the timer, cancel it first. This is because the
+ // semantics of WakeupMessage guarantee that if cancel is called then the alarm will
+ // never call its callback (handleLingerComplete), even if it has already fired.
+ // WakeupMessage makes no such guarantees about rescheduling a message, so if mLingerMessage
+ // has already been dispatched, rescheduling to some time in the future won't stop it
+ // from calling its callback immediately.
+ if (mInactivityMessage != null) {
+ mInactivityMessage.cancel();
+ mInactivityMessage = null;
+ }
+
+ if (newExpiry > 0) {
+ mInactivityMessage = new WakeupMessage(
+ mContext, mHandler,
+ "NETWORK_LINGER_COMPLETE." + network.getNetId() /* cmdName */,
+ EVENT_NETWORK_LINGER_COMPLETE /* cmd */,
+ 0 /* arg1 (unused) */, 0 /* arg2 (unused) */,
+ this /* obj (NetworkAgentInfo) */);
+ mInactivityMessage.schedule(newExpiry);
+ }
+
+ mInactivityExpiryMs = newExpiry;
+ }
+
+ public void setInactive() {
+ mInactive = true;
+ }
+
+ public void unsetInactive() {
+ mInactive = false;
+ }
+
+ public boolean isInactive() {
+ return mInactive;
+ }
+
+ public boolean isLingering() {
+ return mInactive && !isNascent();
+ }
+
+ /**
+ * Return whether the network is just connected and about to be torn down because of not
+ * satisfying any request.
+ */
+ public boolean isNascent() {
+ return mInactive && mInactivityTimers.size() == 1
+ && mInactivityTimers.first().requestId == NetworkRequest.REQUEST_ID_NONE;
+ }
+
+ public void clearInactivityState() {
+ if (mInactivityMessage != null) {
+ mInactivityMessage.cancel();
+ mInactivityMessage = null;
+ }
+ mInactivityTimers.clear();
+ mInactivityTimerForRequest.clear();
+ // Sets mInactivityExpiryMs, cancels and nulls out mInactivityMessage.
+ updateInactivityTimer();
+ mInactive = false;
+ }
+
+ public void dumpInactivityTimers(PrintWriter pw) {
+ for (InactivityTimer timer : mInactivityTimers) {
+ pw.println(timer);
+ }
+ }
+
+ /**
+ * Sets the most recent ConnectivityReport for this network.
+ *
+ * <p>This should only be called from the ConnectivityService thread.
+ *
+ * @hide
+ */
+ public void setConnectivityReport(@NonNull ConnectivityReport connectivityReport) {
+ mConnectivityReport = connectivityReport;
+ }
+
+ /**
+ * Returns the most recent ConnectivityReport for this network, or null if none have been
+ * reported yet.
+ *
+ * <p>This should only be called from the ConnectivityService thread.
+ *
+ * @hide
+ */
+ @Nullable
+ public ConnectivityReport getConnectivityReport() {
+ return mConnectivityReport;
+ }
+
+ // TODO: Print shorter members first and only print the boolean variable which value is true
+ // to improve readability.
+ public String toString() {
+ return "NetworkAgentInfo{"
+ + "network{" + network + "} handle{" + network.getNetworkHandle() + "} ni{"
+ + networkInfo.toShortString() + "} "
+ + " Score{" + getCurrentScore() + "} "
+ + (isNascent() ? " nascent" : (isLingering() ? " lingering" : ""))
+ + (everValidated ? " everValidated" : "")
+ + (lastValidated ? " lastValidated" : "")
+ + (partialConnectivity ? " partialConnectivity" : "")
+ + (everCaptivePortalDetected ? " everCaptivePortal" : "")
+ + (lastCaptivePortalDetected ? " isCaptivePortal" : "")
+ + (networkAgentConfig.explicitlySelected ? " explicitlySelected" : "")
+ + (networkAgentConfig.acceptUnvalidated ? " acceptUnvalidated" : "")
+ + (networkAgentConfig.acceptPartialConnectivity ? " acceptPartialConnectivity" : "")
+ + (clatd.isStarted() ? " clat{" + clatd + "} " : "")
+ + (declaredUnderlyingNetworks != null
+ ? " underlying{" + Arrays.toString(declaredUnderlyingNetworks) + "}" : "")
+ + " lp{" + linkProperties + "}"
+ + " nc{" + networkCapabilities + "}"
+ + "}";
+ }
+
+ /**
+ * Show a short string representing a Network.
+ *
+ * This is often not enough for debugging purposes for anything complex, but the full form
+ * is very long and hard to read, so this is useful when there isn't a lot of ambiguity.
+ * This represents the network with something like "[100 WIFI|VPN]" or "[108 MOBILE]".
+ */
+ public String toShortString() {
+ return "[" + network.getNetId() + " "
+ + transportNamesOf(networkCapabilities.getTransportTypes()) + "]";
+ }
+
+ // Enables sorting in descending order of score.
+ @Override
+ public int compareTo(NetworkAgentInfo other) {
+ return other.getCurrentScore() - getCurrentScore();
+ }
+
+ /**
+ * Null-guarding version of NetworkAgentInfo#toShortString()
+ */
+ @NonNull
+ public static String toShortString(@Nullable final NetworkAgentInfo nai) {
+ return null != nai ? nai.toShortString() : "[null]";
+ }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkDiagnostics.java b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
new file mode 100644
index 0000000..2e51be3
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkDiagnostics.java
@@ -0,0 +1,757 @@
+/*
+ * Copyright (C) 2015 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.connectivity;
+
+import static android.system.OsConstants.*;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.InetAddresses;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+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;
+import android.system.Os;
+import android.system.StructTimeval;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.NetworkStackConstants;
+
+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;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.NetworkInterface;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+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;
+import java.util.Random;
+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
+ *
+ * A simple class to diagnose network connectivity fundamentals. Current
+ * checks performed are:
+ * - ICMPv4/v6 echo requests for all routers
+ * - ICMPv4/v6 echo requests for all DNS servers
+ * - DNS UDP queries to all DNS servers
+ *
+ * Currently unimplemented checks include:
+ * - report ARP/ND data about on-link neighbors
+ * - DNS TCP queries to all DNS servers
+ * - HTTP DIRECT and PROXY checks
+ * - port 443 blocking/TLS intercept checks
+ * - QUIC reachability checks
+ * - MTU checks
+ *
+ * The supplied timeout bounds the entire diagnostic process. Each specific
+ * check class must implement this upper bound on measurements in whichever
+ * manner is most appropriate and effective.
+ *
+ * @hide
+ */
+public class NetworkDiagnostics {
+ private static final String TAG = "NetworkDiagnostics";
+
+ private static final InetAddress TEST_DNS4 = InetAddresses.parseNumericAddress("8.8.8.8");
+ private static final InetAddress TEST_DNS6 = InetAddresses.parseNumericAddress(
+ "2001:4860:4860::8888");
+
+ // For brevity elsewhere.
+ private static final long now() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ // Values from RFC 1035 section 4.1.1, names from <arpa/nameser.h>.
+ // Should be a member of DnsUdpCheck, but "compiler says no".
+ public static enum DnsResponseCode { NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP, REFUSED };
+
+ private final Network mNetwork;
+ private final LinkProperties mLinkProperties;
+ private final PrivateDnsConfig mPrivateDnsCfg;
+ private final Integer mInterfaceIndex;
+
+ private final long mTimeoutMs;
+ private final long mStartTime;
+ private final long mDeadlineTime;
+
+ // A counter, initialized to the total number of measurements,
+ // so callers can wait for completion.
+ private final CountDownLatch mCountDownLatch;
+
+ public class Measurement {
+ private static final String SUCCEEDED = "SUCCEEDED";
+ private static final String FAILED = "FAILED";
+
+ private boolean succeeded;
+
+ // Package private. TODO: investigate better encapsulation.
+ String description = "";
+ long startTime;
+ long finishTime;
+ String result = "";
+ Thread thread;
+
+ public boolean checkSucceeded() { return succeeded; }
+
+ void recordSuccess(String msg) {
+ maybeFixupTimes();
+ succeeded = true;
+ result = SUCCEEDED + ": " + msg;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ void recordFailure(String msg) {
+ maybeFixupTimes();
+ succeeded = false;
+ result = FAILED + ": " + msg;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ private void maybeFixupTimes() {
+ // Allows the caller to just set success/failure and not worry
+ // about also setting the correct finishing time.
+ if (finishTime == 0) { finishTime = now(); }
+
+ // In cases where, for example, a failure has occurred before the
+ // measurement even began, fixup the start time to reflect as much.
+ if (startTime == 0) { startTime = finishTime; }
+ }
+
+ @Override
+ public String toString() {
+ return description + ": " + result + " (" + (finishTime - startTime) + "ms)";
+ }
+ }
+
+ private final Map<InetAddress, Measurement> mIcmpChecks = new HashMap<>();
+ 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,
+ @NonNull PrivateDnsConfig privateDnsCfg, long timeoutMs) {
+ mNetwork = network;
+ mLinkProperties = lp;
+ mPrivateDnsCfg = privateDnsCfg;
+ mInterfaceIndex = getInterfaceIndex(mLinkProperties.getInterfaceName());
+ mTimeoutMs = timeoutMs;
+ mStartTime = now();
+ mDeadlineTime = mStartTime + mTimeoutMs;
+
+ // Hardcode measurements to TEST_DNS4 and TEST_DNS6 in order to test off-link connectivity.
+ // We are free to modify mLinkProperties with impunity because ConnectivityService passes us
+ // a copy and not the original object. It's easier to do it this way because we don't need
+ // to check whether the LinkProperties already contains these DNS servers because
+ // LinkProperties#addDnsServer checks for duplicates.
+ if (mLinkProperties.isReachable(TEST_DNS4)) {
+ mLinkProperties.addDnsServer(TEST_DNS4);
+ }
+ // TODO: we could use mLinkProperties.isReachable(TEST_DNS6) here, because we won't set any
+ // DNS servers for which isReachable() is false, but since this is diagnostic code, be extra
+ // careful.
+ if (mLinkProperties.hasGlobalIpv6Address() || mLinkProperties.hasIpv6DefaultRoute()) {
+ mLinkProperties.addDnsServer(TEST_DNS6);
+ }
+
+ for (RouteInfo route : mLinkProperties.getRoutes()) {
+ if (route.hasGateway()) {
+ InetAddress gateway = route.getGateway();
+ prepareIcmpMeasurement(gateway);
+ if (route.isIPv6Default()) {
+ prepareExplicitSourceIcmpMeasurements(gateway);
+ }
+ }
+ }
+ for (InetAddress nameserver : mLinkProperties.getDnsServers()) {
+ 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());
+
+ startMeasurements();
+
+ mDescription = "ifaces{" + TextUtils.join(",", mLinkProperties.getAllInterfaceNames()) + "}"
+ + " index{" + mInterfaceIndex + "}"
+ + " network{" + mNetwork + "}"
+ + " nethandle{" + mNetwork.getNetworkHandle() + "}";
+ }
+
+ private static Integer getInterfaceIndex(String ifname) {
+ try {
+ NetworkInterface ni = NetworkInterface.getByName(ifname);
+ return ni.getIndex();
+ } catch (NullPointerException | SocketException e) {
+ return null;
+ }
+ }
+
+ 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();
+ measurement.thread = new Thread(new IcmpCheck(target, measurement));
+ mIcmpChecks.put(target, measurement);
+ }
+ }
+
+ private void prepareExplicitSourceIcmpMeasurements(InetAddress target) {
+ for (LinkAddress l : mLinkProperties.getLinkAddresses()) {
+ InetAddress source = l.getAddress();
+ if (source instanceof Inet6Address && l.isGlobalPreferred()) {
+ Pair<InetAddress, InetAddress> srcTarget = new Pair<>(source, target);
+ if (!mExplicitSourceIcmpChecks.containsKey(srcTarget)) {
+ Measurement measurement = new Measurement();
+ measurement.thread = new Thread(new IcmpCheck(source, target, measurement));
+ mExplicitSourceIcmpChecks.put(srcTarget, measurement);
+ }
+ }
+ }
+ }
+
+ private void prepareDnsMeasurement(InetAddress target) {
+ if (!mDnsUdpChecks.containsKey(target)) {
+ Measurement measurement = new Measurement();
+ measurement.thread = new Thread(new DnsUdpCheck(target, measurement));
+ mDnsUdpChecks.put(target, measurement);
+ }
+ }
+
+ 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()
+ + mDnsTlsChecks.size();
+ }
+
+ private void startMeasurements() {
+ for (Measurement measurement : mIcmpChecks.values()) {
+ measurement.thread.start();
+ }
+ for (Measurement measurement : mExplicitSourceIcmpChecks.values()) {
+ measurement.thread.start();
+ }
+ for (Measurement measurement : mDnsUdpChecks.values()) {
+ measurement.thread.start();
+ }
+ for (Measurement measurement : mDnsTlsChecks.values()) {
+ measurement.thread.start();
+ }
+ }
+
+ public void waitForMeasurements() {
+ try {
+ mCountDownLatch.await(mDeadlineTime - now(), TimeUnit.MILLISECONDS);
+ } catch (InterruptedException ignored) {}
+ }
+
+ public List<Measurement> getMeasurements() {
+ // TODO: Consider moving waitForMeasurements() in here to minimize the
+ // chance of caller errors.
+
+ ArrayList<Measurement> measurements = new ArrayList(totalMeasurementCount());
+
+ // Sort measurements IPv4 first.
+ for (Map.Entry<InetAddress, Measurement> entry : mIcmpChecks.entrySet()) {
+ if (entry.getKey() instanceof Inet4Address) {
+ measurements.add(entry.getValue());
+ }
+ }
+ for (Map.Entry<Pair<InetAddress, InetAddress>, Measurement> entry :
+ mExplicitSourceIcmpChecks.entrySet()) {
+ if (entry.getKey().first instanceof Inet4Address) {
+ measurements.add(entry.getValue());
+ }
+ }
+ for (Map.Entry<InetAddress, Measurement> entry : mDnsUdpChecks.entrySet()) {
+ if (entry.getKey() instanceof Inet4Address) {
+ 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()) {
+ if (entry.getKey() instanceof Inet6Address) {
+ measurements.add(entry.getValue());
+ }
+ }
+ for (Map.Entry<Pair<InetAddress, InetAddress>, Measurement> entry :
+ mExplicitSourceIcmpChecks.entrySet()) {
+ if (entry.getKey().first instanceof Inet6Address) {
+ measurements.add(entry.getValue());
+ }
+ }
+ for (Map.Entry<InetAddress, Measurement> entry : mDnsUdpChecks.entrySet()) {
+ if (entry.getKey() instanceof Inet6Address) {
+ measurements.add(entry.getValue());
+ }
+ }
+ for (Map.Entry<InetAddress, Measurement> entry : mDnsTlsChecks.entrySet()) {
+ if (entry.getKey() instanceof Inet6Address) {
+ measurements.add(entry.getValue());
+ }
+ }
+
+ return measurements;
+ }
+
+ public void dump(IndentingPrintWriter pw) {
+ pw.println(TAG + ":" + mDescription);
+ final long unfinished = mCountDownLatch.getCount();
+ if (unfinished > 0) {
+ // This can't happen unless a caller forgets to call waitForMeasurements()
+ // or a measurement isn't implemented to correctly honor the timeout.
+ pw.println("WARNING: countdown wait incomplete: "
+ + unfinished + " unfinished measurements");
+ }
+
+ pw.increaseIndent();
+
+ String prefix;
+ for (Measurement m : getMeasurements()) {
+ prefix = m.checkSucceeded() ? "." : "F";
+ pw.println(prefix + " " + m.toString());
+ }
+
+ pw.decreaseIndent();
+ }
+
+
+ private class SimpleSocketCheck implements Closeable {
+ protected final InetAddress mSource; // Usually null.
+ protected final InetAddress mTarget;
+ protected final int mAddressFamily;
+ protected final Measurement mMeasurement;
+ protected FileDescriptor mFileDescriptor;
+ protected SocketAddress mSocketAddress;
+
+ protected SimpleSocketCheck(
+ InetAddress source, InetAddress target, Measurement measurement) {
+ mMeasurement = measurement;
+
+ if (target instanceof Inet6Address) {
+ Inet6Address targetWithScopeId = null;
+ if (target.isLinkLocalAddress() && mInterfaceIndex != null) {
+ try {
+ targetWithScopeId = Inet6Address.getByAddress(
+ null, target.getAddress(), mInterfaceIndex);
+ } catch (UnknownHostException e) {
+ mMeasurement.recordFailure(e.toString());
+ }
+ }
+ mTarget = (targetWithScopeId != null) ? targetWithScopeId : target;
+ mAddressFamily = AF_INET6;
+ } else {
+ mTarget = target;
+ mAddressFamily = AF_INET;
+ }
+
+ // We don't need to check the scope ID here because we currently only do explicit-source
+ // measurements from global IPv6 addresses.
+ mSource = source;
+ }
+
+ protected SimpleSocketCheck(InetAddress target, Measurement measurement) {
+ this(null, target, measurement);
+ }
+
+ protected void setupSocket(
+ int sockType, int protocol, long writeTimeout, long readTimeout, int dstPort)
+ throws ErrnoException, IOException {
+ final int oldTag = TrafficStats.getAndSetThreadStatsTag(
+ NetworkStackConstants.TAG_SYSTEM_PROBE);
+ 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.
+ Os.setsockoptTimeval(mFileDescriptor,
+ SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(writeTimeout));
+ Os.setsockoptTimeval(mFileDescriptor,
+ SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(readTimeout));
+ // TODO: Use IP_RECVERR/IPV6_RECVERR, pending OsContants availability.
+ mNetwork.bindSocket(mFileDescriptor);
+ if (mSource != null) {
+ Os.bind(mFileDescriptor, mSource, 0);
+ }
+ Os.connect(mFileDescriptor, mTarget, dstPort);
+ mSocketAddress = Os.getsockname(mFileDescriptor);
+ }
+
+ 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
+ public void close() {
+ IoUtils.closeQuietly(mFileDescriptor);
+ }
+ }
+
+
+ private class IcmpCheck extends SimpleSocketCheck implements Runnable {
+ private static final int TIMEOUT_SEND = 100;
+ private static final int TIMEOUT_RECV = 300;
+ private static final int PACKET_BUFSIZE = 512;
+ private final int mProtocol;
+ private final int mIcmpType;
+
+ public IcmpCheck(InetAddress source, InetAddress target, Measurement measurement) {
+ super(source, target, measurement);
+
+ if (mAddressFamily == AF_INET6) {
+ mProtocol = IPPROTO_ICMPV6;
+ mIcmpType = NetworkConstants.ICMPV6_ECHO_REQUEST_TYPE;
+ mMeasurement.description = "ICMPv6";
+ } else {
+ mProtocol = IPPROTO_ICMP;
+ mIcmpType = NetworkConstants.ICMPV4_ECHO_REQUEST_TYPE;
+ mMeasurement.description = "ICMPv4";
+ }
+
+ mMeasurement.description += " dst{" + mTarget.getHostAddress() + "}";
+ }
+
+ public IcmpCheck(InetAddress target, Measurement measurement) {
+ this(null, target, measurement);
+ }
+
+ @Override
+ public void run() {
+ if (ensureMeasurementNecessary()) return;
+
+ try {
+ setupSocket(SOCK_DGRAM, mProtocol, TIMEOUT_SEND, TIMEOUT_RECV, 0);
+ } catch (ErrnoException | IOException e) {
+ mMeasurement.recordFailure(e.toString());
+ return;
+ }
+ mMeasurement.description += " src{" + socketAddressToString(mSocketAddress) + "}";
+
+ // Build a trivial ICMP packet.
+ final byte[] icmpPacket = {
+ (byte) mIcmpType, 0, 0, 0, 0, 0, 0, 0 // ICMP header
+ };
+
+ int count = 0;
+ mMeasurement.startTime = now();
+ while (now() < mDeadlineTime - (TIMEOUT_SEND + TIMEOUT_RECV)) {
+ count++;
+ icmpPacket[icmpPacket.length - 1] = (byte) count;
+ try {
+ Os.write(mFileDescriptor, icmpPacket, 0, icmpPacket.length);
+ } catch (ErrnoException | InterruptedIOException e) {
+ mMeasurement.recordFailure(e.toString());
+ break;
+ }
+
+ try {
+ ByteBuffer reply = ByteBuffer.allocate(PACKET_BUFSIZE);
+ Os.read(mFileDescriptor, reply);
+ // TODO: send a few pings back to back to guesstimate packet loss.
+ mMeasurement.recordSuccess("1/" + count);
+ break;
+ } catch (ErrnoException | InterruptedIOException e) {
+ continue;
+ }
+ }
+ if (mMeasurement.finishTime == 0) {
+ mMeasurement.recordFailure("0/" + count);
+ }
+
+ close();
+ }
+ }
+
+
+ private class DnsUdpCheck extends SimpleSocketCheck implements Runnable {
+ private static final int TIMEOUT_SEND = 100;
+ private static final int TIMEOUT_RECV = 500;
+ private static final int RR_TYPE_A = 1;
+ private static final int RR_TYPE_AAAA = 28;
+ private static final int PACKET_BUFSIZE = 512;
+
+ protected final Random mRandom = new Random();
+
+ // Should be static, but the compiler mocks our puny, human attempts at reason.
+ protected String responseCodeStr(int rcode) {
+ try {
+ return DnsResponseCode.values()[rcode].toString();
+ } catch (IndexOutOfBoundsException e) {
+ return String.valueOf(rcode);
+ }
+ }
+
+ protected final int mQueryType;
+
+ public DnsUdpCheck(InetAddress target, Measurement measurement) {
+ super(target, measurement);
+
+ // TODO: Ideally, query the target for both types regardless of address family.
+ if (mAddressFamily == AF_INET6) {
+ mQueryType = RR_TYPE_AAAA;
+ } else {
+ mQueryType = RR_TYPE_A;
+ }
+
+ mMeasurement.description = "DNS UDP dst{" + mTarget.getHostAddress() + "}";
+ }
+
+ @Override
+ public void run() {
+ if (ensureMeasurementNecessary()) return;
+
+ try {
+ setupSocket(SOCK_DGRAM, IPPROTO_UDP, TIMEOUT_SEND, TIMEOUT_RECV,
+ NetworkConstants.DNS_SERVER_PORT);
+ } catch (ErrnoException | IOException e) {
+ mMeasurement.recordFailure(e.toString());
+ return;
+ }
+
+ // 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);
+ appendDnsToMeasurementDescription(sixRandomDigits, mSocketAddress);
+
+ // Build a trivial DNS packet.
+ final byte[] dnsPacket = getDnsQueryPacket(sixRandomDigits);
+
+ int count = 0;
+ mMeasurement.startTime = now();
+ while (now() < mDeadlineTime - (TIMEOUT_RECV + TIMEOUT_RECV)) {
+ count++;
+ try {
+ Os.write(mFileDescriptor, dnsPacket, 0, dnsPacket.length);
+ } catch (ErrnoException | InterruptedIOException e) {
+ mMeasurement.recordFailure(e.toString());
+ break;
+ }
+
+ try {
+ ByteBuffer reply = ByteBuffer.allocate(PACKET_BUFSIZE);
+ Os.read(mFileDescriptor, reply);
+ // TODO: more correct and detailed evaluation of the response,
+ // possibly adding the returned IP address(es) to the output.
+ final String rcodeStr = (reply.limit() > 3)
+ ? " " + responseCodeStr((int) (reply.get(3)) & 0x0f)
+ : "";
+ mMeasurement.recordSuccess("1/" + count + rcodeStr);
+ break;
+ } catch (ErrnoException | InterruptedIOException e) {
+ continue;
+ }
+ }
+ if (mMeasurement.finishTime == 0) {
+ mMeasurement.recordFailure("0/" + count);
+ }
+
+ close();
+ }
+
+ 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
+ 1, 0, // [2-3] flags; byte[2] = 1 for recursion desired (RD).
+ 0, 1, // [4-5] QDCOUNT (number of queries)
+ 0, 0, // [6-7] ANCOUNT (number of answers)
+ 0, 0, // [8-9] NSCOUNT (number of name server records)
+ 0, 0, // [10-11] ARCOUNT (number of additional records)
+ 17, rnd[0], rnd[1], rnd[2], rnd[3], rnd[4], rnd[5],
+ '-', 'a', 'n', 'd', 'r', 'o', 'i', 'd', '-', 'd', 's',
+ 6, 'm', 'e', 't', 'r', 'i', 'c',
+ 7, 'g', 's', 't', 'a', 't', 'i', 'c',
+ 3, 'c', 'o', 'm',
+ 0, // null terminator of FQDN (root TLD)
+ 0, (byte) mQueryType, // QTYPE
+ 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{"
+ + (mHostname == null ? "" : 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(NetworkStackConstants.TAG_SYSTEM_PROBE);
+
+ try (SSLSocket sslSocket = setupSSLSocket()) {
+ sendDoTProbe(sslSocket);
+ } catch (IOException e) {
+ mMeasurement.recordFailure(e.toString());
+ }
+ }
+ }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkNotificationManager.java b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
new file mode 100644
index 0000000..0c0d459
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkNotificationManager.java
@@ -0,0 +1,399 @@
+/*
+ * Copyright (C) 2016 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.connectivity;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.drawable.Icon;
+import android.net.ConnectivityResources;
+import android.net.NetworkSpecifier;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.wifi.WifiInfo;
+import android.os.UserHandle;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.widget.Toast;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+
+public class NetworkNotificationManager {
+
+
+ public static enum NotificationType {
+ LOST_INTERNET(SystemMessage.NOTE_NETWORK_LOST_INTERNET),
+ NETWORK_SWITCH(SystemMessage.NOTE_NETWORK_SWITCH),
+ NO_INTERNET(SystemMessage.NOTE_NETWORK_NO_INTERNET),
+ PARTIAL_CONNECTIVITY(SystemMessage.NOTE_NETWORK_PARTIAL_CONNECTIVITY),
+ SIGN_IN(SystemMessage.NOTE_NETWORK_SIGN_IN),
+ PRIVATE_DNS_BROKEN(SystemMessage.NOTE_NETWORK_PRIVATE_DNS_BROKEN);
+
+ public final int eventId;
+
+ NotificationType(int eventId) {
+ this.eventId = eventId;
+ Holder.sIdToTypeMap.put(eventId, this);
+ }
+
+ private static class Holder {
+ private static SparseArray<NotificationType> sIdToTypeMap = new SparseArray<>();
+ }
+
+ public static NotificationType getFromId(int id) {
+ return Holder.sIdToTypeMap.get(id);
+ }
+ };
+
+ private static final String TAG = NetworkNotificationManager.class.getSimpleName();
+ private static final boolean DBG = true;
+
+ // Notification channels used by ConnectivityService mainline module, it should be aligned with
+ // SystemNotificationChannels so the channels are the same as the ones used as the system
+ // server.
+ public static final String NOTIFICATION_CHANNEL_NETWORK_STATUS = "NETWORK_STATUS";
+ public static final String NOTIFICATION_CHANNEL_NETWORK_ALERTS = "NETWORK_ALERTS";
+
+ // The context is for the current user (system server)
+ private final Context mContext;
+ private final Resources mResources;
+ private final TelephonyManager mTelephonyManager;
+ // The notification manager is created from a context for User.ALL, so notifications
+ // will be sent to all users.
+ private final NotificationManager mNotificationManager;
+ // Tracks the types of notifications managed by this instance, from creation to cancellation.
+ private final SparseIntArray mNotificationTypeMap;
+
+ public NetworkNotificationManager(@NonNull final Context c, @NonNull final TelephonyManager t) {
+ mContext = c;
+ mTelephonyManager = t;
+ mNotificationManager =
+ (NotificationManager) c.createContextAsUser(UserHandle.ALL, 0 /* flags */)
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ mNotificationTypeMap = new SparseIntArray();
+ mResources = new ConnectivityResources(mContext).get();
+ }
+
+ @VisibleForTesting
+ protected static int approximateTransportType(NetworkAgentInfo nai) {
+ return nai.isVPN() ? TRANSPORT_VPN : getFirstTransportType(nai);
+ }
+
+ // TODO: deal more gracefully with multi-transport networks.
+ private static int getFirstTransportType(NetworkAgentInfo nai) {
+ // TODO: The range is wrong, the safer and correct way is to change the range from
+ // MIN_TRANSPORT to MAX_TRANSPORT.
+ for (int i = 0; i < 64; i++) {
+ if (nai.networkCapabilities.hasTransport(i)) return i;
+ }
+ return -1;
+ }
+
+ private String getTransportName(final int transportType) {
+ String[] networkTypes = mResources.getStringArray(R.array.network_switch_type_name);
+ try {
+ return networkTypes[transportType];
+ } catch (IndexOutOfBoundsException e) {
+ return mResources.getString(R.string.network_switch_type_name_unknown);
+ }
+ }
+
+ private static int getIcon(int transportType) {
+ return (transportType == TRANSPORT_WIFI)
+ ? R.drawable.stat_notify_wifi_in_range // TODO: Distinguish ! from ?.
+ : R.drawable.stat_notify_rssi_in_range;
+ }
+
+ /**
+ * Show or hide network provisioning notifications.
+ *
+ * We use notifications for two purposes: to notify that a network requires sign in
+ * (NotificationType.SIGN_IN), or to notify that a network does not have Internet access
+ * (NotificationType.NO_INTERNET). We display at most one notification per ID, so on a
+ * particular network we can display the notification type that was most recently requested.
+ * So for example if a captive portal fails to reply within a few seconds of connecting, we
+ * might first display NO_INTERNET, and then when the captive portal check completes, display
+ * SIGN_IN.
+ *
+ * @param id an identifier that uniquely identifies this notification. This must match
+ * between show and hide calls. We use the NetID value but for legacy callers
+ * we concatenate the range of types with the range of NetIDs.
+ * @param notifyType the type of the notification.
+ * @param nai the network with which the notification is associated. For a SIGN_IN, NO_INTERNET,
+ * or LOST_INTERNET notification, this is the network we're connecting to. For a
+ * NETWORK_SWITCH notification it's the network that we switched from. When this network
+ * disconnects the notification is removed.
+ * @param switchToNai for a NETWORK_SWITCH notification, the network we are switching to. Null
+ * in all other cases. Only used to determine the text of the notification.
+ */
+ public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai,
+ NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority) {
+ final String tag = tagFor(id);
+ final int eventId = notifyType.eventId;
+ final int transportType;
+ final CharSequence name;
+ if (nai != null) {
+ transportType = approximateTransportType(nai);
+ final String extraInfo = nai.networkInfo.getExtraInfo();
+ if (nai.linkProperties != null && nai.linkProperties.getCaptivePortalData() != null
+ && !TextUtils.isEmpty(nai.linkProperties.getCaptivePortalData()
+ .getVenueFriendlyName())) {
+ name = nai.linkProperties.getCaptivePortalData().getVenueFriendlyName();
+ } else {
+ name = TextUtils.isEmpty(extraInfo)
+ ? WifiInfo.sanitizeSsid(nai.networkCapabilities.getSsid()) : extraInfo;
+ }
+ // Only notify for Internet-capable networks.
+ if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)) return;
+ } else {
+ // Legacy notifications.
+ transportType = TRANSPORT_CELLULAR;
+ name = "";
+ }
+
+ // Clear any previous notification with lower priority, otherwise return. http://b/63676954.
+ // A new SIGN_IN notification with a new intent should override any existing one.
+ final int previousEventId = mNotificationTypeMap.get(id);
+ final NotificationType previousNotifyType = NotificationType.getFromId(previousEventId);
+ if (priority(previousNotifyType) > priority(notifyType)) {
+ Log.d(TAG, String.format(
+ "ignoring notification %s for network %s with existing notification %s",
+ notifyType, id, previousNotifyType));
+ return;
+ }
+ clearNotification(id);
+
+ if (DBG) {
+ Log.d(TAG, String.format(
+ "showNotification tag=%s event=%s transport=%s name=%s highPriority=%s",
+ tag, nameOf(eventId), getTransportName(transportType), name, highPriority));
+ }
+
+ final Resources r = mResources;
+ final CharSequence title;
+ final CharSequence details;
+ Icon icon = Icon.createWithResource(r, getIcon(transportType));
+ if (notifyType == NotificationType.NO_INTERNET && transportType == TRANSPORT_WIFI) {
+ title = r.getString(R.string.wifi_no_internet, name);
+ details = r.getString(R.string.wifi_no_internet_detailed);
+ } else if (notifyType == NotificationType.PRIVATE_DNS_BROKEN) {
+ if (transportType == TRANSPORT_CELLULAR) {
+ title = r.getString(R.string.mobile_no_internet);
+ } else if (transportType == TRANSPORT_WIFI) {
+ title = r.getString(R.string.wifi_no_internet, name);
+ } else {
+ title = r.getString(R.string.other_networks_no_internet);
+ }
+ details = r.getString(R.string.private_dns_broken_detailed);
+ } else if (notifyType == NotificationType.PARTIAL_CONNECTIVITY
+ && transportType == TRANSPORT_WIFI) {
+ title = r.getString(R.string.network_partial_connectivity, name);
+ details = r.getString(R.string.network_partial_connectivity_detailed);
+ } else if (notifyType == NotificationType.LOST_INTERNET &&
+ transportType == TRANSPORT_WIFI) {
+ title = r.getString(R.string.wifi_no_internet, name);
+ details = r.getString(R.string.wifi_no_internet_detailed);
+ } else if (notifyType == NotificationType.SIGN_IN) {
+ switch (transportType) {
+ case TRANSPORT_WIFI:
+ title = r.getString(R.string.wifi_available_sign_in, 0);
+ details = r.getString(R.string.network_available_sign_in_detailed, name);
+ break;
+ case TRANSPORT_CELLULAR:
+ title = r.getString(R.string.network_available_sign_in, 0);
+ // TODO: Change this to pull from NetworkInfo once a printable
+ // name has been added to it
+ NetworkSpecifier specifier = nai.networkCapabilities.getNetworkSpecifier();
+ int subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
+ if (specifier instanceof TelephonyNetworkSpecifier) {
+ subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
+ }
+
+ details = mTelephonyManager.createForSubscriptionId(subId)
+ .getNetworkOperatorName();
+ break;
+ default:
+ title = r.getString(R.string.network_available_sign_in, 0);
+ details = r.getString(R.string.network_available_sign_in_detailed, name);
+ break;
+ }
+ } else if (notifyType == NotificationType.NETWORK_SWITCH) {
+ String fromTransport = getTransportName(transportType);
+ String toTransport = getTransportName(approximateTransportType(switchToNai));
+ title = r.getString(R.string.network_switch_metered, toTransport);
+ details = r.getString(R.string.network_switch_metered_detail, toTransport,
+ fromTransport);
+ } else if (notifyType == NotificationType.NO_INTERNET
+ || notifyType == NotificationType.PARTIAL_CONNECTIVITY) {
+ // NO_INTERNET and PARTIAL_CONNECTIVITY notification for non-WiFi networks
+ // are sent, but they are not implemented yet.
+ return;
+ } else {
+ Log.wtf(TAG, "Unknown notification type " + notifyType + " on network transport "
+ + getTransportName(transportType));
+ return;
+ }
+ // When replacing an existing notification for a given network, don't alert, just silently
+ // update the existing notification. Note that setOnlyAlertOnce() will only work for the
+ // same id, and the id used here is the NotificationType which is different in every type of
+ // notification. This is required because the notification metrics only track the ID but not
+ // the tag.
+ final boolean hasPreviousNotification = previousNotifyType != null;
+ final String channelId = (highPriority && !hasPreviousNotification)
+ ? NOTIFICATION_CHANNEL_NETWORK_ALERTS : NOTIFICATION_CHANNEL_NETWORK_STATUS;
+ Notification.Builder builder = new Notification.Builder(mContext, channelId)
+ .setWhen(System.currentTimeMillis())
+ .setShowWhen(notifyType == NotificationType.NETWORK_SWITCH)
+ .setSmallIcon(icon)
+ .setAutoCancel(true)
+ .setTicker(title)
+ .setColor(mContext.getColor(android.R.color.system_notification_accent_color))
+ .setContentTitle(title)
+ .setContentIntent(intent)
+ .setLocalOnly(true)
+ .setOnlyAlertOnce(true);
+
+ if (notifyType == NotificationType.NETWORK_SWITCH) {
+ builder.setStyle(new Notification.BigTextStyle().bigText(details));
+ } else {
+ builder.setContentText(details);
+ }
+
+ if (notifyType == NotificationType.SIGN_IN) {
+ builder.extend(new Notification.TvExtender().setChannelId(channelId));
+ }
+
+ Notification notification = builder.build();
+
+ mNotificationTypeMap.put(id, eventId);
+ try {
+ mNotificationManager.notify(tag, eventId, notification);
+ } catch (NullPointerException npe) {
+ Log.d(TAG, "setNotificationVisible: visible notificationManager error", npe);
+ }
+ }
+
+ /**
+ * Clear the notification with the given id, only if it matches the given type.
+ */
+ public void clearNotification(int id, NotificationType notifyType) {
+ final int previousEventId = mNotificationTypeMap.get(id);
+ final NotificationType previousNotifyType = NotificationType.getFromId(previousEventId);
+ if (notifyType != previousNotifyType) {
+ return;
+ }
+ clearNotification(id);
+ }
+
+ public void clearNotification(int id) {
+ if (mNotificationTypeMap.indexOfKey(id) < 0) {
+ return;
+ }
+ final String tag = tagFor(id);
+ final int eventId = mNotificationTypeMap.get(id);
+ if (DBG) {
+ Log.d(TAG, String.format("clearing notification tag=%s event=%s", tag,
+ nameOf(eventId)));
+ }
+ try {
+ mNotificationManager.cancel(tag, eventId);
+ } catch (NullPointerException npe) {
+ Log.d(TAG, String.format(
+ "failed to clear notification tag=%s event=%s", tag, nameOf(eventId)), npe);
+ }
+ mNotificationTypeMap.delete(id);
+ }
+
+ /**
+ * Legacy provisioning notifications coming directly from DcTracker.
+ */
+ public void setProvNotificationVisible(boolean visible, int id, String action) {
+ if (visible) {
+ // For legacy purposes, action is sent as the action + the phone ID from DcTracker.
+ // Split the string here and send the phone ID as an extra instead.
+ String[] splitAction = action.split(":");
+ Intent intent = new Intent(splitAction[0]);
+ try {
+ intent.putExtra("provision.phone.id", Integer.parseInt(splitAction[1]));
+ } catch (NumberFormatException ignored) { }
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(
+ mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE);
+ showNotification(id, NotificationType.SIGN_IN, null, null, pendingIntent, false);
+ } else {
+ clearNotification(id);
+ }
+ }
+
+ public void showToast(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
+ String fromTransport = getTransportName(approximateTransportType(fromNai));
+ String toTransport = getTransportName(approximateTransportType(toNai));
+ String text = mResources.getString(
+ R.string.network_switch_metered_toast, fromTransport, toTransport);
+ Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
+ }
+
+ @VisibleForTesting
+ static String tagFor(int id) {
+ return String.format("ConnectivityNotification:%d", id);
+ }
+
+ @VisibleForTesting
+ static String nameOf(int eventId) {
+ NotificationType t = NotificationType.getFromId(eventId);
+ return (t != null) ? t.name() : "UNKNOWN";
+ }
+
+ /**
+ * A notification with a higher number will take priority over a notification with a lower
+ * number.
+ */
+ private static int priority(NotificationType t) {
+ if (t == null) {
+ return 0;
+ }
+ switch (t) {
+ case SIGN_IN:
+ return 6;
+ case PARTIAL_CONNECTIVITY:
+ return 5;
+ case PRIVATE_DNS_BROKEN:
+ return 4;
+ case NO_INTERNET:
+ return 3;
+ case NETWORK_SWITCH:
+ return 2;
+ case LOST_INTERNET:
+ return 1;
+ default:
+ return 0;
+ }
+ }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkOffer.java b/service/src/com/android/server/connectivity/NetworkOffer.java
new file mode 100644
index 0000000..fa2d465
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkOffer.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2021 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.connectivity;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.INetworkOfferCallback;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Messenger;
+
+import java.util.Objects;
+
+
+/**
+ * Represents an offer made by a NetworkProvider to create a network if a need arises.
+ *
+ * This class contains the prospective score and capabilities of the network. The provider
+ * is not obligated to caps able to create a network satisfying this, nor to build a network
+ * with the exact score and/or capabilities passed ; after all, not all providers know in
+ * advance what a network will look like after it's connected. Instead, this is meant as a
+ * filter to limit requests sent to the provider by connectivity to those that this offer stands
+ * a chance to fulfill.
+ *
+ * @see NetworkProvider#offerNetwork.
+ *
+ * @hide
+ */
+public class NetworkOffer {
+ @NonNull public final FullScore score;
+ @NonNull public final NetworkCapabilities caps;
+ @NonNull public final INetworkOfferCallback callback;
+ @NonNull public final Messenger provider;
+
+ private static NetworkCapabilities emptyCaps() {
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ return nc;
+ }
+
+ // Ideally the caps argument would be non-null, but null has historically meant no filter
+ // and telephony passes null. Keep backward compatibility.
+ public NetworkOffer(@NonNull final FullScore score,
+ @Nullable final NetworkCapabilities caps,
+ @NonNull final INetworkOfferCallback callback,
+ @NonNull final Messenger provider) {
+ this.score = Objects.requireNonNull(score);
+ this.caps = null != caps ? caps : emptyCaps();
+ this.callback = Objects.requireNonNull(callback);
+ this.provider = Objects.requireNonNull(provider);
+ }
+
+ /**
+ * Migrate from, and take over, a previous offer.
+ *
+ * When an updated offer is sent from a provider, call this method on the new offer, passing
+ * the old one, to take over the state.
+ *
+ * @param previousOffer
+ */
+ public void migrateFrom(@NonNull final NetworkOffer previousOffer) {
+ if (!callback.equals(previousOffer.callback)) {
+ throw new IllegalArgumentException("Can only migrate from a previous version of"
+ + " the same offer");
+ }
+ }
+
+ /**
+ * Returns whether an offer can satisfy a NetworkRequest, according to its capabilities.
+ * @param request The request to test against.
+ * @return Whether this offer can satisfy the request.
+ */
+ public final boolean canSatisfy(@NonNull final NetworkRequest request) {
+ return request.networkCapabilities.satisfiedByNetworkCapabilities(caps);
+ }
+
+ @Override
+ public String toString() {
+ return "NetworkOffer [ Score " + score + " ]";
+ }
+}
diff --git a/service/src/com/android/server/connectivity/NetworkRanker.java b/service/src/com/android/server/connectivity/NetworkRanker.java
new file mode 100644
index 0000000..d0aabf9
--- /dev/null
+++ b/service/src/com/android/server/connectivity/NetworkRanker.java
@@ -0,0 +1,50 @@
+/*
+ * 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.connectivity;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkRequest;
+
+import java.util.Collection;
+
+/**
+ * A class that knows how to find the best network matching a request out of a list of networks.
+ */
+public class NetworkRanker {
+ public NetworkRanker() { }
+
+ /**
+ * Find the best network satisfying this request among the list of passed networks.
+ */
+ // Almost equivalent to Collections.max(nais), but allows returning null if no network
+ // satisfies the request.
+ @Nullable
+ public NetworkAgentInfo getBestNetwork(@NonNull final NetworkRequest request,
+ @NonNull final Collection<NetworkAgentInfo> nais) {
+ NetworkAgentInfo bestNetwork = null;
+ int bestScore = Integer.MIN_VALUE;
+ for (final NetworkAgentInfo nai : nais) {
+ if (!nai.satisfies(request)) continue;
+ if (nai.getCurrentScore() > bestScore) {
+ bestNetwork = nai;
+ bestScore = nai.getCurrentScore();
+ }
+ }
+ return bestNetwork;
+ }
+}
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
new file mode 100644
index 0000000..673c804
--- /dev/null
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -0,0 +1,829 @@
+/*
+ * Copyright (C) 2014 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.connectivity;
+
+import static android.Manifest.permission.CHANGE_NETWORK_STATE;
+import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
+import static android.Manifest.permission.INTERNET;
+import static android.Manifest.permission.NETWORK_STACK;
+import static android.Manifest.permission.UPDATE_DEVICE_STATS;
+import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
+import static android.content.pm.PackageManager.GET_PERMISSIONS;
+import static android.content.pm.PackageManager.MATCH_ANY_USER;
+import static android.net.ConnectivitySettingsManager.APPS_ALLOWED_ON_RESTRICTED_NETWORKS;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.os.Process.INVALID_UID;
+import static android.os.Process.SYSTEM_UID;
+
+import static com.android.net.module.util.CollectionUtils.toIntArray;
+
+import android.annotation.NonNull;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.database.ContentObserver;
+import android.net.ConnectivitySettingsManager;
+import android.net.INetd;
+import android.net.UidRange;
+import android.net.Uri;
+import android.os.Build;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.os.SystemConfigManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.system.OsConstants;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * A utility class to inform Netd of UID permisisons.
+ * Does a mass update at boot and then monitors for app install/remove.
+ *
+ * @hide
+ */
+public class PermissionMonitor {
+ private static final String TAG = "PermissionMonitor";
+ private static final boolean DBG = true;
+ protected static final Boolean SYSTEM = Boolean.TRUE;
+ protected static final Boolean NETWORK = Boolean.FALSE;
+ private static final int VERSION_Q = Build.VERSION_CODES.Q;
+
+ private final PackageManager mPackageManager;
+ private final UserManager mUserManager;
+ private final SystemConfigManager mSystemConfigManager;
+ private final INetd mNetd;
+ private final Dependencies mDeps;
+ private final Context mContext;
+
+ @GuardedBy("this")
+ private final Set<UserHandle> mUsers = new HashSet<>();
+
+ // Keys are app uids. Values are true for SYSTEM permission and false for NETWORK permission.
+ @GuardedBy("this")
+ private final Map<Integer, Boolean> mApps = new HashMap<>();
+
+ // Keys are active non-bypassable and fully-routed VPN's interface name, Values are uid ranges
+ // for apps under the VPN
+ @GuardedBy("this")
+ private final Map<String, Set<UidRange>> mVpnUidRanges = new HashMap<>();
+
+ // A set of appIds for apps across all users on the device. We track appIds instead of uids
+ // directly to reduce its size and also eliminate the need to update this set when user is
+ // added/removed.
+ @GuardedBy("this")
+ private final Set<Integer> mAllApps = new HashSet<>();
+
+ // A set of apps which are allowed to use restricted networks. These apps can't hold the
+ // CONNECTIVITY_USE_RESTRICTED_NETWORKS permission because they can't be signature|privileged
+ // apps. However, these apps should still be able to use restricted networks under certain
+ // conditions (e.g. government app using emergency services). So grant netd system permission
+ // to uids whose package name is listed in APPS_ALLOWED_ON_RESTRICTED_NETWORKS setting.
+ @GuardedBy("this")
+ private final Set<String> mAppsAllowedOnRestrictedNetworks = new ArraySet<>();
+
+ private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
+ final Uri packageData = intent.getData();
+ final String packageName =
+ packageData != null ? packageData.getSchemeSpecificPart() : null;
+
+ if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
+ onPackageAdded(packageName, uid);
+ } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
+ onPackageRemoved(packageName, uid);
+ } else {
+ Log.wtf(TAG, "received unexpected intent: " + action);
+ }
+ }
+ };
+
+ /**
+ * Dependencies of PermissionMonitor, for injection in tests.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ /**
+ * Get device first sdk version.
+ */
+ public int getDeviceFirstSdkInt() {
+ return Build.VERSION.FIRST_SDK_INT;
+ }
+
+ /**
+ * Get apps allowed to use restricted networks via ConnectivitySettingsManager.
+ */
+ public Set<String> getAppsAllowedOnRestrictedNetworks(@NonNull Context context) {
+ return ConnectivitySettingsManager.getAppsAllowedOnRestrictedNetworks(context);
+ }
+
+ /**
+ * Register ContentObserver for given Uri.
+ */
+ public void registerContentObserver(@NonNull Context context, @NonNull Uri uri,
+ boolean notifyForDescendants, @NonNull ContentObserver observer) {
+ context.getContentResolver().registerContentObserver(
+ uri, notifyForDescendants, observer);
+ }
+ }
+
+ public PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd) {
+ this(context, netd, new Dependencies());
+ }
+
+ @VisibleForTesting
+ PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd,
+ @NonNull final Dependencies deps) {
+ mPackageManager = context.getPackageManager();
+ mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+ mSystemConfigManager = context.getSystemService(SystemConfigManager.class);
+ mNetd = netd;
+ mDeps = deps;
+ mContext = context;
+ }
+
+ // Intended to be called only once at startup, after the system is ready. Installs a broadcast
+ // receiver to monitor ongoing UID changes, so this shouldn't/needn't be called again.
+ public synchronized void startMonitoring() {
+ log("Monitoring");
+
+ final Context userAllContext = mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */);
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ intentFilter.addDataScheme("package");
+ userAllContext.registerReceiver(
+ mIntentReceiver, intentFilter, null /* broadcastPermission */,
+ null /* scheduler */);
+
+ // Register APPS_ALLOWED_ON_RESTRICTED_NETWORKS setting observer
+ mDeps.registerContentObserver(
+ userAllContext,
+ Settings.Secure.getUriFor(APPS_ALLOWED_ON_RESTRICTED_NETWORKS),
+ false /* notifyForDescendants */,
+ new ContentObserver(null) {
+ @Override
+ public void onChange(boolean selfChange) {
+ onSettingChanged();
+ }
+ });
+
+ // Read APPS_ALLOWED_ON_RESTRICTED_NETWORKS setting and update
+ // mAppsAllowedOnRestrictedNetworks.
+ updateAppsAllowedOnRestrictedNetworks(mDeps.getAppsAllowedOnRestrictedNetworks(mContext));
+
+ List<PackageInfo> apps = mPackageManager.getInstalledPackages(GET_PERMISSIONS
+ | MATCH_ANY_USER);
+ if (apps == null) {
+ loge("No apps");
+ return;
+ }
+
+ SparseIntArray netdPermsUids = new SparseIntArray();
+
+ for (PackageInfo app : apps) {
+ int uid = app.applicationInfo != null ? app.applicationInfo.uid : INVALID_UID;
+ if (uid < 0) {
+ continue;
+ }
+ mAllApps.add(UserHandle.getAppId(uid));
+
+ boolean isNetwork = hasNetworkPermission(app);
+ boolean hasRestrictedPermission = hasRestrictedNetworkPermission(app);
+
+ if (isNetwork || hasRestrictedPermission) {
+ Boolean permission = mApps.get(uid);
+ // If multiple packages share a UID (cf: android:sharedUserId) and ask for different
+ // permissions, don't downgrade (i.e., if it's already SYSTEM, leave it as is).
+ if (permission == null || permission == NETWORK) {
+ mApps.put(uid, hasRestrictedPermission);
+ }
+ }
+
+ //TODO: unify the management of the permissions into one codepath.
+ int otherNetdPerms = getNetdPermissionMask(app.requestedPermissions,
+ app.requestedPermissionsFlags);
+ netdPermsUids.put(uid, netdPermsUids.get(uid) | otherNetdPerms);
+ }
+
+ mUsers.addAll(mUserManager.getUserHandles(true /* excludeDying */));
+
+ final SparseArray<String> netdPermToSystemPerm = new SparseArray<>();
+ netdPermToSystemPerm.put(INetd.PERMISSION_INTERNET, INTERNET);
+ netdPermToSystemPerm.put(INetd.PERMISSION_UPDATE_DEVICE_STATS, UPDATE_DEVICE_STATS);
+ for (int i = 0; i < netdPermToSystemPerm.size(); i++) {
+ final int netdPermission = netdPermToSystemPerm.keyAt(i);
+ final String systemPermission = netdPermToSystemPerm.valueAt(i);
+ final int[] hasPermissionUids =
+ mSystemConfigManager.getSystemPermissionUids(systemPermission);
+ for (int j = 0; j < hasPermissionUids.length; j++) {
+ final int uid = hasPermissionUids[j];
+ netdPermsUids.put(uid, netdPermsUids.get(uid) | netdPermission);
+ }
+ }
+ log("Users: " + mUsers.size() + ", Apps: " + mApps.size());
+ update(mUsers, mApps, true);
+ sendPackagePermissionsToNetd(netdPermsUids);
+ }
+
+ @VisibleForTesting
+ void updateAppsAllowedOnRestrictedNetworks(final Set<String> apps) {
+ mAppsAllowedOnRestrictedNetworks.clear();
+ mAppsAllowedOnRestrictedNetworks.addAll(apps);
+ }
+
+ @VisibleForTesting
+ static boolean isVendorApp(@NonNull ApplicationInfo appInfo) {
+ return appInfo.isVendor() || appInfo.isOem() || appInfo.isProduct();
+ }
+
+ @VisibleForTesting
+ boolean isCarryoverPackage(final ApplicationInfo appInfo) {
+ if (appInfo == null) return false;
+ return (appInfo.targetSdkVersion < VERSION_Q && isVendorApp(appInfo))
+ // Backward compatibility for b/114245686, on devices that launched before Q daemons
+ // and apps running as the system UID are exempted from this check.
+ || (appInfo.uid == SYSTEM_UID && mDeps.getDeviceFirstSdkInt() < VERSION_Q);
+ }
+
+ @VisibleForTesting
+ boolean isAppAllowedOnRestrictedNetworks(@NonNull final PackageInfo app) {
+ // Check whether package name is in allowed on restricted networks app list. If so, this app
+ // can have netd system permission.
+ return mAppsAllowedOnRestrictedNetworks.contains(app.packageName);
+ }
+
+ @VisibleForTesting
+ boolean hasPermission(@NonNull final PackageInfo app, @NonNull final String permission) {
+ if (app.requestedPermissions == null || app.requestedPermissionsFlags == null) {
+ return false;
+ }
+ final int index = CollectionUtils.indexOf(app.requestedPermissions, permission);
+ if (index < 0 || index >= app.requestedPermissionsFlags.length) return false;
+ return (app.requestedPermissionsFlags[index] & REQUESTED_PERMISSION_GRANTED) != 0;
+ }
+
+ @VisibleForTesting
+ boolean hasNetworkPermission(@NonNull final PackageInfo app) {
+ return hasPermission(app, CHANGE_NETWORK_STATE);
+ }
+
+ @VisibleForTesting
+ boolean hasRestrictedNetworkPermission(@NonNull final PackageInfo app) {
+ // TODO : remove carryover package check in the future(b/31479477). All apps should just
+ // request the appropriate permission for their use case since android Q.
+ return isCarryoverPackage(app.applicationInfo) || isAppAllowedOnRestrictedNetworks(app)
+ || hasPermission(app, PERMISSION_MAINLINE_NETWORK_STACK)
+ || hasPermission(app, NETWORK_STACK)
+ || hasPermission(app, CONNECTIVITY_USE_RESTRICTED_NETWORKS);
+ }
+
+ /** Returns whether the given uid has using background network permission. */
+ public synchronized boolean hasUseBackgroundNetworksPermission(final int uid) {
+ // Apps with any of the CHANGE_NETWORK_STATE, NETWORK_STACK, CONNECTIVITY_INTERNAL or
+ // CONNECTIVITY_USE_RESTRICTED_NETWORKS permission has the permission to use background
+ // networks. mApps contains the result of checks for both hasNetworkPermission and
+ // hasRestrictedNetworkPermission. If uid is in the mApps list that means uid has one of
+ // permissions at least.
+ return mApps.containsKey(uid);
+ }
+
+ /**
+ * Returns whether the given uid has permission to use restricted networks.
+ */
+ public synchronized boolean hasRestrictedNetworksPermission(int uid) {
+ return Boolean.TRUE.equals(mApps.get(uid));
+ }
+
+ private void update(Set<UserHandle> users, Map<Integer, Boolean> apps, boolean add) {
+ List<Integer> network = new ArrayList<>();
+ List<Integer> system = new ArrayList<>();
+ for (Entry<Integer, Boolean> app : apps.entrySet()) {
+ List<Integer> list = app.getValue() ? system : network;
+ for (UserHandle user : users) {
+ if (user == null) continue;
+
+ list.add(user.getUid(app.getKey()));
+ }
+ }
+ try {
+ if (add) {
+ mNetd.networkSetPermissionForUser(INetd.PERMISSION_NETWORK, toIntArray(network));
+ mNetd.networkSetPermissionForUser(INetd.PERMISSION_SYSTEM, toIntArray(system));
+ } else {
+ mNetd.networkClearPermissionForUser(toIntArray(network));
+ mNetd.networkClearPermissionForUser(toIntArray(system));
+ }
+ } catch (RemoteException e) {
+ loge("Exception when updating permissions: " + e);
+ }
+ }
+
+ /**
+ * Called when a user is added. See {link #ACTION_USER_ADDED}.
+ *
+ * @param user The integer userHandle of the added user. See {@link #EXTRA_USER_HANDLE}.
+ *
+ * @hide
+ */
+ public synchronized void onUserAdded(@NonNull UserHandle user) {
+ mUsers.add(user);
+
+ Set<UserHandle> users = new HashSet<>();
+ users.add(user);
+ update(users, mApps, true);
+ }
+
+ /**
+ * Called when an user is removed. See {link #ACTION_USER_REMOVED}.
+ *
+ * @param user The integer userHandle of the removed user. See {@link #EXTRA_USER_HANDLE}.
+ *
+ * @hide
+ */
+ public synchronized void onUserRemoved(@NonNull UserHandle user) {
+ mUsers.remove(user);
+
+ Set<UserHandle> users = new HashSet<>();
+ users.add(user);
+ update(users, mApps, false);
+ }
+
+ @VisibleForTesting
+ protected Boolean highestPermissionForUid(Boolean currentPermission, String name) {
+ if (currentPermission == SYSTEM) {
+ return currentPermission;
+ }
+ try {
+ final PackageInfo app = mPackageManager.getPackageInfo(name,
+ GET_PERMISSIONS | MATCH_ANY_USER);
+ final boolean isNetwork = hasNetworkPermission(app);
+ final boolean hasRestrictedPermission = hasRestrictedNetworkPermission(app);
+ if (isNetwork || hasRestrictedPermission) {
+ currentPermission = hasRestrictedPermission;
+ }
+ } catch (NameNotFoundException e) {
+ // App not found.
+ loge("NameNotFoundException " + name);
+ }
+ return currentPermission;
+ }
+
+ private int getPermissionForUid(final int uid) {
+ int permission = INetd.PERMISSION_NONE;
+ // Check all the packages for this UID. The UID has the permission if any of the
+ // packages in it has the permission.
+ final String[] packages = mPackageManager.getPackagesForUid(uid);
+ if (packages != null && packages.length > 0) {
+ for (String name : packages) {
+ final PackageInfo app = getPackageInfo(name);
+ if (app != null && app.requestedPermissions != null) {
+ permission |= getNetdPermissionMask(app.requestedPermissions,
+ app.requestedPermissionsFlags);
+ }
+ }
+ } else {
+ // The last package of this uid is removed from device. Clean the package up.
+ permission = INetd.PERMISSION_UNINSTALLED;
+ }
+ return permission;
+ }
+
+ /**
+ * Called when a package is added.
+ *
+ * @param packageName The name of the new package.
+ * @param uid The uid of the new package.
+ *
+ * @hide
+ */
+ public synchronized void onPackageAdded(@NonNull final String packageName, final int uid) {
+ // TODO: Netd is using appId for checking traffic permission. Correct the methods that are
+ // using appId instead of uid actually
+ sendPackagePermissionsForUid(UserHandle.getAppId(uid), getPermissionForUid(uid));
+
+ // If multiple packages share a UID (cf: android:sharedUserId) and ask for different
+ // permissions, don't downgrade (i.e., if it's already SYSTEM, leave it as is).
+ final Boolean permission = highestPermissionForUid(mApps.get(uid), packageName);
+ if (permission != mApps.get(uid)) {
+ mApps.put(uid, permission);
+
+ Map<Integer, Boolean> apps = new HashMap<>();
+ apps.put(uid, permission);
+ update(mUsers, apps, true);
+ }
+
+ // If the newly-installed package falls within some VPN's uid range, update Netd with it.
+ // This needs to happen after the mApps update above, since removeBypassingUids() depends
+ // on mApps to check if the package can bypass VPN.
+ for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+ if (UidRange.containsUid(vpn.getValue(), uid)) {
+ final Set<Integer> changedUids = new HashSet<>();
+ changedUids.add(uid);
+ removeBypassingUids(changedUids, /* vpnAppUid */ -1);
+ updateVpnUids(vpn.getKey(), changedUids, true);
+ }
+ }
+ mAllApps.add(UserHandle.getAppId(uid));
+ }
+
+ private Boolean highestUidNetworkPermission(int uid) {
+ Boolean permission = null;
+ final String[] packages = mPackageManager.getPackagesForUid(uid);
+ if (!CollectionUtils.isEmpty(packages)) {
+ for (String name : packages) {
+ permission = highestPermissionForUid(permission, name);
+ if (permission == SYSTEM) {
+ break;
+ }
+ }
+ }
+ return permission;
+ }
+
+ /**
+ * Called when a package is removed.
+ *
+ * @param packageName The name of the removed package or null.
+ * @param uid containing the integer uid previously assigned to the package.
+ *
+ * @hide
+ */
+ public synchronized void onPackageRemoved(@NonNull final String packageName, final int uid) {
+ // TODO: Netd is using appId for checking traffic permission. Correct the methods that are
+ // using appId instead of uid actually
+ sendPackagePermissionsForUid(UserHandle.getAppId(uid), getPermissionForUid(uid));
+
+ // If the newly-removed package falls within some VPN's uid range, update Netd with it.
+ // This needs to happen before the mApps update below, since removeBypassingUids() depends
+ // on mApps to check if the package can bypass VPN.
+ for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+ if (UidRange.containsUid(vpn.getValue(), uid)) {
+ final Set<Integer> changedUids = new HashSet<>();
+ changedUids.add(uid);
+ removeBypassingUids(changedUids, /* vpnAppUid */ -1);
+ updateVpnUids(vpn.getKey(), changedUids, false);
+ }
+ }
+ // If the package has been removed from all users on the device, clear it form mAllApps.
+ if (mPackageManager.getNameForUid(uid) == null) {
+ mAllApps.remove(UserHandle.getAppId(uid));
+ }
+
+ Map<Integer, Boolean> apps = new HashMap<>();
+ final Boolean permission = highestUidNetworkPermission(uid);
+ if (permission == SYSTEM) {
+ // An app with this UID still has the SYSTEM permission.
+ // Therefore, this UID must already have the SYSTEM permission.
+ // Nothing to do.
+ return;
+ }
+
+ if (permission == mApps.get(uid)) {
+ // The permissions of this UID have not changed. Nothing to do.
+ return;
+ } else if (permission != null) {
+ mApps.put(uid, permission);
+ apps.put(uid, permission);
+ update(mUsers, apps, true);
+ } else {
+ mApps.remove(uid);
+ apps.put(uid, NETWORK); // doesn't matter which permission we pick here
+ update(mUsers, apps, false);
+ }
+ }
+
+ private static int getNetdPermissionMask(String[] requestedPermissions,
+ int[] requestedPermissionsFlags) {
+ int permissions = 0;
+ if (requestedPermissions == null || requestedPermissionsFlags == null) return permissions;
+ for (int i = 0; i < requestedPermissions.length; i++) {
+ if (requestedPermissions[i].equals(INTERNET)
+ && ((requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) != 0)) {
+ permissions |= INetd.PERMISSION_INTERNET;
+ }
+ if (requestedPermissions[i].equals(UPDATE_DEVICE_STATS)
+ && ((requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) != 0)) {
+ permissions |= INetd.PERMISSION_UPDATE_DEVICE_STATS;
+ }
+ }
+ return permissions;
+ }
+
+ private PackageInfo getPackageInfo(String packageName) {
+ try {
+ PackageInfo app = mPackageManager.getPackageInfo(packageName, GET_PERMISSIONS
+ | MATCH_ANY_USER);
+ return app;
+ } catch (NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Called when a new set of UID ranges are added to an active VPN network
+ *
+ * @param iface The active VPN network's interface name
+ * @param rangesToAdd The new UID ranges to be added to the network
+ * @param vpnAppUid The uid of the VPN app
+ */
+ public synchronized void onVpnUidRangesAdded(@NonNull String iface, Set<UidRange> rangesToAdd,
+ int vpnAppUid) {
+ // Calculate the list of new app uids under the VPN due to the new UID ranges and update
+ // Netd about them. Because mAllApps only contains appIds instead of uids, the result might
+ // be an overestimation if an app is not installed on the user on which the VPN is running,
+ // but that's safe.
+ final Set<Integer> changedUids = intersectUids(rangesToAdd, mAllApps);
+ removeBypassingUids(changedUids, vpnAppUid);
+ updateVpnUids(iface, changedUids, true);
+ if (mVpnUidRanges.containsKey(iface)) {
+ mVpnUidRanges.get(iface).addAll(rangesToAdd);
+ } else {
+ mVpnUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
+ }
+ }
+
+ /**
+ * Called when a set of UID ranges are removed from an active VPN network
+ *
+ * @param iface The VPN network's interface name
+ * @param rangesToRemove Existing UID ranges to be removed from the VPN network
+ * @param vpnAppUid The uid of the VPN app
+ */
+ public synchronized void onVpnUidRangesRemoved(@NonNull String iface,
+ Set<UidRange> rangesToRemove, int vpnAppUid) {
+ // Calculate the list of app uids that are no longer under the VPN due to the removed UID
+ // ranges and update Netd about them.
+ final Set<Integer> changedUids = intersectUids(rangesToRemove, mAllApps);
+ removeBypassingUids(changedUids, vpnAppUid);
+ updateVpnUids(iface, changedUids, false);
+ Set<UidRange> existingRanges = mVpnUidRanges.getOrDefault(iface, null);
+ if (existingRanges == null) {
+ loge("Attempt to remove unknown vpn uid Range iface = " + iface);
+ return;
+ }
+ existingRanges.removeAll(rangesToRemove);
+ if (existingRanges.size() == 0) {
+ mVpnUidRanges.remove(iface);
+ }
+ }
+
+ /**
+ * Compute the intersection of a set of UidRanges and appIds. Returns a set of uids
+ * that satisfies:
+ * 1. falls into one of the UidRange
+ * 2. matches one of the appIds
+ */
+ private Set<Integer> intersectUids(Set<UidRange> ranges, Set<Integer> appIds) {
+ Set<Integer> result = new HashSet<>();
+ for (UidRange range : ranges) {
+ for (int userId = range.getStartUser(); userId <= range.getEndUser(); userId++) {
+ for (int appId : appIds) {
+ final UserHandle handle = UserHandle.of(userId);
+ if (handle == null) continue;
+
+ final int uid = handle.getUid(appId);
+ if (range.contains(uid)) {
+ result.add(uid);
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Remove all apps which can elect to bypass the VPN from the list of uids
+ *
+ * An app can elect to bypass the VPN if it hold SYSTEM permission, or if its the active VPN
+ * app itself.
+ *
+ * @param uids The list of uids to operate on
+ * @param vpnAppUid The uid of the VPN app
+ */
+ private void removeBypassingUids(Set<Integer> uids, int vpnAppUid) {
+ uids.remove(vpnAppUid);
+ uids.removeIf(uid -> mApps.getOrDefault(uid, NETWORK) == SYSTEM);
+ }
+
+ /**
+ * Update netd about the list of uids that are under an active VPN connection which they cannot
+ * bypass.
+ *
+ * This is to instruct netd to set up appropriate filtering rules for these uids, such that they
+ * can only receive ingress packets from the VPN's tunnel interface (and loopback).
+ *
+ * @param iface the interface name of the active VPN connection
+ * @param add {@code true} if the uids are to be added to the interface, {@code false} if they
+ * are to be removed from the interface.
+ */
+ private void updateVpnUids(String iface, Set<Integer> uids, boolean add) {
+ if (uids.size() == 0) {
+ return;
+ }
+ try {
+ if (add) {
+ mNetd.firewallAddUidInterfaceRules(iface, toIntArray(uids));
+ } else {
+ mNetd.firewallRemoveUidInterfaceRules(toIntArray(uids));
+ }
+ } catch (ServiceSpecificException e) {
+ // Silently ignore exception when device does not support eBPF, otherwise just log
+ // the exception and do not crash
+ if (e.errorCode != OsConstants.EOPNOTSUPP) {
+ loge("Exception when updating permissions: ", e);
+ }
+ } catch (RemoteException e) {
+ loge("Exception when updating permissions: ", e);
+ }
+ }
+
+ /**
+ * Called by PackageListObserver when a package is installed/uninstalled. Send the updated
+ * permission information to netd.
+ *
+ * @param uid the app uid of the package installed
+ * @param permissions the permissions the app requested and netd cares about.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ void sendPackagePermissionsForUid(int uid, int permissions) {
+ SparseIntArray netdPermissionsAppIds = new SparseIntArray();
+ netdPermissionsAppIds.put(uid, permissions);
+ sendPackagePermissionsToNetd(netdPermissionsAppIds);
+ }
+
+ /**
+ * Called by packageManagerService to send IPC to netd. Grant or revoke the INTERNET
+ * and/or UPDATE_DEVICE_STATS permission of the uids in array.
+ *
+ * @param netdPermissionsAppIds integer pairs of uids and the permission granted to it. If the
+ * permission is 0, revoke all permissions of that uid.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ void sendPackagePermissionsToNetd(SparseIntArray netdPermissionsAppIds) {
+ if (mNetd == null) {
+ Log.e(TAG, "Failed to get the netd service");
+ return;
+ }
+ ArrayList<Integer> allPermissionAppIds = new ArrayList<>();
+ ArrayList<Integer> internetPermissionAppIds = new ArrayList<>();
+ ArrayList<Integer> updateStatsPermissionAppIds = new ArrayList<>();
+ ArrayList<Integer> noPermissionAppIds = new ArrayList<>();
+ ArrayList<Integer> uninstalledAppIds = new ArrayList<>();
+ for (int i = 0; i < netdPermissionsAppIds.size(); i++) {
+ int permissions = netdPermissionsAppIds.valueAt(i);
+ switch(permissions) {
+ case (INetd.PERMISSION_INTERNET | INetd.PERMISSION_UPDATE_DEVICE_STATS):
+ allPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
+ break;
+ case INetd.PERMISSION_INTERNET:
+ internetPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
+ break;
+ case INetd.PERMISSION_UPDATE_DEVICE_STATS:
+ updateStatsPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
+ break;
+ case INetd.PERMISSION_NONE:
+ noPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
+ break;
+ case INetd.PERMISSION_UNINSTALLED:
+ uninstalledAppIds.add(netdPermissionsAppIds.keyAt(i));
+ break;
+ default:
+ Log.e(TAG, "unknown permission type: " + permissions + "for uid: "
+ + netdPermissionsAppIds.keyAt(i));
+ }
+ }
+ try {
+ // TODO: add a lock inside netd to protect IPC trafficSetNetPermForUids()
+ if (allPermissionAppIds.size() != 0) {
+ mNetd.trafficSetNetPermForUids(
+ INetd.PERMISSION_INTERNET | INetd.PERMISSION_UPDATE_DEVICE_STATS,
+ toIntArray(allPermissionAppIds));
+ }
+ if (internetPermissionAppIds.size() != 0) {
+ mNetd.trafficSetNetPermForUids(INetd.PERMISSION_INTERNET,
+ toIntArray(internetPermissionAppIds));
+ }
+ if (updateStatsPermissionAppIds.size() != 0) {
+ mNetd.trafficSetNetPermForUids(INetd.PERMISSION_UPDATE_DEVICE_STATS,
+ toIntArray(updateStatsPermissionAppIds));
+ }
+ if (noPermissionAppIds.size() != 0) {
+ mNetd.trafficSetNetPermForUids(INetd.PERMISSION_NONE,
+ toIntArray(noPermissionAppIds));
+ }
+ if (uninstalledAppIds.size() != 0) {
+ mNetd.trafficSetNetPermForUids(INetd.PERMISSION_UNINSTALLED,
+ toIntArray(uninstalledAppIds));
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Pass appId list of special permission failed." + e);
+ }
+ }
+
+ /** Should only be used by unit tests */
+ @VisibleForTesting
+ public Set<UidRange> getVpnUidRanges(String iface) {
+ return mVpnUidRanges.get(iface);
+ }
+
+ private synchronized void onSettingChanged() {
+ // Step1. Update apps allowed to use restricted networks and compute the set of packages to
+ // update.
+ final Set<String> packagesToUpdate = new ArraySet<>(mAppsAllowedOnRestrictedNetworks);
+ updateAppsAllowedOnRestrictedNetworks(mDeps.getAppsAllowedOnRestrictedNetworks(mContext));
+ packagesToUpdate.addAll(mAppsAllowedOnRestrictedNetworks);
+
+ final Map<Integer, Boolean> updatedApps = new HashMap<>();
+ final Map<Integer, Boolean> removedApps = new HashMap<>();
+
+ // Step2. For each package to update, find out its new permission.
+ for (String app : packagesToUpdate) {
+ final PackageInfo info = getPackageInfo(app);
+ if (info == null || info.applicationInfo == null) continue;
+
+ final int uid = info.applicationInfo.uid;
+ final Boolean permission = highestUidNetworkPermission(uid);
+
+ if (null == permission) {
+ removedApps.put(uid, NETWORK); // Doesn't matter which permission is set here.
+ mApps.remove(uid);
+ } else {
+ updatedApps.put(uid, permission);
+ mApps.put(uid, permission);
+ }
+ }
+
+ // Step3. Update or revoke permission for uids with netd.
+ update(mUsers, updatedApps, true /* add */);
+ update(mUsers, removedApps, false /* add */);
+ }
+
+ /** Dump info to dumpsys */
+ public void dump(IndentingPrintWriter pw) {
+ pw.println("Interface filtering rules:");
+ pw.increaseIndent();
+ for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+ pw.println("Interface: " + vpn.getKey());
+ pw.println("UIDs: " + vpn.getValue().toString());
+ pw.println();
+ }
+ pw.decreaseIndent();
+ }
+
+ private static void log(String s) {
+ if (DBG) {
+ Log.d(TAG, s);
+ }
+ }
+
+ private static void loge(String s) {
+ Log.e(TAG, s);
+ }
+
+ private static void loge(String s, Throwable e) {
+ Log.e(TAG, s, e);
+ }
+}
diff --git a/service/src/com/android/server/connectivity/ProfileNetworkPreferences.java b/service/src/com/android/server/connectivity/ProfileNetworkPreferences.java
new file mode 100644
index 0000000..dd2815d
--- /dev/null
+++ b/service/src/com/android/server/connectivity/ProfileNetworkPreferences.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2021 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.connectivity;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkCapabilities;
+import android.os.UserHandle;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A data class containing all the per-profile network preferences.
+ *
+ * A given profile can only have one preference.
+ */
+public class ProfileNetworkPreferences {
+ /**
+ * A single preference, as it applies to a given user profile.
+ */
+ public static class Preference {
+ @NonNull public final UserHandle user;
+ // Capabilities are only null when sending an object to remove the setting for a user
+ @Nullable public final NetworkCapabilities capabilities;
+
+ public Preference(@NonNull final UserHandle user,
+ @Nullable final NetworkCapabilities capabilities) {
+ this.user = user;
+ this.capabilities = null == capabilities ? null : new NetworkCapabilities(capabilities);
+ }
+
+ /** toString */
+ public String toString() {
+ return "[ProfileNetworkPreference user=" + user + " caps=" + capabilities + "]";
+ }
+ }
+
+ @NonNull public final List<Preference> preferences;
+
+ public ProfileNetworkPreferences() {
+ preferences = Collections.EMPTY_LIST;
+ }
+
+ private ProfileNetworkPreferences(@NonNull final List<Preference> list) {
+ preferences = Collections.unmodifiableList(list);
+ }
+
+ /**
+ * Returns a new object consisting of this object plus the passed preference.
+ *
+ * If a preference already exists for the same user, it will be replaced by the passed
+ * preference. Passing a Preference object containing a null capabilities object is equivalent
+ * to (and indeed, implemented as) removing the preference for this user.
+ */
+ public ProfileNetworkPreferences plus(@NonNull final Preference pref) {
+ final ArrayList<Preference> newPrefs = new ArrayList<>();
+ for (final Preference existingPref : preferences) {
+ if (!existingPref.user.equals(pref.user)) {
+ newPrefs.add(existingPref);
+ }
+ }
+ if (null != pref.capabilities) {
+ newPrefs.add(pref);
+ }
+ return new ProfileNetworkPreferences(newPrefs);
+ }
+
+ public boolean isEmpty() {
+ return preferences.isEmpty();
+ }
+}
diff --git a/service/src/com/android/server/connectivity/ProxyTracker.java b/service/src/com/android/server/connectivity/ProxyTracker.java
new file mode 100644
index 0000000..f572b46
--- /dev/null
+++ b/service/src/com/android/server/connectivity/ProxyTracker.java
@@ -0,0 +1,353 @@
+/**
+ * Copyright (c) 2018 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.connectivity;
+
+import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_EXCLUSION_LIST;
+import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_HOST;
+import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_PAC;
+import static android.net.ConnectivitySettingsManager.GLOBAL_HTTP_PROXY_PORT;
+import static android.provider.Settings.Global.HTTP_PROXY;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Network;
+import android.net.PacProxyManager;
+import android.net.Proxy;
+import android.net.ProxyInfo;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.net.module.util.ProxyUtils;
+
+import java.util.Collections;
+import java.util.Objects;
+
+/**
+ * A class to handle proxy for ConnectivityService.
+ *
+ * @hide
+ */
+public class ProxyTracker {
+ private static final String TAG = ProxyTracker.class.getSimpleName();
+ private static final boolean DBG = true;
+
+ @NonNull
+ private final Context mContext;
+
+ @NonNull
+ private final Object mProxyLock = new Object();
+ // The global proxy is the proxy that is set device-wide, overriding any network-specific
+ // proxy. Note however that proxies are hints ; the system does not enforce their use. Hence
+ // this value is only for querying.
+ @Nullable
+ @GuardedBy("mProxyLock")
+ private ProxyInfo mGlobalProxy = null;
+ // The default proxy is the proxy that applies to no particular network if the global proxy
+ // is not set. Individual networks have their own settings that override this. This member
+ // is set through setDefaultProxy, which is called when the default network changes proxies
+ // in its LinkProperties, or when ConnectivityService switches to a new default network, or
+ // when PacProxyService resolves the proxy.
+ @Nullable
+ @GuardedBy("mProxyLock")
+ private volatile ProxyInfo mDefaultProxy = null;
+ // Whether the default proxy is enabled.
+ @GuardedBy("mProxyLock")
+ private boolean mDefaultProxyEnabled = true;
+
+ private final Handler mConnectivityServiceHandler;
+
+ private final PacProxyManager mPacProxyManager;
+
+ private class PacProxyInstalledListener implements PacProxyManager.PacProxyInstalledListener {
+ private final int mEvent;
+
+ PacProxyInstalledListener(int event) {
+ mEvent = event;
+ }
+
+ public void onPacProxyInstalled(@Nullable Network network, @NonNull ProxyInfo proxy) {
+ mConnectivityServiceHandler
+ .sendMessage(mConnectivityServiceHandler
+ .obtainMessage(mEvent, new Pair<>(network, proxy)));
+ }
+ }
+
+ public ProxyTracker(@NonNull final Context context,
+ @NonNull final Handler connectivityServiceInternalHandler, final int pacChangedEvent) {
+ mContext = context;
+ mConnectivityServiceHandler = connectivityServiceInternalHandler;
+ mPacProxyManager = context.getSystemService(PacProxyManager.class);
+
+ PacProxyInstalledListener listener = new PacProxyInstalledListener(pacChangedEvent);
+ mPacProxyManager.addPacProxyInstalledListener(
+ mConnectivityServiceHandler::post, listener);
+ }
+
+ // Convert empty ProxyInfo's to null as null-checks are used to determine if proxies are present
+ // (e.g. if mGlobalProxy==null fall back to network-specific proxy, if network-specific
+ // proxy is null then there is no proxy in place).
+ @Nullable
+ private static ProxyInfo canonicalizeProxyInfo(@Nullable final ProxyInfo proxy) {
+ if (proxy != null && TextUtils.isEmpty(proxy.getHost())
+ && Uri.EMPTY.equals(proxy.getPacFileUrl())) {
+ return null;
+ }
+ return proxy;
+ }
+
+ // ProxyInfo equality functions with a couple modifications over ProxyInfo.equals() to make it
+ // better for determining if a new proxy broadcast is necessary:
+ // 1. Canonicalize empty ProxyInfos to null so an empty proxy compares equal to null so as to
+ // avoid unnecessary broadcasts.
+ // 2. Make sure all parts of the ProxyInfo's compare true, including the host when a PAC URL
+ // is in place. This is important so legacy PAC resolver (see com.android.proxyhandler)
+ // changes aren't missed. The legacy PAC resolver pretends to be a simple HTTP proxy but
+ // actually uses the PAC to resolve; this results in ProxyInfo's with PAC URL, host and port
+ // all set.
+ public static boolean proxyInfoEqual(@Nullable final ProxyInfo a, @Nullable final ProxyInfo b) {
+ final ProxyInfo pa = canonicalizeProxyInfo(a);
+ final ProxyInfo pb = canonicalizeProxyInfo(b);
+ // ProxyInfo.equals() doesn't check hosts when PAC URLs are present, but we need to check
+ // hosts even when PAC URLs are present to account for the legacy PAC resolver.
+ return Objects.equals(pa, pb) && (pa == null || Objects.equals(pa.getHost(), pb.getHost()));
+ }
+
+ /**
+ * Gets the default system-wide proxy.
+ *
+ * This will return the global proxy if set, otherwise the default proxy if in use. Note
+ * that this is not necessarily the proxy that any given process should use, as the right
+ * proxy for a process is the proxy for the network this process will use, which may be
+ * different from this value. This value is simply the default in case there is no proxy set
+ * in the network that will be used by a specific process.
+ * @return The default system-wide proxy or null if none.
+ */
+ @Nullable
+ public ProxyInfo getDefaultProxy() {
+ // This information is already available as a world read/writable jvm property.
+ synchronized (mProxyLock) {
+ if (mGlobalProxy != null) return mGlobalProxy;
+ if (mDefaultProxyEnabled) return mDefaultProxy;
+ return null;
+ }
+ }
+
+ /**
+ * Gets the global proxy.
+ *
+ * @return The global proxy or null if none.
+ */
+ @Nullable
+ public ProxyInfo getGlobalProxy() {
+ // This information is already available as a world read/writable jvm property.
+ synchronized (mProxyLock) {
+ return mGlobalProxy;
+ }
+ }
+
+ /**
+ * Read the global proxy settings and cache them in memory.
+ */
+ public void loadGlobalProxy() {
+ if (loadDeprecatedGlobalHttpProxy()) {
+ return;
+ }
+ ContentResolver res = mContext.getContentResolver();
+ String host = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_HOST);
+ int port = Settings.Global.getInt(res, GLOBAL_HTTP_PROXY_PORT, 0);
+ String exclList = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_EXCLUSION_LIST);
+ String pacFileUrl = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_PAC);
+ if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(pacFileUrl)) {
+ ProxyInfo proxyProperties;
+ if (!TextUtils.isEmpty(pacFileUrl)) {
+ proxyProperties = ProxyInfo.buildPacProxy(Uri.parse(pacFileUrl));
+ } else {
+ proxyProperties = ProxyInfo.buildDirectProxy(host, port,
+ ProxyUtils.exclusionStringAsList(exclList));
+ }
+ if (!proxyProperties.isValid()) {
+ if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyProperties);
+ return;
+ }
+
+ synchronized (mProxyLock) {
+ mGlobalProxy = proxyProperties;
+ }
+
+ if (!TextUtils.isEmpty(pacFileUrl)) {
+ mConnectivityServiceHandler.post(
+ () -> mPacProxyManager.setCurrentProxyScriptUrl(proxyProperties));
+ }
+ }
+ }
+
+ /**
+ * Read the global proxy from the deprecated Settings.Global.HTTP_PROXY setting and apply it.
+ * Returns {@code true} when global proxy was set successfully from deprecated setting.
+ */
+ public boolean loadDeprecatedGlobalHttpProxy() {
+ final String proxy = Settings.Global.getString(mContext.getContentResolver(), HTTP_PROXY);
+ if (!TextUtils.isEmpty(proxy)) {
+ String data[] = proxy.split(":");
+ if (data.length == 0) {
+ return false;
+ }
+
+ final String proxyHost = data[0];
+ int proxyPort = 8080;
+ if (data.length > 1) {
+ try {
+ proxyPort = Integer.parseInt(data[1]);
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+ final ProxyInfo p = ProxyInfo.buildDirectProxy(proxyHost, proxyPort,
+ Collections.emptyList());
+ setGlobalProxy(p);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Sends the system broadcast informing apps about a new proxy configuration.
+ *
+ * Confusingly this method also sets the PAC file URL. TODO : separate this, it has nothing
+ * to do in a "sendProxyBroadcast" method.
+ */
+ public void sendProxyBroadcast() {
+ final ProxyInfo defaultProxy = getDefaultProxy();
+ final ProxyInfo proxyInfo = null != defaultProxy ?
+ defaultProxy : ProxyInfo.buildDirectProxy("", 0, Collections.emptyList());
+ mPacProxyManager.setCurrentProxyScriptUrl(proxyInfo);
+
+ if (!shouldSendBroadcast(proxyInfo)) {
+ return;
+ }
+ if (DBG) Log.d(TAG, "sending Proxy Broadcast for " + proxyInfo);
+ Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
+ intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING |
+ Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ intent.putExtra(Proxy.EXTRA_PROXY_INFO, proxyInfo);
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private boolean shouldSendBroadcast(ProxyInfo proxy) {
+ return Uri.EMPTY.equals(proxy.getPacFileUrl()) || proxy.getPort() > 0;
+ }
+
+ /**
+ * Sets the global proxy in memory. Also writes the values to the global settings of the device.
+ *
+ * @param proxyInfo the proxy spec, or null for no proxy.
+ */
+ public void setGlobalProxy(@Nullable ProxyInfo proxyInfo) {
+ synchronized (mProxyLock) {
+ // ProxyInfo#equals is not commutative :( and is public API, so it can't be fixed.
+ if (proxyInfo == mGlobalProxy) return;
+ if (proxyInfo != null && proxyInfo.equals(mGlobalProxy)) return;
+ if (mGlobalProxy != null && mGlobalProxy.equals(proxyInfo)) return;
+
+ final String host;
+ final int port;
+ final String exclList;
+ final String pacFileUrl;
+ if (proxyInfo != null && (!TextUtils.isEmpty(proxyInfo.getHost()) ||
+ !Uri.EMPTY.equals(proxyInfo.getPacFileUrl()))) {
+ if (!proxyInfo.isValid()) {
+ if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo);
+ return;
+ }
+ mGlobalProxy = new ProxyInfo(proxyInfo);
+ host = mGlobalProxy.getHost();
+ port = mGlobalProxy.getPort();
+ exclList = ProxyUtils.exclusionListAsString(mGlobalProxy.getExclusionList());
+ pacFileUrl = Uri.EMPTY.equals(proxyInfo.getPacFileUrl())
+ ? "" : proxyInfo.getPacFileUrl().toString();
+ } else {
+ host = "";
+ port = 0;
+ exclList = "";
+ pacFileUrl = "";
+ mGlobalProxy = null;
+ }
+ final ContentResolver res = mContext.getContentResolver();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ Settings.Global.putString(res, GLOBAL_HTTP_PROXY_HOST, host);
+ Settings.Global.putInt(res, GLOBAL_HTTP_PROXY_PORT, port);
+ Settings.Global.putString(res, GLOBAL_HTTP_PROXY_EXCLUSION_LIST, exclList);
+ Settings.Global.putString(res, GLOBAL_HTTP_PROXY_PAC, pacFileUrl);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+
+ sendProxyBroadcast();
+ }
+ }
+
+ /**
+ * Sets the default proxy for the device.
+ *
+ * The default proxy is the proxy used for networks that do not have a specific proxy.
+ * @param proxyInfo the proxy spec, or null for no proxy.
+ */
+ public void setDefaultProxy(@Nullable ProxyInfo proxyInfo) {
+ synchronized (mProxyLock) {
+ if (Objects.equals(mDefaultProxy, proxyInfo)) return;
+ if (proxyInfo != null && !proxyInfo.isValid()) {
+ if (DBG) Log.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo);
+ return;
+ }
+
+ // This call could be coming from the PacProxyService, containing the port of the
+ // local proxy. If this new proxy matches the global proxy then copy this proxy to the
+ // global (to get the correct local port), and send a broadcast.
+ // TODO: Switch PacProxyService to have its own message to send back rather than
+ // reusing EVENT_HAS_CHANGED_PROXY and this call to handleApplyDefaultProxy.
+ if ((mGlobalProxy != null) && (proxyInfo != null)
+ && (!Uri.EMPTY.equals(proxyInfo.getPacFileUrl()))
+ && proxyInfo.getPacFileUrl().equals(mGlobalProxy.getPacFileUrl())) {
+ mGlobalProxy = proxyInfo;
+ sendProxyBroadcast();
+ return;
+ }
+ mDefaultProxy = proxyInfo;
+
+ if (mGlobalProxy != null) return;
+ if (mDefaultProxyEnabled) {
+ sendProxyBroadcast();
+ }
+ }
+ }
+}
diff --git a/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java b/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
new file mode 100644
index 0000000..534dbe7
--- /dev/null
+++ b/service/src/com/android/server/connectivity/QosCallbackAgentConnection.java
@@ -0,0 +1,199 @@
+/*
+ * 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.connectivity;
+
+import static android.net.QosCallbackException.EX_TYPE_FILTER_NONE;
+
+import android.annotation.NonNull;
+import android.net.IQosCallback;
+import android.net.Network;
+import android.net.QosCallbackException;
+import android.net.QosFilter;
+import android.net.QosSession;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.telephony.data.EpsBearerQosSessionAttributes;
+import android.telephony.data.NrQosSessionAttributes;
+import android.util.Log;
+
+import java.util.Objects;
+
+/**
+ * Wraps callback related information and sends messages between network agent and the application.
+ * <p/>
+ * This is a satellite class of {@link com.android.server.ConnectivityService} and not meant
+ * to be used in other contexts.
+ *
+ * @hide
+ */
+class QosCallbackAgentConnection implements IBinder.DeathRecipient {
+ private static final String TAG = QosCallbackAgentConnection.class.getSimpleName();
+ private static final boolean DBG = false;
+
+ private final int mAgentCallbackId;
+ @NonNull private final QosCallbackTracker mQosCallbackTracker;
+ @NonNull private final IQosCallback mCallback;
+ @NonNull private final IBinder mBinder;
+ @NonNull private final QosFilter mFilter;
+ @NonNull private final NetworkAgentInfo mNetworkAgentInfo;
+
+ private final int mUid;
+
+ /**
+ * Gets the uid
+ * @return uid
+ */
+ int getUid() {
+ return mUid;
+ }
+
+ /**
+ * Gets the binder
+ * @return binder
+ */
+ @NonNull
+ IBinder getBinder() {
+ return mBinder;
+ }
+
+ /**
+ * Gets the callback id
+ *
+ * @return callback id
+ */
+ int getAgentCallbackId() {
+ return mAgentCallbackId;
+ }
+
+ /**
+ * Gets the network tied to the callback of this connection
+ *
+ * @return network
+ */
+ @NonNull
+ Network getNetwork() {
+ return mFilter.getNetwork();
+ }
+
+ QosCallbackAgentConnection(@NonNull final QosCallbackTracker qosCallbackTracker,
+ final int agentCallbackId,
+ @NonNull final IQosCallback callback,
+ @NonNull final QosFilter filter,
+ final int uid,
+ @NonNull final NetworkAgentInfo networkAgentInfo) {
+ Objects.requireNonNull(qosCallbackTracker, "qosCallbackTracker must be non-null");
+ Objects.requireNonNull(callback, "callback must be non-null");
+ Objects.requireNonNull(filter, "filter must be non-null");
+ Objects.requireNonNull(networkAgentInfo, "networkAgentInfo must be non-null");
+
+ mQosCallbackTracker = qosCallbackTracker;
+ mAgentCallbackId = agentCallbackId;
+ mCallback = callback;
+ mFilter = filter;
+ mUid = uid;
+ mBinder = mCallback.asBinder();
+ mNetworkAgentInfo = networkAgentInfo;
+ }
+
+ @Override
+ public void binderDied() {
+ logw("binderDied: binder died with callback id: " + mAgentCallbackId);
+ mQosCallbackTracker.unregisterCallback(mCallback);
+ }
+
+ void unlinkToDeathRecipient() {
+ mBinder.unlinkToDeath(this, 0);
+ }
+
+ // Returns false if the NetworkAgent was never notified.
+ boolean sendCmdRegisterCallback() {
+ final int exceptionType = mFilter.validate();
+ if (exceptionType != EX_TYPE_FILTER_NONE) {
+ try {
+ if (DBG) log("sendCmdRegisterCallback: filter validation failed");
+ mCallback.onError(exceptionType);
+ } catch (final RemoteException e) {
+ loge("sendCmdRegisterCallback:", e);
+ }
+ return false;
+ }
+
+ try {
+ mBinder.linkToDeath(this, 0);
+ } catch (final RemoteException e) {
+ loge("failed linking to death recipient", e);
+ return false;
+ }
+ mNetworkAgentInfo.onQosFilterCallbackRegistered(mAgentCallbackId, mFilter);
+ return true;
+ }
+
+ void sendCmdUnregisterCallback() {
+ if (DBG) log("sendCmdUnregisterCallback: unregistering");
+ mNetworkAgentInfo.onQosCallbackUnregistered(mAgentCallbackId);
+ }
+
+ void sendEventEpsQosSessionAvailable(final QosSession session,
+ final EpsBearerQosSessionAttributes attributes) {
+ try {
+ if (DBG) log("sendEventEpsQosSessionAvailable: sending...");
+ mCallback.onQosEpsBearerSessionAvailable(session, attributes);
+ } catch (final RemoteException e) {
+ loge("sendEventEpsQosSessionAvailable: remote exception", e);
+ }
+ }
+
+ void sendEventNrQosSessionAvailable(final QosSession session,
+ final NrQosSessionAttributes attributes) {
+ try {
+ if (DBG) log("sendEventNrQosSessionAvailable: sending...");
+ mCallback.onNrQosSessionAvailable(session, attributes);
+ } catch (final RemoteException e) {
+ loge("sendEventNrQosSessionAvailable: remote exception", e);
+ }
+ }
+
+ void sendEventQosSessionLost(@NonNull final QosSession session) {
+ try {
+ if (DBG) log("sendEventQosSessionLost: sending...");
+ mCallback.onQosSessionLost(session);
+ } catch (final RemoteException e) {
+ loge("sendEventQosSessionLost: remote exception", e);
+ }
+ }
+
+ void sendEventQosCallbackError(@QosCallbackException.ExceptionType final int exceptionType) {
+ try {
+ if (DBG) log("sendEventQosCallbackError: sending...");
+ mCallback.onError(exceptionType);
+ } catch (final RemoteException e) {
+ loge("sendEventQosCallbackError: remote exception", e);
+ }
+ }
+
+ private static void log(@NonNull final String msg) {
+ Log.d(TAG, msg);
+ }
+
+ private static void logw(@NonNull final String msg) {
+ Log.w(TAG, msg);
+ }
+
+ private static void loge(@NonNull final String msg, final Throwable t) {
+ Log.e(TAG, msg, t);
+ }
+}
diff --git a/service/src/com/android/server/connectivity/QosCallbackTracker.java b/service/src/com/android/server/connectivity/QosCallbackTracker.java
new file mode 100644
index 0000000..b6ab47b
--- /dev/null
+++ b/service/src/com/android/server/connectivity/QosCallbackTracker.java
@@ -0,0 +1,292 @@
+/*
+ * 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.connectivity;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.IQosCallback;
+import android.net.Network;
+import android.net.QosCallbackException;
+import android.net.QosFilter;
+import android.net.QosSession;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.telephony.data.EpsBearerQosSessionAttributes;
+import android.telephony.data.NrQosSessionAttributes;
+import android.util.Log;
+
+import com.android.net.module.util.CollectionUtils;
+import com.android.server.ConnectivityService;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tracks qos callbacks and handles the communication between the network agent and application.
+ * <p/>
+ * Any method prefixed by handle must be called from the
+ * {@link com.android.server.ConnectivityService} handler thread.
+ *
+ * @hide
+ */
+public class QosCallbackTracker {
+ private static final String TAG = QosCallbackTracker.class.getSimpleName();
+ private static final boolean DBG = true;
+
+ @NonNull
+ private final Handler mConnectivityServiceHandler;
+
+ @NonNull
+ private final ConnectivityService.PerUidCounter mNetworkRequestCounter;
+
+ /**
+ * Each agent gets a unique callback id that is used to proxy messages back to the original
+ * callback.
+ * <p/>
+ * Note: The fact that this is initialized to 0 is to ensure that the thread running
+ * {@link #handleRegisterCallback(IQosCallback, QosFilter, int, NetworkAgentInfo)} sees the
+ * initialized value. This would not necessarily be the case if the value was initialized to
+ * the non-default value.
+ * <p/>
+ * Note: The term previous does not apply to the first callback id that is assigned.
+ */
+ private int mPreviousAgentCallbackId = 0;
+
+ @NonNull
+ private final List<QosCallbackAgentConnection> mConnections = new ArrayList<>();
+
+ /**
+ *
+ * @param connectivityServiceHandler must be the same handler used with
+ * {@link com.android.server.ConnectivityService}
+ * @param networkRequestCounter keeps track of the number of open requests under a given
+ * uid
+ */
+ public QosCallbackTracker(@NonNull final Handler connectivityServiceHandler,
+ final ConnectivityService.PerUidCounter networkRequestCounter) {
+ mConnectivityServiceHandler = connectivityServiceHandler;
+ mNetworkRequestCounter = networkRequestCounter;
+ }
+
+ /**
+ * Registers the callback with the tracker
+ *
+ * @param callback the callback to register
+ * @param filter the filter being registered alongside the callback
+ */
+ public void registerCallback(@NonNull final IQosCallback callback,
+ @NonNull final QosFilter filter, @NonNull final NetworkAgentInfo networkAgentInfo) {
+ final int uid = Binder.getCallingUid();
+
+ // Enforce that the number of requests under this uid has exceeded the allowed number
+ mNetworkRequestCounter.incrementCountOrThrow(uid);
+
+ mConnectivityServiceHandler.post(
+ () -> handleRegisterCallback(callback, filter, uid, networkAgentInfo));
+ }
+
+ private void handleRegisterCallback(@NonNull final IQosCallback callback,
+ @NonNull final QosFilter filter, final int uid,
+ @NonNull final NetworkAgentInfo networkAgentInfo) {
+ final QosCallbackAgentConnection ac =
+ handleRegisterCallbackInternal(callback, filter, uid, networkAgentInfo);
+ if (ac != null) {
+ if (DBG) log("handleRegisterCallback: added callback " + ac.getAgentCallbackId());
+ mConnections.add(ac);
+ } else {
+ mNetworkRequestCounter.decrementCount(uid);
+ }
+ }
+
+ private QosCallbackAgentConnection handleRegisterCallbackInternal(
+ @NonNull final IQosCallback callback,
+ @NonNull final QosFilter filter, final int uid,
+ @NonNull final NetworkAgentInfo networkAgentInfo) {
+ final IBinder binder = callback.asBinder();
+ if (CollectionUtils.any(mConnections, c -> c.getBinder().equals(binder))) {
+ // A duplicate registration would have only made this far due to a programming error.
+ logwtf("handleRegisterCallback: Callbacks can only be register once.");
+ return null;
+ }
+
+ mPreviousAgentCallbackId = mPreviousAgentCallbackId + 1;
+ final int newCallbackId = mPreviousAgentCallbackId;
+
+ final QosCallbackAgentConnection ac =
+ new QosCallbackAgentConnection(this, newCallbackId, callback,
+ filter, uid, networkAgentInfo);
+
+ final int exceptionType = filter.validate();
+ if (exceptionType != QosCallbackException.EX_TYPE_FILTER_NONE) {
+ ac.sendEventQosCallbackError(exceptionType);
+ return null;
+ }
+
+ // Only add to the callback maps if the NetworkAgent successfully registered it
+ if (!ac.sendCmdRegisterCallback()) {
+ // There was an issue when registering the agent
+ if (DBG) log("handleRegisterCallback: error sending register callback");
+ mNetworkRequestCounter.decrementCount(uid);
+ return null;
+ }
+ return ac;
+ }
+
+ /**
+ * Unregisters callback
+ * @param callback callback to unregister
+ */
+ public void unregisterCallback(@NonNull final IQosCallback callback) {
+ mConnectivityServiceHandler.post(() -> handleUnregisterCallback(callback.asBinder(), true));
+ }
+
+ private void handleUnregisterCallback(@NonNull final IBinder binder,
+ final boolean sendToNetworkAgent) {
+ final int connIndex =
+ CollectionUtils.indexOf(mConnections, c -> c.getBinder().equals(binder));
+ if (connIndex < 0) {
+ logw("handleUnregisterCallback: no matching agentConnection");
+ return;
+ }
+ final QosCallbackAgentConnection agentConnection = mConnections.get(connIndex);
+
+ if (DBG) {
+ log("handleUnregisterCallback: unregister "
+ + agentConnection.getAgentCallbackId());
+ }
+
+ mNetworkRequestCounter.decrementCount(agentConnection.getUid());
+ mConnections.remove(agentConnection);
+
+ if (sendToNetworkAgent) {
+ agentConnection.sendCmdUnregisterCallback();
+ }
+ agentConnection.unlinkToDeathRecipient();
+ }
+
+ /**
+ * Called when the NetworkAgent sends the qos session available event for EPS
+ *
+ * @param qosCallbackId the callback id that the qos session is now available to
+ * @param session the qos session that is now available
+ * @param attributes the qos attributes that are now available on the qos session
+ */
+ public void sendEventEpsQosSessionAvailable(final int qosCallbackId,
+ final QosSession session,
+ final EpsBearerQosSessionAttributes attributes) {
+ runOnAgentConnection(qosCallbackId, "sendEventEpsQosSessionAvailable: ",
+ ac -> ac.sendEventEpsQosSessionAvailable(session, attributes));
+ }
+
+ /**
+ * Called when the NetworkAgent sends the qos session available event for NR
+ *
+ * @param qosCallbackId the callback id that the qos session is now available to
+ * @param session the qos session that is now available
+ * @param attributes the qos attributes that are now available on the qos session
+ */
+ public void sendEventNrQosSessionAvailable(final int qosCallbackId,
+ final QosSession session,
+ final NrQosSessionAttributes attributes) {
+ runOnAgentConnection(qosCallbackId, "sendEventNrQosSessionAvailable: ",
+ ac -> ac.sendEventNrQosSessionAvailable(session, attributes));
+ }
+
+ /**
+ * Called when the NetworkAgent sends the qos session lost event
+ *
+ * @param qosCallbackId the callback id that lost the qos session
+ * @param session the corresponding qos session
+ */
+ public void sendEventQosSessionLost(final int qosCallbackId,
+ final QosSession session) {
+ runOnAgentConnection(qosCallbackId, "sendEventQosSessionLost: ",
+ ac -> ac.sendEventQosSessionLost(session));
+ }
+
+ /**
+ * Called when the NetworkAgent sends the qos session on error event
+ *
+ * @param qosCallbackId the callback id that should receive the exception
+ * @param exceptionType the type of exception that caused the callback to error
+ */
+ public void sendEventQosCallbackError(final int qosCallbackId,
+ @QosCallbackException.ExceptionType final int exceptionType) {
+ runOnAgentConnection(qosCallbackId, "sendEventQosCallbackError: ",
+ ac -> {
+ ac.sendEventQosCallbackError(exceptionType);
+ handleUnregisterCallback(ac.getBinder(), false);
+ });
+ }
+
+ /**
+ * Unregisters all callbacks associated to this network agent
+ *
+ * Note: Must be called on the connectivity service handler thread
+ *
+ * @param network the network that was released
+ */
+ public void handleNetworkReleased(@Nullable final Network network) {
+ // Iterate in reverse order as agent connections will be removed when unregistering
+ for (int i = mConnections.size() - 1; i >= 0; i--) {
+ final QosCallbackAgentConnection agentConnection = mConnections.get(i);
+ if (!agentConnection.getNetwork().equals(network)) continue;
+ agentConnection.sendEventQosCallbackError(
+ QosCallbackException.EX_TYPE_FILTER_NETWORK_RELEASED);
+
+ // Call unregister workflow w\o sending anything to agent since it is disconnected.
+ handleUnregisterCallback(agentConnection.getBinder(), false);
+ }
+ }
+
+ private interface AgentConnectionAction {
+ void execute(@NonNull QosCallbackAgentConnection agentConnection);
+ }
+
+ @Nullable
+ private void runOnAgentConnection(final int qosCallbackId,
+ @NonNull final String logPrefix,
+ @NonNull final AgentConnectionAction action) {
+ mConnectivityServiceHandler.post(() -> {
+ final int acIndex = CollectionUtils.indexOf(mConnections,
+ c -> c.getAgentCallbackId() == qosCallbackId);
+ if (acIndex == -1) {
+ loge(logPrefix + ": " + qosCallbackId + " missing callback id");
+ return;
+ }
+
+ action.execute(mConnections.get(acIndex));
+ });
+ }
+
+ private static void log(final String msg) {
+ Log.d(TAG, msg);
+ }
+
+ private static void logw(final String msg) {
+ Log.w(TAG, msg);
+ }
+
+ private static void loge(final String msg) {
+ Log.e(TAG, msg);
+ }
+
+ private static void logwtf(final String msg) {
+ Log.wtf(TAG, msg);
+ }
+}
diff --git a/service/src/com/android/server/connectivity/TcpKeepaliveController.java b/service/src/com/android/server/connectivity/TcpKeepaliveController.java
new file mode 100644
index 0000000..c480594
--- /dev/null
+++ b/service/src/com/android/server/connectivity/TcpKeepaliveController.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2019 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.connectivity;
+
+import static android.net.SocketKeepalive.DATA_RECEIVED;
+import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET;
+import static android.net.SocketKeepalive.ERROR_SOCKET_NOT_IDLE;
+import static android.net.SocketKeepalive.ERROR_UNSUPPORTED;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+import static android.system.OsConstants.ENOPROTOOPT;
+import static android.system.OsConstants.FIONREAD;
+import static android.system.OsConstants.IPPROTO_IP;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IP_TOS;
+import static android.system.OsConstants.IP_TTL;
+import static android.system.OsConstants.TIOCOUTQ;
+
+import android.annotation.NonNull;
+import android.net.InvalidPacketException;
+import android.net.NetworkUtils;
+import android.net.SocketKeepalive.InvalidSocketException;
+import android.net.TcpKeepalivePacketData;
+import android.net.TcpKeepalivePacketDataParcelable;
+import android.net.TcpRepairWindow;
+import android.net.util.KeepalivePacketDataUtil;
+import android.os.Handler;
+import android.os.MessageQueue;
+import android.os.Messenger;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.connectivity.KeepaliveTracker.KeepaliveInfo;
+
+import java.io.FileDescriptor;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.SocketException;
+
+/**
+ * Manage tcp socket which offloads tcp keepalive.
+ *
+ * The input socket will be changed to repair mode and the application
+ * will not have permission to read/write data. If the application wants
+ * to write data, it must stop tcp keepalive offload to leave repair mode
+ * first. If a remote packet arrives, repair mode will be turned off and
+ * offload will be stopped. The application will receive a callback to know
+ * it can start reading data.
+ *
+ * {start,stop}SocketMonitor are thread-safe, but care must be taken in the
+ * order in which they are called. Please note that while calling
+ * {@link #startSocketMonitor(FileDescriptor, Messenger, int)} multiple times
+ * with either the same slot or the same FileDescriptor without stopping it in
+ * between will result in an exception, calling {@link #stopSocketMonitor(int)}
+ * multiple times with the same int is explicitly a no-op.
+ * Please also note that switching the socket to repair mode is not synchronized
+ * with either of these operations and has to be done in an orderly fashion
+ * with stopSocketMonitor. Take care in calling these in the right order.
+ * @hide
+ */
+public class TcpKeepaliveController {
+ private static final String TAG = "TcpKeepaliveController";
+ private static final boolean DBG = false;
+
+ private final MessageQueue mFdHandlerQueue;
+
+ private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
+
+ // Reference include/uapi/linux/tcp.h
+ private static final int TCP_REPAIR = 19;
+ private static final int TCP_REPAIR_QUEUE = 20;
+ private static final int TCP_QUEUE_SEQ = 21;
+ private static final int TCP_NO_QUEUE = 0;
+ private static final int TCP_RECV_QUEUE = 1;
+ private static final int TCP_SEND_QUEUE = 2;
+ private static final int TCP_REPAIR_OFF = 0;
+ private static final int TCP_REPAIR_ON = 1;
+ // Reference include/uapi/linux/sockios.h
+ private static final int SIOCINQ = FIONREAD;
+ private static final int SIOCOUTQ = TIOCOUTQ;
+
+ /**
+ * Keeps track of packet listeners.
+ * Key: slot number of keepalive offload.
+ * Value: {@link FileDescriptor} being listened to.
+ */
+ @GuardedBy("mListeners")
+ private final SparseArray<FileDescriptor> mListeners = new SparseArray<>();
+
+ public TcpKeepaliveController(final Handler connectivityServiceHandler) {
+ mFdHandlerQueue = connectivityServiceHandler.getLooper().getQueue();
+ }
+
+ /** Build tcp keepalive packet. */
+ public static TcpKeepalivePacketData getTcpKeepalivePacket(@NonNull FileDescriptor fd)
+ throws InvalidPacketException, InvalidSocketException {
+ try {
+ final TcpKeepalivePacketDataParcelable tcpDetails = switchToRepairMode(fd);
+ return KeepalivePacketDataUtil.fromStableParcelable(tcpDetails);
+ } catch (InvalidPacketException | InvalidSocketException e) {
+ switchOutOfRepairMode(fd);
+ throw e;
+ }
+ }
+ /**
+ * Switch the tcp socket to repair mode and query detail tcp information.
+ *
+ * @param fd the fd of socket on which to use keepalive offload.
+ * @return a {@link TcpKeepalivePacketDataParcelable} object for current
+ * tcp/ip information.
+ */
+ private static TcpKeepalivePacketDataParcelable switchToRepairMode(FileDescriptor fd)
+ throws InvalidSocketException {
+ if (DBG) Log.i(TAG, "switchToRepairMode to start tcp keepalive : " + fd);
+ final TcpKeepalivePacketDataParcelable tcpDetails = new TcpKeepalivePacketDataParcelable();
+ final SocketAddress srcSockAddr;
+ final SocketAddress dstSockAddr;
+ final TcpRepairWindow trw;
+
+ // Query source address and port.
+ try {
+ srcSockAddr = Os.getsockname(fd);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Get sockname fail: ", e);
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+ }
+ if (srcSockAddr instanceof InetSocketAddress) {
+ tcpDetails.srcAddress = getAddress((InetSocketAddress) srcSockAddr);
+ tcpDetails.srcPort = getPort((InetSocketAddress) srcSockAddr);
+ } else {
+ Log.e(TAG, "Invalid or mismatched SocketAddress");
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET);
+ }
+ // Query destination address and port.
+ try {
+ dstSockAddr = Os.getpeername(fd);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Get peername fail: ", e);
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+ }
+ if (dstSockAddr instanceof InetSocketAddress) {
+ tcpDetails.dstAddress = getAddress((InetSocketAddress) dstSockAddr);
+ tcpDetails.dstPort = getPort((InetSocketAddress) dstSockAddr);
+ } else {
+ Log.e(TAG, "Invalid or mismatched peer SocketAddress");
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET);
+ }
+
+ // Query sequence and ack number
+ dropAllIncomingPackets(fd, true);
+ try {
+ // Switch to tcp repair mode.
+ Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR, TCP_REPAIR_ON);
+
+ // Check if socket is idle.
+ if (!isSocketIdle(fd)) {
+ Log.e(TAG, "Socket is not idle");
+ throw new InvalidSocketException(ERROR_SOCKET_NOT_IDLE);
+ }
+ // Query write sequence number from SEND_QUEUE.
+ Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_SEND_QUEUE);
+ tcpDetails.seq = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);
+ // Query read sequence number from RECV_QUEUE.
+ Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_RECV_QUEUE);
+ tcpDetails.ack = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);
+ // Switch to NO_QUEUE to prevent illegal socket read/write in repair mode.
+ Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_NO_QUEUE);
+ // Finally, check if socket is still idle. TODO : this check needs to move to
+ // after starting polling to prevent a race.
+ if (!isReceiveQueueEmpty(fd)) {
+ Log.e(TAG, "Fatal: receive queue of this socket is not empty");
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET);
+ }
+ if (!isSendQueueEmpty(fd)) {
+ Log.e(TAG, "Socket is not idle");
+ throw new InvalidSocketException(ERROR_SOCKET_NOT_IDLE);
+ }
+
+ // Query tcp window size.
+ trw = NetworkUtils.getTcpRepairWindow(fd);
+ tcpDetails.rcvWnd = trw.rcvWnd;
+ tcpDetails.rcvWndScale = trw.rcvWndScale;
+ if (tcpDetails.srcAddress.length == 4 /* V4 address length */) {
+ // Query TOS.
+ tcpDetails.tos = Os.getsockoptInt(fd, IPPROTO_IP, IP_TOS);
+ // Query TTL.
+ tcpDetails.ttl = Os.getsockoptInt(fd, IPPROTO_IP, IP_TTL);
+ }
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Exception reading TCP state from socket", e);
+ if (e.errno == ENOPROTOOPT) {
+ // ENOPROTOOPT may happen in kernel version lower than 4.8.
+ // Treat it as ERROR_UNSUPPORTED.
+ throw new InvalidSocketException(ERROR_UNSUPPORTED, e);
+ } else {
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+ }
+ } finally {
+ dropAllIncomingPackets(fd, false);
+ }
+
+ // Keepalive sequence number is last sequence number - 1. If it couldn't be retrieved,
+ // then it must be set to -1, so decrement in all cases.
+ tcpDetails.seq = tcpDetails.seq - 1;
+
+ return tcpDetails;
+ }
+
+ /**
+ * Switch the tcp socket out of repair mode.
+ *
+ * @param fd the fd of socket to switch back to normal.
+ */
+ private static void switchOutOfRepairMode(@NonNull final FileDescriptor fd) {
+ try {
+ Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR, TCP_REPAIR_OFF);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot switch socket out of repair mode", e);
+ // Well, there is not much to do here to recover
+ }
+ }
+
+ /**
+ * Start monitoring incoming packets.
+ *
+ * @param fd socket fd to monitor.
+ * @param ki a {@link KeepaliveInfo} that tracks information about a socket keepalive.
+ * @param slot keepalive slot.
+ */
+ public void startSocketMonitor(@NonNull final FileDescriptor fd,
+ @NonNull final KeepaliveInfo ki, final int slot)
+ throws IllegalArgumentException, InvalidSocketException {
+ synchronized (mListeners) {
+ if (null != mListeners.get(slot)) {
+ throw new IllegalArgumentException("This slot is already taken");
+ }
+ for (int i = 0; i < mListeners.size(); ++i) {
+ if (fd.equals(mListeners.valueAt(i))) {
+ Log.e(TAG, "This fd is already registered.");
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET);
+ }
+ }
+ mFdHandlerQueue.addOnFileDescriptorEventListener(fd, FD_EVENTS, (readyFd, events) -> {
+ // This can't be called twice because the queue guarantees that once the listener
+ // is unregistered it can't be called again, even for a message that arrived
+ // before it was unregistered.
+ final int reason;
+ if (0 != (events & EVENT_ERROR)) {
+ reason = ERROR_INVALID_SOCKET;
+ } else {
+ reason = DATA_RECEIVED;
+ }
+ ki.onFileDescriptorInitiatedStop(reason);
+ // The listener returns the new set of events to listen to. Because 0 means no
+ // event, the listener gets unregistered.
+ return 0;
+ });
+ mListeners.put(slot, fd);
+ }
+ }
+
+ /** Stop socket monitor */
+ // This slot may have been stopped automatically already because the socket received data,
+ // was closed on the other end or otherwise suffered some error. In this case, this function
+ // is a no-op.
+ public void stopSocketMonitor(final int slot) {
+ final FileDescriptor fd;
+ synchronized (mListeners) {
+ fd = mListeners.get(slot);
+ if (null == fd) return;
+ mListeners.remove(slot);
+ }
+ mFdHandlerQueue.removeOnFileDescriptorEventListener(fd);
+ if (DBG) Log.d(TAG, "Moving socket out of repair mode for stop : " + fd);
+ switchOutOfRepairMode(fd);
+ }
+
+ private static byte [] getAddress(InetSocketAddress inetAddr) {
+ return inetAddr.getAddress().getAddress();
+ }
+
+ private static int getPort(InetSocketAddress inetAddr) {
+ return inetAddr.getPort();
+ }
+
+ private static boolean isSocketIdle(FileDescriptor fd) throws ErrnoException {
+ return isReceiveQueueEmpty(fd) && isSendQueueEmpty(fd);
+ }
+
+ private static boolean isReceiveQueueEmpty(FileDescriptor fd)
+ throws ErrnoException {
+ final int result = Os.ioctlInt(fd, SIOCINQ);
+ if (result != 0) {
+ Log.e(TAG, "Read queue has data");
+ return false;
+ }
+ return true;
+ }
+
+ private static boolean isSendQueueEmpty(FileDescriptor fd)
+ throws ErrnoException {
+ final int result = Os.ioctlInt(fd, SIOCOUTQ);
+ if (result != 0) {
+ Log.e(TAG, "Write queue has data");
+ return false;
+ }
+ return true;
+ }
+
+ private static void dropAllIncomingPackets(FileDescriptor fd, boolean enable)
+ throws InvalidSocketException {
+ try {
+ if (enable) {
+ NetworkUtils.attachDropAllBPFFilter(fd);
+ } else {
+ NetworkUtils.detachBPFFilter(fd);
+ }
+ } catch (SocketException e) {
+ Log.e(TAG, "Socket Exception: ", e);
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+ }
+ }
+}
diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java
index b178bad..a30d4f1 100644
--- a/tests/common/java/android/net/NetworkCapabilitiesTest.java
+++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java
@@ -33,6 +33,7 @@
import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PAID;
import static android.net.NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_WIFI_P2P;
import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION;
@@ -340,7 +341,7 @@
private void testParcelSane(NetworkCapabilities cap) {
if (isAtLeastS()) {
- assertParcelSane(cap, 17);
+ assertParcelSane(cap, 16);
} else if (isAtLeastR()) {
assertParcelSane(cap, 15);
} else {
@@ -1149,4 +1150,15 @@
assertEquals(Set.of(TEST_SUBID1), nc2.getSubscriptionIds());
}
}
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testBuilderWithoutDefaultCap() {
+ final NetworkCapabilities nc =
+ NetworkCapabilities.Builder.withoutDefaultCapabilities().build();
+ assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED));
+ assertFalse(nc.hasCapability(NET_CAPABILITY_TRUSTED));
+ assertFalse(nc.hasCapability(NET_CAPABILITY_NOT_VPN));
+ // Ensure test case fails if new net cap is added into default cap but no update here.
+ assertEquals(0, nc.getCapabilities().length);
+ }
}
diff --git a/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt b/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt
index 87cfb34..f23ba26 100644
--- a/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt
+++ b/tests/common/java/android/net/UnderlyingNetworkInfoTest.kt
@@ -36,15 +36,15 @@
@Test
fun testParcelUnparcel() {
val testInfo = UnderlyingNetworkInfo(TEST_OWNER_UID, TEST_IFACE, TEST_IFACE_LIST)
- assertEquals(TEST_OWNER_UID, testInfo.ownerUid)
- assertEquals(TEST_IFACE, testInfo.iface)
- assertEquals(TEST_IFACE_LIST, testInfo.underlyingIfaces)
+ assertEquals(TEST_OWNER_UID, testInfo.getOwnerUid())
+ assertEquals(TEST_IFACE, testInfo.getInterface())
+ assertEquals(TEST_IFACE_LIST, testInfo.getUnderlyingInterfaces())
assertParcelSane(testInfo, 3)
val emptyInfo = UnderlyingNetworkInfo(0, String(), listOf())
- assertEquals(0, emptyInfo.ownerUid)
- assertEquals(String(), emptyInfo.iface)
- assertEquals(listOf(), emptyInfo.underlyingIfaces)
+ assertEquals(0, emptyInfo.getOwnerUid())
+ assertEquals(String(), emptyInfo.getInterface())
+ assertEquals(listOf(), emptyInfo.getUnderlyingInterfaces())
assertParcelSane(emptyInfo, 3)
}
}
\ No newline at end of file
diff --git a/tests/common/java/android/net/apf/ApfCapabilitiesTest.java b/tests/common/java/android/net/apf/ApfCapabilitiesTest.java
index d50406f..88996d9 100644
--- a/tests/common/java/android/net/apf/ApfCapabilitiesTest.java
+++ b/tests/common/java/android/net/apf/ApfCapabilitiesTest.java
@@ -18,6 +18,7 @@
import static com.android.testutils.ParcelUtils.assertParcelSane;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
@@ -25,12 +26,17 @@
import static org.junit.Assert.assertTrue;
import android.content.Context;
+import android.os.Build;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -39,6 +45,9 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ApfCapabilitiesTest {
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
private Context mContext;
@Before
@@ -85,6 +94,17 @@
assertEquals(shouldDrop8023Frames, actual);
}
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testGetApfDrop8023Frames_S() {
+ // IpClient does not call getApfDrop8023Frames() since S, so any customization of the return
+ // value on S+ is a configuration error as it will not be used by IpClient.
+ assertTrue("android.R.bool.config_apfDrop802_3Frames has been modified to false, but "
+ + "starting from S its value is not used by IpClient. If the modification is "
+ + "intentional, use a runtime resource overlay for the NetworkStack package to "
+ + "overlay com.android.networkstack.R.bool.config_apfDrop802_3Frames instead.",
+ ApfCapabilities.getApfDrop8023Frames());
+ }
+
@Test
public void testGetApfEtherTypeBlackList() {
// Get com.android.internal.R.array.config_apfEthTypeBlackList. The test cannot directly
@@ -96,4 +116,17 @@
assertNotNull(actual);
assertTrue(Arrays.equals(blacklistedEtherTypeArray, actual));
}
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+ public void testGetApfEtherTypeBlackList_S() {
+ // IpClient does not call getApfEtherTypeBlackList() since S, so any customization of the
+ // return value on S+ is a configuration error as it will not be used by IpClient.
+ assertArrayEquals("android.R.array.config_apfEthTypeBlackList has been modified, but "
+ + "starting from S its value is not used by IpClient. If the modification "
+ + "is intentional, use a runtime resource overlay for the NetworkStack "
+ + "package to overlay "
+ + "com.android.networkstack.R.array.config_apfEthTypeDenyList instead.",
+ new int[] { 0x88a2, 0x88a4, 0x88b8, 0x88cd, 0x88e3 },
+ ApfCapabilities.getApfEtherTypeBlackList());
+ }
}
diff --git a/tests/unit/java/android/net/NetworkTemplateTest.kt b/tests/unit/java/android/net/NetworkTemplateTest.kt
index ab6b2f4..cb39a0c 100644
--- a/tests/unit/java/android/net/NetworkTemplateTest.kt
+++ b/tests/unit/java/android/net/NetworkTemplateTest.kt
@@ -40,7 +40,7 @@
import android.net.NetworkTemplate.SUBSCRIBER_ID_MATCH_RULE_EXACT
import android.net.NetworkTemplate.buildTemplateWifi
import android.net.NetworkTemplate.buildTemplateWifiWildcard
-import android.net.NetworkTemplate.buildTemplateCarrier
+import android.net.NetworkTemplate.buildTemplateCarrierMetered
import android.net.NetworkTemplate.buildTemplateMobileWithRatType
import android.telephony.TelephonyManager
import com.android.testutils.assertParcelSane
@@ -73,11 +73,12 @@
type: Int,
subscriberId: String? = null,
ssid: String? = null,
- oemManaged: Int = OEM_NONE
+ oemManaged: Int = OEM_NONE,
+ metered: Boolean = true
): NetworkStateSnapshot {
val lp = LinkProperties()
val caps = NetworkCapabilities().apply {
- setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, false)
+ setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, !metered)
setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, true)
setSSID(ssid)
setCapability(NetworkCapabilities.NET_CAPABILITY_OEM_PAID,
@@ -167,25 +168,38 @@
}
@Test
- fun testCarrierMatches() {
- val templateCarrierImsi1 = buildTemplateCarrier(TEST_IMSI1)
+ fun testCarrierMeteredMatches() {
+ val templateCarrierImsi1Metered = buildTemplateCarrierMetered(TEST_IMSI1)
- val identMobile1 = buildNetworkIdentity(mockContext, buildMobileNetworkState(TEST_IMSI1),
- false, TelephonyManager.NETWORK_TYPE_UMTS)
- val identMobile2 = buildNetworkIdentity(mockContext, buildMobileNetworkState(TEST_IMSI2),
- false, TelephonyManager.NETWORK_TYPE_UMTS)
- val identWifiSsid1 = buildNetworkIdentity(
- mockContext, buildWifiNetworkState(null, TEST_SSID1), true, 0)
- val identCarrierWifiImsi1 = buildNetworkIdentity(
- mockContext, buildWifiNetworkState(TEST_IMSI1, TEST_SSID1), true, 0)
- val identCarrierWifiImsi2 = buildNetworkIdentity(
- mockContext, buildWifiNetworkState(TEST_IMSI2, TEST_SSID1), true, 0)
+ val mobileImsi1 = buildMobileNetworkState(TEST_IMSI1)
+ val mobileImsi1Unmetered = buildNetworkState(TYPE_MOBILE, TEST_IMSI1, null /* ssid */,
+ OEM_NONE, false /* metered */)
+ val mobileImsi2 = buildMobileNetworkState(TEST_IMSI2)
+ val wifiSsid1 = buildWifiNetworkState(null /* subscriberId */, TEST_SSID1)
+ val wifiImsi1Ssid1 = buildWifiNetworkState(TEST_IMSI1, TEST_SSID1)
+ val wifiImsi1Ssid1Unmetered = buildNetworkState(TYPE_WIFI, TEST_IMSI1, TEST_SSID1,
+ OEM_NONE, false /* metered */)
- templateCarrierImsi1.assertMatches(identCarrierWifiImsi1)
- templateCarrierImsi1.assertDoesNotMatch(identCarrierWifiImsi2)
- templateCarrierImsi1.assertDoesNotMatch(identWifiSsid1)
- templateCarrierImsi1.assertMatches(identMobile1)
- templateCarrierImsi1.assertDoesNotMatch(identMobile2)
+ val identMobileImsi1Metered = buildNetworkIdentity(mockContext,
+ mobileImsi1, false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+ val identMobileImsi1Unmetered = buildNetworkIdentity(mockContext,
+ mobileImsi1Unmetered, false /* defaultNetwork */,
+ TelephonyManager.NETWORK_TYPE_UMTS)
+ val identMobileImsi2Metered = buildNetworkIdentity(mockContext,
+ mobileImsi2, false /* defaultNetwork */, TelephonyManager.NETWORK_TYPE_UMTS)
+ val identWifiSsid1Metered = buildNetworkIdentity(
+ mockContext, wifiSsid1, true /* defaultNetwork */, 0 /* subType */)
+ val identCarrierWifiImsi1Metered = buildNetworkIdentity(
+ mockContext, wifiImsi1Ssid1, true /* defaultNetwork */, 0 /* subType */)
+ val identCarrierWifiImsi1NonMetered = buildNetworkIdentity(mockContext,
+ wifiImsi1Ssid1Unmetered, true /* defaultNetwork */, 0 /* subType */)
+
+ templateCarrierImsi1Metered.assertMatches(identMobileImsi1Metered)
+ templateCarrierImsi1Metered.assertDoesNotMatch(identMobileImsi1Unmetered)
+ templateCarrierImsi1Metered.assertDoesNotMatch(identMobileImsi2Metered)
+ templateCarrierImsi1Metered.assertDoesNotMatch(identWifiSsid1Metered)
+ templateCarrierImsi1Metered.assertMatches(identCarrierWifiImsi1Metered)
+ templateCarrierImsi1Metered.assertDoesNotMatch(identCarrierWifiImsi1NonMetered)
}
@Test
diff --git a/tests/unit/java/android/net/QosSocketFilterTest.java b/tests/unit/java/android/net/QosSocketFilterTest.java
index ad58960..40f8f1b 100644
--- a/tests/unit/java/android/net/QosSocketFilterTest.java
+++ b/tests/unit/java/android/net/QosSocketFilterTest.java
@@ -35,7 +35,7 @@
public void testPortExactMatch() {
final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4");
final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4");
- assertTrue(QosSocketFilter.matchesLocalAddress(
+ assertTrue(QosSocketFilter.matchesAddress(
new InetSocketAddress(addressA, 10), addressB, 10, 10));
}
@@ -44,7 +44,7 @@
public void testPortLessThanStart() {
final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4");
final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4");
- assertFalse(QosSocketFilter.matchesLocalAddress(
+ assertFalse(QosSocketFilter.matchesAddress(
new InetSocketAddress(addressA, 8), addressB, 10, 10));
}
@@ -52,7 +52,7 @@
public void testPortGreaterThanEnd() {
final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4");
final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4");
- assertFalse(QosSocketFilter.matchesLocalAddress(
+ assertFalse(QosSocketFilter.matchesAddress(
new InetSocketAddress(addressA, 18), addressB, 10, 10));
}
@@ -60,7 +60,7 @@
public void testPortBetweenStartAndEnd() {
final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4");
final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.4");
- assertTrue(QosSocketFilter.matchesLocalAddress(
+ assertTrue(QosSocketFilter.matchesAddress(
new InetSocketAddress(addressA, 10), addressB, 8, 18));
}
@@ -68,7 +68,7 @@
public void testAddressesDontMatch() {
final InetAddress addressA = InetAddresses.parseNumericAddress("1.2.3.4");
final InetAddress addressB = InetAddresses.parseNumericAddress("1.2.3.5");
- assertFalse(QosSocketFilter.matchesLocalAddress(
+ assertFalse(QosSocketFilter.matchesAddress(
new InetSocketAddress(addressA, 10), addressB, 10, 10));
}
}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index ab50798..1b4f836 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -19,6 +19,7 @@
import static android.Manifest.permission.CHANGE_NETWORK_STATE;
import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
import static android.Manifest.permission.DUMP;
+import static android.Manifest.permission.LOCAL_MAC_ADDRESS;
import static android.Manifest.permission.NETWORK_FACTORY;
import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
@@ -126,6 +127,7 @@
import static com.android.testutils.MiscAsserts.assertEmpty;
import static com.android.testutils.MiscAsserts.assertLength;
import static com.android.testutils.MiscAsserts.assertRunsInAtMost;
+import static com.android.testutils.MiscAsserts.assertSameElements;
import static com.android.testutils.MiscAsserts.assertThrows;
import static org.junit.Assert.assertEquals;
@@ -5805,20 +5807,8 @@
mCm.unregisterNetworkCallback(networkCallback);
}
- private <T> void assertSameElementsNoDuplicates(T[] expected, T[] actual) {
- // Easier to implement than a proper "assertSameElements" method that also correctly deals
- // with duplicates.
- final String msg = Arrays.toString(expected) + " != " + Arrays.toString(actual);
- assertEquals(msg, expected.length, actual.length);
- Set expectedSet = new ArraySet<>(Arrays.asList(expected));
- assertEquals("expected contains duplicates", expectedSet.size(), expected.length);
- // actual cannot have duplicates because it's the same length and has the same elements.
- Set actualSet = new ArraySet<>(Arrays.asList(actual));
- assertEquals(expectedSet, actualSet);
- }
-
- private void expectNetworkStatus(Network[] networks, String defaultIface,
- Integer vpnUid, String vpnIfname, String[] underlyingIfaces) throws Exception {
+ private void expectNotifyNetworkStatus(List<Network> networks, String defaultIface,
+ Integer vpnUid, String vpnIfname, List<String> underlyingIfaces) throws Exception {
ArgumentCaptor<List<Network>> networksCaptor = ArgumentCaptor.forClass(List.class);
ArgumentCaptor<List<UnderlyingNetworkInfo>> vpnInfosCaptor =
ArgumentCaptor.forClass(List.class);
@@ -5826,26 +5816,24 @@
verify(mStatsManager, atLeastOnce()).notifyNetworkStatus(networksCaptor.capture(),
any(List.class), eq(defaultIface), vpnInfosCaptor.capture());
- assertSameElementsNoDuplicates(networksCaptor.getValue().toArray(), networks);
+ assertSameElements(networksCaptor.getValue(), networks);
- UnderlyingNetworkInfo[] infos =
- vpnInfosCaptor.getValue().toArray(new UnderlyingNetworkInfo[0]);
+ List<UnderlyingNetworkInfo> infos = vpnInfosCaptor.getValue();
if (vpnUid != null) {
- assertEquals("Should have exactly one VPN:", 1, infos.length);
- UnderlyingNetworkInfo info = infos[0];
+ assertEquals("Should have exactly one VPN:", 1, infos.size());
+ UnderlyingNetworkInfo info = infos.get(0);
assertEquals("Unexpected VPN owner:", (int) vpnUid, info.getOwnerUid());
- assertEquals("Unexpected VPN interface:", vpnIfname, info.getIface());
- assertSameElementsNoDuplicates(underlyingIfaces,
- info.getUnderlyingIfaces().toArray(new String[0]));
+ assertEquals("Unexpected VPN interface:", vpnIfname, info.getInterface());
+ assertSameElements(underlyingIfaces, info.getUnderlyingInterfaces());
} else {
- assertEquals(0, infos.length);
+ assertEquals(0, infos.size());
return;
}
}
- private void expectNetworkStatus(
- Network[] networks, String defaultIface) throws Exception {
- expectNetworkStatus(networks, defaultIface, null, null, new String[0]);
+ private void expectNotifyNetworkStatus(
+ List<Network> networks, String defaultIface) throws Exception {
+ expectNotifyNetworkStatus(networks, defaultIface, null, null, List.of());
}
@Test
@@ -5853,8 +5841,8 @@
mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
- final Network[] onlyCell = new Network[] {mCellNetworkAgent.getNetwork()};
- final Network[] onlyWifi = new Network[] {mWiFiNetworkAgent.getNetwork()};
+ final List<Network> onlyCell = List.of(mCellNetworkAgent.getNetwork());
+ final List<Network> onlyWifi = List.of(mWiFiNetworkAgent.getNetwork());
LinkProperties cellLp = new LinkProperties();
cellLp.setInterfaceName(MOBILE_IFNAME);
@@ -5865,7 +5853,7 @@
mCellNetworkAgent.connect(false);
mCellNetworkAgent.sendLinkProperties(cellLp);
waitForIdle();
- expectNetworkStatus(onlyCell, MOBILE_IFNAME);
+ expectNotifyNetworkStatus(onlyCell, MOBILE_IFNAME);
reset(mStatsManager);
// Default network switch should update ifaces.
@@ -5873,37 +5861,37 @@
mWiFiNetworkAgent.sendLinkProperties(wifiLp);
waitForIdle();
assertEquals(wifiLp, mService.getActiveLinkProperties());
- expectNetworkStatus(onlyWifi, WIFI_IFNAME);
+ expectNotifyNetworkStatus(onlyWifi, WIFI_IFNAME);
reset(mStatsManager);
// Disconnect should update ifaces.
mWiFiNetworkAgent.disconnect();
waitForIdle();
- expectNetworkStatus(onlyCell, MOBILE_IFNAME);
+ expectNotifyNetworkStatus(onlyCell, MOBILE_IFNAME);
reset(mStatsManager);
// Metered change should update ifaces
mCellNetworkAgent.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
waitForIdle();
- expectNetworkStatus(onlyCell, MOBILE_IFNAME);
+ expectNotifyNetworkStatus(onlyCell, MOBILE_IFNAME);
reset(mStatsManager);
mCellNetworkAgent.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
waitForIdle();
- expectNetworkStatus(onlyCell, MOBILE_IFNAME);
+ expectNotifyNetworkStatus(onlyCell, MOBILE_IFNAME);
reset(mStatsManager);
// Temp metered change shouldn't update ifaces
mCellNetworkAgent.addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED);
waitForIdle();
- verify(mStatsManager, never()).notifyNetworkStatus(eq(Arrays.asList(onlyCell)),
+ verify(mStatsManager, never()).notifyNetworkStatus(eq(onlyCell),
any(List.class), eq(MOBILE_IFNAME), any(List.class));
reset(mStatsManager);
// Roaming change should update ifaces
mCellNetworkAgent.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
waitForIdle();
- expectNetworkStatus(onlyCell, MOBILE_IFNAME);
+ expectNotifyNetworkStatus(onlyCell, MOBILE_IFNAME);
reset(mStatsManager);
// Test VPNs.
@@ -5913,29 +5901,29 @@
mMockVpn.establishForMyUid(lp);
assertUidRangesUpdatedForMyUid(true);
- final Network[] cellAndVpn = new Network[] {
- mCellNetworkAgent.getNetwork(), mMockVpn.getNetwork()};
+ final List<Network> cellAndVpn =
+ List.of(mCellNetworkAgent.getNetwork(), mMockVpn.getNetwork());
// A VPN with default (null) underlying networks sets the underlying network's interfaces...
- expectNetworkStatus(cellAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME,
- new String[]{MOBILE_IFNAME});
+ expectNotifyNetworkStatus(cellAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME,
+ List.of(MOBILE_IFNAME));
// ...and updates them as the default network switches.
mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
mWiFiNetworkAgent.connect(false);
mWiFiNetworkAgent.sendLinkProperties(wifiLp);
final Network[] onlyNull = new Network[]{null};
- final Network[] wifiAndVpn = new Network[] {
- mWiFiNetworkAgent.getNetwork(), mMockVpn.getNetwork()};
- final Network[] cellAndWifi = new Network[] {
- mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork()};
- final Network[] cellNullAndWifi = new Network[] {
- mCellNetworkAgent.getNetwork(), null, mWiFiNetworkAgent.getNetwork()};
+ final List<Network> wifiAndVpn =
+ List.of(mWiFiNetworkAgent.getNetwork(), mMockVpn.getNetwork());
+ final List<Network> cellAndWifi =
+ List.of(mCellNetworkAgent.getNetwork(), mWiFiNetworkAgent.getNetwork());
+ final Network[] cellNullAndWifi =
+ new Network[]{mCellNetworkAgent.getNetwork(), null, mWiFiNetworkAgent.getNetwork()};
waitForIdle();
assertEquals(wifiLp, mService.getActiveLinkProperties());
- expectNetworkStatus(wifiAndVpn, WIFI_IFNAME, Process.myUid(), VPN_IFNAME,
- new String[]{WIFI_IFNAME});
+ expectNotifyNetworkStatus(wifiAndVpn, WIFI_IFNAME, Process.myUid(), VPN_IFNAME,
+ List.of(WIFI_IFNAME));
reset(mStatsManager);
// A VPN that sets its underlying networks passes the underlying interfaces, and influences
@@ -5944,23 +5932,23 @@
// MOBILE_IFNAME even though the default network is wifi.
// TODO: fix this to pass in the actual default network interface. Whether or not the VPN
// applies to the system server UID should not have any bearing on network stats.
- mMockVpn.setUnderlyingNetworks(onlyCell);
+ mMockVpn.setUnderlyingNetworks(onlyCell.toArray(new Network[0]));
waitForIdle();
- expectNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME,
- new String[]{MOBILE_IFNAME});
+ expectNotifyNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME,
+ List.of(MOBILE_IFNAME));
reset(mStatsManager);
- mMockVpn.setUnderlyingNetworks(cellAndWifi);
+ mMockVpn.setUnderlyingNetworks(cellAndWifi.toArray(new Network[0]));
waitForIdle();
- expectNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME,
- new String[]{MOBILE_IFNAME, WIFI_IFNAME});
+ expectNotifyNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME,
+ List.of(MOBILE_IFNAME, WIFI_IFNAME));
reset(mStatsManager);
// Null underlying networks are ignored.
mMockVpn.setUnderlyingNetworks(cellNullAndWifi);
waitForIdle();
- expectNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME,
- new String[]{MOBILE_IFNAME, WIFI_IFNAME});
+ expectNotifyNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME,
+ List.of(MOBILE_IFNAME, WIFI_IFNAME));
reset(mStatsManager);
// If an underlying network disconnects, that interface should no longer be underlying.
@@ -5973,15 +5961,15 @@
mCellNetworkAgent.disconnect();
waitForIdle();
assertNull(mService.getLinkProperties(mCellNetworkAgent.getNetwork()));
- expectNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME,
- new String[]{MOBILE_IFNAME, WIFI_IFNAME});
+ expectNotifyNetworkStatus(wifiAndVpn, MOBILE_IFNAME, Process.myUid(), VPN_IFNAME,
+ List.of(MOBILE_IFNAME, WIFI_IFNAME));
// Confirm that we never tell NetworkStatsService that cell is no longer the underlying
// network for the VPN...
verify(mStatsManager, never()).notifyNetworkStatus(any(List.class),
any(List.class), any() /* anyString() doesn't match null */,
- argThat(infos -> infos.get(0).getUnderlyingIfaces().size() == 1
- && WIFI_IFNAME.equals(infos.get(0).getUnderlyingIfaces().get(0))));
+ argThat(infos -> infos.get(0).getUnderlyingInterfaces().size() == 1
+ && WIFI_IFNAME.equals(infos.get(0).getUnderlyingInterfaces().get(0))));
verifyNoMoreInteractions(mStatsManager);
reset(mStatsManager);
@@ -5994,8 +5982,8 @@
waitForIdle();
verify(mStatsManager).notifyNetworkStatus(any(List.class),
any(List.class), any() /* anyString() doesn't match null */,
- argThat(vpnInfos -> vpnInfos.get(0).getUnderlyingIfaces().size() == 1
- && WIFI_IFNAME.equals(vpnInfos.get(0).getUnderlyingIfaces().get(0))));
+ argThat(vpnInfos -> vpnInfos.get(0).getUnderlyingInterfaces().size() == 1
+ && WIFI_IFNAME.equals(vpnInfos.get(0).getUnderlyingInterfaces().get(0))));
mEthernetNetworkAgent.disconnect();
waitForIdle();
reset(mStatsManager);
@@ -6008,26 +5996,26 @@
// Also, for the same reason as above, the active interface passed in is null.
mMockVpn.setUnderlyingNetworks(new Network[0]);
waitForIdle();
- expectNetworkStatus(wifiAndVpn, null);
+ expectNotifyNetworkStatus(wifiAndVpn, null);
reset(mStatsManager);
// Specifying only a null underlying network is the same as no networks.
mMockVpn.setUnderlyingNetworks(onlyNull);
waitForIdle();
- expectNetworkStatus(wifiAndVpn, null);
+ expectNotifyNetworkStatus(wifiAndVpn, null);
reset(mStatsManager);
// Specifying networks that are all disconnected is the same as specifying no networks.
- mMockVpn.setUnderlyingNetworks(onlyCell);
+ mMockVpn.setUnderlyingNetworks(onlyCell.toArray(new Network[0]));
waitForIdle();
- expectNetworkStatus(wifiAndVpn, null);
+ expectNotifyNetworkStatus(wifiAndVpn, null);
reset(mStatsManager);
// Passing in null again means follow the default network again.
mMockVpn.setUnderlyingNetworks(null);
waitForIdle();
- expectNetworkStatus(wifiAndVpn, WIFI_IFNAME, Process.myUid(), VPN_IFNAME,
- new String[]{WIFI_IFNAME});
+ expectNotifyNetworkStatus(wifiAndVpn, WIFI_IFNAME, Process.myUid(), VPN_IFNAME,
+ List.of(WIFI_IFNAME));
reset(mStatsManager);
}
@@ -9407,9 +9395,9 @@
@Override
public TransportInfo makeCopy(@NetworkCapabilities.RedactionType long redactions) {
return new TestTransportInfo(
- (redactions & REDACT_FOR_ACCESS_FINE_LOCATION) != 0,
- (redactions & REDACT_FOR_LOCAL_MAC_ADDRESS) != 0,
- (redactions & REDACT_FOR_NETWORK_SETTINGS) != 0
+ locationRedacted | (redactions & REDACT_FOR_ACCESS_FINE_LOCATION) != 0,
+ localMacAddressRedacted | (redactions & REDACT_FOR_LOCAL_MAC_ADDRESS) != 0,
+ settingsRedacted | (redactions & REDACT_FOR_NETWORK_SETTINGS) != 0
);
}
@@ -9432,8 +9420,26 @@
public int hashCode() {
return Objects.hash(locationRedacted, localMacAddressRedacted, settingsRedacted);
}
+
+ @Override
+ public String toString() {
+ return String.format(
+ "TestTransportInfo{locationRedacted=%s macRedacted=%s settingsRedacted=%s}",
+ locationRedacted, localMacAddressRedacted, settingsRedacted);
+ }
}
+ private TestTransportInfo getTestTransportInfo(NetworkCapabilities nc) {
+ return (TestTransportInfo) nc.getTransportInfo();
+ }
+
+ private TestTransportInfo getTestTransportInfo(TestNetworkAgentWrapper n) {
+ final NetworkCapabilities nc = mCm.getNetworkCapabilities(n.getNetwork());
+ assertNotNull(nc);
+ return getTestTransportInfo(nc);
+ }
+
+
private void verifyNetworkCallbackLocationDataInclusionUsingTransportInfoAndOwnerUidInNetCaps(
@NonNull TestNetworkCallback wifiNetworkCallback, int actualOwnerUid,
@NonNull TransportInfo actualTransportInfo, int expectedOwnerUid,
@@ -9462,7 +9468,6 @@
wifiNetworkCallback.expectCapabilitiesThat(mWiFiNetworkAgent,
nc -> Objects.equals(expectedOwnerUid, nc.getOwnerUid())
&& Objects.equals(expectedTransportInfo, nc.getTransportInfo()));
-
}
@Test
@@ -9483,6 +9488,40 @@
wifiNetworkCallack, ownerUid, transportInfo, INVALID_UID, sanitizedTransportInfo);
}
+ @Test
+ public void testTransportInfoRedactionInSynchronousCalls() throws Exception {
+ final NetworkCapabilities ncTemplate = new NetworkCapabilities()
+ .addTransportType(TRANSPORT_WIFI)
+ .setTransportInfo(new TestTransportInfo());
+
+ mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, new LinkProperties(),
+ ncTemplate);
+ mWiFiNetworkAgent.connect(true /* validated; waits for callback */);
+
+ // NETWORK_SETTINGS redaction is controlled by the NETWORK_SETTINGS permission
+ assertTrue(getTestTransportInfo(mWiFiNetworkAgent).settingsRedacted);
+ withPermission(NETWORK_SETTINGS, () -> {
+ assertFalse(getTestTransportInfo(mWiFiNetworkAgent).settingsRedacted);
+ });
+ assertTrue(getTestTransportInfo(mWiFiNetworkAgent).settingsRedacted);
+
+ // LOCAL_MAC_ADDRESS redaction is controlled by the LOCAL_MAC_ADDRESS permission
+ assertTrue(getTestTransportInfo(mWiFiNetworkAgent).localMacAddressRedacted);
+ withPermission(LOCAL_MAC_ADDRESS, () -> {
+ assertFalse(getTestTransportInfo(mWiFiNetworkAgent).localMacAddressRedacted);
+ });
+ assertTrue(getTestTransportInfo(mWiFiNetworkAgent).localMacAddressRedacted);
+
+ // Synchronous getNetworkCapabilities calls never return unredacted location-sensitive
+ // information.
+ assertTrue(getTestTransportInfo(mWiFiNetworkAgent).locationRedacted);
+ setupLocationPermissions(Build.VERSION_CODES.S, true, AppOpsManager.OPSTR_FINE_LOCATION,
+ Manifest.permission.ACCESS_FINE_LOCATION);
+ assertTrue(getTestTransportInfo(mWiFiNetworkAgent).locationRedacted);
+ denyAllLocationPrivilegedPermissions();
+ assertTrue(getTestTransportInfo(mWiFiNetworkAgent).locationRedacted);
+ }
+
private void setupConnectionOwnerUid(int vpnOwnerUid, @VpnManager.VpnType int vpnType)
throws Exception {
final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
@@ -9704,28 +9743,32 @@
@Test
public void testCheckConnectivityDiagnosticsPermissionsWrongUidPackageName() throws Exception {
- final NetworkAgentInfo naiWithoutUid = fakeMobileNai(new NetworkCapabilities());
+ final int wrongUid = Process.myUid() + 1;
+
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ nc.setAdministratorUids(new int[] {wrongUid});
+ final NetworkAgentInfo naiWithUid = fakeMobileNai(nc);
mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED);
assertFalse(
"Mismatched uid/package name should not pass the location permission check",
mService.checkConnectivityDiagnosticsPermissions(
- Process.myPid() + 1, Process.myUid() + 1, naiWithoutUid,
- mContext.getOpPackageName()));
+ Process.myPid() + 1, wrongUid, naiWithUid, mContext.getOpPackageName()));
}
@Test
public void testCheckConnectivityDiagnosticsPermissionsNoLocationPermission() throws Exception {
- final NetworkAgentInfo naiWithoutUid = fakeMobileNai(new NetworkCapabilities());
+ final NetworkCapabilities nc = new NetworkCapabilities();
+ nc.setAdministratorUids(new int[] {Process.myUid()});
+ final NetworkAgentInfo naiWithUid = fakeMobileNai(nc);
mServiceContext.setPermission(android.Manifest.permission.NETWORK_STACK, PERMISSION_DENIED);
assertFalse(
"ACCESS_FINE_LOCATION permission necessary for Connectivity Diagnostics",
mService.checkConnectivityDiagnosticsPermissions(
- Process.myPid(), Process.myUid(), naiWithoutUid,
- mContext.getOpPackageName()));
+ Process.myPid(), Process.myUid(), naiWithUid, mContext.getOpPackageName()));
}
@Test
@@ -9840,12 +9883,27 @@
// Connect the cell agent verify that it notifies TestNetworkCallback that it is available
final TestNetworkCallback callback = new TestNetworkCallback();
mCm.registerDefaultNetworkCallback(callback);
- mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+
+ final NetworkCapabilities ncTemplate = new NetworkCapabilities()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .setTransportInfo(new TestTransportInfo());
+ mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, new LinkProperties(),
+ ncTemplate);
mCellNetworkAgent.connect(true);
callback.expectAvailableThenValidatedCallbacks(mCellNetworkAgent);
callback.assertNoCallback();
}
+ private boolean areConnDiagCapsRedacted(NetworkCapabilities nc) {
+ TestTransportInfo ti = (TestTransportInfo) nc.getTransportInfo();
+ return nc.getUids() == null
+ && nc.getAdministratorUids().length == 0
+ && nc.getOwnerUid() == Process.INVALID_UID
+ && getTestTransportInfo(nc).locationRedacted
+ && getTestTransportInfo(nc).localMacAddressRedacted
+ && getTestTransportInfo(nc).settingsRedacted;
+ }
+
@Test
public void testConnectivityDiagnosticsCallbackOnConnectivityReportAvailable()
throws Exception {
@@ -9856,12 +9914,7 @@
// Verify onConnectivityReport fired
verify(mConnectivityDiagnosticsCallback).onConnectivityReportAvailable(
- argThat(report -> {
- final NetworkCapabilities nc = report.getNetworkCapabilities();
- return nc.getUids() == null
- && nc.getAdministratorUids().length == 0
- && nc.getOwnerUid() == Process.INVALID_UID;
- }));
+ argThat(report -> areConnDiagCapsRedacted(report.getNetworkCapabilities())));
}
@Test
@@ -9877,12 +9930,7 @@
// Verify onDataStallSuspected fired
verify(mConnectivityDiagnosticsCallback).onDataStallSuspected(
- argThat(report -> {
- final NetworkCapabilities nc = report.getNetworkCapabilities();
- return nc.getUids() == null
- && nc.getAdministratorUids().length == 0
- && nc.getOwnerUid() == Process.INVALID_UID;
- }));
+ argThat(report -> areConnDiagCapsRedacted(report.getNetworkCapabilities())));
}
@Test
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index 02a5808..c75618f 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -30,6 +30,8 @@
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_REQUIRED;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.content.pm.PackageManager.MATCH_ANY_USER;
+import static android.net.ConnectivitySettingsManager.APPS_ALLOWED_ON_RESTRICTED_NETWORKS;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
import static android.os.Process.SYSTEM_UID;
import static com.android.server.connectivity.PermissionMonitor.NETWORK;
@@ -43,8 +45,10 @@
import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
@@ -61,6 +65,7 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.database.ContentObserver;
import android.net.INetd;
import android.net.UidRange;
import android.net.Uri;
@@ -68,6 +73,7 @@
import android.os.SystemConfigManager;
import android.os.UserHandle;
import android.os.UserManager;
+import android.util.ArraySet;
import android.util.SparseIntArray;
import androidx.test.InstrumentationRegistry;
@@ -136,6 +142,7 @@
final Context asUserCtx = mock(Context.class, AdditionalAnswers.delegatesTo(mContext));
doReturn(UserHandle.ALL).when(asUserCtx).getUser();
when(mContext.createContextAsUser(eq(UserHandle.ALL), anyInt())).thenReturn(asUserCtx);
+ when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>());
mPermissionMonitor = spy(new PermissionMonitor(mContext, mNetdService, mDeps));
@@ -145,8 +152,15 @@
private boolean hasRestrictedNetworkPermission(String partition, int targetSdkVersion, int uid,
String... permissions) {
+ return hasRestrictedNetworkPermission(
+ partition, targetSdkVersion, "" /* packageName */, uid, permissions);
+ }
+
+ private boolean hasRestrictedNetworkPermission(String partition, int targetSdkVersion,
+ String packageName, int uid, String... permissions) {
final PackageInfo packageInfo =
packageInfoWithPermissions(REQUESTED_PERMISSION_GRANTED, permissions, partition);
+ packageInfo.packageName = packageName;
packageInfo.applicationInfo.targetSdkVersion = targetSdkVersion;
packageInfo.applicationInfo.uid = uid;
return mPermissionMonitor.hasRestrictedNetworkPermission(packageInfo);
@@ -280,6 +294,8 @@
PARTITION_SYSTEM, VERSION_P, MOCK_UID1, CONNECTIVITY_USE_RESTRICTED_NETWORKS));
assertFalse(hasRestrictedNetworkPermission(
PARTITION_SYSTEM, VERSION_P, MOCK_UID1, CHANGE_WIFI_STATE));
+ assertTrue(hasRestrictedNetworkPermission(
+ PARTITION_SYSTEM, VERSION_P, MOCK_UID1, PERMISSION_MAINLINE_NETWORK_STACK));
assertFalse(hasRestrictedNetworkPermission(PARTITION_SYSTEM, VERSION_Q, MOCK_UID1));
assertFalse(hasRestrictedNetworkPermission(
@@ -324,6 +340,90 @@
PARTITION_VENDOR, VERSION_Q, MOCK_UID1, CHANGE_NETWORK_STATE));
}
+ @Test
+ public void testHasRestrictedNetworkPermissionAppAllowedOnRestrictedNetworks() {
+ mPermissionMonitor.updateAppsAllowedOnRestrictedNetworks(
+ new ArraySet<>(new String[] { MOCK_PACKAGE1 }));
+ assertTrue(hasRestrictedNetworkPermission(
+ PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE1, MOCK_UID1));
+ assertTrue(hasRestrictedNetworkPermission(
+ PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE1, MOCK_UID1, CHANGE_NETWORK_STATE));
+ assertTrue(hasRestrictedNetworkPermission(
+ PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE1, MOCK_UID1, CONNECTIVITY_INTERNAL));
+
+ assertFalse(hasRestrictedNetworkPermission(
+ PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE2, MOCK_UID1));
+ assertFalse(hasRestrictedNetworkPermission(
+ PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE2, MOCK_UID1, CHANGE_NETWORK_STATE));
+ assertFalse(hasRestrictedNetworkPermission(
+ PARTITION_VENDOR, VERSION_Q, MOCK_PACKAGE2, MOCK_UID1, CONNECTIVITY_INTERNAL));
+
+ }
+
+ private boolean wouldBeCarryoverPackage(String partition, int targetSdkVersion, int uid) {
+ final PackageInfo packageInfo = packageInfoWithPermissions(
+ REQUESTED_PERMISSION_GRANTED, new String[] {}, partition);
+ packageInfo.applicationInfo.targetSdkVersion = targetSdkVersion;
+ packageInfo.applicationInfo.uid = uid;
+ return mPermissionMonitor.isCarryoverPackage(packageInfo.applicationInfo);
+ }
+
+ @Test
+ public void testIsCarryoverPackage() {
+ doReturn(VERSION_P).when(mDeps).getDeviceFirstSdkInt();
+ assertTrue(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_P, SYSTEM_UID));
+ assertTrue(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_P, SYSTEM_UID));
+ assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_P, MOCK_UID1));
+ assertTrue(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_P, MOCK_UID1));
+ assertTrue(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_Q, SYSTEM_UID));
+ assertTrue(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_Q, SYSTEM_UID));
+ assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_Q, MOCK_UID1));
+ assertFalse(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_Q, MOCK_UID1));
+
+ doReturn(VERSION_Q).when(mDeps).getDeviceFirstSdkInt();
+ assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_P, SYSTEM_UID));
+ assertTrue(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_P, SYSTEM_UID));
+ assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_P, MOCK_UID1));
+ assertTrue(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_P, MOCK_UID1));
+ assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_Q, SYSTEM_UID));
+ assertFalse(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_Q, SYSTEM_UID));
+ assertFalse(wouldBeCarryoverPackage(PARTITION_SYSTEM, VERSION_Q, MOCK_UID1));
+ assertFalse(wouldBeCarryoverPackage(PARTITION_VENDOR, VERSION_Q, MOCK_UID1));
+
+ assertFalse(wouldBeCarryoverPackage(PARTITION_OEM, VERSION_Q, SYSTEM_UID));
+ assertFalse(wouldBeCarryoverPackage(PARTITION_PRODUCT, VERSION_Q, SYSTEM_UID));
+ assertFalse(wouldBeCarryoverPackage(PARTITION_OEM, VERSION_Q, MOCK_UID1));
+ assertFalse(wouldBeCarryoverPackage(PARTITION_PRODUCT, VERSION_Q, MOCK_UID1));
+ }
+
+ private boolean wouldBeAppAllowedOnRestrictedNetworks(String packageName) {
+ final PackageInfo packageInfo = new PackageInfo();
+ packageInfo.packageName = packageName;
+ return mPermissionMonitor.isAppAllowedOnRestrictedNetworks(packageInfo);
+ }
+
+ @Test
+ public void testIsAppAllowedOnRestrictedNetworks() {
+ mPermissionMonitor.updateAppsAllowedOnRestrictedNetworks(new ArraySet<>());
+ assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE1));
+ assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE2));
+
+ mPermissionMonitor.updateAppsAllowedOnRestrictedNetworks(
+ new ArraySet<>(new String[] { MOCK_PACKAGE1 }));
+ assertTrue(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE1));
+ assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE2));
+
+ mPermissionMonitor.updateAppsAllowedOnRestrictedNetworks(
+ new ArraySet<>(new String[] { MOCK_PACKAGE2 }));
+ assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE1));
+ assertTrue(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE2));
+
+ mPermissionMonitor.updateAppsAllowedOnRestrictedNetworks(
+ new ArraySet<>(new String[] { "com.android.test" }));
+ assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE1));
+ assertFalse(wouldBeAppAllowedOnRestrictedNetworks(MOCK_PACKAGE2));
+ }
+
private void assertBackgroundPermission(boolean hasPermission, String name, int uid,
String... permissions) throws Exception {
when(mPackageManager.getPackageInfo(eq(name), anyInt()))
@@ -800,4 +900,102 @@
mNetdServiceMonitor.expectPermission(INetd.PERMISSION_UNINSTALLED, new int[] { MOCK_UID1 });
}
-}
+ @Test
+ public void testAppsAllowedOnRestrictedNetworksChanged() throws Exception {
+ final NetdMonitor mNetdMonitor = new NetdMonitor(mNetdService);
+ final ArgumentCaptor<ContentObserver> captor =
+ ArgumentCaptor.forClass(ContentObserver.class);
+ verify(mDeps, times(1)).registerContentObserver(any(),
+ argThat(uri -> uri.getEncodedPath().contains(APPS_ALLOWED_ON_RESTRICTED_NETWORKS)),
+ anyBoolean(), captor.capture());
+ final ContentObserver contentObserver = captor.getValue();
+
+ mPermissionMonitor.onUserAdded(MOCK_USER1);
+ // Prepare PackageInfo for MOCK_PACKAGE1
+ final PackageInfo packageInfo = buildPackageInfo(
+ false /* hasSystemPermission */, MOCK_UID1, MOCK_USER1);
+ packageInfo.packageName = MOCK_PACKAGE1;
+ when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE1), anyInt())).thenReturn(packageInfo);
+ when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{MOCK_PACKAGE1});
+ // Prepare PackageInfo for MOCK_PACKAGE2
+ final PackageInfo packageInfo2 = buildPackageInfo(
+ false /* hasSystemPermission */, MOCK_UID2, MOCK_USER1);
+ packageInfo2.packageName = MOCK_PACKAGE2;
+ when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE2), anyInt())).thenReturn(packageInfo2);
+ when(mPackageManager.getPackagesForUid(MOCK_UID2)).thenReturn(new String[]{MOCK_PACKAGE2});
+
+ // MOCK_PACKAGE1 is listed in setting that allow to use restricted networks, MOCK_UID1
+ // should have SYSTEM permission.
+ when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn(
+ new ArraySet<>(new String[] { MOCK_PACKAGE1 }));
+ contentObserver.onChange(true /* selfChange */);
+ mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+ mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID2});
+
+ // MOCK_PACKAGE2 is listed in setting that allow to use restricted networks, MOCK_UID2
+ // should have SYSTEM permission but MOCK_UID1 should revoke permission.
+ when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn(
+ new ArraySet<>(new String[] { MOCK_PACKAGE2 }));
+ contentObserver.onChange(true /* selfChange */);
+ mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID2});
+ mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+
+ // No app lists in setting, should revoke permission from all uids.
+ when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>());
+ contentObserver.onChange(true /* selfChange */);
+ mNetdMonitor.expectNoPermission(
+ new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1, MOCK_UID2});
+ }
+
+ @Test
+ public void testAppsAllowedOnRestrictedNetworksChangedWithSharedUid() throws Exception {
+ final NetdMonitor mNetdMonitor = new NetdMonitor(mNetdService);
+ final ArgumentCaptor<ContentObserver> captor =
+ ArgumentCaptor.forClass(ContentObserver.class);
+ verify(mDeps, times(1)).registerContentObserver(any(),
+ argThat(uri -> uri.getEncodedPath().contains(APPS_ALLOWED_ON_RESTRICTED_NETWORKS)),
+ anyBoolean(), captor.capture());
+ final ContentObserver contentObserver = captor.getValue();
+
+ mPermissionMonitor.onUserAdded(MOCK_USER1);
+ // Prepare PackageInfo for MOCK_PACKAGE1 and MOCK_PACKAGE2 with shared uid MOCK_UID1.
+ final PackageInfo packageInfo = systemPackageInfoWithPermissions(CHANGE_NETWORK_STATE);
+ packageInfo.applicationInfo.uid = MOCK_USER1.getUid(MOCK_UID1);
+ packageInfo.packageName = MOCK_PACKAGE1;
+ final PackageInfo packageInfo2 = buildPackageInfo(
+ false /* hasSystemPermission */, MOCK_UID1, MOCK_USER1);
+ packageInfo2.packageName = MOCK_PACKAGE2;
+ when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE1), anyInt())).thenReturn(packageInfo);
+ when(mPackageManager.getPackageInfo(eq(MOCK_PACKAGE2), anyInt())).thenReturn(packageInfo2);
+ when(mPackageManager.getPackagesForUid(MOCK_UID1))
+ .thenReturn(new String[]{MOCK_PACKAGE1, MOCK_PACKAGE2});
+
+ // MOCK_PACKAGE1 have CHANGE_NETWORK_STATE, MOCK_UID1 should have NETWORK permission.
+ addPackageForUsers(new UserHandle[]{MOCK_USER1}, MOCK_PACKAGE1, MOCK_UID1);
+ mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+
+ // MOCK_PACKAGE2 is listed in setting that allow to use restricted networks, MOCK_UID1
+ // should upgrade to SYSTEM permission.
+ when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn(
+ new ArraySet<>(new String[] { MOCK_PACKAGE2 }));
+ contentObserver.onChange(true /* selfChange */);
+ mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+
+ // MOCK_PACKAGE1 is listed in setting that allow to use restricted networks, MOCK_UID1
+ // should still have SYSTEM permission.
+ when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn(
+ new ArraySet<>(new String[] { MOCK_PACKAGE1 }));
+ contentObserver.onChange(true /* selfChange */);
+ mNetdMonitor.expectPermission(SYSTEM, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+
+ // No app lists in setting, MOCK_UID1 should downgrade to NETWORK permission.
+ when(mDeps.getAppsAllowedOnRestrictedNetworks(any())).thenReturn(new ArraySet<>());
+ contentObserver.onChange(true /* selfChange */);
+ mNetdMonitor.expectPermission(NETWORK, new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+
+ // MOCK_PACKAGE1 removed, should revoke permission from MOCK_UID1.
+ when(mPackageManager.getPackagesForUid(MOCK_UID1)).thenReturn(new String[]{MOCK_PACKAGE2});
+ removePackageForUsers(new UserHandle[]{MOCK_USER1}, MOCK_PACKAGE1, MOCK_UID1);
+ mNetdMonitor.expectNoPermission(new UserHandle[]{MOCK_USER1}, new int[]{MOCK_UID1});
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
index f3ae9b0..93599f3 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsFactoryTest.java
@@ -43,6 +43,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.frameworks.tests.net.R;
+import com.android.internal.util.test.FsUtil;
import libcore.io.IoUtils;
import libcore.io.Streams;
@@ -71,7 +72,7 @@
public void setUp() throws Exception {
mTestProc = new File(InstrumentationRegistry.getContext().getFilesDir(), "proc");
if (mTestProc.exists()) {
- IoUtils.deleteContents(mTestProc);
+ FsUtil.deleteContents(mTestProc);
}
// The libandroid_servers which have the native method is not available to
@@ -87,7 +88,7 @@
mFactory = null;
if (mTestProc.exists()) {
- IoUtils.deleteContents(mTestProc);
+ FsUtil.deleteContents(mTestProc);
}
}
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index fd374bc..0ba5f7d 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -112,13 +112,12 @@
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.test.BroadcastInterceptingContext;
+import com.android.internal.util.test.FsUtil;
import com.android.server.net.NetworkStatsService.NetworkStatsSettings;
import com.android.server.net.NetworkStatsService.NetworkStatsSettings.Config;
import com.android.testutils.HandlerUtils;
import com.android.testutils.TestableNetworkStatsProviderBinder;
-import libcore.io.IoUtils;
-
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
@@ -152,6 +151,8 @@
private static final String TEST_SSID = "AndroidAP";
private static NetworkTemplate sTemplateWifi = buildTemplateWifi(TEST_SSID);
+ private static NetworkTemplate sTemplateCarrierWifi1 =
+ buildTemplateWifi(NetworkTemplate.WIFI_NETWORKID_ALL, IMSI_1);
private static NetworkTemplate sTemplateImsi1 = buildTemplateMobileAll(IMSI_1);
private static NetworkTemplate sTemplateImsi2 = buildTemplateMobileAll(IMSI_2);
@@ -213,7 +214,7 @@
mServiceContext = new MockContext(context);
mStatsDir = context.getFilesDir();
if (mStatsDir.exists()) {
- IoUtils.deleteContents(mStatsDir);
+ FsUtil.deleteContents(mStatsDir);
}
PowerManager powerManager = (PowerManager) mServiceContext.getSystemService(
@@ -283,7 +284,7 @@
@After
public void tearDown() throws Exception {
- IoUtils.deleteContents(mStatsDir);
+ FsUtil.deleteContents(mStatsDir);
mServiceContext = null;
mStatsDir = null;
@@ -297,45 +298,82 @@
mHandlerThread.quitSafely();
}
- @Test
- public void testNetworkStatsWifi() throws Exception {
+ private void initWifiStats(NetworkStateSnapshot snapshot) throws Exception {
// pretend that wifi network comes online; service should ask about full
// network state, and poll any existing interfaces before updating.
expectDefaultSettings();
- NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
+ NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {snapshot};
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
+ }
- // verify service has empty history for wifi
+ private void incrementWifiStats(long durationMillis, String iface,
+ long rxb, long rxp, long txb, long txp) throws Exception {
+ incrementCurrentTime(durationMillis);
+ expectDefaultSettings();
+ expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+ .insertEntry(iface, rxb, rxp, txb, txp));
+ expectNetworkStatsUidDetail(buildEmptyStats());
+ forcePollAndWaitForIdle();
+ }
+
+ @Test
+ public void testNetworkStatsCarrierWifi() throws Exception {
+ initWifiStats(buildWifiState(true, TEST_IFACE, IMSI_1));
+ // verify service has empty history for carrier merged wifi and non-carrier wifi
+ assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0);
assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
// modify some number on wifi, and trigger poll event
- incrementCurrentTime(HOUR_IN_MILLIS);
- expectDefaultSettings();
- expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
- .insertEntry(TEST_IFACE, 1024L, 1L, 2048L, 2L));
- expectNetworkStatsUidDetail(buildEmptyStats());
- forcePollAndWaitForIdle();
+ incrementWifiStats(HOUR_IN_MILLIS, TEST_IFACE, 1024L, 1L, 2048L, 2L);
// verify service recorded history
- assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0);
+ assertNetworkTotal(sTemplateCarrierWifi1, 1024L, 1L, 2048L, 2L, 0);
+
+ // verify service recorded history for wifi with SSID filter
+ assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0);
// and bump forward again, with counters going higher. this is
// important, since polling should correctly subtract last snapshot.
- incrementCurrentTime(DAY_IN_MILLIS);
- expectDefaultSettings();
- expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
- .insertEntry(TEST_IFACE, 4096L, 4L, 8192L, 8L));
- expectNetworkStatsUidDetail(buildEmptyStats());
- forcePollAndWaitForIdle();
+ incrementWifiStats(DAY_IN_MILLIS, TEST_IFACE, 4096L, 4L, 8192L, 8L);
+
+ // verify service recorded history
+ assertNetworkTotal(sTemplateCarrierWifi1, 4096L, 4L, 8192L, 8L, 0);
+ // verify service recorded history for wifi with SSID filter
+ assertNetworkTotal(sTemplateWifi, 4096L, 4L, 8192L, 8L, 0);
+ }
+
+ @Test
+ public void testNetworkStatsNonCarrierWifi() throws Exception {
+ initWifiStats(buildWifiState());
+
+ // verify service has empty history for wifi
+ assertNetworkTotal(sTemplateWifi, 0L, 0L, 0L, 0L, 0);
+ // verify service has empty history for carrier merged wifi
+ assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0);
+
+ // modify some number on wifi, and trigger poll event
+ incrementWifiStats(HOUR_IN_MILLIS, TEST_IFACE, 1024L, 1L, 2048L, 2L);
+
+ // verify service recorded history
+ assertNetworkTotal(sTemplateWifi, 1024L, 1L, 2048L, 2L, 0);
+ // verify service has empty history for carrier wifi since current network is non carrier
+ // wifi
+ assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0);
+
+ // and bump forward again, with counters going higher. this is
+ // important, since polling should correctly subtract last snapshot.
+ incrementWifiStats(DAY_IN_MILLIS, TEST_IFACE, 4096L, 4L, 8192L, 8L);
// verify service recorded history
assertNetworkTotal(sTemplateWifi, 4096L, 4L, 8192L, 8L, 0);
-
+ // verify service has empty history for carrier wifi since current network is non carrier
+ // wifi
+ assertNetworkTotal(sTemplateCarrierWifi1, 0L, 0L, 0L, 0L, 0);
}
@Test
@@ -349,7 +387,7 @@
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// verify service has empty history for wifi
@@ -423,7 +461,7 @@
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// modify some number on wifi, and trigger poll event
@@ -464,7 +502,7 @@
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// create some traffic on first network
@@ -499,7 +537,7 @@
.insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xF00D, 512L, 4L, 512L, 4L, 0L)
.insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 512L, 4L, 0L, 0L, 0L));
- mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
forcePollAndWaitForIdle();
@@ -539,7 +577,7 @@
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// create some traffic
@@ -607,7 +645,7 @@
expectNetworkStatsUidDetail(buildEmptyStats());
setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS);
- mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// Create some traffic.
@@ -699,7 +737,7 @@
new int[]{NetworkCapabilities.NET_CAPABILITY_OEM_PAID})};
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// Create some traffic.
@@ -714,7 +752,7 @@
new int[]{NetworkCapabilities.NET_CAPABILITY_OEM_PRIVATE})};
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// Create some traffic.
@@ -730,7 +768,7 @@
NetworkCapabilities.NET_CAPABILITY_OEM_PAID})};
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// Create some traffic.
@@ -744,7 +782,7 @@
states = new NetworkStateSnapshot[]{buildOemManagedMobileState(IMSI_1, false, new int[]{})};
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// Create some traffic.
@@ -797,7 +835,7 @@
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// create some traffic for two apps
@@ -856,7 +894,7 @@
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
NetworkStats.Entry entry1 = new NetworkStats.Entry(
@@ -900,7 +938,7 @@
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
NetworkStats.Entry uidStats = new NetworkStats.Entry(
@@ -931,7 +969,7 @@
// mStatsFactory#readNetworkStatsDetail() has the following invocations:
// 1) NetworkStatsService#systemReady from #setUp.
- // 2) mService#forceUpdateIfaces in the test above.
+ // 2) mService#notifyNetworkStatus in the test above.
//
// Additionally, we should have one call from the above call to mService#getDetailedUidStats
// with the augmented ifaceFilter.
@@ -955,7 +993,7 @@
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// create some initial traffic
@@ -1013,7 +1051,7 @@
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// create some initial traffic
@@ -1053,7 +1091,7 @@
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// Create some traffic
@@ -1092,7 +1130,7 @@
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// create some tethering traffic
@@ -1149,7 +1187,7 @@
expectNetworkStatsSummary(buildEmptyStats());
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// verify service has empty history for wifi
@@ -1255,7 +1293,7 @@
mService.registerNetworkStatsProvider("TEST", provider);
assertNotNull(cb);
- mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// Verifies that one requestStatsUpdate will be called during iface update.
@@ -1320,7 +1358,7 @@
mService.registerNetworkStatsProvider("TEST", provider);
assertNotNull(cb);
- mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// Verifies that one requestStatsUpdate will be called during iface update.
@@ -1378,7 +1416,7 @@
expectDefaultSettings();
NetworkStateSnapshot[] states =
new NetworkStateSnapshot[]{buildWifiState(true /* isMetered */, TEST_IFACE)};
- mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// Register custom provider and retrieve callback.
@@ -1428,7 +1466,7 @@
// 3G network comes online.
setMobileRatTypeAndWaitForIdle(TelephonyManager.NETWORK_TYPE_UMTS);
- mService.forceUpdateIfaces(NETWORKS_MOBILE, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_MOBILE, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// Create some traffic.
@@ -1450,7 +1488,8 @@
setCombineSubtypeEnabled(true);
// Call handleOnCollapsedRatTypeChanged manually to simulate the callback fired
- // when stopping monitor, this is needed by NetworkStatsService to trigger updateIfaces.
+ // when stopping monitor, this is needed by NetworkStatsService to trigger
+ // handleNotifyNetworkStatus.
mService.handleOnCollapsedRatTypeChanged();
HandlerUtils.waitForIdle(mHandlerThread, WAIT_TIMEOUT);
// Create some traffic.
@@ -1499,7 +1538,7 @@
NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{
buildWifiState(true /*isMetered*/, TEST_IFACE2), buildMobile3gState(IMSI_1)};
expectNetworkStatsUidDetail(buildEmptyStats());
- mService.forceUpdateIfaces(NETWORKS_WIFI, states, getActiveIface(states),
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
new UnderlyingNetworkInfo[0]);
// Create some traffic on mobile network.
@@ -1661,10 +1700,15 @@
}
private static NetworkStateSnapshot buildWifiState() {
- return buildWifiState(false, TEST_IFACE);
+ return buildWifiState(false, TEST_IFACE, null);
}
private static NetworkStateSnapshot buildWifiState(boolean isMetered, @NonNull String iface) {
+ return buildWifiState(isMetered, iface, null);
+ }
+
+ private static NetworkStateSnapshot buildWifiState(boolean isMetered, @NonNull String iface,
+ String subscriberId) {
final LinkProperties prop = new LinkProperties();
prop.setInterfaceName(iface);
final NetworkCapabilities capabilities = new NetworkCapabilities();
@@ -1672,7 +1716,7 @@
capabilities.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING, true);
capabilities.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
capabilities.setSSID(TEST_SSID);
- return new NetworkStateSnapshot(WIFI_NETWORK, capabilities, prop, null, TYPE_WIFI);
+ return new NetworkStateSnapshot(WIFI_NETWORK, capabilities, prop, subscriberId, TYPE_WIFI);
}
private static NetworkStateSnapshot buildMobile3gState(String subscriberId) {