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:
@@ -52,8 +52,8 @@ cc_library_shared {
|
|||||||
java_library {
|
java_library {
|
||||||
name: "service-connectivity-pre-jarjar",
|
name: "service-connectivity-pre-jarjar",
|
||||||
srcs: [
|
srcs: [
|
||||||
|
"src/**/*.java",
|
||||||
":framework-connectivity-shared-srcs",
|
":framework-connectivity-shared-srcs",
|
||||||
":connectivity-service-srcs",
|
|
||||||
],
|
],
|
||||||
libs: [
|
libs: [
|
||||||
"android.net.ipsec.ike",
|
"android.net.ipsec.ike",
|
||||||
|
|||||||
10083
service/src/com/android/server/ConnectivityService.java
Normal file
10083
service/src/com/android/server/ConnectivityService.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
386
service/src/com/android/server/TestNetworkService.java
Normal file
386
service/src/com/android/server/TestNetworkService.java
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
494
service/src/com/android/server/connectivity/DnsManager.java
Normal file
494
service/src/com/android/server/connectivity/DnsManager.java
Normal 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(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
239
service/src/com/android/server/connectivity/FullScore.java
Normal file
239
service/src/com/android/server/connectivity/FullScore.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
330
service/src/com/android/server/connectivity/LingerMonitor.java
Normal file
330
service/src/com/android/server/connectivity/LingerMonitor.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
523
service/src/com/android/server/connectivity/Nat464Xlat.java
Normal file
523
service/src/com/android/server/connectivity/Nat464Xlat.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1127
service/src/com/android/server/connectivity/NetworkAgentInfo.java
Normal file
1127
service/src/com/android/server/connectivity/NetworkAgentInfo.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 + " ]";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
353
service/src/com/android/server/connectivity/ProxyTracker.java
Normal file
353
service/src/com/android/server/connectivity/ProxyTracker.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user