diff --git a/services/core/java/com/android/server/TestNetworkService.java b/services/core/java/com/android/server/TestNetworkService.java index 013e9cc063..e64ab78d1e 100644 --- a/services/core/java/com/android/server/TestNetworkService.java +++ b/services/core/java/com/android/server/TestNetworkService.java @@ -20,23 +20,47 @@ import static com.android.internal.util.Preconditions.checkNotNull; import android.annotation.NonNull; 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.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkInfo.DetailedState; +import android.net.RouteInfo; +import android.net.StringNetworkSpecifier; import android.net.TestNetworkInterface; import android.net.util.NetdService; +import android.os.Binder; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.INetworkManagementService; -import android.util.Log; +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 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.concurrent.atomic.AtomicInteger; + /** @hide */ class TestNetworkService extends ITestNetworkManager.Stub { @NonNull private static final String TAG = TestNetworkService.class.getSimpleName(); @NonNull private static final String TEST_NETWORK_TYPE = "TEST_NETWORK"; + @NonNull private static final String TEST_TUN_PREFIX = "testtun"; + @NonNull private static final AtomicInteger sTestTunIndex = new AtomicInteger(); @NonNull private final Context mContext; @NonNull private final INetworkManagementService mNMS; @@ -45,18 +69,19 @@ class TestNetworkService extends ITestNetworkManager.Stub { @NonNull private final HandlerThread mHandlerThread; @NonNull private final Handler mHandler; + // Native method stubs + private static native int jniCreateTun(@NonNull String iface); + @VisibleForTesting protected TestNetworkService( @NonNull Context context, @NonNull INetworkManagementService netManager) { - Log.d(TAG, "TestNetworkService starting up"); - mHandlerThread = new HandlerThread("TestNetworkServiceThread"); mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); mContext = checkNotNull(context, "missing Context"); mNMS = checkNotNull(netManager, "missing INetworkManagementService"); - mNetd = NetdService.getInstance(); + mNetd = checkNotNull(NetdService.getInstance(), "could not get netd instance"); } /** @@ -66,8 +91,167 @@ class TestNetworkService extends ITestNetworkManager.Stub { * TUN interface. */ @Override - public synchronized TestNetworkInterface createTunInterface(@NonNull LinkAddress[] linkAddrs) { - return null; + public TestNetworkInterface createTunInterface(@NonNull LinkAddress[] linkAddrs) { + enforceTestNetworkPermissions(mContext); + + checkNotNull(linkAddrs, "missing linkAddrs"); + + String iface = TEST_TUN_PREFIX + sTestTunIndex.getAndIncrement(); + return Binder.withCleanCallingIdentity( + () -> { + try { + ParcelFileDescriptor tunIntf = + ParcelFileDescriptor.adoptFd(jniCreateTun(iface)); + for (LinkAddress addr : linkAddrs) { + mNetd.interfaceAddAddress( + iface, + addr.getAddress().getHostAddress(), + addr.getPrefixLength()); + } + + return new TestNetworkInterface(tunIntf, iface); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + }); + } + + // Tracker for TestNetworkAgents + @GuardedBy("mTestNetworkTracker") + @NonNull + private final SparseArray mTestNetworkTracker = new SparseArray<>(); + + public class TestNetworkAgent extends NetworkAgent implements IBinder.DeathRecipient { + private static final int NETWORK_SCORE = 1; // Use a low, non-zero score. + + private final int mUid; + @NonNull private final NetworkInfo mNi; + @NonNull private final NetworkCapabilities mNc; + @NonNull private final LinkProperties mLp; + + @GuardedBy("mBinderLock") + @NonNull + private IBinder mBinder; + + @NonNull private final Object mBinderLock = new Object(); + + private TestNetworkAgent( + @NonNull Looper looper, + @NonNull Context context, + @NonNull NetworkInfo ni, + @NonNull NetworkCapabilities nc, + @NonNull LinkProperties lp, + int uid, + @NonNull IBinder binder) + throws RemoteException { + super(looper, context, TEST_NETWORK_TYPE, ni, nc, lp, NETWORK_SCORE); + + mUid = uid; + mNi = ni; + mNc = nc; + mLp = lp; + + 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() { + mNi.setDetailedState(DetailedState.DISCONNECTED, null, null); + mNi.setIsAvailable(false); + sendNetworkInfo(mNi); + + // 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(netId); + } + } + } + + private TestNetworkAgent registerTestNetworkAgent( + @NonNull Looper looper, + @NonNull Context context, + @NonNull String iface, + int callingUid, + @NonNull IBinder binder) + throws RemoteException, SocketException { + checkNotNull(looper, "missing Looper"); + checkNotNull(context, "missing Context"); + // iface and binder validity checked by caller + + // Build network info with special testing type + NetworkInfo ni = new NetworkInfo(ConnectivityManager.TYPE_TEST, 0, TEST_NETWORK_TYPE, ""); + ni.setDetailedState(DetailedState.CONNECTED, null, null); + ni.setIsAvailable(true); + + // 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.setNetworkSpecifier(new StringNetworkSpecifier(iface)); + + // Build LinkProperties + LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(iface); + + // Find the currently assigned addresses, and add them to LinkProperties + boolean allowIPv4 = false, allowIPv6 = false; + NetworkInterface netIntf = NetworkInterface.getByName(iface); + checkNotNull(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(Inet4Address.ANY, 0), null, iface)); + } + if (allowIPv6) { + lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null, iface)); + } + + return new TestNetworkAgent(looper, context, ni, nc, lp, callingUid, binder); } /** @@ -77,9 +261,78 @@ class TestNetworkService extends ITestNetworkManager.Stub { *

This method provides a Network that is useful only for testing. */ @Override - public synchronized void setupTestNetwork(@NonNull String iface, @NonNull IBinder binder) {} + public void setupTestNetwork(@NonNull String iface, @NonNull IBinder binder) { + enforceTestNetworkPermissions(mContext); + + checkNotNull(iface, "missing Iface"); + checkNotNull(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"); + } + + // Setup needs to be done with NETWORK_STACK privileges. + int callingUid = Binder.getCallingUid(); + Binder.withCleanCallingIdentity( + () -> { + try { + mNMS.setInterfaceUp(iface); + + // 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, + callingUid, + binder); + + mTestNetworkTracker.put(agent.netId, agent); + } + } catch (SocketException e) { + throw new UncheckedIOException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + }); + } /** Teardown a test network */ @Override - public synchronized void teardownTestNetwork(int netId) {} + public void teardownTestNetwork(int netId) { + enforceTestNetworkPermissions(mContext); + + 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(); + } + + // STOPSHIP: Change this back to android.Manifest.permission.MANAGE_TEST_NETWORKS + private static final String PERMISSION_NAME = "dummy"; + + public static void enforceTestNetworkPermissions(@NonNull Context context) { + // STOPSHIP: Re-enable these checks. Disabled until adoptShellPermissionIdentity() can be + // called from CTS test code. + if (false) { + context.enforceCallingOrSelfPermission(PERMISSION_NAME, "TestNetworkService"); + } + } } diff --git a/services/core/jni/com_android_server_TestNetworkService.cpp b/services/core/jni/com_android_server_TestNetworkService.cpp new file mode 100644 index 0000000000..b90ff233c1 --- /dev/null +++ b/services/core/jni/com_android_server_TestNetworkService.cpp @@ -0,0 +1,107 @@ +/* + * 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. + */ + +#define LOG_NDEBUG 0 + +#define LOG_TAG "TestNetworkServiceJni" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "netutils/ifc.h" + +#include "jni.h" +#include +#include +#include +#include + +namespace android { + +//------------------------------------------------------------------------------ + +static void throwException(JNIEnv* env, int error, const char* action, const char* iface) { + const std::string& msg = + android::base::StringPrintf("Error %s %s: %s", action, iface, strerror(error)); + + jniThrowException(env, "java/lang/IllegalStateException", msg.c_str()); +} + +static int createTunInterface(JNIEnv* env, const char* iface) { + base::unique_fd tun(open("/dev/tun", O_RDWR | O_NONBLOCK)); + ifreq ifr{}; + + // Allocate interface. + ifr.ifr_flags = IFF_TUN | IFF_NO_PI; + strlcpy(ifr.ifr_name, iface, IFNAMSIZ); + if (ioctl(tun.get(), TUNSETIFF, &ifr)) { + throwException(env, errno, "allocating", ifr.ifr_name); + return -1; + } + + // Activate interface using an unconnected datagram socket. + base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0)); + ifr.ifr_flags = IFF_UP; + + if (ioctl(inet6CtrlSock.get(), SIOCSIFFLAGS, &ifr)) { + throwException(env, errno, "activating", ifr.ifr_name); + return -1; + } + + return tun.release(); +} + +//------------------------------------------------------------------------------ + +static jint create(JNIEnv* env, jobject /* thiz */, jstring jIface) { + ScopedUtfChars iface(env, jIface); + if (!iface.c_str()) { + jniThrowNullPointerException(env, "iface"); + return -1; + } + + int tun = createTunInterface(env, iface.c_str()); + + // Any exceptions will be thrown from the createTunInterface call + return tun; +} + +//------------------------------------------------------------------------------ + +static const JNINativeMethod gMethods[] = { + {"jniCreateTun", "(Ljava/lang/String;)I", (void*)create}, +}; + +int register_android_server_TestNetworkService(JNIEnv* env) { + return jniRegisterNativeMethods(env, "com/android/server/TestNetworkService", gMethods, + NELEM(gMethods)); +} + +}; // namespace android