前言
限于笔者技术水平与精力有限,此处就不涵盖面向Windows用户的说明了。对于MAC用户以及*nix以及BSD用户来说,注意需要补齐的软件依赖后应该可以参考本文。
本文也在一定程度上尝试研究NAT为何物。可能有错误或未能完全阐释的细节。其思维方式也与RFC不同。其对NAT类型的划分主要参考了UDP。对于TCP的讨论很可能漏洞百出。
本文暂未涉及ICMP协议于NAT的表现。
本文所涉及的IP
一律指代IPv4
。
本文涉及一些socket编程的知识。
看不懂本文的一些表述则可以去看看RFC4787的原文。
参阅
关键概念
- NAT于TCP和UDP具有不同的表现
- NAT于传出包和传入包可能具有不同的表现(mapping与filtering)
因此测试将需要进行4次。
详细概念解释
IP包与TCP、UDP
TCP和UDP都从属于IP协议的子类,其数据包共性为:继承了IP头的源地址(src_addr)与目的地址(dest_addr),且都具有源端口(src_port)与目的端口(dest_port)
因此,本文为了方便起见,将混淆IP与TCP、UDP的界限,将源地址与源端口、目的地址与目的端口绑在一起讨论。
UDP
在编程角度而言,发送方需要指定目的地址与目的端口,也可以通过bind()
指定源地址和源端口,不指定则会由系统分配一个空闲端口,并依据路由表、网络接口的IP地址来确定使用一个合适的源地址。
因此,每一个UDP包都包含地址四元组信息。NAT依据该信息对UDP包进行分拣。
TCP
在编程角度而言,需要分步分条件提供该地址四元组。
对于使用connect()
建立的传出连接,向外发送的包的目的地址和端口由程序员提供,而源地址和端口由操作系统(IP协议栈)提供。
传出连接会接受的从外边接收的包的源地址和端口是connect时提供的,而目的地址则是由操作系统(IP协议栈)为你选择的。
编程时仅需提供对方的端口与地址。
对于TCP server,想要接收的包的目的地址和端口由程序员提供。源端口和源地址是IP包里带着的。写代码时可能会使用0.0.0.0
或者127.0.0.1
,这就是传入连接的过滤规则。如果这里提供一个不属于你的设备上的任何一个网络接口的IP,那么将没有TCP连接可以连上你的socket server。
而发送出去的包的目的地址和端口是建立连接时收到的握手包头提供的,源端口和地址是bind()
时提供的。如果地址是0.0.0.0
那么则由操作系统(IP协议栈)负责选一个能用的。端口在bind时是必须提供的。
编程时至少需要提供本地的监听端口。监听的地址也需要提供但是可以也经常被忽略。可以拿到对方的IP和端口。
因此,每一个TCP包都包含地址四元组信息。NAT依据该信息对TCP包进行分拣。当然因为TCP有状态,因此除了简单分拣以外还有其它更加复杂的策略。比如TCP也需要借助ICMP包进行容错,因此对TCP进行NAT还会牵扯到ICMP的NAT表现。不进行深入讨论(不懂)。
虽然TCP编程时经常不提供完整四元组,但是TCP封装成数据包后,包还是带着完整四元组的。
传出数据包(mapping)
TCP和UDP最终都可以视为是四元组数据包在网络中跑,因此这里将不再区分TCP和UDP,转而站在packet角度进行解说当前栏目与下一个栏目。
传出数据包则是指源地址端口是自己的包。其经过NAT时的表现称之为mapping behavior。即NAT后面的包的源信息在经过NAT后将会被NAT修改成什么。目的信息不会被修改。(为什么要改)
改源信息,是因为过了NAT后,包里原本的源信息已经在网络上不可达了(它位于NAT后的子网里)。把不可达的源换成NAT自己的源信息,这个包的回复包才能够用NAT的源信息给传回来。这里就是地址转换的作用。
RFC里划定的分类标准:
- Endpoint-Independent Mapping:从里面建立到外边的映射时,只要改之前的源信息是一样的,无论目的信息,都保证改之后的源信息一样
- Address-Dependent Mapping:从里面建立到外边的映射时是区分目的地址的(但不区分端口),发给不同地址的源信息是不同的(这里不同的应该只有源端口,毕竟源地址没得改)
- Address and Port-Dependent Mapping:从里面建立到外边的映射时是区分目的地址和端口。发给不同目的信息所使用的修改后的源信息都不一样。
传入数据包(filtering)
传入数据包指目的端口和地址是自己的包。其经过NAT时的表现称为filtering。即从外边来到NAT的包,目的信息在经过NAT后会被修改成什么,再丢进NAT里边。源信息不会被修改。(为什么要改)
改目的信息,是为了让这个包还能以原生IP协议递交给NAT后边的设备。目的信息改成什么?通常都是由NAT的实现来负责决定,比如记录下传出包的映射,在收到传入包时让它原路回去。因此基本上想要在NAT后收包,都必须先发包出去。例外的也有,比如DNAT(也就是路由器的端口转发功能),但是是需要手动配置的。
RFC里划定的分类标准:
- Endpoint-Independent Filtering:外面有包到达时,只根据包修改前的目的信息来判定修改后的目的信息。
- Address-Dependent Filtering:外面有包到达时,多检查一项源地址。
- Address and Port-Dependent Filtering:再把源端口也检查了。
通常检查策略就是查表。传出包时会给表里增加一项,并在一定时间后删除。如果传入的包的四元组信息符合表里某一项,就按照查出来的结果修改目的信息,发回NAT后。
测试目的
- 确定自己的测试设备所在网络所穿越的层层NAT的映射(mapping)与过滤(filtering)行为
需要上报的测试结果
- traceroute到
stun.stunprotocol.org
的前5条输出 - TCP/UDP分别的mapping和filtering结果,共四项
- 如果自己可以控制自己网络的NAT环境,则提交控制前后对比的上条四项
测试方法流程
连通性测试
traceroute stun.stunprotocol.org
在打印出第一个公网节点后即可停止测试。记录并上报结果。
我找到的最靠谱的一个服务器。如果连不上的话就很不幸。
安装测试软件
选择使用stunserver。
自行根据readme编译。记得仔细看,并补依赖。
在源码根目录make完毕后,cd进入client
目录。ls看看stunclient
是不是已经编译出来了。
TCP 传出测试
./stunclient --protocol tcp --mode behavior stun.stunprotocol.org 3478
TCP 传入测试
./stunclient --protocol tcp --mode filtering stun.stunprotocol.org 3478
UDP 传出测试
./stunclient --protocol udp --mode behavior stun.stunprotocol.org 3478
UDP 传入测试
./stunclient --protocol udp --mode filtering stun.stunprotocol.org 3478
Tips
- 如果好奇NAT类型是如何判定的,那么你可以在理解了本文上述内容后阅读一下
stunclient
和stunserver
的help输出。着重理解为什么需要在服务器上有两个IP地址。顺带协会在校园网内也搭了一个stunserver,可以拿来在受控网络环境下做实验。 - 协会路由器,以及我的教程提供的OpenWRT的fullcone补丁,只处理了UDP的case,TCP上不生效。在楼下我的测试结果中有所体现。
- 运营商超管设置光猫的fullcone nat选项,大概率也是用的这个补丁(明白我的意思吧
关于多层NAT
涉及传出和传入的不同作用影响。
传出时源信息会被修改,每一级NAT根据不同的规则决定是否将源信息按目的信息进行区分,最终探测到NAT的mapping行为是受每一级NAT的AND式综合影响。
传入时目的信息会被修改,但只要NAT映射链没断,第一级被放行进来,就一定会到达最后的目标设备。决定filtering行为的只有最外层NAT。这里的最外层是一种相对的说法。