Android VPN编程

来源:互联网 发布:高韶青 离开中国 知乎 编辑:程序博客网 时间:2024/04/30 09:42
  最近公司需要开发android vpn相关方面的功能,但是网上的资料并不多,本人经过大量的搜索后,最终找到了两个可行的方法:
(1)用官方提供的VpnService进行开发,具体怎么样开发可以搜索官方toyVpn例子来看一下。
(2)查看Android源码,利用反射调用隐藏的API开启VPN。
由于项目中需要用PPTP协议进行VPN连接,如果用第一种方法进行开发的话,因为官方没有提供PPTP的实现类,需要自己实现协议,而我在stackOverflow和github上都没有找到可用的代码,所以我只好选择利用第二种方法进行开发。

首先我们先看源码,我的源码版本是4.4.4,在\packages\apps\Settings\src\com\android\settings\vpn2包下找到VPN相关的类,首先我们来看VpnDialog.java。

VpnProfile getProfile() {        // First, save common fields.        VpnProfile profile = new VpnProfile(mProfile.key);        profile.name = mName.getText().toString();        profile.type = mType.getSelectedItemPosition();        profile.server = mServer.getText().toString().trim();        profile.username = mUsername.getText().toString();        profile.password = mPassword.getText().toString();        profile.searchDomains = mSearchDomains.getText().toString().trim();        profile.dnsServers = mDnsServers.getText().toString().trim();        profile.routes = mRoutes.getText().toString().trim();        // Then, save type-specific fields.        switch (profile.type) {            case VpnProfile.TYPE_PPTP:                profile.mppe = mMppe.isChecked();                break;            case VpnProfile.TYPE_L2TP_IPSEC_PSK:                profile.l2tpSecret = mL2tpSecret.getText().toString();                // fall through            case VpnProfile.TYPE_IPSEC_XAUTH_PSK:                profile.ipsecIdentifier = mIpsecIdentifier.getText().toString();                profile.ipsecSecret = mIpsecSecret.getText().toString();                break;            case VpnProfile.TYPE_L2TP_IPSEC_RSA:                profile.l2tpSecret = mL2tpSecret.getText().toString();                // fall through            case VpnProfile.TYPE_IPSEC_XAUTH_RSA:                if (mIpsecUserCert.getSelectedItemPosition() != 0) {                    profile.ipsecUserCert = (String) mIpsecUserCert.getSelectedItem();                }                // fall through            case VpnProfile.TYPE_IPSEC_HYBRID_RSA:                if (mIpsecCaCert.getSelectedItemPosition() != 0) {                    profile.ipsecCaCert = (String) mIpsecCaCert.getSelectedItem();                }                if (mIpsecServerCert.getSelectedItemPosition() != 0) {                    profile.ipsecServerCert = (String) mIpsecServerCert.getSelectedItem();                }                break;        }        profile.saveLogin = mSaveLogin.isChecked();        return profile;    }
以上是一部分VpnDialog的一部分代码,由此可以知道创建VPN连接的相关信息被封装在VpnProfile中。
我们再来看VpnSettings.java。
    @Override    public boolean onOptionsItemSelected(MenuItem item) {        switch (item.getItemId()) {            case R.id.vpn_create: {                // Generate a new key. Here we just use the current time.                // 生成VpnProfile的key                long millis = System.currentTimeMillis();                while (mPreferences.containsKey(Long.toHexString(millis))) {                    ++millis;                }                //弹出VpnDialog                mDialog = new VpnDialog(                        getActivity(), this, new VpnProfile(Long.toHexString(millis)), true);                mDialog.setOnDismissListener(this);                mDialog.show();                return true;            }            case R.id.vpn_lockdown: {                LockdownConfigFragment.show(this);                return true;            }        }        return super.onOptionsItemSelected(item);    }    @Override    public void onResume() {        super.onResume();        final boolean pickLockdown = getActivity()                .getIntent().getBooleanExtra(EXTRA_PICK_LOCKDOWN, false);        if (pickLockdown) {            LockdownConfigFragment.show(this);        }        // Check KeyStore here, so others do not need to deal with it.        // j检查是否设置了锁屏密码        if (!mKeyStore.isUnlocked()) {            if (!mUnlocking) {                // Let us unlock KeyStore. See you later!                Credentials.getInstance().unlock(getActivity());            } else {                // We already tried, but it is still not working!                finishFragment();            }            mUnlocking = !mUnlocking;            return;        }        // Now KeyStore is always unlocked. Reset the flag.        mUnlocking = false;        // Currently we are the only user of profiles in KeyStore.        // Assuming KeyStore and KeyGuard do the right thing, we can        // safely cache profiles in the memory.        if (mPreferences.size() == 0) {            PreferenceGroup group = getPreferenceScreen();            final Context context = getActivity();            final List profiles = loadVpnProfiles(mKeyStore);            for (VpnProfile profile : profiles) {                final VpnPreference pref = new VpnPreference(context, profile);                pref.setOnPreferenceClickListener(this);                mPreferences.put(profile.key, pref);                group.addPreference(pref);            }        }        // Show the dialog if there is one.        if (mDialog != null) {            mDialog.setOnDismissListener(this);            mDialog.show();        }        // Start monitoring.        if (mUpdater == null) {            mUpdater = new Handler(this);        }        mUpdater.sendEmptyMessage(0);        // Register for context menu. Hmmm, getListView() is hidden?        registerForContextMenu(getListView());    }        @Override    public void onClick(DialogInterface dialog, int button) {        if (button == DialogInterface.BUTTON_POSITIVE) {            // Always save the profile.            VpnProfile profile = mDialog.getProfile();            //将VpnProfile对象存储起来,有KeyStore.java可知,KeyStore.UID_SELF = -1,KeyStore.FLAG_ENCRYPTED = 1            mKeyStore.put(Credentials.VPN + profile.key, profile.encode(), KeyStore.UID_SELF,                    KeyStore.FLAG_ENCRYPTED);            // Update the preference.            VpnPreference preference = mPreferences.get(profile.key);            if (preference != null) {                disconnect(profile.key);                preference.update(profile);            } else {                preference = new VpnPreference(getActivity(), profile);                preference.setOnPreferenceClickListener(this);                mPreferences.put(profile.key, preference);                getPreferenceScreen().addPreference(preference);            }            // If we are not editing, connect!            if (!mDialog.isEditing()) {                try {                    connect(profile);                } catch (Exception e) {                    Log.e(TAG, "connect", e);                }            }        }    }    private void connect(VpnProfile profile) throws Exception {        try {            //连接            mService.startLegacyVpn(profile);        } catch (IllegalStateException e) {            Toast.makeText(getActivity(), R.string.vpn_no_network, Toast.LENGTH_LONG).show();        }    }    private void disconnect(String key) {        if (mInfo != null && key.equals(mInfo.key)) {            try {                //断开连接                mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN);            } catch (Exception e) {                // ignore            }        }    }    //获取已经创建的VPN对应的VpnProfile对象    private static List loadVpnProfiles(KeyStore keyStore, int... excludeTypes) {        final ArrayList result = Lists.newArrayList();        final String[] keys = keyStore.saw(Credentials.VPN);        if (keys != null) {            for (String key : keys) {//Credentials.VPN 是“VPN_”,key是VpnProfile.key,是我们随机生成的字符串                final VpnProfile profile = VpnProfile.decode(                        key, keyStore.get(Credentials.VPN + key));                if (profile != null && !ArrayUtils.contains(excludeTypes, profile.type)) {                    result.add(profile);                }            }        }        return result;    }
以上我们基本理清了思路,重要的代码也就那么几行,接下来我们就可以利用反射进行编程了,以下是我的代码:
package com.smartgiant.util;import android.content.Context;import android.net.ConnectivityManager;import android.util.Log;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.net.Inet4Address;import java.net.InetAddress;import java.net.NetworkInterface;import java.net.SocketException;import java.util.Enumeration;public class VpnUtil {    private static Class vpnProfileClz;    private static Class credentialsClz;    private static Class keyStoreClz;    private static Class iConManagerClz;    private static Object iConManagerObj;    /**     * 使用其他方法前先调用该方法     * 初始化vpn相关的类     * @param context     */    public static void init(Context context){        try {            vpnProfileClz = Class.forName("com.android.internal.net.VpnProfile");            keyStoreClz = Class.forName("android.security.KeyStore");            ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);            Field fieldIConManager = null;            fieldIConManager = cm.getClass().getDeclaredField("mService");            fieldIConManager.setAccessible(true);            iConManagerObj = fieldIConManager.get(cm);            iConManagerClz = Class.forName(iConManagerObj.getClass().getName());        } catch (Exception e) {            e.printStackTrace();        }    }    /**     * @param name     vpn连接名,自定义     * @param server   服务器地址     * @param username 用户名     * @param password 用户密码     * @return 返回一个com.android.internal.net.VpnProfile的实例     */    public static Object createVpnProfile(String name, String server, String username, String password) {        Object vpnProfileObj = null;        try {            //生成vpn的key            long millis = System.currentTimeMillis();            String vpnKey = Long.toHexString(millis);            //获得构造函数            Constructor constructor = vpnProfileClz.getConstructor(String.class);            vpnProfileObj = constructor.newInstance(vpnKey);            //设置参数            setParams(vpnProfileObj,name,server,username,password);            //插入vpn数据            insertVpn(vpnProfileObj,vpnKey);        } catch (Exception e) {            e.printStackTrace();        }        return vpnProfileObj;    }    /**     * @param name     vpn连接名,自定义     * @param server   服务器地址     * @param username 用户名     * @param password 用户密码     * @return 返回一个com.android.internal.net.VpnProfile的实例     */    public static Object setParams(Object vpnProfileObj,String name, String server, String username, String password) {        try {            Field field_username = vpnProfileClz.getDeclaredField("username");            Field field_password = vpnProfileClz.getDeclaredField("password");            Field field_server = vpnProfileClz.getDeclaredField("server");            Field field_name = vpnProfileClz.getDeclaredField("name");            //设置参数            field_name.set(vpnProfileObj, name);            field_server.set(vpnProfileObj, server);            field_username.set(vpnProfileObj, username);            field_password.set(vpnProfileObj, password);        } catch (Exception e) {            e.printStackTrace();        }        return vpnProfileObj;    }    /**     * 连接vpn     * @param context     * @param profile com.android.internal.net.VpnProfile的实例     * @return true:连接成功,false:连接失败     */    public static boolean connect(Context context, Object profile) {        boolean isConnected = true;        try {            Method metStartLegacyVpn = iConManagerClz.getDeclaredMethod("startLegacyVpn", vpnProfileClz);            metStartLegacyVpn.setAccessible(true);            //解锁KeyStore            unlock(context);            //开启vpn连接            metStartLegacyVpn.invoke(iConManagerObj, profile);        } catch (Exception e) {            isConnected = false;            e.printStackTrace();        }        return isConnected;    }    /**     * 断开vpn连接     * @param context     * @return true:已断开,false:断开失败     */    public static boolean disconnect(Context context) {        boolean disconnected = true;        try {            Method metPrepare = iConManagerClz.getDeclaredMethod("prepareVpn", String.class, String.class);            //断开连接            metPrepare.invoke(iConManagerObj, "[Legacy VPN]", "[Legacy VPN]");        } catch (Exception e) {            disconnected = false;            e.printStackTrace();        }        return disconnected;    }    /**     * @return 返回一个已存在的vpn实例     */    public static Object getVpnProfile() {        try {            Object keyStoreObj = getKeyStoreInstance();            Method keyStore_saw = keyStoreClz.getMethod("saw",String.class);            keyStore_saw.setAccessible(true);            //查找数据库            String[] keys = (String[]) keyStore_saw.invoke(keyStoreObj,"VPN_");            //如果之前没有创建过vpn,则返回null            if(keys == null || keys.length == 0){                return null;            }            for(String s : keys){                Log.i("key:",s);            }            Method vpnProfile_decode = vpnProfileClz.getDeclaredMethod("decode", String.class, byte[].class);            vpnProfile_decode.setAccessible(true);            Method keyStore_get = keyStoreClz.getDeclaredMethod("get", String.class);            keyStore_get.setAccessible(true);            //获得第一个vpn            Object byteArrayValue = keyStore_get.invoke(keyStoreObj,"VPN_"+keys[0]);            //反序列化返回VpnProfile实例            Object vpnProfileObj = vpnProfile_decode.invoke(null, keys[0], byteArrayValue);            return vpnProfileObj;        } catch (Exception e) {            e.printStackTrace();            return null;        }    }    private static void insertVpn(Object profieObj,String key)throws Exception{        Method keyStore_put = keyStoreClz.getDeclaredMethod("put", String.class, byte[].class, int.class, int.class);        Object keyStoreObj = getKeyStoreInstance();        Class vpnProfileClz = Class.forName("com.android.internal.net.VpnProfile");        Method vpnProfile_encode = vpnProfileClz.getDeclaredMethod("encode");        byte[] bytes = (byte[]) vpnProfile_encode.invoke(profieObj);        keyStore_put.invoke(keyStoreObj,"VPN_"+key,bytes,-1,1);    }    private static Object getKeyStoreInstance() throws Exception {        Method keyStore_getInstance = keyStoreClz.getMethod("getInstance");        keyStore_getInstance.setAccessible(true);        Object keyStoreObj = keyStore_getInstance.invoke(null);        return keyStoreObj;    }    private static void unlock(Context mContext) throws Exception {        credentialsClz = Class.forName("android.security.Credentials");        Method credentials_getInstance = credentialsClz.getDeclaredMethod("getInstance");        Object credentialsObj = credentials_getInstance.invoke(null);        Method credentials_unlock = credentialsClz.getDeclaredMethod("unlock",Context.class);        credentials_unlock.invoke(credentialsObj,mContext);    }}
public void onClick(View view){        VpnUtil.init(MainActivity.this);        if(view.getId() == R.id.bt_connect){            //查询检查是否已经存在VPN            Object vpnProfile = VpnUtil.getVpnProfile();            if(vpnProfile == null){                vpnProfile = VpnUtil.createVpnProfile("vpn1", "192.168.191.1", "vpntest", "123456");            }else{                VpnUtil.setParams(vpnProfile,"vpn1", "192.168.191.1", "vpntest", "123456");            }            //连接             VpnUtil.connect(MainActivity.this,vpnProfile);        }else if(view.getId() == R.id.bt_disconnect){            //断开连接            VpnUtil.disconnect(MainActivity.this);        }    }

这个APP需要改变为系统应用才能使用,不然会在连接过程中出现java.lang.SecurityException: Unauthorized Caller错误,并且需要在AndroidManifest.xml文件下添加 android:sharedUserId="android.uid.system",另外如果没有设置锁屏密码,连接过程会提示设置锁屏密码,但是可以通过注释掉源码里面Vpn.java文件里的以下三行代码去掉锁屏。
   if (!keyStore.isUnlocked()) {
            throw new IllegalStateException("KeyStore isn't unlocked");
        }
以上是我对于android系统下VPN编程的一些理解,希望可以帮到大家,如果有问题,我们可以在评论区交流。



原创粉丝点击