TL;DR
在不改变数据包本身的情况下 tproxy 为满足规则的 skb 直接分配了 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 当然也不例外), 而如果没有额外的策略路由这个 skb 是不会走到本机的 input 上去的,要么被丢掉要么被转发
# 比如 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
上面添加的那条路由是 local 的, 会通过ip_local_deliver 直接交给本机协议栈上层处理 (其实所谓某个接口绑定了某个 ip 就是通过往路由表里自动添加了 local 的路由来实现的),这里相当于给一个本地的 dev 绑了 0.0.0.0,无论 skb 的目的地是什么都可以被这个 dev 处理。这条路由规则只对被标记了 0x01 的数据包适用(也就是被提前指定了要走到透明代理上面去的流量)
References:
[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