diff --git a/core/java/android/net/TestNetworkInterface.java b/core/java/android/net/TestNetworkInterface.java new file mode 100644 index 0000000000..30e68f5b98 --- /dev/null +++ b/core/java/android/net/TestNetworkInterface.java @@ -0,0 +1,74 @@ +/* + * 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 android.net; + +import android.annotation.TestApi; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; + +/** + * This class is used to return the interface name and fd of the test interface + * + * @hide + */ +@TestApi +public final class TestNetworkInterface implements Parcelable { + private static final String TAG = "TestNetworkInterface"; + + private final ParcelFileDescriptor mFileDescriptor; + private final String mInterfaceName; + + @Override + public int describeContents() { + return (mFileDescriptor != null) ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeParcelable(mFileDescriptor, PARCELABLE_WRITE_RETURN_VALUE); + out.writeString(mInterfaceName); + } + + public TestNetworkInterface(ParcelFileDescriptor pfd, String intf) { + mFileDescriptor = pfd; + mInterfaceName = intf; + } + + private TestNetworkInterface(Parcel in) { + mFileDescriptor = in.readParcelable(ParcelFileDescriptor.class.getClassLoader()); + mInterfaceName = in.readString(); + } + + public ParcelFileDescriptor getFileDescriptor() { + return mFileDescriptor; + } + + public String getInterfaceName() { + return mInterfaceName; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public TestNetworkInterface createFromParcel(Parcel in) { + return new TestNetworkInterface(in); + } + + public TestNetworkInterface[] newArray(int size) { + return new TestNetworkInterface[size]; + } + }; +} diff --git a/core/java/android/net/TestNetworkManager.java b/core/java/android/net/TestNetworkManager.java new file mode 100644 index 0000000000..cd58e6641e --- /dev/null +++ b/core/java/android/net/TestNetworkManager.java @@ -0,0 +1,91 @@ +/* + * 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 android.net; + +import android.annotation.NonNull; +import android.annotation.TestApi; +import android.content.Context; +import android.os.IBinder; +import android.os.RemoteException; + +import com.android.internal.util.Preconditions; + +/** + * Class that allows creation and management of per-app, test-only networks + * + * @hide + */ +@TestApi +public class TestNetworkManager { + @NonNull private static final String TAG = TestNetworkManager.class.getSimpleName(); + + @NonNull private final ITestNetworkManager mService; + @NonNull private final Context mContext; + + /** @hide */ + public TestNetworkManager(@NonNull Context context, @NonNull ITestNetworkManager service) { + mContext = Preconditions.checkNotNull(context, "missing Context"); + mService = Preconditions.checkNotNull(service, "missing ITestNetworkManager"); + } + + /** + * Teardown the capability-limited, testing-only network for a given interface + * + * @param network The test network that should be torn down + * @hide + */ + @TestApi + public void teardownTestNetwork(@NonNull Network network) { + try { + mService.teardownTestNetwork(network.netId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Sets up a capability-limited, testing-only network for a given interface + * + * @param iface the name of the interface to be used for the Network LinkProperties. + * @param binder A binder object guarding the lifecycle of this test network. + * @hide + */ + @TestApi + public void setupTestNetwork(@NonNull String iface, @NonNull IBinder binder) { + try { + mService.setupTestNetwork(iface, binder); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Create a tun interface for testing purposes + * + * @param linkAddrs an array of LinkAddresses to assign to the TUN interface + * @return A ParcelFileDescriptor of the underlying TUN interface. Close this to tear down the + * TUN interface. + * @hide + */ + @TestApi + public TestNetworkInterface createTunInterface(@NonNull LinkAddress[] linkAddrs) { + try { + return mService.createTunInterface(linkAddrs); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/services/core/java/com/android/server/TestNetworkService.java b/services/core/java/com/android/server/TestNetworkService.java new file mode 100644 index 0000000000..e64ab78d1e --- /dev/null +++ b/services/core/java/com/android/server/TestNetworkService.java @@ -0,0 +1,338 @@ +/* + * 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 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.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; + @NonNull private final INetd mNetd; + + @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) { + mHandlerThread = new HandlerThread("TestNetworkServiceThread"); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + + mContext = checkNotNull(context, "missing Context"); + mNMS = checkNotNull(netManager, "missing INetworkManagementService"); + mNetd = checkNotNull(NetdService.getInstance(), "could not get netd instance"); + } + + /** + * Create a TUN interface with the given interface name and link addresses + * + *

This method will return the FileDescriptor to the TUN interface. Close it to tear down the + * TUN interface. + */ + @Override + public TestNetworkInterface createTunInterface(@NonNull LinkAddress[] linkAddrs) { + 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); + } + + /** + * Sets up a Network with extremely limited privileges, guarded by the MANAGE_TEST_NETWORKS + * permission. + * + *

This method provides a Network that is useful only for testing. + */ + @Override + public void setupTestNetwork(@NonNull String iface, @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 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