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