| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server; |
| |
| import static android.Manifest.permission.DUMP; |
| import static android.net.IpSecManager.INVALID_RESOURCE_ID; |
| import static android.system.OsConstants.AF_INET; |
| import static android.system.OsConstants.IPPROTO_UDP; |
| import static android.system.OsConstants.SOCK_DGRAM; |
| import static com.android.internal.util.Preconditions.checkNotNull; |
| |
| import android.content.Context; |
| import android.net.IIpSecService; |
| import android.net.INetd; |
| import android.net.IpSecAlgorithm; |
| import android.net.IpSecConfig; |
| import android.net.IpSecManager; |
| import android.net.IpSecSpiResponse; |
| import android.net.IpSecTransform; |
| import android.net.IpSecTransformResponse; |
| import android.net.IpSecUdpEncapResponse; |
| import android.net.NetworkUtils; |
| import android.net.util.NetdService; |
| import android.os.Binder; |
| import android.os.IBinder; |
| import android.os.ParcelFileDescriptor; |
| import android.os.RemoteException; |
| import android.os.ServiceSpecificException; |
| import android.system.ErrnoException; |
| import android.system.Os; |
| import android.system.OsConstants; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.util.Slog; |
| import android.util.SparseArray; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.io.FileDescriptor; |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.net.InetAddress; |
| import java.net.InetSocketAddress; |
| import java.net.UnknownHostException; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| import libcore.io.IoUtils; |
| |
| /** @hide */ |
| public class IpSecService extends IIpSecService.Stub { |
| private static final String TAG = "IpSecService"; |
| private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); |
| |
| private static final String NETD_SERVICE_NAME = "netd"; |
| private static final int[] DIRECTIONS = |
| new int[] {IpSecTransform.DIRECTION_OUT, IpSecTransform.DIRECTION_IN}; |
| |
| private static final int NETD_FETCH_TIMEOUT_MS = 5000; // ms |
| private static final int MAX_PORT_BIND_ATTEMPTS = 10; |
| private static final InetAddress INADDR_ANY; |
| |
| static { |
| try { |
| INADDR_ANY = InetAddress.getByAddress(new byte[] {0, 0, 0, 0}); |
| } catch (UnknownHostException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| static final int FREE_PORT_MIN = 1024; // ports 1-1023 are reserved |
| static final int PORT_MAX = 0xFFFF; // ports are an unsigned 16-bit integer |
| |
| /* Binder context for this service */ |
| private final Context mContext; |
| |
| /** Should be a never-repeating global ID for resources */ |
| private static AtomicInteger mNextResourceId = new AtomicInteger(0x00FADED0); |
| |
| @GuardedBy("this") |
| private final ManagedResourceArray<SpiRecord> mSpiRecords = new ManagedResourceArray<>(); |
| |
| @GuardedBy("this") |
| private final ManagedResourceArray<TransformRecord> mTransformRecords = |
| new ManagedResourceArray<>(); |
| |
| @GuardedBy("this") |
| private final ManagedResourceArray<UdpSocketRecord> mUdpSocketRecords = |
| new ManagedResourceArray<>(); |
| |
| interface IpSecServiceConfiguration { |
| INetd getNetdInstance() throws RemoteException; |
| |
| static IpSecServiceConfiguration GETSRVINSTANCE = |
| new IpSecServiceConfiguration() { |
| @Override |
| public INetd getNetdInstance() throws RemoteException { |
| final INetd netd = NetdService.getInstance(); |
| if (netd == null) { |
| throw new RemoteException("Failed to Get Netd Instance"); |
| } |
| return netd; |
| } |
| }; |
| } |
| |
| private final IpSecServiceConfiguration mSrvConfig; |
| |
| /* Very simple counting class that looks much like a counting semaphore */ |
| public static class ResourceTracker { |
| private final int mMax; |
| int mCurrent; |
| |
| ResourceTracker(int max) { |
| mMax = max; |
| mCurrent = 0; |
| } |
| |
| synchronized boolean isAvailable() { |
| return (mCurrent < mMax); |
| } |
| |
| synchronized void take() { |
| if (!isAvailable()) { |
| Log.wtf(TAG, "Too many resources allocated!"); |
| } |
| mCurrent++; |
| } |
| |
| synchronized void give() { |
| if (mCurrent <= 0) { |
| Log.wtf(TAG, "We've released this resource too many times"); |
| } |
| mCurrent--; |
| } |
| |
| @Override |
| public String toString() { |
| return new StringBuilder() |
| .append("{mCurrent=") |
| .append(mCurrent) |
| .append(", mMax=") |
| .append(mMax) |
| .append("}") |
| .toString(); |
| } |
| } |
| |
| private static final class UserQuotaTracker { |
| /* Maximum number of UDP Encap Sockets that a single UID may possess */ |
| public static final int MAX_NUM_ENCAP_SOCKETS = 2; |
| |
| /* Maximum number of IPsec Transforms that a single UID may possess */ |
| public static final int MAX_NUM_TRANSFORMS = 4; |
| |
| /* Maximum number of IPsec Transforms that a single UID may possess */ |
| public static final int MAX_NUM_SPIS = 8; |
| |
| /* Record for one users's IpSecService-managed objects */ |
| public static class UserRecord { |
| public final ResourceTracker socket = new ResourceTracker(MAX_NUM_ENCAP_SOCKETS); |
| public final ResourceTracker transform = new ResourceTracker(MAX_NUM_TRANSFORMS); |
| public final ResourceTracker spi = new ResourceTracker(MAX_NUM_SPIS); |
| |
| @Override |
| public String toString() { |
| return new StringBuilder() |
| .append("{socket=") |
| .append(socket) |
| .append(", transform=") |
| .append(transform) |
| .append(", spi=") |
| .append(spi) |
| .append("}") |
| .toString(); |
| } |
| } |
| |
| private final SparseArray<UserRecord> mUserRecords = new SparseArray<>(); |
| |
| /* a never-fail getter so that we can populate the list of UIDs as-needed */ |
| public synchronized UserRecord getUserRecord(int uid) { |
| UserRecord r = mUserRecords.get(uid); |
| if (r == null) { |
| r = new UserRecord(); |
| mUserRecords.put(uid, r); |
| } |
| return r; |
| } |
| |
| @Override |
| public String toString() { |
| return mUserRecords.toString(); |
| } |
| } |
| |
| private final UserQuotaTracker mUserQuotaTracker = new UserQuotaTracker(); |
| |
| /** |
| * The ManagedResource class provides a facility to cleanly and reliably release system |
| * resources. It relies on two things: an IBinder that allows ManagedResource to automatically |
| * clean up in the event that the Binder dies and a user-provided resourceId that should |
| * uniquely identify the managed resource. To use this class, the user should implement the |
| * releaseResources() method that is responsible for releasing system resources when invoked. |
| */ |
| private abstract class ManagedResource implements IBinder.DeathRecipient { |
| final int pid; |
| final int uid; |
| private IBinder mBinder; |
| protected int mResourceId; |
| |
| private AtomicInteger mReferenceCount = new AtomicInteger(0); |
| |
| ManagedResource(int resourceId, IBinder binder) { |
| super(); |
| if (resourceId == INVALID_RESOURCE_ID) { |
| throw new IllegalArgumentException("Resource ID must not be INVALID_RESOURCE_ID"); |
| } |
| mBinder = binder; |
| mResourceId = resourceId; |
| pid = Binder.getCallingPid(); |
| uid = Binder.getCallingUid(); |
| |
| getResourceTracker().take(); |
| try { |
| mBinder.linkToDeath(this, 0); |
| } catch (RemoteException e) { |
| binderDied(); |
| } |
| } |
| |
| public void addReference() { |
| mReferenceCount.incrementAndGet(); |
| } |
| |
| public void removeReference() { |
| if (mReferenceCount.decrementAndGet() < 0) { |
| Log.wtf(TAG, "Programming error: negative reference count"); |
| } |
| } |
| |
| public boolean isReferenced() { |
| return (mReferenceCount.get() > 0); |
| } |
| |
| /** |
| * Ensures that the caller is either the owner of this resource or has the system UID and |
| * throws a SecurityException otherwise. |
| */ |
| public void checkOwnerOrSystem() { |
| if (uid != Binder.getCallingUid() |
| && android.os.Process.SYSTEM_UID != Binder.getCallingUid()) { |
| throw new SecurityException("Only the owner may access managed resources!"); |
| } |
| } |
| |
| /** |
| * When this record is no longer needed for managing system resources this function should |
| * clean up all system resources and nullify the record. This function shall perform all |
| * necessary cleanup of the resources managed by this record. |
| * |
| * <p>NOTE: this function verifies ownership before allowing resources to be freed. |
| */ |
| public final void release() throws RemoteException { |
| synchronized (IpSecService.this) { |
| if (isReferenced()) { |
| throw new IllegalStateException( |
| "Cannot release a resource that has active references!"); |
| } |
| |
| if (mResourceId == INVALID_RESOURCE_ID) { |
| return; |
| } |
| |
| releaseResources(); |
| getResourceTracker().give(); |
| if (mBinder != null) { |
| mBinder.unlinkToDeath(this, 0); |
| } |
| mBinder = null; |
| |
| mResourceId = INVALID_RESOURCE_ID; |
| } |
| } |
| |
| /** |
| * If the Binder object dies, this function is called to free the system resources that are |
| * being managed by this record and to subsequently release this record for garbage |
| * collection |
| */ |
| public final void binderDied() { |
| try { |
| release(); |
| } catch (Exception e) { |
| Log.e(TAG, "Failed to release resource: " + e); |
| } |
| } |
| |
| /** |
| * Implement this method to release all system resources that are being protected by this |
| * record. Once the resources are released, the record should be invalidated and no longer |
| * used by calling release(). This should NEVER be called directly. |
| * |
| * <p>Calls to this are always guarded by IpSecService#this |
| */ |
| protected abstract void releaseResources() throws RemoteException; |
| |
| /** Get the resource tracker for this resource */ |
| protected abstract ResourceTracker getResourceTracker(); |
| |
| @Override |
| public String toString() { |
| return new StringBuilder() |
| .append("{mResourceId=") |
| .append(mResourceId) |
| .append(", pid=") |
| .append(pid) |
| .append(", uid=") |
| .append(uid) |
| .append(", mReferenceCount=") |
| .append(mReferenceCount.get()) |
| .append("}") |
| .toString(); |
| } |
| }; |
| |
| /** |
| * Minimal wrapper around SparseArray that performs ownership validation on element accesses. |
| */ |
| private class ManagedResourceArray<T extends ManagedResource> { |
| SparseArray<T> mArray = new SparseArray<>(); |
| |
| T getAndCheckOwner(int key) { |
| T val = mArray.get(key); |
| // The value should never be null unless the resource doesn't exist |
| // (since we do not allow null resources to be added). |
| if (val != null) { |
| val.checkOwnerOrSystem(); |
| } |
| return val; |
| } |
| |
| void put(int key, T obj) { |
| checkNotNull(obj, "Null resources cannot be added"); |
| mArray.put(key, obj); |
| } |
| |
| void remove(int key) { |
| mArray.remove(key); |
| } |
| |
| @Override |
| public String toString() { |
| return mArray.toString(); |
| } |
| } |
| |
| private final class TransformRecord extends ManagedResource { |
| private final IpSecConfig mConfig; |
| private final SpiRecord[] mSpis; |
| private final UdpSocketRecord mSocket; |
| |
| TransformRecord( |
| int resourceId, |
| IBinder binder, |
| IpSecConfig config, |
| SpiRecord[] spis, |
| UdpSocketRecord socket) { |
| super(resourceId, binder); |
| mConfig = config; |
| mSpis = spis; |
| mSocket = socket; |
| |
| for (int direction : DIRECTIONS) { |
| mSpis[direction].addReference(); |
| mSpis[direction].setOwnedByTransform(); |
| } |
| |
| if (mSocket != null) { |
| mSocket.addReference(); |
| } |
| } |
| |
| public IpSecConfig getConfig() { |
| return mConfig; |
| } |
| |
| public SpiRecord getSpiRecord(int direction) { |
| return mSpis[direction]; |
| } |
| |
| /** always guarded by IpSecService#this */ |
| @Override |
| protected void releaseResources() { |
| for (int direction : DIRECTIONS) { |
| int spi = mSpis[direction].getSpi(); |
| try { |
| mSrvConfig |
| .getNetdInstance() |
| .ipSecDeleteSecurityAssociation( |
| mResourceId, |
| direction, |
| mConfig.getLocalAddress(), |
| mConfig.getRemoteAddress(), |
| spi); |
| } catch (ServiceSpecificException e) { |
| // FIXME: get the error code and throw is at an IOException from Errno Exception |
| } catch (RemoteException e) { |
| Log.e(TAG, "Failed to delete SA with ID: " + mResourceId); |
| } |
| } |
| |
| for (int direction : DIRECTIONS) { |
| mSpis[direction].removeReference(); |
| } |
| |
| if (mSocket != null) { |
| mSocket.removeReference(); |
| } |
| } |
| |
| protected ResourceTracker getResourceTracker() { |
| return mUserQuotaTracker.getUserRecord(this.uid).transform; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder strBuilder = new StringBuilder(); |
| strBuilder |
| .append("{super=") |
| .append(super.toString()) |
| .append(", mSocket=") |
| .append(mSocket) |
| .append(", mSpis[OUT].mResourceId=") |
| .append(mSpis[IpSecTransform.DIRECTION_OUT].mResourceId) |
| .append(", mSpis[IN].mResourceId=") |
| .append(mSpis[IpSecTransform.DIRECTION_IN].mResourceId) |
| .append(", mConfig=") |
| .append(mConfig) |
| .append("}"); |
| return strBuilder.toString(); |
| } |
| } |
| |
| private final class SpiRecord extends ManagedResource { |
| private final int mDirection; |
| private final String mLocalAddress; |
| private final String mRemoteAddress; |
| private int mSpi; |
| |
| private boolean mOwnedByTransform = false; |
| |
| SpiRecord( |
| int resourceId, |
| IBinder binder, |
| int direction, |
| String localAddress, |
| String remoteAddress, |
| int spi) { |
| super(resourceId, binder); |
| mDirection = direction; |
| mLocalAddress = localAddress; |
| mRemoteAddress = remoteAddress; |
| mSpi = spi; |
| } |
| |
| /** always guarded by IpSecService#this */ |
| @Override |
| protected void releaseResources() { |
| if (mOwnedByTransform) { |
| Log.d(TAG, "Cannot release Spi " + mSpi + ": Currently locked by a Transform"); |
| // Because SPIs are "handed off" to transform, objects, they should never be |
| // freed from the SpiRecord once used in a transform. (They refer to the same SA, |
| // thus ownership and responsibility for freeing these resources passes to the |
| // Transform object). Thus, we should let the user free them without penalty once |
| // they are applied in a Transform object. |
| return; |
| } |
| |
| try { |
| mSrvConfig |
| .getNetdInstance() |
| .ipSecDeleteSecurityAssociation( |
| mResourceId, mDirection, mLocalAddress, mRemoteAddress, mSpi); |
| } catch (ServiceSpecificException e) { |
| // FIXME: get the error code and throw is at an IOException from Errno Exception |
| } catch (RemoteException e) { |
| Log.e(TAG, "Failed to delete SPI reservation with ID: " + mResourceId); |
| } |
| |
| mSpi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX; |
| } |
| |
| @Override |
| protected ResourceTracker getResourceTracker() { |
| return mUserQuotaTracker.getUserRecord(this.uid).spi; |
| } |
| |
| public int getSpi() { |
| return mSpi; |
| } |
| |
| public void setOwnedByTransform() { |
| if (mOwnedByTransform) { |
| // Programming error |
| throw new IllegalStateException("Cannot own an SPI twice!"); |
| } |
| |
| mOwnedByTransform = true; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder strBuilder = new StringBuilder(); |
| strBuilder |
| .append("{super=") |
| .append(super.toString()) |
| .append(", mSpi=") |
| .append(mSpi) |
| .append(", mDirection=") |
| .append(mDirection) |
| .append(", mLocalAddress=") |
| .append(mLocalAddress) |
| .append(", mRemoteAddress=") |
| .append(mRemoteAddress) |
| .append(", mOwnedByTransform=") |
| .append(mOwnedByTransform) |
| .append("}"); |
| return strBuilder.toString(); |
| } |
| } |
| |
| private final class UdpSocketRecord extends ManagedResource { |
| private FileDescriptor mSocket; |
| private final int mPort; |
| |
| UdpSocketRecord(int resourceId, IBinder binder, FileDescriptor socket, int port) { |
| super(resourceId, binder); |
| mSocket = socket; |
| mPort = port; |
| } |
| |
| /** always guarded by IpSecService#this */ |
| @Override |
| protected void releaseResources() { |
| Log.d(TAG, "Closing port " + mPort); |
| IoUtils.closeQuietly(mSocket); |
| mSocket = null; |
| } |
| |
| @Override |
| protected ResourceTracker getResourceTracker() { |
| return mUserQuotaTracker.getUserRecord(this.uid).socket; |
| } |
| |
| public int getPort() { |
| return mPort; |
| } |
| |
| public FileDescriptor getSocket() { |
| return mSocket; |
| } |
| |
| @Override |
| public String toString() { |
| return new StringBuilder() |
| .append("{super=") |
| .append(super.toString()) |
| .append(", mSocket=") |
| .append(mSocket) |
| .append(", mPort=") |
| .append(mPort) |
| .append("}") |
| .toString(); |
| } |
| } |
| |
| /** |
| * Constructs a new IpSecService instance |
| * |
| * @param context Binder context for this service |
| */ |
| private IpSecService(Context context) { |
| this(context, IpSecServiceConfiguration.GETSRVINSTANCE); |
| } |
| |
| static IpSecService create(Context context) throws InterruptedException { |
| final IpSecService service = new IpSecService(context); |
| service.connectNativeNetdService(); |
| return service; |
| } |
| |
| /** @hide */ |
| @VisibleForTesting |
| public IpSecService(Context context, IpSecServiceConfiguration config) { |
| mContext = context; |
| mSrvConfig = config; |
| } |
| |
| public void systemReady() { |
| if (isNetdAlive()) { |
| Slog.d(TAG, "IpSecService is ready"); |
| } else { |
| Slog.wtf(TAG, "IpSecService not ready: failed to connect to NetD Native Service!"); |
| } |
| } |
| |
| private void connectNativeNetdService() { |
| // Avoid blocking the system server to do this |
| new Thread() { |
| @Override |
| public void run() { |
| synchronized (IpSecService.this) { |
| NetdService.get(NETD_FETCH_TIMEOUT_MS); |
| } |
| } |
| }.start(); |
| } |
| |
| synchronized boolean isNetdAlive() { |
| try { |
| final INetd netd = mSrvConfig.getNetdInstance(); |
| if (netd == null) { |
| return false; |
| } |
| return netd.isAlive(); |
| } catch (RemoteException re) { |
| return false; |
| } |
| } |
| |
| /** |
| * Checks that the provided InetAddress is valid for use in an IPsec SA. The address must not be |
| * a wildcard address and must be in a numeric form such as 1.2.3.4 or 2001::1. |
| */ |
| private static void checkInetAddress(String inetAddress) { |
| if (TextUtils.isEmpty(inetAddress)) { |
| throw new IllegalArgumentException("Unspecified address"); |
| } |
| |
| InetAddress checkAddr = NetworkUtils.numericToInetAddress(inetAddress); |
| |
| if (checkAddr.isAnyLocalAddress()) { |
| throw new IllegalArgumentException("Inappropriate wildcard address: " + inetAddress); |
| } |
| } |
| |
| /** |
| * Checks the user-provided direction field and throws an IllegalArgumentException if it is not |
| * DIRECTION_IN or DIRECTION_OUT |
| */ |
| private static void checkDirection(int direction) { |
| switch (direction) { |
| case IpSecTransform.DIRECTION_OUT: |
| case IpSecTransform.DIRECTION_IN: |
| return; |
| } |
| throw new IllegalArgumentException("Invalid Direction: " + direction); |
| } |
| |
| @Override |
| /** Get a new SPI and maintain the reservation in the system server */ |
| public synchronized IpSecSpiResponse reserveSecurityParameterIndex( |
| int direction, String remoteAddress, int requestedSpi, IBinder binder) |
| throws RemoteException { |
| checkDirection(direction); |
| checkInetAddress(remoteAddress); |
| /* requestedSpi can be anything in the int range, so no check is needed. */ |
| checkNotNull(binder, "Null Binder passed to reserveSecurityParameterIndex"); |
| |
| int resourceId = mNextResourceId.getAndIncrement(); |
| |
| int spi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX; |
| String localAddress = ""; |
| |
| try { |
| if (!mUserQuotaTracker.getUserRecord(Binder.getCallingUid()).spi.isAvailable()) { |
| return new IpSecSpiResponse( |
| IpSecManager.Status.RESOURCE_UNAVAILABLE, INVALID_RESOURCE_ID, spi); |
| } |
| spi = |
| mSrvConfig |
| .getNetdInstance() |
| .ipSecAllocateSpi( |
| resourceId, |
| direction, |
| localAddress, |
| remoteAddress, |
| requestedSpi); |
| Log.d(TAG, "Allocated SPI " + spi); |
| mSpiRecords.put( |
| resourceId, |
| new SpiRecord(resourceId, binder, direction, localAddress, remoteAddress, spi)); |
| } catch (ServiceSpecificException e) { |
| // TODO: Add appropriate checks when other ServiceSpecificException types are supported |
| return new IpSecSpiResponse( |
| IpSecManager.Status.SPI_UNAVAILABLE, INVALID_RESOURCE_ID, spi); |
| } catch (RemoteException e) { |
| throw e.rethrowFromSystemServer(); |
| } |
| return new IpSecSpiResponse(IpSecManager.Status.OK, resourceId, spi); |
| } |
| |
| /* This method should only be called from Binder threads. Do not call this from |
| * within the system server as it will crash the system on failure. |
| */ |
| private synchronized <T extends ManagedResource> void releaseManagedResource( |
| ManagedResourceArray<T> resArray, int resourceId, String typeName) |
| throws RemoteException { |
| // We want to non-destructively get so that we can check credentials before removing |
| // this from the records. |
| T record = resArray.getAndCheckOwner(resourceId); |
| |
| if (record == null) { |
| throw new IllegalArgumentException( |
| typeName + " " + resourceId + " is not available to be deleted"); |
| } |
| |
| record.release(); |
| resArray.remove(resourceId); |
| } |
| |
| /** Release a previously allocated SPI that has been registered with the system server */ |
| @Override |
| public void releaseSecurityParameterIndex(int resourceId) throws RemoteException { |
| releaseManagedResource(mSpiRecords, resourceId, "SecurityParameterIndex"); |
| } |
| |
| /** |
| * This function finds and forcibly binds to a random system port, ensuring that the port cannot |
| * be unbound. |
| * |
| * <p>A socket cannot be un-bound from a port if it was bound to that port by number. To select |
| * a random open port and then bind by number, this function creates a temp socket, binds to a |
| * random port (specifying 0), gets that port number, and then uses is to bind the user's UDP |
| * Encapsulation Socket forcibly, so that it cannot be un-bound by the user with the returned |
| * FileHandle. |
| * |
| * <p>The loop in this function handles the inherent race window between un-binding to a port |
| * and re-binding, during which the system could *technically* hand that port out to someone |
| * else. |
| */ |
| private int bindToRandomPort(FileDescriptor sockFd) throws IOException { |
| for (int i = MAX_PORT_BIND_ATTEMPTS; i > 0; i--) { |
| try { |
| FileDescriptor probeSocket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); |
| Os.bind(probeSocket, INADDR_ANY, 0); |
| int port = ((InetSocketAddress) Os.getsockname(probeSocket)).getPort(); |
| Os.close(probeSocket); |
| Log.v(TAG, "Binding to port " + port); |
| Os.bind(sockFd, INADDR_ANY, port); |
| return port; |
| } catch (ErrnoException e) { |
| // Someone miraculously claimed the port just after we closed probeSocket. |
| if (e.errno == OsConstants.EADDRINUSE) { |
| continue; |
| } |
| throw e.rethrowAsIOException(); |
| } |
| } |
| throw new IOException("Failed " + MAX_PORT_BIND_ATTEMPTS + " attempts to bind to a port"); |
| } |
| |
| /** |
| * Open a socket via the system server and bind it to the specified port (random if port=0). |
| * This will return a PFD to the user that represent a bound UDP socket. The system server will |
| * cache the socket and a record of its owner so that it can and must be freed when no longer |
| * needed. |
| */ |
| @Override |
| public synchronized IpSecUdpEncapResponse openUdpEncapsulationSocket(int port, IBinder binder) |
| throws RemoteException { |
| if (port != 0 && (port < FREE_PORT_MIN || port > PORT_MAX)) { |
| throw new IllegalArgumentException( |
| "Specified port number must be a valid non-reserved UDP port"); |
| } |
| checkNotNull(binder, "Null Binder passed to openUdpEncapsulationSocket"); |
| |
| int resourceId = mNextResourceId.getAndIncrement(); |
| FileDescriptor sockFd = null; |
| try { |
| if (!mUserQuotaTracker.getUserRecord(Binder.getCallingUid()).socket.isAvailable()) { |
| return new IpSecUdpEncapResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE); |
| } |
| |
| sockFd = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); |
| |
| if (port != 0) { |
| Log.v(TAG, "Binding to port " + port); |
| Os.bind(sockFd, INADDR_ANY, port); |
| } else { |
| port = bindToRandomPort(sockFd); |
| } |
| // This code is common to both the unspecified and specified port cases |
| Os.setsockoptInt( |
| sockFd, |
| OsConstants.IPPROTO_UDP, |
| OsConstants.UDP_ENCAP, |
| OsConstants.UDP_ENCAP_ESPINUDP); |
| |
| mUdpSocketRecords.put( |
| resourceId, new UdpSocketRecord(resourceId, binder, sockFd, port)); |
| return new IpSecUdpEncapResponse(IpSecManager.Status.OK, resourceId, port, sockFd); |
| } catch (IOException | ErrnoException e) { |
| IoUtils.closeQuietly(sockFd); |
| } |
| // If we make it to here, then something has gone wrong and we couldn't open a socket. |
| // The only reasonable condition that would cause that is resource unavailable. |
| return new IpSecUdpEncapResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE); |
| } |
| |
| /** close a socket that has been been allocated by and registered with the system server */ |
| @Override |
| public void closeUdpEncapsulationSocket(int resourceId) throws RemoteException { |
| |
| releaseManagedResource(mUdpSocketRecords, resourceId, "UdpEncapsulationSocket"); |
| } |
| |
| /** |
| * Checks an IpSecConfig parcel to ensure that the contents are sane and throws an |
| * IllegalArgumentException if they are not. |
| */ |
| private void checkIpSecConfig(IpSecConfig config) { |
| if (config.getLocalAddress() == null) { |
| throw new IllegalArgumentException("Invalid null Local InetAddress"); |
| } |
| |
| if (config.getRemoteAddress() == null) { |
| throw new IllegalArgumentException("Invalid null Remote InetAddress"); |
| } |
| |
| switch (config.getMode()) { |
| case IpSecTransform.MODE_TRANSPORT: |
| if (!config.getLocalAddress().isEmpty()) { |
| throw new IllegalArgumentException("Non-empty Local Address"); |
| } |
| // Must be valid, and not a wildcard |
| checkInetAddress(config.getRemoteAddress()); |
| break; |
| case IpSecTransform.MODE_TUNNEL: |
| break; |
| default: |
| throw new IllegalArgumentException( |
| "Invalid IpSecTransform.mode: " + config.getMode()); |
| } |
| |
| switch (config.getEncapType()) { |
| case IpSecTransform.ENCAP_NONE: |
| break; |
| case IpSecTransform.ENCAP_ESPINUDP: |
| case IpSecTransform.ENCAP_ESPINUDP_NON_IKE: |
| if (mUdpSocketRecords.getAndCheckOwner( |
| config.getEncapSocketResourceId()) == null) { |
| throw new IllegalStateException( |
| "No Encapsulation socket for Resource Id: " |
| + config.getEncapSocketResourceId()); |
| } |
| |
| int port = config.getEncapRemotePort(); |
| if (port <= 0 || port > 0xFFFF) { |
| throw new IllegalArgumentException("Invalid remote UDP port: " + port); |
| } |
| break; |
| default: |
| throw new IllegalArgumentException("Invalid Encap Type: " + config.getEncapType()); |
| } |
| |
| for (int direction : DIRECTIONS) { |
| IpSecAlgorithm crypt = config.getEncryption(direction); |
| IpSecAlgorithm auth = config.getAuthentication(direction); |
| IpSecAlgorithm authenticatedEncryption = config.getAuthenticatedEncryption(direction); |
| if (authenticatedEncryption == null && crypt == null && auth == null) { |
| throw new IllegalArgumentException( |
| "No Encryption or Authentication algorithms specified"); |
| } else if (authenticatedEncryption != null && (auth != null || crypt != null)) { |
| throw new IllegalArgumentException( |
| "Authenticated Encryption is mutually" |
| + " exclusive with other Authentication or Encryption algorithms"); |
| } |
| |
| if (mSpiRecords.getAndCheckOwner(config.getSpiResourceId(direction)) == null) { |
| throw new IllegalStateException("No SPI for specified Resource Id"); |
| } |
| } |
| } |
| |
| /** |
| * Create a transport mode transform, which represent two security associations (one in each |
| * direction) in the kernel. The transform will be cached by the system server and must be freed |
| * when no longer needed. It is possible to free one, deleting the SA from underneath sockets |
| * that are using it, which will result in all of those sockets becoming unable to send or |
| * receive data. |
| */ |
| @Override |
| public synchronized IpSecTransformResponse createTransportModeTransform( |
| IpSecConfig c, IBinder binder) throws RemoteException { |
| checkIpSecConfig(c); |
| checkNotNull(binder, "Null Binder passed to createTransportModeTransform"); |
| int resourceId = mNextResourceId.getAndIncrement(); |
| if (!mUserQuotaTracker.getUserRecord(Binder.getCallingUid()).transform.isAvailable()) { |
| return new IpSecTransformResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE); |
| } |
| SpiRecord[] spis = new SpiRecord[DIRECTIONS.length]; |
| |
| int encapType, encapLocalPort = 0, encapRemotePort = 0; |
| UdpSocketRecord socketRecord = null; |
| encapType = c.getEncapType(); |
| if (encapType != IpSecTransform.ENCAP_NONE) { |
| socketRecord = mUdpSocketRecords.getAndCheckOwner(c.getEncapSocketResourceId()); |
| encapLocalPort = socketRecord.getPort(); |
| encapRemotePort = c.getEncapRemotePort(); |
| } |
| |
| for (int direction : DIRECTIONS) { |
| IpSecAlgorithm auth = c.getAuthentication(direction); |
| IpSecAlgorithm crypt = c.getEncryption(direction); |
| IpSecAlgorithm authCrypt = c.getAuthenticatedEncryption(direction); |
| |
| spis[direction] = mSpiRecords.getAndCheckOwner(c.getSpiResourceId(direction)); |
| int spi = spis[direction].getSpi(); |
| try { |
| mSrvConfig |
| .getNetdInstance() |
| .ipSecAddSecurityAssociation( |
| resourceId, |
| c.getMode(), |
| direction, |
| c.getLocalAddress(), |
| c.getRemoteAddress(), |
| (c.getNetwork() != null) ? c.getNetwork().getNetworkHandle() : 0, |
| spi, |
| (auth != null) ? auth.getName() : "", |
| (auth != null) ? auth.getKey() : null, |
| (auth != null) ? auth.getTruncationLengthBits() : 0, |
| (crypt != null) ? crypt.getName() : "", |
| (crypt != null) ? crypt.getKey() : null, |
| (crypt != null) ? crypt.getTruncationLengthBits() : 0, |
| (authCrypt != null) ? authCrypt.getName() : "", |
| (authCrypt != null) ? authCrypt.getKey() : null, |
| (authCrypt != null) ? authCrypt.getTruncationLengthBits() : 0, |
| encapType, |
| encapLocalPort, |
| encapRemotePort); |
| } catch (ServiceSpecificException e) { |
| // FIXME: get the error code and throw is at an IOException from Errno Exception |
| return new IpSecTransformResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE); |
| } |
| } |
| // Both SAs were created successfully, time to construct a record and lock it away |
| mTransformRecords.put( |
| resourceId, new TransformRecord(resourceId, binder, c, spis, socketRecord)); |
| return new IpSecTransformResponse(IpSecManager.Status.OK, resourceId); |
| } |
| |
| /** |
| * Delete a transport mode transform that was previously allocated by + registered with the |
| * system server. If this is called on an inactive (or non-existent) transform, it will not |
| * return an error. It's safe to de-allocate transforms that may have already been deleted for |
| * other reasons. |
| */ |
| @Override |
| public void deleteTransportModeTransform(int resourceId) throws RemoteException { |
| releaseManagedResource(mTransformRecords, resourceId, "IpSecTransform"); |
| } |
| |
| /** |
| * Apply an active transport mode transform to a socket, which will apply the IPsec security |
| * association as a correspondent policy to the provided socket |
| */ |
| @Override |
| public synchronized void applyTransportModeTransform( |
| ParcelFileDescriptor socket, int resourceId) throws RemoteException { |
| // Synchronize liberally here because we are using ManagedResources in this block |
| TransformRecord info; |
| // FIXME: this code should be factored out into a security check + getter |
| info = mTransformRecords.getAndCheckOwner(resourceId); |
| |
| if (info == null) { |
| throw new IllegalArgumentException("Transform " + resourceId + " is not active"); |
| } |
| |
| // TODO: make this a function. |
| if (info.pid != getCallingPid() || info.uid != getCallingUid()) { |
| throw new SecurityException("Only the owner of an IpSec Transform may apply it!"); |
| } |
| |
| IpSecConfig c = info.getConfig(); |
| try { |
| for (int direction : DIRECTIONS) { |
| mSrvConfig |
| .getNetdInstance() |
| .ipSecApplyTransportModeTransform( |
| socket.getFileDescriptor(), |
| resourceId, |
| direction, |
| c.getLocalAddress(), |
| c.getRemoteAddress(), |
| info.getSpiRecord(direction).getSpi()); |
| } |
| } catch (ServiceSpecificException e) { |
| // FIXME: get the error code and throw is at an IOException from Errno Exception |
| } |
| } |
| |
| /** |
| * Remove a transport mode transform from a socket, applying the default (empty) policy. This |
| * will ensure that NO IPsec policy is applied to the socket (would be the equivalent of |
| * applying a policy that performs no IPsec). Today the resourceId parameter is passed but not |
| * used: reserved for future improved input validation. |
| */ |
| @Override |
| public void removeTransportModeTransform(ParcelFileDescriptor socket, int resourceId) |
| throws RemoteException { |
| try { |
| mSrvConfig |
| .getNetdInstance() |
| .ipSecRemoveTransportModeTransform(socket.getFileDescriptor()); |
| } catch (ServiceSpecificException e) { |
| // FIXME: get the error code and throw is at an IOException from Errno Exception |
| } |
| } |
| |
| @Override |
| protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { |
| mContext.enforceCallingOrSelfPermission(DUMP, TAG); |
| |
| pw.println("IpSecService dump:"); |
| pw.println("NetdNativeService Connection: " + (isNetdAlive() ? "alive" : "dead")); |
| pw.println(); |
| |
| pw.println("mUserQuotaTracker:"); |
| pw.println(mUserQuotaTracker); |
| pw.println("mTransformRecords:"); |
| pw.println(mTransformRecords); |
| pw.println("mUdpSocketRecords:"); |
| pw.println(mUdpSocketRecords); |
| pw.println("mSpiRecords:"); |
| pw.println(mSpiRecords); |
| } |
| } |