引子
今年 4 月,在家的时候意外看到了 ss-redir 透明代理,对其中的详细说明持有怀疑态度:
由于笔者才疏学浅,刚开始居然以为 TCP 透明代理和 UDP 透明代理是一样的,只要无脑 REDIRECT 到 ss-redir 监听端口就可以了。
…
但是,上面这种情况只针对 TCP;对于 UDP,如果你做了 DNAT,就无法再获取数据包的原目的地址和目的端口了。
于是对此专门做了一番调研。整篇分为三部分:第一部分是我对上述叙述的调研结果,第二部分讨论 TPROXY,第三部分叙述一些 NAT 的知识。
ss-redir 中的 UDP REDIRECT 问题
ss-redir 的原理很简单:使用 iptables 对 PREROUTING 与 OUTPUT 的 TCP/UDP 流量进行 REDIRECT(REDIRECT 是 DNAT 的特例),ss—redir 在捕获网络流量后,通过一些技术手段获取 REDIRECT 之前的目的地址(dst)与端口(port),连同网络流量一起转发至远程服务器。
针对 TCP 连接,的确是因为 Linux Kernel 连接跟踪机制的实现才使获取数据包原本的 dst 和 port 成为可能,但这种连接跟踪机制并非只存在于 TCP 连接中,UDP 连接同样存在,conntrack -p udp
便能看到 UDP 的连接跟踪记录。内核中有关 TCP 与 UDP 的 NAT 源码 /net/netfilter/nf_nat_proto_tcp.c 和 /net/netfilter/nf_nat_proto_udp.c 几乎一模一样,都是根据 NAT 的类型做 SNAT 或 DNAT。
那这究竟是怎么一回事?为什么对于 UDP 连接就失效了呢?
回过头来看看 ss-redir 有关获取 TCP 原本的 dst 和 port 的源码,核心函数是 getdestaddr
:
static int |
在内核源码中搜了下有关 SO_ORIGINAL_DST
的东西,看到了 getorigdst:
/* Reversing the socket's dst/src point of view gives us the reply |
We only do TCP and SCTP at the moment。Oh,shit!只针对 TCP 与 SCTP 才能这么做,并非技术上不可行,只是人为地阻止罢了。
TPROXY
为了在 redirect UDP 后还能够获取原本的 dst 和 port,ss-redir 采用了 TPROXY。Linux 系统有关 TPROXY 的设置是以下三条命令:
# iptables -t mangle -A PREROUTING -p udp -j TPROXY --tproxy-mark 0x2333/0x2333 --on-ip 127.0.0.1 --on-port 1080 |
获取原本的 dst 和 port 的相关源码如下:
static int |
大意就是在 mangle 表的 PREROUTING 中为每个 UDP 数据包打上 0x2333/0x2333 标志,之后在路由选择中将具有 0x2333/0x2333 标志的数据包投递到本地环回设备上的 1080 端口;对监听 0.0.0.0 地址的 1080 端口的 socket 启用 IP_TRANSPARENT
标志,使 IPv4 路由能够将非本机的数据报投递到传输层,传递给监听 1080 端口的 ss-redir。IP_RECVORIGDSTADDR
与 IPV6_RECVORIGDSTADDR
则表示获取送达数据包的 dst 与 port。
可问题来了:要知道 mangle 表并不会修改数据包,那么 TPROXY 是如何做到在不修改数据包的前提下将非本机 dst 的数据包投递到换回设备上的 1080 端口呢?
与之有关的内核源码我没有完全看懂。根据 TProxy - Transparent proxying, again 和 2.6.26 时代的 patch set,在 netfilter 中的 PREROUTING 阶段,将符合规则的 IP 数据报 skb_buff 中的成员 sk(它表示数据包从属的套接字)给 assign_sock,这个 sock 就是利用 iptables TPROXY 的 target 信息找到的:
// /net/netfilter/xt_TPROXY.c |
sock 是根据四元组 saddr, sport, daddr, dport 来选择的,其中 saddr 与 sport 来自 skb_buff,另外俩为 target 所定义。没搞懂的地方在于:在 ip_rcv_finish
中,是怎样将数据包投递到上层协议以及指定端口的?
目前的猜测如下:
// kernel version 4.17 |
通过查找路由表确定 res-type
的类型为 RTN_LOCAL
,goto 到 local_input,进而调用 rt_dst_alloc
,形参参数 (flag & RTCF_LOCAL) == true
,设置了 rt->dst.input
是 ip_local_deliver
。ip_local_deliver
中使用协议回调函数 handler
来进一步处理数据包。
进入传输层后,对 IPv4 下的 UDP 协议来说,它的 handler
为 udplite_rcv
(v4.17),通过调用 skb_steal_sock
来获取 sock,这个 sock 与 TPROXY 中在 nf_tproxy_get_sock_v4
获取到的 sock 是一致的。sock 的判断是根据 compute_score
计算的得分来选择的,分高者赢。
// UDP |
有趣的是,在查找资料过程中,我还看到了这篇文章:TPROXY 之殇 - NAT 设备加代理的恶果。
最后来回到原点,谈一谈 NAT。
NAT
根据 RFC 2663,NAT 分为基本网络地址转换(Basic NAT,also called a one-to-one NAT)和网络地址端口转换(NAPT(network address and port translation),other names include port address translation (PAT), IP masquerading, NAT overload and many-to-one NAT)两类。基本网络地址转换仅支持地址转换,不支持端口映射,要求每一个内网地址都对应一个公网地址;网络地址端口转换支持端口的映射,允许多台主机共享一个公网地址。支持端口转换的 NAT 又可以分为两类:源地址转换(SNAT)和目的地址转换(DNAT)。前一种情形下发起连接的计算机的 IP 地址将会被重写,使得内网主机发出的数据包能够到达外网主机。后一种情况下被连接计算机的 IP 地址将被重写,使得外网主机发出的数据包能够到达内网主机。
Linux 下,iptables 的 SNAT 除了 SNAT target 外,还有 MASQUERADE、NETMAP。MASQUERADE target 与 SNAT 差不多,区别主要是 MASQUERADE 能够自动选择出口网卡的动态 IP 地址,而 NETMAP 则是只转换 IP 地址。DNAT 的 target 有 DNAT、REDIRECT,区别是 REDIRECT 只进行端口转换而 IP 地址并不会改变。还有一类 target 叫做 NETMAP,只转换 IP 地址,同时拥有 SNAT 与 DNAT 的功能。讨论 Linux kernelNAT 实现的文章不少,比如 iptables 深入解析 - nat 篇,在此不想谈论这些,而是其他的一些东西。
时隔一个半月,继续…
- REDIRECT 只进行端口映射,并不改变 IP 地址。这点可以在源码中看到
// /net/netfilter/nf_nat_redirect.c, function: nf_nat_redirect_ipv4 |
NAT 穿透
NAT 穿透是比较常见的一个问题,在 P2P 中被广泛应用。在了解 NAT 穿透之前需要了解 NAT 的种类,Wikipedia 上面给了很详细的说明。STUN 是一种网络协议,它允许位于 NAT(或多重 NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的 NAT 之后以及 NAT 为某一个本地端口所绑定的 Internet 端端口。这些信息被用来在两个同时处于 NAT 路由器之后的主机之间建立 UDP 通信。该协议由 RFC 5389 定义。UPnP 是由 “通用即插即用论坛” 推广的一套网络协议。该协议的目标是使家庭网络(数据共享、通信和娱乐)和公司网络中的各种设备能够相互无缝连接,并简化相关网络的实现。UPnP 通过定义和发布基于开放、因特网通讯网协议标准的 UPnP 设备控制协议来实现这一目标,也是 NAT 穿透的标准之一。
IPSec 中的 NAT
提起 NAT 的源于一篇 gist 朴素 VPN:一个纯内核级静态隧道,上面提到的东西在这里不提。值得注意的是,IPSec 本身就有 UDP 封装的配置,也有响应的 RFC 规定了如何穿透 NAT,但这里为什么要多此一举呢?简单地说,这在 RFC 有关 IPSec 的地方中有提及。
结束与 08 月 01 日 18 点 38 分,太懒了,不写了。
参考资料
- ss-redir 透明代理
- [TPROXY] implemented IP_RECVORIGDSTADDR socket option
- TProxy - Transparent proxying, again
- qsorix/udp_socket_addr.cc
- 【Linux 内核网络协议栈源码剖析】网络栈主要结构介绍(socket、sock、sk_buff,etc)
- TPROXY 之殇 - NAT 设备加代理的恶果
- Understanding Network Address Translation, NAT
- Network address translation
- RFC 2663: IP Network Address Translator (NAT) Terminology and Considerations
- iptables 深入解析 - nat 篇
- 从 DNAT 到 netfilter 内核子系统,浅谈 Linux 的 Full Cone NAT 实现
- Linux 内核态实现 Full Cone NAT(2)
- NAT 穿透
- STUN
- 朴素 VPN:一个纯内核级静态隧道