diff options
5 files changed, 849 insertions, 37 deletions
diff --git a/core/java/android/net/IpSecManager.java b/core/java/android/net/IpSecManager.java index 09ec6c35fcb9..d83715c692f7 100644 --- a/core/java/android/net/IpSecManager.java +++ b/core/java/android/net/IpSecManager.java @@ -51,7 +51,7 @@ import java.net.Socket; * * <p>Note that not all aspects of IPsec are permitted by this API. Applications may create * transport mode security associations and apply them to individual sockets. Applications looking - * to create a VPN should use {@link VpnService}. + * to create an IPsec VPN should use {@link VpnManager} and {@link Ikev2VpnProfile}. * * @see <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the * Internet Protocol</a> diff --git a/services/core/java/com/android/server/IpSecService.java b/services/core/java/com/android/server/IpSecService.java index a629b3fbb8fa..98ac4cb7122a 100644 --- a/services/core/java/com/android/server/IpSecService.java +++ b/services/core/java/com/android/server/IpSecService.java @@ -1557,16 +1557,16 @@ public class IpSecService extends IIpSecService.Stub { } checkNotNull(callingPackage, "Null calling package cannot create IpSec tunnels"); - switch (getAppOpsManager().noteOp(TUNNEL_OP, Binder.getCallingUid(), callingPackage)) { - case AppOpsManager.MODE_DEFAULT: - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.MANAGE_IPSEC_TUNNELS, "IpSecService"); - break; - case AppOpsManager.MODE_ALLOWED: - return; - default: - throw new SecurityException("Request to ignore AppOps for non-legacy API"); + + // OP_MANAGE_IPSEC_TUNNELS will return MODE_ERRORED by default, including for the system + // server. If the appop is not granted, require that the caller has the MANAGE_IPSEC_TUNNELS + // permission or is the System Server. + if (AppOpsManager.MODE_ALLOWED == getAppOpsManager().noteOpNoThrow( + TUNNEL_OP, Binder.getCallingUid(), callingPackage)) { + return; } + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.MANAGE_IPSEC_TUNNELS, "IpSecService"); } private void createOrUpdateTransform( diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 99560226f99a..7f6dc55a369d 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -48,8 +48,12 @@ import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.net.ConnectivityManager; import android.net.INetworkManagementEventObserver; +import android.net.Ikev2VpnProfile; import android.net.IpPrefix; import android.net.IpSecManager; +import android.net.IpSecManager.IpSecTunnelInterface; +import android.net.IpSecManager.UdpEncapsulationSocket; +import android.net.IpSecTransform; import android.net.LinkAddress; import android.net.LinkProperties; import android.net.LocalSocket; @@ -65,6 +69,12 @@ import android.net.RouteInfo; import android.net.UidRange; import android.net.VpnManager; import android.net.VpnService; +import android.net.ipsec.ike.ChildSessionCallback; +import android.net.ipsec.ike.ChildSessionConfiguration; +import android.net.ipsec.ike.ChildSessionParams; +import android.net.ipsec.ike.IkeSession; +import android.net.ipsec.ike.IkeSessionCallback; +import android.net.ipsec.ike.IkeSessionParams; import android.os.Binder; import android.os.Build.VERSION_CODES; import android.os.Bundle; @@ -113,6 +123,7 @@ import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -122,6 +133,9 @@ import java.util.Objects; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; /** @@ -176,14 +190,14 @@ public class Vpn { private final Context mContext; private final NetworkInfo mNetworkInfo; - private String mPackage; + @VisibleForTesting protected String mPackage; private int mOwnerUID; private boolean mIsPackageTargetingAtLeastQ; private String mInterface; private Connection mConnection; /** Tracks the runners for all VPN types managed by the platform (eg. LegacyVpn, PlatformVpn) */ - private VpnRunner mVpnRunner; + @VisibleForTesting protected VpnRunner mVpnRunner; private PendingIntent mStatusIntent; private volatile boolean mEnableTeardown = true; @@ -196,6 +210,7 @@ public class Vpn { @VisibleForTesting protected final NetworkCapabilities mNetworkCapabilities; private final SystemServices mSystemServices; + private final Ikev2SessionCreator mIkev2SessionCreator; /** * Whether to keep the connection active after rebooting, or upgrading or reinstalling. This @@ -238,17 +253,20 @@ public class Vpn { public Vpn(Looper looper, Context context, INetworkManagementService netService, @UserIdInt int userHandle) { - this(looper, context, netService, userHandle, new SystemServices(context)); + this(looper, context, netService, userHandle, + new SystemServices(context), new Ikev2SessionCreator()); } @VisibleForTesting protected Vpn(Looper looper, Context context, INetworkManagementService netService, - int userHandle, SystemServices systemServices) { + int userHandle, SystemServices systemServices, + Ikev2SessionCreator ikev2SessionCreator) { mContext = context; mNetd = netService; mUserHandle = userHandle; mLooper = looper; mSystemServices = systemServices; + mIkev2SessionCreator = ikev2SessionCreator; mPackage = VpnConfig.LEGACY_VPN; mOwnerUID = getAppUid(mPackage, mUserHandle); @@ -749,8 +767,9 @@ public class Vpn { private boolean isCurrentPreparedPackage(String packageName) { // We can't just check that packageName matches mPackage, because if the app was uninstalled - // and reinstalled it will no longer be prepared. Instead check the UID. - return getAppUid(packageName, mUserHandle) == mOwnerUID; + // and reinstalled it will no longer be prepared. Similarly if there is a shared UID, the + // calling package may not be the same as the prepared package. Check both UID and package. + return getAppUid(packageName, mUserHandle) == mOwnerUID && mPackage.equals(packageName); } /** Prepare the VPN for the given package. Does not perform permission checks. */ @@ -975,7 +994,11 @@ public class Vpn { } lp.setDomains(buffer.toString().trim()); - // TODO: Stop setting the MTU in jniCreate and set it here. + if (mConfig.mtu > 0) { + lp.setMtu(mConfig.mtu); + } + + // TODO: Stop setting the MTU in jniCreate return lp; } @@ -2000,30 +2023,369 @@ public class Vpn { protected abstract void exit(); } - private class IkeV2VpnRunner extends VpnRunner { - private static final String TAG = "IkeV2VpnRunner"; + interface IkeV2VpnRunnerCallback { + void onDefaultNetworkChanged(@NonNull Network network); - private final IpSecManager mIpSecManager; - private final VpnProfile mProfile; + void onChildOpened( + @NonNull Network network, @NonNull ChildSessionConfiguration childConfig); + + void onChildTransformCreated( + @NonNull Network network, @NonNull IpSecTransform transform, int direction); + + void onSessionLost(@NonNull Network network); + } + + /** + * Internal class managing IKEv2/IPsec VPN connectivity + * + * <p>The IKEv2 VPN will listen to, and run based on the lifecycle of Android's default Network. + * As a new default is selected, old IKE sessions will be torn down, and a new one will be + * started. + * + * <p>This class uses locking minimally - the Vpn instance lock is only ever held when fields of + * the outer class are modified. As such, care must be taken to ensure that no calls are added + * that might modify the outer class' state without acquiring a lock. + * + * <p>The overall structure of the Ikev2VpnRunner is as follows: + * + * <ol> + * <li>Upon startup, a NetworkRequest is registered with ConnectivityManager. This is called + * any time a new default network is selected + * <li>When a new default is connected, an IKE session is started on that Network. If there + * were any existing IKE sessions on other Networks, they are torn down before starting + * the new IKE session + * <li>Upon establishment, the onChildTransformCreated() callback is called twice, one for + * each direction, and finally onChildOpened() is called + * <li>Upon the onChildOpened() call, the VPN is fully set up. + * <li>Subsequent Network changes result in new onDefaultNetworkChanged() callbacks. See (2). + * </ol> + */ + class IkeV2VpnRunner extends VpnRunner implements IkeV2VpnRunnerCallback { + @NonNull private static final String TAG = "IkeV2VpnRunner"; + + @NonNull private final IpSecManager mIpSecManager; + @NonNull private final Ikev2VpnProfile mProfile; + @NonNull private final ConnectivityManager.NetworkCallback mNetworkCallback; + + /** + * Executor upon which ALL callbacks must be run. + * + * <p>This executor MUST be a single threaded executor, in order to ensure the consistency + * of the mutable Ikev2VpnRunner fields. The Ikev2VpnRunner is built mostly lock-free by + * virtue of everything being serialized on this executor. + */ + @NonNull private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); - IkeV2VpnRunner(VpnProfile profile) { + /** Signal to ensure shutdown is honored even if a new Network is connected. */ + private boolean mIsRunning = true; + + @Nullable private UdpEncapsulationSocket mEncapSocket; + @Nullable private IpSecTunnelInterface mTunnelIface; + @Nullable private IkeSession mSession; + @Nullable private Network mActiveNetwork; + + IkeV2VpnRunner(@NonNull Ikev2VpnProfile profile) { super(TAG); mProfile = profile; - - // TODO: move this to startVpnRunnerPrivileged() - mConfig = new VpnConfig(); - mIpSecManager = mContext.getSystemService(IpSecManager.class); + mIpSecManager = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE); + mNetworkCallback = new VpnIkev2Utils.Ikev2VpnNetworkCallback(TAG, this); } @Override public void run() { - // TODO: Build IKE config, start IKE session + // Explicitly use only the network that ConnectivityService thinks is the "best." In + // other words, only ever use the currently selected default network. This does mean + // that in both onLost() and onConnected(), any old sessions MUST be torn down. This + // does NOT include VPNs. + final ConnectivityManager cm = ConnectivityManager.from(mContext); + cm.requestNetwork(cm.getDefaultRequest(), mNetworkCallback); + } + + private boolean isActiveNetwork(@Nullable Network network) { + return Objects.equals(mActiveNetwork, network) && mIsRunning; + } + + /** + * Called when an IKE Child session has been opened, signalling completion of the startup. + * + * <p>This method is only ever called once per IkeSession, and MUST run on the mExecutor + * thread in order to ensure consistency of the Ikev2VpnRunner fields. + */ + public void onChildOpened( + @NonNull Network network, @NonNull ChildSessionConfiguration childConfig) { + if (!isActiveNetwork(network)) { + Log.d(TAG, "onOpened called for obsolete network " + network); + + // Do nothing; this signals that either: (1) a new/better Network was found, + // and the Ikev2VpnRunner has switched to it in onDefaultNetworkChanged, or (2) this + // IKE session was already shut down (exited, or an error was encountered somewhere + // else). In both cases, all resources and sessions are torn down via + // resetIkeState(). + return; + } + + try { + final String interfaceName = mTunnelIface.getInterfaceName(); + final int maxMtu = mProfile.getMaxMtu(); + final List<LinkAddress> internalAddresses = childConfig.getInternalAddresses(); + + final Collection<RouteInfo> newRoutes = VpnIkev2Utils.getRoutesFromTrafficSelectors( + childConfig.getOutboundTrafficSelectors()); + for (final LinkAddress address : internalAddresses) { + mTunnelIface.addAddress(address.getAddress(), address.getPrefixLength()); + } + + final NetworkAgent networkAgent; + final LinkProperties lp; + + synchronized (Vpn.this) { + mInterface = interfaceName; + mConfig.mtu = maxMtu; + mConfig.interfaze = mInterface; + + mConfig.addresses.clear(); + mConfig.addresses.addAll(internalAddresses); + + mConfig.routes.clear(); + mConfig.routes.addAll(newRoutes); + + // TODO: Add DNS servers from negotiation + + networkAgent = mNetworkAgent; + + // The below must be done atomically with the mConfig update, otherwise + // isRunningLocked() will be racy. + if (networkAgent == null) { + if (isSettingsVpnLocked()) { + prepareStatusIntent(); + } + agentConnect(); + return; // Link properties are already sent. + } + + lp = makeLinkProperties(); // Accesses VPN instance fields; must be locked + } + + networkAgent.sendLinkProperties(lp); + } catch (Exception e) { + Log.d(TAG, "Error in ChildOpened for network " + network, e); + onSessionLost(network); + } + } + + /** + * Called when an IPsec transform has been created, and should be applied. + * + * <p>This method is called multiple times over the lifetime of an IkeSession (or default + * network), and is MUST always be called on the mExecutor thread in order to ensure + * consistency of the Ikev2VpnRunner fields. + */ + public void onChildTransformCreated( + @NonNull Network network, @NonNull IpSecTransform transform, int direction) { + if (!isActiveNetwork(network)) { + Log.d(TAG, "ChildTransformCreated for obsolete network " + network); + + // Do nothing; this signals that either: (1) a new/better Network was found, + // and the Ikev2VpnRunner has switched to it in onDefaultNetworkChanged, or (2) this + // IKE session was already shut down (exited, or an error was encountered somewhere + // else). In both cases, all resources and sessions are torn down via + // resetIkeState(). + return; + } + + try { + // Transforms do not need to be persisted; the IkeSession will keep + // them alive for us + mIpSecManager.applyTunnelModeTransform(mTunnelIface, direction, transform); + } catch (IOException e) { + Log.d(TAG, "Transform application failed for network " + network, e); + onSessionLost(network); + } + } + + /** + * Called when a new default network is connected. + * + * <p>The Ikev2VpnRunner will unconditionally switch to the new network, killing the old IKE + * state in the process, and starting a new IkeSession instance. + * + * <p>This method is called multiple times over the lifetime of the Ikev2VpnRunner, and is + * called on the ConnectivityService thread. Thus, the actual work MUST be proxied to the + * mExecutor thread in order to ensure consistency of the Ikev2VpnRunner fields. + */ + public void onDefaultNetworkChanged(@NonNull Network network) { + Log.d(TAG, "Starting IKEv2/IPsec session on new network: " + network); + + // Proxy to the Ikev2VpnRunner (single-thread) executor to ensure consistency in lieu + // of locking. + mExecutor.execute(() -> { + try { + if (!mIsRunning) { + Log.d(TAG, "onDefaultNetworkChanged after exit"); + return; // VPN has been shut down. + } + + // Without MOBIKE, we have no way to seamlessly migrate. Close on old + // (non-default) network, and start the new one. + resetIkeState(); + mActiveNetwork = network; + + // TODO(b/149356682): Update this based on new IKE API + mEncapSocket = mIpSecManager.openUdpEncapsulationSocket(); + + // TODO(b/149356682): Update this based on new IKE API + final IkeSessionParams ikeSessionParams = + VpnIkev2Utils.buildIkeSessionParams(mProfile, mEncapSocket); + final ChildSessionParams childSessionParams = + VpnIkev2Utils.buildChildSessionParams(); + + // TODO: Remove the need for adding two unused addresses with + // IPsec tunnels. + mTunnelIface = + mIpSecManager.createIpSecTunnelInterface( + ikeSessionParams.getServerAddress() /* unused */, + ikeSessionParams.getServerAddress() /* unused */, + network); + mNetd.setInterfaceUp(mTunnelIface.getInterfaceName()); + + // Socket must be bound to prevent network switches from causing + // the IKE teardown to fail/timeout. + // TODO(b/149356682): Update this based on new IKE API + network.bindSocket(mEncapSocket.getFileDescriptor()); + + mSession = mIkev2SessionCreator.createIkeSession( + mContext, + ikeSessionParams, + childSessionParams, + mExecutor, + new VpnIkev2Utils.IkeSessionCallbackImpl( + TAG, IkeV2VpnRunner.this, network), + new VpnIkev2Utils.ChildSessionCallbackImpl( + TAG, IkeV2VpnRunner.this, network)); + Log.d(TAG, "Ike Session started for network " + network); + } catch (Exception e) { + Log.i(TAG, "Setup failed for network " + network + ". Aborting", e); + onSessionLost(network); + } + }); + } + + /** + * Handles loss of a session + * + * <p>The loss of a session might be due to an onLost() call, the IKE session getting torn + * down for any reason, or an error in updating state (transform application, VPN setup) + * + * <p>This method MUST always be called on the mExecutor thread in order to ensure + * consistency of the Ikev2VpnRunner fields. + */ + public void onSessionLost(@NonNull Network network) { + if (!isActiveNetwork(network)) { + Log.d(TAG, "onSessionLost() called for obsolete network " + network); + + // Do nothing; this signals that either: (1) a new/better Network was found, + // and the Ikev2VpnRunner has switched to it in onDefaultNetworkChanged, or (2) this + // IKE session was already shut down (exited, or an error was encountered somewhere + // else). In both cases, all resources and sessions are torn down via + // onSessionLost() and resetIkeState(). + return; + } + + mActiveNetwork = null; + + // Close all obsolete state, but keep VPN alive incase a usable network comes up. + // (Mirrors VpnService behavior) + Log.d(TAG, "Resetting state for network: " + network); + + synchronized (Vpn.this) { + // Since this method handles non-fatal errors only, set mInterface to null to + // prevent the NetworkManagementEventObserver from killing this VPN based on the + // interface going down (which we expect). + mInterface = null; + mConfig.interfaze = null; + + // Set as unroutable to prevent traffic leaking while the interface is down. + if (mConfig != null && mConfig.routes != null) { + final List<RouteInfo> oldRoutes = new ArrayList<>(mConfig.routes); + + mConfig.routes.clear(); + for (final RouteInfo route : oldRoutes) { + mConfig.routes.add(new RouteInfo(route.getDestination(), RTN_UNREACHABLE)); + } + if (mNetworkAgent != null) { + mNetworkAgent.sendLinkProperties(makeLinkProperties()); + } + } + } + + resetIkeState(); + } + + /** + * Cleans up all IKE state + * + * <p>This method MUST always be called on the mExecutor thread in order to ensure + * consistency of the Ikev2VpnRunner fields. + */ + private void resetIkeState() { + if (mTunnelIface != null) { + // No need to call setInterfaceDown(); the IpSecInterface is being fully torn down. + mTunnelIface.close(); + mTunnelIface = null; + } + if (mSession != null) { + mSession.kill(); // Kill here to make sure all resources are released immediately + mSession = null; + } + + // TODO(b/149356682): Update this based on new IKE API + if (mEncapSocket != null) { + try { + mEncapSocket.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to close encap socket", e); + } + mEncapSocket = null; + } + } + + /** + * Triggers cleanup of outer class' state + * + * <p>Can be called from any thread, as it does not mutate state in the Ikev2VpnRunner. + */ + private void cleanupVpnState() { + synchronized (Vpn.this) { + agentDisconnect(); + } + } + + /** + * Cleans up all Ikev2VpnRunner internal state + * + * <p>This method MUST always be called on the mExecutor thread in order to ensure + * consistency of the Ikev2VpnRunner fields. + */ + private void shutdownVpnRunner() { + mActiveNetwork = null; + mIsRunning = false; + + resetIkeState(); + + final ConnectivityManager cm = ConnectivityManager.from(mContext); + cm.unregisterNetworkCallback(mNetworkCallback); + + mExecutor.shutdown(); } @Override public void exit() { - // TODO: Teardown IKE session & any resources. - agentDisconnect(); + // Cleanup outer class' state immediately, otherwise race conditions may ensue. + cleanupVpnState(); + + mExecutor.execute(() -> { + shutdownVpnRunner(); + }); } } @@ -2484,12 +2846,46 @@ public class Vpn { throw new IllegalArgumentException("No profile found for " + packageName); } - startVpnProfilePrivileged(profile); + startVpnProfilePrivileged(profile, packageName); }); } - private void startVpnProfilePrivileged(@NonNull VpnProfile profile) { - // TODO: Start PlatformVpnRunner + private void startVpnProfilePrivileged( + @NonNull VpnProfile profile, @NonNull String packageName) { + // Ensure that no other previous instance is running. + if (mVpnRunner != null) { + mVpnRunner.exit(); + mVpnRunner = null; + } + updateState(DetailedState.CONNECTING, "startPlatformVpn"); + + try { + // Build basic config + mConfig = new VpnConfig(); + mConfig.user = packageName; + mConfig.isMetered = profile.isMetered; + mConfig.startTime = SystemClock.elapsedRealtime(); + mConfig.proxyInfo = profile.proxy; + + switch (profile.type) { + case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS: + case VpnProfile.TYPE_IKEV2_IPSEC_PSK: + case VpnProfile.TYPE_IKEV2_IPSEC_RSA: + mVpnRunner = new IkeV2VpnRunner(Ikev2VpnProfile.fromVpnProfile(profile)); + mVpnRunner.start(); + break; + default: + updateState(DetailedState.FAILED, "Invalid platform VPN type"); + Log.d(TAG, "Unknown VPN profile type: " + profile.type); + break; + } + } catch (IOException | GeneralSecurityException e) { + // Reset mConfig + mConfig = null; + + updateState(DetailedState.FAILED, "VPN startup failed"); + throw new IllegalArgumentException("VPN startup failed", e); + } } /** @@ -2503,13 +2899,37 @@ public class Vpn { public synchronized void stopVpnProfile(@NonNull String packageName) { checkNotNull(packageName, "No package name provided"); - // To stop the VPN profile, the caller must be the current prepared package. Otherwise, - // the app is not prepared, and we can just return. - if (!isCurrentPreparedPackage(packageName)) { - // TODO: Also check to make sure that the running VPN is a VPN profile. + // To stop the VPN profile, the caller must be the current prepared package and must be + // running an Ikev2VpnProfile. + if (!isCurrentPreparedPackage(packageName) && mVpnRunner instanceof IkeV2VpnRunner) { return; } prepareInternal(VpnConfig.LEGACY_VPN); } + + /** + * Proxy to allow testing + * + * @hide + */ + @VisibleForTesting + public static class Ikev2SessionCreator { + /** Creates a IKE session */ + public IkeSession createIkeSession( + @NonNull Context context, + @NonNull IkeSessionParams ikeSessionParams, + @NonNull ChildSessionParams firstChildSessionParams, + @NonNull Executor userCbExecutor, + @NonNull IkeSessionCallback ikeSessionCallback, + @NonNull ChildSessionCallback firstChildSessionCallback) { + return new IkeSession( + context, + ikeSessionParams, + firstChildSessionParams, + userCbExecutor, + ikeSessionCallback, + firstChildSessionCallback); + } + } } diff --git a/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java b/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java new file mode 100644 index 000000000000..33fc32b78df7 --- /dev/null +++ b/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2020 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.connectivity; + +import static android.net.ConnectivityManager.NetworkCallback; +import static android.net.ipsec.ike.SaProposal.DH_GROUP_1024_BIT_MODP; +import static android.net.ipsec.ike.SaProposal.DH_GROUP_2048_BIT_MODP; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_CBC; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_16; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_AES_XCBC_96; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_256_128; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_384_192; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_512_256; +import static android.net.ipsec.ike.SaProposal.KEY_LEN_AES_128; +import static android.net.ipsec.ike.SaProposal.KEY_LEN_AES_192; +import static android.net.ipsec.ike.SaProposal.KEY_LEN_AES_256; +import static android.net.ipsec.ike.SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC; +import static android.net.ipsec.ike.SaProposal.PSEUDORANDOM_FUNCTION_HMAC_SHA1; + +import android.annotation.NonNull; +import android.net.Ikev2VpnProfile; +import android.net.InetAddresses; +import android.net.IpPrefix; +import android.net.IpSecManager.UdpEncapsulationSocket; +import android.net.IpSecTransform; +import android.net.Network; +import android.net.RouteInfo; +import android.net.eap.EapSessionConfig; +import android.net.ipsec.ike.ChildSaProposal; +import android.net.ipsec.ike.ChildSessionCallback; +import android.net.ipsec.ike.ChildSessionConfiguration; +import android.net.ipsec.ike.ChildSessionParams; +import android.net.ipsec.ike.IkeFqdnIdentification; +import android.net.ipsec.ike.IkeIdentification; +import android.net.ipsec.ike.IkeIpv4AddrIdentification; +import android.net.ipsec.ike.IkeIpv6AddrIdentification; +import android.net.ipsec.ike.IkeKeyIdIdentification; +import android.net.ipsec.ike.IkeRfc822AddrIdentification; +import android.net.ipsec.ike.IkeSaProposal; +import android.net.ipsec.ike.IkeSessionCallback; +import android.net.ipsec.ike.IkeSessionConfiguration; +import android.net.ipsec.ike.IkeSessionParams; +import android.net.ipsec.ike.IkeTrafficSelector; +import android.net.ipsec.ike.TunnelModeChildSessionParams; +import android.net.ipsec.ike.exceptions.IkeException; +import android.net.ipsec.ike.exceptions.IkeProtocolException; +import android.net.util.IpRange; +import android.system.OsConstants; +import android.util.Log; + +import com.android.internal.net.VpnProfile; +import com.android.internal.util.HexDump; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +/** + * Utility class to build and convert IKEv2/IPsec parameters. + * + * @hide + */ +public class VpnIkev2Utils { + static IkeSessionParams buildIkeSessionParams( + @NonNull Ikev2VpnProfile profile, @NonNull UdpEncapsulationSocket socket) { + // TODO(b/149356682): Update this based on new IKE API. Only numeric addresses supported + // until then. All others throw IAE (caught by caller). + final InetAddress serverAddr = InetAddresses.parseNumericAddress(profile.getServerAddr()); + final IkeIdentification localId = parseIkeIdentification(profile.getUserIdentity()); + final IkeIdentification remoteId = parseIkeIdentification(profile.getServerAddr()); + + // TODO(b/149356682): Update this based on new IKE API. + final IkeSessionParams.Builder ikeOptionsBuilder = + new IkeSessionParams.Builder() + .setServerAddress(serverAddr) + .setUdpEncapsulationSocket(socket) + .setLocalIdentification(localId) + .setRemoteIdentification(remoteId); + setIkeAuth(profile, ikeOptionsBuilder); + + for (final IkeSaProposal ikeProposal : getIkeSaProposals()) { + ikeOptionsBuilder.addSaProposal(ikeProposal); + } + + return ikeOptionsBuilder.build(); + } + + static ChildSessionParams buildChildSessionParams() { + final TunnelModeChildSessionParams.Builder childOptionsBuilder = + new TunnelModeChildSessionParams.Builder(); + + for (final ChildSaProposal childProposal : getChildSaProposals()) { + childOptionsBuilder.addSaProposal(childProposal); + } + + childOptionsBuilder.addInternalAddressRequest(OsConstants.AF_INET); + childOptionsBuilder.addInternalAddressRequest(OsConstants.AF_INET6); + childOptionsBuilder.addInternalDnsServerRequest(OsConstants.AF_INET); + childOptionsBuilder.addInternalDnsServerRequest(OsConstants.AF_INET6); + + return childOptionsBuilder.build(); + } + + private static void setIkeAuth( + @NonNull Ikev2VpnProfile profile, @NonNull IkeSessionParams.Builder builder) { + switch (profile.getType()) { + case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS: + final EapSessionConfig eapConfig = + new EapSessionConfig.Builder() + .setEapMsChapV2Config(profile.getUsername(), profile.getPassword()) + .build(); + builder.setAuthEap(profile.getServerRootCaCert(), eapConfig); + break; + case VpnProfile.TYPE_IKEV2_IPSEC_PSK: + builder.setAuthPsk(profile.getPresharedKey()); + break; + case VpnProfile.TYPE_IKEV2_IPSEC_RSA: + builder.setAuthDigitalSignature( + profile.getServerRootCaCert(), + profile.getUserCert(), + profile.getRsaPrivateKey()); + break; + default: + throw new IllegalArgumentException("Unknown auth method set"); + } + } + + private static List<IkeSaProposal> getIkeSaProposals() { + // TODO: filter this based on allowedAlgorithms + final List<IkeSaProposal> proposals = new ArrayList<>(); + + // Encryption Algorithms: Currently only AES_CBC is supported. + final IkeSaProposal.Builder normalModeBuilder = new IkeSaProposal.Builder(); + + // Currently only AES_CBC is supported. + normalModeBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_256); + normalModeBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_192); + normalModeBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_128); + + // Authentication/Integrity Algorithms + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_512_256); + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_384_192); + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_256_128); + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_AES_XCBC_96); + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA1_96); + + // Add AEAD options + final IkeSaProposal.Builder aeadBuilder = new IkeSaProposal.Builder(); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_16, KEY_LEN_AES_256); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_256); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_8, KEY_LEN_AES_256); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_16, KEY_LEN_AES_192); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_192); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_8, KEY_LEN_AES_192); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_16, KEY_LEN_AES_128); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_128); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_8, KEY_LEN_AES_128); + + // Add dh, prf for both builders + for (final IkeSaProposal.Builder builder : Arrays.asList(normalModeBuilder, aeadBuilder)) { + builder.addDhGroup(DH_GROUP_2048_BIT_MODP); + builder.addDhGroup(DH_GROUP_1024_BIT_MODP); + builder.addPseudorandomFunction(PSEUDORANDOM_FUNCTION_AES128_XCBC); + builder.addPseudorandomFunction(PSEUDORANDOM_FUNCTION_HMAC_SHA1); + } + + proposals.add(normalModeBuilder.build()); + proposals.add(aeadBuilder.build()); + return proposals; + } + + private static List<ChildSaProposal> getChildSaProposals() { + // TODO: filter this based on allowedAlgorithms + final List<ChildSaProposal> proposals = new ArrayList<>(); + + // Add non-AEAD options + final ChildSaProposal.Builder normalModeBuilder = new ChildSaProposal.Builder(); + + // Encryption Algorithms: Currently only AES_CBC is supported. + normalModeBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_256); + normalModeBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_192); + normalModeBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_128); + + // Authentication/Integrity Algorithms + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_512_256); + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_384_192); + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_256_128); + normalModeBuilder.addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA1_96); + + // Add AEAD options + final ChildSaProposal.Builder aeadBuilder = new ChildSaProposal.Builder(); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_16, KEY_LEN_AES_256); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_256); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_8, KEY_LEN_AES_256); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_16, KEY_LEN_AES_192); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_192); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_8, KEY_LEN_AES_192); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_16, KEY_LEN_AES_128); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_128); + aeadBuilder.addEncryptionAlgorithm(ENCRYPTION_ALGORITHM_AES_GCM_8, KEY_LEN_AES_128); + + proposals.add(normalModeBuilder.build()); + proposals.add(aeadBuilder.build()); + return proposals; + } + + static class IkeSessionCallbackImpl implements IkeSessionCallback { + private final String mTag; + private final Vpn.IkeV2VpnRunnerCallback mCallback; + private final Network mNetwork; + + IkeSessionCallbackImpl(String tag, Vpn.IkeV2VpnRunnerCallback callback, Network network) { + mTag = tag; + mCallback = callback; + mNetwork = network; + } + + @Override + public void onOpened(@NonNull IkeSessionConfiguration ikeSessionConfig) { + Log.d(mTag, "IkeOpened for network " + mNetwork); + // Nothing to do here. + } + + @Override + public void onClosed() { + Log.d(mTag, "IkeClosed for network " + mNetwork); + mCallback.onSessionLost(mNetwork); // Server requested session closure. Retry? + } + + @Override + public void onClosedExceptionally(@NonNull IkeException exception) { + Log.d(mTag, "IkeClosedExceptionally for network " + mNetwork, exception); + mCallback.onSessionLost(mNetwork); + } + + @Override + public void onError(@NonNull IkeProtocolException exception) { + Log.d(mTag, "IkeError for network " + mNetwork, exception); + // Non-fatal, log and continue. + } + } + + static class ChildSessionCallbackImpl implements ChildSessionCallback { + private final String mTag; + private final Vpn.IkeV2VpnRunnerCallback mCallback; + private final Network mNetwork; + + ChildSessionCallbackImpl(String tag, Vpn.IkeV2VpnRunnerCallback callback, Network network) { + mTag = tag; + mCallback = callback; + mNetwork = network; + } + + @Override + public void onOpened(@NonNull ChildSessionConfiguration childConfig) { + Log.d(mTag, "ChildOpened for network " + mNetwork); + mCallback.onChildOpened(mNetwork, childConfig); + } + + @Override + public void onClosed() { + Log.d(mTag, "ChildClosed for network " + mNetwork); + mCallback.onSessionLost(mNetwork); + } + + @Override + public void onClosedExceptionally(@NonNull IkeException exception) { + Log.d(mTag, "ChildClosedExceptionally for network " + mNetwork, exception); + mCallback.onSessionLost(mNetwork); + } + + @Override + public void onIpSecTransformCreated(@NonNull IpSecTransform transform, int direction) { + Log.d(mTag, "ChildTransformCreated; Direction: " + direction + "; network " + mNetwork); + mCallback.onChildTransformCreated(mNetwork, transform, direction); + } + + @Override + public void onIpSecTransformDeleted(@NonNull IpSecTransform transform, int direction) { + // Nothing to be done; no references to the IpSecTransform are held by the + // Ikev2VpnRunner (or this callback class), and this transform will be closed by the + // IKE library. + Log.d(mTag, + "ChildTransformDeleted; Direction: " + direction + "; for network " + mNetwork); + } + } + + static class Ikev2VpnNetworkCallback extends NetworkCallback { + private final String mTag; + private final Vpn.IkeV2VpnRunnerCallback mCallback; + + Ikev2VpnNetworkCallback(String tag, Vpn.IkeV2VpnRunnerCallback callback) { + mTag = tag; + mCallback = callback; + } + + @Override + public void onAvailable(@NonNull Network network) { + Log.d(mTag, "Starting IKEv2/IPsec session on new network: " + network); + mCallback.onDefaultNetworkChanged(network); + } + + @Override + public void onLost(@NonNull Network network) { + Log.d(mTag, "Tearing down; lost network: " + network); + mCallback.onSessionLost(network); + } + } + + /** + * Identity parsing logic using similar logic to open source implementations of IKEv2 + * + * <p>This method does NOT support using type-prefixes (eg 'fqdn:' or 'keyid'), or ASN.1 encoded + * identities. + */ + private static IkeIdentification parseIkeIdentification(@NonNull String identityStr) { + // TODO: Add identity formatting to public API javadocs. + if (identityStr.contains("@")) { + if (identityStr.startsWith("@#")) { + // KEY_ID + final String hexStr = identityStr.substring(2); + return new IkeKeyIdIdentification(HexDump.hexStringToByteArray(hexStr)); + } else if (identityStr.startsWith("@@")) { + // RFC822 (USER_FQDN) + return new IkeRfc822AddrIdentification(identityStr.substring(2)); + } else if (identityStr.startsWith("@")) { + // FQDN + return new IkeFqdnIdentification(identityStr.substring(1)); + } else { + // RFC822 (USER_FQDN) + return new IkeRfc822AddrIdentification(identityStr); + } + } else if (InetAddresses.isNumericAddress(identityStr)) { + final InetAddress addr = InetAddresses.parseNumericAddress(identityStr); + if (addr instanceof Inet4Address) { + // IPv4 + return new IkeIpv4AddrIdentification((Inet4Address) addr); + } else if (addr instanceof Inet6Address) { + // IPv6 + return new IkeIpv6AddrIdentification((Inet6Address) addr); + } else { + throw new IllegalArgumentException("IP version not supported"); + } + } else { + if (identityStr.contains(":")) { + // KEY_ID + return new IkeKeyIdIdentification(identityStr.getBytes()); + } else { + // FQDN + return new IkeFqdnIdentification(identityStr); + } + } + } + + static Collection<RouteInfo> getRoutesFromTrafficSelectors( + List<IkeTrafficSelector> trafficSelectors) { + final HashSet<RouteInfo> routes = new HashSet<>(); + + for (final IkeTrafficSelector selector : trafficSelectors) { + for (final IpPrefix prefix : + new IpRange(selector.startingAddress, selector.endingAddress).asIpPrefixes()) { + routes.add(new RouteInfo(prefix, null)); + } + } + + return routes; + } +} diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java index 155c61f3f8c7..eb78529e8715 100644 --- a/tests/net/java/com/android/server/connectivity/VpnTest.java +++ b/tests/net/java/com/android/server/connectivity/VpnTest.java @@ -148,6 +148,7 @@ public class VpnTest { @Mock private AppOpsManager mAppOps; @Mock private NotificationManager mNotificationManager; @Mock private Vpn.SystemServices mSystemServices; + @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator; @Mock private ConnectivityManager mConnectivityManager; @Mock private KeyStore mKeyStore; private final VpnProfile mVpnProfile = new VpnProfile("key"); @@ -867,7 +868,8 @@ public class VpnTest { * Mock some methods of vpn object. */ private Vpn createVpn(@UserIdInt int userId) { - return new Vpn(Looper.myLooper(), mContext, mNetService, userId, mSystemServices); + return new Vpn(Looper.myLooper(), mContext, mNetService, + userId, mSystemServices, mIkev2SessionCreator); } private static void assertBlocked(Vpn vpn, int... uids) { |