跳到主要内容

16. 详细示例

本节给出一个使用 TURN 的示例, 详细展示所交换消息的内容. 该示例使用概述 (Overview) 中图 1 所示的网络图.

对于每条消息, 下文展示该消息包含的属性及其取值. 为方便阅读, 取值以人类可读的格式展示, 而不是展示实际八位组; 例如, "XOR-RELAYED-ADDRESS=192.0.2.15:9000" 表示存在一个 XOR-RELAYED-ADDRESS 属性, 地址为 192.0.2.15, 端口为 9000, 其中地址和端口显示的是应用 XOR 操作之前的值. 对于具有类似字符串取值的属性, 例如 SOFTWARE="Example client, version 1.03" 和 NONCE="adl7W7PeDU4hKE72jdaQvbAMcr6h39sm", 为了可读性, 属性值以引号显示, 但这些引号并不出现在实际取值中.

 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 | |
| SOFTWARE="Example client 1.03" | | |
| LIFETIME=3600 (1 hour) | | |
| 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) | | |
| XOR-RELAYED-ADDRESS=192.0.2.15:50000 | |
| XOR-MAPPED-ADDRESS=192.0.2.1:7000 | |
| MESSAGE-INTEGRITY=... | | |

客户端首先选择一个用于 TURN 会话的主机传输地址. 在本例中, 如图 1 所示, 客户端选择了 10.1.1.2:49721. 随后, 客户端向服务器传输地址上的服务器发送 Allocate 请求. 客户端为此事务随机选择了一个 96 位事务 ID, 即 0xA56250D3F17ABE679422DE85, 该值编码在固定头部的事务 ID 字段中. 客户端包含一个 SOFTWARE 属性, 用于给出客户端软件的信息; 此处取值为 "Example client, version 1.03", 表明这是名为 Example client 的某个软件的 1.03 版本. 客户端包含一个 LIFETIME 属性, 因为它希望分配 (allocation) 的生命周期长于默认的 10 分钟; 该属性的值为 3600 秒, 对应 1 小时. 客户端必须始终在 Allocate 请求中包含 REQUESTED-TRANSPORT 属性; 本规范唯一允许的值是 17, 表示服务器与对等方之间使用 UDP 传输. 客户端还包含 DONT-FRAGMENT 属性, 因为它希望稍后在 Send indication 中使用 DONT-FRAGMENT 属性; 该属性只包含属性头部, 没有取值部分. 最后, 我们假定客户端最近未与服务器交互, 因此未包含 USERNAME, REALM, NONCE 或 MESSAGE-INTEGRITY 属性. 注意, 消息中属性的顺序是任意的, MESSAGE-INTEGRITY 和 FINGERPRINT 属性除外, 客户端也可以使用不同的顺序.

服务器要求所有请求都经过认证. 因此, 当服务器收到初始 Allocate 请求时, 由于该请求不包含认证属性, 服务器会拒绝该请求. 按照 STUN [RFC5389] 长期凭据机制的过程, 服务器包含一个取值为 401 (Unauthorized) 的 ERROR-CODE 属性, 一个指定服务器所用认证域的 REALM 属性 (此处为服务器的域 "example.com"), 以及 NONCE 属性中的 nonce 值. 服务器还包含一个 SOFTWARE 属性, 用于给出服务器软件的信息.

收到 401 错误后, 客户端再次尝试 Allocate 请求, 这次会包含认证属性. 客户端选择了新的事务 ID, 并用与之前相同的属性填充新的 Allocate 请求. 客户端包含 USERNAME 属性; 它使用从服务器接收的 realm 值来帮助确定应使用哪个取值, 此处客户端被配置为针对 realm "example.com" 使用用户名 "George". 客户端包含 REALM 和 NONCE 属性, 二者从 401 错误响应中复制而来. 最后, 客户端将 MESSAGE-INTEGRITY 属性作为消息中的最后一个属性; 该属性的值是对消息内容计算得到的 HMAC-SHA1 哈希 (上文仅显示为 "..."); 此 HMAC-SHA1 计算包含一个密码值. 因此, 攻击者如果不知道秘密密码, 就无法计算消息完整性值.

收到已认证的 Allocate 请求后, 服务器检查一切正常, 然后创建一个分配. 服务器以 Allocate 成功响应进行回复. 服务器包含 LIFETIME 属性, 用于给出该分配的生命周期; 此处, 服务器将客户端请求的 1 小时生命周期缩短为仅 20 分钟, 因为这个特定服务器不允许超过 20 分钟的生命周期. 服务器包含 XOR-RELAYED-ADDRESS 属性, 其取值是该分配的中继传输地址. 服务器包含 XOR-MAPPED-ADDRESS 属性, 其取值是客户端的服务器反射地址; 该值在 TURN 中的其他地方并不使用, 返回它只是为了方便客户端. 服务器包含 MESSAGE-INTEGRITY 属性, 用于认证响应并保证其完整性. 注意, 该响应不包含 USERNAME, REALM 和 NONCE 属性. 服务器还包含 SOFTWARE 属性.

 TURN                                 TURN           Peer          Peer
client server A B
|--- CreatePermission request ------>| | |
| Transaction-Id=0xE5913A8F460956CA277D3319 | |
| XOR-PEER-ADDRESS=192.0.2.150:0 | | |
| USERNAME="George" | | |
| REALM="example.com" | | |
| NONCE="adl7W7PeDU4hKE72jdaQvbAMcr6h39sm" | |
| MESSAGE-INTEGRITY=... | | |
| | | |
|<-- CreatePermission success resp.--| | |
| Transaction-Id=0xE5913A8F460956CA277D3319 | |
| MESSAGE-INTEGRITY=... | | |

随后, 客户端为对等方 A 创建一个权限 (permission), 以便准备向其发送一些应用数据. 这是通过 CreatePermission 请求完成的. XOR-PEER-ADDRESS 属性包含要为其建立权限的 IP 地址 (即对等方 A 的 IP 地址). 注意, 在 CreatePermission 请求中使用该属性时, 属性中的端口号会被忽略; 此处端口号被设置为零. 还要注意, 客户端使用的是对等方 A 的服务器反射 IP 地址, 而不是它的 (私有) 主机地址. 客户端复用该分配上一次请求中的 username, realm 和 nonce 值. 虽然这样做是允许的, 但客户端选择不在此请求中包含 SOFTWARE 属性.

服务器收到 CreatePermission 请求后, 创建相应权限, 并以 CreatePermission 成功响应进行回复. 与客户端一样, 服务器也选择不在其回复中包含 SOFTWARE 属性. 再次注意, 成功响应包含 MESSAGE-INTEGRITY 属性 (假定服务器使用长期凭据机制), 但不包含 USERNAME, REALM 和 NONCE 属性.

 TURN                                 TURN           Peer          Peer
client server A B
|--- Send indication --------------->| | |
| Transaction-Id=0x1278E9ACA2711637EF7D3328 | |
| XOR-PEER-ADDRESS=192.0.2.150:32102 | |
| DONT-FRAGMENT | | |
| DATA=... | | |
| |-- UDP dgm ->| |
| | data=... | |
| | | |
| |<- UDP dgm --| |
| | data=... | |
|<-- Data indication ----------------| | |
| Transaction-Id=0x8231AE8F9242DA9FF287FEFF | |
| XOR-PEER-ADDRESS=192.0.2.150:32102 | |
| DATA=... | | |

客户端现在使用 Send indication 向对等方 A 发送一些应用数据. 对等方 A 的服务器反射传输地址在 XOR-PEER-ADDRESS 属性中指定, 应用数据在 DATA 属性中指定 (上文仅显示为 "..."). 客户端正在应用层执行某种形式的路径 MTU 发现, 因此它通过包含 DONT-FRAGMENT 属性来指定服务器应在发送给对等方的 UDP 数据报中设置 DF 位. 无法使用 STUN 的长期凭据机制来认证 indication, 因此该消息中没有 MESSAGE-INTEGRITY 属性. 如果应用希望确保其数据未被篡改或伪造, 就必须在应用层对其数据进行完整性保护.

收到 Send indication 后, 服务器提取应用数据, 并在 UDP 数据报中将其发送给对等方 A, 数据报的源地址为中继传输地址, 且按请求设置 DF 位. 注意, 如果客户端之前没有为对等方 A 的服务器反射 IP 地址建立权限, 服务器会静默丢弃该 Send indication.

随后, 对等方 A 用自己的 UDP 数据报回复, 该数据报包含应用数据. 该数据报被发送到服务器上的中继传输地址. 当数据报到达时, 服务器创建一个 Data indication, 其中 UDP 数据报的源在 XOR-PEER-ADDRESS 属性中给出, UDP 数据报的数据在 DATA 属性中给出. 随后, 服务器将生成的 Data indication 发送给客户端.

 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=... | | |

客户端现在将一个信道 (channel) 绑定到对等方 B, 在 CHANNEL-NUMBER 属性中指定一个未使用的信道号 (0x4000), 并在 XOR-PEER-ADDRESS 属性中指定对等方 B 的传输地址. 与之前一样, 客户端复用其上一次请求中的 username, realm 和 nonce.

收到该请求后, 服务器将信道号绑定到该对等方, 并为对等方 B 的 IP 地址安装一个权限, 然后以 ChannelBind 成功响应进行回复.

 TURN                                 TURN           Peer          Peer
client server A B
|--- ChannelData ------------------->| | |
| Channel-number=0x4000 |--- UDP datagram --------->|
| Data=... | Data=... |
| | | |
| |<-- UDP datagram ----------|
| | Data=... | |
|<-- ChannelData --------------------| | |
| Channel-number=0x4000 | | |
| Data=... | | |

客户端现在向服务器发送一个 ChannelData 消息, 其中包含发往对等方 B 的数据. ChannelData 消息不是 STUN 消息, 因此没有事务 ID; 相反, 它只有三个字段: 信道号, 数据, 以及数据长度. 此处, 信道号字段为 0x4000 (即客户端刚刚绑定到对等方 B 的信道). 当服务器收到 ChannelData 消息时, 它会检查该信道当前是否已绑定 (确实已绑定), 然后在 UDP 数据报中将数据转发给对等方 B, 使用中继传输地址作为源传输地址, 并使用 192.0.2.210:49191 (ChannelBind 请求中 XOR-PEER-ADDRESS 属性的值) 作为目的传输地址.

稍后, 对等方 B 向中继传输地址发送一个 UDP 数据报. 这会使服务器向客户端发送一个 ChannelData 消息, 其中包含来自该 UDP 数据报的数据. 服务器知道应将 ChannelData 消息发送给哪个客户端, 因为 UDP 数据报发送到了对应的中继传输地址; 服务器也知道应使用信道 0x4000, 因为该信道绑定到了 192.0.2.210:49191. 注意, 如果没有信道号绑定到该地址, 服务器会改用 Data indication.

 TURN                                 TURN           Peer          Peer
client server A B
|--- Refresh request --------------->| | |
| Transaction-Id=0x0864B3C27ADE9354B4312414 | |
| SOFTWARE="Example client 1.03" | | |
| USERNAME="George" | | |
| REALM="example.com" | | |
| NONCE="adl7W7PeDU4hKE72jdaQvbAMcr6h39sm" | |
| MESSAGE-INTEGRITY=... | | |
| | | |
|<-- Refresh error response ---------| | |
| Transaction-Id=0x0864B3C27ADE9354B4312414 | |
| SOFTWARE="Example server, version 1.17" | |
| ERROR-CODE=438 (Stale Nonce) | | |
| REALM="example.com" | | |
| NONCE="npSw1Xw239bBwGYhjNWgz2yH47sxB2j" | |
| | | |
|--- Refresh request --------------->| | |
| Transaction-Id=0x427BD3E625A85FC731DC4191 | |
| SOFTWARE="Example client 1.03" | | |
| USERNAME="George" | | |
| REALM="example.com" | | |
| NONCE="npSw1Xw239bBwGYhjNWgz2yH47sxB2j" | |
| MESSAGE-INTEGRITY=... | | |
| | | |
|<-- Refresh success response -------| | |
| Transaction-Id=0x427BD3E625A85FC731DC4191 | |
| SOFTWARE="Example server, version 1.17" | |
| LIFETIME=600 (10 minutes) | | |

在 20 分钟生命周期到期之前的某个时间, 客户端刷新该分配. 这是使用 Refresh 请求完成的. 与之前一样, 客户端在请求中包含最新的 username, realm 和 nonce 值. 客户端还包含 SOFTWARE 属性, 遵循在 Allocate 和 Refresh 消息中始终包含该属性的建议. 当服务器收到 Refresh 请求时, 它发现 nonce 已经过期, 因此以 438 (Stale Nonce) 错误进行回复, 并提供新的 nonce 值. 随后客户端再次尝试该请求, 这次使用新的 nonce 值. 第二次尝试被接受, 服务器以成功响应进行回复. 注意, 客户端未在请求中包含 LIFETIME 属性, 因此服务器按默认生命周期 10 分钟刷新该分配 (如成功响应中的 LIFETIME 属性所示).