TL;DR
在不改变数据包本身的情况下 tproxy 为满足规则的数据包直接分配了 bind 在本地的某个设置了 IP_TRANSPARENT 的 socket* (即把 skb->sk 设置为某个本地的 socket, 换句话说此时的 skb 还没有经过 3 层的路由就已经被提前指定了 socket)
注: 如果透明代理需要支持 UDP 的话 (用得上的场景并不多) 还需要在 socket 上启用 IP_RECVORIGDSTADDR 的 option, 并且增加额外的逻辑处理原始目的地址的 IP_ORIGDSTADDR 消息, 要发送响应的话需要手动在那个原始目的地址上创建新 socket, 因为 UDP 的 socket 实现并不会像 TCP 那样在 accept 的时候自动 bind 到 skb 的原始目的地址
tproxy 在 PREROUTING 里就给 skb 提前分配好了 sk, skb 随后才会被路由, 通过策略路由(见下文)进到 input 的逻辑里交给协议栈的更高层来处理(会根据传输层协议的不同分别由协议栈的不同部分来处理最终分发给对应的 socket), 而在 hash table 里真正开始查找 socket 前有这样一处逻辑可以直接从 skb 里拿到 tproxy 在之前分配好的 socket (TCP 和 UDP 的实现是分开的, 分别在__inet_lookup_skb 和 __udp4_lib_rcv 里)
光是为 skb 分配了一个 sk 当然还是不够的, skb 必须走到 input 上去才能被本机 socket 处理(透明代理的 socket 当然也不例外), 否则就直接被 forward 了, 而这就是 tproxy 需要配合额外的策略路由才能正常工作的原因
# 比如 ss-redir 文档里的
# Add any UDP rules
ip route add local default dev lo table 100
ip rule add fwmark 1 lookup 100
iptables -t mangle -A SHADOWSOCKS -p udp --dport 53 -j TPROXY --on-port 12345 --tproxy-mark 0x01/0x01
# Apply the rules
...
iptables -t mangle -A PREROUTING -j SHADOWSOCKS
本来行文至此已经可以结束了, 但我感觉好像有哪里怪怪的.
lo 上面并没有绑定 skb 的目的 IP 地址耶, 却还是能正常处理这个 skb 然后交给上层的 socket , 这就让我很好奇了.....
原因其实并不复杂, 因为上面添加的那条路由规则是 local 的, 根据这里的逻辑会通过ip_local_deliver 直接交给本机协议栈上层处理 (其实正常情况下所谓某个接口绑定了某个 ip 就是通过往路由表里自动添加了 local 的路由来实现的)
最后留个有点跑题的小问题叭, local 的路由规则里指定的 dev 有意义吗? 答案就在 route.c 里 (或者大概也能猜到 雾)
Reference:
[1] https://blog.cloudflare.com/how-we-built-spectrum/
[2] http://vger.kernel.org/~davem/skb.html
[3] https://man7.org/linux/man-pages/man8/ip-route.8.html
[4] https://stackoverflow.com/questions/42738588/ip-transparent-usage
[5] https://man7.org/linux/man-pages/man7/ip.7.html
[6] https://elixir.bootlin.com/linux/v5.11.11/source
[7] https://vincent.bernat.ch/en/blog/2017-ipv4-route-lookup-linux