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

VpnService 能否原样将三层的 IP 报文发出去?

  •  
  •   gam2046 · 2019-08-05 17:28:27 +08:00 · 14874 次点击
    这是一个创建于 1934 天前的主题,其中的信息可能已经有所发展或是发生改变。

    需要开发一款流量统计的软件( Android ),于是乎就想到了 VpnService,天然适合。

    但是呢,由于 VpnService 收到的是三层的 IP 报文,而我本身没有对于数据修改的需求,只是想简单的记录。

    参考了一下部分开源软件的实现,各种代理类软件(比如 SS ),都是将收到的三层报文解析后,发向远程端。 当然这类软件需要修改数据内容。

    而我现在不需要修改任何数据,只想单纯的统计发向每个服务端的数据量、时间等,并不关心数据内容。

    由于 RawSocket 肯定没法用,设备不存在 root。Java 似乎本身也未提供能够工作在三层的网络通讯方式。

    那么是否有什么尽可能简单的方法(或大佬的轮子),可以把三层报文转换成四层,发出去后,再返还给应用的方法?

    这里还有一个疑问,由于 Java 中没有找到直接操作三层网络协议的方法,对于非 TCP/UDP 报文(例如 ICMP/IGMP 等),又如何实现直接出口呢?

    25 条回复    2024-03-31 17:39:34 +08:00
    nondanee
        1
    nondanee  
       2019-08-05 18:29:51 +08:00
    不需要修改的话好像挺简单的

    我之前看 VPNservice 的 demo 都是 read 进来 write 回去就完了
    参考 https://blog.csdn.net/jsqfengbao/article/details/52462125

    demo 代码 Github 里有很多
    https://github.com/search?q=vpnservice+FileOutputStream&type=Code
    realpg
        2
    realpg  
       2019-08-05 18:32:43 +08:00
    模拟个 null 0 接口出来
    用 loopback
    手动开阀下一跳丢进去
    realpg
        3
    realpg  
       2019-08-05 18:33:16 +08:00
    哦 没注意是 android 节点 请无视
    gam2046
        4
    gam2046  
    OP
       2019-08-05 18:46:17 +08:00
    @nondanee #1,实际上并不是。VpnService.Builder#establish()返回的是 tun0 的句柄,因此 out 实际上应该写入的是返回给应用的响应内容,而把 in 都进来的直接 out 写回去,就变成了 echo 方法,这个请求根本就没有从真是的物理网卡出口。

    你给的地址,我搜到过,中间的过程被三个点给一笔带过了....

    // Read packets sending to this interface

    int length = in.read(packet.array());

    ... // <- 我关心的恰恰是这里应该怎么做

    // Write response packets back

    out.write(packet.array(), 0, length);
    DioV
        5
    DioV  
       2019-08-05 20:11:35 +08:00
    没有办法,必须程序处理。
    现在几个开源的就两种实现,一个是两次 NAT,还一种就是用用户态的 TCP/IP 栈
    ysc3839
        6
    ysc3839  
       2019-08-05 21:02:19 +08:00
    @gam2046 这段代码或许参考的是 Android 的 ToyVpn 示例代码 https://android.googlesource.com/platform/development/+/master/samples/ToyVpn/src/com/example/android/toyvpn/ToyVpnConnection.java#225
    这个代码是把 IP 包直接通过 UDP 发给远程服务器。

    许多开源软件的实现都是用 LwIP 解析 IP 包然后再处理的,可以参考一下 tun2socks。
    1423
        7
    1423  
       2019-08-05 21:03:13 +08:00 via Android
    allenforrest
        8
    allenforrest  
       2019-08-05 21:07:11 +08:00
    @gam2046 要先 vpnService.protect(socket) 一下,这样才能确保绑定物理设备发出去,否则还是 tun0,write 就又回去了。
    gam2046
        9
    gam2046  
    OP
       2019-08-05 21:16:19 +08:00
    @DioV 二次 NAT,工程量毕竟大,相比较我更愿意接受用户态自己实现协议栈,但问题在于 Java 也好,NDK 也罢,在不取得 ROOT 权限的情况下,似乎都不能发起 TCP/UDP 以外的请求。那么我在用户态收到例如 ICMP 的请求,应该如何实现呢?


    @ysc3839 #6
    @1423 #7

    感谢两位,我现在去了解一下 LwIP/tun2socks 等相关信息。稍后再来询问 /感谢两位。
    ysc3839
        10
    ysc3839  
       2019-08-05 23:25:16 +08:00
    @gam2046 应该是可以发 ICMP 的,不然哪来那么多 ping 的工具?
    gam2046
        11
    gam2046  
    OP
       2019-08-05 23:30:31 +08:00
    @ysc3839 然而并不可以,NDK 创建 socket 直接失败(需要 root 权限),Java 没有提供相关方法,Java 只提供封装后的 UDP/TCP 相关类。网上的实现是调用 shell ping 然后读取 stdin
    ysc3839
        12
    ysc3839  
       2019-08-05 23:47:27 +08:00 via Android
    @gam2046 那调用 ping 为何不需要 root 权限呢?假如真的不需要,你也起个新进程来发送 ICMP 不就好了?
    wwqgtxx
        13
    wwqgtxx  
       2019-08-06 09:41:36 +08:00
    @ysc3839 因为 Ping 设置了 suid 呀,和 su/sudo 一样的原理
    ysc3839
        14
    ysc3839  
       2019-08-06 14:40:51 +08:00 via Android
    @wwqgtxx 我确认了一下,ping 并没有 suid 权限。
    ```
    ~$ which ping
    /system/bin/ping
    ~$ stat /system/bin/ping
    File: `/system/bin/ping'
    Size: 69208 Blocks: 88 IO Blocks: 512 regular file
    Device: fc00h/64512d Inode: 7235 Links: 1
    Access: (755/-rwxr-xr-x) Uid: ( 0/ root) Gid: ( 2000/ shell)Access: 2009-01-01 00:00:00.000000000
    Modify: 2009-01-01 00:00:00.000000000
    Change: 2009-01-01 00:00:00.000000000
    ```

    再者,即使系统自带的 ping 有 suid,Termux 这个软件用的可不是系统的 ping 而是自己的 ping,仍然是可以正常使用的。
    ```
    $ which ping
    /data/data/com.termux/files/usr/bin/ping
    $ /data/data/com.termux/files/usr/bin/ping 192.168.1.1
    PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data.
    64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=18.7 ms
    ^C --- 192.168.1.1 ping statistics ---
    1 packets transmitted, 1 received, 0% packet loss, time 0ms
    rtt min/avg/max/mdev = 18.752/18.752/18.752/0.000 ms
    ```
    gam2046
        15
    gam2046  
    OP
       2019-08-06 16:09:29 +08:00
    @wwqgtxx #13
    @ysc3839 #14 感谢。我翻阅了一下 aosp 的源码。

    https://android.googlesource.com/platform/external/ping/+/27ca8cd5cb0891c8a15175b52c5c24253dea5b17/ping.c

    结果....

    我原本是这样写的 socket(AF_INET, SOCK_RAW, IPPROTO_ICMP),毫无疑问返回了-1

    然而 aosp 里 ping 是创建的

    icmp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP); // 1

    if (icmp_sock != -1)
    using_ping_socket = 1;
    else
    icmp_sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); // 2

    嗯....显然在 1 处创建时一定成功的,如果 1 的地方失败,按理说 2 应该是一定创建失败的。
    wwqgtxx
        16
    wwqgtxx  
       2019-08-06 18:09:06 +08:00 via iPhone
    @gam2046 我记得一篇文章上写过,直接创建 SOCK_DGRAM 是可以成功的,不需要 root 权限,但是好像是拿不到完整的 ICMP 数据包,需要自己想办法构建,可以看一下这个
    https://github.com/bgylde/PingForAndroid
    lkbert
        17
    lkbert  
       2020-07-17 11:01:55 +08:00
    @gam2046 这个问题最后是咋解决的啊,我现在也遇到这个问题,现在在写一个 vpn 的 app 来模拟网络延迟,对 TCP 和 UDP 做了处理,但是对 ICMP 协议不知道咋处理,查遍所有资料基本都是对 ICMP 丢弃,应用层执行 shell ping 就会收不到链接,我尝试直接将 ICMP 包写入 ,如你之前所说的“VpnService.Builder#establish()返回的是 tun0 的句柄,因此 out 实际上应该写入的是返回给应用的响应内容”,就没有任何响应了 ,这个不知道该咋处理了
    gam2046
        18
    gam2046  
    OP
       2020-07-17 13:20:09 +08:00
    @lkbert #17,如果 TCP/UDP 你已经处理完毕,那么你遇到关于 ICMP 情况可以参考 AOSP 相关的代码。

    https://android.googlesource.com/platform/external/ping/+/27ca8cd5cb0891c8a15175b52c5c24253dea5b17/ping.c#121

    Java 层并没有暴露除 TCP/UDP 以外的编程能力。
    lkbert
        19
    lkbert  
       2020-07-17 20:42:06 +08:00
    @gam2046 嗯 ,现在是我用 jni 实现 native ICMP 了,但是 Echo Reply 怎么给回应用的 socket,不知道怎么整了。请教下,你那边有处理方案吗?
    CrazyBoyFeng
        20
    CrazyBoyFeng  
       2021-03-24 16:07:52 +08:00
    @gam2046 #9
    请问你最终实现报文转发了吗?
    我搜了一圈,网上几乎都是 NAT 实现的。java 层似乎并不能实现用户态协议栈,因为不能发 raw 包,只能发 java 封装好的 tcp 和 udp 包。可以借助 jni 可以发 raw 包,但是如你所说,需要 root 。
    CrazyBoyFeng
        21
    CrazyBoyFeng  
       2021-03-24 19:07:02 +08:00
    至于为什么 tun2socks 使用用户态协议栈 lwip,那是因为它把包发给 socks 服务器了,而不是传输修改了 header 的 tcp 和 udp 。jvm 上并不能修改包头并重新发送。如果能直接发的话,题目的要求(转发)将变得十分简单。
    所以 jvm 要转发只有俩实现方案:
    1. 本地起个 socks 服务器,tun2socks 转给 socks 服务器。socks 往外的连接要传给 android protect() 一下。
    2. nat 实现。各自缓存一套 tcp 和 udp 的 natsession map 。收到来自 lan 的包,检查一下有没有 session,有的话直接取出来往 wan 传送 data 。没有的话建立一个 protect() wan 连接并存入 session map 。tcp 要处理握手和挥手,收到 lan 握手包建立外部连接,lan 挥手包关闭连接清除 session,如果是 wan 关闭连接则向 lan 发送挥手包。wan udp 连接设置个 timeout,超时自动关闭,关闭时清除 session 。
    9ttttttt
        22
    9ttttttt  
       2021-04-08 14:24:16 +08:00
    @CrazyBoyFeng 请问 Android 上面只能发 TCP 和 UDP 包么?那如果我实现一个基于 VPNService 的 Android VPN 应用,是不是除了 TCP 和 UDP 包的其他数据包会被截断呢?想问一下可以对 IP 包里面有没有可以自定义的标志位或信息字段呢,我想基于 IP 包里面自定义的标志位或信息字段来在 Android 上实现是否向 VPN 发数据的判断,类似于 PAC 那种想法。谢谢大佬!
    CrazyBoyFeng
        23
    CrazyBoyFeng  
       2021-04-23 01:49:02 +08:00
    @9ttttttt java 层无法建立除 tcp 和 udp 以外的通信。VPNService 可以收到 icmp 包,以字节数组的形式,但是无法在不 root 的情况下发出去,只能丢弃。
    你的第二个问题,大概是想实现类似 iptables 这类的东西? iptables 打标记的原理并不是修改数据包,而是建立数据表。而 java 层也不能改包,所以就不能发送自定义内容的数据包。
    jeesk
        24
    jeesk  
       2022-12-21 23:08:39 +08:00
    问题解决了吗? 这个恐怕需要创建一张网卡来操作?
    lysShub
        25
    lysShub  
       234 天前
    四年了,请问解决了吗?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3343 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 11:31 · PVG 19:31 · LAX 03:31 · JFK 06:31
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.