关于NAT,ALG和P2P连接

Last Updated: 2023-04-07 10:03:16 Friday

-- TOC --

看不出NAT这个技术会在什么时候被淘汰,在内网,它的知名度和生命力都很顽强。甚至有人会说因为有了NAT,企业内网更加安全了!是的,是会更加安全,因为外网很可能不能直接向内网主机发起连接,内网主机被NAT影藏了起来。NAT是一个天然的防火墙。

本文有部分内容,转自:https://evilpan.com/2015/10/31/p2p-over-middle-box/

NAT的概念和分类

Basic NAT

只转换IP地址。

静态NAT

在出口路由器上配置的静态NAT转换条目,永久有效。

动态NAT

动态NAT条目,顾名思义,就是动态生成的,有老化时间。

NAPT(PAT)

P是指Port。

这样分类,好像除了NAPT,其它几种NAT只映射IP地址,不管port。也许有这样的场景,这需要有足够的公网IP。看来孤陋寡闻的我,接触过的场景,都是NAPT。而我们在沟通交流提到的NAT,也其实都是NAPT。

NAPT也可以有静态配置,TPLINK路由器上的虚拟服务器配置,其实就是在配置静态NAPT条目,让外面通过某个端口,能够直接访问内网服务器。

NAT-T

NAT Transverse,即NAT穿越。

锥形NAT,Cone NAT

在建立了一对(公网IP,公网端口)和(内网IP,内网端口)二元组的绑定之后,只要还有一路会话还是激活的,Cone NAT会重用这组绑定用于接下来该应用程序的所有会话(同一内网IP和端口)。 例如,假设客户端A建立了两个连续的对外会话,从相同的内部端点(10.0.0.1:1234)到两个不同的外部服务端S1和S2。

Cone NAT只为两个会话映射了一个公网端点(155.99.25.11:62000),确保客户端端口的“身份”在地址转换的时候保持不变。由于基本NAT和防火墙都不改变数据包的端口号,因此这些类型的中间件也可以看作是退化的Cone NAT。

Server S1                                     Server S2
18.181.0.31:1235                              138.76.29.7:1235
       |                                             |
       |                                             |
       +----------------------+----------------------+
                              |
  ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^
  |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |
  v 155.99.25.11:62000 v      |      v 155.99.25.11:62000 v
                              |
                           Cone NAT
                         155.99.25.11
                              |
  ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^
  |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |
  v   10.0.0.1:1234    v      |      v   10.0.0.1:1234    v
                              |
                           Client A
                        10.0.0.1:1234

不管是TCP还是UDP,都会显式或隐式的调用bind,可以实现用相同的ip+port去与不同的远端ip+远端port通信。UDP比较简单,直接sendto带上不同的目标地址。TCP复杂一点,需要用到SO_REUSEPORT,然后bind但相同的地址,再connect

这个锥形Cone的形成,其实是因为本地的ip+port没有变化,在NAT映射的时候,保持不变也是自然的选择。

Full Cone NAT

在一个新会话建立了公网/内网端口绑定之后,全锥形NAT接下来会接受对应公网端口的所有数据,无论是来自哪个(公网)终端。全锥NAT有时候也被称为“混杂”NAT(promiscuous NAT)。

这种NAT,更像是实现时留下的bug,或者debug接口。

[Port] Restricted Cone NAT

只有当外部数据包的IP地址和端口号匹配内网主机发送过的地址和端口号时,NAT网关才进行转发,端口受限锥形NAT为内部结点提供了和对称NAT相同等级的保护,以隔离未关联的数据。

对称NAT,Symmetric NAT

对称NAT与Cone NAT正好相反,不在所有公网-内网pair的会话中维持一个固定的端口绑定, 其为每个新的会话开辟一个新的端口。 如下图所示:

Server S1                                     Server S2
18.181.0.31:1235                              138.76.29.7:1235
       |                                             |
       |                                             |
       +----------------------+----------------------+
                              |
  ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^
  |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |
  v 155.99.25.11:62000 v      |      v 155.99.25.11:62001 v
                              |
                         Symmetric NAT
                         155.99.25.11
                              |
  ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^
  |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |
  v   10.0.0.1:1234    v      |      v   10.0.0.1:1234    v
                              |
                           Client A
                        10.0.0.1:1234

虽然Client A使用了相同的ip+port,在NAT网关,由于访问不同的remote地址,还是映射了不同的公网port。

SNAT

这里的S,指Source,即对源IP和源Port进行转换。从内网向外发起访问的时候,就是SNAT。

尽管只配置SNAT,但是在进行源地址和源端口转换的时候,会在路由器的NAT表中插入一行,当响应报文到达路由器的时候,参照NAT表中的信息,进行目的地址和目的端口的转换,所以在不配置DNAT的时候,目的地址和目的端口仍然可以被转换。但是这个过程,只能是局域网内的用户发起请求,响应报文的目的地址和目的端口才会被转换为局域网地址。外网的主机是不能直接访问局域网中的主机的。

DNAT

这里的D,指Destination,即对目的IP和目的端口进行转换。用于从外到内访问的时候,使用DNAT,相当于将内网的ip+port组合映射到了外网,让外网用户可以直接访问。

ICMP流量

NAT映射TCP和UDP很好理解,都是ip+port的映射,但是ICMP报文呢?

网上有人说:对于ICMP ping包,type+code就是源port,identifier是目的port。NAT通过更改ICMP的ID,来实现多对少的映射。这就是把ICMP中的ID字段当做port来使用了。

但并不是所有的ICMP都有identifier,如果是外网发回来的port unreachable icmp报文,如何通过NAT传递给发送的主机呢?

线索:很多icmp返回的报文,都会带上源ip头和64byte的payload。源ip头中有目的ip,NAT是否可以查到是内部那个ip在向这个目的ip发送数据呢?

Relay

思考一下NAT的实现,当IP+Port的映射关系建立起来后,NAT的实现就需要为这个映射关系实现一个Relay,无脑中继流量。

各种代理,最后都会实现一个Relay。

老化时间

当某一对映射没有流量后,映射需要老化,老化后才能被新的流量所使用。

下面各协议老化时间数据来自网络:

NAT存在的问题

在一个具有NAT功能的路由器下,主机并没有真正与外网服务器建立端到端的连接。

默认无法从外到内发起连接,TCP和UDP都不行,因此有很多内网穿透技术。

NAT违反了IP地址结构模型的设计原则,NAT使得有很多不同的主机使用相同的地址,如10.0.0.1。也有人说,这是一种共享。

NAT使得IP协议从面向无连接变成面向连接,NAT必须维护专用IP地址与公用IP地址以及端口号的映射关系。NAT这个结点,成为了网络的单点故障源。

NAT违反了基本的网络分层结构模型的设计原则。因为在传统的网络分层结构模型中,第N层是不能修改第N+1层的报头内容的。NAT破坏了这种各层独立的原则。

比如,基于IPSEC的应用,对于IPSEC而言,如果更改了IPsec包中的IP地址,IPsec的加密机制将会丢弃此报文,从而破坏了基于IPsec的应用。

对NAT网关的DDoS攻击

原理很简单:每一路会话在NAT都有一组ip+port的映射,如果这个组合资源用完了,就无法在创建新的会话了。

看到TPLINK的一款企业路由器有这样的选择,就是用来防范此类攻击的:

nat_ddos.png

ALG

ALG:Application Layer Gateway

它就像ss5的BIND功能。

比如主动模式的FTP,客户端会主动连接服务端的21端口,并且开放一个本地的随机端口,然后通过PORT命令告诉服务端自己的随机端口具体是哪个,服务端再用自己的20端口主动连接客户端。在有NAT的情况下,外网的FTP Server是无法主动向内网发起TCP连接。

ALG也是运行在NAT网关上的,专门处理像FTP这类的“奇葩”协议。它会识别应用层的协议类型,如果一个主动模式的FTP连接建立了,ALG是能识别出来的,然后在客户端发送PORT命令时修改PORT命令指定的IP和端口,把他们换成外网的IP和端口。当服务端向客户端主动发起连接时再把它改成内网客户端的IP和端口,如此就可以让主动模式FTP正常工作了。

ALG.png

从上面这张图可以看出来,ALG本质上就是在FTP发送PORT命令时篡改命令内容,并且给NAT增加一条地址映射。后续FTP服务器发起的主动连接就能通过这条新的地址映射来访问内网的IP了。

P2P

根据客户端的不同,客户端之间进行P2P传输的方法也略有不同,这里介绍了现有的穿越中间件进行P2P通信的几种技术。

Relay

这是最可靠但也是最低效的一种P2P通信实现:

                       Server S
                          |
                          |
   +----------------------+----------------------+
   |                                             |
 NAT A                                         NAT B
   |                                             |
   |                                             |
Client A                                      Client B

客户端A和客户端B不直接通信,而是先都与服务端S建立链接,然后再通过S和对方建立的通路来中继传递的数据。这钟方法的缺陷很明显, 当链接的客户端变多之后,会显著增加服务器的负担,完全没体现出P2P的优势。但这种方法的好处是能保证成功,因此在实践中也常作为一种备选方案。

逆向链接(Connection reversal)

第二种方法在当两个端点中有一个不存在中间件的时候有效。 例如,客户端A在NAT之后,而客户端B拥有全局IP地址,如下图:

                           Server S
                        18.181.0.31:1235
                               |
                               |
        +----------------------+----------------------+
        |                                             |
      NAT A                                           |
155.99.25.11:62000                                    |
        |                                             |
        |                                             |
     Client A                                      Client B
  10.0.0.1:1234                               138.76.29.7:1234 

客户端A的内网地址为10.0.0.1,且应用程序正在使用TCP端口1234。 A和服务器S建立了一个链接,服务器的IP地址为18.181.0.31,监听1235端口。 NAT A给客户端A分配了TCP端口62000,地址为NAT的公网IP地址155.99.25.11, 作为客户端A对外当前会话的临时IP和端口。因此S认为客户端A就是155.99.25.11:62000。而B由于有公网地址,所以对S来说B就是138.76.29.7:1234。

当客户端B想要发起一个对客户端A的P2P链接时,要么链接A的外网地址155.99.25.11:62000,要么链接A的内网地址10.0.0.1:1234,然而,两种方式链接都会失败。链接10.0.0.1:1234失败自不用说,为什么链接155.99.25.11:62000也会失败呢?因为来自B的TCP SYN握手请求到达NAT A的时候会被拒绝,因为对NAT A来说,只有外出的链接才是允许的。在直接链接A失败之后,B可以通过S向A中继一个链接请求,从而从A方向“逆向”地建立起 A--B 之间的点对点链接。

很多当前的P2P系统都实现了这种技术,但其局限性也是很明显的,只有当其中一方有公网IP时链接才能建立。越来越多的情况下,通信的双方都在NAT之后,因此就要用到我们下面介绍的第三种技术了。

UDP打洞(UDP hole punching)

第三种P2P通信技术,被广泛采用的,名为“P2P打洞”。P2P打洞技术依赖于通常的防火墙和Cone NAT允许正当的P2P应用程序在中间件中打洞且与对方建立直接链接的特性。下面主要考虑两种常见的场景,以及应用程序如何设计去完美地处理这些场景。第一种场景代表了大多数情况,即两个需要直接链接的客户端处在两个不同的NAT之后;第二种场景是两个客户端在同一个NAT之后,但客户端自己可能并不知道(比如同一ISP下面的不同子网)。

端点在不同的NAT之后

假设客户端A和客户端B的地址都是内网地址,且在不同的NAT后面。A、B上运行的P2P应用程序和服务器S都使用了UDP端口1234,A和B分别初始化了与Server的UDP通信,地址映射如图所示:

                            Server S
                        18.181.0.31:1234
                               |
                               |
        +----------------------+----------------------+
        |                                             |
      NAT A                                         NAT B
155.99.25.11:62000                            138.76.29.7:31000
        |                                             |
        |                                             |
     Client A                                      Client B
  10.0.0.1:1234                                 10.1.1.3:1234

现在假设客户端A打算与客户端B直接建立一个UDP通信会话。如果A直接给B的公网地址138.76.29.7:31000发送UDP数据,NAT B将很可能会无视进入的 数据(除非是Full Cone NAT),因为源地址和端口与S不匹配,B最初只与S建立过会话。B往A直接发信息也类似.

假设A开始给B的公网地址发送UDP数据的同时,给服务器S发送一个中继请求,要求B开始给A的公网地址发送UDP信息。A向B的发出信息,会导致NAT A打开一个A的内网地址与B的外网地址之间的新通讯会话,B往A亦然。一旦新的UDP会话在两个方向都打开之后,客户端A和客户端B就能直接通讯,而无须再通过引导服务器S了。

UDP打洞技术有许多有用的性质。一旦一个P2P链接建立,链接的双方都能反过来作为“引导服务器”来帮助其他中间件后的客户端进行打洞(比如A通知B让C给A发数据),极大减少了服务器的负载。应用程序不需要知道中间件具体是什么(如果有的话),因为以上的过程在没有中间件或者有多个中间件的情况下,也一样能建立通信链路。

端点在相同的NAT之后

考虑这样一种情景,两个客户端A和B正好在同一个NAT之后(而且很可能他们自己并不知道),因此在同一个内网网段之内。客户端A和服务器S建立了一个UDP会话,NAT为此分配了公网端口62000,B同样和S建立会话,分配到了端口62001,如下图:

                          Server S
                      18.181.0.31:1234
                             |
                             |
                            NAT
                   A-S 155.99.25.11:62000
                   B-S 155.99.25.11:62001
                             |
      +----------------------+----------------------+
      |                                             |
   Client A                                      Client B
10.0.0.1:1234                                 10.1.1.3:1234

假设A和B使用了上节介绍的UDP打洞技术来建立P2P通路,那么会发生什么呢?首先A和B会得到由S观测到的对方的公网IP和端口号,然后给对方的地址发送信息。两个客户端只有在NAT允许内网主机对内网其他主机发起UDP会话的时候才能正常通信,我们把这种情况称之为回环传输(loopback transmission),因为从内部到达NAT的数据会被“回送”到内网中,而不是转发到外网。

例如,当A发送一个UDP数据包给B的公网地址时,数据包最初有源IP地址和端口地址10.0.0.1:1234和目的地址155.99.25.11:62001,NAT收到包后,将其转换为源155.99.25.11:62000(A的公网地址)和目的10.1.1.3:1234,然后再转发给B。

即便NAT支持回环传输,这种转换和转发在此情况下也是没必要的,且有可能会增加A与B的对话延时和加重NAT网关的负担。

对于这个情况,优化方案是很直观的。当A和B最初通过S交换地址信息时,他们应该包含自身的IP地址和端口号(从自己看),同时也包含从服务器看的自己的 地址和端口号。然后客户端同时开始从对方已知的两个的地址中同时开始互相发送数据,并使用第一个成功通信的地址作为对方地址。如果两个客户端在同一个NAT后,发送到对方内网地址的数据最有可能先到达,从而可以建立一条不经过NAT的通信链路;如果两个客户端在不同的NAT之后,发送给对方内网地址的数据包根本就到达不了对方,但仍然可以通过公网地址来建立通路。值得一提的是,虽然这些数据包通过某种方式验证,但是在不同NAT的情况下完全有可能会导致A往B 发送的信息发送到其他A内网网段中无关的结点上去的。

不一定要同时发向不同的地址,看那个先到,而是判断和尝试。

端点在多级NAT之后

在一些拓朴结构中,可能会存在多级NAT设备,在这种情况下,如果没有关于拓朴的具体信息,两个Peer要建立“最优”的P2P链接是不可能的。下面来说为什么?以下图为例:

                            Server S
                        18.181.0.31:1234
                               |
                               |
                             NAT X
                     A-S 155.99.25.11:62000
                     B-S 155.99.25.11:62001
                               |
                               |
        +----------------------+----------------------+
        |                                             |
      NAT A                                         NAT B
192.168.1.1:30000                             192.168.1.2:31000
        |                                             |
        |                                             |
     Client A                                      Client B
  10.0.0.1:1234                                 10.1.1.3:1234

假设NAT X是一个网络提供商ISP部署的工业级NAT,其下子网共用一个公网地址155.99.25.11,NAT A和NAT B分别是其下不同用户的网关部署的NAT。只有服务器S和NAT X有全局的路由地址。

Client A在NAT A的子网中,同时Client B在NAT B的子网中,每经过一级NAT都要进行一次网络地址转换。

现在假设A和B打算建立直接P2P链接,用一般的方法(通过Server S来打洞)自然是没问题的,那能不能优化呢?一种想当然的优化办法是A直接把信息发送给NAT B的内网地址192.168.1.2:31000,且B通过NAT B把信息发送给A的路由地址192.168.1.1:30000。不幸的是,A和B都没有办法得知这两个目的地址,因为S只看见了客户端公网地址155.99.25.11。 退一步说,即便A和B通过某种方法得知了那些地址,我们也无法保证他们是可用的。 因为ISP分配的子网地址可能和NAT A B分配的子网地址域相冲突。 因此客户端没有其他选择,只能使用S来进行打洞并进行回环传输。

UDP打洞技术有一个主要的条件:只有当两个NAT都是Cone NAT(或者非NAT的防火墙)时才能工作。因为其维持了一个给定的(内网IP,内网UDP)二元组 和(公网IP, 公网UDP)二元组固定的端口绑定,只要该UDP端口还在使用中,就不会变化。 如果像对称NAT一样,给每个新会话分配一个新的公网端口,就 会导致UDP应用程序无法使用跟外部端点已经打通了的通信链路。由于Cone NAT是当今最广泛使用的,尽管有一小部分的对称NAT是不支持打洞的,UDP打洞 技术也还是被广泛采纳应用。

TCP打洞(TCP Hole Punching)

关于TCP打洞,有一点需要提的是,因为TCP是基于连接的,所以任何未经连接而发送的数据都会被丢弃,这导致在recv的时候无法直接从peer端读取数据。其实这对UDP也一样,如果对UDP的socket进行了connect,其也会忽略连接之外的数据,详见connect(2)。

所以,如果我们要进行TCP打洞,通常需要重用本地的endpoint来发起新的TCP连接,这样才能将已经打开的NAT利用起来。具体来说,要设置socket的SO_REUSEADDRSO_REUSEPORT属性,根据系统不同,其实现也不尽一致。一般来说,TCP打洞的步骤如下:

由于TCP连接是由操作系统控制的,而不是由应用控制的,而且TCP包的序列号是随机生成的,所以TCP打洞的成功率就相对较低。因此如果NAT对接收到的包进行TCP序列号检测时若没有现有的连接可以对应, 该TCP包很可能会被NAT丢弃掉。

P2P实践经验

按照前述理论,自己写代码测试,发现ISP的NAT环境非常复杂,UDP打洞无法成功,而通过两台TPLINK路由器设置两个内网来打洞,确实可以成功。但TCP一直无法成功。

一点总结:

  1. Server可以拿到每个peer的local addr和outside addr;
  2. 通过比较peer的local addr,可以尝试判断这些peer是否就在一个IP可以互通的内网内部,这种尝试就是peer之间直接发起TCP连接;
  3. 如果TCP直连不能成功,再尝试用outside addr来打洞;
  4. 如果用outside addr打洞也失败,最后就是relay,relay总能成功,只是会占用server的资源。

TCP直连如果成功,就是P2P TCP;

outside addr打洞成功,就是P2P UDP;

ToDesk软件有如下几种连接方式:

p2p_connect.png

上面这个图,我没有想明白的是,当P2P失败,比如要relay的时候,为什么还要分TCP和UDP?

发Email后,ToDesk给出了回答:

您好,很多情况下,例如,跨运营商,跨省UDP包有可能会被运营商丢包,就是QOS,但是TCP不会。如果没有被qos的话,udp速度会比tcp快,但是有qos那么tcp就比tcp稳定。我们会动态根据网络情况进行调整协议,以保证用户最佳体验。

UDP快,TCP稳!

当我们在尝试TCP P2P的时候,一直无法成功,有一台主机会收到connection refuse,这是网关收到了SYN后,直接回复RST的现象:

[root@astute p2p]# tcpdump -i ens3 -nnvv tcp and  host 192.222.9.202
tcpdump: listening on ens3, link-type EN10MB (Ethernet), capture size 262144 bytes

17:20:13.135901 IP (tos 0x0, ttl 64, id 17775, offset 0, flags [DF], proto TCP (6), length 60)
    192.168.1.103.48136 > 192.222.9.202.57242: Flags [S], cksum 0x8ce6 (incorrect -> 0xb575), seq 525634756, win 29200, options [mss 1460,sackOK,TS val 2186987947 ecr 0,nop,wscale 7], length 0
17:20:13.137574 IP (tos 0x0, ttl 63, id 17135, offset 0, flags [none], proto TCP (6), length 40)
    192.222.9.202.57242 > 192.168.1.103.48136: Flags [R.], cksum 0xdb5b (correct), seq 0, ack 525634757, win 0, length 0

本文链接:https://cs.pynote.net/net/202206011/

-- EOF --

-- MORE --