WebRTC STUN:从消息识别的视角理解消息结构

在 RTC 通信中,媒体传输策略一般是单端口复用,即 RTP/RTCP/DTLS/STUN 这些不同协议的数据包都是复用端口进行传输。

假如我们要处理 STUN 事务,那么当端口收到数据之后,首先要做的就是判断这个数据包是不是 STUN 消息,如果是,才会进行下一步的事务处理(比如验证 FINGERPRINT 和 M-I)。

那么 STUN 消息该如何识别呢?本文介绍识别 STUN 消息的方法,并从消息识别的视角带你理解 STUN 消息的结构。

消息结构

所有的 STUN 消息都包含长度固定为 20 字节的头部,后面跟着一个或多个属性。

STUN 的头部包含消息类型 STUN Message Type、消息长度 Message LengthMagic Cookie 以及事务 ID Transaction ID 这四个字段。STUN 的属性使用 TLV 编码方式。STUN 消息的整体结构,参考下图:

WebRTC STUN:从消息识别的视角理解消息结构

消息头部

关于 STUN 消息的头部字段,这里重点讲述 Message Type 和 Transaction ID 字段,其它字段会在消息识别部分介绍。

Message Type

Message Type 决定了 STUN 消息的类别(message class)以及消息的方法(message method)。

消息类别是指 request、success response、error response、indication,共四种。

消息方法则包括大家所熟知的 binding,以及和 TURN 协议相关的方法,比如 allocate/refresh 等。

STUN 消息的 Message Type 字段被进一步分解为下图所示的结构:

WebRTC STUN:从消息识别的视角理解消息结构

其中,M11 到 M0 这 12 位编码表示消息方法(message method),C1 和 C0 这 2 位编码表示消息类别(message class)。

Class of 0b00 is a request
Class of 0b01 is an indication
Class of 0b10 is a success response
Class of 0b11 is an error response

例如,STUN binding request 消息的类别是 class = 0b00(request),方法是 method = 0b000000000001(Binding),再加上消息的最高的两位(0b00),因此消息头部的前 16 位的 16 进制编码表示为 0x0001。

同理, STUN Binding response 消息的类别是 class = 0b10(success response),方法是 method = 0b000000000001(Binding),再加上消息的最高的两位(0b00),因此消息头部的前 16 位的 16 进制编码表示为 0x0101。

Transaction ID

Transaction ID 是一个长度为 96-bit 的标识符(在 WebRTC 中,是一个随机生成的 12 字节的字符串),用于唯一标识 STUN 事务。

STUN 协议有两种事务,对于 request/response 事务,Transaction ID 由客户端生成,服务端在 response 中回应与 request 一致的事务 ID。对于 indication 事务,Transaction ID 由 STUN agent 随机生成,由于 indication 没有响应,因此事务 ID 仅用于辅助 debug。

Transaction ID 的主要作用是将 request 和 response 关联起来。

Transaction ID 的另一个作用是,可以防止某种特定类型的攻击[1]

比如,攻击者要在 response 中注入假的 MAPPED-ADDRESS,这要求攻击者能够窃听到从客户端发到服务端的 request。这是因为 STUN 请求头部包含随机的 96-bit 的 Transaction ID,服务端会在 response 中回应相同的值,客户端会忽略任何与 request 中的 Transaction ID 不匹配的 response。因此,攻击者要想生成一个可以被客户端接受的假的 response,就必须要知道客户端发送的 request 中的 Transaction ID。由于 Transaction ID 会从 0 到 2**96 – 1 这个区间中随机选择,因此大量的随机性,加上需要知道客户端何时发送 request,排除了攻击者猜测 Transaction ID 的可能性。

消息属性

STUN 消息属性的类型 Type 和长度 Length 都是两字节。其中,根据 Type value 的范围,STUN 消息的属性被划分为两种类型:comprehension-required 和 comprehension-optional。

Type value 在 0x0000 和 0x7FFF 之间的是 comprehension-required 类型的属性,Type value 在 0x8000 和 0xFFFF 之间的是 comprehension-optional 类型的属性。

对于不理解的 comprehension-optional 属性,STUN agent 可以在处理 STUN 消息时安全的忽略它。但是对于不理解的 comprehension-required 属性,STUN agent 就不能够成功的处理这个 STUN 消息。

由于 STUN 协议要求 STUN 消息属性按照 32-bit 对齐,因此每个属性的内容大小必须是 4 字节的倍数,否则就需要填充 1,2 或者 3 字节的 padding 填充数据,以满足上述 4 字节对齐的原则。

RFC8489[2] 规定:在发送 STUN 消息时,填充数据(padding bits)的值必须设置为 0,而且接收方必须忽略 padding。

remark: STUN 消息属性 Length 字段的值只表示 TLV 中 V(Value) 的长度,既不包括 T(Type) 和 L(length),又不包括 padding 填充数据的长度。

消息识别

在某些 STUN 的应用(比如 ICE 和 SIP)中,STUN 协议必须与其它协议(比如 RTP 协议)多路复用。因此,必须要有一种判断数据包是否是 STUN 消息的方法。

STUN 头部有三处拥有固定值的位和字段,可以用来区分 STUN 消息与其它协议的数据包。如果这还不足以用来识别 STUN 消息,那么 STUN 消息中还可以包含一个 FINGERPRINT  指纹值,可以帮助进一步识别。下面介绍这几种识别 STUN 消息的方法。

Distinguish 1,消息最高的两位

每一个 STUN 消息的最高的两位一定是 0。

当 STUN 协议与其它协议多路复用相同的端口时,可以通过判断数据包的最高的两位是否为 0 来区分 STUN 消息和其它协议的数据包。

Distinguish 2,消息最后的两位

STUN 消息头部的 Message Length 字段表示除了 20 字节的 STUN 头部之外的整个 STUN 消息的大小(即所有 STUN 消息属性的总大小)。

因为 STUN 属性是 4 字节对齐的,所以 STUN 属性的长度会被填充为 4 的倍数,所以 STUN 消息长度 Message Length也是 4 的倍数,所以 Message Length 字段的最后两位总是为 0。

当 STUN 协议与其它协议多路复用相同的端口时,可以通过判断数据包中 Message Length 字段的最后两位是否为 0 来区分 STUN 消息和其它协议的数据包。

Distinguish 3,固定的 Cookie

STUN 消息头部的 Magic Cookie 字段的值一定是固定的,大小为 0x2112A442

当 STUN 协议与其它协议多路复用相同的端口时,可以通过检测数据包中 Magic Cookie 字段的值是否为 0x2112A442 来区分 STUN 消息和其它协议的数据包。

另外,在 RFC3489[3] 规范中,STUN 消息的头部格式参考下图:

WebRTC STUN:从消息识别的视角理解消息结构

也就是说在该规范中,Transaction ID 是 128-bit,RFC5389[4] 规范以及 RFC8489 规范中 32-bit 的 Magic Cookie 其实是老规范中 Transaction ID 的一部分。

remark: 老的规范 RFC3489 与新的规范 RFC5389(包括最新的 RFC8489)的主要区别就是:新规范中增加了 Magic Cookie 字段以及 Transaction ID 长度减少了。

Distinguish 4,fingerprint 机制

FINGERPRINT 机制[5] 是 STUN 协议的一种可选机制,帮助区分多路复用场景下的 STUN 消息和其它协议的数据包。

当 STUN agent 收到它认为是 STUN 消息的数据包时,除了上面介绍的三种固定值的基本检测外,STUN agent 还会检查数据包是否包含 FINGERPRINT 属性以及该属性是否包含正确的指纹值。这个额外的 FINGERPRINT 的检查可以帮助 STUN agent 检查来自其它协议的看起来像是 STUN 消息的数据包。

当 STUN agent 收到一个(可能会是) STUN 消息时,首先按照前面三种规则去检查该消息:最开始的两位是否是 0?Magic Cookie 字段的值是否正确?Message Length 长度是否合理(是否是 4 的倍数,是否大于 20 字节的头部长度)?以及是否是 STUN 协议支持的消息方法。

如果消息类别是 Success Response 或者 Error Response,那么 STUN agent 会检查 Transaction ID 是否匹配。如果使用 FINGERPRINT 机制,那么 STUN agent 会检查消息是否携带 FINGERPRINT 属性以及指纹值是否正确。

如果在上述步骤中检测到任何错误,消息默认被丢弃,在 STUN 协议和其它协议多路复用的场景中,检测出现错误则可能表示收到的数据包可能并不是一个 STUN 消息,此时, STUN agent 应该尝试使用其它协议去解析这个数据包。

与 RTP/RTCP/DTLS 对比

STUN 消息能够和 RTP/RTCP/DTLS 包区分的两个重要的点是 Magic Cookie 和 Fingerprint,因为 RTP/RTCP/DTLS 包是没有的。所以一个数据包如果有 Magic Cookie 和 Fingerprint,那么完全可以认定这是一个 STUN 消息。而开头和最后的 2-bit 是否为 0 只能作为判断 STUN 消息的必要条件。

对于 RTP/RTCP,比如下面两张图分别是 RTP 和 RTCP 的头部,可以看到最开始的 2-bit 是 Version 字段,RFC3550 规范中要求 Version = 0b10 = 2,因此 RTP 和 RTCP 的最高两位不可能是 0,这就可以和 STUN 消息区分开了。

WebRTC STUN:从消息识别的视角理解消息结构
WebRTC STUN:从消息识别的视角理解消息结构

对于 DTLS,一般是通过第一个字节来判断是不是 DTLS 消息。

根据 RFC2246[6] 的描述,DTLS 消息类型如下:

enum {
  change_cipher_spec(20), 
  alert(21), handshake(22),
  application_data(23), (255)
} ContentType;

如果第一个字节的值为 20、21、22、23,那么可以判断是 DTLS 消息,不过由于这几种 DTLS 消息的最高 2-bit 也都是 0,所以无法通过最高 2-bit 是否为 0 来识别 STUN 消息。

对于 RTP/RTCP/DTLS 包,它们的长度不满足 4 的倍数这一原则,因此最后 2-bit 有可能是 0 也有可能不是 0,所以不能通过最后 2-bit 是否为 0 来识别 STUN 消息。

源码剖析

参考 WebRTC M88 版本。

class StunMessage {
  static bool IsStunMethod(
    rtc::ArrayView<int> types,
    const char* data,
    size_t size);

  static bool ValidateFingerprint(
    const char* data, size_t size);

  uint16_t type_;
  uint16_t length_;
  std::string transaction_id_;
  uint32_t reduced_transaction_id_;
  uint32_t stun_magic_cookie_;
};

IsStunMethod 函数

该函数用来验证收到的数据是否是 STUN 消息。输入参数为指向数据的指针 data,数据大小 size 以及指定好的 STUN 消息类型 types

首先检测消息的长度 Message Length,保证是 4 的倍数,且不能小于固定 20 字节的头部大小。

if (size % 4 != 0 || size < kStunHeaderSize)
  return false;

接着检查 Magic Cookie 字段的值是否是 kStunMagicCookie = 0x2112A442

const char* magic_cookie = data + 
  kStunTransactionIdOffset - 
  kStunMagicCookieLength;

if (rtc::GetBE32(magic_cookie) != 
    kStunMagicCookie)
  return false;

最后取数据的前 16-bit,即 Message Type 字段的值,看是否是在参数 types 中指定的消息类型中。

int method = rtc::GetBE16(data);
for (int m : methods) {
if (m == method) {
   return true;
  }
}

如果上述检查全部通过,那么返回 true,认为收到的是 STUN 消息,否则返回 false。

ValidateFingerprint 函数

该函数在 IsStunMethod 函数检查通过后执行,它会检查 STUN 消息是否携带 FINGERPRINT 属性,以及指纹值是否正确。

首先,依然是检查 Message Length 和 Magic Cookie,和 IsStunMethod 函数的实现保持一致。不过在检查 Message Length 时有一点差异,要保证消息大小 size 不小于 20 字节的头部加上 8 字节的 FINGERPRINT 属性。

size_t fingerprint_attr_size =
  kStunAttributeHeaderSize + 
  StunUInt32Attribute::SIZE;

if (size % 4 != 0 || 
size < kStunHeaderSize + 
   fingerprint_attr_size)
  return false;

const char* magic_cookie = data + 
  kStunTransactionIdOffset - 
  kStunMagicCookieLength;

if (rtc::GetBE32(magic_cookie) != 
   kStunMagicCookie)
  return false;

接着,检查 FINGERPRINT 属性的 T(type) 和 L(length),保证 type = STUN_ATTR_FINGERPRINT = 0x8028,因为指纹值是 32-bit,所以 length = StunUInt32Attribute::SIZE = 4

const char* fingerprint_attr_data = 
  data + size - fingerprint_attr_size;

if (rtc::GetBE16(fingerprint_attr_data) 
    != STUN_ATTR_FINGERPRINT ||
    rtc::GetBE16(fingerprint_attr_data 
    + sizeof(uint16_t)) 
    != StunUInt32Attribute::SIZE)
  return false;

最后,检查 FINGERPRINT 属性的 V(value),是否和我们进行 CRC32 计算得到的结果一致。

uint32_t fingerprint =
  rtc::GetBE32(fingerprint_attr_data + 
   kStunAttributeHeaderSize);
   
return ((fingerprint ^ STUN_FINGERPRINT_XOR_VALUE) 
  == rtc::ComputeCrc32(data, 
  size - fingerprint_attr_size));

需要注意的一点是,CRC32 计算指纹值的输入内容不能包括 FINGERPRINT 属性本身,因此输入长度是 size - fingerprint_attr_size(8)

小结

综合来看,IsStunMethod 函数和 ValidateFingerprint 函数使用了 Distinguish 2、3、4 中描述的识别 STUN 消息的方法。不过并没有使用 Distinguish 1 中描述的方法,即检查接收数据的最高的 2-bit 是否为 0。其实,我们完全可以自己实现,如下:

if ((data[0] & 0xc0) != 0x00) {
  return false;
}

抓包分析

先看这张图:

WebRTC STUN:从消息识别的视角理解消息结构

左右两侧分别是在 ICE 场景中的一次 STUN request/response 事务中的 Binding 请求和响应。

除了图中的注解外,还可以知道,request 和 response 的 Transaction ID 完全相同。

另外,识别 STUN 消息的四种方法也都体现在了图中:观察 Message Type 可知消息的前两位是 0,观察 Message Length 可知消息的长度是 4 的倍数,消息携带了固定的 cookie 值和 FINGERPRINT 属性。

再看这张图:

WebRTC STUN:从消息识别的视角理解消息结构

左右两侧分别是在 TURN 场景中的一次 STUN request/response 事务中的 Allocate 请求和响应。

观察上图,Request 和 Error Response 是 STUN 消息的类别(class),Allocate 则是 STUN 消息的方法(method)。

另外,发现左侧的 Allocate request 请求没有携带 FINGERPRINT 属性,而右侧则携带了 FINGERPRINT 属性,这里暂不讨论这种做法是否符合规范,我想要强调的是:FINGERPRINT 机制是 STUN 协议的一种可选机制,并不强制要求携带该属性。不过在标准的 ICE 协议中,STUN 消息都会携带 FINGERPRINT 属性。

至此,STUN 协议系列完结。你可能会好奇上图中的 Allocate 类型的 STUN 消息是做什么的,其实它是 TURN 服务器分配 relay candidate 的关键,下一篇将会开启新的 TURN 协议系列,介绍 TURN 这种 STUN Usage。

参考资料

[1] Launching the Attacks: https://tools.ietf.org/html/rfc3489#section-12.2

[2] RFC8489: https://tools.ietf.org/html/rfc8489#section-5

[3] RFC3489: https://tools.ietf.org/html/rfc3489

[4] RFC5389: https://tools.ietf.org/html/rfc5389

[5] FINGERPRINT 机制: https://tools.ietf.org/html/rfc8489#section-7

[6] RFC2246: https://tools.ietf.org/html/rfc2246#section-6.2.1

作者:于吉太
来源:码神说
原文:https://mp.weixin.qq.com/s/m9sxGTyMe35ygrJTZHa1Hw

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

(0)

相关推荐

发表回复

登录后才能评论