type
status
date
slug
summary
tags
category
icon
password
Sub-item
Last edited time
Oct 28, 2023 10:40 AM
Parent item
领域
内网client访问外网server
有一个私有网络10.0.0.0/24,Client A的地址是10.0.0.1;
这个网络的网关(一个NAT设备)的外网IP是155.99.25.11,内网的IP是10.0.0.10;
如果Client A中的某个进程创建了一个UDP Socket,绑定1234端口,想访问外网主机18.181.0.31的1235端口,那么当数据包通过NAT时会发生什么事情呢?
- 首先NAT会改变这个数据包的原IP地址,改为155.99.25.11。
- 接着NAT会为这个连接分配一个端口,比如62000,然后改变这个数据包的源端口为62000;
- 10.0.0.1:1234->18.181.0.31:1235的数据包到了互联网上变为了155.99.25.11:62000->18.181.0.31:1235;
- NAT会记住62000端口对应的是10.0.0.1的1234端口,以后从18.181.0.31发送到62000端口的数据会被NAT自动的转发到10.0.0.1上;
- 其他的IP发送到这个端口的数据将被NAT抛弃,这样Client A就与Server S1建立以了一个连接。
如果Client A的原来那个Socket(绑定了1234端口的那个UDP Socket)又接着向另外一个Server S2发送了一个UDP包,那么这个UDP包在通过NAT时会怎么样呢?这时可能会有两种情况发生:
- 一种是NAT再次分配一个端口号(比如:62001)。
- 另外一种是NAT不会新分配一个端口号,而是用原来分配的端口号62000。
前一种NAT叫做对称型
Symmetric NAT
,后一种叫做锥形Cone NAT
。如果你的NAT刚好是第一种,那么很可能会有很多P2P软件失灵。不过现在绝大多数的NAT属于后者,即Cone NAT。- 对称NAT(不常见):每个局域网的机器向外发数据,如果发送到不同的网关上,都会分配映射不同的IP地址:端口。访问不同外网设备也就说会存在多条(如2条)链路。映射不同的IP地址:端口,相互不会影响。对称NAT的穿透是非常复杂。如大型电信级设备,可能会选择对称NAT。
- 完全锥形NAT(不常见): NAT把所有来自相同内部IP地址和端口的请求映射到相同的外部IP地址和端口。任何一个外部主机均可通过该映射发送IP包到该内部主机,这种情况的基本很少见。
映射关系为:Client->NatIP:NatPort->Any
- 限制锥形NAT(常见):NAT把所有来自相同内部IP地址和端口的请求映射到相同的外部IP地址和端口。但是,只有当内部主机先给IP地址为X的外部主机发送IP包,该外部主机才能向该内部主机发送IP包。
映射关系为:Client-> NatIP:NatPort->A
- 端口限制锥形NAT(常见):端口限制性圆锥与限制性圆锥类似,只是多了端口号的限制,即只有内部主机先向IP地址为X,端口号为P的外部主机发送1个IP包,该外部主机才能够把源端口号为P的IP包发送给该内部主机。
映射关系为:Client->NatIP:NatPort->A:P1
- IP限制锥形NAT(不常见):类似端口限制锥形NAT。
映射关系为:Client->NatIP:NatPort->A:IP
外网访问内网
那么我们如果想从外部发送一个数据报给内网的计算机有什么办法呢?
基本思路是:A 和 B 互相知道对方的公网 IP:Port,使用对方公网 IP:Port 通信。
首先,我们必须在内网的NAT上打上一个“洞”,这个洞不能由外部来打,只能由内网内的主机来打。而且这个洞是有方向的,比如从内部某台主机(比如:192.168.0.10)向外部的某个IP(比如:219.237.60.1)发送一个UDP包,那么就在这个内网的NAT设备上打了一个方向为219.237.60.1的“洞”,这就是称为UDP Hole Punching的技术(使用最广泛)。以后219.237.60.1就可以通过这个洞与内网的192.168.0.10联系了。(但是其他的IP不能利用这个洞)。
- UPnP: 把内网 IP 直接映射为 NAT 外网 IP (纯转发模式),类似公网 IP
- STUN: 比较出名的打洞协议,本质上就是利用一台或多台公网服务器协助位于不同私网的两个节点 A 和 B 进行打洞。
普通的直连式P2P实现
通过上面的理论,实现两个内网的主机通讯就差最后一步了:那就是鸡生蛋还是蛋生鸡的问题了,两边都无法主动发出连接请求,谁也不知道谁的公网地址,那我们如何来打这个洞呢?
我们需要一个中间人来联系这两个内网主机。现在我们来看看一个P2P软件的流程,以下图为例:
- 首先,Client A登录服务器Server S,NAT A分配了一个端口60000,那么Server S收到的Client A的地址是****202.187.45.3:60000,这就是Client A的外网地址了。
- 同样,Client B登录服务器Server S,NAT B分配的端口是40000,那么Server S收到的B的地址是****187.34.1.56:40000。
- 此时,Client A与Client B都可以与Server S通信了。
如果Client A此时想直接发送信息给Client B,那么他可以从Server S那儿获得B的公网地址187.34.1.56:40000,是不是Client A向这个地址发送信息Client B就能收到了呢?
答案是不行,因为如果这样发送信息,NAT B会将这个信息丢弃(因为这样的信息是不请自来的,为了安全,大多数NAT都会执行丢弃动作)。
现在我们需要的是****在NAT B上打一个方向为202.187.45.3(即Client A的外网地址)的洞,那么Client A发送到187.34.1.56:40000的信息,Client B就能收到了。这个打洞命令由谁来发呢?自然是Server S。
总结一下这个过程:
- 如果Client A想向Client B发送信息,那么Client A发送命令给Server S,请求Server S命令Client B向Client A方向打洞。
- 然后Client A就可以通过Client B的外网地址与Client B通信了。
注意:以上过程只适合于
Cone NAT
的情况,如果是Symmetric NAT
,那么当Client B向Client A打洞的端口已经重新分配了,Client B将无法知道这个端口(ps:
如果Symmetric NAT的端口是顺序分配的,那么我们或许可以猜测这个端口号,可是由于可能导致失败的因素太多,这种情况下一般放弃P2P)。实际上,节点 A 和 B 的 NAT 可能是任意一种 NAT 类型
- A 和 B 均是锥形 NAT:可以打洞
- A 和 B 分别是对称型和普通锥形(全锥形,限制型锥形):可以打洞
- A 和 B 分别是对称型和 Port-Restricted Cone NAT (端口限制型):因为限制端口,打洞失败
- A 和 B 都是对称型:端口每次都会变化,打动失败。
A 和 B 均是锥形 NAT
- A、B访问server,serevr记录A、B的公网IP和端口PA1,PB1
- A 向 server 发起与 B 的打洞请求,server 向 B 转发打洞请求
- 同时A 向 PB1 直接发送探测包,那么 A 为 B 在 PA1 已经成功打洞(PB1+端口过来的包将不会被丢弃),但是 A 的消息无法到达,因为 B 的 NAT 会将不明的地址(PA1) 丢弃。(注意:这里有可能不是丢弃,而是拒绝)。
- B 收到从 server 转发过来的打洞请求后,向 PA1 直接发送探测包(PA1+端口过来的包将不会被丢弃),这时 B 的 NAT 可以放行 PA1 的消息了,也就是 B 为 A 在 PB1 上完成了打洞。
- 至此,A 和 B 消息能够互通,打洞成功。
A 和 B 分别是对称型和普通锥形(全锥形,限制型锥形)
- A、B访问server,server记录A、B的公网IP和端口PAx,PB1
- A 向 server 发起与 B 的打洞请求,server 向 B 转发打洞请求,同时发送探测包到 PB1
这个探测包是从 PA2 发出的,不是 PA1(因为对称型)。也就是** A 在端口 PA2 为 PB1 完成打洞**,同时 B 的 NAT 会丢弃来自不明地址 PA2 的包。(注意:这里有可能不是丢弃,而是拒绝)
- B 收到从 server 转发过来的打洞请求,向 PA1 发送初始探测包(一开始不知道 PA2),这个时候 B 已经为 A 在 PB1 打好洞,至此 PA2 的消息能够通过 PB1 到达 B。(注意:因为是普通锥形,不对端口做限制,所以从不同端口 PA2 过来的包能被 B 接受)
- 经过步骤2,B 可以收到 PA2 的消息,同时结合 A 的 NAT 类型**,重新改发探测包到 PA2**,于是 A 在 PA2 能收到 PB1 的探测包,至此 A 和 B 消息可以互通,打洞成功
STUN方式的P2P实现
STUN是RFC5389规定的一种NAT穿透方式,它采用辅助的方法探测NAT的IP和端口。RFC5389中,STUN的全称为Session Traversal Utilities for NAT,即NAT环境下的会话传输工具。STUN是一个C/S架构的协议,支持两种传输类型。
- 一种是请求/响应(request/respond)类型,由客户端给服务器发送请求,并等待服务器返回响应;
- 另一种是指示类型(indication transaction),由服务器或者客户端发送指示,另一方不产生响应。
两种类型的传输都包含一个96位的随机数作为事务ID(transaction ID),对于请求/响应类型,事务ID允许客户端将响应和产生响应的请求连接起来;对于指示类型,事务ID通常作为debugging aid使用。
所有的STUN报文信息都含有一个固定头部,包含了方法,类和事务ID。
方法表示是具体哪一种传输类型(两种传输类型又分了很多具体类型),STUN中只定义了一个方法,即binding(绑定),其他的方法可以由使用者自行拓展;Binding方法可以用于请求/响应类型和指示类型,用于前者时可以用来确定一个NAT给客户端分配的具体绑定,用于后者时可以保持绑定的激活状态。
类表示报文类型是请求/成功响应/错误响应/指示。在固定头部之后是零个或者多个属性(attribute),长度也是不固定的。