P2P技术之STUN、TURN、ICE详解
现在大多数计算机主机都位于防火墙或NAT之后,很少有计算机直接接入Internet。通常,人们希望网络中两天计算机能直接进行通信(P2P通信),而不是需要其他公共服务器的中转。
由于主机位于防火墙或NAT之后,在进行P2P通信之前,需要进行检测确认它们间能否进行P2P通信及如何通信。这种技术通常称为NAT穿透(NAT Traversal)。最常见的NAT穿透是基于UDP技术,如RFC3489中定义的STUN协议。
NAT对待UDP的实现方式:
- Full Cone NAT:
完全锥形NAT,所有从同一个内网IP和端口号发送过来的请求都会被映射成同一个外网IP和端口号,并且任何一个外网主机都可以通过这个映射的外网IP和端口号向这台内网主机发送包。 - Restricted Cone NAT:
限制锥形NAT,它也是所有从同一个内网IP和端口号发送过来的请求都会被映射成同一个外网IP和端口号。与完全锥形不同的是,外网主机只能够向先前已经向它发送过数据包的内网主机发送包。 - Port Restricted Cone NAT:
端口限制锥形NAT,与限制锥形NAT很相似,只不过它包括端口号。也就是说,一台IP地址X和端口P的外网主机想给内网主机发送包,必须是这台内网主机先前已经给这个IP地址X和端口P发送过数据包。 - Symmetric NAT:
对称NAT,所有从同一个内网IP和端口号发送到一个特定的目的IP和端口号的请求,都会被映射到同一个IP和端口号。如果同一台主机使用相同的源地址和端口号发送包,但是发往不同的目的地,NAT将会使用不同的映射。此外,只有收到数据的外网主机才可以反过来向内网主机发送包。
一、STUN详解
STUN,首先在RFC3489中定义,作为一个完整的NAT穿透解决方案,英文全称是Simple Traversal of UDP Through NATs,即简单的用UDP穿透NAT。是个轻量级的协议,是基于UDP的完整的穿透NAT的解决方案。它允许应用程序发现它们与公共互联网之间存在的NAT和防火墙及其他类型。它也可以让应用程序确定NAT分配给它们的公网IP地址和端口号。
在新的RFC5389修订中把STUN协议定位于为传统NAT提供工具,而不是一个完整的解决方案,即NAT会话穿透效用(Session Traversal Utilities for NAT)。RFC5389与RFC3489除了名称变化外,最大的区别是支持TCP穿透。
1、RFC3489/STUN
STUN是一种Client/Server的协议,也是一种Request/Response的协议,默认端口号是3478。
(1)报文结构
【Ø 消息头】
所有的STUN消息都包含20个字节的消息头:16位的消息类型(MessageType)、16位的消息长度(MessageLength)、128位的事务ID(Transaction ID)。
消息类型许可值:0x0001(捆绑请求)、0x0101(捆绑响应)、0x0111(捆绑错误响应)、0x0002(共享私密请求)、0x0102(共享私密响应)、0x0112(共享私密错误响应)。
消息长度:消息大小的字节数,不包括20字节的头部。
事务ID:128位的标识符,用于随机请求和响应,请求与其相应的所有响应具有相同的标识符。
【Ø 消息属性】
消息头之后是0或多个属性,每个属性进行TLV编码,包括16位的属性类型、16位的属性长度和变长属性值。
属性类型定义:
- MAPPED-ADDRESS:MAPPED-ADDRESS属性表示映射过的IP地址和端口。它包括8位的地址族,16位的端口号及长度固定的IP地址。
- RESPONSE-ADDRESS:RESPONSE-ADDRESS属性表示响应的目的地址
- CHASNGE-REQUEST:客户使用32位的CHANGE-REQUEST属性来请求服务器使用不同的地址或端口号来发送响应。
- SOURCE-ADDRESS:SOURCE-ADDRESS属性出现在捆绑响应中,它表示服务器发送响应的源IP地址和端口。
- CHANGED-ADDRESS:如果捆绑请求的CHANGE-REQUEST属性中的“改变IP”和“改变端口”标志设置了,则CHANGED-ADDRESS属性表示响应发出的IP地址和端口号。
- USERNAME:USERNAME属性用于消息的完整性检查,用于消息完整性检查中标识共享私密。USERNAME通常出现在共享私密响应中,与PASSWORD一起。当使用消息完整性检查时,可有选择地出现在捆绑请求中。
- PASSWORD:PASSWORD属性用在共享私密响应中,与USERNAME一起。PASSWORD的值是变长的,用作共享私密,它的长度必须是4字节的倍数,以保证属性与边界对齐。
- MESSAGE-INTEGRITY:MESSAGE-INTEGRITY属性包含STUN消息的HMAC-SHA1,它可以出现在捆绑请求或捆绑响应中;MESSAGE-INTEGRITY属性必须是任何STUN消息的最后一个属性。它的内容决定了HMAC输入的Key值。
- ERROR-CODE:ERROR-CODE属性出现在捆绑错误响应或共享私密错误响应中。它的响应号数值范围从100到699。
- UNKNOWN-ATTRIBUTES:UNKNOWN-ATTRIBUTES属性只存在于其ERROR-CODE属性中的响应号为420的捆绑错误响应或共享私密错误响应中。
- REFLECTED-FROM:REFLECTED-FROM属性只存在于其对应的捆绑请求包含RESPONSE-ADDRESS属性的捆绑响应中。属性包含请求发出的源IP地址,它的目的是提供跟踪能力,这样STUN就不能被用作DOS攻击的反射器。
具体的ERROR-CODE(响应号),与它们缺省的原因语句一起,目前定义如下:
- 400(错误请求):请求变形了。客户在修改先前的尝试前不应该重试该请求。
- 401(未授权):捆绑请求没有包含MESSAGE-INTERITY属性。
- 420(未知属性):服务器不认识请求中的强制属性。
- 430(过期资格):捆绑请求没有包含MESSAGE-INTEGRITY属性,但它使用过期
- 的共享私密。客户应该获得新的共享私密并再次重试。
- 431(完整性检查失败):捆绑请求包含MESSAGE-INTEGRITY属性,但HMAC验
- 证失败。这可能是潜在攻击的表现,或者客户端实现错误
- 432(丢失用户名):捆绑请求包含MESSAGE-INTEGRITY属性,但没有
- USERNAME属性。完整性检查中两项都必须存在。
- 433(使用TLS):共享私密请求已经通过TLS(Transport Layer Security,即安全
- 传输层协议)发送,但没有在TLS上收到。
- 500(服务器错误):服务器遇到临时错误,客户应该再次尝试。
- 600(全局失败):服务器拒绝完成请求,客户不应该重试。
属性空间分为可选部分与强制部分,值超过0x7fff的属性是可选的,即客户或服务器即使不认识该属性也能处理该消息;值小于或等于0x7fff的属性是强制理解的,即除非理解该属性,否则客户或服务器就不能处理该消息。
(2)实现原理
STUN协议的完整交互过程如下所示:
STUN协议具体实现步骤:
一般情况下,客户会配置STUN服务器提供者的域名,该域名被解析为IP地址和SRV过程的端口号。
服务器名是“stun”,使用UDP协议发送捆绑请求,使用TCP协议发送共享私密请求。STUN协议的缺省端口号为3478。若要提供完整性检查,STUN在客户和服务器间使用128位的共享私密,作为捆绑请求和捆绑响应中的密匙。
首先,客户通过发现过程获得它将与之建立TCP连接的IP地址和端口号。客户打开该地址和端口的连接,开始TLS协商,验证服务器的标识。客户发送共享私密请求。该请求没有属性,只有头。服务器生成响应。客户会在该连接上生成多个请求,但在获得用户名和密码后关闭该连接。
其次,服务器收到共享私密请求,验证从TLS连接上到达的该请求;如果不是通过TLS收到的请求,则生成共享私密错误响应,并设置ERROR-CODE属性为响应号433;这里区分两种情况:若通过TCP收到请求,则错误响应通过收到请求的相同连接发送;若通过UDP收到请求,则错误响应发送回请求送出的源IP和端口。
服务器检查请求中的任何属性,当其中有不理解的小于或等于0x7fff的值,则生成共享私密错误响应,设置ERROR-CODE属性为响应号420,并包括UNKNOWN-ATTRIBUTE属性,列出它不理解的小于或等于0x7fff的属性的值。该错误响应通过TLS连接发送。
然后,若请求正确,服务器创建共享私密响应,包含与请求中相同的事务ID,并包含USERNAME和PASSWORD属性。用户名在10分钟内有效。共享私密响应通过与收到请求的相同的TLS连接发送,服务器保持连接打开状态,由客户关闭它。
接着,客户发送捆绑请求,携带的属性包括:
- 可选属性:RESPONSE-ADDRESS属性和CHANGE-REQUEST属性;
- 强制属性:MESSAGE-INTEGRITY属性和USERNAME属性。
客户发送捆绑请求,通过客户重传来提供可靠性。客户开始用100ms的间隔重传,每次重传间隔加倍,直至1.6秒。之间间隔1.6秒的重传继续,直到收到响应或总共已经发送了9次。因此,若9500ms后,还未收到响应,客户认为传输已经失败。
随后,服务器检查捆绑请求的MESSAGE-INTEGRITY属性,不存在则生成捆绑错误响应,设置ERROR-CODE属性为响应号401;若存在,计算请求的HMACKey值。
服务器检查USERNAME属性,不存在则生成捆绑错误响应,设置ERROR-CODE属性为响应号432;若存在,但不认识该USERNAME的共享私密(例如,它超时了),生成捆绑错误响应,设置ERROR-CODE属性为响应号430。
服务器如知道该共享私密,计算HMAC与请求是否相同,如果相同生成捆绑错误响应,设置ERROR-CODE属性为响应号431。
假设消息完整性检查通过了,服务器检查请求中的任何属性的值,若遇到不理解的小于或等于0x7fff的值,生成捆绑错误响应,设置ERROR-CODE属性为响应号420,该响应包含UNKNOWN-ATTRIBUTE属性,并列出不理解的小于或等于0x7fff的属性。
若请求正确,服务器生成单个捆绑响应,包含与捆绑请求相同的事务ID。服务器在捆绑响应中加入MAPPED-ADDRESS属性,该属性的IP地址和端口号为捆绑请求的源IP地址和端口号。
捆绑响应的源地址和端口号取决于捆绑请求中CHANGE-REQUEST属性的值及捆绑请求收到的地址和端口号相关。
(3)总结
服务器在捆绑响应中加入SOURCE-ADDRESS属性,包含用于发送捆绑响应的源地址和端口号;加入CHANGED-ADDRESS属性,包含源IP地址和端口号。
如果捆绑请求中包含了USERNAME和MESSAGE-INTEGRITY属性,则服务器在捆绑响应中加入MESSAGE-INTEGRITY属性。
如果捆绑请求包含RESPONSE-ADDRESS属性,则服务器在捆绑响应中加入REFLECTED-FROM属性:如果捆绑请求使用从共享私密请求获得的用户名进行认证,则REFLECTED-FROM属性包含共享私密请求到达的源IP地址和端口号;若请求中的用户名不是使用共享私密分配的,则REFLECTED-FROM属性包含获得该用户名的实体的源IP地址和端口号;若请求中没有用户名,且服务器愿意处理该请求,则REFLECTED-FROM属性包含请求发出的源IP地址和端口号。
服务器不会重传响应,可靠性通过客户周期性地重发请求来保障,每个请求都会触发服务器进行响应。
客户端判断响应的类型是捆绑错误响应还是捆绑响应。捆绑错误响应通常在请求发送的源地址和端口收到;捆绑响应通常在请求中的RESPONSE-ADDRESS属性的地址和端口收到,若没有该属性,则捆绑响应将在请求发送的源地址和端口号收到。
- 若是捆绑错误响应,客户检查响应中的ERROR-CODE属性的响应号:400至499之间的未知属性按属性400处理,500至599之间的未知属性按500处理,600至699之间的未知属性按600处理。任何100和399之间的响应都会使请求重传中止,但其他则忽略;若客户收到响应的属性类型大于0x7fff,则忽略该属性,若小于或等于0x7fff,则请求重传停止,并忽略整个响应;
- 若是捆绑响应,客户检查响应的MESSAGE-INTEGRITY属性:如果不存在,客户在请求中加入MESSAGE-INTEGRITY属性,并放弃该响应;如果存在,客户计算响应的HMAC。如果计算出的HMAC与响应中的不同,则放弃该响应,并警告客户可能受到了攻击;若计算出的HMAC与响应中的匹配,则过程继续;
- 不论收到捆绑响应还是捆绑错误响应,都将中止该请求的重传。客户在第一次响应后继续监听捆绑请求的响应10秒钟,如果这期间它收到任何消息类型不同的响应或不同的MAPPED-ADDRESS属性,它将警告用户可能受到攻击;并且,如果客户收到的捆绑响应次数超过它发送的捆绑请求数的两倍,它将警告用户可能受到攻击;若捆绑响应经过认证,上述攻击并未导致客户丢弃MAPPED-ADDRESS,则客户可以使用该MAPPED-ADDRESS和SOURCE-ADDRESS属性。
(4)STUN功能举例
客户通过带外方式获得STUN服务器信息后,就打开对应的地址和端口的连接,并开始与STUN服务器进行TLS协商。一旦打开了连接,客户就通过TCP协议发送共享私密请求,服务器生成共享私密响应。STUN在客户和服务器间使用共享私密,用作捆绑请求和捆绑响应中的密匙。之后,客户使用UDP协议向STUN服务器发送捆绑请求,当捆绑请求消息到达服务器的时候,它可能经过了一个或者多个NAT。结果是STUN服务器收到的捆绑请求消息的源IP地址被映射成最靠近STUN服务器的NAT的IP地址,STUN服务器把这个源IP地址和端口号复制到一个捆绑响应消息中,发送回拥有这个IP地址和端口号的客户端。
当STUN客户端收到捆绑响应消息之后,它会将自己发送捆绑请求时绑定的本地IP地址和端口号同捆绑响应消息中的IP地址和端口号进行比较,如果不匹配,就表示客户端正处于一个或者多个NAT的前面。
在Full-Cone NAT的情况下,在捆绑响应消息中的IP地址和端口是属于公网的,公网上的任何主机都可以使用这个IP地址和端口号向这个应用程序发送数据包,应用程序只需要在刚才发送捆绑请求的IP地址和端口上监听即可。
当然,客户可能并不在一个Full-Cone NAT的前面,实际上,它并不知道自己在一个什么类型的NAT的前面。为了确定NAT的类型,客户端使用附加的捆绑请求。具体过程是很灵活的,但一般都会像下面这样工作:客户端再发送一个捆绑请求,这次发往另一个IP地址,但是使用的是跟上一次同一个源IP地址和源端口号,如果返回的数据包里面的IP地址和端口号和第一次返回的数据包中的不同,客户端就会知道它是在一个对称NAT的前面。客户端为了确认自己是否在一个完全锥形NAT的前面,客户端可以发送一个带有标志的捆绑请求,这个标志告诉服务器使用另一个IP地址和端口发送捆绑响应。换句话说,如果客户端使X/Y的IP地址端口对向A/B的IP地址端口对发送捆绑请求,服务器就会使用源IP地址和源端口号为C/D的地址端口对向X/Y发送捆绑响应。如果客户端收到了这个响应,它就知道它是在一个Full-Cone NAT前面。
STUN协议允许客户端请求服务器从收到捆绑请求的IP地址往回发捆绑响应,但是要使用不同的端口号。这可以用来检查客户端是否在Port Restricted Cone NAT的前面还是在Restricted Cone NAT的前面。
2、RFC5389/STUN
http://www.52im.net/thread-557-1-1.html
二、TURN详解