☰
自建STUN服务端:从原理到coturn生产环境部署与运维
2026/10/22 4:29:47 网站建设 项目流程

前两天群里有人问,自家服务器上要不要自己部署一个 STUN服务端。放在两年前,我大概率会反问他为什么不用现成的公共节点,但经历过两次线上呼叫事故之后,我的答案变成了“看场景,但多数情况下自己搭一个没坏处”。那次事故其实很典型:用公共STUN做WebRTC呼叫探测,几分钟后通话中断,客户端日志里全是ICE连接超时,换了一台公共节点稍微好一点,但延迟和抖动依然不稳定。后来我在一台Ubuntu 22.04服务器上,用coturn把STUN服务端跑起来,再把客户端的iceServers指过去,问题彻底消失。

这篇文章把我从选型到验证,再到线上踩坑的整个过程拆开讲。会覆盖STUN到底解决什么问题、为什么公共节点不一定靠谱、coturn在Ubuntu上的安装与最小配置、用什么方法验证服务真的没问题,以及生产环境上线前需要处理的安全和运维细节。不管是纯做WebRTC音视频,还是自研P2P传输、SIP信令,只要你想有一个自己能掌控的STUN服务端,这篇文章可以直接照抄。

1. 为什么放着公共STUN不用,非要自己部署?

很多人理解的STUN就是“填一个地址到WebRTC配置里”,至于这个地址背后的服务器是谁的、稳不稳定,很少有人细想。这里我先花一点篇幅把STUN的作用讲清楚,再来说自建的收益和适用场景。

1.1 一段话讲明白STUN到底做了什么

STUN,全称Session Traversal Utilities for NAT,中文常翻译成“NAT会话穿越工具”。它的核心功能一句话就能说清:当你的设备处在一个NAT后面(绝大多数家里的路由器、公司内网、手机4G/5G网络都属于这种情况),STUN服务器帮你查出“你在公网上暴露出来的IP和端口是什么”。

打个比方。你住在一个小区里,快递员只知道小区大门地址,根本不知道你家是哪栋哪户。小区物业相当于NAT设备,快递员把包裹送到物业,物业再转给你。问题来了,如果你想让外面的朋友直接给你送东西,你得先知道自己在物业那里登记的门牌号是多少——STUN服务器就是帮你向物业问出这个门牌号的服务台。你问它“我在你眼里是从哪个IP:端口访问过来的”,它把你的公网映射地址回复给你。

这个过程在协议上就是一次Binding Request / Binding Response交互。客户端向STUN服务器的3478端口发一个UDP请求,服务器收到后,把请求来源的公网IP和端口填进响应里返回。这个地址就是后面P2P连接要用的candidate之一。

1.2 公共STUN节点的问题:限流、延迟、隐私

公共STUN节点最大的优势是免费、不用自己维护,但它的劣势在真实联调里往往被低估。

先说限流。WebRTC建立一次呼叫,不是只发一个STUN请求就结束的。ICE候选收集阶段,客户端会同时向多个STUN地址发包,还会做连通性检查,一个页面在不同网络环境下可能连续触发几十上百次STUN请求。公共节点的服务策略是针对海量用户设计的,当某个IP的请求频率过高,很容易被限制甚至直接丢弃。表现到用户那边就是“呼叫偶尔建立不起来,过一会儿又好了”。

其次是延迟和路径质量。公共STUN节点部署位置你控制不了,如果你在跨地域甚至跨国的网络环境里做音视频,请求绕了大半个地球,响应时间可能上百毫秒。STUN虽然只是拿一个地址,但超时和丢包会直接拖慢ICE候选收集,连带的后果就是接通变慢。自己在北京、上海、广州各放一台Ubuntu服务器做STUN,让客户端就近接入,效果是完全不一样的。

最后是隐私。STUN请求里包含你的真实内网IP和访问时间,这些日志在公共节点上你拿不到也不可控。某些行业项目对接入审计有要求,所有网络请求必须落在自己可以追溯的服务器上,公共STUN天然不满足这个条件。

1.3 什么项目值得自己搭,什么项目最好别折腾

自建STUN不是万能灵药。我自己的判断标准是:如果你的服务需要长期稳定运行、每天的呼叫量达到一定规模、或者对链路路径有地域控制要求,那就值得自己搭。典型的场景包括企业内部的音视频会议系统、硬件设备远程调试、SIP话务网关、自研的端到端传输组件。自己搭的好处不仅仅是稳,还能和你的TURN服务做联动,生成统一的ICE配置下发到端上。

反过来,如果你只是写个Demo、一次性联调验证,或者压根没有公网服务器资源,还是用公共STUN省事。还有一类情况我也不建议自建:你的业务对可用性要求达到99.99%以上,但公司又没有专业的网络运维人员。STUN服务本身很轻,但一旦挂了,所有依赖它的呼叫都会受影响,单节点自建在这种场景下反而是风险。这类项目更适合直接采购有SLA保障的托管服务,或者自己做多节点冗余,而不是搭一台就不管。

2. 部署之前,把NAT穿越和STUN协议的基本逻辑理一遍

上过手的人都知道,STUN部署本身不难,难的是出问题时不知道怎么排查。排查的前提是理解NAT的行为,以及STUN协议在做什么。这一节把这些基础补上,后面看配置和日志的时候你就不会懵。

2.1 NAT的四种类型:为什么对称型NAT是STUN解决不了的

NAT按映射行为通常分成四种:

NAT类型映射规则对P2P的影响
Full Cone内网IP:端口映射到固定的公网IP:端口,任何外部主机都能访问最容易穿透,STUN一次查询即可
Restricted Cone映射固定,只允许曾经被内网访问过的外部IP进来需要先打洞,STUN可用
Port Restricted Cone在Restricted基础上还限制了外部端口需要打洞并匹配端口,STUN可用
Symmetric每次访问不同目标IP:端口,都会生成新的公网映射STUN查到的地址只对STUN服务器有效,P2P基本穿不通

关键区别在于Symmetric NAT:设备访问STUN服务器时得到了一个映射地址,但这个地址只是“设备到STUN服务器”这条路径上的映射,换一个对端就换一个地址。你把STUN返回的地址给对端,对端用它来连你的时候,你的NAT已经为这条新路径重新分配了端口,原来的映射根本不起作用。这就是为什么很多人发现STUN配了也没用,最终还是要靠TURN中继转发流量。

自己搭STUN之前先搞清楚NAT类型还挺重要的,否则你会误以为STUN服务坏了。实际上服务端没坏,是客户端处在Symmetric NAT后面,STUN本来就解决不了这种场景。

2.2 Binding请求与XOR-MAPPED-ADDRESS:协议上发生了什么

STUN协议在RFC 5389里定义,最简单的交互是这样的:客户端构造一个20字节的消息头,消息类型为0x0001(Binding Request),发到服务器的3478端口。服务器收到后,看消息来源的IP和端口,把它封装在Binding Response的XOR-MAPPED-ADDRESS属性里返回。

XOR-MAPPED-ADDRESS是RFC 5389引入的加密混淆方案,原因是为了兼容DNS。STUN消息头包含一个16位的Transaction ID,客户端凭它来匹配请求和响应。简单理解,整个协议就是“你问一次,我答一次,各自带上一个随机的流水号”,没有会话、没有握手,非常轻。

关于TCP和TLS:STUN虽然大多数场景走UDP,但也支持TCP和TLS,分别对应SIP信令和WebRTC中的TCP中继场景。coturn默认同时监听UDP和TCP的3478端口,如果你用TLS则通常是5349端口。这些端口规划接下来在配置里会用到。

2.3 STUN和TURN:为什么总被放在一起讨论

STUN只负责“告诉你地址在哪”,TURN负责“在连不通的情况下帮你转发数据”。两者经常一起出现,是因为ICE流程里STUN负责找到直接路径,TURN是最后的兜底通道。WebRTC的iceServers配置里可以同时填STUN地址和TURN地址。

这个边界对部署很有用。如果只是做STUN服务,coturn可以关闭TURN相关功能,减少端口暴露和带宽占用。如果后续要开TURN,你只需要修改配置而不需要换软件,coturn在同一套服务里都支持。这也是我选择coturn而不是其他轻量实现的原因之一。

3. Ubuntu 下安装 coturn 并跑通最小可用的 STUN 服务

回到实操。我的环境是Ubuntu 22.04 LTS,coturn版本4.6.1,用apt装。下面从选型、配置、端口、启动四个步骤走一遍。

3.1 为什么选 coturn,而不是自己写或找冷门工具

STUN的协议本身不复杂,网上能找到许多几百行的参考实现,但生产环境不推荐这么做。STUN服务一旦出问题,影响的是所有使用它的呼叫和穿透流程,你需要的是经过大量并发验证、持续更新、社区覆盖足够广的成熟方案。coturn是目前事实上的标准选择,它由原来的rfc5766-turn-server项目演进而来,支持STUN、TURN、DTLS、TLS、Redis和SQLite后端等能力,包体积不大,Ubuntu官方源里直接有。

其他备选方案里还有纯STUN的轻量服务,但它们通常不支持TURN,后续扩展时要再引入一套系统。考虑到你搭完STUN大概率迟早要试TURN,不如一开始就统一在coturn上。我也见过有人用Go语言基于开源库快速封装一个STUN端,调试时可以,长期跑还是算了——日志、限流、权限控制这些都要自己处理,得不偿失。

3.2 安装与核心配置

在Ubuntu上安装很简单:

sudo apt update sudo apt install coturn -y

安装完成后,有两个文件需要改。第一个是/etc/default/coturn,里面的内容是是否以系统服务方式启动coturn的开关:

TURNSERVER_ENABLED=1

这个文件默认是注释状态,如果不改成1,systemd服务即使启动也会立刻退出。第二个文件是/etc/turnserver.conf,我的最小STUN配置长这样:

listening-port=3478 listening-ip=0.0.0.0 # 如果你的服务器通过云主机的NAT映射到公网,必须填下面的公网IP # external-ip=你的公网IP fingerprint no-turn no-cli realm=stun.local

逐行解释一下。

listening-port=3478是STUN标准端口,客户端默认会连它。listening-ip=0.0.0.0表示监听所有网卡。fingerprint会在STUN消息里追加一个可选的CRC校验属性,部分客户端要求开启,建议加上。no-turn表示当前只提供STUN功能,关闭TURN中继,这是纯STUN服务最重要的一个开关。no-cli关闭telnet管理接口,减少一个暴露面。realm是用于认证域标识,纯STUN场景不需要认证,但后续如果你在同一份配置上开TURN,它会用到,现在先随便定义个值。

这里特别注意external-ip。如果你的服务器本身就是公网IP,直接绑在网卡上,可以不用配。但大多数云主机拿到的网卡是私网IP,公网IP通过NAT映射进来,此时不配external-ip,STUN返回给客户端的映射地址会是私网IP,跨公网必然连不通。我见过太多人卡在这一步。

3.3 端口规划与防火墙放行

STUN服务需要开放哪些端口,取决于你怎么用。我这里整理了一张对照表:

端口协议用途是否必需
3478UDPSTUN Binding 主端口,WebRTC默认走UDP必需
3478TCP支持TCP上的STUN,供SIP或工具探测使用按需
5349TCP/TLSSTUN over TLS,加密传输按需
49152-65535UDPTURN中继端口段,纯STUN不需要后续开TURN时必需

如果你只做STUN,最精简的规则是放行UDP 3478。我用ufw的话是这样:

sudo ufw allow 3478/udp sudo ufw allow 3478/tcp

注意云厂商的安全组也别忘了同步放行,只改本机ufw没有用,我踩过一次这个坑。

3.4 用 systemd 把服务拉起来

配置改完后启动,并设置开机自启:

sudo systemctl enable coturn sudo systemctl start coturn sudo systemctl status coturn

状态显示active (running)后,再验证端口监听:

sudo ss -ulpn | grep 3478

正常情况下能看到udp UNCONN ... 0.0.0.0:3478 users:[("turnserver"...)]这样的输出。到这里,一个最小STUN服务端已经跑起来了。整个过程不到五分钟,但真正的问题会出现在验证阶段和上线之后。

4. 怎么验证 STUN 服务真的能用:本地自测 + 跨网络实测

很多同学部署完,拿浏览器页面点击呼叫,发现通了,就觉得STUN没问题。这个结论下得太早。浏览器能通可能走的是TURN中继或主机候选,根本没用上你的STUN。验证要做两层:本地命令行自测,以及跨网络真实场景实测。

4.1 用 coturn 自带的客户端命令行验活

coturn安装包里自带一个测试工具,叫turnutils_stunclient,专门用来做STUN通信测试。路径一般在这里:

/usr/bin/turnutils_stunclient

用法很简单:

turnutils_stunclient -p 3478 127.0.0.1

如果服务正常,输出类似这样:

IPv4. UDP reply from 127.0.0.1:3478 mapped address: 127.0.0.1:随机端口

但这只能说明本机端口通了,不能证明公网链路OK。正确的做法是从另一台公网服务器,或者从手机开热点后的笔记本上去执行,命令一样:

turnutils_stunclient -p 3478 你的服务器公网IP

你拿到的mapped address应该是你当前出口的公网IP:端口。如果你在这里看到的是一个私网IP,比如192.168.x.x,那基本就是external-ip没配对。

4.2 用 WebRTC 完整链路验证 STUN 是否进入候选池

命令行工具验证的是“STUN协议通不通”,WebRTC验证的是“你的客户端会不会真的用到这个STUN服务器”。做法是打开一个WebRTC示例页面,把iceServers里的stun地址替换成你自己的服务端,然后建立音视频通话。

关键观察点在onicecandidate回调里打印出的candidate内容。一个典型的结构长这样:

candidate:... 1 udp 2122260223 192.168.1.5 61223 typ host candidate:... 1 udp 1686052607 公网IP 53123 typ srflx candidate:... 1 tcp 1518280447 公网IP 9 typ relay

其中typ host是主机局域网地址,typ srflx是STUN反射得到的公网映射地址,typ relay是TURN中继地址。如果你能看到本机内网以外、带有你自己公网IP的srflx候选,说明STUN确实被用上了。

跨网络验证时,建议用两个不同运营商的网络,比如一边是移动宽带、一边是电信4G。这样能排除同一个出口NAT下的“自欺欺人”结果。如果两边都出现了srflx候选,且能直接建立P2P连接,STUN服务端才算真正达标。

4.3 服务端抓包:从另一个角度确认协议正常

如果客户端验证还是排查不出问题,直接到服务器上看包。STUN是明文协议,抓包能看得非常清楚。服务器上执行:

sudo tcpdump -i eth0 -nn udp port 3478 -w stun.pcap

用Wireshark打开抓到的pcap,过滤条件写stun。你会看到客户端发来的Binding Request,以及服务器返回的Binding Response,响应里带XOR-MAPPED-ADDRESS属性。如果只有请求没有响应,说明处理逻辑出了问题,多半是外部IP映射配置不对;如果包都没到服务器,查安全组和网络路径。

4.4 持续盯两个指标:成功率与时延

验证结束不代表万事大吉。我建议在部署完成后写一个简单的探活脚本,定时从外网发STUN请求,统计成功率和响应时延。脚本逻辑很简单:

#!/bin/bash server=你的服务器公网IP success=0 total=100 for i in $(seq 1 $total); do if /usr/bin/turnutils_stunclient -p 3478 "$server" | grep -q "mapped address"; then success=$((success+1)) fi done echo "成功率: $((success * 100 / total))%"

跑下来成功率接近100%,单次响应普遍在30ms以内,这个服务就很健康。如果偶尔出现响应超时,先查服务器负载和网络丢包,不要急着怀疑配置。

5. 从“通了”到“稳定”:我踩过的坑和你大概率也会踩的坑

写到这里,我相信你已经能把服务跑通。但跑通和稳定之间还有一段距离。下面是几个我在实际部署过程中遇到过的、以及帮别人排查时反复出现的问题,全部有真实教训价值。

5.1 原以为只开STUN,结果TURN也在跑

coturn默认配置会在没有任何额外设置的情况下启用TURN能力。如果你没有显式加no-turn,服务其实同时承担STUN和TURN职能。这不一定是坏事,但对“只想搭一个STUN服务端”的人来说是个隐患:TURN会开放大量中继端口(默认49152到65535),还会被公网扫描器盯上,被人拿来当流量转发代理用。

我处理过一台被用来做流量转发的服务器,检查发现就是早期部署的coturn把TURN开着了,没有配置认证,任何人发送Allocate请求都能拿到中继资源。所以纯STUN场景一定写死no-turn,后续决定开TURN时再单独评估认证策略。

5.2 external-ip没配,客户端拿到的映射地址是内网IP

一个朋友公司搭STUN,内网测试一切正常,一跑到公网就失败。我去现场看了看,配置文件里只有listening-ip=内网网卡IP,没有external-ip。他用的云主机公网IP是通过NAT映射的,STUN服务器读到包是从内网网卡进来的,于是把内网IP直接作为映射地址返回给了客户端。客户端拿到一个192.168开头的候选,公网对端根本连不上。

修正办法很简单,在config里指定公网地址:

external-ip=你的公网IP

如果你的服务器有多个公网IP,或者走的是路由器端口映射,还可以写成多个值。配完后,再从外网跑一次turnutils_stunclient确认mapped address已经是公网IP。

5.3 systemd和daemon模式打架,服务起不来

coturn早期版本编译出来的二进制是daemon模式,它会自己fork到后台。如果你用systemctl start coturn,systemd可能显示启动成功,但进程很快就脱离控制、或者被systemd判定为退出。更常见的症状是启动时提示端口被占用,但实际上你自己手动跑过一个turnserver进程没有清理。

解决方法是让coturn以systemd托管方式运行:确认/etc/default/coturn里TURNSERVER_ENABLED=1,并且配置文件里不要写daemon相关的开启项。如果端口被占,先用pkill turnserver清干净再启动。用systemctl status coturn和journalctl -u coturn -f看日志,比盲目重启靠谱得多。

5.4 只放行了3478端口,忘了TURN中继端口段

这个坑发生在你后续从纯STUN升级到STUN+TURN的时候。3478端口通了,WebRTC的candidate里也出现了relay类型的地址,但P2P始终建立不起来。排查到最后才发现,服务器安全组只放行了UDP 3478,TURN中继分配的端口段49152到65535全被防火墙挡在外面。

建议你在规划防火墙时就把49152-65535这一整段一起评估。即使现在只做STUN,也把这个规则提前写好,后续开TURN时不用手忙脚乱。放行命令:

sudo ufw allow 49152:65535/udp

5.5 日志爆炸与调试级别

coturn默认日志挺啰嗦的。在没有明确配置日志文件的情况下,日志可能会写到/var/log/turnserver目录,或者通过syslog进入journald。如果你为了调试开了很高级别的verbosity,线上跑几天磁盘空间就会告警。

我现在的做法是生产环境关闭调试日志,错误级别输出到syslog,由journald统一管理。需要排查时才临时把日志级别调高,查完立刻改回来。磁盘如果被日志占满,STUN进程不会自己崩,但系统其他服务会因为写不进去日志而表现异常,不容易定位。

5.6 客户端在Symmetric NAT下,STUN通但不解决问题

最后提醒一个容易误判的情况。客户端和STUN服务的交互始终是成功的,mapped address每次都返回正常,但P2P就是不通。原因开头讲过了,客户端处在Symmetric NAT后面,STUN查到的映射地址只对STUN服务器有效,对端拿这个地址来连接时,NAT已经分配了新的端口。这不是服务器的问题,是网络环境决定了必须上TURN。

遇到这种情况,不要继续调STUN参数,而是尽快把TURN服务补上。这也是我一直建议“即便现在只做STUN,也先想好TURN方案”的原因。

6. 上线前的安全加固与生产化配置清单

服务能稳定运行之后,下一步是让它安全地暴露在公网上。这一节把我在生产环境里会做的加固工作列全。

6.1 限制访问范围:能用白名单就别全开

STUN协议本身没有鉴权设计,任何能发起Binding请求的人都可以用你的服务。纯STUN不用担心像TURN那样被滥用做流量转发,但无意义的并发扫描也可能浪费资源。

如果服务只给固定客户端用,可以按来源IP做白名单。coturn提供了访问控制配置:

allowed-peer-ip=10.0.0.0/8 allowed-peer-ip=192.168.0.0/16

注意这个配置是控制TURN中继会话里的对端IP范围的。对于纯STUN,我建议直接在云安全组层面限制来源IP段,比应用层配置更简洁可靠。

6.2 带宽与配额限制:防白嫖、防攻击

当你开始启用TURN时,配额设置从第一天就要写上。coturn的配额配置主要看这几个参数:

max-bps=5242880 max-allocations=100

max-bps限定每个会话的带宽上限,这里例子给的是5Mbps,你可以按自己业务调整。max-allocations限制同时存在的TURN中继分配数量,防止有人批量申请耗尽端口。

这些参数对纯STUN阶段没有作用,但提前写好不会影响当前功能,而且规范了后续扩展时的行为。

6.3 证书与TLS:SIP和WebRTC TCP场景迟早用得上

如果你的客户端中有SIP话机、或者WebRTC需要走TCP链路,STUN over TLS就很有必要。coturn对TLS的支持是在同一套配置里开启的:

cert=/etc/coturn/certs/fullchain.pem pkey=/etc/coturn/certs/privkey.pem tls-listening-port=5349

证书文件需要提前准备好,路径换成你自己的。开启后,5349端口用TLS加密,客户端可以通过stuns:服务器IP:5349的方式访问。这个配置不会干扰UDP 3478上的STUN请求,两者可以同时工作。

6.4 探活与监控:STUN挂了,呼叫症状会非常隐蔽

STUN服务中断的最典型表现是呼叫建立时间变长、连接质量下降,而不是直接不可用,所以很多人会忽略它。我自己的习惯是用异地探活脚本做主动监控。脚本用第一节里的turnutils_stunclient,放到另外一台和STUN服务器不在同一个网络的主机上,定时执行:

*/5 * * * * /opt/scripts/stun_probe.sh

脚本失败时就发送告警。告警方式可以用邮件hook、企业微信机器人、钉钉机器人,看你们团队的工程习惯。关键是“异地”这两个字,本机探活只能说明进程没死,说明不了公网链路是否正常。

6.5 多地域部署:一台STUN服务撑不起全国音视频

最后一个建议,如果你的用户分布在全国甚至全球,一台STUN服务在延迟上是不合格的。STUN请求要求低延迟,跨地域绕路会让ICE候选收集明显变慢。生产级做法是在主要用户区域各部署一台,客户端配置里按就近策略选择地址。

有人会问,STUN服务器之间要同步状态吗?不需要。STUN是无状态的,每台服务器各响应各的请求即可。这也是它适合多地域部署的原因——不用做任何数据同步,水平扩展非常容易。你在多个地域的Ubuntu服务器上跑同一套配置,客户端根据自己的区域选择最近的地址,效果比任何单一优化都明显。

在实际操作中,我会建议从最简单的单节点起步,确认流程跑通后再逐步增加节点。整个过程不复杂,但每个节点的配置和探活脚本最好用同一套自动化方式管理,避免手工改配置时漏掉external-ip之类的关键项。STUN服务端的价值不在于技术复杂度,而在于稳定性和可控性,把基础打好,后面再上TURN、上多地域、做监控告警,都是顺理成章的事情。

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

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

立即咨询