和同事讨论UDP打洞技术,后做了一个简单的实验,由于Windows上设置NAT以及察看其原理太麻烦或者根本就不可能,于是还是使用Linux做了实验,发现基于Linux ip_conntrack这种对称NAT也能很简单的实现UDP穿越,实验很简单,并且这种UDP穿越还不需要公网服务器的协助(因为它们对于对称NAT或者基于连接的NAT根本帮不上什么忙),很实用。在展示实验之前,首先要明白以下的知识点。
1.Linux的NAT是基于ip_conntrack的
这一点说明仅仅针对五元组决定的一个连接的第一个数据包进行NAT规则的查找和匹配并存入ip_conntrack结构体,后续的包自动应用ip_conntrack结构体中的NAT信息进行NAT。
由于五元组标示了一个连接,因此:
1).即使是同一个内网主机发起连接,最终很大程度上也不一定能被转换成同一个(IP地址,协议端口)对
2).目的地址不同,就不是同一个连接,因此绝不可能做到像cone NAT那样的偷梁换柱的效果
2.Linux目前还没有实现cone NAT的模块
Linux的NAT是处于Netfilter模块中的,标准协议栈中没有任何相关支持,那么只要Netfilter不支持cone NAT,Linux NAT就别指望能被常规方式打洞。
3.Linux的NAT尽力不改变连接的源端口
Linux的NAT是通过iptables工具配置的,对于隐藏内网主机这种类型的NAT是iptables的SNAT实现的,如果你man iptables将会发现:
--to-source [ipaddr[-ipaddr]][:port[-port]]
which can specify a single new source IP address, an inclusive range of IP addresses, and optionally, a port range (which is only valid if the rule also specifies -p tcp or -p udp). If no port range is specified, then source ports below 512 will be mapped to other ports below 512: those between 512 and 1023 inclusive will be mapped to ports below 1024, and other ports will be mapped to 1024 or above. Where possible, no port alteration will occur.
注意最后一句,Linux尽力不改变连接的源端口,除非和另一个tuple相冲突,我们知道一个tuple就是一个五元组。
这一点正是给了我们一点启示,那就是可以通过尝试不同端口,直到找到没有被改变源端口的那次连接。而印证这一点也很容易,那就是打洞成功。这也从反方面说明没有必要存在外部服务器了,因为它根本帮不上什么忙。
4.TCP很难穿越Linux这种对称NAT
这是为什么呢?起初,我以为可以采用伪造ACK包的方式进行穿越,然而实验没有成功,后来看了源码发现ip_conntrack对TCP的状态机,序列号范围进行了严格的审查,凡是不通过的...怎样呢?ip_conntrack没有权力丢弃它,而是直接return ACCEPT了,这下就根本别指望能购匹配到任何既有的conntrack了,比如以下的场景:
A-模拟公网主机10.16.0.1;
N-模拟NAT主机10.16.0.254;
B-N后面的主机;
最终目标:A连接B
配置:A上丢弃到来的TCP 6667数据包
行为:
1).B绑定端口6667连接A的端口6667
2).连接顺利通过N,在N上留下了:
tcp 6 52 SYN_SENT src=172.16.0.35 dst=10.16.0.1 sport=6667 dport=6667 packets=3 bytes=180 [UNREPLIED] src=10.16.0.1 dst=10.16.0.254 sport=6667 dport=6667 packets=0 bytes=0 mark=0 secmark=0 use=1
一条UNREPLIED的conntrack
3).此时B反客为主,停掉客户端程序,启动监听同一端口的服务器;
4).A以端口6667连接N的6667
5).SYN数据包到达N,被N发回了reset,实验失败。
这个原理很简单,如果看一下ip_conntrack_in这个ip_conntrack的入口函数,将会发现一个下面的逻辑:
ret = proto->packet(ct, *pskb, ctinfo);
if (ret < 0) {
/* Invalid: inverse of the return code tells
* the netfilter core what to do*/
nf_conntrack_put((*pskb)->nfct);
(*pskb)->nfct = NULL;
CONNTRACK_STAT_INC(invalid);
//如果发现状态或者序列号有问题,直接返回,跳出Netfilter的当前HOOK点
return -ret;
}
...
if (set_reply)
set_bit(IPS_SEEN_REPLY_BIT, &ct->status);
packet回调函数实际上就是tcp_packet函数,里面有复杂的状态机检查,序列号检查等逻辑,因此出错后直接返回,进而即使找到了与之相匹配的/proc/net/ip_conntrack文件中的连接,由于无法继续运行下面的逻辑,所以始终不会将对应连接的UNREPLIED字眼抹去,数据包继续进入协议栈的上层,最终发往N本地,由于N没有监听6667,因此reset。
Linux NAT的这种行为说明很难用常规的方式去穿越.
实验简述
理解了上述的知识点之后,如何进行实验就很简单了,我们只需要3台机器来模拟,拓扑场景和上述4中的一模一样,只是将TCP换成了UDP,很容易就成功了,如果不成功怎么办呢?如果不成功一定是因为在NAT的时候源端口被改掉了,那么我们只需要再多试几个端口,直到找到那个不被修改掉的端口为止。对于TCP,由于还没有找到什么方法,只有放弃了...
如果希望验证两台同在NAT后面的机器的连通性,原理一样,由于没有那么多机器,作罢了。
注解:关于cone NAT
cone NAT实际上是一种主要以“节约IP地址”为目标的NAT设备,它们并没有维护各个连接的状态,也不采取任何安全策略,是双向的,对于full cone设备,一台内部主机会被映射为一个特定的地址,端口对,而不管它访问什么目标,从任何目标到达full cone NAT设备的映射后端口的都能被该NAT设备转发至内网。
也就是说,所谓的cone NAT正如其名字一样,是一把锥子形状的,从NAT设备向远方辐散开来,一个IP地址,端口对对应多个目标地址,端口对。很显然,这种NAT肯定不是基于连接实现的,而是基于“每包”来实现的。为了提高安全性,还是需要使用对称的NAT或者不管它是什么类型的NAT,只要是基于连接来实现的就行,正如Linux的实现一样,如果以地址,端口对来看,它确实有时是锥子形的,然而如果以连接session来看,它就是对称的,这种NAT的穿越是很难的。