Total Pageviews

Friday 11 March 2022

聊聊Tun 透明代理的多种实现方式,以及如何避免 routing loop

从tun读出来后再写入tun,下次读还会将自己刚写入的packet读出来,如果设置默认路由是tun网卡,会导致死循环。下文会介绍解决routing loop的多种方法

https://www.kernel.org/doc/Documentation/networking/tuntap.txt

  1. How does Virtual network device actually work ?
    Virtual network device can be viewed as a simple Point-to-Point or
    Ethernet device, which instead of receiving packets from a physical
    media, receives them from user space program and instead of sending
    packets via physical media sends them to the user space program.

Let’s say that you configured IPv6 on the tap0, then whenever
the kernel sends an IPv6 packet to tap0, it is passed to the application
(VTun for example). The application encrypts, compresses and sends it to
the other side over TCP or UDP. The application on the other side decompresses
and decrypts the data received and writes the packet to the TAP device,
the kernel handles the packet like it came from real physical device.

一共两个方法, read 和 write

read from tun读取数据包

write将数据包写入tun,tun直接将userspace的packet注入内核,就好像内核刚从物理网卡读取出来一样

https://github.com/gfreezy/seeker/blob/b5a1b83a24c48bb96fb26cc3d5402dd2cd7159f1/README.adoc#%E6%8C%87%E5%AE%9A-ip-%E6%88%96%E6%9F%90%E7%BD%91%E6%AE%B5%E8%B5%B0%E4%BB%A3%E7%90%86

https://github.com/gfreezy/seeker/blob/b5a1b83a24c48bb96fb26cc3d5402dd2cd7159f1/README.adoc#%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86

搞这些东西最好在虚拟机,搞坏了还能还原。

如何避免重新读出刚写入tun设备的数据包

请问,如果自己设置默认路由全部流量都发到tun设备,自己read出来后交给userspace网络栈处理,再将处理后的数据包写入tun,之后的read会不会重新read出来自己刚写入的包呢?

我理解会,内核根据默认路由会重新送给tun,但那样死循环了呀

我看这些利用tun的transparent proxy从未遇到过这问题,就好奇是什么原理

seeker有这句话

== 指定 IP 或某网段走代理
修改路由表,将希望走代理的 IP 或者网段路由到虚拟网卡。如果使用了本机 socks5 代理,则必须确保 socks5 不会直连加入路由表的网段,否则会死循环。

设置默认路由后绝大多数流量都会到tun,为了防止自己刚写到tun包的走默认路由又回来,需要再设置条更短的路由表,发包的ip dest 用这个ip网段,这样就不会再回来。


Active Routes:

Network Destination Netmask Gateway Interface Metric

   0.0.0.0          0.0.0.0      192.168.3.1     192.168.3.21     25
   0.0.0.0          0.0.0.0         On-link        198.18.0.1      0
 127.0.0.0        255.0.0.0         On-link         127.0.0.1    331
198.18.0.0      255.255.0.0         On-link        198.18.0.1

clash为默认网关,clash使用wireguard,在windows上使用 SO_UNICAST_IP 绑定 outgoing 网卡流量。不会再走routing decision

透明代理的tcp listener都是寄生在lwip之上,从tun设备收到数据写入userspace stack,自己的socks代理直接从lwip处理后的数据里就能解析出socket,leaf就这么做的。

net stack写入tun

https://github.com/willdeeper/leaf/blob/0dff09c9bb652c53e197066af863a6f6a083ecff/leaf/src/proxy/tun/inbound.rs#L113

tun写入 net stack
https://github.com/mellow-io/go-tun2socks/blob/6289c4be8d7c2915a73ba0d303d71a3a135e1574/cmd/tun2socks/main.go#L199

https://github.com/mellow-io/mellow/blob/f71f6e54768ded3cfcc46bebb706d46cb8baac08/src/helper/linux/config_route#L1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ip
CMD=$1
# tun gateway
TUN_GW=$2
# 原来的默认网关
ORIG_GW=$3
# 原来的默认网关发给的接口
# https://github.com/mellow-io/mellow/blob/f71f6e54768ded3cfcc46bebb706d46cb8baac08/src/main.js#L866
ORIG_GW_SCOPE=$4
# send through 策略路由
# https://man7.org/linux/man-pages/man8/ip-rule.8.html
# https://github.com/mellow-io/mellow/blob/f71f6e54768ded3cfcc46bebb706d46cb8baac08/src/main.js#L719
ORIG_ST=$5

"$CMD" route del default table main
# 注意,这加到了 main table,是OS默认使用的路由表
"$CMD" route add default via $TUN_GW table main
# 注意,这里加到default table,default table是默认路由表,main路由表未匹配到会使用default
"$CMD" route add default via $ORIG_GW dev $ORIG_GW_SCOPE table default
# 策略路由,从原来的default interface 接口来的ip数据包转发到default表,这样就能送出去流量,不会成环了
"$CMD" rule add from $ORIG_ST table default

看来 wireguard 写入 tun 后并不会出现routing loop,SO_NOTOIF opt 确保不会重新读出刚才写入的数据包
https://www.wireguard.com/netns/

wireguard 之前想添加 SO_NOTOIF 来避免routing loop,https://lists.openwall.net/netdev/2016/02/02/222
但看来维护者认为 ip rule 就够用,不应该在内核做太多magic的功能(直接不走路由)。最后不了了之

wireguard实现
https://github.com/WireGuard/wireguard-go

https://github.com/mellow-io/mellow/issues/310

http://www.policyrouting.org/iproute2.doc.html#ss9.6

防止routing loop的方式

为需要直连的ip设置单独的路由(删除掉默认路由)

同学,我又来了[无辜笑]。我看seal的全局模式可以转发整个系统的流量,最近自己在搞一个类似的透明代理工具,有个问题想请教下

如果自己设置默认路由全部流量都发到tun设备,自己read出来后交给userspace网络栈处理,再将处理后的数据包写入tun,之后的read会不会重新read出来自己刚写入的包呢

⁣我理解会,内核根据默认路由会重新送给tun,但那样死循环了呀

⁣但我看seal的全局模式transparent proxy从未遇到过这问题,这是怎么做到的呢

回复: read出来不会直接写入tun,是重新封包发给VPN server

vpn server是单独加了一条路由,而且发送包不是直接写tun包,是直接通过tcp/udp发出,socket可以绑定指定网卡发送

ip route del default
ip route add default dev wg0
ip route add 163.172.161.0/32 via 192.168.1.1 dev eth0
The Classic Solutions

为直连ip设置单独路由(不删除默认路由,只覆盖)

默认路由的作用是没有匹配到时走default,通过设置 0.0.0.0/1,让这条路由总是先于 default 命中。
再对要直连的 ip(这里是 163.172.161.0) 设置单独的路由,不需要删除原来的默认路由

ip route add 0.0.0.0/1 dev wg0
ip route add 128.0.0.0/1 dev wg0
ip route add 163.172.161.0/32 via 192.168.1.1 dev eth0

这种也叫做 0/1 128/1 trick

但这trick有局限搜0/1

推荐阅读

Overriding The Default Route

iptables

iptables REDIRECT

https://github.com/iamwwc/ooproxy

iptables TPROXY

iptables with fwmark

iptables 配合 fwmark。
https://flylib.com/books/en/2.783.1.50/1/

策略路由 (ip rule with fwmark)

使用 ip rule 排除掉某个网卡流量

Rule-based Routing https://www.wireguard.com/netns/#routing-all-your-traffic

ip rule https://man7.org/linux/man-pages/man8/ip-rule.8.html

https://www.reddit.com/r/WireGuard/comments/m8jwnt/i_dont_understand_how_wgquick_adds_routes/grkomnp?utm_source=share&utm_medium=web2x&context=3

或者 rule 还有更强大的终止routing decision

这方法没用过,但看着能行

https://github.com/tailscale/tailscale/issues/144

namespace solution

https://superuser.com/questions/1664065/tun-device-how-to-avoid-routing-dead-loop-when-write-a-transparent-proxy

The New Namespace Solution

bind before connect

bind之后connect,routing 不会起作用,这样就能解决设置默认网关后导致的 routing loop

通过调试 leaf,我已经能够十分确认上面这句话的正确性

If I bind an interface before to connect, Does that mean the connect for outgoing traffic will use that interface I bind without follow the routing decision?

@nuclear yes, if you bind() to an interface before connect()’ing, the connection will go out through that interface. That is the whole point.

https://stackoverflow.com/questions/4297356/how-does-a-socket-know-which-network-interface-controller-to-use/4297377?noredirect=1#comment121142012_4297377

windows 平台

wireguard也依赖tun device,但这tun和Unix上的tun不太像。

windows并没有tun的概念,为弥补这空缺,wintun 既是个 windows 内核驱动,也是个userspace tunnel,前者从内核拉取、向内核注入packet,后者将前者的数据包传给 userspace 处理 https://git.zx2c4.com/wireguard-windows/about/docs/attacksurface.md#wintun。

windows 并没有策略路由,所以通过 bind interface IP_UNICAST_IF 来避免routing loop(从侧面印证了 bind before connect 可以解决routing loop)

以下三封邮件解释了为什么使用 bind。

https://lists.zx2c4.com/pipermail/wireguard/2019-September/004493.html
https://lists.zx2c4.com/pipermail/wireguard/2019-September/004541.html
https://lists.zx2c4.com/pipermail/wireguard/2019-September/004542.html

其中 https://lists.zx2c4.com/pipermail/wireguard/2019-September/004541.html 谈到的 IP_UNICAST_IF 可理解为指定 outgoing 流量的网络接口。

而里面说的 WFP 是 Windows Filtering Platform,看起来是 windows 平台内核级别的流量过滤API(类似 iptables)

windows 没有Linux 的类似 ip rule 这种策略路由。 wireguard-windows 用来 IP_UNICAST_IF 来开发

下面是 wireguard-go 在 windows 上用 IP_UNICAST_IF 的实现

wireguard-go IP_UNICAST_IF的实现

https://github.com/WireGuard/wireguard-go/blob/5846b622837e04dbc35b153d9ceda7fd66397520/conn/bind_windows.go#L567

31是windows header里定义的

https://www.pinvoke.net/search.aspx?search=IP_UNICAST_IF&namespace=[All]

此外,wireguard 会帮你自动设置路由,bind 来避免routing loop

https://www.reddit.com/r/WireGuard/comments/m8jwnt/i_dont_understand_how_wgquick_adds_routes/

wireguard 对自己使用 IP_UNICAST_IF 的解释

https://git.zx2c4.com/wireguard-windows/about/docs/netquirk.md

Linux 平台

通过 setsockopt syscall 时传递 SO_BINDTODEVICE

https://stackoverflow.com/questions/4584908/how-do-i-send-udp-packet-from-a-specific-interface-on-linux

https://lore.kernel.org/netdev/1328685717.4736.4.camel@edumazet-laptop/T/

阅读leaf代码时确认使用过上述选项

https://github.com/willdeeper/leaf/blob/0dff09c9bb652c53e197066af863a6f6a083ecff/leaf/src/proxy/mod.rs#L235


总结

经测试(leaf只bind,不添加 ip rule),bind SO_BINDTODEVICE 之后再将数据写入socket,数据会直接到达网卡的发送队列,不会再次走routing decision。

路由选择的核心在于找到一个网卡,并 bind SO_BINDTODEVICE 直接绕过了这一步。

对于从外界接收数据,

user mode tcp/ip stack <=> Application
^
| default routing
outside => routing decision => NIC adaptor => tcp/ip stack behind NIC
\
Application
/
outside <= NIC adaptor <= routing decision <= tcp/ip stack behind NIC
^
|bind SO_BINDTODEVICE
user mode tcp/ip stack <=> Application

当 leaf 使用bind到默认网卡时,不需要ip rule策略路由。


即使socks代理在本地,也不会导致routing loop。

这是因为路由表中 local 表优先级最高,我们修改的是main和default表。

配置 socks outbound 为其他机器时正常工作,是因为流量到了原始网卡直接发离机器了。

但如果配置的socks outbound 为本地环路地址会又问题

例子

本地开clash,listen 7890,leaf配置socks outbound 127.0.0.1:7890
socket SO_BINDTODEVICE 到 eth0,用这 socket dial to 127.0.0.1:7890
在vmm8,也就是虚拟机的默认网卡上抓到 ip dst 为 127.0.0.1 的流量,127.0.0.1属于环路地址,不应该在以太网卡上抓到。所以连接失败。

clash这issue https://github.com/Dreamacro/clash/issues/135 谈到了bind,listen的问题

参考的开源项目

C实现的tun2socks
https://github.com/russdill/tunsocks

Go实现的tun2socks,支持windows
https://github.com/Intika-Linux-Network/Tun-2-Socks

Rust实现的透明代理工具
https://github.com/willdeeper/leaf

https://stackoverflow.com/questions/14697963/tcp-ip-connection-on-a-specific-interface

Tcp connection 使用特定interface送出去流量
或者使用策略路由
https://www.reddit.com/r/golang/comments/4x277h/dial_a_tcp_connection_from_a_specific_interface/d6cfr4d?utm_source=share&utm_medium=web2x&context=3

附录

wireguard 架构
https://github.com/WireGuard/wireguard-rs

wireguard white paper 搜 routing loop

https://www.wireguard.com/papers/wireguard.pdf

-----

https://github.com/rfc1036/udptunnel

https://github.com/cbxgyh/usrnet

https://github.com/iamwwc/tunnel

No comments:

Post a Comment