详细解读NAT和内网穿透
详细解读NAT和内网穿透
0.前言
可能是因为之前折腾过搭网站之类的事情的原因,我个人对计算机网络比较感兴趣。半年之前试过花生壳的内网穿透服务之后写过一篇文章谈内网穿透的原理,但是最近发给同学看时候他说打不开,才知道被博客园和谐了,趁机回顾再看才发现之前写的内容也有问题(那时候还没有学计算机网络),所以今天又参考了许多资料,力图正确、准确地解读一下NAT和内网穿透,这两个随着ipv6的普及很可能会被淘汰、但是我个人觉得短时间不可能完全淘汰的技术。
1.NAT-从实际案例讲起
用我自己的情况做个例子吧,我的网络情况是:
- 一栋楼有一个电信的公网ip,绑定到一台提供NAT服务的路由器上
- 每层楼有一个交换机,与1中所说的路由器连接
- 交换机使用不同的端口连接每个房间的路由器
- 房间的路由器又组建了设备间的局域网
我来详细描述一下我的设备和一台公网ip下的服务器通信的过程,也算是复习计算机网络了吧,这里假设我的电脑已经知道目标设备的ip地址了(也就是不算DNS部分了):
-
设备发送数据包,源ip是房间路由器创建的内网下的ip,端口号是socket随机选择的,其他的目标ip、源mac都是确定的,目标mac填的是自己路由器的mac地址
设备会用本机配置的24位子网掩码与目标地址进行“与”运算,得出目标地址与本机不是同一网段,因此发送目标的数据包需要经过路由器的转发。 -[4]
-
路由器先把源ip地址改成自己在楼中使用的那个内网ip,为设备分配一个唯一的端口号,并在路由表中记录下这一映射关系。根据路由表中的记录(确定要访问的地址不是楼里的地址,是公网的地址)获取下一跳的ip地址, 并根据ARP协议确定下一跳(也就是每层的交换机)的MAC地址 ,将目标mac地址修改为下一跳的mac地址,然后在数据链路层和网络层中进行传输
-
传到每一层的交换机时,不需要修改源ip地址(因为是交换机,没有修改Ip的功能),只把目标mac改为NAT路由器的地址,然后再通过数据链路层和网络层传输
-
到了NAT服务器,服务器随机为其分配一个端口[3],并在Track Table中保存这个[内网ip:端口号->目标ip:端口号]的映射,这一过程称为连接跟踪。注意此时的内网ip指的是路由器的ip,端口号是路由器为设备分配的端口号
-
服务器收到数据包,再向NAT路由器返回数据包,NAT路由器通过查询Track table确定内网ip(是路由器的内网ip)和端口号,并改变目标Ip为此,返回发送给每层楼的交换机,交换机通过arp协议借助路由器的ip确定路由器的mac地址,并将目标mac地址改为此,再发送给路由器
-
路由器通过端口号确定是哪一台内部设备,转发给该设备
这里对于NAT,关键的一步是4,NAT只会接受在Track table中有ip和端口记录的外部访问,其他的都一概不转发,这也就是我们常说的NAT只能内网访问外网,不能外网访问内网
2.内网穿透
但是不得不说,P2P的需求是真实存在的,为了解决NAT带来的问题,内网穿透诞生了。
这篇文章里比较好地提到了内网穿透的原理,现摘录出来:
假设现在有内网客户端A和内网客户端B,有公网服务端S。
如果A和B想要进行UDP通信,则必须穿透双方的NAT路由。假设为NAT-A和NAT-B。A发送数据包到公网S,B发送数据包到公网S,则S分别得到了A和B的公网IP,
S也和A B 分别建立了会话,由S发到NAT-A的数据包会被NAT-A直接转发给A,
由S发到NAT-B的数据包会被NAT-B直接转发给B,除了S发出的数据包之外的则会被丢弃。
所以:现在A B 都能分别和S进行全双工通讯了,但是A B之间还不能直接通讯。解决办法是:A向B的公网IP发送一个数据包,则NAT-A能接收来自NAT-B的数据包
并转发给A了(即B现在能访问A了);再由S命令B向A的公网IP发送一个数据包,则
NAT-B能接收来自NAT-A的数据包并转发给B了(即A现在能访问B了)。以上就是“打洞”的原理。
为了保证A的路由器有与B的session,A要定时与B做心跳包,同样,B也要定时与A做心跳,这样,双方的通信通道都是通的,就可以进行任意的通信了。
图解如下:
上面说的就是UDP打洞的原理,但是为什么是UDP呢?
UDP的socket允许多个socket绑定到同一个本地端口,而TCP的socket则不允许。
这是这样一个意思:A B要连接到S,肯定首先A B双方都会在本地创建一个socket,
去连接S上的socket。创建一个socket必然会绑定一个本地端口(就算应用程序里面没写
端口,实际上也是绑定了的,至少java确实如此),假设为8888,这样A和B才分别建立了到
S的通信信道。接下来就需要打洞了,打洞则需要A和B分别发送数据包到对方的公网IP。但是
问题就在这里:因为NAT设备是根据端口号来确定session,如果是UDP的socket,A B可以
分别再创建socket,然后将socket绑定到8888,这样打洞就成功了。但是如果是TCP的
socket,则不能再创建socket并绑定到8888了,这样打洞就无法成功。
道理的确是这么个道理,但是博主说的还不够清楚,我再解读下,就用上面原博主给的例子了:
由于NAT的外部端口是随机指定的,如果A和B分别和服务器通信,都使用8888端口的话,如果A要和B直接打洞且不用8888端口,就会遇到一个问题:A不知道B将来包发出来NAT-B会给它分配什么接口,所以A就没办法指定目标Ip的端口号(因为NAT-B是随机分配端口的,B即使知道了A用了哪个端口打洞也没办法让NAT-B去使用这个特定的端口。综上,A和B能使用的,只有和服务器连接时已经创建在track table中的那个端口,也就是我们例子中的8888了。
注意track table中的记录是有有效期的,由于我们不知道外部设备什么时候会访问我们在内网中的设备,所以我们需要保证设备和服务器之间的连接不能断开,track table中的映射不能被销毁,所以需要在一定的时间间隔之后发包来维持NAT中的映射关系,这就是为什么我们用花生壳的时候它一直要求我们“保持在线”的原因了
但是TCP也不是不能进行穿透,这就需要用到端口重用了:
tcp打洞也需要NAT设备支持才行。
tcp的打洞流程和udp的基本一样,但tcp的api决定了tcp打洞的实现过程和udp不一样。
tcp按cs方式工作,一个端口只能用来connect或listen,所以需要使用端口重用,才能利用本地nat的端口映射关系。(设置SO_REUSEADDR,在支持SO_REUSEPORT的系统上,要设置这两个参数。)连接过程:(以udp打洞的第2种情况为例(典型情况))
nat后的两个peer,A和B,A和B都bind自己listen的端口,向对方发起连接(connect),即使用相同的端口同时连接和等待连接。因为A和B发出连接的顺序有时间差,假设A的syn包到达B的nat时,B的syn包还没有发出,那么B的nat映射还没有建立,会导致A的连接请求失败(连接失败或无法连接,如果nat返回RST或者icmp差错,api上可能表现为被RST;有些nat不返回信息直接丢弃syn包(反而更好)),(应用程序发现失败时,不能关闭socket,closesocket()可能会导致NAT删除端口映射;隔一段时间(1-2s)后未连接还要继续尝试);但后发B的syn包在到达A的nat时,由于A的nat已经建立的映射关系,B的syn包会通过A的nat,被nat转给A的listen端口,从而进去三次握手,完成tcp连接。
另外,NAT还有许多其他功能,更详细的介绍可以看这篇文章