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)

相关推荐
用户20187928316713 小时前
通俗易懂的讲解:Android系统启动全流程与Launcher诞生记
android
二流小码农14 小时前
鸿蒙开发:资讯项目实战之项目框架设计
android·ios·harmonyos
用户20187928316715 小时前
WMS 的核心成员和窗口添加过程
android
用户20187928316715 小时前
PMS 创建之“软件包管理超级工厂”的建设
android
用户20187928316715 小时前
通俗易懂的讲解:Android APK 解析的故事
android
渣渣_Maxz15 小时前
使用 antlr 打造 Android 动态逻辑判断能力
android·设计模式
Android研究员15 小时前
HarmonyOS实战:List拖拽位置交换的多种实现方式
android·ios·harmonyos
guiyanakaung16 小时前
一篇文章让你学会 Compose Multiplatform 推荐的桌面应用打包工具 Conveyor
android·windows·macos
恋猫de小郭16 小时前
Flutter 应该如何实现 iOS 26 的 Liquid Glass ,它为什么很难?
android·前端·flutter
葱段16 小时前
【Compose】Android Compose 监听TextField粘贴事件
android·kotlin·jetbrains