STUN协议详解(webrtc stun通信交互)

STUN协议

STUN是一个C/S架构的协议,支持两种传输类型。一种是请求/响应(request/respond)类型,由客户端给服务器发送请求,并等待服务器返回响应;另一种是指示类型(indication transaction),由服务器或者客户端发送指示,另一方不产生响应。

两种类型的传输都包含一个96位的随机数作为事务ID(transaction ID),对于请求/响应类型,事务ID允许客户端将响应和产生响应的请求连接起来。

STUN报文头部

STUN头部包含了STUN消息类型,magic cookie,事务ID和消息长度,如下:

图片

// rfc 3489 11.1 Message Header (p25)
struct stun_header_t
{
  uint16_t msgtype;
  uint16_t length; // payload length, don't include header
  uint32_t cookie; // rfc 5398 magic cookie
  uint8_t tid[12]; // transaction id
};

最高的2位必须置零,这可以在当STUN和其他协议复用的时候,用来区分STUN包和其他数据包。

bool is_stun(const uint8_t* data, size_t len)
{
   return len > 0 && (data[0] == 0 || data[0] == 1);
 }
 0                   1                   2                   3
   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |0 0|     STUN      msgType     |         Message Length        |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                         Magic Cookie                          |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                                                               |
  |                     Transaction ID (96 bits)                  |
  |                                                               |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

STUN msgtype字段定义了消息的类型(请求/成功响应/失败响应/指示)和消息的主方法。

图片

STUN length字段存储了信息的长度,以字节为单位,不包括20字节的STUN头部。由于所有的STUN属性都是都是4字节对齐(填充)的。

STUN cookie字段包含固定值0x2112A442

STUN tid ID字段是个96位的标识符,用来区分不同的STUN传输事务。对于request/response传输,事务ID由客户端选择,服务器收到后以同样的事务ID返回response;对于indication则由发送方自行选择。事务ID的主要功能是把request和response联系起来,同时也在防止攻击方面有一定作用。服务端也把事务ID当作一个Key来识别不同的STUN客户端,因此必须格式化且随机在0~2^(96-1)之间。本人就用这个 tid 作为识别客户端的唯一。把客户端的指针地址转为intptr_t类型。


static inline void be_write_uint64(uint8_t* ptr, intptr_t val)
{
  ptr[0] = (uint8_t)((val >> 56) & 0xFF);
  ptr[1] = (uint8_t)((val >> 48) & 0xFF);
  ptr[2] = (uint8_t)((val >> 40) & 0xFF);
  ptr[3] = (uint8_t)((val >> 32) & 0xFF);
  ptr[4] = (uint8_t)((val >> 24) & 0xFF);
  ptr[5] = (uint8_t)((val >> 16) & 0xFF);
  ptr[6] = (uint8_t)((val >> 8) & 0xFF);
  ptr[7] = (uint8_t)(val & 0xFF);
}
be_write_uint64(msg->header.tid ,(intptr_t)ice);

然后在接收stun数据的时候再次获取传进去的唯一值。


static inline void be_read_uint64(const uint8_t* ptr, intptr_t* val)
{
  *val = (((uint64_t)ptr[0]) << 56) | (((uint64_t)ptr[1]) << 48)
    | (((uint64_t)ptr[2]) << 40) | (((uint64_t)ptr[3]) << 32)
    | (((uint64_t)ptr[4]) << 24) | (((uint64_t)ptr[5]) << 16)
    | (((uint64_t)ptr[6]) << 8) | ptr[7];
}
intptr_t stun_get_ice_from_tid(const void* data, int bytes)
{
  intptr_t ice= 0;
  be_read_uint64(&header.tid, &ice);
  return ice;
}

这样ice做为单端口就能解决一一对应关系。

STUN属性

在STUN报文头部之后,通常跟着0个或者多个属性,每个属性必须是TLV编码的(Type-Length-Value)。其中Type字段和Length字段都是16位,Value字段为为32位表示。

type值在0x0000-0x7FFF之间的属性被指定为强制理解,意思是STUN终端必须要理解此属性,否则将返回错误信息;而0x8000-0xFFFF之间的属性为选择性理解,即如果STUN终端不识别此属性则将其忽略。


struct stun_attr_t
{
  uint16_t type;
  uint16_t length;
  union
  {
    uint8_t          u8;
    uint16_t        u16;
    uint32_t        u32;
    uint64_t        u64;
    void*          ptr;
    uint8_t                 sha1[20];
    struct sockaddr_storage addr; // MAPPED-ADDRESS/XOR-MAPPED-ADDRESS
    struct {
      uint32_t      code;
      char*        reason_phrase;
    } errcode;
  } v;

  int unknown; // read(decode) only, -1-unknown attribute, other-valid
};

Length字段必须包含Value部分需要补齐的长度,以字节为单位。由于STUN属性以32bit边界对齐,因此属性内容不足4字节的都会以padding bit进行补齐。

padding bit会被忽略,但可以是任何值。


struct stun_message_t
{
  struct stun_header_t header;

  struct stun_attr_t attrs[32];
  int nattrs;
};
int stun_message_add_string(struct stun_message_t* msg, uint16_t attr, const char* value)
{
  msg->attrs[msg->nattrs].type = attr;
  msg->attrs[msg->nattrs].length = (uint16_t)strlen(value);
  msg->attrs[msg->nattrs].v.ptr = (void*)value;
  msg->header.length += 4 + ALGIN_4BYTES(msg->attrs[msg->nattrs].length);
  msg->nattrs += 1;
  return 0;
}
eg:
stun_message_add_string(msg, STUN_ATTR_SOFTWARE, STUN_SOFTWARE);

属性类型定义如下:

  • 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的属性是强制理解的,即除非理解该属性,否则客户或服务器就不能处理该消息。

STUN 通信过程

产生一个Request或Indication

当产生一个Request或者Indication报文时,终端必须根据上文提到的规则来生成头部,注意在发送Request报文时候,需要加上SOFTWARE属性(内含软件版本描述)。(流媒体服务端向stun服务发送)

图片

接收STUN消息

当STUN终端接收到一个STUN报文时,首先检查报文的规则是否合法,即前两位是否为0,magic cookie是否为0x2112A442,报文长度是否正确以及对应的方法是否支持。

如果消息类别为Success/Error Response,终端会检测其事务ID是否与当前正在处理的事务ID相同。如果使用了FINGERPRINT拓展的话还会检查FINGERPRINT属性是否正确。

完成身份认证检查之后,STUN终端会接着检查其余未知属性。

图片

6 客户端与流媒体的STUN交互过程

6.1 交互过程

6.1.1 在WebRTC中,STUN客户端内置在浏览器用户代理中,在会话建立之前,先发送stun测试报文,以便浏览器确定其是否位于NAT之后并发现映射地址和端口。
携带的属性包括:

  • 可选属性:RESPONSE-ADDRESS属性和CHANGE-REQUEST属性;
  • 强制属性:MESSAGE-INTEGRITY属性和USERNAME属性。
  • 图片

6.1.2 当STUN服务器收到STUN Binding请求时,它会记录Binding请求来自哪个IP地址和端口号,此地址和端口号随后将以STUN Binding响应的形式返回客户端。(通过XOR-MAPPED-ADDRESS属性)。

STUN协议详解(webrtc stun通信交互)

6.1.3 客户端将响应中发来的IP地址和端口与其发送的IP地址和端口进行比较,以此来判断客户端和服务器之间有没有NAT,若不同,则说明至少有一个NAT,客户端能够识别由最外层的NAT分配的IP地址和端口。存在多个NAT时,STUN只能识别最外层NAT的相关信息。

6.1.4 STUN服务器将源传输地址复制到STUN Binding响应中XOR-MAPPED-ADDRESS属性中,并将绑定响应发送回STUN客户端。当这个数据包通过NAT返回时,NAT将修改IP报头中的目的传输地址,但是STUN响应主体中XOR-MAPPED-ADDRESS属性中的传输地址将保持不变。通过这种方式,客户端可以了解最外面的NAT相对于STUN服务器分配的反射传输地址。

6.2 保活机制

在ICE中,STUN协议用于连通性检查和ICE保活,客户端需要按固定间隔以指示形式发送STUN Binding请求,防止NAT映射超时,指示型请求可使NAT重置其UDP定时器,发送间隔短于NAT UDP定时器设置,就可以保持NAT映射。

6.3 身份验证机制

服务器检查捆绑请求的MESSAGE-INTEGRITY属性,不存在则生成捆绑错误响应,设置ERROR-CODE属性为响应号401;若存在,计算请求的HMACKey值。

6.3.1 短期身份验证

短期身份验证采用用户名/密码方式,此机制适用于单个会话。ICE使用此方法对每组连接检查应用不同的身份验证。短期身份验证机制假设在STUN事务之前,客户端和服务器已经使用了其他协议来交换了证书,以username和password形式。这个证书是有时间限制的。在WebRTC中通过SDP进行相关信息的交互。


static int stun_request_short_term_auth_check(stun_agent_t* stun, struct stun_request_t* req, const void* data, int bytes)
{
  int r;
  const struct stun_attr_t* integrity;
  integrity = stun_message_attr_find(&req->msg, STUN_ATTR_MESSAGE_INTEGRITY);

  // If the message does not contain both a MESSAGE-INTEGRITY and a USERNAME attribute
  if (0 == req->auth.usr[0] && !integrity)
  {
    stun_auth_response(stun, req, 400, "Bad Request", NULL, NULL);
    return 400;
  }
  else if (0 == req->auth.usr[0] || !integrity)
  {
    stun_auth_response(stun, req, 401, "Unauthorized", NULL, NULL);
    return 401;
  }

  r = stun->handler.auth(stun->param, req->auth.credential, req->auth.usr, NULL, NULL, req->auth.pwd);
  if (0 != r || 0 != stun_message_check_integrity(data, bytes, &req->msg, &req->auth))
  {
    stun_auth_response(stun, req, 401, "Unauthorized", NULL, NULL);
    return 401;
  }

  return 0;
}

6.3.2长期身份验证

长期身份验证采用质询/响应机制,允许使用长期凭据。长期身份验证机制依赖于一个长期证书,username和password在客户端和服务器中是共用的。这个证书从它提供给用户开始将一直是有效的,直到该用户不再是该系统的用户。

客户端初始发送一个请求,没有提供任何证书和任何完整性检测。服务器拒绝这个请求,并提供给用户一个范围(用于指导用户或代理选择username和password)和一个nonce。这个nonce提供重放保护。它是一个cookie,由服务器选择,以这样一种方式来标示有效时间或客户端身份是有效的。客户端重试这个请求,这次包括它的username和realm和服务器提供的nonce来回应。服务器确认这个nonce和检查这个message integrity。如果它们匹配,请求则通过认证。如果这个nonce不再有效,即过期了,服务器就拒绝该请求,并提供一个新的nonce。在随后的到同一服务器的请求,客户端重新使用这个nonce、username和realm,和先前使用的password。这样,随后的请求不会被拒绝直到这个nonce变成无效的。需要注意的是,长期证书机制不能用来保护Indications,由于Indications不能被改变,因此,使用Indications时要么使用短期证书,要么就省略认证和消息完整性。因为长期证书机制对离线字典攻击敏感,部署的时候应该使用很难猜测的密码。

// rfc5389 10.2.2. Receiving a Request(p26)
static int stun_request_long_term_auth_check(stun_agent_t* stun, struct stun_request_t* req, const void* data, int bytes)
{
  int r;
  const struct stun_attr_t* integrity;
  integrity = stun_message_attr_find(&req->msg, STUN_ATTR_MESSAGE_INTEGRITY);

  // If the message does not contain both a MESSAGE-INTEGRITY attribute
  if (!integrity)
  {
    stun->handler.getnonce(stun->param, req->auth.realm, req->auth.nonce);
    stun_auth_response(stun, req, 401, "Unauthorized", req->auth.realm, req->auth.nonce);
    return 401;
  }
  else if (0 == req->auth.usr[0] || 0 == req->auth.realm[0] || 0 == req->auth.nonce[0])
  {
    // If the message contains a MESSAGE-INTEGRITY attribute, but is missing the 
    // USERNAME, REALM, or NONCE attribute, the server MUST generate an error 
    // response with an error code of 400 (Bad Request). This response SHOULD NOT 
    // include a USERNAME, NONCE, REALM, or MESSAGE-INTEGRITY attribute.
    stun_auth_response(stun, req, 400, "Bad Request", NULL, NULL);
    return 400;
  }

  req->auth.credential = STUN_CREDENTIAL_LONG_TERM;
  r = stun->handler.auth(stun->param, req->auth.credential, req->auth.usr, req->auth.realm, req->auth.nonce, req->auth.pwd);
  if (0 != r)
  {
    // If the NONCE is no longer valid, the server MUST generate an error 
    // response with an error code of 438 (Stale Nonce).
    r = stun_auth_response(stun, req, 438, "Stale Nonce", req->auth.realm, req->auth.nonce);
    return 438;
  }

  if (0 != stun_message_check_integrity(data, bytes, &req->msg, &req->auth))
  {
    // If the username in the USERNAME attribute is not valid, the server MUST 
    // generate an error response with an error code of 401 (Unauthorized).

    // If the resulting value does not match the contents of the MESSAGE-INTEGRITY 
    // attribute, the server MUST reject the request with an error response. 
    // This response MUST use an error code of 401 (Unauthorized).
    stun_auth_response(stun, req, 401, "Unauthorized", req->auth.realm, req->auth.nonce);
    return 401;
  }

  return 0;
}

NAT类型

以下是典型NAT的分类:

full cone:不受限的NAT处理模式,在这种NAT模式中,来自外部网络的数据包将被无条件路由到内部网络;

Restricted cone:IP受限型NAT,会将来自内部设备相同的IP+端口的数据包,统一映射成同一个外部IP+端口进行发送。当收到外部的数据包时,如果收到数据包的IP是之前发送数据包的目标IP,则转发到内部设备,否则丢弃。

restricted port cone:在IP受限型的基础上,添加了端口的约束。

Symmetric NAT:对称型NAT,则是在端口受限型的基础上,内部设备使用相同的IP+端口向不同的外部IP+端口(IP不同或端口不同或两者均不同)发送数据包时,NAT会分配不同的公网IP+端口。

图片

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论