Move connectivity sources to packages/Connectivity

The service-connectivity sources should be in
packages/modules/Connectivity. Move them to
frameworks/base/packages/Connectivity, so that the whole directory can
be moved to the dedicated packages/modules/Connectivity git project.

Bug: 186628461
Test: m
Merged-In: I26d1a274058fa38763ad4f605549d880865b4d76
Change-Id: Ie0562db92ebee269b901926d763ae907bde61b98
This commit is contained in:
Remi NGUYEN VAN
2021-05-13 12:53:15 +00:00
parent 393e7b3d6a
commit cdb45f8e37
22 changed files with 17315 additions and 1 deletions

View File

@@ -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",

File diff suppressed because it is too large Load Diff

View File

@@ -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);
}
}

View File

@@ -0,0 +1,386 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server;
import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
import static android.net.TestNetworkManager.TEST_TUN_PREFIX;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.INetd;
import android.net.ITestNetworkManager;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.NetworkAgent;
import android.net.NetworkAgentConfig;
import android.net.NetworkCapabilities;
import android.net.NetworkProvider;
import android.net.RouteInfo;
import android.net.TestNetworkInterface;
import android.net.TestNetworkSpecifier;
import android.net.util.NetdService;
import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.NetworkStackConstants;
import com.android.net.module.util.PermissionUtils;
import java.io.UncheckedIOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
/** @hide */
class TestNetworkService extends ITestNetworkManager.Stub {
@NonNull private static final String TEST_NETWORK_LOGTAG = "TestNetworkAgent";
@NonNull private static final String TEST_NETWORK_PROVIDER_NAME = "TestNetworkProvider";
@NonNull private static final AtomicInteger sTestTunIndex = new AtomicInteger();
@NonNull private final Context mContext;
@NonNull private final INetd mNetd;
@NonNull private final HandlerThread mHandlerThread;
@NonNull private final Handler mHandler;
@NonNull private final ConnectivityManager mCm;
@NonNull private final NetworkProvider mNetworkProvider;
// Native method stubs
private static native int jniCreateTunTap(boolean isTun, @NonNull String iface);
@VisibleForTesting
protected TestNetworkService(@NonNull Context context) {
mHandlerThread = new HandlerThread("TestNetworkServiceThread");
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
mContext = Objects.requireNonNull(context, "missing Context");
mNetd = Objects.requireNonNull(NetdService.getInstance(), "could not get netd instance");
mCm = mContext.getSystemService(ConnectivityManager.class);
mNetworkProvider = new NetworkProvider(mContext, mHandler.getLooper(),
TEST_NETWORK_PROVIDER_NAME);
final long token = Binder.clearCallingIdentity();
try {
mCm.registerNetworkProvider(mNetworkProvider);
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* Create a TUN or TAP interface with the given interface name and link addresses
*
* <p>This method will return the FileDescriptor to the interface. Close it to tear down the
* interface.
*/
private TestNetworkInterface createInterface(boolean isTun, LinkAddress[] linkAddrs) {
enforceTestNetworkPermissions(mContext);
Objects.requireNonNull(linkAddrs, "missing linkAddrs");
String ifacePrefix = isTun ? TEST_TUN_PREFIX : TEST_TAP_PREFIX;
String iface = ifacePrefix + sTestTunIndex.getAndIncrement();
final long token = Binder.clearCallingIdentity();
try {
ParcelFileDescriptor tunIntf =
ParcelFileDescriptor.adoptFd(jniCreateTunTap(isTun, iface));
for (LinkAddress addr : linkAddrs) {
mNetd.interfaceAddAddress(
iface,
addr.getAddress().getHostAddress(),
addr.getPrefixLength());
}
return new TestNetworkInterface(tunIntf, iface);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* Create a TUN interface with the given interface name and link addresses
*
* <p>This method will return the FileDescriptor to the TUN interface. Close it to tear down the
* TUN interface.
*/
@Override
public TestNetworkInterface createTunInterface(@NonNull LinkAddress[] linkAddrs) {
return createInterface(true, linkAddrs);
}
/**
* Create a TAP interface with the given interface name
*
* <p>This method will return the FileDescriptor to the TAP interface. Close it to tear down the
* TAP interface.
*/
@Override
public TestNetworkInterface createTapInterface() {
return createInterface(false, new LinkAddress[0]);
}
// Tracker for TestNetworkAgents
@GuardedBy("mTestNetworkTracker")
@NonNull
private final SparseArray<TestNetworkAgent> mTestNetworkTracker = new SparseArray<>();
public class TestNetworkAgent extends NetworkAgent implements IBinder.DeathRecipient {
private static final int NETWORK_SCORE = 1; // Use a low, non-zero score.
private final int mUid;
@GuardedBy("mBinderLock")
@NonNull
private IBinder mBinder;
@NonNull private final Object mBinderLock = new Object();
private TestNetworkAgent(
@NonNull Context context,
@NonNull Looper looper,
@NonNull NetworkCapabilities nc,
@NonNull LinkProperties lp,
@NonNull NetworkAgentConfig config,
int uid,
@NonNull IBinder binder,
@NonNull NetworkProvider np)
throws RemoteException {
super(context, looper, TEST_NETWORK_LOGTAG, nc, lp, NETWORK_SCORE, config, np);
mUid = uid;
synchronized (mBinderLock) {
mBinder = binder; // Binder null-checks in create()
try {
mBinder.linkToDeath(this, 0);
} catch (RemoteException e) {
binderDied();
throw e; // Abort, signal failure up the stack.
}
}
}
/**
* If the Binder object dies, this function is called to free the resources of this
* TestNetworkAgent
*/
@Override
public void binderDied() {
teardown();
}
@Override
protected void unwanted() {
teardown();
}
private void teardown() {
unregister();
// Synchronize on mBinderLock to ensure that unlinkToDeath is never called more than
// once (otherwise it could throw an exception)
synchronized (mBinderLock) {
// If mBinder is null, this Test Network has already been cleaned up.
if (mBinder == null) return;
mBinder.unlinkToDeath(this, 0);
mBinder = null;
}
// Has to be in TestNetworkAgent to ensure all teardown codepaths properly clean up
// resources, even for binder death or unwanted calls.
synchronized (mTestNetworkTracker) {
mTestNetworkTracker.remove(getNetwork().getNetId());
}
}
}
private TestNetworkAgent registerTestNetworkAgent(
@NonNull Looper looper,
@NonNull Context context,
@NonNull String iface,
@Nullable LinkProperties lp,
boolean isMetered,
int callingUid,
@NonNull int[] administratorUids,
@NonNull IBinder binder)
throws RemoteException, SocketException {
Objects.requireNonNull(looper, "missing Looper");
Objects.requireNonNull(context, "missing Context");
// iface and binder validity checked by caller
// Build narrow set of NetworkCapabilities, useful only for testing
NetworkCapabilities nc = new NetworkCapabilities();
nc.clearAll(); // Remove default capabilities.
nc.addTransportType(NetworkCapabilities.TRANSPORT_TEST);
nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED);
nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
nc.setNetworkSpecifier(new TestNetworkSpecifier(iface));
nc.setAdministratorUids(administratorUids);
if (!isMetered) {
nc.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
}
// Build LinkProperties
if (lp == null) {
lp = new LinkProperties();
} else {
lp = new LinkProperties(lp);
// Use LinkAddress(es) from the interface itself to minimize how much the caller
// is trusted.
lp.setLinkAddresses(new ArrayList<>());
}
lp.setInterfaceName(iface);
// Find the currently assigned addresses, and add them to LinkProperties
boolean allowIPv4 = false, allowIPv6 = false;
NetworkInterface netIntf = NetworkInterface.getByName(iface);
Objects.requireNonNull(netIntf, "No such network interface found: " + netIntf);
for (InterfaceAddress intfAddr : netIntf.getInterfaceAddresses()) {
lp.addLinkAddress(
new LinkAddress(intfAddr.getAddress(), intfAddr.getNetworkPrefixLength()));
if (intfAddr.getAddress() instanceof Inet6Address) {
allowIPv6 |= !intfAddr.getAddress().isLinkLocalAddress();
} else if (intfAddr.getAddress() instanceof Inet4Address) {
allowIPv4 = true;
}
}
// Add global routes (but as non-default, non-internet providing network)
if (allowIPv4) {
lp.addRoute(new RouteInfo(new IpPrefix(
NetworkStackConstants.IPV4_ADDR_ANY, 0), null, iface));
}
if (allowIPv6) {
lp.addRoute(new RouteInfo(new IpPrefix(
NetworkStackConstants.IPV6_ADDR_ANY, 0), null, iface));
}
final TestNetworkAgent agent = new TestNetworkAgent(context, looper, nc, lp,
new NetworkAgentConfig.Builder().build(), callingUid, binder,
mNetworkProvider);
agent.register();
agent.markConnected();
return agent;
}
/**
* Sets up a Network with extremely limited privileges, guarded by the MANAGE_TEST_NETWORKS
* permission.
*
* <p>This method provides a Network that is useful only for testing.
*/
@Override
public void setupTestNetwork(
@NonNull String iface,
@Nullable LinkProperties lp,
boolean isMetered,
@NonNull int[] administratorUids,
@NonNull IBinder binder) {
enforceTestNetworkPermissions(mContext);
Objects.requireNonNull(iface, "missing Iface");
Objects.requireNonNull(binder, "missing IBinder");
if (!(iface.startsWith(INetd.IPSEC_INTERFACE_PREFIX)
|| iface.startsWith(TEST_TUN_PREFIX))) {
throw new IllegalArgumentException(
"Cannot create network for non ipsec, non-testtun interface");
}
try {
final long token = Binder.clearCallingIdentity();
try {
PermissionUtils.enforceNetworkStackPermission(mContext);
NetdUtils.setInterfaceUp(mNetd, iface);
} finally {
Binder.restoreCallingIdentity(token);
}
// Synchronize all accesses to mTestNetworkTracker to prevent the case where:
// 1. TestNetworkAgent successfully binds to death of binder
// 2. Before it is added to the mTestNetworkTracker, binder dies, binderDied() is called
// (on a different thread)
// 3. This thread is pre-empted, put() is called after remove()
synchronized (mTestNetworkTracker) {
TestNetworkAgent agent =
registerTestNetworkAgent(
mHandler.getLooper(),
mContext,
iface,
lp,
isMetered,
Binder.getCallingUid(),
administratorUids,
binder);
mTestNetworkTracker.put(agent.getNetwork().getNetId(), agent);
}
} catch (SocketException e) {
throw new UncheckedIOException(e);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/** Teardown a test network */
@Override
public void teardownTestNetwork(int netId) {
enforceTestNetworkPermissions(mContext);
final TestNetworkAgent agent;
synchronized (mTestNetworkTracker) {
agent = mTestNetworkTracker.get(netId);
}
if (agent == null) {
return; // Already torn down
} else if (agent.mUid != Binder.getCallingUid()) {
throw new SecurityException("Attempted to modify other user's test networks");
}
// Safe to be called multiple times.
agent.teardown();
}
private static final String PERMISSION_NAME =
android.Manifest.permission.MANAGE_TEST_NETWORKS;
public static void enforceTestNetworkPermissions(@NonNull Context context) {
context.enforceCallingOrSelfPermission(PERMISSION_NAME, "TestNetworkService");
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.connectivity;
import android.annotation.NonNull;
import java.util.concurrent.atomic.AtomicReference;
/**
* A ref that autodestructs at the first usage of it.
* @param <T> The type of the held object
* @hide
*/
public class AutodestructReference<T> {
private final AtomicReference<T> mHeld;
public AutodestructReference(@NonNull T obj) {
if (null == obj) throw new NullPointerException("Autodestruct reference to null");
mHeld = new AtomicReference<>(obj);
}
/** Get the ref and destruct it. NPE if already destructed. */
@NonNull
public T getAndDestroy() {
final T obj = mHeld.getAndSet(null);
if (null == obj) throw new NullPointerException("Already autodestructed");
return obj;
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,494 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.connectivity;
import static android.net.ConnectivitySettingsManager.DNS_RESOLVER_MAX_SAMPLES;
import static android.net.ConnectivitySettingsManager.DNS_RESOLVER_MIN_SAMPLES;
import static android.net.ConnectivitySettingsManager.DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS;
import static android.net.ConnectivitySettingsManager.DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT;
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_DEFAULT_MODE;
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE;
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_SPECIFIER;
import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_FAILURE;
import static android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_SUCCESS;
import android.annotation.NonNull;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.ConnectivitySettingsManager;
import android.net.IDnsResolver;
import android.net.InetAddresses;
import android.net.LinkProperties;
import android.net.Network;
import android.net.ResolverOptionsParcel;
import android.net.ResolverParamsParcel;
import android.net.Uri;
import android.net.shared.PrivateDnsConfig;
import android.os.Binder;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* Encapsulate the management of DNS settings for networks.
*
* This class it NOT designed for concurrent access. Furthermore, all non-static
* methods MUST be called from ConnectivityService's thread. However, an exceptional
* case is getPrivateDnsConfig(Network) which is exclusively for
* ConnectivityService#dumpNetworkDiagnostics() on a random binder thread.
*
* [ Private DNS ]
* The code handling Private DNS is spread across several components, but this
* seems like the least bad place to collect all the observations.
*
* Private DNS handling and updating occurs in response to several different
* events. Each is described here with its corresponding intended handling.
*
* [A] Event: A new network comes up.
* Mechanics:
* [1] ConnectivityService gets notifications from NetworkAgents.
* [2] in updateNetworkInfo(), the first time the NetworkAgent goes into
* into CONNECTED state, the Private DNS configuration is retrieved,
* programmed, and strict mode hostname resolution (if applicable) is
* enqueued in NetworkAgent's NetworkMonitor, via a call to
* handlePerNetworkPrivateDnsConfig().
* [3] Re-resolution of strict mode hostnames that fail to return any
* IP addresses happens inside NetworkMonitor; it sends itself a
* delayed CMD_EVALUATE_PRIVATE_DNS message in a simple backoff
* schedule.
* [4] Successfully resolved hostnames are sent to ConnectivityService
* inside an EVENT_PRIVATE_DNS_CONFIG_RESOLVED message. The resolved
* IP addresses are programmed into netd via:
*
* updatePrivateDns() -> updateDnses()
*
* both of which make calls into DnsManager.
* [5] Upon a successful hostname resolution NetworkMonitor initiates a
* validation attempt in the form of a lookup for a one-time hostname
* that uses Private DNS.
*
* [B] Event: Private DNS settings are changed.
* Mechanics:
* [1] ConnectivityService gets notifications from its SettingsObserver.
* [2] handlePrivateDnsSettingsChanged() is called, which calls
* handlePerNetworkPrivateDnsConfig() and the process proceeds
* as if from A.3 above.
*
* [C] Event: An application calls ConnectivityManager#reportBadNetwork().
* Mechanics:
* [1] NetworkMonitor is notified and initiates a reevaluation, which
* always bypasses Private DNS.
* [2] Once completed, NetworkMonitor checks if strict mode is in operation
* and if so enqueues another evaluation of Private DNS, as if from
* step A.5 above.
*
* @hide
*/
public class DnsManager {
private static final String TAG = DnsManager.class.getSimpleName();
private static final PrivateDnsConfig PRIVATE_DNS_OFF = new PrivateDnsConfig();
/* Defaults for resolver parameters. */
private static final int DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS = 1800;
private static final int DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT = 25;
private static final int DNS_RESOLVER_DEFAULT_MIN_SAMPLES = 8;
private static final int DNS_RESOLVER_DEFAULT_MAX_SAMPLES = 64;
/**
* Get PrivateDnsConfig.
*/
public static PrivateDnsConfig getPrivateDnsConfig(Context context) {
final int mode = ConnectivitySettingsManager.getPrivateDnsMode(context);
final boolean useTls = mode != PRIVATE_DNS_MODE_OFF;
if (PRIVATE_DNS_MODE_PROVIDER_HOSTNAME == mode) {
final String specifier = getStringSetting(context.getContentResolver(),
PRIVATE_DNS_SPECIFIER);
return new PrivateDnsConfig(specifier, null);
}
return new PrivateDnsConfig(useTls);
}
public static Uri[] getPrivateDnsSettingsUris() {
return new Uri[]{
Settings.Global.getUriFor(PRIVATE_DNS_DEFAULT_MODE),
Settings.Global.getUriFor(PRIVATE_DNS_MODE),
Settings.Global.getUriFor(PRIVATE_DNS_SPECIFIER),
};
}
public static class PrivateDnsValidationUpdate {
public final int netId;
public final InetAddress ipAddress;
public final String hostname;
// Refer to IDnsResolverUnsolicitedEventListener.VALIDATION_RESULT_*.
public final int validationResult;
public PrivateDnsValidationUpdate(int netId, InetAddress ipAddress,
String hostname, int validationResult) {
this.netId = netId;
this.ipAddress = ipAddress;
this.hostname = hostname;
this.validationResult = validationResult;
}
}
private static class PrivateDnsValidationStatuses {
enum ValidationStatus {
IN_PROGRESS,
FAILED,
SUCCEEDED
}
// Validation statuses of <hostname, ipAddress> pairs for a single netId
// Caution : not thread-safe. As mentioned in the top file comment, all
// methods of this class must only be called on ConnectivityService's thread.
private Map<Pair<String, InetAddress>, ValidationStatus> mValidationMap;
private PrivateDnsValidationStatuses() {
mValidationMap = new HashMap<>();
}
private boolean hasValidatedServer() {
for (ValidationStatus status : mValidationMap.values()) {
if (status == ValidationStatus.SUCCEEDED) {
return true;
}
}
return false;
}
private void updateTrackedDnses(String[] ipAddresses, String hostname) {
Set<Pair<String, InetAddress>> latestDnses = new HashSet<>();
for (String ipAddress : ipAddresses) {
try {
latestDnses.add(new Pair(hostname,
InetAddresses.parseNumericAddress(ipAddress)));
} catch (IllegalArgumentException e) {}
}
// Remove <hostname, ipAddress> pairs that should not be tracked.
for (Iterator<Map.Entry<Pair<String, InetAddress>, ValidationStatus>> it =
mValidationMap.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<Pair<String, InetAddress>, ValidationStatus> entry = it.next();
if (!latestDnses.contains(entry.getKey())) {
it.remove();
}
}
// Add new <hostname, ipAddress> pairs that should be tracked.
for (Pair<String, InetAddress> p : latestDnses) {
if (!mValidationMap.containsKey(p)) {
mValidationMap.put(p, ValidationStatus.IN_PROGRESS);
}
}
}
private void updateStatus(PrivateDnsValidationUpdate update) {
Pair<String, InetAddress> p = new Pair(update.hostname,
update.ipAddress);
if (!mValidationMap.containsKey(p)) {
return;
}
if (update.validationResult == VALIDATION_RESULT_SUCCESS) {
mValidationMap.put(p, ValidationStatus.SUCCEEDED);
} else if (update.validationResult == VALIDATION_RESULT_FAILURE) {
mValidationMap.put(p, ValidationStatus.FAILED);
} else {
Log.e(TAG, "Unknown private dns validation operation="
+ update.validationResult);
}
}
private LinkProperties fillInValidatedPrivateDns(LinkProperties lp) {
lp.setValidatedPrivateDnsServers(Collections.EMPTY_LIST);
mValidationMap.forEach((key, value) -> {
if (value == ValidationStatus.SUCCEEDED) {
lp.addValidatedPrivateDnsServer(key.second);
}
});
return lp;
}
}
private final Context mContext;
private final ContentResolver mContentResolver;
private final IDnsResolver mDnsResolver;
private final ConcurrentHashMap<Integer, PrivateDnsConfig> mPrivateDnsMap;
// TODO: Replace the Map with SparseArrays.
private final Map<Integer, PrivateDnsValidationStatuses> mPrivateDnsValidationMap;
private final Map<Integer, LinkProperties> mLinkPropertiesMap;
private final Map<Integer, int[]> mTransportsMap;
private int mSampleValidity;
private int mSuccessThreshold;
private int mMinSamples;
private int mMaxSamples;
public DnsManager(Context ctx, IDnsResolver dnsResolver) {
mContext = ctx;
mContentResolver = mContext.getContentResolver();
mDnsResolver = dnsResolver;
mPrivateDnsMap = new ConcurrentHashMap<>();
mPrivateDnsValidationMap = new HashMap<>();
mLinkPropertiesMap = new HashMap<>();
mTransportsMap = new HashMap<>();
// TODO: Create and register ContentObservers to track every setting
// used herein, posting messages to respond to changes.
}
public PrivateDnsConfig getPrivateDnsConfig() {
return getPrivateDnsConfig(mContext);
}
public void removeNetwork(Network network) {
mPrivateDnsMap.remove(network.getNetId());
mPrivateDnsValidationMap.remove(network.getNetId());
mTransportsMap.remove(network.getNetId());
mLinkPropertiesMap.remove(network.getNetId());
}
// This is exclusively called by ConnectivityService#dumpNetworkDiagnostics() which
// is not on the ConnectivityService handler thread.
public PrivateDnsConfig getPrivateDnsConfig(@NonNull Network network) {
return mPrivateDnsMap.getOrDefault(network.getNetId(), PRIVATE_DNS_OFF);
}
public PrivateDnsConfig updatePrivateDns(Network network, PrivateDnsConfig cfg) {
Log.w(TAG, "updatePrivateDns(" + network + ", " + cfg + ")");
return (cfg != null)
? mPrivateDnsMap.put(network.getNetId(), cfg)
: mPrivateDnsMap.remove(network.getNetId());
}
public void updatePrivateDnsStatus(int netId, LinkProperties lp) {
// Use the PrivateDnsConfig data pushed to this class instance
// from ConnectivityService.
final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId,
PRIVATE_DNS_OFF);
final boolean useTls = privateDnsCfg.useTls;
final PrivateDnsValidationStatuses statuses =
useTls ? mPrivateDnsValidationMap.get(netId) : null;
final boolean validated = (null != statuses) && statuses.hasValidatedServer();
final boolean strictMode = privateDnsCfg.inStrictMode();
final String tlsHostname = strictMode ? privateDnsCfg.hostname : null;
final boolean usingPrivateDns = strictMode || validated;
lp.setUsePrivateDns(usingPrivateDns);
lp.setPrivateDnsServerName(tlsHostname);
if (usingPrivateDns && null != statuses) {
statuses.fillInValidatedPrivateDns(lp);
} else {
lp.setValidatedPrivateDnsServers(Collections.EMPTY_LIST);
}
}
public void updatePrivateDnsValidation(PrivateDnsValidationUpdate update) {
final PrivateDnsValidationStatuses statuses = mPrivateDnsValidationMap.get(update.netId);
if (statuses == null) return;
statuses.updateStatus(update);
}
/**
* When creating a new network or transport types are changed in a specific network,
* transport types are always saved to a hashMap before update dns config.
* When destroying network, the specific network will be removed from the hashMap.
* The hashMap is always accessed on the same thread.
*/
public void updateTransportsForNetwork(int netId, @NonNull int[] transportTypes) {
mTransportsMap.put(netId, transportTypes);
sendDnsConfigurationForNetwork(netId);
}
/**
* When {@link LinkProperties} are changed in a specific network, they are
* always saved to a hashMap before update dns config.
* When destroying network, the specific network will be removed from the hashMap.
* The hashMap is always accessed on the same thread.
*/
public void noteDnsServersForNetwork(int netId, @NonNull LinkProperties lp) {
mLinkPropertiesMap.put(netId, lp);
sendDnsConfigurationForNetwork(netId);
}
/**
* Send dns configuration parameters to resolver for a given network.
*/
public void sendDnsConfigurationForNetwork(int netId) {
final LinkProperties lp = mLinkPropertiesMap.get(netId);
final int[] transportTypes = mTransportsMap.get(netId);
if (lp == null || transportTypes == null) return;
updateParametersSettings();
final ResolverParamsParcel paramsParcel = new ResolverParamsParcel();
// We only use the PrivateDnsConfig data pushed to this class instance
// from ConnectivityService because it works in coordination with
// NetworkMonitor to decide which networks need validation and runs the
// blocking calls to resolve Private DNS strict mode hostnames.
//
// At this time we do not attempt to enable Private DNS on non-Internet
// networks like IMS.
final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.getOrDefault(netId,
PRIVATE_DNS_OFF);
final boolean useTls = privateDnsCfg.useTls;
final boolean strictMode = privateDnsCfg.inStrictMode();
paramsParcel.netId = netId;
paramsParcel.sampleValiditySeconds = mSampleValidity;
paramsParcel.successThreshold = mSuccessThreshold;
paramsParcel.minSamples = mMinSamples;
paramsParcel.maxSamples = mMaxSamples;
paramsParcel.servers = makeStrings(lp.getDnsServers());
paramsParcel.domains = getDomainStrings(lp.getDomains());
paramsParcel.tlsName = strictMode ? privateDnsCfg.hostname : "";
paramsParcel.tlsServers =
strictMode ? makeStrings(
Arrays.stream(privateDnsCfg.ips)
.filter((ip) -> lp.isReachable(ip))
.collect(Collectors.toList()))
: useTls ? paramsParcel.servers // Opportunistic
: new String[0]; // Off
paramsParcel.resolverOptions = new ResolverOptionsParcel();
paramsParcel.transportTypes = transportTypes;
// Prepare to track the validation status of the DNS servers in the
// resolver config when private DNS is in opportunistic or strict mode.
if (useTls) {
if (!mPrivateDnsValidationMap.containsKey(netId)) {
mPrivateDnsValidationMap.put(netId, new PrivateDnsValidationStatuses());
}
mPrivateDnsValidationMap.get(netId).updateTrackedDnses(paramsParcel.tlsServers,
paramsParcel.tlsName);
} else {
mPrivateDnsValidationMap.remove(netId);
}
Log.d(TAG, String.format("sendDnsConfigurationForNetwork(%d, %s, %s, %d, %d, %d, %d, "
+ "%d, %d, %s, %s)", paramsParcel.netId, Arrays.toString(paramsParcel.servers),
Arrays.toString(paramsParcel.domains), paramsParcel.sampleValiditySeconds,
paramsParcel.successThreshold, paramsParcel.minSamples,
paramsParcel.maxSamples, paramsParcel.baseTimeoutMsec,
paramsParcel.retryCount, paramsParcel.tlsName,
Arrays.toString(paramsParcel.tlsServers)));
try {
mDnsResolver.setResolverConfiguration(paramsParcel);
} catch (RemoteException | ServiceSpecificException e) {
Log.e(TAG, "Error setting DNS configuration: " + e);
return;
}
}
/**
* Flush DNS caches and events work before boot has completed.
*/
public void flushVmDnsCache() {
/*
* Tell the VMs to toss their DNS caches
*/
final Intent intent = new Intent(ConnectivityManager.ACTION_CLEAR_DNS_CACHE);
intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
/*
* Connectivity events can happen before boot has completed ...
*/
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
final long ident = Binder.clearCallingIdentity();
try {
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
private void updateParametersSettings() {
mSampleValidity = getIntSetting(
DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS,
DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS);
if (mSampleValidity < 0 || mSampleValidity > 65535) {
Log.w(TAG, "Invalid sampleValidity=" + mSampleValidity + ", using default="
+ DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS);
mSampleValidity = DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS;
}
mSuccessThreshold = getIntSetting(
DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT,
DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT);
if (mSuccessThreshold < 0 || mSuccessThreshold > 100) {
Log.w(TAG, "Invalid successThreshold=" + mSuccessThreshold + ", using default="
+ DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT);
mSuccessThreshold = DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT;
}
mMinSamples = getIntSetting(DNS_RESOLVER_MIN_SAMPLES, DNS_RESOLVER_DEFAULT_MIN_SAMPLES);
mMaxSamples = getIntSetting(DNS_RESOLVER_MAX_SAMPLES, DNS_RESOLVER_DEFAULT_MAX_SAMPLES);
if (mMinSamples < 0 || mMinSamples > mMaxSamples || mMaxSamples > 64) {
Log.w(TAG, "Invalid sample count (min, max)=(" + mMinSamples + ", " + mMaxSamples
+ "), using default=(" + DNS_RESOLVER_DEFAULT_MIN_SAMPLES + ", "
+ DNS_RESOLVER_DEFAULT_MAX_SAMPLES + ")");
mMinSamples = DNS_RESOLVER_DEFAULT_MIN_SAMPLES;
mMaxSamples = DNS_RESOLVER_DEFAULT_MAX_SAMPLES;
}
}
private int getIntSetting(String which, int dflt) {
return Settings.Global.getInt(mContentResolver, which, dflt);
}
/**
* Create a string array of host addresses from a collection of InetAddresses
*
* @param addrs a Collection of InetAddresses
* @return an array of Strings containing their host addresses
*/
private String[] makeStrings(Collection<InetAddress> addrs) {
String[] result = new String[addrs.size()];
int i = 0;
for (InetAddress addr : addrs) {
result[i++] = addr.getHostAddress();
}
return result;
}
private static String getStringSetting(ContentResolver cr, String which) {
return Settings.Global.getString(cr, which);
}
private static String[] getDomainStrings(String domains) {
return (TextUtils.isEmpty(domains)) ? new String[0] : domains.split(" ");
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,768 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.connectivity;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.net.NattSocketKeepalive.NATT_PORT;
import static android.net.NetworkAgent.CMD_START_SOCKET_KEEPALIVE;
import static android.net.SocketKeepalive.BINDER_DIED;
import static android.net.SocketKeepalive.DATA_RECEIVED;
import static android.net.SocketKeepalive.ERROR_INSUFFICIENT_RESOURCES;
import static android.net.SocketKeepalive.ERROR_INVALID_INTERVAL;
import static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS;
import static android.net.SocketKeepalive.ERROR_INVALID_NETWORK;
import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET;
import static android.net.SocketKeepalive.ERROR_NO_SUCH_SLOT;
import static android.net.SocketKeepalive.ERROR_STOP_REASON_UNINITIALIZED;
import static android.net.SocketKeepalive.ERROR_UNSUPPORTED;
import static android.net.SocketKeepalive.MAX_INTERVAL_SEC;
import static android.net.SocketKeepalive.MIN_INTERVAL_SEC;
import static android.net.SocketKeepalive.NO_KEEPALIVE;
import static android.net.SocketKeepalive.SUCCESS;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.net.ConnectivityResources;
import android.net.ISocketKeepaliveCallback;
import android.net.InetAddresses;
import android.net.InvalidPacketException;
import android.net.KeepalivePacketData;
import android.net.NattKeepalivePacketData;
import android.net.NetworkAgent;
import android.net.SocketKeepalive.InvalidSocketException;
import android.net.TcpKeepalivePacketData;
import android.net.util.KeepaliveUtils;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
import android.util.Pair;
import com.android.connectivity.resources.R;
import com.android.internal.util.IndentingPrintWriter;
import com.android.net.module.util.HexDump;
import com.android.net.module.util.IpUtils;
import java.io.FileDescriptor;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
/**
* Manages socket keepalive requests.
*
* Provides methods to stop and start keepalive requests, and keeps track of keepalives across all
* networks. This class is tightly coupled to ConnectivityService. It is not thread-safe and its
* handle* methods must be called only from the ConnectivityService handler thread.
*/
public class KeepaliveTracker {
private static final String TAG = "KeepaliveTracker";
private static final boolean DBG = false;
public static final String PERMISSION = android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
/** Keeps track of keepalive requests. */
private final HashMap <NetworkAgentInfo, HashMap<Integer, KeepaliveInfo>> mKeepalives =
new HashMap<> ();
private final Handler mConnectivityServiceHandler;
@NonNull
private final TcpKeepaliveController mTcpController;
@NonNull
private final Context mContext;
// Supported keepalive count for each transport type, can be configured through
// config_networkSupportedKeepaliveCount. For better error handling, use
// {@link getSupportedKeepalivesForNetworkCapabilities} instead of direct access.
@NonNull
private final int[] mSupportedKeepalives;
// Reserved privileged keepalive slots per transport. Caller's permission will be enforced if
// the number of remaining keepalive slots is less than or equal to the threshold.
private final int mReservedPrivilegedSlots;
// Allowed unprivileged keepalive slots per uid. Caller's permission will be enforced if
// the number of remaining keepalive slots is less than or equal to the threshold.
private final int mAllowedUnprivilegedSlotsForUid;
public KeepaliveTracker(Context context, Handler handler) {
mConnectivityServiceHandler = handler;
mTcpController = new TcpKeepaliveController(handler);
mContext = context;
mSupportedKeepalives = KeepaliveUtils.getSupportedKeepalives(mContext);
// TODO (b/183076074): stop reading legacy resources after migrating overlays
final int legacyReservedSlots = mContext.getResources().getInteger(
mContext.getResources().getIdentifier(
"config_reservedPrivilegedKeepaliveSlots", "integer", "android"));
final int legacyAllowedSlots = mContext.getResources().getInteger(
mContext.getResources().getIdentifier(
"config_allowedUnprivilegedKeepalivePerUid", "integer", "android"));
final ConnectivityResources res = new ConnectivityResources(mContext);
mReservedPrivilegedSlots = Math.min(legacyReservedSlots, res.get().getInteger(
R.integer.config_reservedPrivilegedKeepaliveSlots));
mAllowedUnprivilegedSlotsForUid = Math.min(legacyAllowedSlots, res.get().getInteger(
R.integer.config_allowedUnprivilegedKeepalivePerUid));
}
/**
* Tracks information about a socket keepalive.
*
* All information about this keepalive is known at construction time except the slot number,
* which is only returned when the hardware has successfully started the keepalive.
*/
class KeepaliveInfo implements IBinder.DeathRecipient {
// Bookkeeping data.
private final ISocketKeepaliveCallback mCallback;
private final int mUid;
private final int mPid;
private final boolean mPrivileged;
private final NetworkAgentInfo mNai;
private final int mType;
private final FileDescriptor mFd;
public static final int TYPE_NATT = 1;
public static final int TYPE_TCP = 2;
// Keepalive slot. A small integer that identifies this keepalive among the ones handled
// by this network.
private int mSlot = NO_KEEPALIVE;
// Packet data.
private final KeepalivePacketData mPacket;
private final int mInterval;
// Whether the keepalive is started or not. The initial state is NOT_STARTED.
private static final int NOT_STARTED = 1;
private static final int STARTING = 2;
private static final int STARTED = 3;
private static final int STOPPING = 4;
private int mStartedState = NOT_STARTED;
private int mStopReason = ERROR_STOP_REASON_UNINITIALIZED;
KeepaliveInfo(@NonNull ISocketKeepaliveCallback callback,
@NonNull NetworkAgentInfo nai,
@NonNull KeepalivePacketData packet,
int interval,
int type,
@Nullable FileDescriptor fd) throws InvalidSocketException {
mCallback = callback;
mPid = Binder.getCallingPid();
mUid = Binder.getCallingUid();
mPrivileged = (PERMISSION_GRANTED == mContext.checkPermission(PERMISSION, mPid, mUid));
mNai = nai;
mPacket = packet;
mInterval = interval;
mType = type;
// For SocketKeepalive, a dup of fd is kept in mFd so the source port from which the
// keepalives are sent cannot be reused by another app even if the fd gets closed by
// the user. A null is acceptable here for backward compatibility of PacketKeepalive
// API.
try {
if (fd != null) {
mFd = Os.dup(fd);
} else {
Log.d(TAG, toString() + " calls with null fd");
if (!mPrivileged) {
throw new SecurityException(
"null fd is not allowed for unprivileged access.");
}
if (mType == TYPE_TCP) {
throw new IllegalArgumentException(
"null fd is not allowed for tcp socket keepalives.");
}
mFd = null;
}
} catch (ErrnoException e) {
Log.e(TAG, "Cannot dup fd: ", e);
throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
}
try {
mCallback.asBinder().linkToDeath(this, 0);
} catch (RemoteException e) {
binderDied();
}
}
public NetworkAgentInfo getNai() {
return mNai;
}
private String startedStateString(final int state) {
switch (state) {
case NOT_STARTED : return "NOT_STARTED";
case STARTING : return "STARTING";
case STARTED : return "STARTED";
case STOPPING : return "STOPPING";
}
throw new IllegalArgumentException("Unknown state");
}
public String toString() {
return "KeepaliveInfo ["
+ " type=" + mType
+ " network=" + mNai.network
+ " startedState=" + startedStateString(mStartedState)
+ " "
+ IpUtils.addressAndPortToString(mPacket.getSrcAddress(), mPacket.getSrcPort())
+ "->"
+ IpUtils.addressAndPortToString(mPacket.getDstAddress(), mPacket.getDstPort())
+ " interval=" + mInterval
+ " uid=" + mUid + " pid=" + mPid + " privileged=" + mPrivileged
+ " packetData=" + HexDump.toHexString(mPacket.getPacket())
+ " ]";
}
/** Called when the application process is killed. */
public void binderDied() {
stop(BINDER_DIED);
}
void unlinkDeathRecipient() {
if (mCallback != null) {
mCallback.asBinder().unlinkToDeath(this, 0);
}
}
private int checkNetworkConnected() {
if (!mNai.networkInfo.isConnectedOrConnecting()) {
return ERROR_INVALID_NETWORK;
}
return SUCCESS;
}
private int checkSourceAddress() {
// Check that we have the source address.
for (InetAddress address : mNai.linkProperties.getAddresses()) {
if (address.equals(mPacket.getSrcAddress())) {
return SUCCESS;
}
}
return ERROR_INVALID_IP_ADDRESS;
}
private int checkInterval() {
if (mInterval < MIN_INTERVAL_SEC || mInterval > MAX_INTERVAL_SEC) {
return ERROR_INVALID_INTERVAL;
}
return SUCCESS;
}
private int checkPermission() {
final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(mNai);
if (networkKeepalives == null) {
return ERROR_INVALID_NETWORK;
}
if (mPrivileged) return SUCCESS;
final int supported = KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(
mSupportedKeepalives, mNai.networkCapabilities);
int takenUnprivilegedSlots = 0;
for (final KeepaliveInfo ki : networkKeepalives.values()) {
if (!ki.mPrivileged) ++takenUnprivilegedSlots;
}
if (takenUnprivilegedSlots > supported - mReservedPrivilegedSlots) {
return ERROR_INSUFFICIENT_RESOURCES;
}
// Count unprivileged keepalives for the same uid across networks.
int unprivilegedCountSameUid = 0;
for (final HashMap<Integer, KeepaliveInfo> kaForNetwork : mKeepalives.values()) {
for (final KeepaliveInfo ki : kaForNetwork.values()) {
if (ki.mUid == mUid) {
unprivilegedCountSameUid++;
}
}
}
if (unprivilegedCountSameUid > mAllowedUnprivilegedSlotsForUid) {
return ERROR_INSUFFICIENT_RESOURCES;
}
return SUCCESS;
}
private int checkLimit() {
final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(mNai);
if (networkKeepalives == null) {
return ERROR_INVALID_NETWORK;
}
final int supported = KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(
mSupportedKeepalives, mNai.networkCapabilities);
if (supported == 0) return ERROR_UNSUPPORTED;
if (networkKeepalives.size() > supported) return ERROR_INSUFFICIENT_RESOURCES;
return SUCCESS;
}
private int isValid() {
synchronized (mNai) {
int error = checkInterval();
if (error == SUCCESS) error = checkLimit();
if (error == SUCCESS) error = checkPermission();
if (error == SUCCESS) error = checkNetworkConnected();
if (error == SUCCESS) error = checkSourceAddress();
return error;
}
}
void start(int slot) {
mSlot = slot;
int error = isValid();
if (error == SUCCESS) {
Log.d(TAG, "Starting keepalive " + mSlot + " on " + mNai.toShortString());
switch (mType) {
case TYPE_NATT:
final NattKeepalivePacketData nattData = (NattKeepalivePacketData) mPacket;
mNai.onAddNattKeepalivePacketFilter(slot, nattData);
mNai.onStartNattSocketKeepalive(slot, mInterval, nattData);
break;
case TYPE_TCP:
try {
mTcpController.startSocketMonitor(mFd, this, mSlot);
} catch (InvalidSocketException e) {
handleStopKeepalive(mNai, mSlot, ERROR_INVALID_SOCKET);
return;
}
final TcpKeepalivePacketData tcpData = (TcpKeepalivePacketData) mPacket;
mNai.onAddTcpKeepalivePacketFilter(slot, tcpData);
// TODO: check result from apf and notify of failure as needed.
mNai.onStartTcpSocketKeepalive(slot, mInterval, tcpData);
break;
default:
Log.wtf(TAG, "Starting keepalive with unknown type: " + mType);
handleStopKeepalive(mNai, mSlot, error);
return;
}
mStartedState = STARTING;
} else {
handleStopKeepalive(mNai, mSlot, error);
return;
}
}
void stop(int reason) {
int uid = Binder.getCallingUid();
if (uid != mUid && uid != Process.SYSTEM_UID) {
if (DBG) {
Log.e(TAG, "Cannot stop unowned keepalive " + mSlot + " on " + mNai.network);
}
}
// Ignore the case when the network disconnects immediately after stop() has been
// called and the keepalive code is waiting for the response from the modem. This
// might happen when the caller listens for a lower-layer network disconnect
// callback and stop the keepalive at that time. But the stop() races with the
// stop() generated in ConnectivityService network disconnection code path.
if (mStartedState == STOPPING && reason == ERROR_INVALID_NETWORK) return;
// Store the reason of stopping, and report it after the keepalive is fully stopped.
if (mStopReason != ERROR_STOP_REASON_UNINITIALIZED) {
throw new IllegalStateException("Unexpected stop reason: " + mStopReason);
}
mStopReason = reason;
Log.d(TAG, "Stopping keepalive " + mSlot + " on " + mNai.toShortString()
+ ": " + reason);
switch (mStartedState) {
case NOT_STARTED:
// Remove the reference of the keepalive that meet error before starting,
// e.g. invalid parameter.
cleanupStoppedKeepalive(mNai, mSlot);
break;
default:
mStartedState = STOPPING;
switch (mType) {
case TYPE_TCP:
mTcpController.stopSocketMonitor(mSlot);
// fall through
case TYPE_NATT:
mNai.onStopSocketKeepalive(mSlot);
mNai.onRemoveKeepalivePacketFilter(mSlot);
break;
default:
Log.wtf(TAG, "Stopping keepalive with unknown type: " + mType);
}
}
// Close the duplicated fd that maintains the lifecycle of socket whenever
// keepalive is running.
if (mFd != null) {
try {
Os.close(mFd);
} catch (ErrnoException e) {
// This should not happen since system server controls the lifecycle of fd when
// keepalive offload is running.
Log.wtf(TAG, "Error closing fd for keepalive " + mSlot + ": " + e);
}
}
}
void onFileDescriptorInitiatedStop(final int socketKeepaliveReason) {
handleStopKeepalive(mNai, mSlot, socketKeepaliveReason);
}
}
void notifyErrorCallback(ISocketKeepaliveCallback cb, int error) {
if (DBG) Log.w(TAG, "Sending onError(" + error + ") callback");
try {
cb.onError(error);
} catch (RemoteException e) {
Log.w(TAG, "Discarded onError(" + error + ") callback");
}
}
private int findFirstFreeSlot(NetworkAgentInfo nai) {
HashMap networkKeepalives = mKeepalives.get(nai);
if (networkKeepalives == null) {
networkKeepalives = new HashMap<Integer, KeepaliveInfo>();
mKeepalives.put(nai, networkKeepalives);
}
// Find the lowest-numbered free slot. Slot numbers start from 1, because that's what two
// separate chipset implementations independently came up with.
int slot;
for (slot = 1; slot <= networkKeepalives.size(); slot++) {
if (networkKeepalives.get(slot) == null) {
return slot;
}
}
return slot;
}
public void handleStartKeepalive(Message message) {
KeepaliveInfo ki = (KeepaliveInfo) message.obj;
NetworkAgentInfo nai = ki.getNai();
int slot = findFirstFreeSlot(nai);
mKeepalives.get(nai).put(slot, ki);
ki.start(slot);
}
public void handleStopAllKeepalives(NetworkAgentInfo nai, int reason) {
final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
if (networkKeepalives != null) {
final ArrayList<KeepaliveInfo> kalist = new ArrayList(networkKeepalives.values());
for (KeepaliveInfo ki : kalist) {
ki.stop(reason);
// Clean up keepalives since the network agent is disconnected and unable to pass
// back asynchronous result of stop().
cleanupStoppedKeepalive(nai, ki.mSlot);
}
}
}
public void handleStopKeepalive(NetworkAgentInfo nai, int slot, int reason) {
final String networkName = NetworkAgentInfo.toShortString(nai);
HashMap <Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
if (networkKeepalives == null) {
Log.e(TAG, "Attempt to stop keepalive on nonexistent network " + networkName);
return;
}
KeepaliveInfo ki = networkKeepalives.get(slot);
if (ki == null) {
Log.e(TAG, "Attempt to stop nonexistent keepalive " + slot + " on " + networkName);
return;
}
ki.stop(reason);
// Clean up keepalives will be done as a result of calling ki.stop() after the slots are
// freed.
}
private void cleanupStoppedKeepalive(NetworkAgentInfo nai, int slot) {
final String networkName = NetworkAgentInfo.toShortString(nai);
HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
if (networkKeepalives == null) {
Log.e(TAG, "Attempt to remove keepalive on nonexistent network " + networkName);
return;
}
KeepaliveInfo ki = networkKeepalives.get(slot);
if (ki == null) {
Log.e(TAG, "Attempt to remove nonexistent keepalive " + slot + " on " + networkName);
return;
}
// Remove the keepalive from hash table so the slot can be considered available when reusing
// it.
networkKeepalives.remove(slot);
Log.d(TAG, "Remove keepalive " + slot + " on " + networkName + ", "
+ networkKeepalives.size() + " remains.");
if (networkKeepalives.isEmpty()) {
mKeepalives.remove(nai);
}
// Notify app that the keepalive is stopped.
final int reason = ki.mStopReason;
if (reason == SUCCESS) {
try {
ki.mCallback.onStopped();
} catch (RemoteException e) {
Log.w(TAG, "Discarded onStop callback: " + reason);
}
} else if (reason == DATA_RECEIVED) {
try {
ki.mCallback.onDataReceived();
} catch (RemoteException e) {
Log.w(TAG, "Discarded onDataReceived callback: " + reason);
}
} else if (reason == ERROR_STOP_REASON_UNINITIALIZED) {
throw new IllegalStateException("Unexpected stop reason: " + reason);
} else if (reason == ERROR_NO_SUCH_SLOT) {
throw new IllegalStateException("No such slot: " + reason);
} else {
notifyErrorCallback(ki.mCallback, reason);
}
ki.unlinkDeathRecipient();
}
public void handleCheckKeepalivesStillValid(NetworkAgentInfo nai) {
HashMap <Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
if (networkKeepalives != null) {
ArrayList<Pair<Integer, Integer>> invalidKeepalives = new ArrayList<>();
for (int slot : networkKeepalives.keySet()) {
int error = networkKeepalives.get(slot).isValid();
if (error != SUCCESS) {
invalidKeepalives.add(Pair.create(slot, error));
}
}
for (Pair<Integer, Integer> slotAndError: invalidKeepalives) {
handleStopKeepalive(nai, slotAndError.first, slotAndError.second);
}
}
}
/** Handle keepalive events from lower layer. */
public void handleEventSocketKeepalive(@NonNull NetworkAgentInfo nai, int slot, int reason) {
KeepaliveInfo ki = null;
try {
ki = mKeepalives.get(nai).get(slot);
} catch(NullPointerException e) {}
if (ki == null) {
Log.e(TAG, "Event " + NetworkAgent.EVENT_SOCKET_KEEPALIVE + "," + slot + "," + reason
+ " for unknown keepalive " + slot + " on " + nai.toShortString());
return;
}
// This can be called in a number of situations :
// - startedState is STARTING.
// - reason is SUCCESS => go to STARTED.
// - reason isn't SUCCESS => it's an error starting. Go to NOT_STARTED and stop keepalive.
// - startedState is STARTED.
// - reason is SUCCESS => it's a success stopping. Go to NOT_STARTED and stop keepalive.
// - reason isn't SUCCESS => it's an error in exec. Go to NOT_STARTED and stop keepalive.
// The control is not supposed to ever come here if the state is NOT_STARTED. This is
// because in NOT_STARTED state, the code will switch to STARTING before sending messages
// to start, and the only way to NOT_STARTED is this function, through the edges outlined
// above : in all cases, keepalive gets stopped and can't restart without going into
// STARTING as messages are ordered. This also depends on the hardware processing the
// messages in order.
// TODO : clarify this code and get rid of mStartedState. Using a StateMachine is an
// option.
if (KeepaliveInfo.STARTING == ki.mStartedState) {
if (SUCCESS == reason) {
// Keepalive successfully started.
Log.d(TAG, "Started keepalive " + slot + " on " + nai.toShortString());
ki.mStartedState = KeepaliveInfo.STARTED;
try {
ki.mCallback.onStarted(slot);
} catch (RemoteException e) {
Log.w(TAG, "Discarded onStarted(" + slot + ") callback");
}
} else {
Log.d(TAG, "Failed to start keepalive " + slot + " on " + nai.toShortString()
+ ": " + reason);
// The message indicated some error trying to start: do call handleStopKeepalive.
handleStopKeepalive(nai, slot, reason);
}
} else if (KeepaliveInfo.STOPPING == ki.mStartedState) {
// The message indicated result of stopping : clean up keepalive slots.
Log.d(TAG, "Stopped keepalive " + slot + " on " + nai.toShortString()
+ " stopped: " + reason);
ki.mStartedState = KeepaliveInfo.NOT_STARTED;
cleanupStoppedKeepalive(nai, slot);
} else {
Log.wtf(TAG, "Event " + NetworkAgent.EVENT_SOCKET_KEEPALIVE + "," + slot + "," + reason
+ " for keepalive in wrong state: " + ki.toString());
}
}
/**
* Called when requesting that keepalives be started on a IPsec NAT-T socket. See
* {@link android.net.SocketKeepalive}.
**/
public void startNattKeepalive(@Nullable NetworkAgentInfo nai,
@Nullable FileDescriptor fd,
int intervalSeconds,
@NonNull ISocketKeepaliveCallback cb,
@NonNull String srcAddrString,
int srcPort,
@NonNull String dstAddrString,
int dstPort) {
if (nai == null) {
notifyErrorCallback(cb, ERROR_INVALID_NETWORK);
return;
}
InetAddress srcAddress, dstAddress;
try {
srcAddress = InetAddresses.parseNumericAddress(srcAddrString);
dstAddress = InetAddresses.parseNumericAddress(dstAddrString);
} catch (IllegalArgumentException e) {
notifyErrorCallback(cb, ERROR_INVALID_IP_ADDRESS);
return;
}
KeepalivePacketData packet;
try {
packet = NattKeepalivePacketData.nattKeepalivePacket(
srcAddress, srcPort, dstAddress, NATT_PORT);
} catch (InvalidPacketException e) {
notifyErrorCallback(cb, e.getError());
return;
}
KeepaliveInfo ki = null;
try {
ki = new KeepaliveInfo(cb, nai, packet, intervalSeconds,
KeepaliveInfo.TYPE_NATT, fd);
} catch (InvalidSocketException | IllegalArgumentException | SecurityException e) {
Log.e(TAG, "Fail to construct keepalive", e);
notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
return;
}
Log.d(TAG, "Created keepalive: " + ki.toString());
mConnectivityServiceHandler.obtainMessage(
NetworkAgent.CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget();
}
/**
* Called by ConnectivityService to start TCP keepalive on a file descriptor.
*
* In order to offload keepalive for application correctly, sequence number, ack number and
* other fields are needed to form the keepalive packet. Thus, this function synchronously
* puts the socket into repair mode to get the necessary information. After the socket has been
* put into repair mode, the application cannot access the socket until reverted to normal.
*
* See {@link android.net.SocketKeepalive}.
**/
public void startTcpKeepalive(@Nullable NetworkAgentInfo nai,
@NonNull FileDescriptor fd,
int intervalSeconds,
@NonNull ISocketKeepaliveCallback cb) {
if (nai == null) {
notifyErrorCallback(cb, ERROR_INVALID_NETWORK);
return;
}
final TcpKeepalivePacketData packet;
try {
packet = TcpKeepaliveController.getTcpKeepalivePacket(fd);
} catch (InvalidSocketException e) {
notifyErrorCallback(cb, e.error);
return;
} catch (InvalidPacketException e) {
notifyErrorCallback(cb, e.getError());
return;
}
KeepaliveInfo ki = null;
try {
ki = new KeepaliveInfo(cb, nai, packet, intervalSeconds,
KeepaliveInfo.TYPE_TCP, fd);
} catch (InvalidSocketException | IllegalArgumentException | SecurityException e) {
Log.e(TAG, "Fail to construct keepalive e=" + e);
notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
return;
}
Log.d(TAG, "Created keepalive: " + ki.toString());
mConnectivityServiceHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget();
}
/**
* Called when requesting that keepalives be started on a IPsec NAT-T socket. This function is
* identical to {@link #startNattKeepalive}, but also takes a {@code resourceId}, which is the
* resource index bound to the {@link UdpEncapsulationSocket} when creating by
* {@link com.android.server.IpSecService} to verify whether the given
* {@link UdpEncapsulationSocket} is legitimate.
**/
public void startNattKeepalive(@Nullable NetworkAgentInfo nai,
@Nullable FileDescriptor fd,
int resourceId,
int intervalSeconds,
@NonNull ISocketKeepaliveCallback cb,
@NonNull String srcAddrString,
@NonNull String dstAddrString,
int dstPort) {
// Ensure that the socket is created by IpSecService.
if (!isNattKeepaliveSocketValid(fd, resourceId)) {
notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
}
// Get src port to adopt old API.
int srcPort = 0;
try {
final SocketAddress srcSockAddr = Os.getsockname(fd);
srcPort = ((InetSocketAddress) srcSockAddr).getPort();
} catch (ErrnoException e) {
notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
}
// Forward request to old API.
startNattKeepalive(nai, fd, intervalSeconds, cb, srcAddrString, srcPort,
dstAddrString, dstPort);
}
/**
* Verify if the IPsec NAT-T file descriptor and resource Id hold for IPsec keepalive is valid.
**/
public static boolean isNattKeepaliveSocketValid(@Nullable FileDescriptor fd, int resourceId) {
// TODO: 1. confirm whether the fd is called from system api or created by IpSecService.
// 2. If the fd is created from the system api, check that it's bounded. And
// call dup to keep the fd open.
// 3. If the fd is created from IpSecService, check if the resource ID is valid. And
// hold the resource needed in IpSecService.
if (null == fd) {
return false;
}
return true;
}
public void dump(IndentingPrintWriter pw) {
pw.println("Supported Socket keepalives: " + Arrays.toString(mSupportedKeepalives));
pw.println("Reserved Privileged keepalives: " + mReservedPrivilegedSlots);
pw.println("Allowed Unprivileged keepalives per uid: " + mAllowedUnprivilegedSlotsForUid);
pw.println("Socket keepalives:");
pw.increaseIndent();
for (NetworkAgentInfo nai : mKeepalives.keySet()) {
pw.println(nai.toShortString());
pw.increaseIndent();
for (int slot : mKeepalives.get(nai).keySet()) {
KeepaliveInfo ki = mKeepalives.get(nai).get(slot);
pw.println(slot + ": " + ki.toString());
}
pw.decreaseIndent();
}
pw.decreaseIndent();
}
}

View File

@@ -0,0 +1,330 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.connectivity;
import static android.net.ConnectivityManager.NETID_UNSET;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.net.ConnectivityResources;
import android.net.NetworkCapabilities;
import android.os.SystemClock;
import android.os.UserHandle;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import com.android.connectivity.resources.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.MessageUtils;
import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
import java.util.Arrays;
import java.util.HashMap;
/**
* Class that monitors default network linger events and possibly notifies the user of network
* switches.
*
* This class is not thread-safe and all its methods must be called on the ConnectivityService
* handler thread.
*/
public class LingerMonitor {
private static final boolean DBG = true;
private static final boolean VDBG = false;
private static final String TAG = LingerMonitor.class.getSimpleName();
public static final int DEFAULT_NOTIFICATION_DAILY_LIMIT = 3;
public static final long DEFAULT_NOTIFICATION_RATE_LIMIT_MILLIS = DateUtils.MINUTE_IN_MILLIS;
private static final HashMap<String, Integer> TRANSPORT_NAMES = makeTransportToNameMap();
@VisibleForTesting
public static final Intent CELLULAR_SETTINGS = new Intent().setComponent(new ComponentName(
"com.android.settings", "com.android.settings.Settings$DataUsageSummaryActivity"));
@VisibleForTesting
public static final int NOTIFY_TYPE_NONE = 0;
public static final int NOTIFY_TYPE_NOTIFICATION = 1;
public static final int NOTIFY_TYPE_TOAST = 2;
private static SparseArray<String> sNotifyTypeNames = MessageUtils.findMessageNames(
new Class[] { LingerMonitor.class }, new String[]{ "NOTIFY_TYPE_" });
private final Context mContext;
final Resources mResources;
private final NetworkNotificationManager mNotifier;
private final int mDailyLimit;
private final long mRateLimitMillis;
private long mFirstNotificationMillis;
private long mLastNotificationMillis;
private int mNotificationCounter;
/** Current notifications. Maps the netId we switched away from to the netId we switched to. */
private final SparseIntArray mNotifications = new SparseIntArray();
/** Whether we ever notified that we switched away from a particular network. */
private final SparseBooleanArray mEverNotified = new SparseBooleanArray();
public LingerMonitor(Context context, NetworkNotificationManager notifier,
int dailyLimit, long rateLimitMillis) {
mContext = context;
mResources = new ConnectivityResources(mContext).get();
mNotifier = notifier;
mDailyLimit = dailyLimit;
mRateLimitMillis = rateLimitMillis;
// Ensure that (now - mLastNotificationMillis) >= rateLimitMillis at first
mLastNotificationMillis = -rateLimitMillis;
}
private static HashMap<String, Integer> makeTransportToNameMap() {
SparseArray<String> numberToName = MessageUtils.findMessageNames(
new Class[] { NetworkCapabilities.class }, new String[]{ "TRANSPORT_" });
HashMap<String, Integer> nameToNumber = new HashMap<>();
for (int i = 0; i < numberToName.size(); i++) {
// MessageUtils will fail to initialize if there are duplicate constant values, so there
// are no duplicates here.
nameToNumber.put(numberToName.valueAt(i), numberToName.keyAt(i));
}
return nameToNumber;
}
private static boolean hasTransport(NetworkAgentInfo nai, int transport) {
return nai.networkCapabilities.hasTransport(transport);
}
private int getNotificationSource(NetworkAgentInfo toNai) {
for (int i = 0; i < mNotifications.size(); i++) {
if (mNotifications.valueAt(i) == toNai.network.getNetId()) {
return mNotifications.keyAt(i);
}
}
return NETID_UNSET;
}
private boolean everNotified(NetworkAgentInfo nai) {
return mEverNotified.get(nai.network.getNetId(), false);
}
@VisibleForTesting
public boolean isNotificationEnabled(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
// TODO: Evaluate moving to CarrierConfigManager.
String[] notifySwitches = mResources.getStringArray(R.array.config_networkNotifySwitches);
if (VDBG) {
Log.d(TAG, "Notify on network switches: " + Arrays.toString(notifySwitches));
}
for (String notifySwitch : notifySwitches) {
if (TextUtils.isEmpty(notifySwitch)) continue;
String[] transports = notifySwitch.split("-", 2);
if (transports.length != 2) {
Log.e(TAG, "Invalid network switch notification configuration: " + notifySwitch);
continue;
}
int fromTransport = TRANSPORT_NAMES.get("TRANSPORT_" + transports[0]);
int toTransport = TRANSPORT_NAMES.get("TRANSPORT_" + transports[1]);
if (hasTransport(fromNai, fromTransport) && hasTransport(toNai, toTransport)) {
return true;
}
}
return false;
}
private void showNotification(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
mNotifier.showNotification(fromNai.network.getNetId(), NotificationType.NETWORK_SWITCH,
fromNai, toNai, createNotificationIntent(), true);
}
@VisibleForTesting
protected PendingIntent createNotificationIntent() {
return PendingIntent.getActivity(
mContext.createContextAsUser(UserHandle.CURRENT, 0 /* flags */),
0 /* requestCode */,
CELLULAR_SETTINGS,
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
}
// Removes any notification that was put up as a result of switching to nai.
private void maybeStopNotifying(NetworkAgentInfo nai) {
int fromNetId = getNotificationSource(nai);
if (fromNetId != NETID_UNSET) {
mNotifications.delete(fromNetId);
mNotifier.clearNotification(fromNetId);
// Toasts can't be deleted.
}
}
// Notify the user of a network switch using a notification or a toast.
private void notify(NetworkAgentInfo fromNai, NetworkAgentInfo toNai, boolean forceToast) {
int notifyType = mResources.getInteger(R.integer.config_networkNotifySwitchType);
if (notifyType == NOTIFY_TYPE_NOTIFICATION && forceToast) {
notifyType = NOTIFY_TYPE_TOAST;
}
if (VDBG) {
Log.d(TAG, "Notify type: " + sNotifyTypeNames.get(notifyType, "" + notifyType));
}
switch (notifyType) {
case NOTIFY_TYPE_NONE:
return;
case NOTIFY_TYPE_NOTIFICATION:
showNotification(fromNai, toNai);
break;
case NOTIFY_TYPE_TOAST:
mNotifier.showToast(fromNai, toNai);
break;
default:
Log.e(TAG, "Unknown notify type " + notifyType);
return;
}
if (DBG) {
Log.d(TAG, "Notifying switch from=" + fromNai.toShortString()
+ " to=" + toNai.toShortString()
+ " type=" + sNotifyTypeNames.get(notifyType, "unknown(" + notifyType + ")"));
}
mNotifications.put(fromNai.network.getNetId(), toNai.network.getNetId());
mEverNotified.put(fromNai.network.getNetId(), true);
}
/**
* Put up or dismiss a notification or toast for of a change in the default network if needed.
*
* Putting up a notification when switching from no network to some network is not supported
* and as such this method can't be called with a null |fromNai|. It can be called with a
* null |toNai| if there isn't a default network any more.
*
* @param fromNai switching from this NAI
* @param toNai switching to this NAI
*/
// The default network changed from fromNai to toNai due to a change in score.
public void noteLingerDefaultNetwork(@NonNull final NetworkAgentInfo fromNai,
@Nullable final NetworkAgentInfo toNai) {
if (VDBG) {
Log.d(TAG, "noteLingerDefaultNetwork from=" + fromNai.toShortString()
+ " everValidated=" + fromNai.everValidated
+ " lastValidated=" + fromNai.lastValidated
+ " to=" + toNai.toShortString());
}
// If we are currently notifying the user because the device switched to fromNai, now that
// we are switching away from it we should remove the notification. This includes the case
// where we switch back to toNai because its score improved again (e.g., because it regained
// Internet access).
maybeStopNotifying(fromNai);
// If the network was simply lost (either because it disconnected or because it stopped
// being the default with no replacement), then don't show a notification.
if (null == toNai) return;
// If this network never validated, don't notify. Otherwise, we could do things like:
//
// 1. Unvalidated wifi connects.
// 2. Unvalidated mobile data connects.
// 3. Cell validates, and we show a notification.
// or:
// 1. User connects to wireless printer.
// 2. User turns on cellular data.
// 3. We show a notification.
if (!fromNai.everValidated) return;
// If this network is a captive portal, don't notify. This cannot happen on initial connect
// to a captive portal, because the everValidated check above will fail. However, it can
// happen if the captive portal reasserts itself (e.g., because its timeout fires). In that
// case, as soon as the captive portal reasserts itself, we'll show a sign-in notification.
// We don't want to overwrite that notification with this one; the user has already been
// notified, and of the two, the captive portal notification is the more useful one because
// it allows the user to sign in to the captive portal. In this case, display a toast
// in addition to the captive portal notification.
//
// Note that if the network we switch to is already up when the captive portal reappears,
// this won't work because NetworkMonitor tells ConnectivityService that the network is
// unvalidated (causing a switch) before asking it to show the sign in notification. In this
// case, the toast won't show and we'll only display the sign in notification. This is the
// best we can do at this time.
boolean forceToast = fromNai.networkCapabilities.hasCapability(
NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL);
// Only show the notification once, in order to avoid irritating the user every time.
// TODO: should we do this?
if (everNotified(fromNai)) {
if (VDBG) {
Log.d(TAG, "Not notifying handover from " + fromNai.toShortString()
+ ", already notified");
}
return;
}
// Only show the notification if we switched away because a network became unvalidated, not
// because its score changed.
// TODO: instead of just skipping notification, keep a note of it, and show it if it becomes
// unvalidated.
if (fromNai.lastValidated) return;
if (!isNotificationEnabled(fromNai, toNai)) return;
final long now = SystemClock.elapsedRealtime();
if (isRateLimited(now) || isAboveDailyLimit(now)) return;
notify(fromNai, toNai, forceToast);
}
public void noteDisconnect(NetworkAgentInfo nai) {
mNotifications.delete(nai.network.getNetId());
mEverNotified.delete(nai.network.getNetId());
maybeStopNotifying(nai);
// No need to cancel notifications on nai: NetworkMonitor does that on disconnect.
}
private boolean isRateLimited(long now) {
final long millisSinceLast = now - mLastNotificationMillis;
if (millisSinceLast < mRateLimitMillis) {
return true;
}
mLastNotificationMillis = now;
return false;
}
private boolean isAboveDailyLimit(long now) {
if (mFirstNotificationMillis == 0) {
mFirstNotificationMillis = now;
}
final long millisSinceFirst = now - mFirstNotificationMillis;
if (millisSinceFirst > DateUtils.DAY_IN_MILLIS) {
mNotificationCounter = 0;
mFirstNotificationMillis = 0;
}
if (mNotificationCounter >= mDailyLimit) {
return true;
}
mNotificationCounter++;
return false;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,757 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.connectivity;
import static android.system.OsConstants.*;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.InetAddresses;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.RouteInfo;
import android.net.TrafficStats;
import android.net.shared.PrivateDnsConfig;
import android.net.util.NetworkConstants;
import android.os.SystemClock;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructTimeval;
import android.text.TextUtils;
import android.util.Pair;
import com.android.internal.util.IndentingPrintWriter;
import com.android.net.module.util.NetworkStackConstants;
import libcore.io.IoUtils;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
/**
* NetworkDiagnostics
*
* A simple class to diagnose network connectivity fundamentals. Current
* checks performed are:
* - ICMPv4/v6 echo requests for all routers
* - ICMPv4/v6 echo requests for all DNS servers
* - DNS UDP queries to all DNS servers
*
* Currently unimplemented checks include:
* - report ARP/ND data about on-link neighbors
* - DNS TCP queries to all DNS servers
* - HTTP DIRECT and PROXY checks
* - port 443 blocking/TLS intercept checks
* - QUIC reachability checks
* - MTU checks
*
* The supplied timeout bounds the entire diagnostic process. Each specific
* check class must implement this upper bound on measurements in whichever
* manner is most appropriate and effective.
*
* @hide
*/
public class NetworkDiagnostics {
private static final String TAG = "NetworkDiagnostics";
private static final InetAddress TEST_DNS4 = InetAddresses.parseNumericAddress("8.8.8.8");
private static final InetAddress TEST_DNS6 = InetAddresses.parseNumericAddress(
"2001:4860:4860::8888");
// For brevity elsewhere.
private static final long now() {
return SystemClock.elapsedRealtime();
}
// Values from RFC 1035 section 4.1.1, names from <arpa/nameser.h>.
// Should be a member of DnsUdpCheck, but "compiler says no".
public static enum DnsResponseCode { NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP, REFUSED };
private final Network mNetwork;
private final LinkProperties mLinkProperties;
private final PrivateDnsConfig mPrivateDnsCfg;
private final Integer mInterfaceIndex;
private final long mTimeoutMs;
private final long mStartTime;
private final long mDeadlineTime;
// A counter, initialized to the total number of measurements,
// so callers can wait for completion.
private final CountDownLatch mCountDownLatch;
public class Measurement {
private static final String SUCCEEDED = "SUCCEEDED";
private static final String FAILED = "FAILED";
private boolean succeeded;
// Package private. TODO: investigate better encapsulation.
String description = "";
long startTime;
long finishTime;
String result = "";
Thread thread;
public boolean checkSucceeded() { return succeeded; }
void recordSuccess(String msg) {
maybeFixupTimes();
succeeded = true;
result = SUCCEEDED + ": " + msg;
if (mCountDownLatch != null) {
mCountDownLatch.countDown();
}
}
void recordFailure(String msg) {
maybeFixupTimes();
succeeded = false;
result = FAILED + ": " + msg;
if (mCountDownLatch != null) {
mCountDownLatch.countDown();
}
}
private void maybeFixupTimes() {
// Allows the caller to just set success/failure and not worry
// about also setting the correct finishing time.
if (finishTime == 0) { finishTime = now(); }
// In cases where, for example, a failure has occurred before the
// measurement even began, fixup the start time to reflect as much.
if (startTime == 0) { startTime = finishTime; }
}
@Override
public String toString() {
return description + ": " + result + " (" + (finishTime - startTime) + "ms)";
}
}
private final Map<InetAddress, Measurement> mIcmpChecks = new HashMap<>();
private final Map<Pair<InetAddress, InetAddress>, Measurement> mExplicitSourceIcmpChecks =
new HashMap<>();
private final Map<InetAddress, Measurement> mDnsUdpChecks = new HashMap<>();
private final Map<InetAddress, Measurement> mDnsTlsChecks = new HashMap<>();
private final String mDescription;
public NetworkDiagnostics(Network network, LinkProperties lp,
@NonNull PrivateDnsConfig privateDnsCfg, long timeoutMs) {
mNetwork = network;
mLinkProperties = lp;
mPrivateDnsCfg = privateDnsCfg;
mInterfaceIndex = getInterfaceIndex(mLinkProperties.getInterfaceName());
mTimeoutMs = timeoutMs;
mStartTime = now();
mDeadlineTime = mStartTime + mTimeoutMs;
// Hardcode measurements to TEST_DNS4 and TEST_DNS6 in order to test off-link connectivity.
// We are free to modify mLinkProperties with impunity because ConnectivityService passes us
// a copy and not the original object. It's easier to do it this way because we don't need
// to check whether the LinkProperties already contains these DNS servers because
// LinkProperties#addDnsServer checks for duplicates.
if (mLinkProperties.isReachable(TEST_DNS4)) {
mLinkProperties.addDnsServer(TEST_DNS4);
}
// TODO: we could use mLinkProperties.isReachable(TEST_DNS6) here, because we won't set any
// DNS servers for which isReachable() is false, but since this is diagnostic code, be extra
// careful.
if (mLinkProperties.hasGlobalIpv6Address() || mLinkProperties.hasIpv6DefaultRoute()) {
mLinkProperties.addDnsServer(TEST_DNS6);
}
for (RouteInfo route : mLinkProperties.getRoutes()) {
if (route.hasGateway()) {
InetAddress gateway = route.getGateway();
prepareIcmpMeasurement(gateway);
if (route.isIPv6Default()) {
prepareExplicitSourceIcmpMeasurements(gateway);
}
}
}
for (InetAddress nameserver : mLinkProperties.getDnsServers()) {
prepareIcmpMeasurement(nameserver);
prepareDnsMeasurement(nameserver);
// Unlike the DnsResolver which doesn't do certificate validation in opportunistic mode,
// DoT probes to the DNS servers will fail if certificate validation fails.
prepareDnsTlsMeasurement(null /* hostname */, nameserver);
}
for (InetAddress tlsNameserver : mPrivateDnsCfg.ips) {
// Reachability check is necessary since when resolving the strict mode hostname,
// NetworkMonitor always queries for both A and AAAA records, even if the network
// is IPv4-only or IPv6-only.
if (mLinkProperties.isReachable(tlsNameserver)) {
// If there are IPs, there must have been a name that resolved to them.
prepareDnsTlsMeasurement(mPrivateDnsCfg.hostname, tlsNameserver);
}
}
mCountDownLatch = new CountDownLatch(totalMeasurementCount());
startMeasurements();
mDescription = "ifaces{" + TextUtils.join(",", mLinkProperties.getAllInterfaceNames()) + "}"
+ " index{" + mInterfaceIndex + "}"
+ " network{" + mNetwork + "}"
+ " nethandle{" + mNetwork.getNetworkHandle() + "}";
}
private static Integer getInterfaceIndex(String ifname) {
try {
NetworkInterface ni = NetworkInterface.getByName(ifname);
return ni.getIndex();
} catch (NullPointerException | SocketException e) {
return null;
}
}
private static String socketAddressToString(@NonNull SocketAddress sockAddr) {
// The default toString() implementation is not the prettiest.
InetSocketAddress inetSockAddr = (InetSocketAddress) sockAddr;
InetAddress localAddr = inetSockAddr.getAddress();
return String.format(
(localAddr instanceof Inet6Address ? "[%s]:%d" : "%s:%d"),
localAddr.getHostAddress(), inetSockAddr.getPort());
}
private void prepareIcmpMeasurement(InetAddress target) {
if (!mIcmpChecks.containsKey(target)) {
Measurement measurement = new Measurement();
measurement.thread = new Thread(new IcmpCheck(target, measurement));
mIcmpChecks.put(target, measurement);
}
}
private void prepareExplicitSourceIcmpMeasurements(InetAddress target) {
for (LinkAddress l : mLinkProperties.getLinkAddresses()) {
InetAddress source = l.getAddress();
if (source instanceof Inet6Address && l.isGlobalPreferred()) {
Pair<InetAddress, InetAddress> srcTarget = new Pair<>(source, target);
if (!mExplicitSourceIcmpChecks.containsKey(srcTarget)) {
Measurement measurement = new Measurement();
measurement.thread = new Thread(new IcmpCheck(source, target, measurement));
mExplicitSourceIcmpChecks.put(srcTarget, measurement);
}
}
}
}
private void prepareDnsMeasurement(InetAddress target) {
if (!mDnsUdpChecks.containsKey(target)) {
Measurement measurement = new Measurement();
measurement.thread = new Thread(new DnsUdpCheck(target, measurement));
mDnsUdpChecks.put(target, measurement);
}
}
private void prepareDnsTlsMeasurement(@Nullable String hostname, @NonNull InetAddress target) {
// This might overwrite an existing entry in mDnsTlsChecks, because |target| can be an IP
// address configured by the network as well as an IP address learned by resolving the
// strict mode DNS hostname. If the entry is overwritten, the overwritten measurement
// thread will not execute.
Measurement measurement = new Measurement();
measurement.thread = new Thread(new DnsTlsCheck(hostname, target, measurement));
mDnsTlsChecks.put(target, measurement);
}
private int totalMeasurementCount() {
return mIcmpChecks.size() + mExplicitSourceIcmpChecks.size() + mDnsUdpChecks.size()
+ mDnsTlsChecks.size();
}
private void startMeasurements() {
for (Measurement measurement : mIcmpChecks.values()) {
measurement.thread.start();
}
for (Measurement measurement : mExplicitSourceIcmpChecks.values()) {
measurement.thread.start();
}
for (Measurement measurement : mDnsUdpChecks.values()) {
measurement.thread.start();
}
for (Measurement measurement : mDnsTlsChecks.values()) {
measurement.thread.start();
}
}
public void waitForMeasurements() {
try {
mCountDownLatch.await(mDeadlineTime - now(), TimeUnit.MILLISECONDS);
} catch (InterruptedException ignored) {}
}
public List<Measurement> getMeasurements() {
// TODO: Consider moving waitForMeasurements() in here to minimize the
// chance of caller errors.
ArrayList<Measurement> measurements = new ArrayList(totalMeasurementCount());
// Sort measurements IPv4 first.
for (Map.Entry<InetAddress, Measurement> entry : mIcmpChecks.entrySet()) {
if (entry.getKey() instanceof Inet4Address) {
measurements.add(entry.getValue());
}
}
for (Map.Entry<Pair<InetAddress, InetAddress>, Measurement> entry :
mExplicitSourceIcmpChecks.entrySet()) {
if (entry.getKey().first instanceof Inet4Address) {
measurements.add(entry.getValue());
}
}
for (Map.Entry<InetAddress, Measurement> entry : mDnsUdpChecks.entrySet()) {
if (entry.getKey() instanceof Inet4Address) {
measurements.add(entry.getValue());
}
}
for (Map.Entry<InetAddress, Measurement> entry : mDnsTlsChecks.entrySet()) {
if (entry.getKey() instanceof Inet4Address) {
measurements.add(entry.getValue());
}
}
// IPv6 measurements second.
for (Map.Entry<InetAddress, Measurement> entry : mIcmpChecks.entrySet()) {
if (entry.getKey() instanceof Inet6Address) {
measurements.add(entry.getValue());
}
}
for (Map.Entry<Pair<InetAddress, InetAddress>, Measurement> entry :
mExplicitSourceIcmpChecks.entrySet()) {
if (entry.getKey().first instanceof Inet6Address) {
measurements.add(entry.getValue());
}
}
for (Map.Entry<InetAddress, Measurement> entry : mDnsUdpChecks.entrySet()) {
if (entry.getKey() instanceof Inet6Address) {
measurements.add(entry.getValue());
}
}
for (Map.Entry<InetAddress, Measurement> entry : mDnsTlsChecks.entrySet()) {
if (entry.getKey() instanceof Inet6Address) {
measurements.add(entry.getValue());
}
}
return measurements;
}
public void dump(IndentingPrintWriter pw) {
pw.println(TAG + ":" + mDescription);
final long unfinished = mCountDownLatch.getCount();
if (unfinished > 0) {
// This can't happen unless a caller forgets to call waitForMeasurements()
// or a measurement isn't implemented to correctly honor the timeout.
pw.println("WARNING: countdown wait incomplete: "
+ unfinished + " unfinished measurements");
}
pw.increaseIndent();
String prefix;
for (Measurement m : getMeasurements()) {
prefix = m.checkSucceeded() ? "." : "F";
pw.println(prefix + " " + m.toString());
}
pw.decreaseIndent();
}
private class SimpleSocketCheck implements Closeable {
protected final InetAddress mSource; // Usually null.
protected final InetAddress mTarget;
protected final int mAddressFamily;
protected final Measurement mMeasurement;
protected FileDescriptor mFileDescriptor;
protected SocketAddress mSocketAddress;
protected SimpleSocketCheck(
InetAddress source, InetAddress target, Measurement measurement) {
mMeasurement = measurement;
if (target instanceof Inet6Address) {
Inet6Address targetWithScopeId = null;
if (target.isLinkLocalAddress() && mInterfaceIndex != null) {
try {
targetWithScopeId = Inet6Address.getByAddress(
null, target.getAddress(), mInterfaceIndex);
} catch (UnknownHostException e) {
mMeasurement.recordFailure(e.toString());
}
}
mTarget = (targetWithScopeId != null) ? targetWithScopeId : target;
mAddressFamily = AF_INET6;
} else {
mTarget = target;
mAddressFamily = AF_INET;
}
// We don't need to check the scope ID here because we currently only do explicit-source
// measurements from global IPv6 addresses.
mSource = source;
}
protected SimpleSocketCheck(InetAddress target, Measurement measurement) {
this(null, target, measurement);
}
protected void setupSocket(
int sockType, int protocol, long writeTimeout, long readTimeout, int dstPort)
throws ErrnoException, IOException {
final int oldTag = TrafficStats.getAndSetThreadStatsTag(
NetworkStackConstants.TAG_SYSTEM_PROBE);
try {
mFileDescriptor = Os.socket(mAddressFamily, sockType, protocol);
} finally {
// TODO: The tag should remain set until all traffic is sent and received.
// Consider tagging the socket after the measurement thread is started.
TrafficStats.setThreadStatsTag(oldTag);
}
// Setting SNDTIMEO is purely for defensive purposes.
Os.setsockoptTimeval(mFileDescriptor,
SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(writeTimeout));
Os.setsockoptTimeval(mFileDescriptor,
SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(readTimeout));
// TODO: Use IP_RECVERR/IPV6_RECVERR, pending OsContants availability.
mNetwork.bindSocket(mFileDescriptor);
if (mSource != null) {
Os.bind(mFileDescriptor, mSource, 0);
}
Os.connect(mFileDescriptor, mTarget, dstPort);
mSocketAddress = Os.getsockname(mFileDescriptor);
}
protected boolean ensureMeasurementNecessary() {
if (mMeasurement.finishTime == 0) return false;
// Countdown latch was not decremented when the measurement failed during setup.
mCountDownLatch.countDown();
return true;
}
@Override
public void close() {
IoUtils.closeQuietly(mFileDescriptor);
}
}
private class IcmpCheck extends SimpleSocketCheck implements Runnable {
private static final int TIMEOUT_SEND = 100;
private static final int TIMEOUT_RECV = 300;
private static final int PACKET_BUFSIZE = 512;
private final int mProtocol;
private final int mIcmpType;
public IcmpCheck(InetAddress source, InetAddress target, Measurement measurement) {
super(source, target, measurement);
if (mAddressFamily == AF_INET6) {
mProtocol = IPPROTO_ICMPV6;
mIcmpType = NetworkConstants.ICMPV6_ECHO_REQUEST_TYPE;
mMeasurement.description = "ICMPv6";
} else {
mProtocol = IPPROTO_ICMP;
mIcmpType = NetworkConstants.ICMPV4_ECHO_REQUEST_TYPE;
mMeasurement.description = "ICMPv4";
}
mMeasurement.description += " dst{" + mTarget.getHostAddress() + "}";
}
public IcmpCheck(InetAddress target, Measurement measurement) {
this(null, target, measurement);
}
@Override
public void run() {
if (ensureMeasurementNecessary()) return;
try {
setupSocket(SOCK_DGRAM, mProtocol, TIMEOUT_SEND, TIMEOUT_RECV, 0);
} catch (ErrnoException | IOException e) {
mMeasurement.recordFailure(e.toString());
return;
}
mMeasurement.description += " src{" + socketAddressToString(mSocketAddress) + "}";
// Build a trivial ICMP packet.
final byte[] icmpPacket = {
(byte) mIcmpType, 0, 0, 0, 0, 0, 0, 0 // ICMP header
};
int count = 0;
mMeasurement.startTime = now();
while (now() < mDeadlineTime - (TIMEOUT_SEND + TIMEOUT_RECV)) {
count++;
icmpPacket[icmpPacket.length - 1] = (byte) count;
try {
Os.write(mFileDescriptor, icmpPacket, 0, icmpPacket.length);
} catch (ErrnoException | InterruptedIOException e) {
mMeasurement.recordFailure(e.toString());
break;
}
try {
ByteBuffer reply = ByteBuffer.allocate(PACKET_BUFSIZE);
Os.read(mFileDescriptor, reply);
// TODO: send a few pings back to back to guesstimate packet loss.
mMeasurement.recordSuccess("1/" + count);
break;
} catch (ErrnoException | InterruptedIOException e) {
continue;
}
}
if (mMeasurement.finishTime == 0) {
mMeasurement.recordFailure("0/" + count);
}
close();
}
}
private class DnsUdpCheck extends SimpleSocketCheck implements Runnable {
private static final int TIMEOUT_SEND = 100;
private static final int TIMEOUT_RECV = 500;
private static final int RR_TYPE_A = 1;
private static final int RR_TYPE_AAAA = 28;
private static final int PACKET_BUFSIZE = 512;
protected final Random mRandom = new Random();
// Should be static, but the compiler mocks our puny, human attempts at reason.
protected String responseCodeStr(int rcode) {
try {
return DnsResponseCode.values()[rcode].toString();
} catch (IndexOutOfBoundsException e) {
return String.valueOf(rcode);
}
}
protected final int mQueryType;
public DnsUdpCheck(InetAddress target, Measurement measurement) {
super(target, measurement);
// TODO: Ideally, query the target for both types regardless of address family.
if (mAddressFamily == AF_INET6) {
mQueryType = RR_TYPE_AAAA;
} else {
mQueryType = RR_TYPE_A;
}
mMeasurement.description = "DNS UDP dst{" + mTarget.getHostAddress() + "}";
}
@Override
public void run() {
if (ensureMeasurementNecessary()) return;
try {
setupSocket(SOCK_DGRAM, IPPROTO_UDP, TIMEOUT_SEND, TIMEOUT_RECV,
NetworkConstants.DNS_SERVER_PORT);
} catch (ErrnoException | IOException e) {
mMeasurement.recordFailure(e.toString());
return;
}
// This needs to be fixed length so it can be dropped into the pre-canned packet.
final String sixRandomDigits = String.valueOf(mRandom.nextInt(900000) + 100000);
appendDnsToMeasurementDescription(sixRandomDigits, mSocketAddress);
// Build a trivial DNS packet.
final byte[] dnsPacket = getDnsQueryPacket(sixRandomDigits);
int count = 0;
mMeasurement.startTime = now();
while (now() < mDeadlineTime - (TIMEOUT_RECV + TIMEOUT_RECV)) {
count++;
try {
Os.write(mFileDescriptor, dnsPacket, 0, dnsPacket.length);
} catch (ErrnoException | InterruptedIOException e) {
mMeasurement.recordFailure(e.toString());
break;
}
try {
ByteBuffer reply = ByteBuffer.allocate(PACKET_BUFSIZE);
Os.read(mFileDescriptor, reply);
// TODO: more correct and detailed evaluation of the response,
// possibly adding the returned IP address(es) to the output.
final String rcodeStr = (reply.limit() > 3)
? " " + responseCodeStr((int) (reply.get(3)) & 0x0f)
: "";
mMeasurement.recordSuccess("1/" + count + rcodeStr);
break;
} catch (ErrnoException | InterruptedIOException e) {
continue;
}
}
if (mMeasurement.finishTime == 0) {
mMeasurement.recordFailure("0/" + count);
}
close();
}
protected byte[] getDnsQueryPacket(String sixRandomDigits) {
byte[] rnd = sixRandomDigits.getBytes(StandardCharsets.US_ASCII);
return new byte[] {
(byte) mRandom.nextInt(), (byte) mRandom.nextInt(), // [0-1] query ID
1, 0, // [2-3] flags; byte[2] = 1 for recursion desired (RD).
0, 1, // [4-5] QDCOUNT (number of queries)
0, 0, // [6-7] ANCOUNT (number of answers)
0, 0, // [8-9] NSCOUNT (number of name server records)
0, 0, // [10-11] ARCOUNT (number of additional records)
17, rnd[0], rnd[1], rnd[2], rnd[3], rnd[4], rnd[5],
'-', 'a', 'n', 'd', 'r', 'o', 'i', 'd', '-', 'd', 's',
6, 'm', 'e', 't', 'r', 'i', 'c',
7, 'g', 's', 't', 'a', 't', 'i', 'c',
3, 'c', 'o', 'm',
0, // null terminator of FQDN (root TLD)
0, (byte) mQueryType, // QTYPE
0, 1 // QCLASS, set to 1 = IN (Internet)
};
}
protected void appendDnsToMeasurementDescription(
String sixRandomDigits, SocketAddress sockAddr) {
mMeasurement.description += " src{" + socketAddressToString(sockAddr) + "}"
+ " qtype{" + mQueryType + "}"
+ " qname{" + sixRandomDigits + "-android-ds.metric.gstatic.com}";
}
}
// TODO: Have it inherited from SimpleSocketCheck, and separate common DNS helpers out of
// DnsUdpCheck.
private class DnsTlsCheck extends DnsUdpCheck {
private static final int TCP_CONNECT_TIMEOUT_MS = 2500;
private static final int TCP_TIMEOUT_MS = 2000;
private static final int DNS_TLS_PORT = 853;
private static final int DNS_HEADER_SIZE = 12;
private final String mHostname;
public DnsTlsCheck(@Nullable String hostname, @NonNull InetAddress target,
@NonNull Measurement measurement) {
super(target, measurement);
mHostname = hostname;
mMeasurement.description = "DNS TLS dst{" + mTarget.getHostAddress() + "} hostname{"
+ (mHostname == null ? "" : mHostname) + "}";
}
private SSLSocket setupSSLSocket() throws IOException {
// A TrustManager will be created and initialized with a KeyStore containing system
// CaCerts. During SSL handshake, it will be used to validate the certificates from
// the server.
SSLSocket sslSocket = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
sslSocket.setSoTimeout(TCP_TIMEOUT_MS);
if (!TextUtils.isEmpty(mHostname)) {
// Set SNI.
final List<SNIServerName> names =
Collections.singletonList(new SNIHostName(mHostname));
SSLParameters params = sslSocket.getSSLParameters();
params.setServerNames(names);
sslSocket.setSSLParameters(params);
}
mNetwork.bindSocket(sslSocket);
return sslSocket;
}
private void sendDoTProbe(@Nullable SSLSocket sslSocket) throws IOException {
final String sixRandomDigits = String.valueOf(mRandom.nextInt(900000) + 100000);
final byte[] dnsPacket = getDnsQueryPacket(sixRandomDigits);
mMeasurement.startTime = now();
sslSocket.connect(new InetSocketAddress(mTarget, DNS_TLS_PORT), TCP_CONNECT_TIMEOUT_MS);
// Synchronous call waiting for the TLS handshake complete.
sslSocket.startHandshake();
appendDnsToMeasurementDescription(sixRandomDigits, sslSocket.getLocalSocketAddress());
final DataOutputStream output = new DataOutputStream(sslSocket.getOutputStream());
output.writeShort(dnsPacket.length);
output.write(dnsPacket, 0, dnsPacket.length);
final DataInputStream input = new DataInputStream(sslSocket.getInputStream());
final int replyLength = Short.toUnsignedInt(input.readShort());
final byte[] reply = new byte[replyLength];
int bytesRead = 0;
while (bytesRead < replyLength) {
bytesRead += input.read(reply, bytesRead, replyLength - bytesRead);
}
if (bytesRead > DNS_HEADER_SIZE && bytesRead == replyLength) {
mMeasurement.recordSuccess("1/1 " + responseCodeStr((int) (reply[3]) & 0x0f));
} else {
mMeasurement.recordFailure("1/1 Read " + bytesRead + " bytes while expected to be "
+ replyLength + " bytes");
}
}
@Override
public void run() {
if (ensureMeasurementNecessary()) return;
// No need to restore the tag, since this thread is only used for this measurement.
TrafficStats.getAndSetThreadStatsTag(NetworkStackConstants.TAG_SYSTEM_PROBE);
try (SSLSocket sslSocket = setupSSLSocket()) {
sendDoTProbe(sslSocket);
} catch (IOException e) {
mMeasurement.recordFailure(e.toString());
}
}
}
}

View File

@@ -0,0 +1,399 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.connectivity;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import android.annotation.NonNull;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.drawable.Icon;
import android.net.ConnectivityResources;
import android.net.NetworkSpecifier;
import android.net.TelephonyNetworkSpecifier;
import android.net.wifi.WifiInfo;
import android.os.UserHandle;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.widget.Toast;
import com.android.connectivity.resources.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
public class NetworkNotificationManager {
public static enum NotificationType {
LOST_INTERNET(SystemMessage.NOTE_NETWORK_LOST_INTERNET),
NETWORK_SWITCH(SystemMessage.NOTE_NETWORK_SWITCH),
NO_INTERNET(SystemMessage.NOTE_NETWORK_NO_INTERNET),
PARTIAL_CONNECTIVITY(SystemMessage.NOTE_NETWORK_PARTIAL_CONNECTIVITY),
SIGN_IN(SystemMessage.NOTE_NETWORK_SIGN_IN),
PRIVATE_DNS_BROKEN(SystemMessage.NOTE_NETWORK_PRIVATE_DNS_BROKEN);
public final int eventId;
NotificationType(int eventId) {
this.eventId = eventId;
Holder.sIdToTypeMap.put(eventId, this);
}
private static class Holder {
private static SparseArray<NotificationType> sIdToTypeMap = new SparseArray<>();
}
public static NotificationType getFromId(int id) {
return Holder.sIdToTypeMap.get(id);
}
};
private static final String TAG = NetworkNotificationManager.class.getSimpleName();
private static final boolean DBG = true;
// Notification channels used by ConnectivityService mainline module, it should be aligned with
// SystemNotificationChannels so the channels are the same as the ones used as the system
// server.
public static final String NOTIFICATION_CHANNEL_NETWORK_STATUS = "NETWORK_STATUS";
public static final String NOTIFICATION_CHANNEL_NETWORK_ALERTS = "NETWORK_ALERTS";
// The context is for the current user (system server)
private final Context mContext;
private final Resources mResources;
private final TelephonyManager mTelephonyManager;
// The notification manager is created from a context for User.ALL, so notifications
// will be sent to all users.
private final NotificationManager mNotificationManager;
// Tracks the types of notifications managed by this instance, from creation to cancellation.
private final SparseIntArray mNotificationTypeMap;
public NetworkNotificationManager(@NonNull final Context c, @NonNull final TelephonyManager t) {
mContext = c;
mTelephonyManager = t;
mNotificationManager =
(NotificationManager) c.createContextAsUser(UserHandle.ALL, 0 /* flags */)
.getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationTypeMap = new SparseIntArray();
mResources = new ConnectivityResources(mContext).get();
}
@VisibleForTesting
protected static int approximateTransportType(NetworkAgentInfo nai) {
return nai.isVPN() ? TRANSPORT_VPN : getFirstTransportType(nai);
}
// TODO: deal more gracefully with multi-transport networks.
private static int getFirstTransportType(NetworkAgentInfo nai) {
// TODO: The range is wrong, the safer and correct way is to change the range from
// MIN_TRANSPORT to MAX_TRANSPORT.
for (int i = 0; i < 64; i++) {
if (nai.networkCapabilities.hasTransport(i)) return i;
}
return -1;
}
private String getTransportName(final int transportType) {
String[] networkTypes = mResources.getStringArray(R.array.network_switch_type_name);
try {
return networkTypes[transportType];
} catch (IndexOutOfBoundsException e) {
return mResources.getString(R.string.network_switch_type_name_unknown);
}
}
private static int getIcon(int transportType) {
return (transportType == TRANSPORT_WIFI)
? R.drawable.stat_notify_wifi_in_range // TODO: Distinguish ! from ?.
: R.drawable.stat_notify_rssi_in_range;
}
/**
* Show or hide network provisioning notifications.
*
* We use notifications for two purposes: to notify that a network requires sign in
* (NotificationType.SIGN_IN), or to notify that a network does not have Internet access
* (NotificationType.NO_INTERNET). We display at most one notification per ID, so on a
* particular network we can display the notification type that was most recently requested.
* So for example if a captive portal fails to reply within a few seconds of connecting, we
* might first display NO_INTERNET, and then when the captive portal check completes, display
* SIGN_IN.
*
* @param id an identifier that uniquely identifies this notification. This must match
* between show and hide calls. We use the NetID value but for legacy callers
* we concatenate the range of types with the range of NetIDs.
* @param notifyType the type of the notification.
* @param nai the network with which the notification is associated. For a SIGN_IN, NO_INTERNET,
* or LOST_INTERNET notification, this is the network we're connecting to. For a
* NETWORK_SWITCH notification it's the network that we switched from. When this network
* disconnects the notification is removed.
* @param switchToNai for a NETWORK_SWITCH notification, the network we are switching to. Null
* in all other cases. Only used to determine the text of the notification.
*/
public void showNotification(int id, NotificationType notifyType, NetworkAgentInfo nai,
NetworkAgentInfo switchToNai, PendingIntent intent, boolean highPriority) {
final String tag = tagFor(id);
final int eventId = notifyType.eventId;
final int transportType;
final CharSequence name;
if (nai != null) {
transportType = approximateTransportType(nai);
final String extraInfo = nai.networkInfo.getExtraInfo();
if (nai.linkProperties != null && nai.linkProperties.getCaptivePortalData() != null
&& !TextUtils.isEmpty(nai.linkProperties.getCaptivePortalData()
.getVenueFriendlyName())) {
name = nai.linkProperties.getCaptivePortalData().getVenueFriendlyName();
} else {
name = TextUtils.isEmpty(extraInfo)
? WifiInfo.sanitizeSsid(nai.networkCapabilities.getSsid()) : extraInfo;
}
// Only notify for Internet-capable networks.
if (!nai.networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)) return;
} else {
// Legacy notifications.
transportType = TRANSPORT_CELLULAR;
name = "";
}
// Clear any previous notification with lower priority, otherwise return. http://b/63676954.
// A new SIGN_IN notification with a new intent should override any existing one.
final int previousEventId = mNotificationTypeMap.get(id);
final NotificationType previousNotifyType = NotificationType.getFromId(previousEventId);
if (priority(previousNotifyType) > priority(notifyType)) {
Log.d(TAG, String.format(
"ignoring notification %s for network %s with existing notification %s",
notifyType, id, previousNotifyType));
return;
}
clearNotification(id);
if (DBG) {
Log.d(TAG, String.format(
"showNotification tag=%s event=%s transport=%s name=%s highPriority=%s",
tag, nameOf(eventId), getTransportName(transportType), name, highPriority));
}
final Resources r = mResources;
final CharSequence title;
final CharSequence details;
Icon icon = Icon.createWithResource(r, getIcon(transportType));
if (notifyType == NotificationType.NO_INTERNET && transportType == TRANSPORT_WIFI) {
title = r.getString(R.string.wifi_no_internet, name);
details = r.getString(R.string.wifi_no_internet_detailed);
} else if (notifyType == NotificationType.PRIVATE_DNS_BROKEN) {
if (transportType == TRANSPORT_CELLULAR) {
title = r.getString(R.string.mobile_no_internet);
} else if (transportType == TRANSPORT_WIFI) {
title = r.getString(R.string.wifi_no_internet, name);
} else {
title = r.getString(R.string.other_networks_no_internet);
}
details = r.getString(R.string.private_dns_broken_detailed);
} else if (notifyType == NotificationType.PARTIAL_CONNECTIVITY
&& transportType == TRANSPORT_WIFI) {
title = r.getString(R.string.network_partial_connectivity, name);
details = r.getString(R.string.network_partial_connectivity_detailed);
} else if (notifyType == NotificationType.LOST_INTERNET &&
transportType == TRANSPORT_WIFI) {
title = r.getString(R.string.wifi_no_internet, name);
details = r.getString(R.string.wifi_no_internet_detailed);
} else if (notifyType == NotificationType.SIGN_IN) {
switch (transportType) {
case TRANSPORT_WIFI:
title = r.getString(R.string.wifi_available_sign_in, 0);
details = r.getString(R.string.network_available_sign_in_detailed, name);
break;
case TRANSPORT_CELLULAR:
title = r.getString(R.string.network_available_sign_in, 0);
// TODO: Change this to pull from NetworkInfo once a printable
// name has been added to it
NetworkSpecifier specifier = nai.networkCapabilities.getNetworkSpecifier();
int subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
if (specifier instanceof TelephonyNetworkSpecifier) {
subId = ((TelephonyNetworkSpecifier) specifier).getSubscriptionId();
}
details = mTelephonyManager.createForSubscriptionId(subId)
.getNetworkOperatorName();
break;
default:
title = r.getString(R.string.network_available_sign_in, 0);
details = r.getString(R.string.network_available_sign_in_detailed, name);
break;
}
} else if (notifyType == NotificationType.NETWORK_SWITCH) {
String fromTransport = getTransportName(transportType);
String toTransport = getTransportName(approximateTransportType(switchToNai));
title = r.getString(R.string.network_switch_metered, toTransport);
details = r.getString(R.string.network_switch_metered_detail, toTransport,
fromTransport);
} else if (notifyType == NotificationType.NO_INTERNET
|| notifyType == NotificationType.PARTIAL_CONNECTIVITY) {
// NO_INTERNET and PARTIAL_CONNECTIVITY notification for non-WiFi networks
// are sent, but they are not implemented yet.
return;
} else {
Log.wtf(TAG, "Unknown notification type " + notifyType + " on network transport "
+ getTransportName(transportType));
return;
}
// When replacing an existing notification for a given network, don't alert, just silently
// update the existing notification. Note that setOnlyAlertOnce() will only work for the
// same id, and the id used here is the NotificationType which is different in every type of
// notification. This is required because the notification metrics only track the ID but not
// the tag.
final boolean hasPreviousNotification = previousNotifyType != null;
final String channelId = (highPriority && !hasPreviousNotification)
? NOTIFICATION_CHANNEL_NETWORK_ALERTS : NOTIFICATION_CHANNEL_NETWORK_STATUS;
Notification.Builder builder = new Notification.Builder(mContext, channelId)
.setWhen(System.currentTimeMillis())
.setShowWhen(notifyType == NotificationType.NETWORK_SWITCH)
.setSmallIcon(icon)
.setAutoCancel(true)
.setTicker(title)
.setColor(mContext.getColor(android.R.color.system_notification_accent_color))
.setContentTitle(title)
.setContentIntent(intent)
.setLocalOnly(true)
.setOnlyAlertOnce(true);
if (notifyType == NotificationType.NETWORK_SWITCH) {
builder.setStyle(new Notification.BigTextStyle().bigText(details));
} else {
builder.setContentText(details);
}
if (notifyType == NotificationType.SIGN_IN) {
builder.extend(new Notification.TvExtender().setChannelId(channelId));
}
Notification notification = builder.build();
mNotificationTypeMap.put(id, eventId);
try {
mNotificationManager.notify(tag, eventId, notification);
} catch (NullPointerException npe) {
Log.d(TAG, "setNotificationVisible: visible notificationManager error", npe);
}
}
/**
* Clear the notification with the given id, only if it matches the given type.
*/
public void clearNotification(int id, NotificationType notifyType) {
final int previousEventId = mNotificationTypeMap.get(id);
final NotificationType previousNotifyType = NotificationType.getFromId(previousEventId);
if (notifyType != previousNotifyType) {
return;
}
clearNotification(id);
}
public void clearNotification(int id) {
if (mNotificationTypeMap.indexOfKey(id) < 0) {
return;
}
final String tag = tagFor(id);
final int eventId = mNotificationTypeMap.get(id);
if (DBG) {
Log.d(TAG, String.format("clearing notification tag=%s event=%s", tag,
nameOf(eventId)));
}
try {
mNotificationManager.cancel(tag, eventId);
} catch (NullPointerException npe) {
Log.d(TAG, String.format(
"failed to clear notification tag=%s event=%s", tag, nameOf(eventId)), npe);
}
mNotificationTypeMap.delete(id);
}
/**
* Legacy provisioning notifications coming directly from DcTracker.
*/
public void setProvNotificationVisible(boolean visible, int id, String action) {
if (visible) {
// For legacy purposes, action is sent as the action + the phone ID from DcTracker.
// Split the string here and send the phone ID as an extra instead.
String[] splitAction = action.split(":");
Intent intent = new Intent(splitAction[0]);
try {
intent.putExtra("provision.phone.id", Integer.parseInt(splitAction[1]));
} catch (NumberFormatException ignored) { }
PendingIntent pendingIntent = PendingIntent.getBroadcast(
mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE);
showNotification(id, NotificationType.SIGN_IN, null, null, pendingIntent, false);
} else {
clearNotification(id);
}
}
public void showToast(NetworkAgentInfo fromNai, NetworkAgentInfo toNai) {
String fromTransport = getTransportName(approximateTransportType(fromNai));
String toTransport = getTransportName(approximateTransportType(toNai));
String text = mResources.getString(
R.string.network_switch_metered_toast, fromTransport, toTransport);
Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
}
@VisibleForTesting
static String tagFor(int id) {
return String.format("ConnectivityNotification:%d", id);
}
@VisibleForTesting
static String nameOf(int eventId) {
NotificationType t = NotificationType.getFromId(eventId);
return (t != null) ? t.name() : "UNKNOWN";
}
/**
* A notification with a higher number will take priority over a notification with a lower
* number.
*/
private static int priority(NotificationType t) {
if (t == null) {
return 0;
}
switch (t) {
case SIGN_IN:
return 6;
case PARTIAL_CONNECTIVITY:
return 5;
case PRIVATE_DNS_BROKEN:
return 4;
case NO_INTERNET:
return 3;
case NETWORK_SWITCH:
return 2;
case LOST_INTERNET:
return 1;
default:
return 0;
}
}
}

View File

@@ -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 + " ]";
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.connectivity;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.NetworkRequest;
import java.util.Collection;
/**
* A class that knows how to find the best network matching a request out of a list of networks.
*/
public class NetworkRanker {
public NetworkRanker() { }
/**
* Find the best network satisfying this request among the list of passed networks.
*/
// Almost equivalent to Collections.max(nais), but allows returning null if no network
// satisfies the request.
@Nullable
public NetworkAgentInfo getBestNetwork(@NonNull final NetworkRequest request,
@NonNull final Collection<NetworkAgentInfo> nais) {
NetworkAgentInfo bestNetwork = null;
int bestScore = Integer.MIN_VALUE;
for (final NetworkAgentInfo nai : nais) {
if (!nai.satisfies(request)) continue;
if (nai.getCurrentScore() > bestScore) {
bestNetwork = nai;
bestScore = nai.getCurrentScore();
}
}
return bestNetwork;
}
}

View File

@@ -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<UserHandle> mUsers = new HashSet<>();
// Keys are app uids. Values are true for SYSTEM permission and false for NETWORK permission.
@GuardedBy("this")
private final Map<Integer, Boolean> mApps = new HashMap<>();
// Keys are active non-bypassable and fully-routed VPN's interface name, Values are uid ranges
// for apps under the VPN
@GuardedBy("this")
private final Map<String, Set<UidRange>> mVpnUidRanges = new HashMap<>();
// A set of appIds for apps across all users on the device. We track appIds instead of uids
// directly to reduce its size and also eliminate the need to update this set when user is
// added/removed.
@GuardedBy("this")
private final Set<Integer> mAllApps = new HashSet<>();
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<PackageInfo> apps = mPackageManager.getInstalledPackages(GET_PERMISSIONS
| MATCH_ANY_USER);
if (apps == null) {
loge("No apps");
return;
}
SparseIntArray netdPermsUids = new SparseIntArray();
for (PackageInfo app : apps) {
int uid = app.applicationInfo != null ? app.applicationInfo.uid : INVALID_UID;
if (uid < 0) {
continue;
}
mAllApps.add(UserHandle.getAppId(uid));
boolean isNetwork = hasNetworkPermission(app);
boolean hasRestrictedPermission = hasRestrictedNetworkPermission(app);
if (isNetwork || hasRestrictedPermission) {
Boolean permission = mApps.get(uid);
// If multiple packages share a UID (cf: android:sharedUserId) and ask for different
// permissions, don't downgrade (i.e., if it's already SYSTEM, leave it as is).
if (permission == null || permission == NETWORK) {
mApps.put(uid, hasRestrictedPermission);
}
}
//TODO: unify the management of the permissions into one codepath.
int otherNetdPerms = getNetdPermissionMask(app.requestedPermissions,
app.requestedPermissionsFlags);
netdPermsUids.put(uid, netdPermsUids.get(uid) | otherNetdPerms);
}
mUsers.addAll(mUserManager.getUserHandles(true /* excludeDying */));
final SparseArray<String> netdPermToSystemPerm = new SparseArray<>();
netdPermToSystemPerm.put(INetd.PERMISSION_INTERNET, INTERNET);
netdPermToSystemPerm.put(INetd.PERMISSION_UPDATE_DEVICE_STATS, UPDATE_DEVICE_STATS);
for (int i = 0; i < netdPermToSystemPerm.size(); i++) {
final int netdPermission = netdPermToSystemPerm.keyAt(i);
final String systemPermission = netdPermToSystemPerm.valueAt(i);
final int[] hasPermissionUids =
mSystemConfigManager.getSystemPermissionUids(systemPermission);
for (int j = 0; j < hasPermissionUids.length; j++) {
final int uid = hasPermissionUids[j];
netdPermsUids.put(uid, netdPermsUids.get(uid) | netdPermission);
}
}
log("Users: " + mUsers.size() + ", Apps: " + mApps.size());
update(mUsers, mApps, true);
sendPackagePermissionsToNetd(netdPermsUids);
}
@VisibleForTesting
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<UserHandle> users, Map<Integer, Boolean> apps, boolean add) {
List<Integer> network = new ArrayList<>();
List<Integer> system = new ArrayList<>();
for (Entry<Integer, Boolean> app : apps.entrySet()) {
List<Integer> list = app.getValue() ? system : network;
for (UserHandle user : users) {
if (user == null) continue;
list.add(user.getUid(app.getKey()));
}
}
try {
if (add) {
mNetd.networkSetPermissionForUser(INetd.PERMISSION_NETWORK, toIntArray(network));
mNetd.networkSetPermissionForUser(INetd.PERMISSION_SYSTEM, toIntArray(system));
} else {
mNetd.networkClearPermissionForUser(toIntArray(network));
mNetd.networkClearPermissionForUser(toIntArray(system));
}
} catch (RemoteException e) {
loge("Exception when updating permissions: " + e);
}
}
/**
* Called when a user is added. See {link #ACTION_USER_ADDED}.
*
* @param user The integer userHandle of the added user. See {@link #EXTRA_USER_HANDLE}.
*
* @hide
*/
public synchronized void onUserAdded(@NonNull UserHandle user) {
mUsers.add(user);
Set<UserHandle> users = new HashSet<>();
users.add(user);
update(users, mApps, true);
}
/**
* Called when an user is removed. See {link #ACTION_USER_REMOVED}.
*
* @param user The integer userHandle of the removed user. See {@link #EXTRA_USER_HANDLE}.
*
* @hide
*/
public synchronized void onUserRemoved(@NonNull UserHandle user) {
mUsers.remove(user);
Set<UserHandle> users = new HashSet<>();
users.add(user);
update(users, mApps, false);
}
@VisibleForTesting
protected Boolean highestPermissionForUid(Boolean currentPermission, String name) {
if (currentPermission == SYSTEM) {
return currentPermission;
}
try {
final PackageInfo app = mPackageManager.getPackageInfo(name,
GET_PERMISSIONS | MATCH_ANY_USER);
final boolean isNetwork = hasNetworkPermission(app);
final boolean hasRestrictedPermission = hasRestrictedNetworkPermission(app);
if (isNetwork || hasRestrictedPermission) {
currentPermission = hasRestrictedPermission;
}
} catch (NameNotFoundException e) {
// App not found.
loge("NameNotFoundException " + name);
}
return currentPermission;
}
private int getPermissionForUid(final int uid) {
int permission = INetd.PERMISSION_NONE;
// Check all the packages for this UID. The UID has the permission if any of the
// packages in it has the permission.
final String[] packages = mPackageManager.getPackagesForUid(uid);
if (packages != null && packages.length > 0) {
for (String name : packages) {
final PackageInfo app = getPackageInfo(name);
if (app != null && app.requestedPermissions != null) {
permission |= getNetdPermissionMask(app.requestedPermissions,
app.requestedPermissionsFlags);
}
}
} else {
// The last package of this uid is removed from device. Clean the package up.
permission = INetd.PERMISSION_UNINSTALLED;
}
return permission;
}
/**
* Called when a package is added.
*
* @param packageName The name of the new package.
* @param uid The uid of the new package.
*
* @hide
*/
public synchronized void onPackageAdded(@NonNull final String packageName, final int uid) {
// TODO: Netd is using appId for checking traffic permission. Correct the methods that are
// using appId instead of uid actually
sendPackagePermissionsForUid(UserHandle.getAppId(uid), getPermissionForUid(uid));
// If multiple packages share a UID (cf: android:sharedUserId) and ask for different
// permissions, don't downgrade (i.e., if it's already SYSTEM, leave it as is).
final Boolean permission = highestPermissionForUid(mApps.get(uid), packageName);
if (permission != mApps.get(uid)) {
mApps.put(uid, permission);
Map<Integer, Boolean> apps = new HashMap<>();
apps.put(uid, permission);
update(mUsers, apps, true);
}
// If the newly-installed package falls within some VPN's uid range, update Netd with it.
// This needs to happen after the mApps update above, since removeBypassingUids() depends
// on mApps to check if the package can bypass VPN.
for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
if (UidRange.containsUid(vpn.getValue(), uid)) {
final Set<Integer> changedUids = new HashSet<>();
changedUids.add(uid);
removeBypassingUids(changedUids, /* vpnAppUid */ -1);
updateVpnUids(vpn.getKey(), changedUids, true);
}
}
mAllApps.add(UserHandle.getAppId(uid));
}
/**
* Called when a package is removed.
*
* @param packageName The name of the removed package or null.
* @param uid containing the integer uid previously assigned to the package.
*
* @hide
*/
public synchronized void onPackageRemoved(@NonNull final String packageName, final int uid) {
// TODO: Netd is using appId for checking traffic permission. Correct the methods that are
// using appId instead of uid actually
sendPackagePermissionsForUid(UserHandle.getAppId(uid), getPermissionForUid(uid));
// If the newly-removed package falls within some VPN's uid range, update Netd with it.
// This needs to happen before the mApps update below, since removeBypassingUids() depends
// on mApps to check if the package can bypass VPN.
for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
if (UidRange.containsUid(vpn.getValue(), uid)) {
final Set<Integer> changedUids = new HashSet<>();
changedUids.add(uid);
removeBypassingUids(changedUids, /* vpnAppUid */ -1);
updateVpnUids(vpn.getKey(), changedUids, false);
}
}
// If the package has been removed from all users on the device, clear it form mAllApps.
if (mPackageManager.getNameForUid(uid) == null) {
mAllApps.remove(UserHandle.getAppId(uid));
}
Map<Integer, Boolean> apps = new HashMap<>();
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<UidRange> rangesToAdd,
int vpnAppUid) {
// Calculate the list of new app uids under the VPN due to the new UID ranges and update
// Netd about them. Because mAllApps only contains appIds instead of uids, the result might
// be an overestimation if an app is not installed on the user on which the VPN is running,
// but that's safe.
final Set<Integer> changedUids = intersectUids(rangesToAdd, mAllApps);
removeBypassingUids(changedUids, vpnAppUid);
updateVpnUids(iface, changedUids, true);
if (mVpnUidRanges.containsKey(iface)) {
mVpnUidRanges.get(iface).addAll(rangesToAdd);
} else {
mVpnUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
}
}
/**
* Called when a set of UID ranges are removed from an active VPN network
*
* @param iface The VPN network's interface name
* @param rangesToRemove Existing UID ranges to be removed from the VPN network
* @param vpnAppUid The uid of the VPN app
*/
public synchronized void onVpnUidRangesRemoved(@NonNull String iface,
Set<UidRange> rangesToRemove, int vpnAppUid) {
// Calculate the list of app uids that are no longer under the VPN due to the removed UID
// ranges and update Netd about them.
final Set<Integer> changedUids = intersectUids(rangesToRemove, mAllApps);
removeBypassingUids(changedUids, vpnAppUid);
updateVpnUids(iface, changedUids, false);
Set<UidRange> existingRanges = mVpnUidRanges.getOrDefault(iface, null);
if (existingRanges == null) {
loge("Attempt to remove unknown vpn uid Range iface = " + iface);
return;
}
existingRanges.removeAll(rangesToRemove);
if (existingRanges.size() == 0) {
mVpnUidRanges.remove(iface);
}
}
/**
* Compute the intersection of a set of UidRanges and appIds. Returns a set of uids
* that satisfies:
* 1. falls into one of the UidRange
* 2. matches one of the appIds
*/
private Set<Integer> intersectUids(Set<UidRange> ranges, Set<Integer> appIds) {
Set<Integer> result = new HashSet<>();
for (UidRange range : ranges) {
for (int userId = range.getStartUser(); userId <= range.getEndUser(); userId++) {
for (int appId : appIds) {
final UserHandle handle = UserHandle.of(userId);
if (handle == null) continue;
final int uid = handle.getUid(appId);
if (range.contains(uid)) {
result.add(uid);
}
}
}
}
return result;
}
/**
* Remove all apps which can elect to bypass the VPN from the list of uids
*
* An app can elect to bypass the VPN if it hold SYSTEM permission, or if its the active VPN
* app itself.
*
* @param uids The list of uids to operate on
* @param vpnAppUid The uid of the VPN app
*/
private void removeBypassingUids(Set<Integer> uids, int vpnAppUid) {
uids.remove(vpnAppUid);
uids.removeIf(uid -> mApps.getOrDefault(uid, NETWORK) == SYSTEM);
}
/**
* Update netd about the list of uids that are under an active VPN connection which they cannot
* bypass.
*
* This is to instruct netd to set up appropriate filtering rules for these uids, such that they
* can only receive ingress packets from the VPN's tunnel interface (and loopback).
*
* @param iface the interface name of the active VPN connection
* @param add {@code true} if the uids are to be added to the interface, {@code false} if they
* are to be removed from the interface.
*/
private void updateVpnUids(String iface, Set<Integer> uids, boolean add) {
if (uids.size() == 0) {
return;
}
try {
if (add) {
mNetd.firewallAddUidInterfaceRules(iface, toIntArray(uids));
} else {
mNetd.firewallRemoveUidInterfaceRules(toIntArray(uids));
}
} catch (ServiceSpecificException e) {
// Silently ignore exception when device does not support eBPF, otherwise just log
// the exception and do not crash
if (e.errorCode != OsConstants.EOPNOTSUPP) {
loge("Exception when updating permissions: ", e);
}
} catch (RemoteException e) {
loge("Exception when updating permissions: ", e);
}
}
/**
* Called by PackageListObserver when a package is installed/uninstalled. Send the updated
* permission information to netd.
*
* @param uid the app uid of the package installed
* @param permissions the permissions the app requested and netd cares about.
*
* @hide
*/
@VisibleForTesting
void sendPackagePermissionsForUid(int uid, int permissions) {
SparseIntArray netdPermissionsAppIds = new SparseIntArray();
netdPermissionsAppIds.put(uid, permissions);
sendPackagePermissionsToNetd(netdPermissionsAppIds);
}
/**
* Called by packageManagerService to send IPC to netd. Grant or revoke the INTERNET
* and/or UPDATE_DEVICE_STATS permission of the uids in array.
*
* @param netdPermissionsAppIds integer pairs of uids and the permission granted to it. If the
* permission is 0, revoke all permissions of that uid.
*
* @hide
*/
@VisibleForTesting
void sendPackagePermissionsToNetd(SparseIntArray netdPermissionsAppIds) {
if (mNetd == null) {
Log.e(TAG, "Failed to get the netd service");
return;
}
ArrayList<Integer> allPermissionAppIds = new ArrayList<>();
ArrayList<Integer> internetPermissionAppIds = new ArrayList<>();
ArrayList<Integer> updateStatsPermissionAppIds = new ArrayList<>();
ArrayList<Integer> noPermissionAppIds = new ArrayList<>();
ArrayList<Integer> uninstalledAppIds = new ArrayList<>();
for (int i = 0; i < netdPermissionsAppIds.size(); i++) {
int permissions = netdPermissionsAppIds.valueAt(i);
switch(permissions) {
case (INetd.PERMISSION_INTERNET | INetd.PERMISSION_UPDATE_DEVICE_STATS):
allPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
break;
case INetd.PERMISSION_INTERNET:
internetPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
break;
case INetd.PERMISSION_UPDATE_DEVICE_STATS:
updateStatsPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
break;
case INetd.PERMISSION_NONE:
noPermissionAppIds.add(netdPermissionsAppIds.keyAt(i));
break;
case INetd.PERMISSION_UNINSTALLED:
uninstalledAppIds.add(netdPermissionsAppIds.keyAt(i));
break;
default:
Log.e(TAG, "unknown permission type: " + permissions + "for uid: "
+ netdPermissionsAppIds.keyAt(i));
}
}
try {
// TODO: add a lock inside netd to protect IPC trafficSetNetPermForUids()
if (allPermissionAppIds.size() != 0) {
mNetd.trafficSetNetPermForUids(
INetd.PERMISSION_INTERNET | INetd.PERMISSION_UPDATE_DEVICE_STATS,
toIntArray(allPermissionAppIds));
}
if (internetPermissionAppIds.size() != 0) {
mNetd.trafficSetNetPermForUids(INetd.PERMISSION_INTERNET,
toIntArray(internetPermissionAppIds));
}
if (updateStatsPermissionAppIds.size() != 0) {
mNetd.trafficSetNetPermForUids(INetd.PERMISSION_UPDATE_DEVICE_STATS,
toIntArray(updateStatsPermissionAppIds));
}
if (noPermissionAppIds.size() != 0) {
mNetd.trafficSetNetPermForUids(INetd.PERMISSION_NONE,
toIntArray(noPermissionAppIds));
}
if (uninstalledAppIds.size() != 0) {
mNetd.trafficSetNetPermForUids(INetd.PERMISSION_UNINSTALLED,
toIntArray(uninstalledAppIds));
}
} catch (RemoteException e) {
Log.e(TAG, "Pass appId list of special permission failed." + e);
}
}
/** Should only be used by unit tests */
@VisibleForTesting
public Set<UidRange> getVpnUidRanges(String iface) {
return mVpnUidRanges.get(iface);
}
/** Dump info to dumpsys */
public void dump(IndentingPrintWriter pw) {
pw.println("Interface filtering rules:");
pw.increaseIndent();
for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
pw.println("Interface: " + vpn.getKey());
pw.println("UIDs: " + vpn.getValue().toString());
pw.println();
}
pw.decreaseIndent();
}
private static void log(String s) {
if (DBG) {
Log.d(TAG, s);
}
}
private static void loge(String s) {
Log.e(TAG, s);
}
private static void loge(String s, Throwable e) {
Log.e(TAG, s, e);
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -0,0 +1,199 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.connectivity;
import static android.net.QosCallbackException.EX_TYPE_FILTER_NONE;
import android.annotation.NonNull;
import android.net.IQosCallback;
import android.net.Network;
import android.net.QosCallbackException;
import android.net.QosFilter;
import android.net.QosSession;
import android.os.IBinder;
import android.os.RemoteException;
import android.telephony.data.EpsBearerQosSessionAttributes;
import android.telephony.data.NrQosSessionAttributes;
import android.util.Log;
import java.util.Objects;
/**
* Wraps callback related information and sends messages between network agent and the application.
* <p/>
* This is a satellite class of {@link com.android.server.ConnectivityService} and not meant
* to be used in other contexts.
*
* @hide
*/
class QosCallbackAgentConnection implements IBinder.DeathRecipient {
private static final String TAG = QosCallbackAgentConnection.class.getSimpleName();
private static final boolean DBG = false;
private final int mAgentCallbackId;
@NonNull private final QosCallbackTracker mQosCallbackTracker;
@NonNull private final IQosCallback mCallback;
@NonNull private final IBinder mBinder;
@NonNull private final QosFilter mFilter;
@NonNull private final NetworkAgentInfo mNetworkAgentInfo;
private final int mUid;
/**
* Gets the uid
* @return uid
*/
int getUid() {
return mUid;
}
/**
* Gets the binder
* @return binder
*/
@NonNull
IBinder getBinder() {
return mBinder;
}
/**
* Gets the callback id
*
* @return callback id
*/
int getAgentCallbackId() {
return mAgentCallbackId;
}
/**
* Gets the network tied to the callback of this connection
*
* @return network
*/
@NonNull
Network getNetwork() {
return mFilter.getNetwork();
}
QosCallbackAgentConnection(@NonNull final QosCallbackTracker qosCallbackTracker,
final int agentCallbackId,
@NonNull final IQosCallback callback,
@NonNull final QosFilter filter,
final int uid,
@NonNull final NetworkAgentInfo networkAgentInfo) {
Objects.requireNonNull(qosCallbackTracker, "qosCallbackTracker must be non-null");
Objects.requireNonNull(callback, "callback must be non-null");
Objects.requireNonNull(filter, "filter must be non-null");
Objects.requireNonNull(networkAgentInfo, "networkAgentInfo must be non-null");
mQosCallbackTracker = qosCallbackTracker;
mAgentCallbackId = agentCallbackId;
mCallback = callback;
mFilter = filter;
mUid = uid;
mBinder = mCallback.asBinder();
mNetworkAgentInfo = networkAgentInfo;
}
@Override
public void binderDied() {
logw("binderDied: binder died with callback id: " + mAgentCallbackId);
mQosCallbackTracker.unregisterCallback(mCallback);
}
void unlinkToDeathRecipient() {
mBinder.unlinkToDeath(this, 0);
}
// Returns false if the NetworkAgent was never notified.
boolean sendCmdRegisterCallback() {
final int exceptionType = mFilter.validate();
if (exceptionType != EX_TYPE_FILTER_NONE) {
try {
if (DBG) log("sendCmdRegisterCallback: filter validation failed");
mCallback.onError(exceptionType);
} catch (final RemoteException e) {
loge("sendCmdRegisterCallback:", e);
}
return false;
}
try {
mBinder.linkToDeath(this, 0);
} catch (final RemoteException e) {
loge("failed linking to death recipient", e);
return false;
}
mNetworkAgentInfo.onQosFilterCallbackRegistered(mAgentCallbackId, mFilter);
return true;
}
void sendCmdUnregisterCallback() {
if (DBG) log("sendCmdUnregisterCallback: unregistering");
mNetworkAgentInfo.onQosCallbackUnregistered(mAgentCallbackId);
}
void sendEventEpsQosSessionAvailable(final QosSession session,
final EpsBearerQosSessionAttributes attributes) {
try {
if (DBG) log("sendEventEpsQosSessionAvailable: sending...");
mCallback.onQosEpsBearerSessionAvailable(session, attributes);
} catch (final RemoteException e) {
loge("sendEventEpsQosSessionAvailable: remote exception", e);
}
}
void sendEventNrQosSessionAvailable(final QosSession session,
final NrQosSessionAttributes attributes) {
try {
if (DBG) log("sendEventNrQosSessionAvailable: sending...");
mCallback.onNrQosSessionAvailable(session, attributes);
} catch (final RemoteException e) {
loge("sendEventNrQosSessionAvailable: remote exception", e);
}
}
void sendEventQosSessionLost(@NonNull final QosSession session) {
try {
if (DBG) log("sendEventQosSessionLost: sending...");
mCallback.onQosSessionLost(session);
} catch (final RemoteException e) {
loge("sendEventQosSessionLost: remote exception", e);
}
}
void sendEventQosCallbackError(@QosCallbackException.ExceptionType final int exceptionType) {
try {
if (DBG) log("sendEventQosCallbackError: sending...");
mCallback.onError(exceptionType);
} catch (final RemoteException e) {
loge("sendEventQosCallbackError: remote exception", e);
}
}
private static void log(@NonNull final String msg) {
Log.d(TAG, msg);
}
private static void logw(@NonNull final String msg) {
Log.w(TAG, msg);
}
private static void loge(@NonNull final String msg, final Throwable t) {
Log.e(TAG, msg, t);
}
}

View File

@@ -0,0 +1,292 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.connectivity;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.IQosCallback;
import android.net.Network;
import android.net.QosCallbackException;
import android.net.QosFilter;
import android.net.QosSession;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.telephony.data.EpsBearerQosSessionAttributes;
import android.telephony.data.NrQosSessionAttributes;
import android.util.Log;
import com.android.net.module.util.CollectionUtils;
import com.android.server.ConnectivityService;
import java.util.ArrayList;
import java.util.List;
/**
* Tracks qos callbacks and handles the communication between the network agent and application.
* <p/>
* Any method prefixed by handle must be called from the
* {@link com.android.server.ConnectivityService} handler thread.
*
* @hide
*/
public class QosCallbackTracker {
private static final String TAG = QosCallbackTracker.class.getSimpleName();
private static final boolean DBG = true;
@NonNull
private final Handler mConnectivityServiceHandler;
@NonNull
private final ConnectivityService.PerUidCounter mNetworkRequestCounter;
/**
* Each agent gets a unique callback id that is used to proxy messages back to the original
* callback.
* <p/>
* Note: The fact that this is initialized to 0 is to ensure that the thread running
* {@link #handleRegisterCallback(IQosCallback, QosFilter, int, NetworkAgentInfo)} sees the
* initialized value. This would not necessarily be the case if the value was initialized to
* the non-default value.
* <p/>
* Note: The term previous does not apply to the first callback id that is assigned.
*/
private int mPreviousAgentCallbackId = 0;
@NonNull
private final List<QosCallbackAgentConnection> mConnections = new ArrayList<>();
/**
*
* @param connectivityServiceHandler must be the same handler used with
* {@link com.android.server.ConnectivityService}
* @param networkRequestCounter keeps track of the number of open requests under a given
* uid
*/
public QosCallbackTracker(@NonNull final Handler connectivityServiceHandler,
final ConnectivityService.PerUidCounter networkRequestCounter) {
mConnectivityServiceHandler = connectivityServiceHandler;
mNetworkRequestCounter = networkRequestCounter;
}
/**
* Registers the callback with the tracker
*
* @param callback the callback to register
* @param filter the filter being registered alongside the callback
*/
public void registerCallback(@NonNull final IQosCallback callback,
@NonNull final QosFilter filter, @NonNull final NetworkAgentInfo networkAgentInfo) {
final int uid = Binder.getCallingUid();
// Enforce that the number of requests under this uid has exceeded the allowed number
mNetworkRequestCounter.incrementCountOrThrow(uid);
mConnectivityServiceHandler.post(
() -> handleRegisterCallback(callback, filter, uid, networkAgentInfo));
}
private void handleRegisterCallback(@NonNull final IQosCallback callback,
@NonNull final QosFilter filter, final int uid,
@NonNull final NetworkAgentInfo networkAgentInfo) {
final QosCallbackAgentConnection ac =
handleRegisterCallbackInternal(callback, filter, uid, networkAgentInfo);
if (ac != null) {
if (DBG) log("handleRegisterCallback: added callback " + ac.getAgentCallbackId());
mConnections.add(ac);
} else {
mNetworkRequestCounter.decrementCount(uid);
}
}
private QosCallbackAgentConnection handleRegisterCallbackInternal(
@NonNull final IQosCallback callback,
@NonNull final QosFilter filter, final int uid,
@NonNull final NetworkAgentInfo networkAgentInfo) {
final IBinder binder = callback.asBinder();
if (CollectionUtils.any(mConnections, c -> c.getBinder().equals(binder))) {
// A duplicate registration would have only made this far due to a programming error.
logwtf("handleRegisterCallback: Callbacks can only be register once.");
return null;
}
mPreviousAgentCallbackId = mPreviousAgentCallbackId + 1;
final int newCallbackId = mPreviousAgentCallbackId;
final QosCallbackAgentConnection ac =
new QosCallbackAgentConnection(this, newCallbackId, callback,
filter, uid, networkAgentInfo);
final int exceptionType = filter.validate();
if (exceptionType != QosCallbackException.EX_TYPE_FILTER_NONE) {
ac.sendEventQosCallbackError(exceptionType);
return null;
}
// Only add to the callback maps if the NetworkAgent successfully registered it
if (!ac.sendCmdRegisterCallback()) {
// There was an issue when registering the agent
if (DBG) log("handleRegisterCallback: error sending register callback");
mNetworkRequestCounter.decrementCount(uid);
return null;
}
return ac;
}
/**
* Unregisters callback
* @param callback callback to unregister
*/
public void unregisterCallback(@NonNull final IQosCallback callback) {
mConnectivityServiceHandler.post(() -> handleUnregisterCallback(callback.asBinder(), true));
}
private void handleUnregisterCallback(@NonNull final IBinder binder,
final boolean sendToNetworkAgent) {
final int connIndex =
CollectionUtils.indexOf(mConnections, c -> c.getBinder().equals(binder));
if (connIndex < 0) {
logw("handleUnregisterCallback: no matching agentConnection");
return;
}
final QosCallbackAgentConnection agentConnection = mConnections.get(connIndex);
if (DBG) {
log("handleUnregisterCallback: unregister "
+ agentConnection.getAgentCallbackId());
}
mNetworkRequestCounter.decrementCount(agentConnection.getUid());
mConnections.remove(agentConnection);
if (sendToNetworkAgent) {
agentConnection.sendCmdUnregisterCallback();
}
agentConnection.unlinkToDeathRecipient();
}
/**
* Called when the NetworkAgent sends the qos session available event for EPS
*
* @param qosCallbackId the callback id that the qos session is now available to
* @param session the qos session that is now available
* @param attributes the qos attributes that are now available on the qos session
*/
public void sendEventEpsQosSessionAvailable(final int qosCallbackId,
final QosSession session,
final EpsBearerQosSessionAttributes attributes) {
runOnAgentConnection(qosCallbackId, "sendEventEpsQosSessionAvailable: ",
ac -> ac.sendEventEpsQosSessionAvailable(session, attributes));
}
/**
* Called when the NetworkAgent sends the qos session available event for NR
*
* @param qosCallbackId the callback id that the qos session is now available to
* @param session the qos session that is now available
* @param attributes the qos attributes that are now available on the qos session
*/
public void sendEventNrQosSessionAvailable(final int qosCallbackId,
final QosSession session,
final NrQosSessionAttributes attributes) {
runOnAgentConnection(qosCallbackId, "sendEventNrQosSessionAvailable: ",
ac -> ac.sendEventNrQosSessionAvailable(session, attributes));
}
/**
* Called when the NetworkAgent sends the qos session lost event
*
* @param qosCallbackId the callback id that lost the qos session
* @param session the corresponding qos session
*/
public void sendEventQosSessionLost(final int qosCallbackId,
final QosSession session) {
runOnAgentConnection(qosCallbackId, "sendEventQosSessionLost: ",
ac -> ac.sendEventQosSessionLost(session));
}
/**
* Called when the NetworkAgent sends the qos session on error event
*
* @param qosCallbackId the callback id that should receive the exception
* @param exceptionType the type of exception that caused the callback to error
*/
public void sendEventQosCallbackError(final int qosCallbackId,
@QosCallbackException.ExceptionType final int exceptionType) {
runOnAgentConnection(qosCallbackId, "sendEventQosCallbackError: ",
ac -> {
ac.sendEventQosCallbackError(exceptionType);
handleUnregisterCallback(ac.getBinder(), false);
});
}
/**
* Unregisters all callbacks associated to this network agent
*
* Note: Must be called on the connectivity service handler thread
*
* @param network the network that was released
*/
public void handleNetworkReleased(@Nullable final Network network) {
// Iterate in reverse order as agent connections will be removed when unregistering
for (int i = mConnections.size() - 1; i >= 0; i--) {
final QosCallbackAgentConnection agentConnection = mConnections.get(i);
if (!agentConnection.getNetwork().equals(network)) continue;
agentConnection.sendEventQosCallbackError(
QosCallbackException.EX_TYPE_FILTER_NETWORK_RELEASED);
// Call unregister workflow w\o sending anything to agent since it is disconnected.
handleUnregisterCallback(agentConnection.getBinder(), false);
}
}
private interface AgentConnectionAction {
void execute(@NonNull QosCallbackAgentConnection agentConnection);
}
@Nullable
private void runOnAgentConnection(final int qosCallbackId,
@NonNull final String logPrefix,
@NonNull final AgentConnectionAction action) {
mConnectivityServiceHandler.post(() -> {
final int acIndex = CollectionUtils.indexOf(mConnections,
c -> c.getAgentCallbackId() == qosCallbackId);
if (acIndex == -1) {
loge(logPrefix + ": " + qosCallbackId + " missing callback id");
return;
}
action.execute(mConnections.get(acIndex));
});
}
private static void log(final String msg) {
Log.d(TAG, msg);
}
private static void logw(final String msg) {
Log.w(TAG, msg);
}
private static void loge(final String msg) {
Log.e(TAG, msg);
}
private static void logwtf(final String msg) {
Log.wtf(TAG, msg);
}
}

View File

@@ -0,0 +1,339 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.connectivity;
import static android.net.SocketKeepalive.DATA_RECEIVED;
import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET;
import static android.net.SocketKeepalive.ERROR_SOCKET_NOT_IDLE;
import static android.net.SocketKeepalive.ERROR_UNSUPPORTED;
import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
import static android.system.OsConstants.ENOPROTOOPT;
import static android.system.OsConstants.FIONREAD;
import static android.system.OsConstants.IPPROTO_IP;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IP_TOS;
import static android.system.OsConstants.IP_TTL;
import static android.system.OsConstants.TIOCOUTQ;
import android.annotation.NonNull;
import android.net.InvalidPacketException;
import android.net.NetworkUtils;
import android.net.SocketKeepalive.InvalidSocketException;
import android.net.TcpKeepalivePacketData;
import android.net.TcpKeepalivePacketDataParcelable;
import android.net.TcpRepairWindow;
import android.net.util.KeepalivePacketDataUtil;
import android.os.Handler;
import android.os.MessageQueue;
import android.os.Messenger;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.server.connectivity.KeepaliveTracker.KeepaliveInfo;
import java.io.FileDescriptor;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketException;
/**
* Manage tcp socket which offloads tcp keepalive.
*
* The input socket will be changed to repair mode and the application
* will not have permission to read/write data. If the application wants
* to write data, it must stop tcp keepalive offload to leave repair mode
* first. If a remote packet arrives, repair mode will be turned off and
* offload will be stopped. The application will receive a callback to know
* it can start reading data.
*
* {start,stop}SocketMonitor are thread-safe, but care must be taken in the
* order in which they are called. Please note that while calling
* {@link #startSocketMonitor(FileDescriptor, Messenger, int)} multiple times
* with either the same slot or the same FileDescriptor without stopping it in
* between will result in an exception, calling {@link #stopSocketMonitor(int)}
* multiple times with the same int is explicitly a no-op.
* Please also note that switching the socket to repair mode is not synchronized
* with either of these operations and has to be done in an orderly fashion
* with stopSocketMonitor. Take care in calling these in the right order.
* @hide
*/
public class TcpKeepaliveController {
private static final String TAG = "TcpKeepaliveController";
private static final boolean DBG = false;
private final MessageQueue mFdHandlerQueue;
private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
// Reference include/uapi/linux/tcp.h
private static final int TCP_REPAIR = 19;
private static final int TCP_REPAIR_QUEUE = 20;
private static final int TCP_QUEUE_SEQ = 21;
private static final int TCP_NO_QUEUE = 0;
private static final int TCP_RECV_QUEUE = 1;
private static final int TCP_SEND_QUEUE = 2;
private static final int TCP_REPAIR_OFF = 0;
private static final int TCP_REPAIR_ON = 1;
// Reference include/uapi/linux/sockios.h
private static final int SIOCINQ = FIONREAD;
private static final int SIOCOUTQ = TIOCOUTQ;
/**
* Keeps track of packet listeners.
* Key: slot number of keepalive offload.
* Value: {@link FileDescriptor} being listened to.
*/
@GuardedBy("mListeners")
private final SparseArray<FileDescriptor> mListeners = new SparseArray<>();
public TcpKeepaliveController(final Handler connectivityServiceHandler) {
mFdHandlerQueue = connectivityServiceHandler.getLooper().getQueue();
}
/** Build tcp keepalive packet. */
public static TcpKeepalivePacketData getTcpKeepalivePacket(@NonNull FileDescriptor fd)
throws InvalidPacketException, InvalidSocketException {
try {
final TcpKeepalivePacketDataParcelable tcpDetails = switchToRepairMode(fd);
return KeepalivePacketDataUtil.fromStableParcelable(tcpDetails);
} catch (InvalidPacketException | InvalidSocketException e) {
switchOutOfRepairMode(fd);
throw e;
}
}
/**
* Switch the tcp socket to repair mode and query detail tcp information.
*
* @param fd the fd of socket on which to use keepalive offload.
* @return a {@link TcpKeepalivePacketDataParcelable} object for current
* tcp/ip information.
*/
private static TcpKeepalivePacketDataParcelable switchToRepairMode(FileDescriptor fd)
throws InvalidSocketException {
if (DBG) Log.i(TAG, "switchToRepairMode to start tcp keepalive : " + fd);
final TcpKeepalivePacketDataParcelable tcpDetails = new TcpKeepalivePacketDataParcelable();
final SocketAddress srcSockAddr;
final SocketAddress dstSockAddr;
final TcpRepairWindow trw;
// Query source address and port.
try {
srcSockAddr = Os.getsockname(fd);
} catch (ErrnoException e) {
Log.e(TAG, "Get sockname fail: ", e);
throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
}
if (srcSockAddr instanceof InetSocketAddress) {
tcpDetails.srcAddress = getAddress((InetSocketAddress) srcSockAddr);
tcpDetails.srcPort = getPort((InetSocketAddress) srcSockAddr);
} else {
Log.e(TAG, "Invalid or mismatched SocketAddress");
throw new InvalidSocketException(ERROR_INVALID_SOCKET);
}
// Query destination address and port.
try {
dstSockAddr = Os.getpeername(fd);
} catch (ErrnoException e) {
Log.e(TAG, "Get peername fail: ", e);
throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
}
if (dstSockAddr instanceof InetSocketAddress) {
tcpDetails.dstAddress = getAddress((InetSocketAddress) dstSockAddr);
tcpDetails.dstPort = getPort((InetSocketAddress) dstSockAddr);
} else {
Log.e(TAG, "Invalid or mismatched peer SocketAddress");
throw new InvalidSocketException(ERROR_INVALID_SOCKET);
}
// Query sequence and ack number
dropAllIncomingPackets(fd, true);
try {
// Switch to tcp repair mode.
Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR, TCP_REPAIR_ON);
// Check if socket is idle.
if (!isSocketIdle(fd)) {
Log.e(TAG, "Socket is not idle");
throw new InvalidSocketException(ERROR_SOCKET_NOT_IDLE);
}
// Query write sequence number from SEND_QUEUE.
Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_SEND_QUEUE);
tcpDetails.seq = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);
// Query read sequence number from RECV_QUEUE.
Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_RECV_QUEUE);
tcpDetails.ack = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ);
// Switch to NO_QUEUE to prevent illegal socket read/write in repair mode.
Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_NO_QUEUE);
// Finally, check if socket is still idle. TODO : this check needs to move to
// after starting polling to prevent a race.
if (!isReceiveQueueEmpty(fd)) {
Log.e(TAG, "Fatal: receive queue of this socket is not empty");
throw new InvalidSocketException(ERROR_INVALID_SOCKET);
}
if (!isSendQueueEmpty(fd)) {
Log.e(TAG, "Socket is not idle");
throw new InvalidSocketException(ERROR_SOCKET_NOT_IDLE);
}
// Query tcp window size.
trw = NetworkUtils.getTcpRepairWindow(fd);
tcpDetails.rcvWnd = trw.rcvWnd;
tcpDetails.rcvWndScale = trw.rcvWndScale;
if (tcpDetails.srcAddress.length == 4 /* V4 address length */) {
// Query TOS.
tcpDetails.tos = Os.getsockoptInt(fd, IPPROTO_IP, IP_TOS);
// Query TTL.
tcpDetails.ttl = Os.getsockoptInt(fd, IPPROTO_IP, IP_TTL);
}
} catch (ErrnoException e) {
Log.e(TAG, "Exception reading TCP state from socket", e);
if (e.errno == ENOPROTOOPT) {
// ENOPROTOOPT may happen in kernel version lower than 4.8.
// Treat it as ERROR_UNSUPPORTED.
throw new InvalidSocketException(ERROR_UNSUPPORTED, e);
} else {
throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
}
} finally {
dropAllIncomingPackets(fd, false);
}
// Keepalive sequence number is last sequence number - 1. If it couldn't be retrieved,
// then it must be set to -1, so decrement in all cases.
tcpDetails.seq = tcpDetails.seq - 1;
return tcpDetails;
}
/**
* Switch the tcp socket out of repair mode.
*
* @param fd the fd of socket to switch back to normal.
*/
private static void switchOutOfRepairMode(@NonNull final FileDescriptor fd) {
try {
Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR, TCP_REPAIR_OFF);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot switch socket out of repair mode", e);
// Well, there is not much to do here to recover
}
}
/**
* Start monitoring incoming packets.
*
* @param fd socket fd to monitor.
* @param ki a {@link KeepaliveInfo} that tracks information about a socket keepalive.
* @param slot keepalive slot.
*/
public void startSocketMonitor(@NonNull final FileDescriptor fd,
@NonNull final KeepaliveInfo ki, final int slot)
throws IllegalArgumentException, InvalidSocketException {
synchronized (mListeners) {
if (null != mListeners.get(slot)) {
throw new IllegalArgumentException("This slot is already taken");
}
for (int i = 0; i < mListeners.size(); ++i) {
if (fd.equals(mListeners.valueAt(i))) {
Log.e(TAG, "This fd is already registered.");
throw new InvalidSocketException(ERROR_INVALID_SOCKET);
}
}
mFdHandlerQueue.addOnFileDescriptorEventListener(fd, FD_EVENTS, (readyFd, events) -> {
// This can't be called twice because the queue guarantees that once the listener
// is unregistered it can't be called again, even for a message that arrived
// before it was unregistered.
final int reason;
if (0 != (events & EVENT_ERROR)) {
reason = ERROR_INVALID_SOCKET;
} else {
reason = DATA_RECEIVED;
}
ki.onFileDescriptorInitiatedStop(reason);
// The listener returns the new set of events to listen to. Because 0 means no
// event, the listener gets unregistered.
return 0;
});
mListeners.put(slot, fd);
}
}
/** Stop socket monitor */
// This slot may have been stopped automatically already because the socket received data,
// was closed on the other end or otherwise suffered some error. In this case, this function
// is a no-op.
public void stopSocketMonitor(final int slot) {
final FileDescriptor fd;
synchronized (mListeners) {
fd = mListeners.get(slot);
if (null == fd) return;
mListeners.remove(slot);
}
mFdHandlerQueue.removeOnFileDescriptorEventListener(fd);
if (DBG) Log.d(TAG, "Moving socket out of repair mode for stop : " + fd);
switchOutOfRepairMode(fd);
}
private static byte [] getAddress(InetSocketAddress inetAddr) {
return inetAddr.getAddress().getAddress();
}
private static int getPort(InetSocketAddress inetAddr) {
return inetAddr.getPort();
}
private static boolean isSocketIdle(FileDescriptor fd) throws ErrnoException {
return isReceiveQueueEmpty(fd) && isSendQueueEmpty(fd);
}
private static boolean isReceiveQueueEmpty(FileDescriptor fd)
throws ErrnoException {
final int result = Os.ioctlInt(fd, SIOCINQ);
if (result != 0) {
Log.e(TAG, "Read queue has data");
return false;
}
return true;
}
private static boolean isSendQueueEmpty(FileDescriptor fd)
throws ErrnoException {
final int result = Os.ioctlInt(fd, SIOCOUTQ);
if (result != 0) {
Log.e(TAG, "Write queue has data");
return false;
}
return true;
}
private static void dropAllIncomingPackets(FileDescriptor fd, boolean enable)
throws InvalidSocketException {
try {
if (enable) {
NetworkUtils.attachDropAllBPFFilter(fd);
} else {
NetworkUtils.detachBPFFilter(fd);
}
} catch (SocketException e) {
Log.e(TAG, "Socket Exception: ", e);
throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
}
}
}