UDP和TCP打洞 - daniel-qa/Network GitHub Wiki

UDP和TCP打洞

为什么网上讲到的P2P打洞基本上都是基于UDP协议的打洞?难道TCP不可能打洞?还是TCP打洞难于实现?

假设现在有内网客户端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做心跳,这样,双方的通信通道都是通的,就可以进行任意的通信了。

但是 TCP和 UDP在打洞上却有点不同。这是因为**伯克利socket(标准socket规范)**的 API造成的。

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了,这样打洞就无法成功。

UDP打洞的过程大致如此

1、双方都通过UDP与服务器通讯后,网关默认就是做了一个外网IP和端口号 与你内网IP与端口号的映射,这个无需设置的,服务器也不需要知道客户的真正内网IP

2、用户A先通过服务器知道用户B的外网地址与端口

3、用户A向用户B的外网地址与端口发送消息,

4、在这一次发送中,用户B的网关会拒收这条消息,因为它的映射中并没有这条规则。

5、但是用户A的网关就会增加了一条允许规则,允许接收从B发送过来的消息

6、服务器要求用户B发送一个消息到用户A的外网IP与端口号

7、用户B发送一条消息,这时用户A就可以接收到B的消息,而且网关B也增加了允许规则

8、之后,由于网关A与网关B都增加了允许规则,所以A与B都可以向对方的外网IP和端口号发送消息

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连接

从应用程序角度看,连接成功的过程可能有两种不同表现:(以上述假设过程为例)

1、连接建立成功表现为 A 的connect 返回成功。即A端以TCP的同时打开流程完成连接

2、A 端通过l isten 的端口完成和 B 的握手而 connect尝试持续失败,应用程序通过 accept 获取到连接,最终放弃 connect(这时可closesocket(conn_fd))。 多数Linux和Windows的协议栈表现为第2种。

但有一个问题是,建立连接的client端,其 connect绑定的端口号就是主机listen的端口号,或许这个peer后续还会有更多的这种socket。

虽然理论上说,socket是一个五元组,端口号是一个逻辑数字,传输层能够因为五元组的不同而区分开这些socket,但是是否存在实际上的异常,还有待更多观察。

一些现在常用的技术:

ALG(应用层网关):它可以是一个设备或插件,用于支持SIP协议,主要类似与在网关上专门开辟一个通道,用于建立内网与外网的连接,也就是说,这是一种定制的网关。更多只适用于使用他们的应用群体内部之间。

UpnP:它是让网关设备在进行工作时寻找一个全球共享的可路由IP来作为通道,这样避免端口造成的影响。要求设备支持且开启upnp功能,但大部分时候,这些功能处于安全考虑,是被关闭的。即时开启,实际应用效果还没经过测试。

STUN(Simple Traversalof UDP Through Network):这种方式即是类似于我们上面举例中服务器C的处理方式。也是目前普遍采用的方式。但具体实现要比我们描述的复杂许多,光是做网关Nat类型判断就由许多工作,RFC3489中详细描述了。

TURN(Traveral Using Relay NAT):该方式是将所有的数据交换都经由服务器来完成,这样NAT将没有障碍,但服务器的负载、丢包、延迟性就是很大的问题。目前很多游戏均采用该方式避开NAT的问题。这种方式不叫p2p。

ICE(Interactive Connectivity Establishment):是对上述各种技术的综合,但明显带来了复杂性。