/* * 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 android.net; import static android.net.IpSecManager.INVALID_RESOURCE_ID; import static com.android.internal.util.Preconditions.checkNotNull; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.content.Context; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import dalvik.system.CloseGuard; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.net.InetAddress; /** * This class represents a transform, which roughly corresponds to an IPsec Security Association. * *

Transforms are created using {@link IpSecTransform.Builder}. Each {@code IpSecTransform} * object encapsulates the properties and state of an IPsec security association. That includes, * but is not limited to, algorithm choice, key material, and allocated system resources. * * @see RFC 4301, Security Architecture for the * Internet Protocol */ public final class IpSecTransform implements AutoCloseable { private static final String TAG = "IpSecTransform"; /** @hide */ public static final int MODE_TRANSPORT = 0; /** @hide */ public static final int MODE_TUNNEL = 1; /** @hide */ public static final int ENCAP_NONE = 0; /** * IPsec traffic will be encapsulated within UDP, but with 8 zero-value bytes between the UDP * header and payload. This prevents traffic from being interpreted as ESP or IKEv2. * * @hide */ public static final int ENCAP_ESPINUDP_NON_IKE = 1; /** * IPsec traffic will be encapsulated within UDP as per * RFC 3498. * * @hide */ public static final int ENCAP_ESPINUDP = 2; /** @hide */ @IntDef(value = {ENCAP_NONE, ENCAP_ESPINUDP, ENCAP_ESPINUDP_NON_IKE}) @Retention(RetentionPolicy.SOURCE) public @interface EncapType {} /** @hide */ @VisibleForTesting public IpSecTransform(Context context, IpSecConfig config) { mContext = context; mConfig = new IpSecConfig(config); mResourceId = INVALID_RESOURCE_ID; } private IIpSecService getIpSecService() { IBinder b = ServiceManager.getService(android.content.Context.IPSEC_SERVICE); if (b == null) { throw new RemoteException("Failed to connect to IpSecService") .rethrowAsRuntimeException(); } return IIpSecService.Stub.asInterface(b); } /** * Checks the result status and throws an appropriate exception if the status is not Status.OK. */ private void checkResultStatus(int status) throws IOException, IpSecManager.ResourceUnavailableException, IpSecManager.SpiUnavailableException { switch (status) { case IpSecManager.Status.OK: return; // TODO: Pass Error string back from bundle so that errors can be more specific case IpSecManager.Status.RESOURCE_UNAVAILABLE: throw new IpSecManager.ResourceUnavailableException( "Failed to allocate a new IpSecTransform"); case IpSecManager.Status.SPI_UNAVAILABLE: Log.wtf(TAG, "Attempting to use an SPI that was somehow not reserved"); // Fall through default: throw new IllegalStateException( "Failed to Create a Transform with status code " + status); } } private IpSecTransform activate() throws IOException, IpSecManager.ResourceUnavailableException, IpSecManager.SpiUnavailableException { synchronized (this) { try { IIpSecService svc = getIpSecService(); IpSecTransformResponse result = svc.createTransform(mConfig, new Binder()); int status = result.status; checkResultStatus(status); mResourceId = result.resourceId; Log.d(TAG, "Added Transform with Id " + mResourceId); mCloseGuard.open("build"); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } return this; } /** * Equals method used for testing * * @hide */ @VisibleForTesting public static boolean equals(IpSecTransform lhs, IpSecTransform rhs) { if (lhs == null || rhs == null) return (lhs == rhs); return IpSecConfig.equals(lhs.getConfig(), rhs.getConfig()) && lhs.mResourceId == rhs.mResourceId; } /** * Deactivate this {@code IpSecTransform} and free allocated resources. * *

Deactivating a transform while it is still applied to a socket will result in errors on * that socket. Make sure to remove transforms by calling {@link * IpSecManager#removeTransportModeTransforms}. Note, removing an {@code IpSecTransform} from a * socket will not deactivate it (because one transform may be applied to multiple sockets). * *

It is safe to call this method on a transform that has already been deactivated. */ public void close() { Log.d(TAG, "Removing Transform with Id " + mResourceId); // Always safe to attempt cleanup if (mResourceId == INVALID_RESOURCE_ID) { mCloseGuard.close(); return; } try { IIpSecService svc = getIpSecService(); svc.deleteTransform(mResourceId); stopNattKeepalive(); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } finally { mResourceId = INVALID_RESOURCE_ID; mCloseGuard.close(); } } /** Check that the transform was closed properly. */ @Override protected void finalize() throws Throwable { if (mCloseGuard != null) { mCloseGuard.warnIfOpen(); } close(); } /* Package */ IpSecConfig getConfig() { return mConfig; } private final IpSecConfig mConfig; private int mResourceId; private final Context mContext; private final CloseGuard mCloseGuard = CloseGuard.get(); private ConnectivityManager.PacketKeepalive mKeepalive; private Handler mCallbackHandler; private final ConnectivityManager.PacketKeepaliveCallback mKeepaliveCallback = new ConnectivityManager.PacketKeepaliveCallback() { @Override public void onStarted() { synchronized (this) { mCallbackHandler.post(() -> mUserKeepaliveCallback.onStarted()); } } @Override public void onStopped() { synchronized (this) { mKeepalive = null; mCallbackHandler.post(() -> mUserKeepaliveCallback.onStopped()); } } @Override public void onError(int error) { synchronized (this) { mKeepalive = null; mCallbackHandler.post(() -> mUserKeepaliveCallback.onError(error)); } } }; private NattKeepaliveCallback mUserKeepaliveCallback; /** @hide */ @VisibleForTesting public int getResourceId() { return mResourceId; } /** * A callback class to provide status information regarding a NAT-T keepalive session * *

Use this callback to receive status information regarding a NAT-T keepalive session * by registering it when calling {@link #startNattKeepalive}. * * @hide */ @SystemApi public static class NattKeepaliveCallback { /** The specified {@code Network} is not connected. */ public static final int ERROR_INVALID_NETWORK = 1; /** The hardware does not support this request. */ public static final int ERROR_HARDWARE_UNSUPPORTED = 2; /** The hardware returned an error. */ public static final int ERROR_HARDWARE_ERROR = 3; /** The requested keepalive was successfully started. */ public void onStarted() {} /** The keepalive was successfully stopped. */ public void onStopped() {} /** An error occurred. */ public void onError(int error) {} } /** * Start a NAT-T keepalive session for the current transform. * * For a transform that is using UDP encapsulated IPv4, NAT-T offloading provides * a power efficient mechanism of sending NAT-T packets at a specified interval. * * @param userCallback a {@link #NattKeepaliveCallback} to receive asynchronous status * information about the requested NAT-T keepalive session. * @param intervalSeconds the interval between NAT-T keepalives being sent. The * the allowed range is between 20 and 3600 seconds. * @param handler a handler on which to post callbacks when received. * * @hide */ @SystemApi @RequiresPermission(anyOf = { android.Manifest.permission.MANAGE_IPSEC_TUNNELS, android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD }) public void startNattKeepalive(@NonNull NattKeepaliveCallback userCallback, int intervalSeconds, @NonNull Handler handler) throws IOException { checkNotNull(userCallback); if (intervalSeconds < 20 || intervalSeconds > 3600) { throw new IllegalArgumentException("Invalid NAT-T keepalive interval"); } checkNotNull(handler); if (mResourceId == INVALID_RESOURCE_ID) { throw new IllegalStateException( "Packet keepalive cannot be started for an inactive transform"); } synchronized (mKeepaliveCallback) { if (mKeepaliveCallback != null) { throw new IllegalStateException("Keepalive already active"); } mUserKeepaliveCallback = userCallback; ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService( Context.CONNECTIVITY_SERVICE); mKeepalive = cm.startNattKeepalive( mConfig.getNetwork(), intervalSeconds, mKeepaliveCallback, NetworkUtils.numericToInetAddress(mConfig.getSourceAddress()), 4500, // FIXME urgently, we need to get the port number from the Encap socket NetworkUtils.numericToInetAddress(mConfig.getDestinationAddress())); mCallbackHandler = handler; } } /** * Stop an ongoing NAT-T keepalive session. * * Calling this API will request that an ongoing NAT-T keepalive session be terminated. * If this API is not called when a Transform is closed, the underlying NAT-T session will * be terminated automatically. * * @hide */ @SystemApi @RequiresPermission(anyOf = { android.Manifest.permission.MANAGE_IPSEC_TUNNELS, android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD }) public void stopNattKeepalive() { synchronized (mKeepaliveCallback) { if (mKeepalive == null) { Log.e(TAG, "No active keepalive to stop"); return; } mKeepalive.stop(); } } /** This class is used to build {@link IpSecTransform} objects. */ public static class Builder { private Context mContext; private IpSecConfig mConfig; /** * Set the encryption algorithm. * *

Encryption is mutually exclusive with authenticated encryption. * * @param algo {@link IpSecAlgorithm} specifying the encryption to be applied. */ @NonNull public IpSecTransform.Builder setEncryption(@NonNull IpSecAlgorithm algo) { // TODO: throw IllegalArgumentException if algo is not an encryption algorithm. Preconditions.checkNotNull(algo); mConfig.setEncryption(algo); return this; } /** * Set the authentication (integrity) algorithm. * *

Authentication is mutually exclusive with authenticated encryption. * * @param algo {@link IpSecAlgorithm} specifying the authentication to be applied. */ @NonNull public IpSecTransform.Builder setAuthentication(@NonNull IpSecAlgorithm algo) { // TODO: throw IllegalArgumentException if algo is not an authentication algorithm. Preconditions.checkNotNull(algo); mConfig.setAuthentication(algo); return this; } /** * Set the authenticated encryption algorithm. * *

The Authenticated Encryption (AE) class of algorithms are also known as * Authenticated Encryption with Associated Data (AEAD) algorithms, or Combined mode * algorithms (as referred to in * RFC 4301). * *

Authenticated encryption is mutually exclusive with encryption and authentication. * * @param algo {@link IpSecAlgorithm} specifying the authenticated encryption algorithm to * be applied. */ @NonNull public IpSecTransform.Builder setAuthenticatedEncryption(@NonNull IpSecAlgorithm algo) { Preconditions.checkNotNull(algo); mConfig.setAuthenticatedEncryption(algo); return this; } /** * Add UDP encapsulation to an IPv4 transform. * *

This allows IPsec traffic to pass through a NAT. * * @see RFC 3948, UDP Encapsulation of IPsec * ESP Packets * @see RFC 7296 section 2.23, * NAT Traversal of IKEv2 * @param localSocket a socket for sending and receiving encapsulated traffic * @param remotePort the UDP port number of the remote host that will send and receive * encapsulated traffic. In the case of IKEv2, this should be port 4500. */ @NonNull public IpSecTransform.Builder setIpv4Encapsulation( @NonNull IpSecManager.UdpEncapsulationSocket localSocket, int remotePort) { Preconditions.checkNotNull(localSocket); mConfig.setEncapType(ENCAP_ESPINUDP); if (localSocket.getResourceId() == INVALID_RESOURCE_ID) { throw new IllegalArgumentException("Invalid UdpEncapsulationSocket"); } mConfig.setEncapSocketResourceId(localSocket.getResourceId()); mConfig.setEncapRemotePort(remotePort); return this; } /** * Build a transport mode {@link IpSecTransform}. * *

This builds and activates a transport mode transform. Note that an active transform * will not affect any network traffic until it has been applied to one or more sockets. * * @see IpSecManager#applyTransportModeTransform * @param sourceAddress the source {@code InetAddress} of traffic on sockets that will use * this transform; this address must belong to the Network used by all sockets that * utilize this transform; if provided, then only traffic originating from the * specified source address will be processed. * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed * traffic * @throws IllegalArgumentException indicating that a particular combination of transform * properties is invalid * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms * are active * @throws IpSecManager.SpiUnavailableException indicating the rare case where an SPI * collides with an existing transform * @throws IOException indicating other errors */ @NonNull public IpSecTransform buildTransportModeTransform( @NonNull InetAddress sourceAddress, @NonNull IpSecManager.SecurityParameterIndex spi) throws IpSecManager.ResourceUnavailableException, IpSecManager.SpiUnavailableException, IOException { Preconditions.checkNotNull(sourceAddress); Preconditions.checkNotNull(spi); if (spi.getResourceId() == INVALID_RESOURCE_ID) { throw new IllegalArgumentException("Invalid SecurityParameterIndex"); } mConfig.setMode(MODE_TRANSPORT); mConfig.setSourceAddress(sourceAddress.getHostAddress()); mConfig.setSpiResourceId(spi.getResourceId()); // FIXME: modifying a builder after calling build can change the built transform. return new IpSecTransform(mContext, mConfig).activate(); } /** * Build and return an {@link IpSecTransform} object as a Tunnel Mode Transform. Some * parameters have interdependencies that are checked at build time. * * @param sourceAddress the {@link InetAddress} that provides the source address for this * IPsec tunnel. This is almost certainly an address belonging to the {@link Network} * that will originate the traffic, which is set as the {@link #setUnderlyingNetwork}. * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed * traffic * @throws IllegalArgumentException indicating that a particular combination of transform * properties is invalid. * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms * are active * @throws IpSecManager.SpiUnavailableException indicating the rare case where an SPI * collides with an existing transform * @throws IOException indicating other errors * @hide */ @SystemApi @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public IpSecTransform buildTunnelModeTransform( @NonNull InetAddress sourceAddress, @NonNull IpSecManager.SecurityParameterIndex spi) throws IpSecManager.ResourceUnavailableException, IpSecManager.SpiUnavailableException, IOException { Preconditions.checkNotNull(sourceAddress); Preconditions.checkNotNull(spi); if (spi.getResourceId() == INVALID_RESOURCE_ID) { throw new IllegalArgumentException("Invalid SecurityParameterIndex"); } mConfig.setMode(MODE_TUNNEL); mConfig.setSourceAddress(sourceAddress.getHostAddress()); mConfig.setSpiResourceId(spi.getResourceId()); return new IpSecTransform(mContext, mConfig).activate(); } /** * Create a new IpSecTransform.Builder. * * @param context current context */ public Builder(@NonNull Context context) { Preconditions.checkNotNull(context); mContext = context; mConfig = new IpSecConfig(); } } }