文章目录
- WebRTC nat 穿越及相关协议
- Nat 类型
- Session Traversal Utilities for NAT (STUN)-NAT会话遍历工具
- Traversal Using Relays around NAT (TURN)
- 协议文档 RFC 5766
- Relay Extensions to Session Traversal Utilities for NAT (STUN)
- 相关概念
- Transports:传输方式
- Allocations:client 请求 server 分配中继地址
- exchange application data:数据交换
- New STUN Methods
- 0x0003 : Allocate (only request/response semantics defined)
- 0x0004 : Refresh (only request/response semantics defined)
- 0x0006 : Send (only indication semantics defined): 将数据从 client 传递 server
- 0x0007 : Data (only indication semantics defined): 将数据从 server 传递 client
- 0x0008 : CreatePermission (only request/response semantics defined
- 0x0009 : ChannelBind (only request/response semantics defined)
- New STUN Attributes
- New STUN Error Response Codes
- Interactive Connectivity Establishment (ICE)
- b 站抓包分析
- mediasoup webrtc stun 源码分析
- 参考文献
WebRTC nat 穿越及相关协议
Nat 类型
Full cone NAT(完全锥形NAT)
所有从同一个内网的(IP,端口)发送出来的请求都会被映射到同一个外网(IP,端口),且任何一个外网主机都可以通过访问映射后的公网地址,实现访问位于内网的主机设备功能。
外网主机可以主动连接内网主机
Restricted Cone NAT(地址受限锥形NAT)
所有从同一个内网的(IP,端口)发送出来的请求都会被映射到通过一个外网(IP,端口),但与全锥形不同点在于:生成的映射表项与目的IP有关,只有符合要求的目的IP(要访问的公网服务器IP)才可以通讯。此NAT还有个特点:不能主动连接内网中的主机地址,连接必须由内网地址发起。
限制比全锥形NAT多了:IP地址限制。
Port Restricted Cone NAT(端口受限锥形NAT)
所有从同一个内网的(IP,端口)发送出来的请求都会被映射到通过一个外网(IP,端口),但是在地址受限锥形NAT基础上增加了端口的限制。地址受限锥形NAT时,只有内网主机主动连接的公网主机才可与之进行通讯,而不用担心端口号是否与请求的端口相同。
端口受限锥形NAT除了IP限制外,增加了端口限制。
Symetric NAT(对称NAT)
所有从同一个内网(IP,端口)发送到同一个目的IP和端口的请求都会被映射到同一个IP和端口。换句话说(SIP,Sport, DIP, Dport)只要有一个发生变化都会使用不同的映射条目,即此NAT映射与报文四元组绑定。
Session Traversal Utilities for NAT (STUN)-NAT会话遍历工具
协议文档 RFC 3489 和 RFC 5389
- STUN,首先在 RFC3489 中定义,作为一个完整的 NAT 穿透解决方案,英文全称是 Simple Traversal of UDP Through NATs,即简单的用 UDP 穿透 NAT。
- 在 RFC5389 修订中把 STUN 协议定位于为穿透 NAT 提供工具,而不是一个完整的解决方案,英文全称是 Session Traversal Utilities for NAT,即 NAT 会话穿透效用。
RFC5389与RFC3489的不同点如下:
- 去掉STUN是一种完整的NAT穿透方案的概念,现在是一种用于提供NAT穿透解决方案的工具。因而,协议的名称变为NAT会话穿透效用;
- 定义了STUN的用途;
- 去掉了STUN关于NAT类型检测和绑定生命期发现的用法,去掉了RESPONSE-ADDRESS、CHANGED-ADDRESS、CHANGE-REQUEST、SOURCE-ADDRESS和REFLECTED-FROM属性;
- 增加了一个固定的32位的魔术字字段,事务ID字段减少了32位长度;
- 增加了XOR-MAPPED-ADDRESS属性,若魔术字在捆绑请求中出现时,该属性包括在捆绑响应中。否则,RFC3489中的行为是保留的(换句话说,捆绑响应中包括MAPPED-ADDRESS);
- 介绍了消息类型字段的正式结构,带有一对明确的位来标识Request、Response、Error-Response或Indication消息。因此,消息类型字段被划分为类别和方法两部分;
- 明确的指出了STUN的最高2位是0b00,当用于ICE时可以简单的与RTP包区分开来;
- 增加指纹属性来提供一种明确的方法来检测当STUN协议多路复用时,STUN与其他协议之间的差异;
- 增加支持IPv6,IPv4客户端可以获取一个IPv6映射地址,反之亦然;
- 增加一个long-term-credential-based认证机制;
- 增加了SOFTWARE、REALM、NONCE和ALTERNATE-SERVER属性;
- 去掉了共享密匙方法,因此PASSWORD属性也去掉了;
- 去掉了使用连续10秒侦听STUN响应来识别一个攻击的做法;
- 改变事务计时器来增加TCP友好性;
- 去掉了STUN例子如集中分离控制和媒体面,代替的,在使用STUN协议时提供了更多的信息;
- 定义了一类填充机制来改变长度属性的说明;
- REALM、SERVER、原因语句和NONCE限制在127个字符,USERNAME限制在513个字节以内;
- 为TCP和TLS改变了DNS SRV规程,UDP仍然和以前保持一致;
相关概念
STUN Message Header
stun 消息由 stun 消息头和 stun 属性构成,stun 消息头为 20 固定字节:
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 Message Type | Message Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Magic Cookie |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Transaction ID (96 bits) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 2: Format of STUN Message Header
- STUN Message Type 和 class
/*
The message type field is decomposed further into the following structure:
0 1
2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+-+-+-+-+-+-+-+-+-+-+-+-+
|M |M |M|M|M|C|M|M|M|C|M|M|M|M|
|11|10|9|8|7|1|6|5|4|0|3|2|1|0|
+--+--+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 3: Format of STUN Message Type Field
Here the bits in the message type field are shown as most significant
(M11) through least significant (M0). M11 through M0 represent a 12-
bit encoding of the method. C1 and C0 represent a 2-bit encoding of
the class.
*/
C1 and C0 represent a 2-bit encoding of the class
- 0b00 is a request:client --> server
- 0b01 is an indication:client 或者 server 发起不需要响应回复
- 0b10 is a success response:server->client
- 0b11 is an error response:server->client
- message length
The message length MUST contain the size, in bytes, of the message
not including the 20-byte STUN header.
-
magic cookie
The magic cookie field MUST contain the fixed value0x2112A442
in
network byte order. -
The transaction ID is a 96-bit identifier, used to uniquely identify
STUN transactions. -
STUN methods:The initial STUN methods are:
- 0x000: (Reserved)
- 0x001: Binding
- 0x002: (Reserved; was SharedSecret)
STUN methods in the range 0x000 - 0x7FF are assigned by IETF Review
[RFC5226]. STUN methods in the range 0x800 - 0xFFF are assigned by
Designated Expert [RFC5226]
/* Class variables. */
const uint8_t StunPacket::magicCookie[] = { 0x21, 0x12, 0xA4, 0x42 };
static bool IsStun(const uint8_t* data, size_t len)
{
// clang-format off
return (
// STUN headers are 20 bytes.
(len >= 20) &&
// DOC: https://tools.ietf.org/html/draft-ietf-avtcore-rfc5764-mux-fixes
(data[0] < 3) &&
// Magic cookie must match.
(data[4] == StunPacket::magicCookie[0]) && (data[5] == StunPacket::magicCookie[1]) &&
(data[6] == StunPacket::magicCookie[2]) && (data[7] == StunPacket::magicCookie[3])
);
// clang-format on
}
// Get type field.
uint16_t msgType = Utils::Byte::Get2Bytes(data, 0);
// Get length field.
uint16_t msgLength = Utils::Byte::Get2Bytes(data, 2);
// length field must be total size minus header's 20 bytes, and must be multiple of 4 Bytes.
if ((static_cast<size_t>(msgLength) != len - 20) || ((msgLength & 0x03) != 0))
{
MS_WARN_TAG(
ice,
"length field + 20 does not match total size (or it is not multiple of 4 bytes), "
"packet discarded");
return nullptr;
}
// Get STUN method :
uint16_t msgMethod = (msgType & 0x000f) | ((msgType & 0x00e0) >> 1) | ((msgType & 0x3E00) >> 2);
// Get STUN class: C1C0
uint16_t msgClass = ((data[0] & 0x01) << 1) | ((data[1] & 0x10) >> 4);
STUN Methods
Binding Request(0x0001): UDP 传输
- Binding Request 用于确定 nat 分配的绑定,客户端通过 udp 发送 Binding Request 到服务器,服务器获取到发送源的ip地址和端口,并将其复制到 Binding Response 中发送回客户端。在 Binding Request 消息中有一些参数设置使得客户端可以要求服务器在其他地方,或者用不同的 ip 和端口发送 Binding Response 。有一些属性用于提供消息完整性和身份验证。
- 在stun客户端和服务器端之间,Binding Request 可能会通过一层或多层 NAT 。最终的结果是,服务器接收到的请求消息的源 IP 地址和端口,是最靠近 stun 服务器的那一层 NAT 映射后的公网地址。服务器将映射后的IP地址和端口复制并填写到 Binding Response 消息中发送给客户端
- 当客户端接收到 Binding Response 后,将解析出来的IP地址和端口与本地 IP 地址和端口进行比较,如果不匹配,则表明 stun 客户端处于一层或多层 NAT 之后。此时只是表明 stun 客户端处于 NAT 之后,还无法判断 NAT 的类型,为了进一步决定 NAT 的类型,客户端会发送第二个 Binding Request ,这一次是往不同的 ip 地址发送,如果 Binding Response 中的 IP 地址和端口和第一次的 Binding Response 的 IP 地址和端口不一样,那么 stun 客户端处于对称型锥的 NAT 之后,为了判断是否是完全型锥的 NAT ,客户端可以发送一个 Binding Request 并要求服务端用一个不同的 IP 和端口发送 Binding Response ,换句话说,如果客户但使用 IP 为 X 端口为 Y 向 IP 为 A 端口为 B 的主机发送请求, stun 服务器使用 IP 为 C 端口为 D 向客户端发送回应,如果客户端能收到回应则表明其处于完全型锥。客户端也可以要求服务器使用相同的IP地址但端口不同来发送回应,以此来判断是处于端口限制型锥还是限制型锥。
Shared Secret Request(0x0002): (Reserved; was SharedSecret): TCP 的 TLS 传输
- Shared Secret Request 要求服务器返回一个临时的 username 和 password,这个 username 和 password 用于后续的 Binding Request 和 Binding Response ,保证身份认证和消息完整。
STUN Attributes
由 0~N个消息属性构成
/*
STUN Attributes
After the STUN header are zero or more attributes. Each attribute
MUST be TLV encoded, with a 16-bit type, 16-bit length, and value.
Each STUN attribute MUST end on a 32-bit boundary. As mentioned
above, all fields in an attribute are transmitted most significant
bit first.
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Value (variable) ....
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
// Start looking for attributes after STUN header (Byte #20).
- Type
类型值介于 0x0000 和 0x7FFF 之间的属性是需要理解的属性,这意味着 STUN 代理无法成功处理消息,除非它理解该属性。类型值介于 0x8000 和 0xFFFF 之间的属性是可选属性,这意味着如果 STUN 代理不理解这些属性,则可以忽略这些属性。
Comprehension-required range (0x0000-0x7FFF):
0x0000: (Reserved)
0x0001: MAPPED-ADDRESS
0x0002: (Reserved; was RESPONSE-ADDRESS)
0x0003: (Reserved; was CHANGE-ADDRESS)
0x0004: (Reserved; was SOURCE-ADDRESS)
0x0005: (Reserved; was CHANGED-ADDRESS)
0x0006: USERNAME
0x0007: (Reserved; was PASSWORD)
0x0008: MESSAGE-INTEGRITY
0x0009: ERROR-CODE
0x000A: UNKNOWN-ATTRIBUTES
0x000B: (Reserved; was REFLECTED-FROM)
0x0014: REALM
0x0015: NONCE
0x0020: XOR-MAPPED-ADDRESS
Comprehension-optional range (0x8000-0xFFFF)
0x8022: SOFTWARE
0x8023: ALTERNATE-SERVER
0x8028: FINGERPRINT #FINGERPRINT must be the last attribute
- Length
lenght 字段的值只表示 TLV 中 V(Value) 的长度,既不包括 T(Type) 和 L(length),又不包括 padding 填充数据的长度
MAPPED-ADDRESS(0x0001)
MAPPED-ADDRESS 属性表示客户端的映射传输地址。它由一个8位地址族和一个16位端口组成,后跟一个表示IP地址的固定长度值。如果地址族为IPv4,则地址必须为32位。如果地址族为IPv6,则地址必须为128位。所有字段必须按网络字节顺序排列。
The format of the MAPPED-ADDRESS attribute is:
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 0 0 0 0 0 0| Family | Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Address (32 bits or 128 bits) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 5: Format of MAPPED-ADDRESS Attribute
The address family can take on the following values:
0x01:IPv4
0x02:IPv6
The first 8 bits of the MAPPED-ADDRESS MUST be set to 0 and MUST be
ignored by receivers. These bits are present for aligning parameters
on natural 32-bit boundaries.
This attribute is used only by servers for achieving backwards
compatibility with RFC 3489 [RFC3489] clients.
USERNAME(0x0006)
USERNAME 属性用于消息的完整性检查,用于消息完整性检查中标识共享私密。USERNAME 通常出现在共享私密响应中,与 PASSWORD 一起。当使用消息完整性检查时,可有选择地出现在捆绑请求中。
MESSAGE-INTEGRITY(0x0008)
MESSAGE-INTEGRITY 属性包含 STUN 消息的 HMAC-SHA1,它可以出现在捆绑请求或捆绑响应中;MESSAGE-INTEGRITY 属性必须是任何 STUN 消息的最后一个属性。它的内容决定了 HMAC 输入的 Key 值。
The key for the HMAC depends on whether long-term or short-term
credentials are in use. For long-term credentials, the key is 16
bytes:
key = MD5(username ":" realm ":" SASLprep(password))
That is, the 16-byte key is formed by taking the MD5 hash of the
result of concatenating the following five fields: (1) the username,
with any quotes and trailing nulls removed, as taken from the
USERNAME attribute (in which case SASLprep has already been applied);
(2) a single colon; (3) the realm, with any quotes and trailing nulls
removed; (4) a single colon; and (5) the password, with any trailing
nulls removed and after processing using SASLprep. For example, if
the username was 'user', the realm was 'realm', and the password was
'pass', then the 16-byte HMAC key would be the result of performing
an MD5 hash on the string 'user:realm:pass', the resulting hash being
0x8493fbc53ba582fb4c044c456bdc40eb.
For short-term credentials:
key = SASLprep(password)
where MD5 is defined in RFC 1321 [RFC1321] and SASLprep() is defined
in RFC 4013 [RFC4013].
ERROR-CODE(0x0009)
ERROR-CODE 属性出现在捆绑错误响应或共享私密错误响应中。它的响应号数值范围从 100 到 699,目前定义如下:
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reserved, should be 0 |Class| Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reason Phrase (variable) ..
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 7: ERROR-CODE Attribute
300(尝试备用):客户端应为此请求联系备用服务器。只有当请求包含USERNAME属性和有效的MESSAGEINTEGRITY属性时,才能发送此错误响应;否则,不得发送
400(错误请求):请求变形了。客户在修改先前的尝试前不应该重试该请求。
401(未授权):捆绑请求没有包含MESSAGE-INTERITY属性。
420(未知属性):服务器不认识请求中的强制属性。
438(过时的Nonce):客户端使用的Nonce不再有效
500(服务器错误):服务器遇到临时错误,客户应该再次尝试。
UNKNOWN-ATTRIBUTES(0x000A)
UNKNOWN-ATTRIBUTES 属性只在错误代码为420的的错误响应中出现。
REALM(0x0014)
REALM 属性可能出现在请求和响应中。请求中存在 REALM 属性表示正在使用长期凭据进行身份验证。当在错误响应中出现表明服务器希望客户端使用长期凭据进行身份验证。
Nonce(0x0015)
Number used once
-
在密码学中Nonce是一个只被使用一次的任意或非重复的随机数值,在加密技术中的初始向量和加密散列函数都发挥着重要作用,在各类验证协议的通信应用中确保验证信息不被重复使用以对抗重放攻击(Replay Attack)。在信息安全中,Nonce是一个在加密通信只能使用一次的数字。在认证协议中,它往往是一个随机或伪随机数,以避免重放攻击。Nonce也用于流密码以确保安全。如果需要使用相同的密钥加密一个以上的消息,就需要Nonce来确保不同的消息与该密钥加密的密钥流不同。
-
具体应用:在摘要认证中服务器让客户选一个随机数(称作”nonce“),然后浏览器使用一个单向的加密函数生成一个消息摘要(message digest),该摘要是关于用户名、密码、给定的nonce值、HTTP方法,以及所请求的URL。
- 一个典型的基于 nonce 的验证协议如下:
这里的 cnonce 为 client nonce(后面将讨论为什么需要 cnonce)。Client 并不直接发送密码(或者密码直接加密后的密文)用以避免攻击者直接窃取密码(或者密码直接加密后的密文)并冒充用户进行身份验证。先不考虑 Client nonce。Server 首先发送 nonce 到 Client,Client 将密码和 nonce 通过 hash 运算再提交到 Server 进行身份验证,这样我们可以认为(而非绝对)每次用于身份验证的 hash 值都不相同,即使攻击者窃取了前一次用于身份验证的 hash 值也无法冒充用户进行登录。公式如下:
A = Hash(nonce, password);
在不安全的网络环境中,A、nonce、Hash 算法都可以被攻击者获取,如果能够满足以下条件:
nonce 仅仅被使用一次
Hash 运算不出现冲突
攻击者则在理论上无法实施 Replay attack
因此 nonce 在特定上下文中仅仅被使用一次以及 Hash 算法尽量避免冲突是安全的关键。为了确保 nonce 在特定上下文中仅仅被使用一次,可以使用以下策略生成 nonce :nonce 可以是一个时间相关变量,nonce 可以是一个通过足够随机算法生成的足够长的 bits。
- stun 应用:客户端初始发送一个请求,没有提供任何证书和任何完整性检测。服务器拒绝这个请求,并提供给用户一个范围(用于指导用户或代理选择 username 和 password )和一个 nonce 。这个 nonce 提供重放保护。它是一个 cookie ,由服务器选择,以这样一种方式来标示有效时间或客户端身份是有效的。客户端重试这个请求,这次包括它的 username 和 realm 和服务器提供的 nonce 来回应。服务器确认这个 nonce 和检查这 message integrity 。如果它们匹配,请求则通过认证。如果这个 nonce 不再有效,即过期了,服务器就拒绝该请求,并提供一个新的nonce。在随后的到同一服务器的请求,客户端重新使用这个 nonce、username 和 realm,和先前使用的 password 。这样,随后的请求不会被拒绝直到这个 nonce 变成无效的。
XOR-MAPPED-ADDRESS(0x0020)
XOR-MAPPED-ADDRESS 属性与 MAPPED-ADDDRESS 属性相同,只是反射传输地址通过 XOR 函数进行模糊处理
SOFTWARE attribute(0x8022)
包含软件的文本描述并由发送消息的代理使用。客户和服务器都可以使用,其值应包括制造商和版本。该属性对协议的操作没有影响,并且仅用作诊断和调试目的的工具。SOFTWARE的值是可变长度。它必须是UTF-8[RFC3629]少于128个字符的编码序列(可以长达763字节)
If the agent is sending a request, it SHOULD add a SOFTWARE attribute
to the request. Agents MAY include a SOFTWARE attribute in
indications, depending on the method. Extensions to STUN should
discuss whether SOFTWARE is useful in new indications
FINGERPRINT(0x8028)
FINGERPRINT 属性可能存在于所有STUN消息中。这个属性值计算 STUN 消息的 CRC-32,直到(但不包括)FINGERPRINT 属性本身,用 32 位值 0x5354554e 进行异或运算(如果应用程序数据包中也使用CRC-32)。
Traversal Using Relays around NAT (TURN)
协议文档 RFC 5766
Relay Extensions to Session Traversal Utilities for NAT (STUN)
STUN 的中继扩展。TURN 与 STUN 的共同点都是通过修改应用层中的私网地址达到 NAT 穿透的效果,异同点是 TURN 是通过两方通讯的“中间人”方式实现穿透。如果一个主机位于 NAT 的后面,在某些情况下它不能够与其他主机点对点直接连接(对称 NAT)。在这些情况下,它需要使用中间网点提供的中继连接服务。TURN 协议就是用来允许主机控制中继的操作并且使用中继与对端交换数据。TURN 与其他中继控制协议不同的是它能够允许一个客户端使用一个中继地址与多个对端连接。
Peer A
Server-Reflexive +---------+
Transport Address | |
192.0.2.150:32102 | |
| /| |
TURN | / ^| Peer A |
Client's Server | / || |
Host Transport Transport | // || |
Address Address | // |+---------+
10.1.1.2:49721 192.0.2.15:3478 |+-+ // Peer A
| | ||N| / Host Transport
| +-+ | ||A|/ Address
| | | | v|T| 192.168.100.2:49582
| | | | /+-+
+---------+| | | |+---------+ / +---------+
| || |N| || | // | |
| TURN |v | | v| TURN |/ | |
| Client |----|A|----------| Server |------------------| Peer B |
| | | |^ | |^ ^| |
| | |T|| | || || |
+---------+ | || +---------+| |+---------+
| || | |
| || | |
+-+| | |
| | |
| | |
Client's | Peer B
Server-Reflexive Relayed Transport
Transport Address Transport Address Address
192.0.2.1:7000 192.0.2.15:50000 192.0.2.210:49191
Figure 1: shows a typical deployment.
相关概念
Transports:传输方式
TURN, as defined in this specification, always uses UDP between the
server and the peer. However, this specification allows the use of
any one of UDP, TCP, or Transport Layer Security (TLS) over TCP to
carry the TURN messages between the client and the server.
+----------------------------+---------------------+
| TURN client to TURN server | TURN server to peer |
+----------------------------+---------------------+
| UDP | UDP |
| TCP | UDP |
| TLS over TCP | UDP |
+----------------------------+---------------------+
Allocations:client 请求 server 分配中继地址
客户端在没有凭据的情况下向服务器发送 Allocate 请求。由于服务器要求使用 STUN 的长期凭据机制对所有请求进行身份验证,因此服务器拒绝请求,并返回 401(未授权)错误代码。然后客户端再次尝试,这次包括凭据(未显示)。这一次,服务器接受 Allocate 请求并返回一个 Allocate success 响应,其中包含分配给中继传输地址。一旦分配了中继传输地址,客户端必须保留分配活动。为此,客户端定期向服务器发送刷新请求。TURN 故意使用不同的方法(刷新而不是分配)进行刷新,以确保在分配因某种原因消失时通知客户端。刷新事务的频率由分配的生存期决定,分配的默认生存期为 10 分钟——该值被选择为足够长,这样刷新通常不会给客户端带来负担,而在客户端意外及时退出的情况下,分配将过期。但是,客户机可以在 Allocate 请求中请求更长的生存期,也可以在 Refresh 请求中修改其请求,并且服务器总是在响应中指示实际的生存期。客户端必须在“生存期”秒内发出新的刷新事务。
TURN TURN Peer Peer
client server A B
|-- Allocate request --------------->| | |
| | | |
|<--------------- Allocate failure --| | |
| (401 Unauthorized) | | |
| | | |
|-- Allocate request --------------->| | |
| | | |
|<---------- Allocate success resp --| | |
| (192.0.2.15:50000) | | |
// // // //
| | | |
|-- Refresh request ---------------->| | |
| | | |
|<----------- Refresh success resp --| | |
| | | |
Figure 2
- client Allocate request 相关属性:
- MUST include a REQUESTED-TRANSPORT attribute:指明传输方式,目前只支持 UDP
- MAY include a LIFETIME attribute:请求过期时间(默认 10 分钟),低于此值服务器忽略,高于根据服务器实际响应确定
- MAY include a DONT-FRAGMENT attribute:ip 报文分片不启用
- MAY include a EVEN-PORT attribute:期望分配偶数端口
- MAY include a RESERVATION-TOKEN attribute:请求服务器分配一个预先保留的端口
- Receiving an Allocate request success response
- An XOR-RELAYED-ADDRESS attribute: containing the relayed transport address.
- A LIFETIME attribute: 目前的过期时间
- A RESERVATION-TOKEN attribute: (if a second relayed transport address was reserved).
- An XOR-MAPPED-ADDRESS attribute: client’s IP address and port (from the 5-tuple).
具体的例子如下:
TURN TURN Peer Peer
client server A B
| | | |
|--- Allocate request -------------->| | |
| Transaction-Id=0xA56250D3F17ABE679422DE85 | |
| SOFTWARE="Example client, version 1.03" | |
| LIFETIME=3600 (1 hour) | | |
| REQUESTED-TRANSPORT=17 (UDP) | | |
| DONT-FRAGMENT | | |
| | | |
|<-- Allocate error response --------| | |
| Transaction-Id=0xA56250D3F17ABE679422DE85 | |
| SOFTWARE="Example server, version 1.17" | |
| ERROR-CODE=401 (Unauthorized) | | |
| REALM="example.com" | | |
| NONCE="adl7W7PeDU4hKE72jdaQvbAMcr6h39sm" | |
| | | |
|--- Allocate request -------------->| | |
| Transaction-Id=0xC271E932AD7446A32C234492 | |// 重新分配 Transaction-Id
| SOFTWARE="Example client 1.03" | | |
| LIFETIME=3600 (1 hour) | | |// 请求过期时间 3600s
| REQUESTED-TRANSPORT=17 (UDP) | | |
| DONT-FRAGMENT | | |
| USERNAME="George" | | |
| REALM="example.com" | | |
| NONCE="adl7W7PeDU4hKE72jdaQvbAMcr6h39sm" | |
| MESSAGE-INTEGRITY=... | | |
| | | |
|<-- Allocate success response ------| | |
| Transaction-Id=0xC271E932AD7446A32C234492 | |
| SOFTWARE="Example server, version 1.17" | |
| LIFETIME=1200 (20 minutes) | | |// 以实际为准:1200s
| XOR-RELAYED-ADDRESS=192.0.2.15:50000 | |// 中继 ip:port
| XOR-MAPPED-ADDRESS=192.0.2.1:7000 | |// client nat 映射的 ip:port
| MESSAGE-INTEGRITY=... | | |
exchange application data:数据交换
client 和 peer 方有两种应用程序数据交换机制使用TURN服务器:
- Send Mechanism: Send and Data methods
- Channels
Send Mechanism: Send and Data methods
TURN TURN Peer Peer
client server A B
| | | |
|-- CreatePermission req (Peer A) -->| | |
|<-- CreatePermission success resp --| | |
| | | |
|--- Send ind (Peer A)-------------->| | |
| |=== data ===>| |
| | | |
| |<== data ====| |
|<-------------- Data ind (Peer A) --| | |
| | | |
| | | |
|--- Send ind (Peer B)-------------->| | |
| | dropped | |
| | | |
| |<== data ==================|
| dropped | | |
| | | |
Figure 3
- CreatePermission request
- MUST include at least one XOR-PEER-ADDRESS attribute, and MAY include more than one such attribute:XOR-PEER-ADDRESS 属性的 IP 地址部分包含应安装或刷新权限的 IP 地址。每个 XOR-PEER-ADDRESS 属性的端口部分将被忽略,可以是任意值。各种 XOR-PEER-ADDRESS 属性可以以任何顺序出现。
- Receiving a CreatePermission Response
- 如果客户端收到有效的 CreatePermission 成功响应,则客户端会更新其数据结构,以指示已安装或刷新权限
TURN TURN Peer Peer
client server A B
|--- CreatePermission request ------>| | |
| Transaction-Id=0xE5913A8F460956CA277D3319 | |
| XOR-PEER-ADDRESS=192.0.2.150:0 | | |// peer 端的 ip,port 可为任意值
| USERNAME="George" | | |
| REALM="example.com" | | |
| NONCE="adl7W7PeDU4hKE72jdaQvbAMcr6h39sm" | |
| MESSAGE-INTEGRITY=... | | |
| | | |
|<-- CreatePermission success resp.--| | |
| Transaction-Id=0xE5913A8F460956CA277D3319 | |
| MESSAGE-INTEGRITY=... | | |
- Send indication
- MUST include an XOR-PEERADDRESS attribute
- MUST include a DATA attribute
- MAY include a DONT-FRAGMENT attribute
- Data Indication
- MUST include an XOR-PEERADDRESS attribute
- MUST include a DATA attribute
TURN TURN Peer Peer
client server A B
|--- Send indication --------------->| | |
| Transaction-Id=0x1278E9ACA2711637EF7D3328 | |
| XOR-PEER-ADDRESS=192.0.2.150:32102 | |// peer nat 映射 ip:port
| DONT-FRAGMENT | | |
| DATA=... | | |
| |-- UDP dgm ->| |
| | data=... | |
| | | |
| |<- UDP dgm --| |
| | data=... | |
|<-- Data indication ----------------| | |
| Transaction-Id=0x8231AE8F9242DA9FF287FEFF | |
| XOR-PEER-ADDRESS=192.0.2.150:32102 | |
| DATA=... | | |
client sends application data to Peer A using a Send indication.
Channels
- ChannelData Message: 封装 client 和 server 之间的 application data
- Channel Number:2 字节:
0x0000 through 0x3FFF: These values can never be used for channel numbers.
0x4000 through 0x7FFF: These values are the allowed channel numbers (16,383 possible values).
0x8000 through 0xFFFF: These values are reserved for future use.
从上面的划分可以前两位的值来区分 STUN-formatted message 和 ChannelData message:
0b00: STUN-formatted message (since the first two bits of a STUN-formatted message are always zero).
0b01: ChannelData message (since the channel number is the first field in the ChannelData message
and channel numbers fall in the range 0x4000 - 0x7FFF).
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Channel Number | Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
/ Application Data /
/ /
| |
| +-------------------------------+
| |
+-------------------------------+
The Channel Number field specifies the number of the channel on which
the data is traveling, and thus the address of the peer that is
sending or is to receive the data.
The Length field specifies the length in bytes of the application
data field (i.e., it does not include the size of the ChannelData
header). Note that 0 is a valid length.
The Application Data field carries the data the client is trying to
send to the peer, or that the peer is sending to the client.
client server A B
| | | |
|-- ChannelBind req ---------------->| | |
| (Peer A to 0x4001) | | |
| | | |
|<---------- ChannelBind succ resp --| | |
| | | |
|-- [0x4001] data ------------------>| | |
| |=== data ===>| |
| | | |
| |<== data ====| |
|<------------------ [0x4001] data --| | |
| | | |
|--- Send ind (Peer A)-------------->| | |
| |=== data ===>| |
| | | |
| |<== data ====| |
|<------------------ [0x4001] data --| | |
| | | |
Figure 4
- Sending a ChannelBind Request
- MUST include a CHANNEL-NUMBER attribute
- MUST include an XOR-PEER-ADDRESS attribute : the peer’s transport address
- Receiving a ChannelBind Response
- 当客户端收到 ChannelBind 成功响应时,它会更新其数据结构,以记录通道绑定现在处于活动状态。它还更新其数据结构,以记录已安装或刷新了相应的权限。
TURN TURN Peer Peer
client server A B
|--- ChannelBind request ----------->| | |
| Transaction-Id=0x6490D3BC175AFF3D84513212 | |
| CHANNEL-NUMBER=0x4000 | | |
| XOR-PEER-ADDRESS=192.0.2.210:49191 | |
| USERNAME="George" | | |
| REALM="example.com" | | |
| NONCE="adl7W7PeDU4hKE72jdaQvbAMcr6h39sm" | |
| MESSAGE-INTEGRITY=... | | |
| | | |
|<-- ChannelBind success response ---| | |
| Transaction-Id=0x6490D3BC175AFF3D84513212 | |
| MESSAGE-INTEGRITY=... | | |
The client now binds a channel to Peer B, specifying a free channel
number (0x4000) in the CHANNEL-NUMBER attribute, and Peer B's
transport address in the XOR-PEER-ADDRESS attribute. As before, the
client re-uses the username, realm, and nonce from its last request
in the message.
Upon receipt of the request, the server binds the channel number to
the peer, installs a permission for Peer B's IP address, and then
replies with ChannelBind success response.
TURN TURN Peer Peer
client server A B
|--- ChannelData ------------------->| | |
| Channel-number=0x4000 |--- UDP datagram --------->|
| Data=... | Data=... |
| | | |
| |<-- UDP datagram ----------|
| | Data=... | |
|<-- ChannelData --------------------| | |
| Channel-number=0x4000 | | |
| Data=... | | |
The client now sends a ChannelData message to the server with data
destined for Peer B. The ChannelData message is not a STUN message,
and thus has no transaction id. Instead, it has only three fields: a
channel number, data, and data length
New STUN Methods
0x0003 : Allocate (only request/response semantics defined)
0x0004 : Refresh (only request/response semantics defined)
0x0006 : Send (only indication semantics defined): 将数据从 client 传递 server
0x0007 : Data (only indication semantics defined): 将数据从 server 传递 client
0x0008 : CreatePermission (only request/response semantics defined
0x0009 : ChannelBind (only request/response semantics defined)
New STUN Attributes
0x000C: CHANNEL-NUMBER
0x000D: LIFETIME
0x0010: Reserved (was BANDWIDTH)
0x0012: XOR-PEER-ADDRESS
0x0013: DATA
0x0016: XOR-RELAYED-ADDRESS
0x0018: EVEN-PORT
0x0019: REQUESTED-TRANSPORT
0x001A: DONT-FRAGMENT
0x0021: Reserved (was TIMER-VAL)
0x0022: RESERVATION-TOKEN
CHANNEL-NUMBER(0x000C)
CHANNEL-NUMBER 属性包含通道的号码。属性长 4 字节,包含 16 比特的无符号整数和2字节的 RFFU(Reserved For Future Use)字段,该字段必须设为 0 且在接收时被忽略。
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Channel Number | RFFU = 0 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
LIFETIME(0x000D)
LIFETIME 属性表示服务器在没有收到 refresh 时维持一个 allocation 的持续时间。属性长 4 字节,包含一个 32 比特的无符号整数值,表示剩余多少秒终止。
XOR-PEER-ADDRESS(0x0012)
XOR-PEER-ADDRESS 指明从 TURN 服务器侧看到的对等方的地址和端口。(例如,如果对等方位于 NAT 之后,则值为对等方的服务器反射传输地址。)它的编码方式与 XOR-MAPPED-ADDRESS 相同。
DATA(0x0013)
DATA 属性存在于所有的 Send 和 Data indications 消息中。属性的值是可变长度的,包括应用数据。如果属性的长度不上4字节的倍数,必须进行填充。
XOR-RELAYED-ADDRESS(0x0016)
XOR-RELAYED-ADDRESS 存在于所有的 Allocate 响应中。它指定了服务器分配给客户端的地址和端口。和 XOR-MAPPED-ADDRESS 属性编码方式相同:
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 0 0 0 0 0 0| Family | Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Address (32 bits or 128 bits) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
EVEN-PORT(0x0018):偶数端口
此属性允许客户端请求中继传输地址中的端口为偶数,并且(可选)服务器保留下一个更高的端口号。此属性的值部分长度为 1 字节。其格式为:
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|R| RFFU |
+-+-+-+-+-+-+-+-+
The value contains a single 1-bit flag:
R: If 1, the server is requested to reserve the next-higher port
number (on the same IP address) for a subsequent allocation. If
0, no such reservation is requested.
The other 7 bits of the attribute's value must be set to zero on
transmission and ignored on reception.
Since the length of this attribute is not a multiple of 4, padding
must immediately follow this attribute.
REQUESTED-TRANSPORT(0x0019)
client 发出 Allocation 请求指定传输类型:
- Protocol:指定需求的协议。可以取自 IPv4 报头中的协议字段的值或 IPv6 报头的下一个报头字段的协议号。目前仅允许设置为 17 ,即UDP。
- RFFU 字段在传输时必须设置为0,并在接收时被忽略。保留用于未来使用。
This attribute is used by the client to request a specific transport
protocol for the allocated transport address. The value of this
attribute is 4 bytes with the following format:
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Protocol | RFFU |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The Protocol field specifies the desired protocol. The codepoints
used in this field are taken from those allowed in the Protocol field
in the IPv4 header and the NextHeader field in the IPv6 header
[Protocol-Numbers]. This specification only allows the use of
codepoint 17 (User Datagram Protocol).
The RFFU field MUST be set to zero on transmission and MUST be
ignored on reception. It is reserved for future uses.
DONT-FRAGMENT(0x001A)
客户端使用该属性来请求服务器设置 IP 报头中的 DF(不要分片)位,当中继应用数据到对端时。该属性没有值,因此属性长度字段为 0。
RESERVATION-TOKEN(0x0022)
RESERVATION-TOKEN 属性包含一个 token 来唯一的标识一个中继传输地址已经被服务器保留。服务器在一个成功响应中包含该属性来告诉客户端这个 token,客户端在接下来的 Allocate 请求中包括该属性来请求服务器为这个 allocation 使用那个中继传输地址。属性值是8字节长。
New STUN Error Response Codes
- 403(Forbidden):请求是有效的,但因管理或类似的规定而不能被执行。
- 437(Allocation Mismatch):服务器接收到请求,要求在适当的位置的allocation,但没有allocation存在,或者一个收到的请求不指定任何allocation,但是一个allocation存在。
- 441(Wrong Credentials):请求中的证书没有匹配那些用来创建allocation的证书
- 442(不支持的传输协议):Allocate请求要求服务器使用一个用于服务器和对端的传输协议但是该服务器不支持该传输协议。
- 486(Allocation Quota Reached):目前没有更多的allocations资源使用相同的用户名可被创建。
- 508(Insufficient Capacity):服务器不能够完成请求因为一些性能限制已经达到上限。在一个Allocate响应中,这可能因为服务器此时已经没有更多的中继传输地址资源了,没有更多的被请求的性能,或者相当于特定的保留的token不可用。
Interactive Connectivity Establishment (ICE)
协议文档 RFC 5245
A Protocol for Network Address Translator (NAT) Traversal for
Offer/Answer Protocols
+-------+
| SIP |
+-------+ | Srvr | +-------+
| STUN | | | | STUN |
| Srvr | +-------+ | Srvr |
| | / \ | |
+-------+ / \ +-------+
/ \
/ \
/ \
/ \
/ <- Signaling -> \
/ \
/ \
+--------+ +--------+
| NAT | | NAT |
+--------+ +--------+
/ \
/ \
/ \
+-------+ +-------+
| Agent | | Agent |
| L | | R |
| | | |
+-------+ +-------+
Figure 1: ICE Deployment Scenario
ICE 的基本想法是:两端要进行连接时,每一端都会提供多个候选者:
- 直接连接网络接口上的传输地址:如一端有两块网卡,那么每块网卡的不同端口都对应一个候选者
- NAT公共端的转换传输地址(“服务器反射”地址)
- 从TURN服务器分配的传输地址(“中继地址”)。
潜在地,L 的任何候选传输地址都可以用于与R的任何候选传送地址通信。然而在实践中,许多组合都不起作用。例如,如果L和R都在NAT之后,它们直接连接的接口地址就不太可能直接通信(这就是为什么需要ICE的原因!)。ICE的目的是发现哪些地址对可以工作。ICE这样做的方式是系统地尝试所有可能的配对(按仔细排序的顺序),直到找到一个或多个有效的配对。
相关概念
Controlling Agent:控制方
负责选择最终候选配对并通过STUN和更新 offer
Controlled Agent:被控制方
等待控制代理选择最终候选对的ICE代理
Gathering Candidate Addresses:收集候选者
- Host Candidates:本机内网的 IP 和 Port;
- 候选主机是通过绑定到连接到主机上接口(物理或虚拟,包括VPN接口)的 IP 地址上的端口(通常是临时的)获得的
- Server Reflexive Candidates:本机 NAT 映射后的外网的 IP 和 Port
- Server Relayed Candidates:中继服务器的 IP 和 Port
Sending the Initial Offer
client 需要执行以下5个步骤,并提供了全量(full implementations)和轻量(lite implementations)两种方式:
- gather candidates
- prioritize them
- eliminate redundant candidates
- choose default candidates
- formulate and send the SDP offer
full implementations
lite implementations
lite 实现仅使用候选主机,必须为每个媒体流分配零个或一个 IPv4 候选者。它可以为主机使用的每个 IPv6 地址分配零个或多个 IPv6 候选者。由于每个媒体流的每个组件最多只能有一个 IPv4 候选地址,因此如果代理具有多个 IPv6 地址,则必须选择一个地址来分配候选地址。如果主机是双堆栈,建议它分配一个 IPv4 候选地址和一个全局 IPv6 地址。在精简实现中,ICE不能用于动态选择候选者。
Encoding the SDP
对SDP进行编码的过程在full和lite之间是相同的实现。
- ice-ufrag:username fragment:用户名片段
- ice-pwd attributes:password
- lite implementation: MUST include an “a=ice-lite”
- utilizing RTCP: MUST encode the RTCP candidate using the a=rtcp attribute
- RTCP is not in use: MUST signal that using b=RS:0 and b=RR:0
- 与 non-ICE peers通信时,作为媒体默认目的,传输地址也必须作为候选地址出现在一个或多个a=候选行中
SDP message that includes ICE attributes:
v=0
o=jdoe 2890844526 2890842807 IN IP4 10.0.1.1
s=
c=IN IP4 192.0.2.3
t=0 0
a=ice-pwd:asd88fgpdd777uzjYhagZg
a=ice-ufrag:8hhY
m=audio 45664 RTP/AVP 0
b=RS:0
b=RR:0
a=rtpmap:0 PCMU/8000
a=candidate:1 1 UDP 2130706431 10.0.1.1 8998 typ host
a=candidate:2 1 UDP 1694498815 192.0.2.3 45664 typ srflx raddr 10.0.1.1 rport 8998
STUN Extensions:four new attributes
- PRIORITY :0x0024
- USECANDIDATE :0x0025
- ICE-CONTROLLED
- ICE-CONTROLLING
PRIORITY(0x0024)
指明了 a peer reflexive candidate 的优先级,其值为一个 32 位无符号整数。
USECANDIDATE(0x0025)
表示此检查产生的候选对应该要用于媒体传输,该属性没有内容(该属性的“长度”字段为零)
ICE-CONTROLLED(64 位网络字节序无符号整)
在 Binding request 和 indicates 使用此属性,表明 client 信任 controlled role
ICE-CONTROLLING(64 位网络字节序无符号整数)
在 Binding request 和 indicates 使用此属性,表明 client 信任 controlling role
STUN Extensions:New Error Response Codes: 487 (Role Conflict)
b 站抓包分析
Binding Request to STUN Server
Binding success respose from STUN Server
Binding Request to a Peer
- 当两端角色都为controlling或者controlled角色冲突时,在连通性检查阶段,要求发送binding request消息里必须要带上tie-breaker属性。
- 当出现冲突时,比较tie-breaker大小,值比较大的则被认为是controlling,同时回应487错误给对端,对端收到487错误后切换角色。
Binding Request from a Peer
Binding success respose from a Client
the Peer Host Condition
Binding success respose from a Peer
mediasoup webrtc stun 源码分析
接收 stunData
inline void WebRtcTransport::OnStunDataReceived(
RTC::TransportTuple* tuple, const uint8_t* data, size_t len)
{
MS_TRACE();
RTC::StunPacket* packet = RTC::StunPacket::Parse(data, len);
if (!packet)
{
MS_WARN_DEV("ignoring wrong STUN packet received");
return;
}
// Pass it to the IceServer.
this->iceServer->ProcessStunPacket(packet, tuple);
delete packet;
}
解析 stunData:获取 attribute
private:
// Passed by argument.
Class klass; // 2 bytes.
Method method; // 2 bytes.
const uint8_t* transactionId{ nullptr }; // 12 bytes.
uint8_t* data{ nullptr }; // Pointer to binary data.
size_t size{ 0u }; // The full message size (including header).
// STUN attributes.
std::string username; // Less than 513 bytes.
uint32_t priority{ 0u }; // 4 bytes unsigned integer.
uint64_t iceControlling{ 0u }; // 8 bytes unsigned integer.
uint64_t iceControlled{ 0u }; // 8 bytes unsigned integer.
bool hasNomination{ false };
uint32_t nomination{ 0u }; // 4 bytes unsigned integer.
bool hasUseCandidate{ false }; // 0 bytes.
const uint8_t* messageIntegrity{ nullptr }; // 20 bytes.
bool hasFingerprint{ false }; // 4 bytes.
const struct sockaddr* xorMappedAddress{ nullptr }; // 8 or 20 bytes.
uint16_t errorCode{ 0u }; // 4 bytes (no reason phrase).
std::string password;
StunPacket* StunPacket::Parse(const uint8_t* data, size_t len)
{
MS_TRACE();
if (!StunPacket::IsStun(data, len))
return nullptr;
/*
The message type field is decomposed further into the following
structure:
0 1
2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+-+-+-+-+-+-+-+-+-+-+-+-+
|M |M |M|M|M|C|M|M|M|C|M|M|M|M|
|11|10|9|8|7|1|6|5|4|0|3|2|1|0|
+--+--+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 3: Format of STUN Message Type Field
Here the bits in the message type field are shown as most significant
(M11) through least significant (M0). M11 through M0 represent a 12-
bit encoding of the method. C1 and C0 represent a 2-bit encoding of
the class.
*/
// Get type field.
uint16_t msgType = Utils::Byte::Get2Bytes(data, 0);
// Get length field.
uint16_t msgLength = Utils::Byte::Get2Bytes(data, 2);
// length field must be total size minus header's 20 bytes, and must be multiple of 4 Bytes.
if ((static_cast<size_t>(msgLength) != len - 20) || ((msgLength & 0x03) != 0))
{
MS_WARN_TAG(
ice,
"length field + 20 does not match total size (or it is not multiple of 4 bytes), "
"packet discarded");
return nullptr;
}
// Get STUN method.
uint16_t msgMethod = (msgType & 0x000f) | ((msgType & 0x00e0) >> 1) | ((msgType & 0x3E00) >> 2);
// Get STUN class.
uint16_t msgClass = ((data[0] & 0x01) << 1) | ((data[1] & 0x10) >> 4);
// Create a new StunPacket (data + 8 points to the received TransactionID field).
auto* packet = new StunPacket(
static_cast<Class>(msgClass), static_cast<Method>(msgMethod), data + 8, data, len);
/*
STUN Attributes
After the STUN header are zero or more attributes. Each attribute
MUST be TLV encoded, with a 16-bit type, 16-bit length, and value.
Each STUN attribute MUST end on a 32-bit boundary. As mentioned
above, all fields in an attribute are transmitted most significant
bit first.
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Value (variable) ....
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
// Start looking for attributes after STUN header (Byte #20).
size_t pos{ 20 };
// Flags (positions) for special MESSAGE-INTEGRITY and FINGERPRINT attributes.
bool hasMessageIntegrity{ false };
bool hasFingerprint{ false };
size_t fingerprintAttrPos; // Will point to the beginning of the attribute.
uint32_t fingerprint; // Holds the value of the FINGERPRINT attribute.
// Ensure there are at least 4 remaining bytes (attribute with 0 length).
while (pos + 4 <= len)
{
// Get the attribute type.
auto attrType = static_cast<Attribute>(Utils::Byte::Get2Bytes(data, pos));
// Get the attribute length.
uint16_t attrLength = Utils::Byte::Get2Bytes(data, pos + 2);
// Ensure the attribute length is not greater than the remaining size.
if ((pos + 4 + attrLength) > len)
{
MS_WARN_TAG(ice, "the attribute length exceeds the remaining size, packet discarded");
delete packet;
return nullptr;
}
// FINGERPRINT must be the last attribute.
if (hasFingerprint)
{
MS_WARN_TAG(ice, "attribute after FINGERPRINT is not allowed, packet discarded");
delete packet;
return nullptr;
}
// After a MESSAGE-INTEGRITY attribute just FINGERPRINT is allowed.
if (hasMessageIntegrity && attrType != Attribute::FINGERPRINT)
{
MS_WARN_TAG(
ice,
"attribute after MESSAGE-INTEGRITY other than FINGERPRINT is not allowed, "
"packet discarded");
delete packet;
return nullptr;
}
const uint8_t* attrValuePos = data + pos + 4;
switch (attrType)
{
case Attribute::USERNAME:
{
packet->SetUsername(
reinterpret_cast<const char*>(attrValuePos), static_cast<size_t>(attrLength));
break;
}
case Attribute::PRIORITY:
{
// Ensure attribute length is 4 bytes.
if (attrLength != 4)
{
MS_WARN_TAG(ice, "attribute PRIORITY must be 4 bytes length, packet discarded");
delete packet;
return nullptr;
}
packet->SetPriority(Utils::Byte::Get4Bytes(attrValuePos, 0));
break;
}
case Attribute::ICE_CONTROLLING:
{
// Ensure attribute length is 8 bytes.
if (attrLength != 8)
{
MS_WARN_TAG(ice, "attribute ICE-CONTROLLING must be 8 bytes length, packet discarded");
delete packet;
return nullptr;
}
packet->SetIceControlling(Utils::Byte::Get8Bytes(attrValuePos, 0));
break;
}
case Attribute::ICE_CONTROLLED:
{
// Ensure attribute length is 8 bytes.
if (attrLength != 8)
{
MS_WARN_TAG(ice, "attribute ICE-CONTROLLED must be 8 bytes length, packet discarded");
delete packet;
return nullptr;
}
packet->SetIceControlled(Utils::Byte::Get8Bytes(attrValuePos, 0));
break;
}
case Attribute::USE_CANDIDATE:
{
// Ensure attribute length is 0 bytes.
if (attrLength != 0)
{
MS_WARN_TAG(ice, "attribute USE-CANDIDATE must be 0 bytes length, packet discarded");
delete packet;
return nullptr;
}
packet->SetUseCandidate();
break;
}
case Attribute::NOMINATION:
{
// Ensure attribute length is 4 bytes.
if (attrLength != 4)
{
MS_WARN_TAG(ice, "attribute NOMINATION must be 4 bytes length, packet discarded");
delete packet;
return nullptr;
}
packet->SetHasNomination();
packet->SetNomination(Utils::Byte::Get4Bytes(attrValuePos, 0));
break;
}
case Attribute::MESSAGE_INTEGRITY:
{
// Ensure attribute length is 20 bytes.
if (attrLength != 20)
{
MS_WARN_TAG(ice, "attribute MESSAGE-INTEGRITY must be 20 bytes length, packet discarded");
delete packet;
return nullptr;
}
hasMessageIntegrity = true;
packet->SetMessageIntegrity(attrValuePos);
break;
}
case Attribute::FINGERPRINT:
{
// Ensure attribute length is 4 bytes.
if (attrLength != 4)
{
MS_WARN_TAG(ice, "attribute FINGERPRINT must be 4 bytes length, packet discarded");
delete packet;
return nullptr;
}
hasFingerprint = true;
fingerprintAttrPos = pos;
fingerprint = Utils::Byte::Get4Bytes(attrValuePos, 0);
packet->SetFingerprint();
break;
}
case Attribute::ERROR_CODE:
{
// Ensure attribute length >= 4bytes.
if (attrLength < 4)
{
MS_WARN_TAG(ice, "attribute ERROR-CODE must be >= 4bytes length, packet discarded");
delete packet;
return nullptr;
}
uint8_t errorClass = Utils::Byte::Get1Byte(attrValuePos, 2);
uint8_t errorNumber = Utils::Byte::Get1Byte(attrValuePos, 3);
auto errorCode = static_cast<uint16_t>(errorClass * 100 + errorNumber);
packet->SetErrorCode(errorCode);
break;
}
default:;
}
// Set next attribute position.
pos =
static_cast<size_t>(Utils::Byte::PadTo4Bytes(static_cast<uint16_t>(pos + 4 + attrLength)));
}
// Ensure current position matches the total length.
if (pos != len)
{
MS_WARN_TAG(ice, "computed packet size does not match total size, packet discarded");
delete packet;
return nullptr;
}
// If it has FINGERPRINT attribute then verify it.
if (hasFingerprint)
{
// Compute the CRC32 of the received packet up to (but excluding) the
// FINGERPRINT attribute and XOR it with 0x5354554e.
uint32_t computedFingerprint = Utils::Crypto::GetCRC32(data, fingerprintAttrPos) ^ 0x5354554e;
// Compare with the FINGERPRINT value in the packet.
if (fingerprint != computedFingerprint)
{
MS_WARN_TAG(
ice,
"computed FINGERPRINT value does not match the value in the packet, "
"packet discarded");
delete packet;
return nullptr;
}
}
return packet;
}
ICE Server 处理
void IceServer::ProcessStunPacket(RTC::StunPacket* packet, RTC::TransportTuple* tuple)
{
MS_TRACE();
// Must be a Binding method.
if (packet->GetMethod() != RTC::StunPacket::Method::BINDING)
{
if (packet->GetClass() == RTC::StunPacket::Class::REQUEST)
{
MS_WARN_TAG(
ice,
"unknown method %#.3x in STUN Request => 400",
static_cast<unsigned int>(packet->GetMethod()));
// Reply 400.
RTC::StunPacket* response = packet->CreateErrorResponse(400);
response->Serialize(StunSerializeBuffer);
this->listener->OnIceServerSendStunPacket(this, response, tuple);
delete response;
}
else
{
MS_WARN_TAG(
ice,
"ignoring STUN Indication or Response with unknown method %#.3x",
static_cast<unsigned int>(packet->GetMethod()));
}
return;
}
// Must use FINGERPRINT (optional for ICE STUN indications).
if (!packet->HasFingerprint() && packet->GetClass() != RTC::StunPacket::Class::INDICATION)
{
if (packet->GetClass() == RTC::StunPacket::Class::REQUEST)
{
MS_WARN_TAG(ice, "STUN Binding Request without FINGERPRINT => 400");
// Reply 400.
RTC::StunPacket* response = packet->CreateErrorResponse(400);
response->Serialize(StunSerializeBuffer);
this->listener->OnIceServerSendStunPacket(this, response, tuple);
delete response;
}
else
{
MS_WARN_TAG(ice, "ignoring STUN Binding Response without FINGERPRINT");
}
return;
}
switch (packet->GetClass())
{
case RTC::StunPacket::Class::REQUEST:
{
// USERNAME, MESSAGE-INTEGRITY and PRIORITY are required.
if (!packet->HasMessageIntegrity() || (packet->GetPriority() == 0u) || packet->GetUsername().empty())
{
MS_WARN_TAG(ice, "mising required attributes in STUN Binding Request => 400");
// Reply 400.
RTC::StunPacket* response = packet->CreateErrorResponse(400);
response->Serialize(StunSerializeBuffer);
this->listener->OnIceServerSendStunPacket(this, response, tuple);
delete response;
return;
}
// Check authentication.
switch (packet->CheckAuthentication(this->usernameFragment, this->password))
{
case RTC::StunPacket::Authentication::OK:
{
if (!this->oldPassword.empty())
{
MS_DEBUG_TAG(ice, "new ICE credentials applied");
this->oldUsernameFragment.clear();
this->oldPassword.clear();
}
break;
}
case RTC::StunPacket::Authentication::UNAUTHORIZED:
{
// We may have changed our usernameFragment and password, so check
// the old ones.
// clang-format off
if (
!this->oldUsernameFragment.empty() &&
!this->oldPassword.empty() &&
packet->CheckAuthentication(this->oldUsernameFragment, this->oldPassword) == RTC::StunPacket::Authentication::OK
)
// clang-format on
{
MS_DEBUG_TAG(ice, "using old ICE credentials");
break;
}
MS_WARN_TAG(ice, "wrong authentication in STUN Binding Request => 401");
// Reply 401.
RTC::StunPacket* response = packet->CreateErrorResponse(401);
response->Serialize(StunSerializeBuffer);
this->listener->OnIceServerSendStunPacket(this, response, tuple);
delete response;
return;
}
case RTC::StunPacket::Authentication::BAD_REQUEST:
{
MS_WARN_TAG(ice, "cannot check authentication in STUN Binding Request => 400");
// Reply 400.
RTC::StunPacket* response = packet->CreateErrorResponse(400);
response->Serialize(StunSerializeBuffer);
this->listener->OnIceServerSendStunPacket(this, response, tuple);
delete response;
return;
}
}
// The remote peer must be ICE controlling.
if (packet->GetIceControlled())
{
MS_WARN_TAG(ice, "peer indicates ICE-CONTROLLED in STUN Binding Request => 487");
// Reply 487 (Role Conflict).
RTC::StunPacket* response = packet->CreateErrorResponse(487);
response->Serialize(StunSerializeBuffer);
this->listener->OnIceServerSendStunPacket(this, response, tuple);
delete response;
return;
}
MS_DEBUG_DEV(
"processing STUN Binding Request [Priority:%" PRIu32 ", UseCandidate:%s]",
static_cast<uint32_t>(packet->GetPriority()),
packet->HasUseCandidate() ? "true" : "false");
// Create a success response.
RTC::StunPacket* response = packet->CreateSuccessResponse();
// Add XOR-MAPPED-ADDRESS.
response->SetXorMappedAddress(tuple->GetRemoteAddress());
// Authenticate the response.
if (this->oldPassword.empty())
response->Authenticate(this->password);
else
response->Authenticate(this->oldPassword);
// Send back.
response->Serialize(StunSerializeBuffer);
this->listener->OnIceServerSendStunPacket(this, response, tuple);
delete response;
uint32_t nomination{ 0u };
if (packet->HasNomination())
nomination = packet->GetNomination();
// Handle the tuple.
HandleTuple(tuple, packet->HasUseCandidate(), packet->HasNomination(), nomination);
break;
}
case RTC::StunPacket::Class::INDICATION:
{
MS_DEBUG_TAG(ice, "STUN Binding Indication processed");
break;
}
case RTC::StunPacket::Class::SUCCESS_RESPONSE:
{
MS_DEBUG_TAG(ice, "STUN Binding Success Response processed");
break;
}
case RTC::StunPacket::Class::ERROR_RESPONSE:
{
MS_DEBUG_TAG(ice, "STUN Binding Error Response processed");
break;
}
}
}