引子
今年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
:
1 | static int |
在内核源码中搜了下有关SO_ORIGINAL_DST
的东西,看到了getorigdst:
1 | /* 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的设置是以下三条命令:
1 | # iptables -t mangle -A PREROUTING -p udp -j TPROXY --tproxy-mark 0x2333/0x2333 --on-ip 127.0.0.1 --on-port 1080 |
获取原本的dst和port的相关源码如下:
1 | 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信息找到的:
1 | // /net/netfilter/xt_TPROXY.c |
sock是根据四元组saddr, sport, daddr, dport来选择的,其中saddr与sport来自skb_buff,另外俩为target所定义。没搞懂的地方在于:在ip_rcv_finish
中,是怎样将数据包投递到上层协议以及指定端口的?
目前的猜测如下:
1 | // 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
计算的得分来选择的,分高者赢。
1 | // 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地址。这点可以在源码中看到
1 | // /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:一个纯内核级静态隧道