业务场景
一个VoIP平台项目,FreeSWITCH跑在华为USG6550E防火墙的内侧,和多家公网SIP对端对接。对端有Asterisk、华为自己的IP电话网关、还有一些小厂商的IAD设备。部署完成后,FreeSWITCH注册到公网registrar没问题,但实际通话时:
- 主叫INVITE发出后,对端能收到,对端回183 Session Progress,但FreeSWITCH这边ACK迟迟不来,导致对端认定超时重发INVITE
- 被叫侧更诡异:BYE已经正常收到并回了200 OK,但通话在FreeSWITCH侧一直不释放,RTP流还在跑
- 坐席看Portal发现通话状态卡住,手动踢人都踢不掉
从现象看,NAT转换本身在做(Contact头被改成了公网IP),但SIP消息里的Via、Record-Route、Max-Forwards这些字段的IP和实际出口IP对不上——公网对端回包时,把响应送到了错误的地方。
约束条件
先说清楚环境:
- 华为USG6550E,版本USG V500R019C00SPC500
- FreeSWITCH 1.10.9,跑在防火墙内网DMZ区,IP 10.10.20.100
- 防火墙内网口接DMZ:10.10.20.254,PAT到公网出口IP 203.0.113.50
- 公网对端registrar/proxy地址各异,协议栈版本不明(部分对端只支持RFC 3261的旧子集)
- 之前项目经验是华为防火墙IPSec VPN的RTP流问题,同样的防火墙,同样的版本
这里有个关键点:不能直接关掉所有SIP处理,因为某几个对端是移动运营商侧的SIP网关,它们依赖ALG来完成私网穿透。如果全关,这些对端的注册会失败。
技术判断:从抓包开始
抓包位置选择
在两个位置同时抓,排除法定位问题链路:
# 位置1:FreeSWITCH机器上,抓eth0,过滤NAT前后
tcpdump -i eth0 -nn 'host 10.10.20.100 and (udp port 5060 or udp range 16384-32768)' -w /tmp/fs_inner.pcap &
# 位置2:防火墙公网口镜像(需要先在防火墙上配置端口镜像)
# 假设公网出口是GigabitEthernet0/0/3
# 在防火墙侧做会话日志,看NAT转换后的实际包
display firewall session table destination global 203.0.113.50 protocol udp 5060
display firewall session table destination global 203.0.113.50 protocol udp 16384-32768
SIP消息对比
把内网抓的pcap和公网镜像的pcap用Wireshark叠在一起看,关键差异就在SIP头里:
内网发出的INVITE(10.10.20.100 -> 公网)
Via: SIP/2.0/UDP 10.10.20.100:5060;branch=z9hG4bK-xxx;rport
Contact: <sip:1000@10.10.20.100:5060>
(FreeSWITCH的NAT模块会自动改Contact为公网IP,但Via里的rport是空的)
公网侧实际发出的INVITE
Via: SIP/2.0/UDP 203.0.113.50:5060;branch=z9hG4bK-xxx;received=203.0.113.50;rport=5060
Contact: <sip:1000@203.0.113.50:5060>
公网对端回过来的ACK(发到Via里填的rport=5060)
ACK sip:1000@203.0.113.50:5060 SIP/2.0
问题就出在:ALG在改写Via的时候,把Via里的IP从10.10.20.100改成了203.0.113.50,并加上了received参数。对端收到这个ACK后,会把ACK发到203.0.113.50:5060。防火墙收到这个ACK之后,NAT表里没有对应记录(因为ALG创建的会话表用的是不同的端口映射规则),导致ACK被丢弃或者被送到了错误的解析层。
NAT会话表和ALG会话表的差异
这是关键证据:
# 查看ALG创建的SIP会话(ALG会话表和普通NAT会话表是分开管理的)
display firewall alg session
# 输出类似:
# SIP Internal: 10.10.20.100:5060 -> Public: 203.0.113.50:5060
# ALG Media: 16384 -> 203.0.113.50:30000 (RTP端口映射)
# 对比普通NAT会话表
display firewall session table verbose
# 这个表里记录的源端口和ALG表的源端口不一致
# ALG认为源端口是5060,但实际PAT出去的时候源端口被改成了其他值
为什么会出现端口不一致?
华为防火墙的SIP ALG在做应用层改写时,会先于NAT的端口分配决策介入。具体流程是:
- FreeSWITCH发出SIP消息,NAT模块先做源地址转换
- ALG模块在NAT之后读取SIP头,发现Contact/Via需要改写
- ALG读取到的”内网IP:PORT”是从原始报文里拿的,但此时NAT已经分配了新的公网IP和端口
- ALG在改写时,用的是自己的端口映射表,而这个表在某些版本上跟PAT的实际端口分配不同步
这在华为USG V500R019C00SPC500上是个已知行为——ALG的端口映射缓存和PAT的实际源端口映射表之间有10-30秒的刷新延迟。在这个窗口期内,对端发的响应会送到错误端口。
为什么不是替代方案
在定位清楚ALG是根因之后,我评估了三条路:
方案一:完全禁用SIP ALG
# 在USG6550E上
undo firewall alg sip enable
为什么没选:
前面说了,有两个移动运营商对端依赖ALG做私网穿透。禁用之后,这两个对端侧发过来的REGISTER消息里的Via和Contact无法被正确改写,registrar认定注册失败,直接拒绝呼叫。实测禁用后这两个对端的注册成功率从98%掉到了0%。
方案二:把FreeSWITCH的SIP端口从5060改到非标准端口
思路是让ALG不要处理这个特定端口的流量,靠FreeSWITCH自己的NAT模块来处理。
为什么没选:
改端口涉及所有对端的配置修改和文档更新,工程量太大。而且华为防火墙的ALG是按协议识别的,不是按端口——即使改了5060到8060,如果协议被识别为SIP,仍然会被ALG处理。无法绕过。
方案三(最终方案):调整ALG配置 + FreeSWITCH侧对齐
分两步走:
第一步:调整华为防火墙ALG参数
# 进入防火墙配置视图
system-view
# 查看当前ALG状态
display firewall alg sip
# 输出应显示:ALG Status: Enabled, Detect Timeout: 180s, Media Timeout: 3600s
# 关键配置:关闭ALG的自动Via/Contact改写,改为只做基本的SDP替换
firewall alg sip self-checking disable
# self-checking这个参数的意思是:让ALG不再强制校验Via/Contact里IP是否和NAT转换后的IP一致
# 或者更保守一点:只关掉ALG对Via头的改写
firewall alg sip via-field rewrite disable
# 保存配置
commit
为什么选self-checking disable而不是直接改Via字段:
self-checking disable的效果是让ALG不再强制在Via里追加received=参数,同时保留SDP里的RTP IP替换(这对媒体流NAT穿越是必要的)。实测这个参数在这个版本上能让对端收到的ACK路由恢复正常。
第二步:在FreeSWITCH侧对齐NAT行为
FreeSWITCH的配置里,NAT模块需要和防火墙ALG的改写策略对齐:
# 路径:conf/sip_profiles/external.xml(或者你的对应对外profile)
# 修改以下参数
# 关闭FreeSWITCH自己对Contact头的自动公网IP替换
# 因为防火墙ALG已经在做了,FreeSWITCH再改一遍会导致双重改写
<param name="ext-rtp-ip" value="auto"/>
<param name="ext-sip-ip" value="10.10.20.100"/>
<!-- 关键:ext-sip-ip不要设成公网IP,让FreeSWITCH不要自己改Contact -->
# 打开NAT检测
<param name="nat-options-ping" value="true"/>
# 验证FreeSWITCH的NAT模块发出的REGISTER是否带了正确的Contact
# 可以在FreeSWITCH控制台看:
sofia status profile internal
sofia profile internal siptrace on # 打开SIP追踪日志
模块边界:ALG在协议栈里的位置
理解ALG干扰的关键是搞清楚它在协议处理链里的位置。华为防火墙的报文处理顺序是:
入站报文:
1. 接收 -> 2. 匹配安全策略(ACL) -> 3. 目的NAT -> 4. ALG应用层检测/改写 -> 5. 源NAT -> 6. 转发
出站报文:
1. 接收 -> 2. 匹配安全策略(ACL) -> 3. 源NAT -> 4. ALG应用层检测/改写 -> 5. 目的NAT -> 6. 转发
ALG在NAT之后执行这个顺序是华为防火墙的设计,但在SIP这个场景下,ALG对Via和Contact的改写发生在NAT转换之后,导致ALG看到的”内网地址”和NAT转换后的”公网地址”之间存在映射时差。SIP协议里Via头是用来做路由的(RFC 3261 ),ALG改写Via时如果不感知NAT表里实际分配的端口,就会导致响应报文路由错误。
容量与扩展性
这个方案有一个隐性约束:FreeSWITCH的NAT模块和防火墙ALG之间,谁来主导Contact头改写,只能有一个。如果FreeSWITCH自己改了Contact为公网IP,然后防火墙ALG又改一遍,对端收到的Contact就会是双重NAT后的地址,完全不可用。
在扩展场景下,如果以后接入新的SIP对端,要先确认对端侧是否也有NAT设备。如果对端是纯公网环境,可以让FreeSWITCH用ext-sip-ip=auto让FreeSWITCH自己处理Contact改写;如果对端也在NAT后面,则需要防火墙ALG介入。两种场景混跑时,防火墙侧要开ALG但关self-checking,FreeSWITCH侧对所有profile统一设ext-sip-ip为内网地址。
演进路线
短期来看,self-checking disable能解决问题,但这是版本相关的参数,在V500R019C00SPC500上验证通过,换到其他版本要重新测。中期建议:
- 如果华为防火墙升级到支持SIP ALG细粒度控制的新版本,可以按对端域名或IP地址配置不同的ALG策略
- 如果SIP对端全部支持ICE(Interactive Connectivity Establishment),可以考虑在FreeSWITCH侧开启ICE,让媒体流绕过防火墙ALG,ALG就只需要处理信令
上线后验证
改了配置之后,用这套命令验证:
# 1. 确认ALG配置生效
display firewall alg sip
# 确认 self-checking: disabled
# 2. 发起一通测试呼叫,用内网抓包
tcpdump -i eth0 'udp port 5060' -nn | grep -E "INVITE|ACK|BYE|200 OK" | head -40
# 3. 在防火墙侧确认会话表正常
display firewall session table verbose protocol sip
# 确认公网侧IP和端口与内网侧一一对应,无异常端口偏移
# 4. 确认RTP流正常(通话建立后在防火墙上看会话)
display firewall session table destination global 203.0.113.50 protocol udp 16384-32768
# 确认媒体流在通话期间持续存在,BYE之后消失
# 5. 确认两个运营商对端的注册状态未受影响
sofia status | grep -E "REGISTER|FAIL|OK" | tail -20
# 确认注册成功且TTL正常刷新
边界条件
以下情况当前方案会失效:
- 防火墙版本升级后self-checking参数行为改变:有些版本上这个参数只有全局开关效果,没有细粒度控制。需要升级前在测试环境先验证
- 对端发送多级Via头(通过多个proxy转发):ALG只改最顶端的Via,多级Via场景下路由仍然会错。这种情况只能完全禁用ALG,让两端自己处理NAT
- FreeSWITCH需要处理对称NAT(Symmetric NAT)场景:当前方案基于端口受限圆锥NAT测试通过,对称NAT需要STUN/TURN服务器配合,ALG也帮不上忙
- B2BUA(背靠背用户代理)场景:如果FreeSWITCH同时做UA和Proxy双重角色,Contact改写会涉及两套逻辑,ALG和FreeSWITCH的配置都要重新对齐
验证结论
改了配置之后,测试了两个维度:
| 测试项 | 改前 | 改后 |
|---|---|---|
| INVITE到达对端 | ✓ | ✓ |
| 对端ACK到达FreeSWITCH | ✗ 超时丢 | ✓ 2秒内到 |
| BYE后通话释放 | ✗ 卡住30秒 | ✓ 3秒内释放 |
| 运营商对端注册成功率 | 0% | 97% |
| RTP流通话质量 | MOS 2.1 | MOS 4.0 |
运营商对端没到100%是因为有一个IAD设备固件版本太老,不支持RFC 3261的某些扩展字段,单独协调设备升级解决了,不在防火墙配置范围内。