/* * 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.net.IpSecManager.KEY_RESOURCE_ID; import static android.net.IpSecManager.KEY_SPI; import static android.net.IpSecManager.KEY_STATUS; 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.IpSecTransform; import android.net.util.NetdService; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceSpecificException; import android.util.Log; import android.util.Slog; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.concurrent.atomic.AtomicInteger; /** @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}; /** Binder context for this service */ private final Context mContext; private Object mLock = new Object(); private static final int NETD_FETCH_TIMEOUT = 5000; //ms private AtomicInteger mNextResourceId = new AtomicInteger(0x00FADED0); private abstract class ManagedResource implements IBinder.DeathRecipient { final int pid; final int uid; private IBinder mBinder; ManagedResource(IBinder binder) { super(); mBinder = binder; pid = Binder.getCallingPid(); uid = Binder.getCallingUid(); try { mBinder.linkToDeath(this, 0); } catch (RemoteException e) { binderDied(); } } /** * When this record is no longer needed for managing system resources this function should * unlink all references held by the record to allow efficient garbage collection. */ public final void release() { //Release all the underlying system resources first releaseResources(); if (mBinder != null) { mBinder.unlinkToDeath(this, 0); } mBinder = null; //remove this record so that it can be cleaned up nullifyRecord(); } /** * 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() { release(); } /** * Implement this method to release all object references contained in the subclass to allow * efficient garbage collection of the record. This should remove any references to the * record from all other locations that hold a reference as the record is no longer valid. */ protected abstract void nullifyRecord(); /** * 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 releaseRecord() */ protected abstract void releaseResources(); }; private final class TransformRecord extends ManagedResource { private IpSecConfig mConfig; private int mResourceId; TransformRecord(IpSecConfig config, int resourceId, IBinder binder) { super(binder); mConfig = config; mResourceId = resourceId; } public IpSecConfig getConfig() { return mConfig; } @Override protected void releaseResources() { for (int direction : DIRECTIONS) { try { getNetdInstance() .ipSecDeleteSecurityAssociation( mResourceId, direction, (mConfig.getLocalAddress() != null) ? mConfig.getLocalAddress().getHostAddress() : "", (mConfig.getRemoteAddress() != null) ? mConfig.getRemoteAddress().getHostAddress() : "", mConfig.getSpi(direction)); } 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); } } } @Override protected void nullifyRecord() { mConfig = null; mResourceId = INVALID_RESOURCE_ID; } } private final class SpiRecord extends ManagedResource { private final int mDirection; private final String mLocalAddress; private final String mRemoteAddress; private final IBinder mBinder; private int mSpi; private int mResourceId; SpiRecord( int resourceId, int direction, String localAddress, String remoteAddress, int spi, IBinder binder) { super(binder); mResourceId = resourceId; mDirection = direction; mLocalAddress = localAddress; mRemoteAddress = remoteAddress; mSpi = spi; mBinder = binder; } protected void releaseResources() { try { 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); } } protected void nullifyRecord() { mSpi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX; mResourceId = INVALID_RESOURCE_ID; } } @GuardedBy("mSpiRecords") private final SparseArray<SpiRecord> mSpiRecords = new SparseArray<>(); @GuardedBy("mTransformRecords") private final SparseArray<TransformRecord> mTransformRecords = new SparseArray<>(); /** * Constructs a new IpSecService instance * * @param context Binder context for this service */ private IpSecService(Context context) { mContext = context; } static IpSecService create(Context context) throws InterruptedException { final IpSecService service = new IpSecService(context); service.connectNativeNetdService(); return service; } 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 Thread t = new Thread( new Runnable() { @Override public void run() { synchronized (mLock) { NetdService.get(NETD_FETCH_TIMEOUT); } } }); t.run(); } INetd getNetdInstance() throws RemoteException { final INetd netd = NetdService.getInstance(); if (netd == null) { throw new RemoteException("Failed to Get Netd Instance"); } return netd; } boolean isNetdAlive() { synchronized (mLock) { try { final INetd netd = getNetdInstance(); if (netd == null) { return false; } return netd.isAlive(); } catch (RemoteException re) { return false; } } } @Override /** Get a new SPI and maintain the reservation in the system server */ public Bundle reserveSecurityParameterIndex( int direction, String remoteAddress, int requestedSpi, IBinder binder) throws RemoteException { int resourceId = mNextResourceId.getAndIncrement(); int spi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX; String localAddress = ""; Bundle retBundle = new Bundle(3); try { spi = getNetdInstance() .ipSecAllocateSpi( resourceId, direction, localAddress, remoteAddress, requestedSpi); Log.d(TAG, "Allocated SPI " + spi); retBundle.putInt(KEY_STATUS, IpSecManager.Status.OK); retBundle.putInt(KEY_RESOURCE_ID, resourceId); retBundle.putInt(KEY_SPI, spi); synchronized (mSpiRecords) { mSpiRecords.put( resourceId, new SpiRecord( resourceId, direction, localAddress, remoteAddress, spi, binder)); } } catch (ServiceSpecificException e) { // TODO: Add appropriate checks when other ServiceSpecificException types are supported retBundle.putInt(KEY_STATUS, IpSecManager.Status.SPI_UNAVAILABLE); retBundle.putInt(KEY_RESOURCE_ID, resourceId); retBundle.putInt(KEY_SPI, spi); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } return retBundle; } /** Release a previously allocated SPI that has been registered with the system server */ @Override public void releaseSecurityParameterIndex(int resourceId) throws RemoteException {} /** * 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 Bundle openUdpEncapsulationSocket(int port, IBinder binder) throws RemoteException { return null; } /** close a socket that has been been allocated by and registered with the system server */ @Override public void closeUdpEncapsulationSocket(ParcelFileDescriptor socket) {} /** * 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 Bundle createTransportModeTransform(IpSecConfig c, IBinder binder) throws RemoteException { // TODO: Basic input validation here since it's coming over the Binder int resourceId = mNextResourceId.getAndIncrement(); for (int direction : DIRECTIONS) { IpSecAlgorithm auth = c.getAuthentication(direction); IpSecAlgorithm crypt = c.getEncryption(direction); try { int result = getNetdInstance() .ipSecAddSecurityAssociation( resourceId, c.getMode(), direction, (c.getLocalAddress() != null) ? c.getLocalAddress().getHostAddress() : "", (c.getRemoteAddress() != null) ? c.getRemoteAddress().getHostAddress() : "", (c.getNetwork() != null) ? c.getNetwork().getNetworkHandle() : 0, c.getSpi(direction), (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, c.getEncapType(), c.getEncapLocalPort(), c.getEncapRemotePort()); if (result != c.getSpi(direction)) { // TODO: cleanup the first SA if creation of second SA fails Bundle retBundle = new Bundle(2); retBundle.putInt(KEY_STATUS, IpSecManager.Status.SPI_UNAVAILABLE); retBundle.putInt(KEY_RESOURCE_ID, INVALID_RESOURCE_ID); return retBundle; } } catch (ServiceSpecificException e) { // FIXME: get the error code and throw is at an IOException from Errno Exception } } synchronized (mTransformRecords) { mTransformRecords.put(resourceId, new TransformRecord(c, resourceId, binder)); } Bundle retBundle = new Bundle(2); retBundle.putInt(KEY_STATUS, IpSecManager.Status.OK); retBundle.putInt(KEY_RESOURCE_ID, resourceId); return retBundle; } /** * 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 { synchronized (mTransformRecords) { TransformRecord record; // We want to non-destructively get so that we can check credentials before removing // this from the records. record = mTransformRecords.get(resourceId); if (record == null) { throw new IllegalArgumentException( "Transform " + resourceId + " is not available to be deleted"); } if (record.pid != Binder.getCallingPid() || record.uid != Binder.getCallingUid()) { throw new SecurityException("Only the owner of an IpSec Transform may delete it!"); } // TODO: if releaseResources() throws RemoteException, we can try again to clean up on // binder death. Need to make sure that path is actually functional. record.releaseResources(); mTransformRecords.remove(resourceId); record.nullifyRecord(); } } /** * 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 void applyTransportModeTransform(ParcelFileDescriptor socket, int resourceId) throws RemoteException { synchronized (mTransformRecords) { TransformRecord info; // FIXME: this code should be factored out into a security check + getter info = mTransformRecords.get(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) { getNetdInstance() .ipSecApplyTransportModeTransform( socket.getFileDescriptor(), resourceId, direction, (c.getLocalAddress() != null) ? c.getLocalAddress().getHostAddress() : "", (c.getRemoteAddress() != null) ? c.getRemoteAddress().getHostAddress() : "", c.getSpi(direction)); } } 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 { getNetdInstance().ipSecRemoveTransportModeTransform(socket.getFileDescriptor()); } catch (ServiceSpecificException e) { // FIXME: get the error code and throw is at an IOException from Errno Exception } } @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { mContext.enforceCallingOrSelfPermission(DUMP, TAG); pw.println("IpSecService Log:"); pw.println("NetdNativeService Connection: " + (isNetdAlive() ? "alive" : "dead")); pw.println(); } }