一、引言
1.1 背景:IPv4地址短缺,引入NAT
全球IPv4地址早已不够用,因此人们发明了NAT(网络地址转换)来缓解这个问题。
简单来说,大部分机器都使用私有IP地址,如果它们需要访问公网服务,那么,
- 出向流量:需要经过一台NAT设备,它会对流量进行SNAT,将私有srcIP+Port转换成NAT设备的公网IP+Port(这样应答包才能回来),然后再将包发出去;
- 应答流量(入向):到达NAT设备后进行相反的转换,然后再转发给客户端。
整个过程对双方透明。
更多关于NAT的内容,可参考(译)NAT-网络地址转换(2016)。译注。
以上就是本文所讨论的问题的基本背景。
1.2 需求:两台经过NAT的机器建立点对点连接
在以上所描述的NAT背景下,我们从最简单的问题开始:如何在两台经过NAT的机器之间建立点对点连接(直连)。如下图所示:
直接用机器的IP互连显然是不行的,因为它们都是私有IP(例如192.168.1.x)。在Tailscale中,我们会建立一个WireGuard®隧道来解决这个问题——但这并不是太重要,因为我们将过去几代人努力都整合到了一个工具集,这些技术广泛适用于各种场景。例如,
- WebRTC使用这些技术在浏览器之间完成peer-to-peer语音、视频和数据传输,
- VoIP电话和一些视频游戏也使用类似机制,虽然不是所有情况下都很成功。
接下来,本文将在一般意义上讨论这些技术,并在合适的地方拿Tailscale和其他一些东西作为例子。
1.3 方案:NAT穿透
1.3.1 两个必备前提:UDP+能直接控制socket
如果想设计自己的协议来实现NAT穿透,那必须满足以下两个条件:
- 协议应该基于UDP。
理论上用TCP也能实现,但它会给本已相当复杂的问题再增加一层复杂性,甚至还需要定制化内核——取决于你想实现到什么程度。本文接下来都将关注在UDP上。
如果考虑TCP是想在NAT穿透时获得面向流的连接(stream-orientedconnection),可以考虑用QUIC来替代,它构建在UDP之上,因此我们能将关注点放在UDPNAT穿透,而仍然能获得一个很好的流协议(streamprotocol)。
- 对收发包的socket有直接控制权。
例如,从经验上来说,无法基于某个现有的网络库实现NAT穿透,因为我们必须在使用的“主要”协议之外,发送和接收额外的数据包。
某些协议(例如WebRTC)将NAT穿透与其他部分紧密集成。但如果你在构建自己的协议,建议将NAT穿透作为一个独立实体,与主协议并行运行,二者仅仅是共享socket的关系,如下图所示,这将带来很大帮助:
1.3.2 保底方式:中继
在某些场景中,直接访问socket这一条件可能很难满足。
退而求其次的一个方式是设置一个localproxy(本地代理),主协议与这个proxy通信,后者来完成NAT穿透,将包中继(relay)给对端。这种方式增加了一个额外的间接成本,但好处是:
- 仍然能获得NAT穿透,
- 不需要对已有的应用程序做任何改动。
1.4 挑战:有状态防火墙和NAT设备
有了以上铺垫,下面就从最基本的原则开始,一步步看如何实现一个企业级的NAT穿透方案。
我们的目标是:在两个设备之间通过UDP实现双向通信,有了这个基础,上层的其他协议(WireGuard,QUIC,WebRTC等)就能做一些更酷的事情。
但即便这个看似最基本的功能,在实现上也要解决两个障碍:
- 有状态防火墙
- NAT设备
二、穿透防火墙
有状态防火墙是以上两个问题中相对比较容易解决的。实际上,大部分NAT设备都自带了一个有状态防火墙,因此要解决第二个问题,必须先解决有第一个问题。
有状态防火墙具体有很多种类型,有些你可能见过:
- WindowsDefenderfirewall
- Ubuntu’sufw(usingiptables/nftables)
- BSD/macOSpf
- AWSSecurityGroups(安全组)
2.1 有状态防火墙
2.1.1 默认行为(策略)
以上防火墙的配置都是很灵活的,但大部分配置默认都是如下行为:
- 允许所有出向连接(allowsall“outbound”connections)
- 禁止所有入向连接(blocksall“inbound”connections)
可能有少量例外规则,例如allowinginboundSSH。
2.1.2 如何区分入向和出向包
连接(connection)和方向(direction)都是协议设计者头脑中的概念,到了物理传输层,每个连接都是双向的;允许所有的宝物双向传输。那防火墙是如何区分哪些是入向包、哪些是出向包的呢?这就要回到“有状态”(stateful)这三个字了:有状态防火墙会记录它看到的每个包,当收到下一个包时,会利用这些信息(状态)来判断应该做什么。
对UDP来说,规则很简单:如果防火墙之前看到过一个出向包(outbound),就会允许相应的入向包(inbound)通过,以下图为例:
笔记本电脑中自带了一个防火墙,当该防火墙看到从这台机器出去的2.2.2.2:1234->5.5.5.5:5678包时,就会记录一下:5.5.5.5:5678->2.2.2.2:1234入向包应该放行。这里的逻辑是:我们信任的世界(即笔记本)想主动与5.5.5.5:5678通信,因此应该放行(allow)其回报路径。
某些非常宽松的防火墙只要看到有从2.2.2.2:1234出去的包,就会允许所有从外部进入2.2.2.2:1234的流量。这种防火墙对我们的NAT穿透来说非常友好,但已经越来越少见了。
2.2 防火墙朝向(face-off)与穿透方案
2.2.1 防火墙朝向相同
场景特点:服务端IP可直接访问
在NAT穿透场景中,以上默认规则对UDP流量的影响不大——只要路径上所有防火墙的“朝向”是一样的。一般来说,从内网访问公网上的某个服务器都属于这种情况。
我们唯一的要求是:连接必须是由防火墙后面的机器发起的。这是因为在它主动和别人通信之前,没人能主动和它通信,如下图所示:
穿透方案:客户端直连服务端,或hub-and-spoke拓扑
但上图是假设了通信双方中,其中一端(服务端)是能直接访问到的。在VPN场景中,这就形成了所谓的hub-and-spoke拓扑:中心的hub没有任何防火墙策略,谁都能访问到;防火墙后面的spokes连接到hub。如下图所示:
2.2.2 防火墙朝向不同的方向
场景特点:服务端IP不可直接访问
但如果两个“客户端”相连,以上方式就不行了,此时两边的防火墙相向而立,如下图所示:
根据前面的讨论,这种情况意味着:两边要同时发起连接请求,但也意味着两边都无法发起有效请求,因为对方先发起请求才能在它的防火墙上打开一条缝让我们进去!如何破解这个问题呢?一种方式是让用户重新配置一边或两边的防火墙,打开一个端口,允许对方的流量进来。
- 这显然对用户不友好,在像Tailscale这样的mesh网络中的扩展性也不好,在mesh网络中,我们假设对端会以一定的粒度在公网上移动。
- 此外,在很多情况下用户也没有防火墙的控制权限:例如在咖啡馆或机场中,连接的路由器是不受你控制的(否则你可能就有麻烦了)。
因此,我们需要寻找一种不用重新配置防火墙的方式。
穿透方案:两边同时主动建连,在本地防火墙为对方打开一个洞
解决的思路还是先重新审视前面提到的有状态防火墙规则:
- 对于UDP,其规则(逻辑)是:包必须先出去才能进来(packetsmustflowoutbeforepacketscanflowbackin)。
- 注意,这里除了要满足包的IP和端口要匹配这一条件之外,并没有要求包必须是相关的(related)。换句话说,只要某些包带着正确的来源和目的地址出去了,任何看起来像是响应的包都会被防火墙放进来——即使对端根本没收到你发出去的包。
因此,要穿透这些有状态防火墙,我们只需要共享一些信息:让两端提前知道对方使用的ip:port:
- 手动静态配置是一种方式,但显然扩展性不好;
- 我们开发了一个coordinationserver,以灵活、安全的方式来同步ip:port信息。
有了对方的ip:port信息之后,两端开始给对方发送UDP包。在这个过程中,我们预料到某些宝物将会被丢弃。因此,双方必须要接受某些包会丢失的事实,因此如果是重要信息,你必须自己准备好重传。对UDP来说丢包是可接受的,但这里尤其需要接受。
来看一下具体建连(穿透)过程:
- 如图所示,笔记本出去的第一包,2.2.2.2:1234->7.7.7.7:5678,穿过WindowsDefender防火墙进入到公网。
对方的防火墙会将这个包拦截掉,因为它没有7.7.7.7:5678->2.2.2.2:1234的流量记录。但另一方面,WindowsDefender此时已经记录了出向连接,因此会允许7.7.7.7:5678->2.2.2.2:1234的应答包进来。
- 接着,第一个7.7.7.7:5678->2.2.2.2:1234穿过它自己的防火墙到达公网。
到达客户端侧时,WindowsDefender认为这是刚才出向包的应答包,因此就放行它进入了!此外,右侧的防火墙此时也记录了:2.2.2.2:1234->7.7.7.7:5678的包应该放行。
- 笔记本收到服务器发来的包之后,发送一个包作为应答。这个包穿过WindowsDefender防火墙和服务端防火墙(因为这是对服务端发送的包的应答包),达到服务端。
成功!这样我们就建立了一个穿透两个相向防火墙的双向通信连接。而初看之下,这项任务似乎是不可能完成的。
2.3 关于穿透防火墙的一些思考
穿透防火墙并非永远这么轻松,有时会受一些第三方系统的间接影响,需要仔细处理。那穿透防火墙需要注意什么呢?重要的一点是:通信双方必须几乎同时发起通信,这样才能在路径上的防火墙打开一条缝,而且两端还都是活着的。
2.3.1 双向主动建连:旁路信道
如何实现“同时”呢?一种方式是两端不断重试,但显然这种方式很浪费资源。假如双方都知道何时开始建连就好了。
- 这听上去是鸡生蛋蛋生鸡的问题了:双方想要通信,必须先提前通个信。
- 但实际上,我们可以通过旁路信道(sidechannel)来达到这个目的,并且这个旁路信道并不需要很fancy:它可以有几秒钟的延迟、只需要传送几KB的信息,因此即使是一个配置非常低的虚拟机,也能为几千台机器提供这样的旁路通信服务。在遥远的过去,我曾用XMPP聊天消息作为旁路,效果非常不错。另一个例子是WebRTC,它需要你提供一个自己的“信令信道”(signallingchannel,这个词也暗示了WebRTC的IPtelephonyancestry),并将其配置到WebRTCAPI。在Tailscale,我们的协调服务器(coordinationserver)和DERP(DetourEncryptedRoutingProtocol)服务器集群是我们的旁路信道。
2.3.2 非活跃连接被防火墙清理
有状态防火墙内存通常比较有限,因此会定期清理不活跃的连接(UDP常见的是30s),因此要保持连接alive的话需要定期通信,否则就会被防火墙关闭,为避免这个问题,我们,
- 要么定期向对方发包来keepalive,
- 要么有某种带外方式来按需重建连接。
2.3.3 问题都解决了?不,挑战刚刚开始
对于防火墙穿透来说,我们并不需要关心路径上有几堵墙——只要它们是有状态防火墙且允许出向连接,这种同时发包(simultaneoustransmission)机制就能穿透任意多层防火墙。这一点对我们来说非常友好,因为只需要实现一个逻辑,然后能适用于任何地方了。
…对吗?
其实,不完全对。这个机制有效的前提是:我们能提前知道对方的ip:port。而这就涉及到了我们今天的主题:NAT,它会使前面我们刚获得的一点满足感顿时消失。
下面,进入本文正题。
三、NAT的本质
3.1 NAT设备与有状态防火墙
可以认为NAT设备是一个增强版的有状态防火墙,虽然它的增强功能对于本文场景来说并不受欢迎:除了前面提到的有状态拦截/放行功能之外,它们还会在数据包经过时修改这些包。
3.2 NAT穿透与SNAT/DNAT
具体来说,NAT设备能完成某种类型的网络地址转换,例如,替换源或目的IP地址或端口。
- 讨论连接问题和NAT穿透问题时,我们只会受sourceNAT——SNAT的影响。
- DNAT不会影响NAT穿透。
3.3 SNAT的意义:解决IPv4地址短缺问题
SNAT最常见的使用场景是将很多设备连接到公网,而只使用少数几个公网IP。例如对于消费级路由器,会将所有设备的(私有)IP地址映射为单个连接到公网的IP地址。
这种方式存在的意义是:我们有远多于可用公网IP数量的设备需要连接到公网,(至少对IPv4来说如此,IPv6的情况后面会讨论)。NAT使多个设备能共享同一IP地址,因此即使面临IPv4地址短缺的问题,我们仍然能不断扩张互联网的规模。
3.4 SNAT过程:以家用路由器为例
假设你的笔记本连接到家里的WiFi,下面看一下它连接到公网某个服务器时的情形:
- 笔记本发送UDPpacket192.168.0.20:1234->7.7.7.7:5678。
这一步就好像笔记本有一个公网IP一样,但源地址192.168.0.20是私有地址,只能出现在私有网络,公网不认,收到这样的包时它不知道如何应答。
- 家用路由器出场,执行SNAT。
包经过路由器时,路由器发现这是一个它没有见过的新会话(session)。它知道192.168.0.20是私有IP,公网无法给这样的地址回包,但它有办法解决:
- 在它自己的公网IP上挑一个可用的UDP端口,例如2.2.2.2:4242,然后创建一个NATmapping:192.168.0.20:1234<-->2.2.2.2:4242,然后将包发到公网,此时源地址变成了2.2.2.2:4242而不是原来的192.168.0.20:1234。因此服务端看到的是转换之后地址,接下来,每个能匹配到这条映射规则的包,都会被路由器改写IP和端口。
- 反向路径是类似的,路由器会执行相反的地址转换,将2.2.2.2:4242变回192.168.0.20:1234。对于笔记本来说,它根本感知不知道这正反两次变换过程。
这里是拿家用路由器作为例子,但办公网的原理是一样的。不同之处在于,办公网的NAT可能有多台设备组成(高可用、容量等目的),而且它们有不止一个公网IP地址可用,因此在选择可用的公网ip:port来做映射时,选择空间更大,能支持更多客户端。
3.5 SNAT给穿透带来的挑战
现在我们遇到了与前面有状态防火墙类似的情况,但这次是NAT设备:通信双方不知道对方的ip:port是什么,因此无法主动建连,如下图所示:
但这次比有状态防火墙更糟糕,严格来说,在双方发包之前,根本无法确定(自己及对方的)ip:port信息,因为只有出向包经过路由器之后才会产生NATmapping(即,可以被对方连接的ip:port信息)。
因此我们又回到了与防火墙遇到的问题,并且情况更糟糕:双方都需要主动和对方建连,但又不知道对方的公网地址是多少,只有当对方先说话之后,我们才能拿到它的地址信息。
如何破解以上死锁呢?这就轮到STUN登场了。
四、穿透“NAT+防火墙”:STUN(SessionTraversalUtilitiesforNAT)协议
STUN既是一些对NAT设备行为的详细研究,也是一种协助NAT穿透的协议。本文主要关注STUN协议。
4.1 STUN原理
STUN基于一个简单的观察:从一个会被NAT的客户端访问公网服务器时,服务器看到的是NAT设备的公网ip:port地址,而非该客户端的局域网ip:port地址。
也就是说,服务器能告诉客户端它看到的客户端的ip:port是什么。因此,只要将这个信息以某种方式告诉通信对端(peer),后者就知道该和哪个地址建连了!这样就又简化为前面的防火墙穿透问题了。
本质上这就是STUN协议的工作原理,如下图所示:
- 笔记本向STUN服务器发送一个请求:“从你的角度看,我的地址什么?”
- STUN服务器返回一个响应:“我看到你的UDP包是从这个地址来的:ip:port”。
4.2 为什么NAT穿透逻辑和主协议要共享同一个socket
理解了STUN原理,也就能理解为什么我们在文章开头说,如果要实现自己的NAT穿透逻辑和主协议,就必须让二者共享同一个socket:
- 每个socket在NAT设备上都对应一个映射关系(私网地址->公网地址),
- STUN服务器只是辅助穿透的基础设施,
- 与STUN服务器通信之后,在NAT及防火墙设备上打开了一个连接,允许入向包进来(回忆前面内容,只要目的地址对,UDP包就能进来,不管这些包是不是从STUN服务器来的),
- 因此,接下来只要将这个地址告诉我们的通信对端(peer),让它往这个地址发包,就能实现穿透了。
4.3 STUN的问题:不能穿透所有NAT设备(例如企业级NAT网关)
有了STUN,我们的穿透目的似乎已经实现了:每台机器都通过STUN来获取自己的私网socket对应的公网ip:port,然后把这个信息告诉对端,然后两端同时发起穿透防火墙的尝试,后面的过程就和上一节介绍的防火墙穿透一样了,对吗?
答案是:看情况。某些情况下确实如此,但有些情况下却不行。通常来说,
- 对于大部分家用路由器场景,这种方式是没问题的;
- 但对于一些企业级NAT网关来说,这种方式无法奏效。
NAT设备的说明书上越强调它的安全性,STUN方式失败的可能性就越高。(但注意,从实际意义上来说,NAT设备在任何方面都并不会增强网络的安全性,但这不是本文重点,因此不展开。)
4.4 重新审视STUN的前提
再次审视前面关于STUN的假设:当STUN服务器告诉客户端在公网看来它的地址是2.2.2.2:4242时,那所有目的地址是2.2.2.2:4242的包就都能穿透防火墙到达该客户端。
这也正是问题所在:这一点并不总是成立。
- 某些NAT设备的行为与我们假设的一致,它们的有状态防火墙组件只要看到有客户端自己发起的出向包,就会允许相应的入向包进入;因此只要利用STUN功能,再加上两端同时发起防火墙穿透,就能把连接打通;
- 另外一些NAT设备就要困难很多了,它会针对每个目的地址来生成一条相应的映射关系。在这样的设备上,如果我们用相同的socket来分别发送数据包到5.5.5.5:1234and7.7.7.7:2345,我们就会得到2.2.2.2上的两个不同的端口,每个目的地址对应一个。如果反向包的端口用的不对,包就无法通过防火墙。如下图所示:
五、中场补课:NAT正式术语
知道NAT设备的行为并不是完全一样之后,我们来引入一些正式术语。
5.1 早期术语
如果之前接触过NAT穿透,可能会听说过下面这些名词:
- “FullCone”
- “RestrictedCone”
- “Port-RestrictedCone”
- “Symmetric”NATs
这些都是NAT穿透领域的早期术语。
但其实这些术语相当让人困惑。我每次都要查一下RestrictedConeNAT是什么意思。从实际经验来看,我并不是唯一对此感到困惑的人。例如,如今互联网上将“easy”NAT归类为FullCone,而实际上它们更应该归类为Port-RestrictedCone。
5.2 近期研究与新术语
最近的一些研究和RFC已经提出了一些更准确的术语。
- 首先,它们明确了如下事实:NAT设备的行为差异表现在多个维度,而并非只有早期研究中所说的“cone”这一个维度,因此基于“cone”来划分类别并不是很有帮助。
- 其次,新研究和新术语能更准确地描述NAT在做什么。
前面提到的所谓"easy"和"hard"NAT,只在一个维度有不同:NAT映射是否考虑到目的地址信息。RFC4787中,
- 将easyNAT及其变种称为“Endpoint-IndependentMapping”(EIM,终点无关的映射)
但是,从“命名很难”这一程序员界的伟大传统来说,EIM这个词其实也并不是100%准确,因为这种NAT仍然依赖endpoint,只不过依赖的是源endpoint:每个sourceip:port对应一个映射——否则你的包就会和别人的包混在一起,导致混乱。
严格来说,EIM应该称为“DestinationEndpointIndependentMapping”(DEIM?),但这个名字太拗口了,而且按照惯例,Endpoint永远指的是DestinationEndpoint。
- 将hardNAT以及变种称为“Endpoint-DependentMapping”(EDM,终点相关的映射)。
EDM中还有一个子类型,依据是只根据dst_ip做映射,还是根据dst_ip+dst_port做映射。对于NAT穿透来说,这种区分对来说是一样的:它们都会导致STUN方式不可用。
5.3 老的cone类型划分
你可能会有疑问:根据是否依赖endpoint这一条件,只能组合出两种可能,那为什么传统分类中会有四种cone类型呢?答案是cone包含了两个正交维度的NAT行为:
- NAT映射行为:前面已经介绍过了,
- 有状态防火墙行为:与前者类似,也是分为与endpoint相关还是无关两种类型。
因此最终组合如下:
NATConeTypes
Endpoint无关NATmapping
Endpoint相关NATmapping(alltypes)
Endpoint无关防火墙
FullConeNAT
N/A*
Endpoint相关防火墙(dst.IPonly)
RestrictedConeNAT
N/A*
Endpoint相关防火墙(dst.IP+port)
Port-RestrictedConeNAT
SymmetricNAT
分解到这种程度之后就可以看出,cone类型对NAT穿透场景来说并没有什么意义。我们关心的只有一点:是否是Symmetric——换句话说,一个NAT设备是EIM还是EDM类型的。
5.4 针对NAT穿透场景:简化NAT分类
以上讨论可知,虽然理解防火墙的具体行为很重要,但对于编写NAT穿透代码来说,这一点并不重要。我们的两端同时发包方式(simultaneoustransmissiontrick)能有效穿透以上三种类型的防火墙。在真实场景中,我们主要在处理的是IP-and-portendpoint-dependent防火墙。
因此,对于实际NAT穿透实现,我们可以将以上分类简化成:
Endpoint-IndependentNATmapping
Endpoint-DependentNATmapping(dst.IPonly)
Firewallisyes
EasyNAT
HardNAT
5.5 更多NAT规范(RFC)
想了解更多新的NAT术语,可参考
- RFC4787(NATBehavioralRequirementsforUDP)
- RFC5382(forTCP)
- RFC5508(forICMP)
如果自己实现NAT,那应该(should)遵循这些RFC的规范,这样才能使你的NAT行为符合业界惯例,与其他厂商的设备或软件良好兼容。
六、穿透NAT+防火墙:STUN不可用时,fallback到中继模式
6.1 问题回顾与保底方式(中继)
补完基础知识(尤其是定义了什么是hardNAT)之后,回到我们的NAT穿透主题。
- 第1~4节已经解决了STUN和防火墙穿透的问题,
- 但hardNAT对我们来说是个大问题,只要路径上出现一个这种设备,前面的方案就行不通了。
准备放弃了吗?这才进入NAT真正有挑战的部分:如果已经试过了前面介绍的所有方式仍然不能穿透,我们该怎么办呢?
- 实际上,确实有很多NAT实现在这种情况下都会选择放弃,向用户报一个“无法连接”之类的错误。
- 但对我们来说,这么快就放弃显然是不可接受的——解决不了连通性问题,Tailscale就没有存在的意义。
我们的保底解决方式是:创建一个中继连接(relay)实现双方的无障碍地通信。但是,中继方式性能不是很差吗?这要看具体情况:
- 如果能直连,那显然没必要用中继方式;
- 但如果无法直连,而中继路径又非常接近双方直连的真实路径,并且带宽足够大,那中继方式并不会明显降低通信质量。延迟肯定会增加一点,带宽会占用一些,但相比完全连接不上,还是更能让用户接受的。
不过要注意:我们只有在无法直连时才会选择中继方式。实际场景中,
- 对于大部分网络,我们都能通过前面介绍的方式实现直连,
- 剩下的长尾用中继方式来解决,并不算一个很糟的方式。
此外,某些网络会阻止NAT穿透,其影响比这种hardNAT大多了。例如,我们观察到UCBerkeleyguestWiFi禁止除DNS流量之外的所有outboundUDP流量。不管用什么NAT黑科技,都无法绕过这个拦截。因此我们终归还是需要一些可靠的fallback机制。
6.2 中继协议:TURN、DERP
有多种中继实现方式。
- TURN(TraversalUsingRelaysaroundNAT):经典方式,核心理念是
- 用户(人)先去公网上的TURN服务器认证,成功后后者会告诉你:“我已经为你分配了ip:port,接下来将为你中继流量”,然后将这个ip:port地址告诉对方,让它去连接这个地址,接下去就是非常简单的客户端/服务器通信模型了。
Tailscale并不使用TURN。这种协议用起来并不是很好,而且与STUN不同,它没有真正的交互性,因为互联网上并没有公开的TURN服务器。
- DERP(DetouredEncryptedRoutingProtocol)
这是我们创建的一个协议,DERP,
- 它是一个通用目的包中继协议,运行在HTTP之上,而大部分网络都是允许HTTP通信的。它根据目的公钥(destination’spublickey)来中继加密的流量(encryptedpayloads)。
前面也简单提到过,DERP既是我们在NAT穿透失败时的保底通信方式(此时的角色与TURN类似),也是在其他一些场景下帮助我们完成NAT穿透的旁路信道。换句话说,它既是我们的保底方式,也是有更好的穿透链路时,帮助我们进行连接升级(upgradetoapeer-to-peerconnection)的基础设施。
6.3 小结
有了“中继”这种保底方式之后,我们穿透的成功率大大增加了。如果此时不再阅读本文接下来的内容,而是把上面介绍的穿透方式都实现了,我预计:
- 90%的情况下,你都能实现直连穿透;
- 剩下的10%里,用中继方式能穿透一些(some);
这已经算是一个“足够好”的穿透实现了。
七、穿透NAT+防火墙:企业级改进
如果你并不满足于“足够好”,那我们可以做的事情还有很多!
本节将介绍一些五花八门的tricks,在某些特殊场景下会帮到我们。单独使用这项技术都无法解决NAT穿透问题,但将它们巧妙地组合起来,我们能更加接近100%的穿透成功率。
7.1 穿透hardNAT:暴力端口扫描
回忆hardNAT中遇到的问题,如下图所示,关键问题是:easyNAT不知道该往hardNAT方的哪个ip:port发包。
但必须要往正确的ip:port发包,才能穿透防火墙,实现双向互通。怎么办呢?
- 首先,我们能知道hardNAT的一些ip:port,因为我们有STUN服务器。
这里先假设我们获得的这些IP地址都是正确的(这一点并不总是成立,但这里先这么假设。而实际上,大部分情况下这一点都是成立的,如果对此有兴趣,可以参考REQ-2inRFC4787)。
- IP地址确定了,剩下的就是端口了。总共有65535中可能,我们能遍历这个端口范围吗?
如果发包速度是100packets/s,那最坏情况下,需要10分钟来找到正确的端口。还是那句话,这虽然不是最优的,但总比连不上好。
这很像是端口扫描(事实上,确实是),实际中可能会触发对方的网络入侵检测软件。
7.2 基于生日悖论改进暴力扫描:hardside多开端口+easyside随机探测
利用birthdayparadox算法,我们能对端口扫描进行改进。
- 上一节的基本前提是:hardside只打开一个端口,然后easyside暴力扫描65535个端口来寻找这个端口;
- 这里的改进是:在hardsize开多个端口,例如256个(即同时打开256个socket,目的地址都是easyside的ip:port),然后easyside随机探测这边的端口。
这里省去算法的数学模型,如果你对实现干兴趣,可以看看我写的pythoncalculator。计算过程是“经典”生日悖论的一个小变种。下面是随着easysiderandomprobe次数(假设hardsize256个端口)的变化,两边打开的端口有重合(即通信成功)的概率:
随机探测次数
成功概率
174
50%
256
64%
1024
98%
2048
99.9%
根据以上结果,如果还是假设100ports/s这样相当温和的探测速率,那2秒钟就有约50%的成功概率。即使非常不走运,我们仍然能在20s时几乎100%穿透成功,而此时只探测了总端口空间的4%。
非常好!虽然这种hardNAT给我们带来了严重的穿透延迟,但最终结果仍然是成功的。那么,如果是两个hardNAT,我们还能处理吗?
7.3 双hardNAT场景
这种情况下仍然可以用前面的多端口+随机探测方式,但成功概率要低很多了:
- 每次通过一台hardNAT去探测对方的端口(目的端口)时,我们自己同时也生成了一个随机源端口,
- 这意味着我们的搜索空间变成了二维{srcport,dstport}对,而不再是之前的一维dstport空间。
这里我们也不就具体计算展开,只告诉结果:仍然假设目的端打开256个端口,从源端发起2048次(20秒),成功的概率是:0.01%。
如果你之前学过生日悖论,就并不会对这个结果感到惊讶。理论上来说,
- 要达到99.9%的成功率,我们需要两边各进行170,000次探测——如果还是以100packets/sec的速度,就需要28分钟。
- 要达到50%的成功率,“只”需要54,000packets,也就是9分钟。
- 如果不使用生日悖论方式,而且暴力穷举,需要1.2年时间!
对于某些应用来说,28分钟可能仍然是一个可接受的时间。用半个小时暴力穿透NAT之后,这个连接就可以一直用着——除非NAT设备重启,那样就需要再次花半个小时穿透建个新连接。但对于交互式应用来说,这样显然是不可接受的。
更糟糕的是,如果去看常见的办公网路由器,你会震惊于它的activesessionlowlimit有多么低。例如,一台JuniperSRX300最多支持64,000activesessions。也就是说,
- 如果我们想创建一个成功的穿透连接,就会把它的整张session表打爆(因为我们要暴力探测65535个端口,每次探测都是一条新连接记录)!这显然要求这台路由器能从容优雅地处理过载的情况。
- 这只是创建一条连接带来的影响!如果20台机器同时对这台路由器发起穿透呢?绝对的灾难!
至此,我们通过这种方式穿透了比之前更难一些的网络拓扑。这是一个很大的成就,因为家用路由器一般都是easyNAT,hardNAT一般都是办公网路由器或云NAT网关。这意味着这种方式能帮我们解决
- home-to-office(家->办公室)
- home-to-cloud(家->云)
的场景,以及一部分
- office-to-cloud(办公室->云)
- cloud-to-cloud(云->办公室)
场景。
7.4 控制端口映射(portmapping)过程:UPnP/NAT-PMP/PCP协议
如果我们能让NAT设备的行为简单点,不要把事情搞这么复杂,那建立连接(穿透)就会简单很多。真有这样的好事吗?还真有,有专门的一种协议叫端口映射协议(portmappingprotocols)。通过这种协议禁用掉前面遇到的那些乱七八糟的东西之后,我们将得到一个非常简单的“请求-响应”。
下面是三个具体的端口映射协议:
- UPnPIGD(UniversalPlug’n’PlayInternetGatewayDevice)
最老的端口控制协议,诞生于1990s晚期,因此使用了很多上世纪90年代的技术(XML、SOAP、multicastHTTPoverUDP——对,HTTPoverUDP),而且很难准确和安全地实现这个协议。但以前很多路由器都内置了UPnP协议,现在仍然很多。
请求和响应:
- “你好,请将我的lan-ip:port转发到公网(WAN)”,“好的,我已经为你分配了一个公网映射wan-ip:port”。
- NAT-PMP
UPnPIGD出来几年之后,Apple推出了一个功能类似的协议,名为NAT-PMP(NATPortMappingProtocol)。
但与UPnP不同,这个协议只做端口转发,不管是在客户端还是服务端,实现起来都非常简单。
- PCP
稍后一点,又出现了NAT-PMPv2版,并起了个新名字PCP(PortControlProtocol)。
因此要更好地实现穿透,可以
- 先判断本地的默认网关上是否启用了UPnPIGD,NAT-PMPandPCP,
- 如果探测发现其中任何一种协议有响应,我们就申请一个公网端口映射,
可以将这理解为一个加强版STUN:我们不仅能发现自己的公网ip:port,而且能指示我们的NAT设备对我们的通信对端友好一些——但并不是为这个端口修改或添加防火墙规则。
- 接下来,任何到达我们NAT设备的、地址是我们申请的端口的包,都会被设备转发到我们。
但我们不能假设这个协议一定可用:
- 本地NAT设备可能不支持这个协议;
- 设备支持但默认禁用了,或者没人知道还有这么个功能,因此从来没开过;
- 安全策略要求关闭这个特性。
这一点非常常见,因为UPnP协议曾曝出一些高危漏洞(后面都修复了,因此如果是较新的设备,可以安全地使用UPnP——如果实现没问题)。不幸的是,某些设备的配置中,UPnP,NAT-PMP,PCP是放在一个开关里的(可能统称为“UPnP”功能),一开全开,一关全关。因此如果有人担心UPnP的安全性,他连另外两个也用不了。
最后,终归来说,只要这种协议可用,就能有效地减少一次NAT,大大方便建连过程。但接下来看一些不常见的场景。
7.5 多NAT协商(NegotiatingnumerousNATs)
目前为止,我们看到的客户端和服务端都各只有一个NAT设备。如果有多个NAT设备会怎么样?例如下面这种拓扑:
这个例子比较简单,不会给穿透带来太大问题。包从客户端A经过多次NAT到达公网的过程,与前面分析的穿过多层有状态防火墙是一样的:
- 额外的这层(NAT设备)对客户端和服务端来说都不可见,我们的穿透技术也不关心中间到底经过了多少层设备。
- 真正有影响的其实只是最后一层设备,因为对端需要在这一层设备上找到入口让包进来。
具体来说,真正有影响的是端口转发协议。
- 客户端使用这种协议分配端口时,为我们分配端口的是最靠近客户端的这层NAT设备;
- 而我们期望的是让最离客户端最远的那层NAT来分配,否则我们得到的就是一个网络中间层分配的ip:port,对端是用不了的;
- 不幸的是,这几种协议都不能递归地告诉我们下一层NAT设备是多少——虽然可以用traceroute之类的工具来探测网络路径,再加上猜路上的设备是不是NAT设备(尝试发送NAT请求)——但这个就看运气了。
这就是为什么互联网上充斥着大量的文章说double-NAT有多糟糕,以及警告用户为保持后向兼容不要使用double-NAT。但实际上,double-NAT对于绝大部分互联网应用来说都是不可见的(透明的),因为大部分应用并不需要主动地做这种NAT穿透。
但我也绝不是在建议你在自己的网络中设置double-NAT。
- 破坏了端口映射协议之后,某些视频游戏的多人(multiplayer)模式就会无法使用,
- 也可能会使你的IPv6网络无法派上用场,后者是不用NAT就能双向直连的一个好方案。
但如果double-NAT并不是你能控制的,那除了不能用到这种端口映射协议之外,其他大部分东西都是不受影响的。
double-NAT的故事到这里就结束了吗?——并没有,而且更大型的double-NAT场景将展现在我们面前。
7.6 运营商级NAT带来的问题
即使用NAT来解决IPv4地址不够的问题,地址仍然是不够用的,ISP(互联网服务提供商)显然无法为每个家庭都分配一个公网IP地址。那怎么解决这个问题呢?ISP的做法是不够了就再嵌套一层NAT:
- 家用路由器将你的客户端SNAT到一个“intermediate”IP然后发送到运营商网络,
- ISP’snetwork中的NAT设备再将这些intermediateIPs映射到少量的公网IP。
后面这种NAT就称为“运营商级NAT”(carrier-gradeNAT,或称电信级NAT),缩写CGNAT。如下图所示:
CGNAT对NAT穿透来说是一个大麻烦。
- 在此之前,办公网用户要快速实现NAT穿透,只需在他们的路由器上手动设置端口映射就行了。
- 但有了CGNAT之后就不管用了,因为你无法控制运营商的CGNAT!
好消息是:这其实是double-NAT的一个小变种,因此前面介绍的解决方式大部分还仍然是适用的。某些东西可能会无法按预期工作,但只要肯给ISP交钱,这些也都能解决。除了portmappingprotocols,其他我们已经介绍的所有东西在CGNAT里都是适用的。
新挑战:同一CGNAT侧直连,STUN不可用
但我们确实遇到了一个新挑战:如何直连两个在同一CGNAT但不同家用路由器中的对端呢?如下图所示:
在这种情况下,STUN就无法正常工作了:STUN看到的是客户端在公网(CGNAT后面)看到的地址,而我们想获得的是在“middlenetwork”中的ip:port,这才是对端真正需要的地址,
解决方案:如果端口映射协议能用:一端做端口映射
怎么办呢?
如果你想到了端口映射协议,那恭喜,答对了!如果peer中任何一个NAT支持端口映射协议,对我们就能实现穿透,因为它分配的ip:port正是对端所需要的信息。
这里讽刺的是:double-NAT(指CGNAT)破坏了端口映射协议,但在这里又救了我们!当然,我们假设这些协议一定可用,因为CGNATISP倾向于在它们的家用路由器侧关闭这些功能,已避免软件得到“错误的”结果,产生混淆。
解决方案:如果端口映射协议不能用:NAThairpin模式
如果不走运,NAT上没有端口映射功能怎么办?
让我们回到基于STUN的技术,看会发生什么。两端在CGNAT的同一侧,假设STUN告诉我们A的地址是2.2.2.2:1234,B的地址是2.2.2.2:5678。
那么接下来的问题是:如果A向2.2.2.2:5678发包会怎么样?期望的CGNAT行为是:
- 执行A的NAT映射规则,即对2.2.2.2:1234->2.2.2.2:5678进行SNAT。
- 注意到目的地址2.2.2.2:5678匹配到的是B的入向NAT映射,因此接着对这个包执行DNAT,将目的IP改成B的私有地址。
- 通过CGNAT的internal接口(而不是public接口,对应公网)将包发给B。
这种NAT行为有个专门的术语,叫hairpinning(直译为发卡,意思是像发卡一样,沿着一边上去,然后从另一边绕回来),
大家应该猜到的一个事实是:不是所以NAT都支持hairpin模式。实际上,大量well-behavedNAT设备都不支持hairpin模式,
- 因为它们都有“只有src_ip是私有地址且dst_ip是公网地址的包才会经过我”之类的假设。
- 因此对于这种目的地址不是公网、需要让路由器把包再转回内网的包,它们会直接丢弃。
- 这些逻辑甚至是直接实现在路由芯片中的,因此除非升级硬件,否则单靠软件编程无法改变这种行为。
Hairpin是所有NAT设备的特性(支持或不支持),并不是CGNAT独有的。
- 在大部分情况下,这个特性对我们的NAT穿透目的来说都是无所谓的,因为我们期望中两个LANNAT设备会直接通信,不会再向上绕到它们的默认网关CGNAT来解决这个问题。
Hairpin特性可有可无这件事有点遗憾,这可能也是为什么hairpin功能经常broken的原因。
- 一旦必须涉及到CGNAT,那hairpinning对连接性来说就至关重要了。
Hairpinning使内网连接的行为与公网连接的行为完成一致,因此我们无需关心目的地址类型,也不用知晓自己是否在一台CGNAT后面。
如果hairpinning和portmappingprotocols都不可用,那只能降级到中继模式了。
7.7 全IPv6网络:理想之地,但并非问题全无
行文至此,一些读者可能已经对着屏幕咆哮:不要再用IPv4了!花这么多时间精力解决这些没意义的东西,还不如直接换成IPv6!
- 的确,之所以有这些乱七八糟的东西,就是因为IPv4地址不够了,我们一直在用越来越复杂的NAT来给IPv4续命。
- 如果IP地址够用,无需NAT就能让世界上的每个设备都有一个自己的公网IP地址,这些问题不就解决了吗?
简单来说,是的,这也正是IPv6能做的事情。但是,也只说对了一半:在理想的全IPv6世界中,所有这些东西会变得更加简单,但我们面临的问题并不会完全消失——因为有状态防火墙仍然还是存在的。
- 办公室中的电脑可能有一个公网IPv6地址,但你们公司肯定会架设一个防火墙,只允许你的电脑主动访问公网,而不允许反向主动建连。
- 其他设备上的防火墙也仍然存在,应用类似的规则。
因此,我们仍然会用到
- 本文最开始介绍的防火墙穿透技术,以及
- 帮助我们获取自己的公网ip:port信息的旁路信道
- 仍然需要在某些场景下fallback到中继模式,例如fallback到最通用的HTTP中继协议,以绕过某些网络禁止outboundUDP的问题。
但我们现在可以抛弃STUN、生日悖论、端口映射协议、hairpin等等东西了。这是一个好消息!
全球IPv4/IPv6部署现状
另一个更加严峻的现实问题是:当前并不是一个全IPv6世界。目前世界上
- 大部分还是IPv4,
- 大约33%是IPv6,而且分布极度不均匀,因此某些通信对所在的可能是100%IPv6,也可能是0%,或二者之间。
不幸的是,这意味着,IPv6**还**无法作为我们的解决方案。就目前来说,它只是我们的工具箱中的一个备选。对于某些peer来说,它简直是完美工具,但对其他peer来说,它是用不了的。如果目标是“任何情况下都能穿透(连接)成功”,那我们就仍然需要IPv4+NAT那些东西。
新场景:NAT64/DNS64
IPv4/IPv6共存也引出了一个新的场景:NAT64设备。
前面介绍的都是NAT44设备:它们将一个IPv4地址转换成另一IPv4地址。NAT64从名字可以看出,是将一个内侧IPv6地址转换成一个外侧IPv4地址。利用DNS64设备,我们能将IPv4DNS应答给IPv6网络,这样对终端来说,它看到的就是一个全IPv6网络,而仍然能访问IPv4公网。
如果需要处理DNS问题,那这种方式工作良好。例如,如果连接到google.com,将这个域名解析成IP地址的过程会涉及到DNS64设备,它又会进一步involveNAT64设备,但后一步对用户来说是无感知的。
但对于NAT和防火墙穿透来说,我们会关心每个具体的IP地址和端口。
解决方案:CLAT(Customer-sidetransLATor)
如果设备支持CLAT(Customer-sidetranslator—fromCustomerXLAT),那我们就很幸运:
- CLAT假装操作系统有直接IPv4连接,而背后使用的是NAT64,以对应用程序无感知。在有CLAT的设备上,我们无需做任何特殊的事情。
- CLAT在移动设备上非常常见,但在桌面电脑、笔记本和服务器上非常少见,因此在后者上,必须自己做CLAT做的事情:检测NAT64+DNS64的存在,然后正确地使用它们。
解决方案:CLAT不存在时,手动穿透NAT64设备
- 首先检测是否存在NAT64+DNS64。
方法很简单:向ipv4only.arpa.发送一个DNS请求。这个域名会解析到一个已知的、固定的IPv4地址,而且是纯IPv4地址。如果得到的是一个IPv6地址,就可以判断有DNS64服务器做了转换,而它必然会用到NAT64。这样就能判断出NAT64的前缀是多少。
- 此后,要向IPv4地址发包时,发送格式为{NAT64prefix+IPv4address}的IPv6包。类似地,收到来源格式为{NAT64prefix+IPv4address}的包时,就是IPv4流量。
- 接下来,通过NAT64网络与STUN通信来获取自己在NAT64上的公网ip:port,接下来就回到经典的NAT穿透问题了——除了需要多做一点点事情。
幸运的是,如今的大部分v6-only网络都是移动运营商网络,而几乎所有手机都支持CLAT。运营v6-only网络的ISPs会在他们给你的路由器上部署CLAT,因此最后你其实不需要做什么事情。但如果想实现100%穿透,就需要解决这种边边角角的问题,即必须显式支持从v6-only网络连接v4-only对端。
7.8 将所有解决方式集成到ICE协议
针对具体场景,该选择哪种穿透方式?
至此,我们的NAT穿透之旅终于快结束了。我们已经覆盖了有状态防火墙、简单和高级NAT、IPv4和IPv6。只要将以上解决方式都实现了,NAT穿透的目的就达到了!
但是,
- 对于给定的peer,如何判断改用哪种方式呢?
- 如何判断这是一个简单有状态防火墙的场景,还是该用到生日悖论算法,还是需要手动处理NAT64呢?
- 还是通信双方在一个WiFi网络下,连防火墙都没有,因此不需要任何操作呢?
早期NAT穿透比较简单,能让我们精确判断出peer之间的路径特点,然后针对性地采用相应的解决方式。但后面,网络工程师和NAT设备开发工程师引入了一些新理念,给路径判断造成很大困难。因此我们需要简化客户端侧的思考(判断逻辑)。
这就要提到InteractiveConnectivityEstablishment(ICE,交换式连接建立)协议了。与STUN/TURN类似,ICE来自电信领域,因此其RFC充满了SIP、SDP、信令会话、拨号等等电话术语。但如果忽略这些领域术语,我们会看到它描述了一个极其优雅的判断最佳连接路径的算法。
真的?这个算法是:每种方法都试一遍,然后选择最佳的那个方法。就是这个算法,惊喜吗?
来更深入地看一下这个算法。
ICE(InteractiveConnectivityEstablishment)算法
这里的讨论不会严格遵循ICEspec,因此如果是在自己实现一个可互操作的ICE客户端,应该通读RFC8445,根据它的描述来实现。这里忽略所有电信术语,只关注核心的算法逻辑,并提供几个在ICE规范允许范围的灵活建议。
- 为实现和某个peer的通信,首先需要确定我们自己用的(客户端侧)这个socket的地址,这是一个列表,至少应该包括:
- 我们自己的IPv6ip:ports我们自己的IPv4LANip:ports(局域网地址)通过STUN服务器获取到的我们自己的IPv4WANip:ports(公网地址,可能会经过NAT64转换)通过端口映射协议获取到的我们自己的IPv4WANip:port(NAT设备的端口映射协议分配的公网地址)运营商提供给我们的endpoints(例如,静态配置的端口转发)
- 通过旁路信道与peer互换这个列表。两边都拿到对方的列表后,就开始互相探测对方提供的地址。列表中地址没有优先级,也就是说,如果对方给的了15个地址,那我们应该把这15个地址都探测一遍。
这些探测包有两个目的:
- 打开防火墙,穿透NAT,也就是本文一直在介绍的内容;健康检测。我们在不断交换(最好是已认证的)“ping/pong”包,来检测某个特定的路径是不是端到端通的。
- 最后,一小会儿之后,从可用的备选地址中(根据某些条件)选择“最佳”的那个,任务完成!
这个算法的优美之处在于:只要选择最佳线路(地址)的算法是正确的,那就总能获得最佳路径。
- ICE会预先对这些备选地址进行排序(通常:LAN>WAN>WAN+NAT),但用户也可以自己指定这个排序行为。
- 从v0.100.0开始,Tailscale从原来的hardcode优先级切换成了根据round-triplatency的方式,它大部分情况下排序的结果和LAN>WAN>WAN+NAT是一致的。但相比于静态排序,我们是动态计算每条路径应该属于哪个类别。
ICEspec将协议组织为两个阶段:
- 探测阶段
- 通信阶段
但不一定要严格遵循这两个步骤的顺序。在Tailscale,
- 我们发现更优的路径之后就会自动切换过去,
- 所有的连接都是先选择DERP模式(中继模式)。这意味着连接立即就能建立(优先级最低但100%能成功的模式),用户不用任何等待,
- 然后并行进行路径发现。通常几秒钟之后,我们就能发现一条更优路径,然后将现有连接透明升级(upgrade)过去。
但有一点需要关心:非对称路径。ICE花了一些精力来保证通信双方选择的是相同的网络路径,这样才能保证这条路径上有双向流量,能保持防火墙和NAT设备的连接一直处于open状态。自己实现的话,其实并不需要花同样大的精力来实现这个保证,但需要确保你所有使用的所有路径上,都有双向流量。这个目标就很简单了,只需要定期在所有已使用的路径上发ping/pong就行了。
健壮性与降级
要实现健壮性,还需要检测当前已选择的路径是否已经失败了(例如,NAT设备维护清掉了所有状态),如果失败了就要降级(downgrade)到其他路径。这里有两种方式:
- 持续探测所有路径,维护一个降级时会用的备用地址列表;
- 直接降级到保底的中继模式,然后再通过路径探测升级到更好的路径。
考虑到发生降级的概率是非常小的,因此这种方式可能是更经济的。
7.9 安全
最后需要提到安全。
本文的所有内容都假设:我们使用的上层协议已经有了自己的安全机制(例如QUIC协议有TLS证书,WireGuard协议有自己的公钥)。如果还没有安全机制,那显然是要立即补上的。一旦动态切换路径,基于IP的安全机制就是无用的了(IP协议最开始就没怎么考虑安全性),至少要有端到端的认证。
- 严格来说,如果上层协议有安全机制,那即使收到是欺骗性的ping/pong流量,问题都不大,最坏的情况也就是攻击者诱导两端通过他们的系统来中继流量。而有了端到端安全机制,这并不是一个大问题(取决于你的威胁模型)。
- 但出于谨慎考虑,最好还是对路径发现的包也做认证和加密。具体如何做可以咨询你们的应用安全工程师。
八、结束语
我们终于完成了NAT穿透的目标!
如果实现了以上提到的所有技术,你将得到一个业内领先的NAT穿透软件,能在绝大多数场景下实现端到端直连。如果直连不了,还可以降级到保底的中继模式(对于长尾来说只能靠中继了)。
但这些工作相当复杂!其中一些问题研究起来很有意思,但很难做到完全正确,尤其是那些非常边边角角的场景,真正出现的概率极小,但解决它们所需花费的经历又极大。不过,这种工作只需要做一次,一旦解决了,你就具备了某种超级能力:探索令人激动的、相对还比较崭新的端到端应用(peer-to-peerapplications)世界。
8.1 跨公网端到端直连
去中心化软件领域中的许多有趣想法,简化之后其实都变成了跨过公网(互联网)实现端到端直连这一问题,开始时可能觉得很简单,但真正做才发现比想象中难多了。现在知道如何解决这个问题了,动手开做吧!
8.2 结束语之TL;DR
实现健壮的NAT穿透需要下列基础:
- 一种基于UDP的协议;
- 能在程序内直接访问socket;
- 有一个与peer通信的旁路信道;
- 若干STUN服务器;
- 一个保底用的中继网络(可选,但强烈推荐)
然后需要:
- 遍历所有的ip:port;
- 查询STUN服务器来获取自己的公网ip:port信息,以及判断自己这一侧的NAT的“难度”(difficulty);
- 使用portmapping协议来获取更多的公网ip:ports;
- 检查NAT64,通过它获取自己的公网ip:port;
- 将自己的所有公网ip:ports信息通过旁路信道与peer交换,以及某些加密秘钥来保证通信安全;
- 通过保底的中继方式与对方开始通信(可选,这样连接能快速建立)
- 如果有必要/想这么做,探测对方的提供的所有ip:port,以及执行生日攻击(birthdayattacks)来穿透harderNAT;
- 发现更优路径之后,透明升级到该路径;
- 如果当前路径断了,降级到其他可用的路径;
- 确保所有东西都是加密的,并且有端到端认证。