/* * Copyright (C) 2013 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.settings.vpn2; import android.content.Context; import android.net.IConnectivityManager; import android.os.Bundle; import android.os.Environment; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.security.Credentials; import android.security.KeyStore; import android.security.NetworkSecurityPolicy; import android.test.InstrumentationTestCase; import android.test.InstrumentationTestRunner; import android.test.suitebuilder.annotation.LargeTest; import android.util.Log; import com.android.internal.net.LegacyVpnInfo; import com.android.internal.net.VpnConfig; import com.android.internal.net.VpnProfile; import java.net.HttpURLConnection; import java.net.URL; import junit.framework.Assert; import libcore.io.Streams; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; /** * Legacy VPN connection tests * * To run the test, use command: * adb shell am instrument -e class com.android.settings.vpn2.VpnTests -e profile foo.xml * -w com.android.settings.tests/android.test.InstrumentationTestRunner * * VPN profiles are saved in an xml file and will be loaded through {@link VpnProfileParser}. * Push the profile (foo.xml) to the external storage, e.g adb push foo.xml /sdcard/ before running * the above command. * * A typical profile looks like the following: * <vpn> * <name></name> * <type></type> * <server></server> * <username></username> * <password></password> * <dnsServers></dnsServers> * <searchDomains></searchDomains> * <routes></routes> * <l2tpSecret></l2tpSecret> * <ipsecIdentifier></ipsecIdentifier> * <ipsecSecret></ipsecSecret> * <ipsecUserCert></ipsecUserCert> * <ipsecCaCert></ipsecCaCert> * <ipsecServerCert></ipsecServerCert> * </vpn> * VPN types include: TYPE_PPTP, TYPE_L2TP_IPSEC_PSK, TYPE_L2TP_IPSEC_RSA, * TYPE_IPSEC_XAUTH_PSK, TYPE_IPSEC_XAUTH_RSA, TYPE_IPSEC_HYBRID_RSA */ public class VpnTests extends InstrumentationTestCase { private static final String TAG = "VpnTests"; /* Maximum time to wait for VPN connection */ private static final long MAX_CONNECTION_TIME = 5 * 60 * 1000; private static final long VPN_STAY_TIME = 60 * 1000; private static final int MAX_DISCONNECTION_TRIES = 3; private static final String EXTERNAL_SERVER = "http://ip2country.sourceforge.net/ip2c.php?format=JSON"; private static final String VPN_INTERFACE = "ppp0"; private final IConnectivityManager mService = IConnectivityManager.Stub .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); private Map<Integer, VpnInfo> mVpnInfoPool = null; private Context mContext; private CertInstallerHelper mCertHelper = null; private KeyStore mKeyStore = KeyStore.getInstance(); private String mPreviousIpAddress = null; private boolean DEBUG = false; @Override protected void setUp() throws Exception { super.setUp(); InputStream in = null; InstrumentationTestRunner mRunner = (InstrumentationTestRunner)getInstrumentation(); mContext = mRunner.getContext(); Bundle arguments = mRunner.getArguments(); String PROFILE_NAME = arguments.getString("profile"); Assert.assertNotNull("Push profile to external storage and load with" + "'-e profile <filename>'", PROFILE_NAME); File profileFile = new File(Environment.getExternalStorageDirectory(), PROFILE_NAME); in = new FileInputStream(profileFile); mVpnInfoPool = VpnProfileParser.parse(in); Assert.assertNotNull("no VPN profiles are parsed", mVpnInfoPool); if (DEBUG) { Log.v(TAG, "print out the vpn profiles"); for (Map.Entry<Integer, VpnInfo> profileEntrySet: mVpnInfoPool.entrySet()) { VpnInfo vpnInfo = profileEntrySet.getValue(); printVpnProfile(vpnInfo.getVpnProfile()); if (vpnInfo.getCertificateFile() != null) { Log.d(TAG, "certificate file for this vpn is " + vpnInfo.getCertificateFile()); } if (vpnInfo.getPassword() != null) { Log.d(TAG, "password for the certificate file is: " + vpnInfo.getPassword()); } } } // disconnect existing vpn if there is any LegacyVpnInfo oldVpn = mService.getLegacyVpnInfo(UserHandle.myUserId()); if (oldVpn != null) { Log.v(TAG, "disconnect legacy VPN"); disconnect(); // wait till the legacy VPN is disconnected. int tries = 0; while (tries < MAX_DISCONNECTION_TRIES && mService.getLegacyVpnInfo(UserHandle.myUserId()) != null) { tries++; Thread.sleep(10 * 1000); Log.v(TAG, "Wait for legacy VPN to be disconnected."); } Assert.assertNull("Failed to disconect VPN", mService.getLegacyVpnInfo(UserHandle.myUserId())); // wait for 30 seconds after the previous VPN is disconnected. sleep(30 * 1000); } // Create CertInstallerHelper to initialize the keystore mCertHelper = new CertInstallerHelper(); } @Override protected void tearDown() throws Exception { sleep(VPN_STAY_TIME); super.tearDown(); } private void printVpnProfile(VpnProfile profile) { Log.v(TAG, "profile: "); Log.v(TAG, "key: " + profile.key); Log.v(TAG, "name: " + profile.name); Log.v(TAG, "type: " + profile.type); Log.v(TAG, "server: " + profile.server); Log.v(TAG, "username: " + profile.username); Log.v(TAG, "password: " + profile.password); Log.v(TAG, "dnsServers: " + profile.dnsServers); Log.v(TAG, "searchDomains: " + profile.searchDomains); Log.v(TAG, "routes: " + profile.routes); Log.v(TAG, "mppe: " + profile.mppe); Log.v(TAG, "l2tpSecret: " + profile.l2tpSecret); Log.v(TAG, "ipsecIdentifier: " + profile.ipsecIdentifier); Log.v(TAG, "ipsecSecret: " + profile.ipsecSecret); Log.v(TAG, "ipsecUserCert: " + profile.ipsecUserCert); Log.v(TAG, "ipsecCaCert: " + profile.ipsecCaCert); Log.v(TAG, "ipsecServerCert: " + profile.ipsecServerCert); } private void printKeyStore(VpnProfile profile) { // print out the information from keystore String privateKey = ""; String userCert = ""; String caCert = ""; String serverCert = ""; if (!profile.ipsecUserCert.isEmpty()) { privateKey = Credentials.USER_PRIVATE_KEY + profile.ipsecUserCert; byte[] value = mKeyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecUserCert); userCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8); } if (!profile.ipsecCaCert.isEmpty()) { byte[] value = mKeyStore.get(Credentials.CA_CERTIFICATE + profile.ipsecCaCert); caCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8); } if (!profile.ipsecServerCert.isEmpty()) { byte[] value = mKeyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecServerCert); serverCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8); } Log.v(TAG, "privateKey: \n" + ((privateKey == null) ? "" : privateKey)); Log.v(TAG, "userCert: \n" + ((userCert == null) ? "" : userCert)); Log.v(TAG, "caCert: \n" + ((caCert == null) ? "" : caCert)); Log.v(TAG, "serverCert: \n" + ((serverCert == null) ? "" : serverCert)); } /** * Connect legacy VPN */ private void connect(VpnProfile profile) throws Exception { try { mService.startLegacyVpn(profile); } catch (IllegalStateException e) { fail(String.format("start legacy vpn: %s failed: %s", profile.name, e.toString())); } } /** * Disconnect legacy VPN */ private void disconnect() throws Exception { try { mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN, UserHandle.myUserId()); } catch (RemoteException e) { Log.e(TAG, String.format("disconnect VPN exception: %s", e.toString())); } } /** * Get external IP address */ private String getIpAddress() { String ip = null; HttpURLConnection urlConnection = null; // TODO: Rewrite this test to use an HTTPS URL. // Because this test uses cleartext HTTP, the network security policy of this app needs to // be temporarily relaxed to permit such traffic. NetworkSecurityPolicy networkSecurityPolicy = NetworkSecurityPolicy.getInstance(); boolean cleartextTrafficPermittedBeforeTest = networkSecurityPolicy.isCleartextTrafficPermitted(); networkSecurityPolicy.setCleartextTrafficPermitted(true); try { URL url = new URL(EXTERNAL_SERVER); urlConnection = (HttpURLConnection) url.openConnection(); Log.i(TAG, "Response from httpget: " + urlConnection.getResponseCode()); InputStream is = urlConnection.getInputStream(); String response; try { response = new String(Streams.readFully(is), StandardCharsets.UTF_8); } finally { is.close(); } JSONObject json_data = new JSONObject(response); ip = json_data.getString("ip"); Log.v(TAG, "json_data: " + ip); } catch (IllegalArgumentException e) { Log.e(TAG, "exception while getting external IP: " + e.toString()); } catch (IOException e) { Log.e(TAG, "IOException while getting IP: " + e.toString()); } catch (JSONException e) { Log.e(TAG, "exception while creating JSONObject: " + e.toString()); } finally { networkSecurityPolicy.setCleartextTrafficPermitted(cleartextTrafficPermittedBeforeTest); if (urlConnection != null) { urlConnection.disconnect(); } } return ip; } /** * Verify the vpn connection by checking the VPN state and external IP */ private void validateVpnConnection(VpnProfile profile) throws Exception { validateVpnConnection(profile, false); } /** * Verify the vpn connection by checking the VPN state, external IP or ping test */ private void validateVpnConnection(VpnProfile profile, boolean pingTestFlag) throws Exception { LegacyVpnInfo legacyVpnInfo = mService.getLegacyVpnInfo(UserHandle.myUserId()); Assert.assertTrue(legacyVpnInfo != null); long start = System.currentTimeMillis(); while (((System.currentTimeMillis() - start) < MAX_CONNECTION_TIME) && (legacyVpnInfo.state != LegacyVpnInfo.STATE_CONNECTED)) { Log.v(TAG, "vpn state: " + legacyVpnInfo.state); sleep(10 * 1000); legacyVpnInfo = mService.getLegacyVpnInfo(UserHandle.myUserId()); } // the vpn state should be CONNECTED Assert.assertTrue(legacyVpnInfo.state == LegacyVpnInfo.STATE_CONNECTED); if (pingTestFlag) { Assert.assertTrue(pingTest(profile.server)); } else { String curIpAddress = getIpAddress(); // the outgoing IP address should be the same as the VPN server address Assert.assertEquals(profile.server, curIpAddress); } } private boolean pingTest(String server) { final long PING_TIMER = 3 * 60 * 1000; // 3 minutes if (server == null || server.isEmpty()) { return false; } long startTime = System.currentTimeMillis(); while ((System.currentTimeMillis() - startTime) < PING_TIMER) { try { Log.v(TAG, "Start ping test, ping " + server); Process p = Runtime.getRuntime().exec("ping -c 10 -w 100 " + server); int status = p.waitFor(); if (status == 0) { // if any of the ping test is successful, return true return true; } } catch (UnknownHostException e) { Log.e(TAG, "Ping test Fail: Unknown Host"); } catch (IOException e) { Log.e(TAG, "Ping test Fail: IOException"); } catch (InterruptedException e) { Log.e(TAG, "Ping test Fail: InterruptedException"); } } // ping test timeout return false; } /** * Install certificates from a file loaded in external stroage on the device * @param profile vpn profile * @param fileName certificate file name * @param password password to extract certificate file */ private void installCertificatesFromFile(VpnProfile profile, String fileName, String password) throws Exception { if (profile == null || fileName == null || password == null) { throw new Exception ("vpn profile, certificate file name and password can not be null"); } int curUid = mContext.getUserId(); mCertHelper.installCertificate(profile, fileName, password); if (DEBUG) { printKeyStore(profile); } } private void sleep(long time) { try { Thread.sleep(time); } catch (InterruptedException e) { Log.e(TAG, "interrupted: " + e.toString()); } } /** * Test PPTP VPN connection */ @LargeTest public void testPPTPConnection() throws Exception { mPreviousIpAddress = getIpAddress(); VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_PPTP); VpnProfile vpnProfile = curVpnInfo.getVpnProfile(); connect(vpnProfile); validateVpnConnection(vpnProfile); } /** * Test L2TP/IPSec PSK VPN connection */ @LargeTest public void testL2tpIpsecPskConnection() throws Exception { mPreviousIpAddress = getIpAddress(); VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_L2TP_IPSEC_PSK); VpnProfile vpnProfile = curVpnInfo.getVpnProfile(); connect(vpnProfile); validateVpnConnection(vpnProfile); } /** * Test L2TP/IPSec RSA VPN connection */ @LargeTest public void testL2tpIpsecRsaConnection() throws Exception { mPreviousIpAddress = getIpAddress(); VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_L2TP_IPSEC_RSA); VpnProfile vpnProfile = curVpnInfo.getVpnProfile(); if (DEBUG) { printVpnProfile(vpnProfile); } String certFile = curVpnInfo.getCertificateFile(); String password = curVpnInfo.getPassword(); installCertificatesFromFile(vpnProfile, certFile, password); connect(vpnProfile); validateVpnConnection(vpnProfile); } /** * Test IPSec Xauth RSA VPN connection */ @LargeTest public void testIpsecXauthRsaConnection() throws Exception { mPreviousIpAddress = getIpAddress(); VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_IPSEC_XAUTH_RSA); VpnProfile vpnProfile = curVpnInfo.getVpnProfile(); if (DEBUG) { printVpnProfile(vpnProfile); } String certFile = curVpnInfo.getCertificateFile(); String password = curVpnInfo.getPassword(); installCertificatesFromFile(vpnProfile, certFile, password); connect(vpnProfile); validateVpnConnection(vpnProfile); } /** * Test IPSec Xauth PSK VPN connection */ @LargeTest public void testIpsecXauthPskConnection() throws Exception { VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_IPSEC_XAUTH_PSK); VpnProfile vpnProfile = curVpnInfo.getVpnProfile(); if (DEBUG) { printVpnProfile(vpnProfile); } connect(vpnProfile); validateVpnConnection(vpnProfile, true); } /** * Test IPSec Hybrid RSA VPN connection */ @LargeTest public void testIpsecHybridRsaConnection() throws Exception { mPreviousIpAddress = getIpAddress(); VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_IPSEC_HYBRID_RSA); VpnProfile vpnProfile = curVpnInfo.getVpnProfile(); if (DEBUG) { printVpnProfile(vpnProfile); } connect(vpnProfile); validateVpnConnection(vpnProfile); } }