diff --git a/service/Android.bp b/service/Android.bp index 1330e719e7..513de19569 100644 --- a/service/Android.bp +++ b/service/Android.bp @@ -52,8 +52,8 @@ cc_library_shared { 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/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java new file mode 100644 index 0000000000..7ffca45efe --- /dev/null +++ b/service/src/com/android/server/ConnectivityService.java @@ -0,0 +1,10083 @@ +/* + * 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 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 + */ + 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 + */ + 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 mProtectedNetworks; + + private Set 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 mBandwidthRequests = new SparseArray(10); + + @VisibleForTesting + final MultinetworkPolicyTracker mMultinetworkPolicyTracker; + + @VisibleForTesting + final Map 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 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 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 loadRestoreTimers() { + final String[] configs = mService.mResources.get().getStringArray( + R.array.config_legacy_networktype_restore_timers); + final ArrayMap 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 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 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 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 both NORMAL and HIGH to + // align the legacy design. + // TODO: Integrate into signal dump. + dumpNormal(fd, pw, args); + pw.println(); + pw.println("DUMP OF SERVICE HIGH connectivity"); + pw.println(); + dumpHigh(fd, pw); + } + } + } + + /** + * 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(NO_SERVICE_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 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 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 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 getAllNetworkStateSnapshots() { + // This contains IMSI details, so make sure the caller is privileged. + PermissionUtils.enforceNetworkStackPermission(mContext); + + final ArrayList 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 netDiags = new ArrayList(); + 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 arg = (Pair) 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 underlying = (List) 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 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 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 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 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 arg = (Pair) 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 arg = + (Pair) 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 arg = + (Pair) msg.obj; + handleSetOemNetworkPreference(arg.first, arg.second); + break; + } + case EVENT_SET_PROFILE_NETWORK_PREFERENCE: { + final Pair arg = + (Pair) + 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 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. + * + *

This method is used to update NetworkStatsService. + * + *

Must be called on the handler thread. + */ + private UnderlyingNetworkInfo[] getAllVpnInfo() { + ensureRunningOnConnectivityServiceThread(); + if (mLockdownEnabled) { + return new UnderlyingNetworkInfo[0]; + } + List 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 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 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 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 mNetworkProviderInfos = new HashMap<>(); + private final HashMap 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 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 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 getUids() { + // networkCapabilities.getUids() returns a defensive copy. + // multilayer requests will all have the same uids so return the first one. + final Set 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 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 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 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 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 initializeRequests(List r) { + // Creating a defensive copy to prevent the sender from modifying the list being + // reflected in the return value of this method. + final List 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 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 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 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 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 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 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 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 mVpnBlockedUidRanges = new ArrayList<>(); + + // Must only be accessed on the handler thread + @NonNull + private final ArrayList mNetworkOffers = new ArrayList<>(); + + @GuardedBy("mBlockedAppUids") + private final HashSet 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 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 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 copyNetworkRequestsForUid( + @NonNull final List requestsToCopy, final int asUid, + final int requestorUid, @NonNull final String requestorPackageName) { + final List 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; + + // TODO replace with INetd.UNREACHABLE_NET_ID when available. + private static final int NO_SERVICE_NET_ID = 52; + // 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 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 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 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 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 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 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 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 prevRanges = null == prevNc ? null : prevNc.getUidRanges(); + Set newRanges = null == newNc ? null : newNc.getUidRanges(); + if (null == prevRanges) prevRanges = new ArraySet<>(); + if (null == newRanges) newRanges = new ArraySet<>(); + final Set 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, 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 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 mReassignments = new ArrayList<>(); + + @NonNull Iterable 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 networkRequests) { + final NetworkReassignment changes = new NetworkReassignment(); + + // Gather the list of all relevant agents and sort them by score. + final ArrayList 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 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 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 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 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 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 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 oldBlockedUidRanges, + List 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 getDefaultNetworks() { + ensureRunningOnConnectivityServiceThread(); + final ArrayList defaultNetworks = new ArrayList<>(); + final Set 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 snapshots = new ArrayList<>(); + // TODO: Directly use NetworkStateSnapshot when feasible. + for (final NetworkState state : getAllNetworkState()) { + final NetworkStateSnapshot snapshot = new NetworkStateSnapshot(state.network, + state.networkCapabilities, state.linkProperties, state.subscriberId, + state.legacyNetworkType); + 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(); + + if (mUserManager.hasUserRestriction(UserManager.DISALLOW_NETWORK_RESET)) { + return; + } + + final long token = Binder.clearCallingIdentity(); + try { + final IpMemoryStore ipMemoryStore = IpMemoryStore.getMemoryStore(mContext); + ipMemoryStore.factoryReset(); + } finally { + Binder.restoreCallingIdentity(token); + } + + // Turn airplane mode off + setAirplaneMode(false); + + // restore private DNS settings to default mode (opportunistic) + if (!mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_PRIVATE_DNS)) { + ConnectivitySettingsManager.setPrivateDnsMode(mContext, PRIVATE_DNS_MODE_OPPORTUNISTIC); + } + + Settings.Global.putString(mContext.getContentResolver(), + ConnectivitySettingsManager.NETWORK_AVOID_BAD_WIFI, null); + } + + @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. + * + *

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. + * + *

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 arg = + (Pair) 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 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 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 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 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 getMatchingPermissionedCallbacks( + @NonNull NetworkAgentInfo nai) { + final List results = new ArrayList<>(); + for (Entry 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; + } + + @VisibleForTesting + boolean checkConnectivityDiagnosticsPermissions( + int callbackPid, int callbackUid, NetworkAgentInfo nai, String callbackPackageName) { + if (checkNetworkStackPermission(callbackPid, callbackUid)) { + return true; + } + + // 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( + callbackPackageName, null /* featureId */, callbackUid, null /* message */)) { + return false; + } + } catch (SecurityException e) { + return false; + } + + for (NetworkAgentInfo virtual : mNetworkAgentInfos) { + if (virtual.supportsUnderlyingNetworks() + && virtual.networkCapabilities.getOwnerUid() == callbackUid + && CollectionUtils.contains(virtual.declaredUnderlyingNetworks, nai.network)) { + return true; + } + } + + // Administrator UIDs also contains the Owner UID + final int[] administratorUids = nai.networkCapabilities.getAdministratorUids(); + return CollectionUtils.contains(administratorUids, 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 mNetworkActivityListeners = + new RemoteCallbackList<>(); + // Indicate the current system default network activity is active or not. + @GuardedBy("mActiveIdleTimers") + private boolean mNetworkActive; + @GuardedBy("mActiveIdleTimers") + private final ArrayMap 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 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 createNrisFromProfileNetworkPreferences( + @NonNull final ProfileNetworkPreferences prefs) { + final ArraySet 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 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 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 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 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 nris) { + ensureRunningOnConnectivityServiceThread(); + mDefaultNetworkRequests.addAll(nris); + final ArraySet perAppCallbackRequestsToUpdate = + getPerAppCallbackRequestsToUpdate(); + final ArraySet 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 getPerAppCallbackRequestsToUpdate() { + final ArraySet 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 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 createPerAppCallbackRequestsToRegister( + @NonNull final ArraySet perAppCallbackRequestsForUpdate) { + final ArraySet 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 requests, + @NonNull final Set 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 createNrisFromOemNetworkPreferences( + @NonNull final OemNetworkPreferences preference) { + final ArraySet nris = new ArraySet<>(); + final SparseArray> uids = + createUidsFromOemNetworkPreferences(preference); + for (int i = 0; i < uids.size(); i++) { + final int key = uids.keyAt(i); + final Set 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> createUidsFromOemNetworkPreferences( + @NonNull final OemNetworkPreferences preference) { + final SparseArray> uids = new SparseArray<>(); + final PackageManager pm = mContext.getPackageManager(); + final List 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 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 uids) { + final List 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(); + 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 0000000000..2465479aad --- /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/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java new file mode 100644 index 0000000000..f5662772f5 --- /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 + * + *

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 + * + *

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 + * + *

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 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. + * + *

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 0000000000..009a43e582 --- /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 The type of the held object + * @hide + */ +public class AutodestructReference { + private final AtomicReference 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 0000000000..325a2cd7bd --- /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 0000000000..05b12bad55 --- /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 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, 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> latestDnses = new HashSet<>(); + for (String ipAddress : ipAddresses) { + try { + latestDnses.add(new Pair(hostname, + InetAddresses.parseNumericAddress(ipAddress))); + } catch (IllegalArgumentException e) {} + } + // Remove pairs that should not be tracked. + for (Iterator, ValidationStatus>> it = + mValidationMap.entrySet().iterator(); it.hasNext(); ) { + Map.Entry, ValidationStatus> entry = it.next(); + if (!latestDnses.contains(entry.getKey())) { + it.remove(); + } + } + // Add new pairs that should be tracked. + for (Pair p : latestDnses) { + if (!mValidationMap.containsKey(p)) { + mValidationMap.put(p, ValidationStatus.IN_PROGRESS); + } + } + } + + private void updateStatus(PrivateDnsValidationUpdate update) { + Pair 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 mPrivateDnsMap; + // TODO: Replace the Map with SparseArrays. + private final Map mPrivateDnsValidationMap; + private final Map mLinkPropertiesMap; + private final Map 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 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 0000000000..9326d692f6 --- /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 0000000000..acf39f05a5 --- /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 > 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 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 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 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(); + 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 networkKeepalives = mKeepalives.get(nai); + if (networkKeepalives != null) { + final ArrayList 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 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 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 networkKeepalives = mKeepalives.get(nai); + if (networkKeepalives != null) { + ArrayList> 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 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 0000000000..032612c6f0 --- /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 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 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 makeTransportToNameMap() { + SparseArray numberToName = MessageUtils.findMessageNames( + new Class[] { NetworkCapabilities.class }, new String[]{ "TRANSPORT_" }); + HashMap 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 0000000000..a25b89ac03 --- /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 0000000000..c66a280f2b --- /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 0000000000..ee32fbf00d --- /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 { + + @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 { + 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 mInactivityTimers = new TreeSet<>(); + + // For fast lookups. Indexes into mInactivityTimers by request ID. + private final SparseArray 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 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 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. + * + *

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. + * + *

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. + * + *

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. + * + *

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 0000000000..2e51be39bf --- /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 . + // 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 mIcmpChecks = new HashMap<>(); + private final Map, Measurement> mExplicitSourceIcmpChecks = + new HashMap<>(); + private final Map mDnsUdpChecks = new HashMap<>(); + private final Map 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 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 getMeasurements() { + // TODO: Consider moving waitForMeasurements() in here to minimize the + // chance of caller errors. + + ArrayList measurements = new ArrayList(totalMeasurementCount()); + + // Sort measurements IPv4 first. + for (Map.Entry entry : mIcmpChecks.entrySet()) { + if (entry.getKey() instanceof Inet4Address) { + measurements.add(entry.getValue()); + } + } + for (Map.Entry, Measurement> entry : + mExplicitSourceIcmpChecks.entrySet()) { + if (entry.getKey().first instanceof Inet4Address) { + measurements.add(entry.getValue()); + } + } + for (Map.Entry entry : mDnsUdpChecks.entrySet()) { + if (entry.getKey() instanceof Inet4Address) { + measurements.add(entry.getValue()); + } + } + for (Map.Entry entry : mDnsTlsChecks.entrySet()) { + if (entry.getKey() instanceof Inet4Address) { + measurements.add(entry.getValue()); + } + } + + // IPv6 measurements second. + for (Map.Entry entry : mIcmpChecks.entrySet()) { + if (entry.getKey() instanceof Inet6Address) { + measurements.add(entry.getValue()); + } + } + for (Map.Entry, Measurement> entry : + mExplicitSourceIcmpChecks.entrySet()) { + if (entry.getKey().first instanceof Inet6Address) { + measurements.add(entry.getValue()); + } + } + for (Map.Entry entry : mDnsUdpChecks.entrySet()) { + if (entry.getKey() instanceof Inet6Address) { + measurements.add(entry.getValue()); + } + } + for (Map.Entry 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 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 0000000000..0c0d45995a --- /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 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 0000000000..fa2d465fff --- /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 0000000000..d0aabf95d5 --- /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 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 0000000000..506cadb2b1 --- /dev/null +++ b/service/src/com/android/server/connectivity/PermissionMonitor.java @@ -0,0 +1,733 @@ +/* + * 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.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.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.system.OsConstants; +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 mUsers = new HashSet<>(); + + // Keys are app uids. Values are true for SYSTEM permission and false for NETWORK permission. + @GuardedBy("this") + private final Map 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> 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 mAllApps = new HashSet<>(); + + 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; + } + } + + 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 IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + intentFilter.addDataScheme("package"); + mContext.createContextAsUser(UserHandle.ALL, 0 /* flags */).registerReceiver( + mIntentReceiver, intentFilter, null /* broadcastPermission */, + null /* scheduler */); + + List 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 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 + static boolean isVendorApp(@NonNull ApplicationInfo appInfo) { + return appInfo.isVendor() || appInfo.isOem() || appInfo.isProduct(); + } + + @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 this check in the future(b/31479477). All apps should just + // request the appropriate permission for their use case since android Q. + if (app.applicationInfo != null) { + // Backward compatibility for b/114245686, on devices that launched before Q daemons + // and apps running as the system UID are exempted from this check. + if (app.applicationInfo.uid == SYSTEM_UID && mDeps.getDeviceFirstSdkInt() < VERSION_Q) { + return true; + } + + if (app.applicationInfo.targetSdkVersion < VERSION_Q + && isVendorApp(app.applicationInfo)) { + return true; + } + } + + return 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 users, Map apps, boolean add) { + List network = new ArrayList<>(); + List system = new ArrayList<>(); + for (Entry app : apps.entrySet()) { + List 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 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 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 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> vpn : mVpnUidRanges.entrySet()) { + if (UidRange.containsUid(vpn.getValue(), uid)) { + final Set changedUids = new HashSet<>(); + changedUids.add(uid); + removeBypassingUids(changedUids, /* vpnAppUid */ -1); + updateVpnUids(vpn.getKey(), changedUids, true); + } + } + mAllApps.add(UserHandle.getAppId(uid)); + } + + /** + * 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> vpn : mVpnUidRanges.entrySet()) { + if (UidRange.containsUid(vpn.getValue(), uid)) { + final Set 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 apps = new HashMap<>(); + Boolean permission = null; + String[] packages = mPackageManager.getPackagesForUid(uid); + if (packages != null && packages.length > 0) { + for (String name : packages) { + permission = highestPermissionForUid(permission, name); + 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 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 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(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 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 changedUids = intersectUids(rangesToRemove, mAllApps); + removeBypassingUids(changedUids, vpnAppUid); + updateVpnUids(iface, changedUids, false); + Set 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 intersectUids(Set ranges, Set appIds) { + Set 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 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 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 allPermissionAppIds = new ArrayList<>(); + ArrayList internetPermissionAppIds = new ArrayList<>(); + ArrayList updateStatsPermissionAppIds = new ArrayList<>(); + ArrayList noPermissionAppIds = new ArrayList<>(); + ArrayList 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 getVpnUidRanges(String iface) { + return mVpnUidRanges.get(iface); + } + + /** Dump info to dumpsys */ + public void dump(IndentingPrintWriter pw) { + pw.println("Interface filtering rules:"); + pw.increaseIndent(); + for (Map.Entry> 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/ProxyTracker.java b/service/src/com/android/server/connectivity/ProxyTracker.java new file mode 100644 index 0000000000..f572b46a9b --- /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 0000000000..534dbe7699 --- /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. + *

+ * 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 0000000000..b6ab47b276 --- /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. + *

+ * 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. + *

+ * 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. + *

+ * Note: The term previous does not apply to the first callback id that is assigned. + */ + private int mPreviousAgentCallbackId = 0; + + @NonNull + private final List 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 0000000000..c480594b8c --- /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 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); + } + } +}