对于VoIP来说,NAT,特别是对称型NAT会产生很多问题。而WebRTC中就有解决这些问题的工具。
WebRTC可在web浏览器之间建立端对端的连接。为了做到这一点,WebRTC采用了ICE(交互式连接建立)这套技术。ICE允许某些执行NAT(网络地址转换)路由器所代表的客户端建立直接连接。(欲知详情,请参考WebRTC glossary entry。)其中最重要的是让客户端找到公共IP地址。为实现这一需求,客户端需向STUN服务器询问其IP地址。
NAT是连接我们本地个人网络和公共互联网的一个个盒子。NAT通过把我们使用的内部IP地址,转换为公共地址来实现连接。不同的NAT工作方式也不尽相同。这就使得WebRTC需要靠STUN和TURN来同时连接呼叫。有关这一点的详细背景,大家可以参考一些之前发布的相关文章,比如这篇和这篇。
在学习Tsahi Levent-Levi的WebRTC架构——NAT穿透一课时,我从下面这张ppt中(重新)理解了对称型NAT的含义。
如果你向两个不同的STUN服务器询问自己的公共IP地址,对称型NAT会返回两个相同的IP地址(理想情况),但实际上两个地址的端口不同。所以我们需要一个TURN服务器来解决这一问题。因为一般来说,对称型NAT不允许建立直接连接。
几年前,在Tokbox演讲时,我谈到了一些提高连接率的技术(详见演讲视频和PPT)。Tsahi的PPT让我不禁思考,我们是否也能测试这样的对称型NAT场景,即每个STUN绑定时,都得到一个不同的返回端口呢?
经过一些测试,我得到了肯定的结论。是的,我们可以测试。
第一步是向两个STUN服务器询问我们的IP地址。我们需要将以下配置传送给服务器。
var pc = new RTCPeerConnection({iceServers: [ {urls: ‘stun:stun1.l.google.com:19302’}, {urls: ‘stun:stun2.l.google.com:19302’} ]})
然后,我们创建一条数据通道,使端对端连接只产生一个本地candidate。
pc.createDataChannel("webrtchacks")
之后,我们查看onicecandidate事件,并解析我们得出的candidate(需要用到我SDP模块的一个辅助函数)。如果candidate属于srflx类型,我们要注意两个端口——一个是NAT设备翻译后的端口,一个是翻译前的端口。
pc.onicecandidate = function(e) { if (e.candidate && e.candidate.candidate.indexOf('srflx') !== -1) { var cand = parseCandidate(e.candidate.candidate); if (!candidates[cand.relatedPort]) candidates[cand.relatedPort] = []; candidates[cand.relatedPort].push(cand.port); } else if (!e.candidate) { if (Object.keys(candidates).length === 1) { var ports = candidates[Object.keys(candidates)[0]]; console.log(ports.length === 1 ? 'cool nat' : 'symmetric nat'); } } };
接下来,我们调用createOffer和setLocalDescription来开始收集:
pc.createOffer() .then(offer => pc.setLocalDescription(offer))
在完成candidate收集后,我们就会得到一个没有设置event.candidate的icecandidate事件。针对我们得到的candidate,有3种选择:
1. 如果我们只有一个candidate,浏览器不会再返回第二个STUN服务器的响应给我们,因为该服务器里包含了和第一个服务器一样端口。也就是,这里不是对称型的 NAT。
2. 如果我们有两个相同relatedPort却不同端口的candidate,那我们就属于一个对称型NAT。
3. 如果我们一个srflx candidate,就说明UDP被阻止了。这意味着我们需要一个TURN/TCP或TURN/TLS服务器(例如这种服务器)。
我们应该如何测试呢?iPhone的个人热点属于对称型NAT,这对我们帮助很大。点击此处,即可在fiddle中在线测试。
// parseCandidate from https://github.com/fippo/sdp function parseCandidate(line) { var parts; // Parse both variants. if (line.indexOf('a=candidate:') === 0) { parts = line.substring(12).split(' '); } else { parts = line.substring(10).split(' '); } var candidate = { foundation: parts[0], component: parts[1], protocol: parts[2].toLowerCase(), priority: parseInt(parts[3], 10), ip: parts[4], port: parseInt(parts[5], 10), // skip parts[6] == 'typ' type: parts[7] }; for (var i = 8; i < parts.length; i += 2) { switch (parts[i]) { case 'raddr': candidate.relatedAddress = parts[i + 1]; break; case 'rport': candidate.relatedPort = parseInt(parts[i + 1], 10); break; case 'tcptype': candidate.tcpType = parts[i + 1]; break; default: // Unknown extensions are silently ignored. break; } } return candidate; }; var candidates = {}; var pc = new RTCPeerConnection({iceServers: [ {urls: 'stun:stun1.l.google.com:19302'}, {urls: 'stun:stun2.l.google.com:19302'} ]}); pc.createDataChannel("foo"); pc.onicecandidate = function(e) { if (e.candidate && e.candidate.candidate.indexOf('srflx') !== -1) { var cand = parseCandidate(e.candidate.candidate); if (!candidates[cand.relatedPort]) candidates[cand.relatedPort] = []; candidates[cand.relatedPort].push(cand.port); } else if (!e.candidate) { if (Object.keys(candidates).length === 1) { var ports = candidates[Object.keys(candidates)[0]]; console.log(ports.length === 1 ? 'normal nat' : 'symmetric nat'); } } }; pc.createOffer() .then(offer => pc.setLocalDescription(offer))
那么,知晓自己是否遇到了对称型NAT有什么用吗?这不得而知。我们现在可以建立一个NAT类型检测器。我并不确定这有什么实际用途,毕竟 WebRTC的ICE机制是一种可以找到连接,且不需要你担心细节的实现。但谁知道呢。不说别的,这确实是一项有趣的知识,起码可以帮助你了解WebRTC中NAT穿透的奥秘。
原文作者:Philipp Hancke