我是不是遇到了对称型 NAT

对于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穿透的奥秘。

文章地址:https://webrtchacks.com/symmetric-nat/

原文作者:Philipp Hancke

填写常用邮箱,接收社区更新

WebRTC 中文社区由

运营