☰
自建STUN服务端:基于coturn在Ubuntu上部署NAT穿透基础设施
2026/10/21 3:22:55 网站建设 项目流程

做音视频通信或者 P2P 传输的同学,对 STUN 这个名字应该都不陌生。STUN(Session Traversal Utilities for NAT)的完整称呼很长,做的事情却相当单纯:帮客户端发现自己经过 NAT 之后的公网地址和端口映射。无论是 WebRTC 通话、SIP 语音,还是自建的 P2P 文件传输,ICE 协商的第一步,几乎都是向 STUN 服务器发一个 Binding 请求,拿回自己的公网映射信息,然后才能继续后续的连通性检测。换句话说,STUN 服务端是这套流程里的“公证人”,所有客户端都要先找它对一下坐标,才知道自己在这个网络里到底长什么样。

我一直劝身边做实时通信和音视频相关的朋友,尽量自己部署一套 STUN 服务端,而不是长期依赖公共 STUN。公共 STUN 服务日常用起来问题不大,但一旦进入生产环境,可用性、限流策略、隐私这些事全都不可控。而且 STUN 服务端本身非常轻量,部署难度说实话比配置一个 Nginx 还低。这篇文章以我自己在 Ubuntu 上部署 coturn 作为 STUN 服务端的完整过程为例,从原理、选型到配置、测试、排障,把整个链路拆开讲一遍,适合刚接触 NAT 穿透的开发者,也适合已经在用 WebRTC 但要自己搭建基础服务的人参考。

1. STUN 服务端到底在解决什么问题

1.1 NAT 环境下的公网地址发现原理

绝大多数终端设备都不在公网上,而是躲在路由器、运营商 NAT 后面。你在内网看到的 IP 是私有地址,比如 192.168.x.x,这个地址出了局域网就没有意义了。外部网络想主动连到你,首先得知道你公网一侧的“门牌号”——也就是 NAT 设备为这次会话分配的映射地址。问题是,这个映射地址只有 NAT 设备自己知道,终端自己是“看不见”的。

STUN 协议解决的就是这个问题。客户端向 STUN 服务器发一个 Binding 请求,STUN 服务器从 UDP 数据包里取出源 IP 和源端口,也就是这个包在公网上显现出来的地址,然后原样写进 Binding 响应里返回给客户端。这个地址叫 Mapped Address,也就是经过 NAT 映射后的公网地址。整个过程只需要一次 UDP 往来,开销极小。

这里有个关键概念必须讲清楚:STUN 能发现的是“当前这条 UDP 会话经过 NAT 后映射出来的地址”,但不同的 NAT 行为差异很大。有的 NAT 映射是固定的,内网同一地址端口出去,公网映射也一直不变,这种叫锥形 NAT,STUN 拿到的映射地址可以被外部直接复用,后续通行很顺畅。还有一类叫对称 NAT,每次连接同一个内网地址端口,发往不同目标地址都会生成不同的公网映射。STUN 只能告诉你“当前到这台服务器”的映射地址,换一个目标地址后映射又变了,这就超出了 STUN 的能力范围,必须靠 TURN 中继来兜底。

1.2 为什么选择自建而不是长期依赖公共服务

公共 STUN 服务器最大的问题是“不受控”。公共服务的提供方随时可能调整限流策略,某段时间请求量一上去,响应延迟就会明显波动。更要命的是,你无法获得任何 SLA 承诺,真出故障了连个反馈渠道都没有,只能看着线上 WebRTC 通话成功率往下掉,干着急。

另一个常被忽视的点是隐私。STUN Binding 请求会暴露客户端的公网 IP、端口、访问时间,这些信息会落到第三方服务手里。对于一个把用户隐私放在重要位置的产品来说,这是不可接受的。自建之后,所有信令相关的网络行为都收敛在自己的基础设施里,日志脱敏规则、保留周期都由自己定。

自建的门槛其实低到可以忽略。一个只跑 STUN 的服务端,CPU 和内存占用都非常小,流量也只有客户端周期性探测产生的那一点 UDP 包。我在实际部署中,给 STUN 服务分配的资源只有 1 核 512MB,运行大半年 CPU 平均占用率一直是个位数。相比它带来的稳定性收益,这点成本完全不值一提。

1.3 适用场景:WebRTC、VoIP、P2P 应用

自建 STUN 服务端最典型的场景有三类。第一类是 WebRTC 实时通信,浏览器和移动端在做 ICE 连接时,需要 STUN 服务器来收集候选地址。只要还在用 WebRTC,这基本是刚需。第二类是 SIP VoIP 终端,很多软电话和 IP 话机在注册和呼叫阶段会通过 STUN 探测 NAT 映射,用来解决单向语音和注册失败问题。第三类是各种 P2P 应用,比如自建的文件传输工具、远程桌面工具、游戏对战平台的房间匹配,这些应用做 UDP 打洞之前,通常也要先通过 STUN 获取映射信息。

我的建议是:只要你的业务里出现了“需要拿到公网地址”或者“两个内网设备需要直接通信”这样的诉求,不管规模多小,都先架一台 STUN 服务端放在那儿。它不一定马上派上用场,但后续排查 NAT 相关问题时,有一个自己完全可控的探测节点,调试效率会高非常非常多。

2. 部署前的环境准备与方案选型

2.1 服务器资源配置与网络要求

先说结论:纯 STUN 服务对服务器资源的需求非常低。我推荐用一台拥有公网 IP 的轻量云主机,1 核 512MB 内存起步就够用,带宽也不需要大,因为 STUN 请求体不过是几十字节的 UDP 包。真正吃带宽的是 TURN 的中继流量,如果你以后打算把 TURN 也开起来做媒体转发兜底,那再考虑加大带宽和内存,纯 STUN 阶段不必浪费这个钱。

网络方面只有两个硬性要求。第一,服务器必须有公网 IP,而且最好直接绑定在网卡上,而不是又套了一层 NAT。如果云主机用的是私网 IP + 弹性公网 IP 的架构,必须在 coturn 里配置 external-ip 参数,否则服务端返回的 Mapped Address 会是内网地址,那整个部署就白费了。第二,必须放通 UDP 端口的入站访问。STUN 默认监听 3478 端口,很多云厂商的安全组默认只放行 TCP,UDP 端口常常被漏掉,这是排查问题时最容易踩的坑。

如果手头有域名,建议给 STUN 服务器配一个子域名,比如 stun.example.com。虽然 IP 也能用,但域名解耦了 IP 变更的影响,以后迁移或扩缩容会方便很多。

2.2 软件选型对比:coturn 与其他实现

STUN 服务端的实现有不少选择,我直接列一个对比表,方便你做选型判断。

软件语言维护状态特点适用建议
coturnC活跃功能最全,同时支持 STUN 和 TURN,配置灵活,社区用户多生产环境首选,文档和踩坑资料最丰富
restundC维护一般轻量,但功能相对单薄,配置风格比较老适合极简场景,不太推荐新手用
stunmanC基本停滞老牌 STUN 实现不建议新项目使用
自研/脚本实现Python 等看自己能快速验证概念,但性能和健壮性差得远只适合本地学习测试,不适合线上

我最终选了 coturn,原因很直接:它有一套完整的 NAT 穿透解决方案,以后业务发展需要 TURN 中继时,不用换软件,改几行配置就能把 TURN 打开。虽然这次的目标只是 STUN,但选型不能只看眼前需求,要留好升级路径。

2.3 Ubuntu 基础系统环境配置

我用的系统是 Ubuntu 22.04 LTS,coturn 在 Ubuntu 软件源里有现成包,直接 apt 安装就行。在装 coturn 之前,我习惯先把系统基础环境梳理一遍。

时间同步这一项容易被忽略,但必须做。TURN 的长期凭证机制依赖时间戳校验,如果服务器和客户端之间的时间偏差超过一定范围,鉴权会直接失败。虽然纯 STUN 阶段用不到时间凭证,但既然以后可能升级 TURN,先把 NTP 服务配置好,timedatectl看一下保证时间源是 ntp 状态。

防火墙规则我习惯用 ufw。如果服务器上还跑着其他服务,务必把放通规则写得明确一些:

ufw allow 22/tcp ufw allow 3478/udp ufw enable

云厂商安全组的 UDP 端口放通别忘了去控制台确认。我遇到过不止一次本地 ufw 配好了,结果流量全被安全组拦在外面的情况。

3. coturn 部署与关键配置参数

3.1 安装 coturn

Ubuntu 上安装 coturn 非常简单,一条命令就能完成:

apt update apt install coturn -y

安装完成后先确认版本,不同版本的配置项有一些细微差异,确认版本能帮助后续排查:

turnserver --version

coturn 安装完成后,默认并不自动启动。这算是一个容易让新手困惑的地方,安装完明明没有报错,可服务就是不在运行。原因是 coturn 的 init 脚本默认策略比较保守,需要你显式开启。这一步我们放到服务管理小节一起讲。

3.2 配置文件逐项解读

coturn 的主配置文件是/etc/turnserver.conf,安装后里面全是注释示例。我建议直接新建一份自己的配置,把不需要的默认项都去掉,保持配置文件的整洁。下面是一份经过我实际验证的纯 STUN 模式配置:

# 基础监听设置 listening-port=3478 listening-ip=0.0.0.0 # 服务器域名标识 realm=stun.example.com # 指纹属性,兼容 WebRTC 等客户端要求 fingerprint # 纯 STUN 模式,关闭 TURN 中继功能 no-turn # 关闭 TCP 中继和 TLS 中继相关功能 no-tcp-relay # 不使用认证(纯 STUN Binding 请求本身不需要鉴权) no-auth

逐项解释一下这几个参数的含义,它们背后都有实际考虑。

listening-port=3478是 STUN 的标准端口,客户端库通常默认也走这个端口,没有特殊原因不要改。listening-ip=0.0.0.0表示监听所有网卡地址,对于只有单一公网网卡的服务器来说这是最省事的写法,但如果你绑定了多个网卡,建议改成具体的公网 IP 更安全。

fingerprint是我强烈建议开启的选项。WebRTC 规范要求 ICE 候选里的 STUN 消息带指纹属性,不开这个,浏览器端的连接可能会异常。加上它只是多加一个属性字节,对性能几乎没有影响。

no-turn是这一份配置的点睛之笔。coturn 默认把 STUN 和 TURN 一起开启,我们这次的部署目标只是 STUN,所以必须显式关掉 TURN 中继。no-tcp-relay是进一步确认不提供 TCP 中继。no-auth是纯 STUN 模式下的合理选择,STUN Binding 请求本身不需要鉴权,开着反而徒增复杂度。以后要升级成 TURN 中继时,把no-turn和no-tcp-relay去掉,加上lt-cred-mech、user、realm等配置即可,改动成本很低。

如果你用的是私网 IP + 弹性公网 IP 的云主机架构,还要额外加一行:

external-ip=203.0.113.10

这行的作用是告诉 coturn,对外公布的公网地址是 203.0.113.10,而不是网卡上实际绑定的私网地址。很多云主机场景下,网卡上能看到的可能是 172.16.x.x 或者 10.x.x.x 这样的私网 IP,没有 external-ip,服务端返回的 Mapped Address 就是私网地址,客户端拿到后一点用都没有。

3.3 服务管理与开机自启

配置写好后,需要修改/etc/default/coturn文件,这个文件是 coturn 的总开关。把默认的注释行打开:

TURNSERVER_ENABLE=1

这一步不做,后面即使你手动启动了 turnserver,系统重启后也不会自动拉起服务。然后启动并设置开机自启:

systemctl enable coturn systemctl start coturn

启动后立刻确认监听状态:

ss -ulpn | grep 3478

正常情况下能看到0.0.0.0:3478的 UDP 监听。这里我提醒一个细节:任何时候改完配置文件,都要用systemctl restart coturn重启服务,单纯 reload 对 coturn 不一定生效,它有些参数只在启动时读取一次。

3.4 防火墙与云安全组放通 UDP 端口

服务起来之后,第一件事就是验证网络层是否真的通了。本地回环测试不能证明外部流量能到达,必须模拟真实访问。

先在服务器本机测试:

ufw status verbose

确认 3478/udp 规则存在。然后从另一台机器上用 UDP 探测工具验证端口可达性。我常用的方式是 nc 的 UDP 模式,发一个包过去看能不能收到回包:

nc -u -z -v 203.0.113.10 3478

如果本机能通而外部不通,那基本可以确定是云安全组的问题,去云厂商控制台把 UDP 3478 的入站规则加上。另外还要检查服务器是否绑定了多个防火墙策略,有些环境里除了 ufw 还有 iptables 的后门规则,前后顺序一乱,UDP 包就被丢了。

4. 服务端验证与客户端测试

4.1 使用 STUN 客户端工具测试

部署完成后的第一项验证,是用 STUN 客户端工具发一个真实的 Binding 请求。Ubuntu 软件源里有个客户端工具包就叫stun,用法一目了然:

apt install stun -y stun 203.0.113.10

或者显式指定端口:

stun 203.0.113.10 3478

正常输出里会包含 Mapped Address 信息,格式大致是:

Mapped Address = 203.0.113.10:3478

如果测试用的客户端和 STUN 服务器不在同一个网络,输出里应该能看到客户端的公网出口地址。这一步能一下子就验证出两个关键点:一是 UDP 通路正常,二是 STUN 返回的映射地址确实是公网地址。

如果系统里不方便装客户端,也可以用一个简单的 Python 脚本,借助 pystun3 库完成同样的验证:

pip install pystun3
import pystun3 nat_type, mapped_ip, mapped_port = pystun3.get_ip_info( stun_host="203.0.113.10", stun_port=3478, source_ip="0.0.0.0", ) print("NAT Type:", nat_type) print("Mapped IP:", mapped_ip) print("Mapped Port:", mapped_port)

这个脚本还会顺带输出探测到的 NAT 类型,调试时很有用。

4.2 从不同网络环境验证 NAT 穿透效果

STUN 服务端部署正确与否,只在本机回环上测是不够的,必须从真实的 NAT 环境里测。我的验证流程是准备三个不同的网络环境:一个是与服务器同机房的内网,一个是我办公室的 NAT 网络,还有一个是手机 4G/5G 网络。

同机房内网测试的目的是确认基础连通性。办公室 NAT 网络测试能反映大多数真实用户的场景,如果客户端在办公室网络拿到的映射地址是出口路由器的公网 IP,说明服务端表现正常。手机 4G/5G 网络测试则比较挑剔,运营商 NAT 往往更复杂,如果这个环境下也能正确拿到公网映射地址,基本可以认为服务端没有任何网络层面的问题。

我个人的经验是,手机蜂窝网络是最适合压测 STUN 配置是否正确的手段。因为运营商的 NAT 行为多种多样,一旦出现 external-ip 配置错误、防火墙拦截之类的问题,在蜂窝网络下暴露得最快。

4.3 WebRTC 场景验证

如果部署 STUN 服务端的最终目的是支撑 WebRTC,那最后一步一定要放到真实的 WebRTC 流程里验。

在 WebRTC 的 RTCPeerConnection 里,把 iceServers 指向自己的 STUN 服务:

const pc = new RTCPeerConnection({ iceServers: [ { urls: "stun:stun.example.com:3478" } ] }); pc.onicecandidate = (event) => { if (event.candidate) { console.log("ICE candidate:", event.candidate.candidate); } };

打开页面后,在浏览器控制台观察 ICE candidate。正常情况下,candidate 列表里会有一个 type 为 srflx 的候选,地址指向你的公网映射 IP。如果看到的是 host 候选或者 type 为 relay 的候选,说明 STUN 路径可能没走通,或者 TURN 被当作了兜底路径。

这里有个容易混淆的点:即使 STUN 配置正确,浏览器也可能同时收集到 host、srflx、relay 多种候选。srflx 候选的出现才是 STUN 服务生效的标志,其他候选类型不能用来判断 STUN 可用性。

5. 常见问题与排查技巧实录

5.1 常见问题速查表

我在实际部署和维护中踩过不少坑,这里整理成一张速查表,排障时对照着看会省很多时间。

现象可能原因处理方式
外部客户端请求超时云安全组未放通 UDP 3478检查云控制台安全组入站规则
外部客户端请求超时ufw 规则未生效或顺序不对用 ufw status 检查,必要时先 disable 再 enable
服务未运行/etc/default/coturn 没有开启设置 TURNSERVER_ENABLE=1 后启动
服务启动失败配置语法错误或端口被占用用 journalctl -u coturn 看具体报错
返回的映射地址是内网 IP服务器在 NAT 后面,缺少 external-ip配置 external-ip 为公网 IP
WebRTC 只有 host 候选STUN 请求根本没发出去或被拦截用 tcpdump 抓包确认请求是否到达服务端
WebRTC 有 srflx 但迟迟连不上对端处于对称 NAT,STUN 无法穿透iceServers 里增加 TURN 服务
TURN 鉴权失败客户端服务器时间偏差过大两端都做好 NTP 时间同步

这里面的高发问题其实集中在两类:一类是网络放通没做到位,另一类是 external-ip 配置缺失。前者占比最高,我在帮朋友排查时,十个有八个问题最后都定位在云安全组或者 ufw 上。

5.2 日志排查实践

coturn 的日志默认写在/var/log/turnserver.log,排查问题时日志的顺序非常重要。出现问题后我习惯先清空日志再复现问题,这样看日志时不会被无关信息干扰:

truncate -s 0 /var/log/turnserver.log systemctl restart coturn # 然后复现问题 tail -f /var/log/turnserver.log

如果你想看到更细的信息,可以在启动命令里加-v,或者修改配置里的verbose参数。Verbose 日志里会记录每个 Binding 请求的来源地址和处理结果,对判断请求有没有真正到达服务端非常有帮助。结合 tcpdump 抓包一起看,基本能覆盖九成以上的排障场景:

tcpdump -i eth0 udp port 3478 -nn

抓包能明确区分两类问题:如果客户端发包了但服务端没收到,是网络路径的问题;如果服务端收到但没回包,是 coturn 配置的问题。这个二分法是最有效的排障思路。

5.3 性能与稳定性优化实录

纯 STUN 服务的性能瓶颈几乎不会出现在 CPU 和内存上,反而在网络栈的内核参数上。高并发场景下,UDP 接收缓冲区不足会导致丢包,表现是客户端请求偶发超时。我一般会在部署时顺手调一下内核参数:

sysctl -w net.core.rmem_max=26214400 sysctl -w net.core.wmem_max=26214400

如果要永久生效,写到/etc/sysctl.conf里。这个值对 STUN 的场景来说绰绰有余,不用为了追求大而继续往上调,够用就好。

稳定性方面,coturn 作为长时间运行的服务,最需要关注的是日志增长和文件句柄数。日志建议配置 logrotate 轮转,Ubuntu 的 coturn 包默认带了 logrotate 配置,但需要确认它确实在生效。文件句柄在极端并发下可能会打满,可以用ulimit -n检查当前的限制值。我自己的生产环境把 nofile 限制调到 65535,然后跑了大半年没出过句柄相关的问题。

另外,我建议把 STUN 服务端的存活状态纳入基础监控。最简单的做法是写一个定时任务,每隔几分钟用 stun 客户端探测一次,失败就告警。因为 STUN 服务平时请求量不大,一旦挂了很容易长期没人发现,直到音视频业务开始报障才反应过来。

最后再分享一个小习惯:每次改完配置,我都会用同一套测试流程从三个环境跑一遍——本机、办公室 NAT、手机蜂窝网络。这套流程跑下来只要几分钟,但能确认的问题覆盖面非常广。很多人部署 STUN 服务端时只在本地验证了一遍就上线了,结果实际使用中各种奇怪问题层出不穷。测试环境覆盖到位,上线后才能真正省心。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询