Java程序  |  495行  |  19.59 KB

/*
 * 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();
    }
}