coturn stateless-nonce 模式怎么配置:防止伪造源 IP 洪水耗尽会话内存
【免费下载链接】coturncoturn TURN server project项目地址: https://gitcode.com/GitHub_Trending/co/coturn
当你的 coturnturnserver以 long-term / auth-secret 认证对外提供 UDP 服务时,会面对一个内存层面的攻击面:UDP 源地址可以被伪造,而一个结构合法的 STUNALLOCATE在认证之前就提交了真实资源——listener 地址表里的一个子 UDP socket、一个约 18KB 的ts_ur_super_session(只为存放 nonce),以及一个让二者存活 60 秒(TURN_MAX_ALLOCATE_TIMEOUT)的 to-be-allocated 定时器。文档给出的量化是:10k 伪造包/秒就能撑起数十万条存活会话、数 GB 内存,而这些流量永远走不到认证。
--stateless-nonce模式就是为封住这个面而加的。开启后,challenge nonce 不再是每会话随机值,而是一个可校验的时间戳 cookie;未认证的伪造洪水不分配任何状态,只有把本服务器发给它的那个 nonce 原样回显回来的源才会拿到会话。这篇文章讲怎么开启/配置它,以及怎么验证它确实生效。
先确认威胁与默认状态
先理解为什么--unauthorized-ratelimit单独不够:它压制的是 401响应(反射/放大防御),但状态在那之前已经分配了,所以它约束不了内存(详见 401-ratelimit.md)。内存约束来自 stateless-nonce 的 listener fast path:
- 来自未知 UDP 源、不带 MESSAGE-INTEGRITY 的请求,直接由 listener 回 401 challenge——不建子 socket、不建会话。
- 带 MESSAGE-INTEGRITY 的请求,只有当它的
NONCE能证明是本服务器发给该源地址的,才被放行进会话路径;其余(realm 不匹配、nonce 缺失/畸形)由 listener 直接按check_stun_auth的字节顺序应答400/437/441/438。
关键前提:该模式默认开启。所以第一步不是"加参数",而是确认你没有显式关掉它:
- 显式开启(等价于默认):
--stateless-nonce - 关闭(配置文件写法
stateless-nonce=false):--stateless-nonce=false
只有在需要兼容硬编码 16 字符 nonce 缓冲的客户端时才考虑关闭(见文末限制)。
配置方法
单实例:默认即生效
单台服务器什么都不用加,fast path 默认已生效。若你希望显式声明或从命令行确认:
turnserver --use-auth-secret --static-auth-secret=<你的auth-secret> --realm=<你的realm> \ --stateless-nonce --log-file=stdout其中--static-auth-secret/--realm是项目测试文档里用的示例值,替换成你自己的认证配置即可。nonce 有效期由--stale-nonce控制,文档与源码一致:未设置时默认 600 秒。
多实例 / 负载均衡:共享密钥
默认密钥是进程内临时随机值——重启即失效,负载均衡下落在另一实例上的重试也会多走一次438。要让整套集群互相校验对方发的 nonce,用--stateless-nonce-secret(配置文件写法stateless-nonce-secret,隐含开启--stateless-nonce):
turnserver --use-auth-secret --static-auth-secret=<你的auth-secret> --realm=<你的realm> \ --stateless-nonce-secret=<你的高熵secret> --log-file=stdout派生公式与运行要点(均来自文档,务必逐条核对):
- 密钥派生:
K = SHA-256( "coturn-stateless-nonce-v1" || secret )。同一 secret 的所有服务器跨重启、跨集群互相校验。 - 熵由你负责:KDF 固定的是密钥长度,不是猜解熵。攻击者收集几个 401 就能对弱口令做离线字典攻击——用长随机字符串。
- 不要复用其他凭据(例如
--static-auth-secret)当这个 secret 的值;label 域分隔只是防碰撞,不是复用许可。 - 优先写配置文件,别放命令行:命令行上的 secret 在进程列表里可见。
- 时钟必须一致:跨实例校验会把 nonce 内嵌的签发时间和校验方时钟比较;整个集群要在
--stale-nonce有效期内良好 NTP 同步(未来偏差容忍只有几秒)。 - 轮换目前 = 改 secret 并重启;客户端经一次标准
438重认证恢复。同时接受新旧两个 secret 是文档里明确的 future work。
可选加强:401 响应限流
stateless-nonce 管"内存不增长",--unauthorized-ratelimit管"响应不被放大"。二者正交,可叠加。它默认关闭、opt-in,只对 UDP 生效(TCP/TLS 无法伪造,从不被限流):
turnserver --use-auth-secret --static-auth-secret=<你的auth-secret> --realm=<你的realm> \ --unauthorized-ratelimit --unauthorized-ratelimit-rps=10--unauthorized-ratelimit-rps=<count>默认 10(每秒每源 IP 的 401 上限);非正值会被拒并回退默认。- 某源在窗口内首次超限时,服务器记一条日志(窗口内其余丢弃静默):
401 rate-limit exceeded from <ip>, suppressing responses for this window - 开启 Prometheus 时,
turn_unauthenticated_401_requests/turn_unauthenticated_401_responses/turn_unauthenticated_401_dropped_responses三个计数器描述这条 UDP 反射面。在 stateless-nonce 下,伪造 MESSAGE-INTEGRITY 触发的438/437/441/400应答消耗同一个每源预算、同样被压制,日志记为unauthorized-response rate-limit exceeded,但不进 Prometheus 的 401 计数器。
验证它真的生效
光启动不等于 fast path 在工作。项目提供了端到端回归与探测脚本,按从快到全的顺序验证。
1. 检查服务器启动日志标记
启动后,在日志里找两条标记(这是回归套件 run_tests_stateless_nonce.sh 实际 grep 的字符串):
stateless-nonce: listener fast-path challenge active—— fast path 确实接管,而非静默回退到逐会话路径。- 使用
--stateless-nonce-secret时另有Stateless nonce key derived from the configured secret—— 确认密钥来自配置的 secret 而非临时随机值。
缺任一条,说明配置没落地,回到上一步核对参数。
2. 标准客户端工作负载(证明线协议透明)
该模式设计为对客户端不可见,所以断言就是"标准负载照常成功"。测试脚本的做法是用turnutils_uclient经 UDP/TCP(Linux 上再加 TLS/DTLS)各中继 1000 字节,成功判定为客户端日志出现:
start_mclient: tot_send_bytes ~ 1000, tot_recv_bytes ~ 1000单条最小验证(UDP,-W为 auth-secret,user为用户名):
turnutils_uclient -u user -W <你的auth-secret> -e 127.0.0.1 -X -g 127.0.0.1TCP 加-t;TLS/DTLS 加-t -S/-S。出现上面的tot_send_bytes ~ 1000, tot_recv_bytes ~ 1000即通过;否则用脚本里的diagnose_failure思路看客户端的 401/438/nonce 行与服务器 auth/session 行。
3. 伪造 MESSAGE-INTEGRITY 探测(证明无会话应答)
examples/scripts/stateless_nonce_forged_mi.py 用纯标准库构造turnutils_uclient造不出的畸形请求,并固定每种应答码:
python3 examples/scripts/stateless_nonce_forged_mi.py 127.0.0.1 3478 <你的用户名>成功时逐条打印OK: ...并以RESULT forged-mi-probe=pass结束。它固定的应答码:未签发 nonce →438;realm 不匹配(ALLOCATE →437,REFRESH →441);UDP 上的 CONNECTION-BIND、缺 NONCE、缺 USERNAME →400;服务器确实发给该 socket 的 nonce→ 放行进会话路径、因伪造的 integrity 失败而回401。这正是"带 MI 的请求只有在 nonce 可证明归属该源时才值得一个会话"的直接证据。
flood子模式可配合限流观察压制效果(配合--unauthorized-ratelimit-rps使用,flood <count>发包并统计应答数)。noncelen子模式用于断言 challenge nonce 长度:stateless 模式应为 24 字符,legacy(--stateless-nonce=false)服务器应为 16 字符。
4. 完整回归套件
要一次跑完"负载 + 日志标记 + 伪造 MI 探测 + legacy 校验",用 examples/run_tests_stateless_nonce.sh。它是专为 stateless-nonce 设计的单一任务套件,可作整体回归。副作用与前提需先说明:
- 需要已构建的二进制,脚本按
../bin/turnserver或../build/bin/turnserver探测; - 脚本会自行启动并 kill它拉起的
turnserver与turnutils_peer进程,在/tmp写临时日志并在退出时删除(trap cleanup EXIT); - TLS/DTLS 用例会引用
../examples/ca/turn_server_cert.pem/turn_server_pkey.pem,需已构建并存在这些证书; - macOS 上会跳过 TLS/DTLS 与非确定性部分,只跑确定性子集;Linux 是完整目标。
限制与已知边界
- 默认开启,
--stateless-nonce=false用于兼容硬编码 16 字符 nonce 缓冲的客户端;正常合规客户端(RFC 8489 要求把 nonce 当作最多 128 字符的不透明串)不受 24 字符长度影响。 - 重启与集群:默认临时密钥意味着重启让未过期 nonce 失效,每客户端一次
438重认证;负载均衡下落在另一实例的重试同理。跨实例用--stateless-nonce-secret消除,并要求 NTP 同步。 - BINDING 不走此路径:BINDING 无需认证、有自己的 listener fast path,与本选项独立;
--secure-stun的 BINDING challenge 只受益于 challenge 会话拆解,不受益于 nonce fast path。 - TCP/TLS 行为不变:这些传输无法伪造源、已按连接固定,会话保持原有行为(派生 nonce 被使用,无害)。
- secret 泄露影响有限:该密钥签的是 DoS 加固 cookie,不是凭据——伪造 nonce 永远无法通过认证(MESSAGE-INTEGRITY 仍被校验),只是让伪造流量重新到达凭据查找,约等于该特性引入前的负载,内存仍被 challenge 会话拆解约束。
配置落地后,验证收敛到两点:日志里能看到listener fast-path challenge active标记、伪造 MI 探测打印forged-mi-probe=pass;同时标准turnutils_uclient负载仍按tot_send_bytes ~ 1000, tot_recv_bytes ~ 1000成功。三者齐备,即代表未认证伪造洪水不再分配会话状态,而合法客户端行为不变。
【免费下载链接】coturncoturn TURN server project项目地址: https://gitcode.com/GitHub_Trending/co/coturn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考