一文自建STUN:在Ubuntu上分步搭建STUN服务

目标:在 Ubuntu 服务器上部署一个 STUN 服务 ,用于WebRTC的NAT 穿透(获取客户端的公网映射地址)。

主方案使用 coturn(业界标准、维护活跃)。


一、STUN 简介

客户端躲在路由器/防火墙后面时,自己并不知道"外面看到的我是什么地址"。

STUN(Session Traversal Utilities for NAT) 就是用来回答这个问题的:

复制代码
客户端 ──STUN Binding 请求──► STUN 服务器
客户端 ◄──返回"我看到的你的公网 IP:端口"── STUN 服务器

STUN 服务器把请求来源的 公网 IP + 端口 回给客户端(该地址在 ICE 里叫 srflx 候选,

即 server-reflexive)。音视频通话双方拿到各自的 srflx 地址后,就能尝试直接互连打洞,

避免流量绕行服务器中转。

只需要 STUN 的场景 :客户端之间 NAT 类型宽松、能打洞成功(大多数家庭/企业网络)。

只有 STUN 不够的场景:双方都是对称 NAT,打洞失败------此时才必须额外部署 TURN 做中转。

STUN 只负责"告诉你地址",不经手任何业务数据,负载极低。

STUN TURN
作用 告诉客户端其公网映射地址 服务器中继转发媒体流
带宽占用 极低(只处理信令) 高(所有媒体都过服务器)
何时需要 打洞可成功时 对称 NAT、打洞失败时兜底
端口 3478/udp 3478/udp + 一段中继端口

二、方案选型

自建还是用公共 STUN,是部署前要做的第一个取舍:

  • 公共 STUN (如 stun.l.google.com:19302)零成本、免维护,适合联调与临时验证;但地址不受你控制,可能被限速、变更或停止服务,不建议生产环境长期依赖。
  • 自建 STUN 的网络路径与延迟可控,不受第三方策略影响,并且 coturn 后续可以原地扩展为 TURN,不需要更换软件。

因此生产环境建议自建,下文以 coturn 为例。

方案 说明
coturn Debian/Ubuntu 官方仓库,支持 STUN(RFC 5389/5780),同时可平滑扩展为 TURN

coturn 本身也是"STUN + TURN"合一,同时具备两者的能力,该技术文章的重点是 STUN ,TURN暂不做详细介绍,若STUN无法满足您的需求,可以基于coturn尝试配置 TURN 无需换软件。


三、环境要求

项 要求
系统 Ubuntu(18.04 及以上)
权限 具备 sudo 权限的账号
网络 服务器需有一个客户端可达的 IP(局域网或公网均可)
端口 UDP 3478(STUN 标准端口);一般也同时放行 TCP 3478

关于网络与规格再补充两点:

  • STUN 服务器自身必须能被客户端直连。如果服务器本身位于 NAT 之后,它回给客户端的映射地址会失效;此时需要为 3478/udp 做端口映射,或直接使用带公网 IP 的云主机(注意确认已绑定弹性公网 IP)。
  • 资源开销很小。纯 STUN 只处理请求与应答,不转发媒体流,1 核 1G 的入门机型即可满足,带宽占用基本只与请求频率相关。

确认你已经在 Ubuntu 机器上(本地终端或已 SSH 登录均可)。下面所有命令都在 Ubuntu 上执行。

bash 复制代码
# 一眼确认系统版本
lsb_release -a

四、配置 STUN 服务

步骤1. 安装 coturn

安装包会一并提供 turnserver 主程序和 turnutils_* 调试工具;配置文件位于 /etc/turnserver.conf,服务开关位于 /etc/default/coturn,后续步骤会依次用到。

bash 复制代码
sudo apt update
sudo apt install -y coturn

验证安装:

bash 复制代码
dpkg -l | grep coturn            # 打印版本号即成功
dpkg -L coturn | grep turnserver # 确认可执行文件路径

若报 E: Unable to locate package coturn,说明源里没启用 universe:

bash 复制代码
sudo add-apt-repository universe && sudo apt update && sudo apt install -y coturn

步骤2. 打开服务启用开关

在 Debian/Ubuntu 上,coturn 包默认不自动运行 ,必须将 /etc/default/coturn 中的 TURNSERVER_ENABLED 设为 1 才能允许 systemd 正常启动该服务,否则即使执行 systemctl start coturn 也可能无效或被提示已禁用。

bash 复制代码
# 把 /etc/default/coturn 中控制 coturn 是否开机启动的选项设置为启用
sudo sed -i '/TURNSERVER_ENABLED/c\TURNSERVER_ENABLED=1' /etc/default/coturn
grep TURNSERVER_ENABLED /etc/default/coturn     # 应输出 TURNSERVER_ENABLED=1

若该文件里原本没有这一行,sed 不会新增,改用:

bash 复制代码
echo 'TURNSERVER_ENABLED=1' | sudo tee -a /etc/default/coturn

步骤3. 写入最小 STUN 配置

先备份,再写入一份"只为 STUN"的精简配置:

bash 复制代码
sudo cp -a /etc/turnserver.conf /etc/turnserver.conf.bak.$(date +%F)

sudo tee /etc/turnserver.conf > /dev/null <<'EOF'
# ===== 监听 =====
# STUN/TURN 标准端口(非特权端口,turnserver 用户即可绑定)
listening-port=3478

# ===== 仅作 STUN 服务的最小配置 =====
fingerprint          # 应答携带 STUN fingerprint(现代 ICE 客户端要求)
realm=stun.lab.local # 认证域,仅用 STUN 时留个占位即可

# 安全加固
no-multicast-peers   # 禁止向组播地址发包
no-cli               # 关闭本地管理端口 5766

# 日志:默认输出到系统日志,用 journalctl 查看,无需额外建目录
EOF

关于配置的几点说明:

  • 纯 STUN 不需要账号密码 。上表里没有 user= / lt-cred-mech,因为 STUN Binding 请求本身不鉴权。
    (只有当你想同时启用 TURN 时才需要加认证配置。)
  • realm 在纯 STUN 下不参与校验,填一个占位值即可,避免 coturn 打印警告。
  • 若想同时准备 TURN 中继,再补上这三行即可复用本服务:
bash 复制代码
# 配置 TURN 中继
lt-cred-mech
# user=bjxia:TurnServer@2026
user=用户名:强密码
min-port=49160
max-port=49200          # 配套防火墙需放行这段 UDP 端口

起服务前做一次语法自检:

bash 复制代码
# 以指定配置文件启动 turnserver 并校验凭据配置是否正确
sudo turnserver -c /etc/turnserver.conf --check-credentials 2>&1 | head -20

步骤4. 放行防火墙

Ubuntu 默认用 ufw。STUN 只需要 3478:

bash 复制代码
# 先放行 SSH,避免启用防火墙时把自己断连
sudo ufw allow 22/tcp

sudo ufw allow 3478/udp     # STUN 走 UDP,必须放行
sudo ufw allow 3478/tcp     # 兼容走 TCP 的客户端,建议一并放行

sudo ufw --force enable     # 若已启用可跳过
sudo ufw status verbose     # 核对

云主机注意 :云平台的安全组 / 网络 ACL 是独立于 ufw 的一层。必须在控制台同样放行 UDP/TCP 3478,否则常见现象是"ss 显示端口在监听、本机自测正常,外部客户端却始终超时"。
红线 :ufw enable 之前务必先 allow 22/tcp(或你的实际 SSH 端口),

否则启用瞬间远程连接会被切断。

若服务器用的是 iptables 而非 ufw:

bash 复制代码
# 在防火墙中放行 TURN 服务默认的 3478 端口(TCP + UDP),确保外部客户端可以正常连接并进行 NAT 穿透
sudo iptables -I INPUT -p udp --dport 3478 -j ACCEPT
sudo iptables -I INPUT -p tcp --dport 3478 -j ACCEPT

步骤5. 启动并设置开机自启

bash 复制代码
# 重新加载 systemd 配置
sudo systemctl daemon-reload
# 开机自启 + 立即启动
sudo systemctl enable --now coturn
# 查看运行状态
sudo systemctl status coturn --no-pager

enable --now 相当于"设置开机自启"加"立即启动"两条命令。看到 active (running) 即成功;若状态为 failed,用 sudo journalctl -u coturn -n 50 --no-pager 查看报错,常见原因是配置文件语法错误或 3478 端口被占用。

确认端口已在监听:

bash 复制代码
sudo ss -lunp | grep 3478     # UDP
sudo ss -ltnp | grep 3478     # TCP

应能看到 turnserver 进程监听 0.0.0.0:3478。


五、验证 STUN 是否工作

分三层验证,任一层通过即可确认 STUN 正常。

5.1 服务器本机自测

bash 复制代码
turnutils_stunclient -p 3478 127.0.0.1
turnutils_stunclient -p 3478 <服务器IP>

输出中会给出映射地址(Mapped address),说明 STUN 正常应答。第一条走回环地址,只证明进程在正常应答;第二条走服务器 IP,才能同时验证监听地址和防火墙放行。

5.2 从另一台机器验证(跨机可达性)

在客户端机器(macOS/Linux 均可,自带 python3)执行:

bash 复制代码
python3 - 192.168.0.116 <<'PY'
import socket, struct, os, sys
host, port = sys.argv[1], 3478

def parse(data):
    _, mlen, _, _ = struct.unpack('>HHI12s', data[:20])
    body, i = data[20:20+mlen], 0
    while i + 4 <= len(body):
        at, al = struct.unpack('>HH', body[i:i+4])
        yield at, body[i+4:i+4+al]
        i += 4 + al + ((4 - al % 4) % 4)

tid = os.urandom(12)
req = struct.pack('>HHI12s', 0x0001, 0, 0x2112A442, tid)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(3)
try:
    s.sendto(req, (host, port)); data, addr = s.recvfrom(2048)
except socket.timeout:
    print("❌ 超时:到服务器的 3478/udp 不通(防火墙?不在同网段?)"); sys.exit(1)
print(f"✅ 收到来自 {addr} 的响应,{len(data)} 字节")
for at, av in parse(data):
    if at == 0x0020:
        xport = struct.unpack('>H', av[2:4])[0] ^ 0x2112
        xip = bytes(b ^ m for b, m in zip(av[4:8], struct.pack('>I', 0x2112A442)))
        print("   服务器看到的你的映射地址:", socket.inet_ntoa(xip), ":", xport)
PY

打印 ✅ ... 映射地址 ... 即 STUN 链路正常。

5.3 浏览器端验证(最接近真实业务)

打开 WebRTC Trickle ICE 测试页,在 "STUN or TURN URI" 中填入 stun:<服务器IP>:3478,点击 Add Server 后再点击 Gather candidates 。候选列表中出现 srflx 类型(其地址就是服务器看到的公网映射)时,说明浏览器侧的 STUN 配置已生效;只有 host 候选则说明请求未得到应答。


六、客户端配置STUN

部署并验证完成后,剩下的工作是把 STUN 地址填进客户端的 ICE 配置。STUN 只参与地址发现,不改变业务逻辑:浏览器和原生客户端都只是把这个地址交给各自的 WebRTC 协议栈,由协议栈完成候选收集与连通性检查。

6.1 客户端如何使用 STUN

部署完成后,把地址交给音视频应用(信令服务器 / WebRTC 客户端):

javascript 复制代码
const iceServers = [
  { urls: "stun:<服务器IP>:3478" }   // 就是这么一行
];
const pc = new RTCPeerConnection({ iceServers });

自检命令汇总:

bash 复制代码
# 查看状态与端口
systemctl status coturn --no-pager
ss -lunp | grep 3478
# 本机验证
turnutils_stunclient -p 3478 127.0.0.1

6.2 C++客户端替换 STUN 地址

libdatachannel 通过 rtc::IceServer 列表接收 ICE 服务器,URL 采用 <scheme>:<host>:<port> 格式,stun、turn、turns 三种 scheme 通用。只需要 NAT 穿透时,把原有的公共 STUN 地址换成自建地址即可:

cpp 复制代码
// 改前
cfg.iceServers.emplace_back(rtc::IceServer{"stun:stun.freeswitch.org:3478"});
// 改后
cfg.iceServers.emplace_back(rtc::IceServer{"stun:<服务器IP>:3478"});

改完后运行客户端,确认日志中收集到了 srflx 候选;若只有 host 候选,说明 STUN 请求没有拿到应答,按第五章逐层排查。

若 STUN 不满足要求、需要 TURN 兜底时,可按如下代码配置:

cpp 复制代码
cfg.iceServers.emplace_back(rtc::IceServer{"turn:<服务器IP>:3478", "用户名", "口令"});

TURN 要求服务器端启用 lt-cred-mech 并配置 user=(见步骤 3 的可选配置),客户端填写的用户名与口令必须与之一致,否则会因认证失败而无法分配中继。


参考

相关推荐
高旭的旭2 小时前
图解 Android 系统架构:从 App、Framework、HAL 到 Linux 驱动
android·linux·系统架构
秦jh_2 小时前
【Docker】镜像、卷原理
运维·docker·容器
物联网软硬件开发-轨物科技2 小时前
【轨物方案】深度解读开关柜智能感知及一键顺控专家系统
大数据·运维·网络·人工智能·算法·机器人
小小、码农2 小时前
[特殊字符]【C++11】类的新功能全面解析:移动构造、移动赋值、default、delete、final 与 override
linux·开发语言·网络·c++
陈让然2 小时前
Ubuntu Podman 容器镜像构建
linux·ubuntu
竣达技术3 小时前
Modbus 转 SNMP 协议转换器|自定义协议 + SNMP TRAP 主动上报
运维·服务器·网络·数据库·安全
优化Henry3 小时前
LTE小区光路降质告警排查与处理实录
运维·网络·学习·5g·tdd
探索云原生3 小时前
Ascend vNPU 实战:从原生 Docker 到 Ascend Docker Runtime
linux·ai·云原生·kubernetes·gpu
无足鸟ICT3 小时前
【RHCA+】_etc_yum.conf
linux·运维