Android vpnservice 流量代理示例

2020-02-14 15:47:31 +08:00
 mightofcode

利用 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 字节数

9314 次点击
所在节点    Android
5 条回复
xFrank
2020-02-14 16:21:19 +08:00
多谢分享
psuwgipgf
2020-02-14 17:28:56 +08:00
很好,
turi
2020-02-14 18:06:40 +08:00
国内这方面的真的少,

我去年写的时候,找了好多都没找到可用的,可能我把 fd 传到 native 中,让 native 层去收数据有关
missdeer
2020-02-15 09:31:49 +08:00
@turi 没去看看影梭以及各种派生 app 的代码?
redcoffeecat
2020-02-26 22:24:29 +08:00
注释能更详细就好了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/644563

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX