diff options
| author | Benedict Wong <benedictwong@google.com> | 2020-01-23 05:11:47 +0000 |
|---|---|---|
| committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2020-01-23 05:11:47 +0000 |
| commit | adb79e3b3946ccd5cc91bd4123991dce7d2e6ad7 (patch) | |
| tree | 7494a669feae87be51c14a40456e06f275db8162 /core/java/android | |
| parent | db46c349d80895abd36a0c3ec55b752762cbfe12 (diff) | |
| parent | 50b444359e95d5325d4c7da536893c45e529d28b (diff) | |
Merge changes from topic "add-ikev2-vpn-types"
* changes:
Add VpnManger API surface
Add Ikev2VpnProfile as public API
Add additional fields to VpnProfile for profile-based IKEv2/IPsec VPNs
Diffstat (limited to 'core/java/android')
| -rw-r--r-- | core/java/android/app/SystemServiceRegistry.java | 10 | ||||
| -rw-r--r-- | core/java/android/content/Context.java | 9 | ||||
| -rw-r--r-- | core/java/android/net/Ikev2VpnProfile.java | 728 | ||||
| -rw-r--r-- | core/java/android/net/LinkProperties.java | 3 | ||||
| -rw-r--r-- | core/java/android/net/PlatformVpnProfile.java | 107 | ||||
| -rw-r--r-- | core/java/android/net/VpnManager.java | 88 |
6 files changed, 944 insertions, 1 deletions
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 8e12a221b5f7..81f6d28db9fe 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -113,6 +113,7 @@ import android.net.NetworkScoreManager; import android.net.NetworkWatchlistManager; import android.net.TestNetworkManager; import android.net.TetheringManager; +import android.net.VpnManager; import android.net.lowpan.ILowpanManager; import android.net.lowpan.LowpanManager; import android.net.nsd.INsdManager; @@ -370,6 +371,15 @@ final class SystemServiceRegistry { return new IpSecManager(ctx, service); }}); + registerService(Context.VPN_MANAGEMENT_SERVICE, VpnManager.class, + new CachedServiceFetcher<VpnManager>() { + @Override + public VpnManager createService(ContextImpl ctx) throws ServiceNotFoundException { + IBinder b = ServiceManager.getService(Context.CONNECTIVITY_SERVICE); + IConnectivityManager service = IConnectivityManager.Stub.asInterface(b); + return new VpnManager(ctx, service); + }}); + registerService(Context.CONNECTIVITY_DIAGNOSTICS_SERVICE, ConnectivityDiagnosticsManager.class, new CachedServiceFetcher<ConnectivityDiagnosticsManager>() { diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 725fd0531911..472d9567e2ad 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -3296,6 +3296,7 @@ public abstract class Context { CONNECTIVITY_SERVICE, //@hide: IP_MEMORY_STORE_SERVICE, IPSEC_SERVICE, + VPN_MANAGEMENT_SERVICE, TEST_NETWORK_SERVICE, //@hide: UPDATE_LOCK_SERVICE, //@hide: NETWORKMANAGEMENT_SERVICE, @@ -3880,6 +3881,14 @@ public abstract class Context { public static final String IPSEC_SERVICE = "ipsec"; /** + * Use with {@link #getSystemService(String)} to retrieve a {@link android.net.VpnManager} to + * manage profiles for the platform built-in VPN. + * + * @see #getSystemService(String) + */ + public static final String VPN_MANAGEMENT_SERVICE = "vpn_management"; + + /** * Use with {@link #getSystemService(String)} to retrieve a {@link * android.net.ConnectivityDiagnosticsManager} for performing network connectivity diagnostics * as well as receiving network connectivity information from the system. diff --git a/core/java/android/net/Ikev2VpnProfile.java b/core/java/android/net/Ikev2VpnProfile.java new file mode 100644 index 000000000000..42b4da14d879 --- /dev/null +++ b/core/java/android/net/Ikev2VpnProfile.java @@ -0,0 +1,728 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import static android.net.PlatformVpnProfile.TYPE_IKEV2_IPSEC_PSK; +import static android.net.PlatformVpnProfile.TYPE_IKEV2_IPSEC_RSA; +import static android.net.PlatformVpnProfile.TYPE_IKEV2_IPSEC_USER_PASS; + +import static com.android.internal.annotations.VisibleForTesting.Visibility; +import static com.android.internal.util.Preconditions.checkStringNotEmpty; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.security.Credentials; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.net.VpnProfile; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * The Ikev2VpnProfile is a configuration for the platform setup of IKEv2/IPsec VPNs. + * + * <p>Together with VpnManager, this allows apps to provision IKEv2/IPsec VPNs that do not require + * the VPN app to constantly run in the background. + * + * @see VpnManager + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296 - Internet Key + * Exchange, Version 2 (IKEv2)</a> + */ +public final class Ikev2VpnProfile extends PlatformVpnProfile { + private static final String MISSING_PARAM_MSG_TMPL = "Required parameter was not provided: %s"; + private static final String EMPTY_CERT = ""; + + @NonNull private final String mServerAddr; + @NonNull private final String mUserIdentity; + + // PSK authentication + @Nullable private final byte[] mPresharedKey; + + // Username/Password, RSA authentication + @Nullable private final X509Certificate mServerRootCaCert; + + // Username/Password authentication + @Nullable private final String mUsername; + @Nullable private final String mPassword; + + // RSA Certificate authentication + @Nullable private final PrivateKey mRsaPrivateKey; + @Nullable private final X509Certificate mUserCert; + + @Nullable private final ProxyInfo mProxyInfo; + @NonNull private final List<String> mAllowedAlgorithms; + private final boolean mIsBypassable; // Defaults in builder + private final boolean mIsMetered; // Defaults in builder + private final int mMaxMtu; // Defaults in builder + + private Ikev2VpnProfile( + int type, + @NonNull String serverAddr, + @NonNull String userIdentity, + @Nullable byte[] presharedKey, + @Nullable X509Certificate serverRootCaCert, + @Nullable String username, + @Nullable String password, + @Nullable PrivateKey rsaPrivateKey, + @Nullable X509Certificate userCert, + @Nullable ProxyInfo proxyInfo, + @NonNull List<String> allowedAlgorithms, + boolean isBypassable, + boolean isMetered, + int maxMtu) { + super(type); + + checkNotNull(serverAddr, MISSING_PARAM_MSG_TMPL, "Server address"); + checkNotNull(userIdentity, MISSING_PARAM_MSG_TMPL, "User Identity"); + checkNotNull(allowedAlgorithms, MISSING_PARAM_MSG_TMPL, "Allowed Algorithms"); + + mServerAddr = serverAddr; + mUserIdentity = userIdentity; + mPresharedKey = + presharedKey == null ? null : Arrays.copyOf(presharedKey, presharedKey.length); + mServerRootCaCert = serverRootCaCert; + mUsername = username; + mPassword = password; + mRsaPrivateKey = rsaPrivateKey; + mUserCert = userCert; + mProxyInfo = new ProxyInfo(proxyInfo); + + // UnmodifiableList doesn't make a defensive copy by default. + mAllowedAlgorithms = Collections.unmodifiableList(new ArrayList<>(allowedAlgorithms)); + + mIsBypassable = isBypassable; + mIsMetered = isMetered; + mMaxMtu = maxMtu; + + validate(); + } + + private void validate() { + // Server Address not validated except to check an address was provided. This allows for + // dual-stack servers and hostname based addresses. + checkStringNotEmpty(mServerAddr, MISSING_PARAM_MSG_TMPL, "Server Address"); + checkStringNotEmpty(mUserIdentity, MISSING_PARAM_MSG_TMPL, "User Identity"); + + // IPv6 MTU is greater; since profiles may be started by the system on IPv4 and IPv6 + // networks, the VPN must provide a link fulfilling the stricter of the two conditions + // (at least that of the IPv6 MTU). + if (mMaxMtu < LinkProperties.MIN_MTU_V6) { + throw new IllegalArgumentException( + "Max MTU must be at least" + LinkProperties.MIN_MTU_V6); + } + + switch (mType) { + case TYPE_IKEV2_IPSEC_USER_PASS: + checkNotNull(mUsername, MISSING_PARAM_MSG_TMPL, "Username"); + checkNotNull(mPassword, MISSING_PARAM_MSG_TMPL, "Password"); + + if (mServerRootCaCert != null) checkCert(mServerRootCaCert); + + break; + case TYPE_IKEV2_IPSEC_PSK: + checkNotNull(mPresharedKey, MISSING_PARAM_MSG_TMPL, "Preshared Key"); + break; + case TYPE_IKEV2_IPSEC_RSA: + checkNotNull(mUserCert, MISSING_PARAM_MSG_TMPL, "User cert"); + checkNotNull(mRsaPrivateKey, MISSING_PARAM_MSG_TMPL, "RSA Private key"); + + checkCert(mUserCert); + if (mServerRootCaCert != null) checkCert(mServerRootCaCert); + + break; + default: + throw new IllegalArgumentException("Invalid auth method set"); + } + + VpnProfile.validateAllowedAlgorithms(mAllowedAlgorithms); + } + + /** Retrieves the server address string. */ + @NonNull + public String getServerAddr() { + return mServerAddr; + } + + /** Retrieves the user identity. */ + @NonNull + public String getUserIdentity() { + return mUserIdentity; + } + + /** + * Retrieves the pre-shared key. + * + * <p>May be null if the profile is not using Pre-shared key authentication. + */ + @Nullable + public byte[] getPresharedKey() { + return mPresharedKey == null ? null : Arrays.copyOf(mPresharedKey, mPresharedKey.length); + } + + /** + * Retrieves the certificate for the server's root CA. + * + * <p>May be null if the profile is not using RSA Digital Signature Authentication or + * Username/Password authentication + */ + @Nullable + public X509Certificate getServerRootCaCert() { + return mServerRootCaCert; + } + + /** + * Retrieves the username. + * + * <p>May be null if the profile is not using Username/Password authentication + */ + @Nullable + public String getUsername() { + return mUsername; + } + + /** + * Retrieves the password. + * + * <p>May be null if the profile is not using Username/Password authentication + */ + @Nullable + public String getPassword() { + return mPassword; + } + + /** + * Retrieves the RSA private key. + * + * <p>May be null if the profile is not using RSA Digital Signature authentication + */ + @Nullable + public PrivateKey getRsaPrivateKey() { + return mRsaPrivateKey; + } + + /** Retrieves the user certificate, if any was set. */ + @Nullable + public X509Certificate getUserCert() { + return mUserCert; + } + + /** Retrieves the proxy information if any was set */ + @Nullable + public ProxyInfo getProxyInfo() { + return mProxyInfo; + } + + /** Returns all the algorithms allowed by this VPN profile. */ + @NonNull + public List<String> getAllowedAlgorithms() { + return mAllowedAlgorithms; + } + + /** Returns whether or not the VPN profile should be bypassable. */ + public boolean isBypassable() { + return mIsBypassable; + } + + /** Returns whether or not the VPN profile should be always considered metered. */ + public boolean isMetered() { + return mIsMetered; + } + + /** Retrieves the maximum MTU set for this VPN profile. */ + public int getMaxMtu() { + return mMaxMtu; + } + + @Override + public int hashCode() { + return Objects.hash( + mType, + mServerAddr, + mUserIdentity, + Arrays.hashCode(mPresharedKey), + mServerRootCaCert, + mUsername, + mPassword, + mRsaPrivateKey, + mUserCert, + mProxyInfo, + mAllowedAlgorithms, + mIsBypassable, + mIsMetered, + mMaxMtu); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Ikev2VpnProfile)) { + return false; + } + + final Ikev2VpnProfile other = (Ikev2VpnProfile) obj; + return mType == other.mType + && Objects.equals(mServerAddr, other.mServerAddr) + && Objects.equals(mUserIdentity, other.mUserIdentity) + && Arrays.equals(mPresharedKey, other.mPresharedKey) + && Objects.equals(mServerRootCaCert, other.mServerRootCaCert) + && Objects.equals(mUsername, other.mUsername) + && Objects.equals(mPassword, other.mPassword) + && Objects.equals(mRsaPrivateKey, other.mRsaPrivateKey) + && Objects.equals(mUserCert, other.mUserCert) + && Objects.equals(mProxyInfo, other.mProxyInfo) + && Objects.equals(mAllowedAlgorithms, other.mAllowedAlgorithms) + && mIsBypassable == other.mIsBypassable + && mIsMetered == other.mIsMetered + && mMaxMtu == other.mMaxMtu; + } + + /** + * Builds a VpnProfile instance for internal use, based on the stored IKEv2/IPsec parameters. + * + * <p>Redundant authentication information (from previous calls to other setAuth* methods) will + * be discarded. + * + * @hide + */ + @NonNull + public VpnProfile toVpnProfile() throws IOException, GeneralSecurityException { + final VpnProfile profile = new VpnProfile("" /* Key; value unused by IKEv2VpnProfile(s) */); + profile.type = mType; + profile.server = mServerAddr; + profile.ipsecIdentifier = mUserIdentity; + profile.proxy = mProxyInfo; + profile.setAllowedAlgorithms(mAllowedAlgorithms); + profile.isBypassable = mIsBypassable; + profile.isMetered = mIsMetered; + profile.maxMtu = mMaxMtu; + profile.areAuthParamsInline = true; + profile.saveLogin = true; + + switch (mType) { + case TYPE_IKEV2_IPSEC_USER_PASS: + profile.username = mUsername; + profile.password = mPassword; + profile.ipsecCaCert = + mServerRootCaCert == null ? "" : certificateToPemString(mServerRootCaCert); + break; + case TYPE_IKEV2_IPSEC_PSK: + profile.ipsecSecret = encodeForIpsecSecret(mPresharedKey); + break; + case TYPE_IKEV2_IPSEC_RSA: + profile.ipsecUserCert = certificateToPemString(mUserCert); + profile.ipsecSecret = encodeForIpsecSecret(mRsaPrivateKey.getEncoded()); + profile.ipsecCaCert = + mServerRootCaCert == null ? "" : certificateToPemString(mServerRootCaCert); + break; + default: + throw new IllegalArgumentException("Invalid auth method set"); + } + + return profile; + } + + /** + * Constructs a Ikev2VpnProfile from an internal-use VpnProfile instance. + * + * <p>Redundant authentication information (not related to profile type) will be discarded. + * + * @hide + */ + @NonNull + public static Ikev2VpnProfile fromVpnProfile(@NonNull VpnProfile profile) + throws IOException, GeneralSecurityException { + final Builder builder = new Builder(profile.server, profile.ipsecIdentifier); + builder.setProxy(profile.proxy); + builder.setAllowedAlgorithms(profile.getAllowedAlgorithms()); + builder.setBypassable(profile.isBypassable); + builder.setMetered(profile.isMetered); + builder.setMaxMtu(profile.maxMtu); + + switch (profile.type) { + case TYPE_IKEV2_IPSEC_USER_PASS: + builder.setAuthUsernamePassword( + profile.username, + profile.password, + certificateFromPemString(profile.ipsecCaCert)); + break; + case TYPE_IKEV2_IPSEC_PSK: + builder.setAuthPsk(decodeFromIpsecSecret(profile.ipsecSecret)); + break; + case TYPE_IKEV2_IPSEC_RSA: + final X509Certificate userCert = certificateFromPemString(profile.ipsecUserCert); + final PrivateKey key = getPrivateKey(profile.ipsecSecret); + final X509Certificate serverRootCa = certificateFromPemString(profile.ipsecCaCert); + builder.setAuthDigitalSignature(userCert, key, serverRootCa); + break; + default: + throw new IllegalArgumentException("Invalid auth method set"); + } + + return builder.build(); + } + + /** + * Converts a X509 Certificate to a PEM-formatted string. + * + * <p>Must be public due to runtime-package restrictions. + * + * @hide + */ + @NonNull + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static String certificateToPemString(@Nullable X509Certificate cert) + throws IOException, CertificateEncodingException { + if (cert == null) { + return EMPTY_CERT; + } + + // Credentials.convertToPem outputs ASCII bytes. + return new String(Credentials.convertToPem(cert), StandardCharsets.US_ASCII); + } + + /** + * Decodes the provided Certificate(s). + * + * <p>Will use the first one if the certStr encodes more than one certificate. + */ + @Nullable + private static X509Certificate certificateFromPemString(@Nullable String certStr) + throws CertificateException { + if (certStr == null || EMPTY_CERT.equals(certStr)) { + return null; + } + + try { + final List<X509Certificate> certs = + Credentials.convertFromPem(certStr.getBytes(StandardCharsets.US_ASCII)); + return certs.isEmpty() ? null : certs.get(0); + } catch (IOException e) { + throw new CertificateException(e); + } + } + + /** @hide */ + @NonNull + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static String encodeForIpsecSecret(@NonNull byte[] secret) { + checkNotNull(secret, MISSING_PARAM_MSG_TMPL, "secret"); + + return Base64.getEncoder().encodeToString(secret); + } + + @NonNull + private static byte[] decodeFromIpsecSecret(@NonNull String encoded) { + checkNotNull(encoded, MISSING_PARAM_MSG_TMPL, "encoded"); + + return Base64.getDecoder().decode(encoded); + } + + @NonNull + private static PrivateKey getPrivateKey(@NonNull String keyStr) + throws InvalidKeySpecException, NoSuchAlgorithmException { + final PKCS8EncodedKeySpec privateKeySpec = + new PKCS8EncodedKeySpec(decodeFromIpsecSecret(keyStr)); + final KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePrivate(privateKeySpec); + } + + private static void checkCert(@NonNull X509Certificate cert) { + try { + certificateToPemString(cert); + } catch (GeneralSecurityException | IOException e) { + throw new IllegalArgumentException("Certificate could not be encoded"); + } + } + + private static @NonNull <T> T checkNotNull( + final T reference, final String messageTemplate, final Object... messageArgs) { + return Objects.requireNonNull(reference, String.format(messageTemplate, messageArgs)); + } + + /** A incremental builder for IKEv2 VPN profiles */ + public static final class Builder { + private int mType = -1; + @NonNull private final String mServerAddr; + @NonNull private final String mUserIdentity; + + // PSK authentication + @Nullable private byte[] mPresharedKey; + + // Username/Password, RSA authentication + @Nullable private X509Certificate mServerRootCaCert; + + // Username/Password authentication + @Nullable private String mUsername; + @Nullable private String mPassword; + + // RSA Certificate authentication + @Nullable private PrivateKey mRsaPrivateKey; + @Nullable private X509Certificate mUserCert; + + @Nullable private ProxyInfo mProxyInfo; + @NonNull private List<String> mAllowedAlgorithms = new ArrayList<>(); + private boolean mIsBypassable = false; + private boolean mIsMetered = true; + private int mMaxMtu = 1360; + + /** + * Creates a new builder with the basic parameters of an IKEv2/IPsec VPN. + * + * @param serverAddr the server that the VPN should connect to + * @param identity the identity string to be used for IKEv2 authentication + */ + public Builder(@NonNull String serverAddr, @NonNull String identity) { + checkNotNull(serverAddr, MISSING_PARAM_MSG_TMPL, "serverAddr"); + checkNotNull(identity, MISSING_PARAM_MSG_TMPL, "identity"); + + mServerAddr = serverAddr; + mUserIdentity = identity; + } + + private void resetAuthParams() { + mPresharedKey = null; + mServerRootCaCert = null; + mUsername = null; + mPassword = null; + mRsaPrivateKey = null; + mUserCert = null; + } + + /** + * Set the IKEv2 authentication to use the provided username/password. + * + * <p>Setting this will configure IKEv2 authentication using EAP-MSCHAPv2. Only one + * authentication method may be set. This method will overwrite any previously set + * authentication method. + * + * @param user the username to be used for EAP-MSCHAPv2 authentication + * @param pass the password to be used for EAP-MSCHAPv2 authentication + * @param serverRootCa the root certificate to be used for verifying the identity of the + * server + * @return this {@link Builder} object to facilitate chaining of method calls + * @throws IllegalArgumentException if any of the certificates were invalid or of an + * unrecognized format + */ + @NonNull + public Builder setAuthUsernamePassword( + @NonNull String user, + @NonNull String pass, + @Nullable X509Certificate serverRootCa) { + checkNotNull(user, MISSING_PARAM_MSG_TMPL, "user"); + checkNotNull(pass, MISSING_PARAM_MSG_TMPL, "pass"); + + // Test to make sure all auth params can be encoded safely. + if (serverRootCa != null) checkCert(serverRootCa); + + resetAuthParams(); + mUsername = user; + mPassword = pass; + mServerRootCaCert = serverRootCa; + mType = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS; + return this; + } + + /** + * Set the IKEv2 authentication to use Digital Signature Authentication with the given key. + * + * <p>Setting this will configure IKEv2 authentication using a Digital Signature scheme. + * Only one authentication method may be set. This method will overwrite any previously set + * authentication method. + * + * @param userCert the username to be used for RSA Digital signiture authentication + * @param key the PrivateKey instance associated with the user ceritificate, used for + * constructing the signature + * @param serverRootCa the root certificate to be used for verifying the identity of the + * server + * @return this {@link Builder} object to facilitate chaining of method calls + * @throws IllegalArgumentException if any of the certificates were invalid or of an + * unrecognized format + */ + @NonNull + public Builder setAuthDigitalSignature( + @NonNull X509Certificate userCert, + @NonNull PrivateKey key, + @Nullable X509Certificate serverRootCa) { + checkNotNull(userCert, MISSING_PARAM_MSG_TMPL, "userCert"); + checkNotNull(key, MISSING_PARAM_MSG_TMPL, "key"); + + // Test to make sure all auth params can be encoded safely. + checkCert(userCert); + if (serverRootCa != null) checkCert(serverRootCa); + + resetAuthParams(); + mUserCert = userCert; + mRsaPrivateKey = key; + mServerRootCaCert = serverRootCa; + mType = VpnProfile.TYPE_IKEV2_IPSEC_RSA; + return this; + } + + /** + * Set the IKEv2 authentication to use Preshared keys. + * + * <p>Setting this will configure IKEv2 authentication using a Preshared Key. Only one + * authentication method may be set. This method will overwrite any previously set + * authentication method. + * + * @param psk the key to be used for Pre-Shared Key authentication + * @return this {@link Builder} object to facilitate chaining of method calls + */ + @NonNull + public Builder setAuthPsk(@NonNull byte[] psk) { + checkNotNull(psk, MISSING_PARAM_MSG_TMPL, "psk"); + + resetAuthParams(); + mPresharedKey = psk; + mType = VpnProfile.TYPE_IKEV2_IPSEC_PSK; + return this; + } + + /** + * Sets whether apps can bypass this VPN connection. + * + * <p>By default, all traffic from apps are forwarded through the VPN interface and it is + * not possible for unprivileged apps to side-step the VPN. If a VPN is set to bypassable, + * apps may use methods such as {@link Network#getSocketFactory} or {@link + * Network#openConnection} to instead send/receive directly over the underlying network or + * any other network they have permissions for. + * + * @param isBypassable Whether or not the VPN should be considered bypassable. Defaults to + * {@code false}. + * @return this {@link Builder} object to facilitate chaining of method calls + */ + @NonNull + public Builder setBypassable(boolean isBypassable) { + mIsBypassable = isBypassable; + return this; + } + + /** + * Sets a proxy for the VPN network. + * + * <p>Note that this proxy is only a recommendation and it may be ignored by apps. + * + * @param proxy the ProxyInfo to be set for the VPN network + * @return this {@link Builder} object to facilitate chaining of method calls + */ + @NonNull + public Builder setProxy(@Nullable ProxyInfo proxy) { + mProxyInfo = proxy; + return this; + } + + /** + * Set the upper bound of the maximum transmission unit (MTU) of the VPN interface. + * + * <p>If it is not set, a safe value will be used. Additionally, the actual link MTU will be + * dynamically calculated/updated based on the underlying link's mtu. + * + * @param mtu the MTU (in bytes) of the VPN interface + * @return this {@link Builder} object to facilitate chaining of method calls + * @throws IllegalArgumentException if the value is not at least the minimum IPv6 MTU (1280) + */ + @NonNull + public Builder setMaxMtu(int mtu) { + // IPv6 MTU is greater; since profiles may be started by the system on IPv4 and IPv6 + // networks, the VPN must provide a link fulfilling the stricter of the two conditions + // (at least that of the IPv6 MTU). + if (mtu < LinkProperties.MIN_MTU_V6) { + throw new IllegalArgumentException( + "Max MTU must be at least " + LinkProperties.MIN_MTU_V6); + } + mMaxMtu = mtu; + return this; + } + + /** + * Marks the VPN network as metered. + * + * <p>A VPN network is classified as metered when the user is sensitive to heavy data usage + * due to monetary costs and/or data limitations. In such cases, you should set this to + * {@code true} so that apps on the system can avoid doing large data transfers. Otherwise, + * set this to {@code false}. Doing so would cause VPN network to inherit its meteredness + * from the underlying network. + * + * @param isMetered {@code true} if the VPN network should be treated as metered regardless + * of underlying network meteredness. Defaults to {@code true}. + * @return this {@link Builder} object to facilitate chaining of method calls + * @see NetworkCapabilities.NET_CAPABILITY_NOT_METERED + */ + @NonNull + public Builder setMetered(boolean isMetered) { + mIsMetered = isMetered; + return this; + } + + /** + * Sets the allowable set of IPsec algorithms + * + * <p>A list of allowed IPsec algorithms as defined in {@link IpSecAlgorithm} + * + * @param algorithmNames the list of supported IPsec algorithms + * @return this {@link Builder} object to facilitate chaining of method calls + * @see IpSecAlgorithm + */ + @NonNull + public Builder setAllowedAlgorithms(@NonNull List<String> algorithmNames) { + checkNotNull(algorithmNames, MISSING_PARAM_MSG_TMPL, "algorithmNames"); + VpnProfile.validateAllowedAlgorithms(algorithmNames); + + mAllowedAlgorithms = algorithmNames; + return this; + } + + /** + * Validates, builds and provisions the VpnProfile. + * + * @throws IllegalArgumentException if any of the required keys or values were invalid + */ + @NonNull + public Ikev2VpnProfile build() { + return new Ikev2VpnProfile( + mType, + mServerAddr, + mUserIdentity, + mPresharedKey, + mServerRootCaCert, + mUsername, + mPassword, + mRsaPrivateKey, + mUserCert, + mProxyInfo, + mAllowedAlgorithms, + mIsBypassable, + mIsMetered, + mMaxMtu); + } + } +} diff --git a/core/java/android/net/LinkProperties.java b/core/java/android/net/LinkProperties.java index ec773ef4a1e2..d25ee0e69e88 100644 --- a/core/java/android/net/LinkProperties.java +++ b/core/java/android/net/LinkProperties.java @@ -80,7 +80,8 @@ public final class LinkProperties implements Parcelable { private final transient boolean mParcelSensitiveFields; private static final int MIN_MTU = 68; - private static final int MIN_MTU_V6 = 1280; + /* package-visibility - Used in other files (such as Ikev2VpnProfile) as minimum iface MTU. */ + static final int MIN_MTU_V6 = 1280; private static final int MAX_MTU = 10000; private static final int INET6_ADDR_LENGTH = 16; diff --git a/core/java/android/net/PlatformVpnProfile.java b/core/java/android/net/PlatformVpnProfile.java new file mode 100644 index 000000000000..fbae63707be2 --- /dev/null +++ b/core/java/android/net/PlatformVpnProfile.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import static android.net.PlatformVpnProfile.TYPE_IKEV2_IPSEC_PSK; +import static android.net.PlatformVpnProfile.TYPE_IKEV2_IPSEC_RSA; +import static android.net.PlatformVpnProfile.TYPE_IKEV2_IPSEC_USER_PASS; + +import android.annotation.IntDef; +import android.annotation.NonNull; + +import com.android.internal.net.VpnProfile; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.security.GeneralSecurityException; + +/** + * PlatformVpnProfile represents a configuration for a platform-based VPN implementation. + * + * <p>Platform-based VPNs allow VPN applications to provide configuration and authentication options + * to leverage the Android OS' implementations of well-defined control plane (authentication, key + * negotiation) and data plane (per-packet encryption) protocols to simplify the creation of VPN + * tunnels. In contrast, {@link VpnService} based VPNs must implement both the control and data + * planes on a per-app basis. + * + * @see Ikev2VpnProfile + */ +public abstract class PlatformVpnProfile { + /** + * Alias to platform VPN related types from VpnProfile, for API use. + * + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + TYPE_IKEV2_IPSEC_USER_PASS, + TYPE_IKEV2_IPSEC_PSK, + TYPE_IKEV2_IPSEC_RSA, + }) + public static @interface PlatformVpnType {} + + public static final int TYPE_IKEV2_IPSEC_USER_PASS = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS; + public static final int TYPE_IKEV2_IPSEC_PSK = VpnProfile.TYPE_IKEV2_IPSEC_PSK; + public static final int TYPE_IKEV2_IPSEC_RSA = VpnProfile.TYPE_IKEV2_IPSEC_RSA; + + /** @hide */ + @PlatformVpnType protected final int mType; + + /** @hide */ + PlatformVpnProfile(@PlatformVpnType int type) { + mType = type; + } + /** Returns the profile integer type. */ + @PlatformVpnType + public final int getType() { + return mType; + } + + /** Returns a type string describing the VPN profile type */ + @NonNull + public final String getTypeString() { + switch (mType) { + case TYPE_IKEV2_IPSEC_USER_PASS: + return "IKEv2/IPsec Username/Password"; + case TYPE_IKEV2_IPSEC_PSK: + return "IKEv2/IPsec Preshared key"; + case TYPE_IKEV2_IPSEC_RSA: + return "IKEv2/IPsec RSA Digital Signature"; + default: + return "Unknown VPN profile type"; + } + } + + /** @hide */ + @NonNull + public abstract VpnProfile toVpnProfile() throws IOException, GeneralSecurityException; + + /** @hide */ + @NonNull + public static PlatformVpnProfile fromVpnProfile(@NonNull VpnProfile profile) + throws IOException, GeneralSecurityException { + switch (profile.type) { + case TYPE_IKEV2_IPSEC_USER_PASS: // fallthrough + case TYPE_IKEV2_IPSEC_PSK: // fallthrough + case TYPE_IKEV2_IPSEC_RSA: + return Ikev2VpnProfile.fromVpnProfile(profile); + default: + throw new IllegalArgumentException("Unknown VPN Profile type"); + } + } +} diff --git a/core/java/android/net/VpnManager.java b/core/java/android/net/VpnManager.java new file mode 100644 index 000000000000..f95807a14f00 --- /dev/null +++ b/core/java/android/net/VpnManager.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import static com.android.internal.util.Preconditions.checkNotNull; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.Intent; + +/** + * This class provides an interface for apps to manage platform VPN profiles + * + * <p>Apps can use this API to provide profiles with which the platform can set up a VPN without + * further app intermediation. When a VPN profile is present and the app is selected as an always-on + * VPN, the platform will directly trigger the negotiation of the VPN without starting or waking the + * app (unlike VpnService). + * + * <p>VPN apps using supported protocols should preferentially use this API over the {@link + * VpnService} API for ease-of-development and reduced maintainance burden. This also give the user + * the guarantee that VPN network traffic is not subjected to on-device packet interception. + * + * @see Ikev2VpnProfile + */ +public class VpnManager { + @NonNull private final Context mContext; + @NonNull private final IConnectivityManager mService; + + /** + * Create an instance of the VpnManger with the given context. + * + * <p>Internal only. Applications are expected to obtain an instance of the VpnManager via the + * {@link Context.getSystemService()} method call. + * + * @hide + */ + public VpnManager(@NonNull Context ctx, @NonNull IConnectivityManager service) { + mContext = checkNotNull(ctx, "missing Context"); + mService = checkNotNull(service, "missing IConnectivityManager"); + } + + /** + * Install a VpnProfile configuration keyed on the calling app's package name. + * + * @param profile the PlatformVpnProfile provided by this package. Will override any previous + * PlatformVpnProfile stored for this package. + * @return an intent to request user consent if needed (null otherwise). + */ + @Nullable + public Intent provisionVpnProfile(@NonNull PlatformVpnProfile profile) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + /** Delete the VPN profile configuration that was provisioned by the calling app */ + public void deleteProvisionedVpnProfile() { + throw new UnsupportedOperationException("Not yet implemented"); + } + + /** + * Request the startup of a previously provisioned VPN. + * + * @throws SecurityException exception if user or device settings prevent this VPN from being + * setup, or if user consent has not been granted + */ + public void startProvisionedVpnProfile() { + throw new UnsupportedOperationException("Not yet implemented"); + } + + /** Tear down the VPN provided by the calling app (if any) */ + public void stopProvisionedVpnProfile() { + throw new UnsupportedOperationException("Not yet implemented"); + } +} |
