Android网络抓包:使用VpnService完成Http请求

目的

在Anroid下完成tcp协议和udp协议,了解Http请求过程。

实现

VPNService类似创建一个虚拟网络,可以设置ip,设置应用,dns服务器。假设我们在自己app下访问http://example.com,本地找不到域名映射就会请求dns拿ip(udp协议),拿到ip再进行tcp连接传输数据就完成http请求。假设我们启动VPNService服务,就可以拦截到udp数据包和tcp数据包,要想完成一个http请求,就得完成 udp和tcp协议。

完成udp协议:使用nio零拷贝,使用selector监听事件状态(减少线程),udp协议比较简单。

完成tcp协议:也使用nio和selector,tcp协议就比较麻烦,有三次握手和四次挥手。解析掉三次握手完,之后拿到数据,数据写入socket,socket读取到得数据再封装好数据包写入网络。socket已经帮完成tcp协议了,我们只需要交换数据。

遇到问题

  1. VPNService创建的ParcelFileDescriptor需要nio(非阻塞)读取,nio不是异步所以需要线程池多线程执行(不然一个包解析到一个包多耗时),多线程就会导致数据包的乱序问题。
  2. 多线程使用Selector问题,一个线程先selector.select();(会阻塞线程),另一个线程后再执行channel.register(selector,...)就会阻塞当前线程,可以先使用selector.wakeup()唤醒第一个线程,再注册事件,大量数据执行还是会出现阻塞线程。
  3. 使用Selector监听所有socket的read事件,他是用一个线程去执行的,如何保证不同ip的socket的读取数据封装数据写到网络中互相不影响。
  4. 多线程使用FileChannel会出错。

最后效果

我是指定拦截com.android.browser(自带浏览器)应用。

代码操作

使用 Android Studio 创建一个项目,创建一个MyVpnService继承VpnService,setupVpn方法启动,builder设置基本参数,主要创建一个ParcelFileDescriptor对象,这个对象可以拿到数据流,进行读写。

scss 复制代码
if (descriptor == null) {
    Builder builder = new Builder();
    // 添加IPv4地址
    builder.addAddress("10.0.0.2", 32);
    // 添加IPv4路由,拦截所有ipv4
    builder.addRoute("0.0.0.0", 0);
    // 添加dns服务器
    builder.addDnsServer("114.114.114.114");
    try {
        //指定应用拦截
        builder.addAllowedApplication("com.example.vpnservicedemo");
        //builder.addAllowedApplication("com.android.browser");
    } catch (PackageManager.NameNotFoundException e) {
        throw new RuntimeException(e);
    }

    // ParcelFileDescriptor是一个文件描述符,它是一种程序读写已打开文件、socket的对象
    descriptor = builder.setConfigureIntent(pendingIntent).establish();

    //创建一个读取对象
    readVpn = new ReadVpn(this, descriptor);
    readVpn.start();

}

修改AndroidManifest.xml文件,添加权限声明,MyVpnService的service声明。

ini 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <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" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.VpnServiceDemo"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name=".MyVpnService" android:permission="android.permission.BIND_VPN_SERVICE"
            android:exported="true">
            <intent-filter>
                <action android:name="android.net.VpnService" />
            </intent-filter>
        </service>
    </application>

</manifest>

ReadVpn的start方法,多线程读取数据并解析数据,pushPc.pushData()是将数据包通过socket方式推送到pc电脑上,好让Wireshark查看数据包情况,parseData解析数据包。

ini 复制代码
public void start() {

    //创建一个线程读取channel数据
    readDataThread = new Thread(() -> {

        FileChannel readChannel = null;
        ExecutorService threadPool = null;
        try {

            //获取cpu核心数
            int numOfCores = Runtime.getRuntime().availableProcessors();
            //创建线程池
            threadPool = Executors.newFixedThreadPool(numOfCores);

            // 获取一个channel,零拷贝读取数据
            readChannel = new FileInputStream(descriptor.getFileDescriptor()).getChannel();


            ByteBuffer buffer = ByteBuffer.allocate(1024 * 20);


            while (!Thread.interrupted()) {

                int len = readChannel.read(buffer);

                if (len == -1) {
                    break;
                }

                if (len > 0) {
                    buffer.flip();
                    byte[] bt = new byte[len];
                    buffer.get(bt);

                    //计算接收数量
                    receiveLen += len;
                    //发送到ui
                    pushUi();


                    threadPool.submit(() -> {

                        //推送PC
                        pushPc.pushData(bt);

                        parseData(bt);
                    });
                    buffer.clear();

                }


            }

        } catch (Exception e) {

            System.out.println("ReadData start 异常: " + e.getMessage());

        } finally {

            if (readChannel != null) {
                try {
                    readChannel.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

            if (threadPool != null) {
                threadPool.shutdown();
            }
        }
    });

    readDataThread.setName("readDataThread");
    readDataThread.start();
}

parseData 方法,IpSelector.newPacket解析数据包,根据源ip源prot判断唯一,如果数据包是tcp就创建TcpPipe,udp就创建UdpPipe,然后再对应执行receive方法。

ini 复制代码
private void parseData(byte[] array) {
    try {
        Packet packet = IpSelector.newPacket(array, 0, array.length);
        if (packet instanceof IpPacket) {

            IpPacket ipPacket = (IpPacket) packet;
            //通过源ip和源port判断唯一SendData
            String key = getKey(ipPacket);

            if (key != null) {

                if (pipeMap.get(key) == null) {
                    Pipe pipe = null;
                    if (ipPacket.getHeader().getProtocol() == IpNumber.TCP) {
                        pipe = new TcpPipe(vpnService, this);
                    } else if (ipPacket.getHeader().getProtocol() == IpNumber.UDP) {
                        pipe = new UdpPipe(vpnService, this);
                    }
                    if (pipe != null) {
                        //防止多线程问题
                        pipeMap.putIfAbsent(key, pipe);
                    }
                }

                Pipe pipe = pipeMap.get(key);

                int code = pipe.receive(ipPacket);

                if (code == -1) {
                    //说明关闭了
                    pipeMap.remove(key);
                }

            }

        }
    } catch (Exception e) {
        e.printStackTrace();
        System.out.println("ReadData parseData 解析包异常:" + e.getMessage());
    }

}

TcpPipe.receive 方法,syn标志(第一次握手)创建SocketChannel.open()初始化属性,因为多线程原因,数据包乱序,所以通过序列号判断是不是可以发送的序号,不是就缓存到map,是的话发送,然后看看map里面还有没有可以发的,有就一起发送。

ini 复制代码
public int receive(IpPacket ipPacket) throws IOException {

    TcpPacket tcpPacket = (TcpPacket) ipPacket.getPayload();

    boolean syn = tcpPacket.getHeader().getSyn();
    boolean fin = tcpPacket.getHeader().getFin();
    boolean psh = tcpPacket.getHeader().getPsh();
    boolean ack = tcpPacket.getHeader().getAck();
    boolean rst = tcpPacket.getHeader().getRst();

    InetAddress dstAddr = ipPacket.getHeader().getDstAddr();
    int dstPort = tcpPacket.getHeader().getDstPort().valueAsInt();

    int sequenceNumber = tcpPacket.getHeader().getSequenceNumber();

    if (rst) {
        close();
        return -1;
    }

    if (sendFin.get() && replyFin.get()) {
        close();
        return -1;
    }

    if (syn) {

        if (socketChannel == null) {

            srcIpPacket = ipPacket;
            replyAck.set(sequenceNumber + 1);

            // 使用nio
            socketChannel = SocketChannel.open();
            // 使连接不被自身拦截
            vpnService.protect(socketChannel.socket());
            // 使用非阻塞io
            socketChannel.configureBlocking(false);
            socketChannel.connect(new InetSocketAddress(dstAddr, dstPort));

            //注册事件
            TcpSelector.registerConnect(socketChannel, this);

        }

    } else {

        if (socketChannel != null && !socketChannel.isConnected()) {
            //连接重置
            replyRst(tcpPacket);
            return -1;
        }

        try {
            Packet payload = tcpPacket.getPayload();

            if ((payload != null && payload.getRawData().length > 0) || fin) {

                //刚好到发送序列只发送
                if (replyAck.get() == sequenceNumber) {

                    sendData(tcpPacket);

                    while (true) {

                        TcpPacket packets = dataMap.get(replyAck.get());
                        if (packets != null) {
                            dataMap.remove(replyAck.get());
                            sendData(packets);

                        } else {
                            break;
                        }
                    }

                } else {
                    dataMap.put(sequenceNumber, tcpPacket);
                }

            }

        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("sendData 异常:" + e.getMessage());
            close();
            return -1;
        }

    }

    return 0;
}

TcpSelector类监听SocketChannel连接和读事件,因为selector.select()是用一个线程执行,为了不让各个ip的构建数据包互相影响,所有推送到自己实例的队列中,队列又保证了顺序。可以看到while循环完,会读registerQueue队列然后注册事件,这是为了解决多线程使用Selector问题。

ini 复制代码
public class TcpSelector {
    private static Selector selector;

    private static ConcurrentLinkedQueue<RegisterData> registerQueue = new ConcurrentLinkedQueue<>();
    private static ExecutorService threadPool = Executors.newFixedThreadPool(5);

    static {

        try {
            selector = Selector.open();
        } catch (IOException e) {
            System.out.println("TcpSelector 启动失败:" + e.getMessage());
        }

        Thread thread = new Thread(() -> {
            try {
                while (!Thread.interrupted()) {
                    // 等待事件
                    selector.select();

                    // 获取事件的迭代器
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();

                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();

                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        TcpPipe tcpPiping = (TcpPipe) key.attachment();

                        if (key.isConnectable()) {

                            try {
                                // 完成连接[这里会出现超时异常]
                                socketChannel.finishConnect();

                                //切换事件
                                key.interestOps(SelectionKey.OP_READ);

                                tcpPiping.replySyn();

                            } catch (Exception ex) {
                                ex.printStackTrace();
                                tcpPiping.close();
                            }


                        } else if (key.isReadable()) {

                            try {

                                ByteBuffer buffer = ByteBuffer.allocate(1024);
                                int bytesRead = socketChannel.read(buffer);

                                if (bytesRead == -1) {


                                    //推送一个结束
                                    tcpPiping.replyPshQueue.offer(null);

                                    tcpPiping.close();

                                } else if (bytesRead > 0) {
                                    buffer.flip();
                                    byte[] array = new byte[buffer.limit()];
                                    buffer.get(array);

                                    tcpPiping.replyPshQueue.offer(array);


                                }

                            } catch (Exception e) {
                                tcpPiping.close();
                                e.printStackTrace();
                            }

                        }

                        // 删除处理过的事件
                        iterator.remove();
                    }
                    RegisterData registerData = null;
                    while ((registerData = registerQueue.poll()) != null) {
                        registerData.socketChannel.register(selector, SelectionKey.OP_CONNECT, registerData.tcpPipe);
                    }

                }

            } catch (Exception e) {
                System.out.println("TcpSelector 线程异常结束:" + e.getMessage());
                e.printStackTrace();
            }

        });
        thread.setName("tcpSelector");
        thread.start();

    }

    public static void registerConnect(SocketChannel socketChannel, TcpPipe tcpPipe) {

        RegisterData registerData = new RegisterData();
        registerData.socketChannel = socketChannel;
        registerData.tcpPipe = tcpPipe;

        registerQueue.offer(registerData);
        selector.wakeup();
    }

    static class RegisterData {
        SocketChannel socketChannel;
        TcpPipe tcpPipe;
    }


}
完整代码,供个人学习

断续/VpnServiceDemo - 码云 - 开源中国 (gitee.com)

相关推荐
阿巴斯甜20 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker20 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952721 小时前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android