1/*
2 * Copyright (C) 2013 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
17package com.android.settings.vpn2;
18
19import android.content.Context;
20import android.net.IConnectivityManager;
21import android.os.Bundle;
22import android.os.Environment;
23import android.os.RemoteException;
24import android.os.ServiceManager;
25import android.os.UserHandle;
26import android.security.Credentials;
27import android.security.KeyStore;
28import android.security.NetworkSecurityPolicy;
29import android.test.InstrumentationTestCase;
30import android.test.InstrumentationTestRunner;
31import android.test.suitebuilder.annotation.LargeTest;
32import android.util.Log;
33
34import com.android.internal.net.LegacyVpnInfo;
35import com.android.internal.net.VpnConfig;
36import com.android.internal.net.VpnProfile;
37
38import java.net.HttpURLConnection;
39import java.net.URL;
40import junit.framework.Assert;
41
42import libcore.io.Streams;
43import org.json.JSONException;
44import org.json.JSONObject;
45
46import java.io.File;
47import java.io.FileInputStream;
48import java.io.IOException;
49import java.io.InputStream;
50import java.net.UnknownHostException;
51import java.nio.charset.StandardCharsets;
52import java.util.List;
53import java.util.Map;
54
55/**
56 * Legacy VPN connection tests
57 *
58 * To run the test, use command:
59 * adb shell am instrument -e class com.android.settings.vpn2.VpnTests -e profile foo.xml
60 * -w com.android.settings.tests/android.test.InstrumentationTestRunner
61 *
62 * VPN profiles are saved in an xml file and will be loaded through {@link VpnProfileParser}.
63 * Push the profile (foo.xml) to the external storage, e.g adb push foo.xml /sdcard/ before running
64 * the above command.
65 *
66 * A typical profile looks like the following:
67 * <vpn>
68 *   <name></name>
69 *   <type></type>
70 *   <server></server>
71 *   <username></username>
72 *   <password></password>
73 *   <dnsServers></dnsServers>
74 *   <searchDomains></searchDomains>
75 *   <routes></routes>
76 *   <l2tpSecret></l2tpSecret>
77 *   <ipsecIdentifier></ipsecIdentifier>
78 *   <ipsecSecret></ipsecSecret>
79 *   <ipsecUserCert></ipsecUserCert>
80 *   <ipsecCaCert></ipsecCaCert>
81 *   <ipsecServerCert></ipsecServerCert>
82 * </vpn>
83 * VPN types include: TYPE_PPTP, TYPE_L2TP_IPSEC_PSK, TYPE_L2TP_IPSEC_RSA,
84 * TYPE_IPSEC_XAUTH_PSK, TYPE_IPSEC_XAUTH_RSA, TYPE_IPSEC_HYBRID_RSA
85 */
86public class VpnTests extends InstrumentationTestCase {
87    private static final String TAG = "VpnTests";
88    /* Maximum time to wait for VPN connection */
89    private static final long MAX_CONNECTION_TIME = 5 * 60 * 1000;
90    private static final long VPN_STAY_TIME = 60 * 1000;
91    private static final int MAX_DISCONNECTION_TRIES = 3;
92    private static final String EXTERNAL_SERVER =
93            "http://ip2country.sourceforge.net/ip2c.php?format=JSON";
94    private static final String VPN_INTERFACE = "ppp0";
95    private final IConnectivityManager mService = IConnectivityManager.Stub
96        .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
97    private Map<Integer, VpnInfo> mVpnInfoPool = null;
98    private Context mContext;
99    private CertInstallerHelper mCertHelper = null;
100    private KeyStore mKeyStore = KeyStore.getInstance();
101    private String mPreviousIpAddress = null;
102    private boolean DEBUG = false;
103
104    @Override
105    protected void setUp() throws Exception {
106        super.setUp();
107        InputStream in = null;
108        InstrumentationTestRunner mRunner = (InstrumentationTestRunner)getInstrumentation();
109        mContext = mRunner.getContext();
110        Bundle arguments = mRunner.getArguments();
111        String PROFILE_NAME = arguments.getString("profile");
112        Assert.assertNotNull("Push profile to external storage and load with"
113                + "'-e profile <filename>'", PROFILE_NAME);
114        File profileFile = new File(Environment.getExternalStorageDirectory(), PROFILE_NAME);
115        in = new FileInputStream(profileFile);
116        mVpnInfoPool = VpnProfileParser.parse(in);
117        Assert.assertNotNull("no VPN profiles are parsed", mVpnInfoPool);
118        if (DEBUG) {
119            Log.v(TAG, "print out the vpn profiles");
120            for (Map.Entry<Integer, VpnInfo> profileEntrySet: mVpnInfoPool.entrySet()) {
121                VpnInfo vpnInfo = profileEntrySet.getValue();
122                printVpnProfile(vpnInfo.getVpnProfile());
123                if (vpnInfo.getCertificateFile() != null) {
124                    Log.d(TAG, "certificate file for this vpn is " + vpnInfo.getCertificateFile());
125                }
126                if (vpnInfo.getPassword() != null) {
127                    Log.d(TAG, "password for the certificate file is: " + vpnInfo.getPassword());
128                }
129            }
130        }
131        // disconnect existing vpn if there is any
132        LegacyVpnInfo oldVpn = mService.getLegacyVpnInfo(UserHandle.myUserId());
133        if (oldVpn != null) {
134            Log.v(TAG, "disconnect legacy VPN");
135            disconnect();
136            // wait till the legacy VPN is disconnected.
137            int tries = 0;
138            while (tries < MAX_DISCONNECTION_TRIES &&
139                    mService.getLegacyVpnInfo(UserHandle.myUserId()) != null) {
140                tries++;
141                Thread.sleep(10 * 1000);
142                Log.v(TAG, "Wait for legacy VPN to be disconnected.");
143            }
144            Assert.assertNull("Failed to disconect VPN",
145                    mService.getLegacyVpnInfo(UserHandle.myUserId()));
146            // wait for 30 seconds after the previous VPN is disconnected.
147            sleep(30 * 1000);
148        }
149        // Create CertInstallerHelper to initialize the keystore
150        mCertHelper = new CertInstallerHelper();
151    }
152
153    @Override
154    protected void tearDown() throws Exception {
155        sleep(VPN_STAY_TIME);
156        super.tearDown();
157    }
158
159    private void printVpnProfile(VpnProfile profile) {
160        Log.v(TAG, "profile: ");
161        Log.v(TAG, "key: " + profile.key);
162        Log.v(TAG, "name: " + profile.name);
163        Log.v(TAG, "type: " + profile.type);
164        Log.v(TAG, "server: " + profile.server);
165        Log.v(TAG, "username: " + profile.username);
166        Log.v(TAG, "password: " + profile.password);
167        Log.v(TAG, "dnsServers: " + profile.dnsServers);
168        Log.v(TAG, "searchDomains: " + profile.searchDomains);
169        Log.v(TAG, "routes: " + profile.routes);
170        Log.v(TAG, "mppe: " + profile.mppe);
171        Log.v(TAG, "l2tpSecret: " + profile.l2tpSecret);
172        Log.v(TAG, "ipsecIdentifier: " + profile.ipsecIdentifier);
173        Log.v(TAG, "ipsecSecret: " + profile.ipsecSecret);
174        Log.v(TAG, "ipsecUserCert: " + profile.ipsecUserCert);
175        Log.v(TAG, "ipsecCaCert: " + profile.ipsecCaCert);
176        Log.v(TAG, "ipsecServerCert: " + profile.ipsecServerCert);
177    }
178
179    private void printKeyStore(VpnProfile profile) {
180        // print out the information from keystore
181        String privateKey = "";
182        String userCert = "";
183        String caCert = "";
184        String serverCert = "";
185        if (!profile.ipsecUserCert.isEmpty()) {
186            privateKey = Credentials.USER_PRIVATE_KEY + profile.ipsecUserCert;
187            byte[] value = mKeyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecUserCert);
188            userCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8);
189        }
190        if (!profile.ipsecCaCert.isEmpty()) {
191            byte[] value = mKeyStore.get(Credentials.CA_CERTIFICATE + profile.ipsecCaCert);
192            caCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8);
193        }
194        if (!profile.ipsecServerCert.isEmpty()) {
195            byte[] value = mKeyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecServerCert);
196            serverCert = (value == null) ? null : new String(value, StandardCharsets.UTF_8);
197        }
198        Log.v(TAG, "privateKey: \n" + ((privateKey == null) ? "" : privateKey));
199        Log.v(TAG, "userCert: \n" + ((userCert == null) ? "" : userCert));
200        Log.v(TAG, "caCert: \n" + ((caCert == null) ? "" : caCert));
201        Log.v(TAG, "serverCert: \n" + ((serverCert == null) ? "" : serverCert));
202    }
203
204    /**
205     * Connect legacy VPN
206     */
207    private void connect(VpnProfile profile) throws Exception {
208        try {
209            mService.startLegacyVpn(profile);
210        } catch (IllegalStateException e) {
211            fail(String.format("start legacy vpn: %s failed: %s", profile.name, e.toString()));
212        }
213    }
214
215    /**
216     * Disconnect legacy VPN
217     */
218    private void disconnect() throws Exception {
219        try {
220            mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN, UserHandle.myUserId());
221        } catch (RemoteException e) {
222            Log.e(TAG, String.format("disconnect VPN exception: %s", e.toString()));
223        }
224    }
225
226    /**
227     * Get external IP address
228     */
229    private String getIpAddress() {
230        String ip = null;
231        HttpURLConnection urlConnection = null;
232        // TODO: Rewrite this test to use an HTTPS URL.
233        // Because this test uses cleartext HTTP, the network security policy of this app needs to
234        // be temporarily relaxed to permit such traffic.
235        NetworkSecurityPolicy networkSecurityPolicy = NetworkSecurityPolicy.getInstance();
236        boolean cleartextTrafficPermittedBeforeTest =
237                networkSecurityPolicy.isCleartextTrafficPermitted();
238        networkSecurityPolicy.setCleartextTrafficPermitted(true);
239        try {
240            URL url = new URL(EXTERNAL_SERVER);
241            urlConnection = (HttpURLConnection) url.openConnection();
242            Log.i(TAG, "Response from httpget: " + urlConnection.getResponseCode());
243
244            InputStream is = urlConnection.getInputStream();
245            String response;
246            try {
247                response = new String(Streams.readFully(is), StandardCharsets.UTF_8);
248            } finally {
249                is.close();
250            }
251
252            JSONObject json_data = new JSONObject(response);
253            ip = json_data.getString("ip");
254            Log.v(TAG, "json_data: " + ip);
255        } catch (IllegalArgumentException e) {
256            Log.e(TAG, "exception while getting external IP: " + e.toString());
257        } catch (IOException e) {
258            Log.e(TAG, "IOException while getting IP: " + e.toString());
259        } catch (JSONException e) {
260            Log.e(TAG, "exception while creating JSONObject: " + e.toString());
261        } finally {
262            networkSecurityPolicy.setCleartextTrafficPermitted(cleartextTrafficPermittedBeforeTest);
263            if (urlConnection != null) {
264                urlConnection.disconnect();
265            }
266        }
267        return ip;
268    }
269
270    /**
271     * Verify the vpn connection by checking the VPN state and external IP
272     */
273    private void validateVpnConnection(VpnProfile profile) throws Exception {
274        validateVpnConnection(profile, false);
275    }
276
277    /**
278     * Verify the vpn connection by checking the VPN state, external IP or ping test
279     */
280    private void validateVpnConnection(VpnProfile profile, boolean pingTestFlag) throws Exception {
281        LegacyVpnInfo legacyVpnInfo = mService.getLegacyVpnInfo(UserHandle.myUserId());
282        Assert.assertTrue(legacyVpnInfo != null);
283
284        long start = System.currentTimeMillis();
285        while (((System.currentTimeMillis() - start)  < MAX_CONNECTION_TIME) &&
286                (legacyVpnInfo.state != LegacyVpnInfo.STATE_CONNECTED)) {
287            Log.v(TAG, "vpn state: " + legacyVpnInfo.state);
288            sleep(10 * 1000);
289            legacyVpnInfo = mService.getLegacyVpnInfo(UserHandle.myUserId());
290        }
291
292        // the vpn state should be CONNECTED
293        Assert.assertTrue(legacyVpnInfo.state == LegacyVpnInfo.STATE_CONNECTED);
294        if (pingTestFlag) {
295            Assert.assertTrue(pingTest(profile.server));
296        } else {
297            String curIpAddress = getIpAddress();
298            // the outgoing IP address should be the same as the VPN server address
299            Assert.assertEquals(profile.server, curIpAddress);
300        }
301    }
302
303    private boolean pingTest(String server) {
304        final long PING_TIMER = 3 * 60 * 1000; // 3 minutes
305        if (server == null || server.isEmpty()) {
306            return false;
307        }
308        long startTime = System.currentTimeMillis();
309        while ((System.currentTimeMillis() - startTime) < PING_TIMER) {
310            try {
311                Log.v(TAG, "Start ping test, ping " + server);
312                Process p = Runtime.getRuntime().exec("ping -c 10 -w 100 " + server);
313                int status = p.waitFor();
314                if (status == 0) {
315                    // if any of the ping test is successful, return true
316                    return true;
317                }
318            } catch (UnknownHostException e) {
319                Log.e(TAG, "Ping test Fail: Unknown Host");
320            } catch (IOException e) {
321                Log.e(TAG, "Ping test Fail:  IOException");
322            } catch (InterruptedException e) {
323                Log.e(TAG, "Ping test Fail: InterruptedException");
324            }
325        }
326        // ping test timeout
327        return false;
328    }
329
330    /**
331     * Install certificates from a file loaded in external stroage on the device
332     * @param profile vpn profile
333     * @param fileName certificate file name
334     * @param password password to extract certificate file
335     */
336    private void installCertificatesFromFile(VpnProfile profile, String fileName, String password)
337            throws Exception {
338        if (profile == null || fileName == null || password == null) {
339            throw new Exception ("vpn profile, certificate file name and password can not be null");
340        }
341
342        int curUid = mContext.getUserId();
343        mCertHelper.installCertificate(profile, fileName, password);
344
345        if (DEBUG) {
346            printKeyStore(profile);
347        }
348    }
349
350    private void sleep(long time) {
351        try {
352            Thread.sleep(time);
353        } catch (InterruptedException e) {
354            Log.e(TAG, "interrupted: " + e.toString());
355        }
356    }
357
358    /**
359     * Test PPTP VPN connection
360     */
361    @LargeTest
362    public void testPPTPConnection() throws Exception {
363        mPreviousIpAddress = getIpAddress();
364        VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_PPTP);
365        VpnProfile vpnProfile = curVpnInfo.getVpnProfile();
366        connect(vpnProfile);
367        validateVpnConnection(vpnProfile);
368    }
369
370    /**
371     * Test L2TP/IPSec PSK VPN connection
372     */
373    @LargeTest
374    public void testL2tpIpsecPskConnection() throws Exception {
375        mPreviousIpAddress = getIpAddress();
376        VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_L2TP_IPSEC_PSK);
377        VpnProfile vpnProfile = curVpnInfo.getVpnProfile();
378        connect(vpnProfile);
379        validateVpnConnection(vpnProfile);
380    }
381
382    /**
383     * Test L2TP/IPSec RSA VPN connection
384     */
385    @LargeTest
386    public void testL2tpIpsecRsaConnection() throws Exception {
387        mPreviousIpAddress = getIpAddress();
388        VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_L2TP_IPSEC_RSA);
389        VpnProfile vpnProfile = curVpnInfo.getVpnProfile();
390        if (DEBUG) {
391            printVpnProfile(vpnProfile);
392        }
393        String certFile = curVpnInfo.getCertificateFile();
394        String password = curVpnInfo.getPassword();
395        installCertificatesFromFile(vpnProfile, certFile, password);
396        connect(vpnProfile);
397        validateVpnConnection(vpnProfile);
398    }
399
400    /**
401     * Test IPSec Xauth RSA VPN connection
402     */
403    @LargeTest
404    public void testIpsecXauthRsaConnection() throws Exception {
405        mPreviousIpAddress = getIpAddress();
406        VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_IPSEC_XAUTH_RSA);
407        VpnProfile vpnProfile = curVpnInfo.getVpnProfile();
408        if (DEBUG) {
409            printVpnProfile(vpnProfile);
410        }
411        String certFile = curVpnInfo.getCertificateFile();
412        String password = curVpnInfo.getPassword();
413        installCertificatesFromFile(vpnProfile, certFile, password);
414        connect(vpnProfile);
415        validateVpnConnection(vpnProfile);
416    }
417
418    /**
419     * Test IPSec Xauth PSK VPN connection
420     */
421    @LargeTest
422    public void testIpsecXauthPskConnection() throws Exception {
423        VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_IPSEC_XAUTH_PSK);
424        VpnProfile vpnProfile = curVpnInfo.getVpnProfile();
425        if (DEBUG) {
426            printVpnProfile(vpnProfile);
427        }
428        connect(vpnProfile);
429        validateVpnConnection(vpnProfile, true);
430    }
431
432    /**
433     * Test IPSec Hybrid RSA VPN connection
434     */
435    @LargeTest
436    public void testIpsecHybridRsaConnection() throws Exception {
437        mPreviousIpAddress = getIpAddress();
438        VpnInfo curVpnInfo = mVpnInfoPool.get(VpnProfile.TYPE_IPSEC_HYBRID_RSA);
439        VpnProfile vpnProfile = curVpnInfo.getVpnProfile();
440        if (DEBUG) {
441            printVpnProfile(vpnProfile);
442        }
443        connect(vpnProfile);
444        validateVpnConnection(vpnProfile);
445    }
446}
447