IpSecTransform.java revision 0486b927b3cc83113ef7b863f4a7331c8182d1a4
1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package android.net;
17
18import static android.net.IpSecManager.INVALID_RESOURCE_ID;
19
20import static com.android.internal.util.Preconditions.checkNotNull;
21
22import android.annotation.IntDef;
23import android.annotation.NonNull;
24import android.annotation.RequiresPermission;
25import android.content.Context;
26import android.os.Binder;
27import android.os.Handler;
28import android.os.IBinder;
29import android.os.RemoteException;
30import android.os.ServiceManager;
31import android.util.Log;
32
33import com.android.internal.annotations.VisibleForTesting;
34import com.android.internal.util.Preconditions;
35
36import dalvik.system.CloseGuard;
37
38import java.io.IOException;
39import java.lang.annotation.Retention;
40import java.lang.annotation.RetentionPolicy;
41import java.net.InetAddress;
42
43/**
44 * This class represents a transform, which roughly corresponds to an IPsec Security Association.
45 *
46 * <p>Transforms are created using {@link IpSecTransform.Builder}. Each {@code IpSecTransform}
47 * object encapsulates the properties and state of an IPsec security association. That includes,
48 * but is not limited to, algorithm choice, key material, and allocated system resources.
49 *
50 * @see <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the
51 *     Internet Protocol</a>
52 */
53public final class IpSecTransform implements AutoCloseable {
54    private static final String TAG = "IpSecTransform";
55
56    /** @hide */
57    public static final int MODE_TRANSPORT = 0;
58
59    /** @hide */
60    public static final int MODE_TUNNEL = 1;
61
62    /** @hide */
63    public static final int ENCAP_NONE = 0;
64
65    /**
66     * IPsec traffic will be encapsulated within UDP, but with 8 zero-value bytes between the UDP
67     * header and payload. This prevents traffic from being interpreted as ESP or IKEv2.
68     *
69     * @hide
70     */
71    public static final int ENCAP_ESPINUDP_NON_IKE = 1;
72
73    /**
74     * IPsec traffic will be encapsulated within UDP as per
75     * <a href="https://tools.ietf.org/html/rfc3948">RFC 3498</a>.
76     *
77     * @hide
78     */
79    public static final int ENCAP_ESPINUDP = 2;
80
81    /** @hide */
82    @IntDef(value = {ENCAP_NONE, ENCAP_ESPINUDP, ENCAP_ESPINUDP_NON_IKE})
83    @Retention(RetentionPolicy.SOURCE)
84    public @interface EncapType {}
85
86    /** @hide */
87    @VisibleForTesting
88    public IpSecTransform(Context context, IpSecConfig config) {
89        mContext = context;
90        mConfig = new IpSecConfig(config);
91        mResourceId = INVALID_RESOURCE_ID;
92    }
93
94    private IIpSecService getIpSecService() {
95        IBinder b = ServiceManager.getService(android.content.Context.IPSEC_SERVICE);
96        if (b == null) {
97            throw new RemoteException("Failed to connect to IpSecService")
98                    .rethrowAsRuntimeException();
99        }
100
101        return IIpSecService.Stub.asInterface(b);
102    }
103
104    /**
105     * Checks the result status and throws an appropriate exception if the status is not Status.OK.
106     */
107    private void checkResultStatus(int status)
108            throws IOException, IpSecManager.ResourceUnavailableException,
109                    IpSecManager.SpiUnavailableException {
110        switch (status) {
111            case IpSecManager.Status.OK:
112                return;
113                // TODO: Pass Error string back from bundle so that errors can be more specific
114            case IpSecManager.Status.RESOURCE_UNAVAILABLE:
115                throw new IpSecManager.ResourceUnavailableException(
116                        "Failed to allocate a new IpSecTransform");
117            case IpSecManager.Status.SPI_UNAVAILABLE:
118                Log.wtf(TAG, "Attempting to use an SPI that was somehow not reserved");
119                // Fall through
120            default:
121                throw new IllegalStateException(
122                        "Failed to Create a Transform with status code " + status);
123        }
124    }
125
126    private IpSecTransform activate()
127            throws IOException, IpSecManager.ResourceUnavailableException,
128                    IpSecManager.SpiUnavailableException {
129        synchronized (this) {
130            try {
131                IIpSecService svc = getIpSecService();
132                IpSecTransformResponse result = svc.createTransform(
133                        mConfig, new Binder(), mContext.getOpPackageName());
134                int status = result.status;
135                checkResultStatus(status);
136                mResourceId = result.resourceId;
137                Log.d(TAG, "Added Transform with Id " + mResourceId);
138                mCloseGuard.open("build");
139            } catch (RemoteException e) {
140                throw e.rethrowAsRuntimeException();
141            }
142        }
143
144        return this;
145    }
146
147    /**
148     * Equals method used for testing
149     *
150     * @hide
151     */
152    @VisibleForTesting
153    public static boolean equals(IpSecTransform lhs, IpSecTransform rhs) {
154        if (lhs == null || rhs == null) return (lhs == rhs);
155        return IpSecConfig.equals(lhs.getConfig(), rhs.getConfig())
156                && lhs.mResourceId == rhs.mResourceId;
157    }
158
159    /**
160     * Deactivate this {@code IpSecTransform} and free allocated resources.
161     *
162     * <p>Deactivating a transform while it is still applied to a socket will result in errors on
163     * that socket. Make sure to remove transforms by calling {@link
164     * IpSecManager#removeTransportModeTransforms}. Note, removing an {@code IpSecTransform} from a
165     * socket will not deactivate it (because one transform may be applied to multiple sockets).
166     *
167     * <p>It is safe to call this method on a transform that has already been deactivated.
168     */
169    public void close() {
170        Log.d(TAG, "Removing Transform with Id " + mResourceId);
171
172        // Always safe to attempt cleanup
173        if (mResourceId == INVALID_RESOURCE_ID) {
174            mCloseGuard.close();
175            return;
176        }
177        try {
178            IIpSecService svc = getIpSecService();
179            svc.deleteTransform(mResourceId);
180            stopNattKeepalive();
181        } catch (RemoteException e) {
182            throw e.rethrowAsRuntimeException();
183        } finally {
184            mResourceId = INVALID_RESOURCE_ID;
185            mCloseGuard.close();
186        }
187    }
188
189    /** Check that the transform was closed properly. */
190    @Override
191    protected void finalize() throws Throwable {
192        if (mCloseGuard != null) {
193            mCloseGuard.warnIfOpen();
194        }
195        close();
196    }
197
198    /* Package */
199    IpSecConfig getConfig() {
200        return mConfig;
201    }
202
203    private final IpSecConfig mConfig;
204    private int mResourceId;
205    private final Context mContext;
206    private final CloseGuard mCloseGuard = CloseGuard.get();
207    private ConnectivityManager.PacketKeepalive mKeepalive;
208    private Handler mCallbackHandler;
209    private final ConnectivityManager.PacketKeepaliveCallback mKeepaliveCallback =
210            new ConnectivityManager.PacketKeepaliveCallback() {
211
212                @Override
213                public void onStarted() {
214                    synchronized (this) {
215                        mCallbackHandler.post(() -> mUserKeepaliveCallback.onStarted());
216                    }
217                }
218
219                @Override
220                public void onStopped() {
221                    synchronized (this) {
222                        mKeepalive = null;
223                        mCallbackHandler.post(() -> mUserKeepaliveCallback.onStopped());
224                    }
225                }
226
227                @Override
228                public void onError(int error) {
229                    synchronized (this) {
230                        mKeepalive = null;
231                        mCallbackHandler.post(() -> mUserKeepaliveCallback.onError(error));
232                    }
233                }
234            };
235
236    private NattKeepaliveCallback mUserKeepaliveCallback;
237
238    /** @hide */
239    @VisibleForTesting
240    public int getResourceId() {
241        return mResourceId;
242    }
243
244    /**
245     * A callback class to provide status information regarding a NAT-T keepalive session
246     *
247     * <p>Use this callback to receive status information regarding a NAT-T keepalive session
248     * by registering it when calling {@link #startNattKeepalive}.
249     *
250     * @hide
251     */
252    public static class NattKeepaliveCallback {
253        /** The specified {@code Network} is not connected. */
254        public static final int ERROR_INVALID_NETWORK = 1;
255        /** The hardware does not support this request. */
256        public static final int ERROR_HARDWARE_UNSUPPORTED = 2;
257        /** The hardware returned an error. */
258        public static final int ERROR_HARDWARE_ERROR = 3;
259
260        /** The requested keepalive was successfully started. */
261        public void onStarted() {}
262        /** The keepalive was successfully stopped. */
263        public void onStopped() {}
264        /** An error occurred. */
265        public void onError(int error) {}
266    }
267
268    /**
269     * Start a NAT-T keepalive session for the current transform.
270     *
271     * For a transform that is using UDP encapsulated IPv4, NAT-T offloading provides
272     * a power efficient mechanism of sending NAT-T packets at a specified interval.
273     *
274     * @param userCallback a {@link #NattKeepaliveCallback} to receive asynchronous status
275     *      information about the requested NAT-T keepalive session.
276     * @param intervalSeconds the interval between NAT-T keepalives being sent. The
277     *      the allowed range is between 20 and 3600 seconds.
278     * @param handler a handler on which to post callbacks when received.
279     *
280     * @hide
281     */
282    @RequiresPermission(anyOf = {
283            android.Manifest.permission.MANAGE_IPSEC_TUNNELS,
284            android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD
285    })
286    public void startNattKeepalive(@NonNull NattKeepaliveCallback userCallback,
287            int intervalSeconds, @NonNull Handler handler) throws IOException {
288        checkNotNull(userCallback);
289        if (intervalSeconds < 20 || intervalSeconds > 3600) {
290            throw new IllegalArgumentException("Invalid NAT-T keepalive interval");
291        }
292        checkNotNull(handler);
293        if (mResourceId == INVALID_RESOURCE_ID) {
294            throw new IllegalStateException(
295                    "Packet keepalive cannot be started for an inactive transform");
296        }
297
298        synchronized (mKeepaliveCallback) {
299            if (mKeepaliveCallback != null) {
300                throw new IllegalStateException("Keepalive already active");
301            }
302
303            mUserKeepaliveCallback = userCallback;
304            ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(
305                    Context.CONNECTIVITY_SERVICE);
306            mKeepalive = cm.startNattKeepalive(
307                    mConfig.getNetwork(), intervalSeconds, mKeepaliveCallback,
308                    NetworkUtils.numericToInetAddress(mConfig.getSourceAddress()),
309                    4500, // FIXME urgently, we need to get the port number from the Encap socket
310                    NetworkUtils.numericToInetAddress(mConfig.getDestinationAddress()));
311            mCallbackHandler = handler;
312        }
313    }
314
315    /**
316     * Stop an ongoing NAT-T keepalive session.
317     *
318     * Calling this API will request that an ongoing NAT-T keepalive session be terminated.
319     * If this API is not called when a Transform is closed, the underlying NAT-T session will
320     * be terminated automatically.
321     *
322     * @hide
323     */
324    @RequiresPermission(anyOf = {
325            android.Manifest.permission.MANAGE_IPSEC_TUNNELS,
326            android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD
327    })
328    public void stopNattKeepalive() {
329        synchronized (mKeepaliveCallback) {
330            if (mKeepalive == null) {
331                Log.e(TAG, "No active keepalive to stop");
332                return;
333            }
334            mKeepalive.stop();
335        }
336    }
337
338    /** This class is used to build {@link IpSecTransform} objects. */
339    public static class Builder {
340        private Context mContext;
341        private IpSecConfig mConfig;
342
343        /**
344         * Set the encryption algorithm.
345         *
346         * <p>Encryption is mutually exclusive with authenticated encryption.
347         *
348         * @param algo {@link IpSecAlgorithm} specifying the encryption to be applied.
349         */
350        @NonNull
351        public IpSecTransform.Builder setEncryption(@NonNull IpSecAlgorithm algo) {
352            // TODO: throw IllegalArgumentException if algo is not an encryption algorithm.
353            Preconditions.checkNotNull(algo);
354            mConfig.setEncryption(algo);
355            return this;
356        }
357
358        /**
359         * Set the authentication (integrity) algorithm.
360         *
361         * <p>Authentication is mutually exclusive with authenticated encryption.
362         *
363         * @param algo {@link IpSecAlgorithm} specifying the authentication to be applied.
364         */
365        @NonNull
366        public IpSecTransform.Builder setAuthentication(@NonNull IpSecAlgorithm algo) {
367            // TODO: throw IllegalArgumentException if algo is not an authentication algorithm.
368            Preconditions.checkNotNull(algo);
369            mConfig.setAuthentication(algo);
370            return this;
371        }
372
373        /**
374         * Set the authenticated encryption algorithm.
375         *
376         * <p>The Authenticated Encryption (AE) class of algorithms are also known as
377         * Authenticated Encryption with Associated Data (AEAD) algorithms, or Combined mode
378         * algorithms (as referred to in
379         * <a href="https://tools.ietf.org/html/rfc4301">RFC 4301</a>).
380         *
381         * <p>Authenticated encryption is mutually exclusive with encryption and authentication.
382         *
383         * @param algo {@link IpSecAlgorithm} specifying the authenticated encryption algorithm to
384         *     be applied.
385         */
386        @NonNull
387        public IpSecTransform.Builder setAuthenticatedEncryption(@NonNull IpSecAlgorithm algo) {
388            Preconditions.checkNotNull(algo);
389            mConfig.setAuthenticatedEncryption(algo);
390            return this;
391        }
392
393        /**
394         * Add UDP encapsulation to an IPv4 transform.
395         *
396         * <p>This allows IPsec traffic to pass through a NAT.
397         *
398         * @see <a href="https://tools.ietf.org/html/rfc3948">RFC 3948, UDP Encapsulation of IPsec
399         *     ESP Packets</a>
400         * @see <a href="https://tools.ietf.org/html/rfc7296#section-2.23">RFC 7296 section 2.23,
401         *     NAT Traversal of IKEv2</a>
402         * @param localSocket a socket for sending and receiving encapsulated traffic
403         * @param remotePort the UDP port number of the remote host that will send and receive
404         *     encapsulated traffic. In the case of IKEv2, this should be port 4500.
405         */
406        @NonNull
407        public IpSecTransform.Builder setIpv4Encapsulation(
408                @NonNull IpSecManager.UdpEncapsulationSocket localSocket, int remotePort) {
409            Preconditions.checkNotNull(localSocket);
410            mConfig.setEncapType(ENCAP_ESPINUDP);
411            if (localSocket.getResourceId() == INVALID_RESOURCE_ID) {
412                throw new IllegalArgumentException("Invalid UdpEncapsulationSocket");
413            }
414            mConfig.setEncapSocketResourceId(localSocket.getResourceId());
415            mConfig.setEncapRemotePort(remotePort);
416            return this;
417        }
418
419        /**
420         * Build a transport mode {@link IpSecTransform}.
421         *
422         * <p>This builds and activates a transport mode transform. Note that an active transform
423         * will not affect any network traffic until it has been applied to one or more sockets.
424         *
425         * @see IpSecManager#applyTransportModeTransform
426         * @param sourceAddress the source {@code InetAddress} of traffic on sockets that will use
427         *     this transform; this address must belong to the Network used by all sockets that
428         *     utilize this transform; if provided, then only traffic originating from the
429         *     specified source address will be processed.
430         * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed
431         *     traffic
432         * @throws IllegalArgumentException indicating that a particular combination of transform
433         *     properties is invalid
434         * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms
435         *     are active
436         * @throws IpSecManager.SpiUnavailableException indicating the rare case where an SPI
437         *     collides with an existing transform
438         * @throws IOException indicating other errors
439         */
440        @NonNull
441        public IpSecTransform buildTransportModeTransform(
442                @NonNull InetAddress sourceAddress,
443                @NonNull IpSecManager.SecurityParameterIndex spi)
444                throws IpSecManager.ResourceUnavailableException,
445                        IpSecManager.SpiUnavailableException, IOException {
446            Preconditions.checkNotNull(sourceAddress);
447            Preconditions.checkNotNull(spi);
448            if (spi.getResourceId() == INVALID_RESOURCE_ID) {
449                throw new IllegalArgumentException("Invalid SecurityParameterIndex");
450            }
451            mConfig.setMode(MODE_TRANSPORT);
452            mConfig.setSourceAddress(sourceAddress.getHostAddress());
453            mConfig.setSpiResourceId(spi.getResourceId());
454            // FIXME: modifying a builder after calling build can change the built transform.
455            return new IpSecTransform(mContext, mConfig).activate();
456        }
457
458        /**
459         * Build and return an {@link IpSecTransform} object as a Tunnel Mode Transform. Some
460         * parameters have interdependencies that are checked at build time.
461         *
462         * @param sourceAddress the {@link InetAddress} that provides the source address for this
463         *     IPsec tunnel. This is almost certainly an address belonging to the {@link Network}
464         *     that will originate the traffic, which is set as the {@link #setUnderlyingNetwork}.
465         * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed
466         *     traffic
467         * @throws IllegalArgumentException indicating that a particular combination of transform
468         *     properties is invalid.
469         * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms
470         *     are active
471         * @throws IpSecManager.SpiUnavailableException indicating the rare case where an SPI
472         *     collides with an existing transform
473         * @throws IOException indicating other errors
474         * @hide
475         */
476        @NonNull
477        @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
478        public IpSecTransform buildTunnelModeTransform(
479                @NonNull InetAddress sourceAddress,
480                @NonNull IpSecManager.SecurityParameterIndex spi)
481                throws IpSecManager.ResourceUnavailableException,
482                        IpSecManager.SpiUnavailableException, IOException {
483            Preconditions.checkNotNull(sourceAddress);
484            Preconditions.checkNotNull(spi);
485            if (spi.getResourceId() == INVALID_RESOURCE_ID) {
486                throw new IllegalArgumentException("Invalid SecurityParameterIndex");
487            }
488            mConfig.setMode(MODE_TUNNEL);
489            mConfig.setSourceAddress(sourceAddress.getHostAddress());
490            mConfig.setSpiResourceId(spi.getResourceId());
491            return new IpSecTransform(mContext, mConfig).activate();
492        }
493
494        /**
495         * Create a new IpSecTransform.Builder.
496         *
497         * @param context current context
498         */
499        public Builder(@NonNull Context context) {
500            Preconditions.checkNotNull(context);
501            mContext = context;
502            mConfig = new IpSecConfig();
503        }
504    }
505}
506