V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
mightofcode
V2EX  ›  Android

Android vpnservice 流量代理示例

  •  
  •   mightofcode · 2020-02-14 15:47:31 +08:00 · 13460 次点击
    这是一个创建于 1742 天前的主题,其中的信息可能已经有所发展或是发生改变。

    利用 Android 提供的 vpnservice 机制,可以拦截所有 ip 流量,接下来可以做任何你想做的事情

    本文利用 vpnservice 实现了一个流量代理,能够转发 udp、tcp 协议,目前只是简单的转发到目标服务器,不做任何加工

    项目地址:https://github.com/mightofcode/android-vpnservice

    1,在 AndroidManifest.xml 中注册权限

        <uses-permission android:name="android.permission.INTERNET" />
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
        <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
        <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
        <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
        <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
        <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
        <uses-permission android:name="android.permission.INTERNET" />
    

    2,创建一个类继承 VpnService

    public class LocalVPNService extends VpnService 
    

    3,启动 vpnservice 执行意图

        private void startVpn() {
    
            Intent vpnIntent = VpnService.prepare(this);
    
            if (vpnIntent != null)
                startActivityForResult(vpnIntent, VPN_REQUEST_CODE);
            else
                onActivityResult(VPN_REQUEST_CODE, RESULT_OK, null);
        }
    

    启动 vpnservice

    private void setupVPN() {
            try {
                if (vpnInterface == null) {
                    Builder builder = new Builder();
                    builder.addAddress(VPN_ADDRESS, 32);
                    builder.addRoute(VPN_ROUTE, 0);
                    builder.addDnsServer(Config.dns);
                    if (Config.testLocal) {
                        builder.addAllowedApplication("com.mocyx.basic_client");
                    }
                    vpnInterface = builder.setSession(getString(R.string.app_name)).setConfigureIntent(pendingIntent).establish();
                }
            } catch (Exception e) {
                Log.e(TAG, "error", e);
                System.exit(0);
            }
        }
    

    4, 读写 vpnservice 文件描述符实现流量转发 创建两个 FileChannel

    FileChannel vpnInput = new FileInputStream(vpnFileDescriptor).getChannel();
     FileChannel vpnOutput = new FileOutputStream(vpnFileDescriptor).getChannel();
    

    5, 从 input 中读取 ip 包并解析 如果是 tcp、udp,进行转发 其他协议( ICMP、IGMP )直接丢弃,丢弃 ICMP、IGMP 会有点问题,不过大部分 app 还是能正常使用的

                    while (!Thread.interrupted()) {
                        bufferToNetwork = ByteBufferPool.acquire();
                        int readBytes = vpnInput.read(bufferToNetwork);
    
                        MainActivity.upByte.addAndGet(readBytes);
    
                        if (readBytes > 0) {
                            bufferToNetwork.flip();
    
                            Packet packet = new Packet(bufferToNetwork);
                            if (packet.isUDP()) {
                                if (Config.logRW) {
                                    Log.i(TAG, "read udp" + readBytes);
                                }
                                deviceToNetworkUDPQueue.offer(packet);
                            } else if (packet.isTCP()) {
                                if (Config.logRW) {
                                    Log.i(TAG, "read tcp " + readBytes);
                                }
                                deviceToNetworkTCPQueue.offer(packet);
                            } else {
                                Log.w(TAG, String.format("Unknown packet protocol type %d", packet.ip4Header.protocolNum));
                            }
                        } else {
                            try {
                                Thread.sleep(10);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
    

    tcp 包的处理比较麻烦,需要处理握手、挥手等协议细节,需要正确处理 seq 号,等等,实现一个正确的 tcp 协议确实不容易

    udp 只需要进行简单的转发

    6,从输出队列中获取数据写入 output

                            ByteBuffer bufferFromNetwork = networkToDeviceQueue.take();
                            bufferFromNetwork.flip();
    
                            while (bufferFromNetwork.hasRemaining()) {
                                int w = vpnOutput.write(bufferFromNetwork);
                                if (w > 0) {
                                    MainActivity.downByte.addAndGet(w);
                                }
    
                                if (Config.logRW) {
                                    Log.d(TAG, "vpn write " + w);
                                }
                            }
    

    7,app 使用: 点击 start 打开 vpn
    点击 dns 进行一次 dns 请求
    点击 http 进行一次 http 请求
    最下方的文本框展示 io 字节数
    image

    5 条回复    2020-02-26 22:24:29 +08:00
    xFrank
        1
    xFrank  
       2020-02-14 16:21:19 +08:00
    多谢分享
    psuwgipgf
        2
    psuwgipgf  
       2020-02-14 17:28:56 +08:00
    很好,
    turi
        3
    turi  
       2020-02-14 18:06:40 +08:00
    国内这方面的真的少,

    我去年写的时候,找了好多都没找到可用的,可能我把 fd 传到 native 中,让 native 层去收数据有关
    missdeer
        4
    missdeer  
       2020-02-15 09:31:49 +08:00
    @turi 没去看看影梭以及各种派生 app 的代码?
    redcoffeecat
        5
    redcoffeecat  
       2020-02-26 22:24:29 +08:00
    注释能更详细就好了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   985 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 18ms · UTC 21:16 · PVG 05:16 · LAX 13:16 · JFK 16:16
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.