IpSecTransform.java revision 5cd64cc8012243c656875be6b84456caeabd60f5
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.annotation.SystemApi;
26import android.content.Context;
27import android.os.Binder;
28import android.os.Handler;
29import android.os.IBinder;
30import android.os.RemoteException;
31import android.os.ServiceManager;
32import android.util.Log;
33
34import com.android.internal.annotations.VisibleForTesting;
35import com.android.internal.util.Preconditions;
36
37import dalvik.system.CloseGuard;
38
39import java.io.IOException;
40import java.lang.annotation.Retention;
41import java.lang.annotation.RetentionPolicy;
42import java.net.InetAddress;
43
44/**
45 * This class represents a transform, which roughly corresponds to an IPsec Security Association.
46 *
47 * <p>Transforms are created using {@link IpSecTransform.Builder}. Each {@code IpSecTransform}
48 * object encapsulates the properties and state of an IPsec security association. That includes,
49 * but is not limited to, algorithm choice, key material, and allocated system resources.
50 *
51 * @see <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the
52 *     Internet Protocol</a>
53 */
54public final class IpSecTransform implements AutoCloseable {
55    private static final String TAG = "IpSecTransform";
56
57    /** @hide */
58    public static final int MODE_TRANSPORT = 0;
59
60    /** @hide */
61    public static final int MODE_TUNNEL = 1;
62
63    /** @hide */
64    public static final int ENCAP_NONE = 0;
65
66    /**
67     * IPsec traffic will be encapsulated within UDP, but with 8 zero-value bytes between the UDP
68     * header and payload. This prevents traffic from being interpreted as ESP or IKEv2.
69     *
70     * @hide
71     */
72    public static final int ENCAP_ESPINUDP_NON_IKE = 1;
73
74    /**
75     * IPsec traffic will be encapsulated within UDP as per
76     * <a href="https://tools.ietf.org/html/rfc3948">RFC 3498</a>.
77     *
78     * @hide
79     */
80    public static final int ENCAP_ESPINUDP = 2;
81
82    /** @hide */
83    @IntDef(value = {ENCAP_NONE, ENCAP_ESPINUDP, ENCAP_ESPINUDP_NON_IKE})
84    @Retention(RetentionPolicy.SOURCE)
85    public @interface EncapType {}
86
87    /** @hide */
88    @VisibleForTesting
89    public IpSecTransform(Context context, IpSecConfig config) {
90        mContext = context;
91        mConfig = new IpSecConfig(config);
92        mResourceId = INVALID_RESOURCE_ID;
93    }
94
95    private IIpSecService getIpSecService() {
96        IBinder b = ServiceManager.getService(android.content.Context.IPSEC_SERVICE);
97        if (b == null) {
98            throw new RemoteException("Failed to connect to IpSecService")
99                    .rethrowAsRuntimeException();
100        }
101
102        return IIpSecService.Stub.asInterface(b);
103    }
104
105    /**
106     * Checks the result status and throws an appropriate exception if the status is not Status.OK.
107     */
108    private void checkResultStatus(int status)
109            throws IOException, IpSecManager.ResourceUnavailableException,
110                    IpSecManager.SpiUnavailableException {
111        switch (status) {
112            case IpSecManager.Status.OK:
113                return;
114                // TODO: Pass Error string back from bundle so that errors can be more specific
115            case IpSecManager.Status.RESOURCE_UNAVAILABLE:
116                throw new IpSecManager.ResourceUnavailableException(
117                        "Failed to allocate a new IpSecTransform");
118            case IpSecManager.Status.SPI_UNAVAILABLE:
119                Log.wtf(TAG, "Attempting to use an SPI that was somehow not reserved");
120                // Fall through
121            default:
122                throw new IllegalStateException(
123                        "Failed to Create a Transform with status code " + status);
124        }
125    }
126
127    private IpSecTransform activate()
128            throws IOException, IpSecManager.ResourceUnavailableException,
129                    IpSecManager.SpiUnavailableException {
130        synchronized (this) {
131            try {
132                IIpSecService svc = getIpSecService();
133                IpSecTransformResponse result = svc.createTransform(mConfig, new Binder());
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    @SystemApi
253    public static class NattKeepaliveCallback {
254        /** The specified {@code Network} is not connected. */
255        public static final int ERROR_INVALID_NETWORK = 1;
256        /** The hardware does not support this request. */
257        public static final int ERROR_HARDWARE_UNSUPPORTED = 2;
258        /** The hardware returned an error. */
259        public static final int ERROR_HARDWARE_ERROR = 3;
260
261        /** The requested keepalive was successfully started. */
262        public void onStarted() {}
263        /** The keepalive was successfully stopped. */
264        public void onStopped() {}
265        /** An error occurred. */
266        public void onError(int error) {}
267    }
268
269    /**
270     * Start a NAT-T keepalive session for the current transform.
271     *
272     * For a transform that is using UDP encapsulated IPv4, NAT-T offloading provides
273     * a power efficient mechanism of sending NAT-T packets at a specified interval.
274     *
275     * @param userCallback a {@link #NattKeepaliveCallback} to receive asynchronous status
276     *      information about the requested NAT-T keepalive session.
277     * @param intervalSeconds the interval between NAT-T keepalives being sent. The
278     *      the allowed range is between 20 and 3600 seconds.
279     * @param handler a handler on which to post callbacks when received.
280     *
281     * @hide
282     */
283    @SystemApi
284    @RequiresPermission(anyOf = {
285            android.Manifest.permission.NETWORK_STACK,
286            android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD
287    })
288    public void startNattKeepalive(@NonNull NattKeepaliveCallback userCallback,
289            int intervalSeconds, @NonNull Handler handler) throws IOException {
290        checkNotNull(userCallback);
291        if (intervalSeconds < 20 || intervalSeconds > 3600) {
292            throw new IllegalArgumentException("Invalid NAT-T keepalive interval");
293        }
294        checkNotNull(handler);
295        if (mResourceId == INVALID_RESOURCE_ID) {
296            throw new IllegalStateException(
297                    "Packet keepalive cannot be started for an inactive transform");
298        }
299
300        synchronized (mKeepaliveCallback) {
301            if (mKeepaliveCallback != null) {
302                throw new IllegalStateException("Keepalive already active");
303            }
304
305            mUserKeepaliveCallback = userCallback;
306            ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(
307                    Context.CONNECTIVITY_SERVICE);
308            mKeepalive = cm.startNattKeepalive(
309                    mConfig.getNetwork(), intervalSeconds, mKeepaliveCallback,
310                    NetworkUtils.numericToInetAddress(mConfig.getSourceAddress()),
311                    4500, // FIXME urgently, we need to get the port number from the Encap socket
312                    NetworkUtils.numericToInetAddress(mConfig.getDestinationAddress()));
313            mCallbackHandler = handler;
314        }
315    }
316
317    /**
318     * Stop an ongoing NAT-T keepalive session.
319     *
320     * Calling this API will request that an ongoing NAT-T keepalive session be terminated.
321     * If this API is not called when a Transform is closed, the underlying NAT-T session will
322     * be terminated automatically.
323     *
324     * @hide
325     */
326    @SystemApi
327    @RequiresPermission(anyOf = {
328            android.Manifest.permission.NETWORK_STACK,
329            android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD
330    })
331    public void stopNattKeepalive() {
332        synchronized (mKeepaliveCallback) {
333            if (mKeepalive == null) {
334                Log.e(TAG, "No active keepalive to stop");
335                return;
336            }
337            mKeepalive.stop();
338        }
339    }
340
341    /** This class is used to build {@link IpSecTransform} objects. */
342    public static class Builder {
343        private Context mContext;
344        private IpSecConfig mConfig;
345
346        /**
347         * Set the encryption algorithm.
348         *
349         * <p>Encryption is mutually exclusive with authenticated encryption.
350         *
351         * @param algo {@link IpSecAlgorithm} specifying the encryption to be applied.
352         */
353        @NonNull
354        public IpSecTransform.Builder setEncryption(@NonNull IpSecAlgorithm algo) {
355            // TODO: throw IllegalArgumentException if algo is not an encryption algorithm.
356            Preconditions.checkNotNull(algo);
357            mConfig.setEncryption(algo);
358            return this;
359        }
360
361        /**
362         * Set the authentication (integrity) algorithm.
363         *
364         * <p>Authentication is mutually exclusive with authenticated encryption.
365         *
366         * @param algo {@link IpSecAlgorithm} specifying the authentication to be applied.
367         */
368        @NonNull
369        public IpSecTransform.Builder setAuthentication(@NonNull IpSecAlgorithm algo) {
370            // TODO: throw IllegalArgumentException if algo is not an authentication algorithm.
371            Preconditions.checkNotNull(algo);
372            mConfig.setAuthentication(algo);
373            return this;
374        }
375
376        /**
377         * Set the authenticated encryption algorithm.
378         *
379         * <p>The Authenticated Encryption (AE) class of algorithms are also known as
380         * Authenticated Encryption with Associated Data (AEAD) algorithms, or Combined mode
381         * algorithms (as referred to in
382         * <a href="https://tools.ietf.org/html/rfc4301">RFC 4301</a>).
383         *
384         * <p>Authenticated encryption is mutually exclusive with encryption and authentication.
385         *
386         * @param algo {@link IpSecAlgorithm} specifying the authenticated encryption algorithm to
387         *     be applied.
388         */
389        @NonNull
390        public IpSecTransform.Builder setAuthenticatedEncryption(@NonNull IpSecAlgorithm algo) {
391            Preconditions.checkNotNull(algo);
392            mConfig.setAuthenticatedEncryption(algo);
393            return this;
394        }
395
396        /**
397         * Add UDP encapsulation to an IPv4 transform.
398         *
399         * <p>This allows IPsec traffic to pass through a NAT.
400         *
401         * @see <a href="https://tools.ietf.org/html/rfc3948">RFC 3948, UDP Encapsulation of IPsec
402         *     ESP Packets</a>
403         * @see <a href="https://tools.ietf.org/html/rfc7296#section-2.23">RFC 7296 section 2.23,
404         *     NAT Traversal of IKEv2</a>
405         * @param localSocket a socket for sending and receiving encapsulated traffic
406         * @param remotePort the UDP port number of the remote host that will send and receive
407         *     encapsulated traffic. In the case of IKEv2, this should be port 4500.
408         */
409        @NonNull
410        public IpSecTransform.Builder setIpv4Encapsulation(
411                @NonNull IpSecManager.UdpEncapsulationSocket localSocket, int remotePort) {
412            Preconditions.checkNotNull(localSocket);
413            mConfig.setEncapType(ENCAP_ESPINUDP);
414            if (localSocket.getResourceId() == INVALID_RESOURCE_ID) {
415                throw new IllegalArgumentException("Invalid UdpEncapsulationSocket");
416            }
417            mConfig.setEncapSocketResourceId(localSocket.getResourceId());
418            mConfig.setEncapRemotePort(remotePort);
419            return this;
420        }
421
422        /**
423         * Build a transport mode {@link IpSecTransform}.
424         *
425         * <p>This builds and activates a transport mode transform. Note that an active transform
426         * will not affect any network traffic until it has been applied to one or more sockets.
427         *
428         * @see IpSecManager#applyTransportModeTransform
429         * @param sourceAddress the source {@code InetAddress} of traffic on sockets that will use
430         *     this transform; this address must belong to the Network used by all sockets that
431         *     utilize this transform; if provided, then only traffic originating from the
432         *     specified source address will be processed.
433         * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed
434         *     traffic
435         * @throws IllegalArgumentException indicating that a particular combination of transform
436         *     properties is invalid
437         * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms
438         *     are active
439         * @throws IpSecManager.SpiUnavailableException indicating the rare case where an SPI
440         *     collides with an existing transform
441         * @throws IOException indicating other errors
442         */
443        @NonNull
444        public IpSecTransform buildTransportModeTransform(
445                @NonNull InetAddress sourceAddress,
446                @NonNull IpSecManager.SecurityParameterIndex spi)
447                throws IpSecManager.ResourceUnavailableException,
448                        IpSecManager.SpiUnavailableException, IOException {
449            Preconditions.checkNotNull(sourceAddress);
450            Preconditions.checkNotNull(spi);
451            if (spi.getResourceId() == INVALID_RESOURCE_ID) {
452                throw new IllegalArgumentException("Invalid SecurityParameterIndex");
453            }
454            mConfig.setMode(MODE_TRANSPORT);
455            mConfig.setSourceAddress(sourceAddress.getHostAddress());
456            mConfig.setSpiResourceId(spi.getResourceId());
457            // FIXME: modifying a builder after calling build can change the built transform.
458            return new IpSecTransform(mContext, mConfig).activate();
459        }
460
461        /**
462         * Build and return an {@link IpSecTransform} object as a Tunnel Mode Transform. Some
463         * parameters have interdependencies that are checked at build time.
464         *
465         * @param sourceAddress the {@link InetAddress} that provides the source address for this
466         *     IPsec tunnel. This is almost certainly an address belonging to the {@link Network}
467         *     that will originate the traffic, which is set as the {@link #setUnderlyingNetwork}.
468         * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed
469         *     traffic
470         * @throws IllegalArgumentException indicating that a particular combination of transform
471         *     properties is invalid.
472         * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms
473         *     are active
474         * @throws IpSecManager.SpiUnavailableException indicating the rare case where an SPI
475         *     collides with an existing transform
476         * @throws IOException indicating other errors
477         * @hide
478         */
479        @SystemApi
480        @NonNull
481        @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
482        public IpSecTransform buildTunnelModeTransform(
483                @NonNull InetAddress sourceAddress,
484                @NonNull IpSecManager.SecurityParameterIndex spi)
485                throws IpSecManager.ResourceUnavailableException,
486                        IpSecManager.SpiUnavailableException, IOException {
487            Preconditions.checkNotNull(sourceAddress);
488            Preconditions.checkNotNull(spi);
489            if (spi.getResourceId() == INVALID_RESOURCE_ID) {
490                throw new IllegalArgumentException("Invalid SecurityParameterIndex");
491            }
492            mConfig.setMode(MODE_TUNNEL);
493            mConfig.setSourceAddress(sourceAddress.getHostAddress());
494            mConfig.setSpiResourceId(spi.getResourceId());
495            return new IpSecTransform(mContext, mConfig).activate();
496        }
497
498        /**
499         * Create a new IpSecTransform.Builder.
500         *
501         * @param context current context
502         */
503        public Builder(@NonNull Context context) {
504            Preconditions.checkNotNull(context);
505            mContext = context;
506            mConfig = new IpSecConfig();
507        }
508    }
509}
510