VpnService 能否原样将三层的 IP 报文发出去? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
gam2046
V2EX    Android

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

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

    需要开发一款流量统计的软件( 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  
    PRO
       2019-08-05 18:32:43 +08:00
    模拟个 null 0 接口出来
    用 loopback
    手动开阀下一跳丢进去
    realpg
        3
    realpg  
    PRO
       2019-08-05 18:33:16 +08:00
    哦 没注意是 android 节点 请无视
    gam2046
        4
    gam2046  
       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。
    slowman
        7
    slowman  
       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  
       2024-03-31 17:39:34 +08:00
    四年了,请问解决了吗?
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     942 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 36ms UTC 20:27 PVG 04:27 LAX 12:27 JFK 15:27
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86