Kamailio解决SIP NAT穿透问题:原理、配置与实战排查

发布时间:2026/9/11 15:53:47
Kamailio解决SIP NAT穿透问题:原理、配置与实战排查 做VoIP的同行应该都有这种经历内网里的SIP话机配置好了注册也显示成功但打电话就是只有一边能听到声音或者干脆注册完一会儿就掉线。问题大多出在NAT上。如果你用的是Kamailio前身OpenSER当SIP服务器那么从server端解决SIP NAT问题其实是有成熟套路的。这篇文章就把我在生产环境里跑过的方案完整捋一遍从原理、模块选型到配置落地、故障排查一次性说清楚。适合正在搞企业VoIP、呼叫中心或者自己搭SIP服务器的小伙伴参考。1. 明确问题根源SIP为什么在NAT面前这么“娇气”1.1 SIP的双通道设计和内嵌地址问题SIP和其他协议不一样的地方在于它是双通道协议。信令走的是SIP自身的报文媒体走的是RTP报文两者互相独立。信令里面通过SDPSession Description Protocol来约定媒体要往哪里发也就是说SDP里直接嵌着IP和端口。这个设计在纯公网环境里没什么问题可一旦牵涉到NAT麻烦就来了。NAT设备会改写IP报文的源地址和端口但不会去改写SIP负载里SDP的IP和端口。结果就是一个私网里的UA它发出的INVITE到达对端时对端读到的SDP里写着“192.168.1.100:7078”可这个地址在公网根本不可达RTP包自然发不过去。这就造成了最典型的“单通”或者干脆“两边都听不到声音”。还有一个坑在Contact头域。UA注册到Kamailio时REGISTER报文的Contact头里写的是它自己的本机地址。如果Kamailio直接把这个地址入库后续其他用户呼叫这个UA时Kamailio就会把INVITE发往一个私网地址请求自然到不了对方。很多人查呼叫失败只盯着SDP看结果真正的坑在Contact头。这个头域里的地址必须根据请求的实际来源IP和端口做重写否则注册表中的位置信息就是一张废纸。1.2 现实中常见的三种NAT场景我在实际项目里遇到的部署场景基本逃不出下面三种。第一种是办公室网络里的话机或者软电话通过公司出口路由NAT到公网注册到云端或者外地机房的Kamailio服务器。这种最普遍话机藏在NAT后面服务器完全看不见客户端真实地址。第二种是家庭办公场景光猫加无线路由器SIP话机藏在双层NAT后面这比单层NAT更让人头疼因为即使你修好了一层NAT另一层还会继续干扰。第三种是分支机构的语音网关网关本身支持NAT穿透问题反而少一些只需要服务器端做常规的检测和转发。处理思路也不一样对于话机和软电话需要在服务器端做完整的NAT检测、Contact修复、RTP代理。对于网关很多时候网关自己就会处理SIP的NAT穿透服务器端只需要做常规的检测和转发。但无论如何作为Kamailio这一侧我们不可能控制客户端所以一切以server端为准。把服务器配置成“不管客户端什么状态都能正确处理”这才是解决问题的核心态度。2. 动手配置前先学会让Kamailio识别NAT2.1 nat_uac_test到底在测什么Kamailio里最核心的NAT检测函数是nat_uac_test(flags)它属于nathelper模块。这个函数通过检查SIP报文的Via、Contact、Route这些头域来判断这个请求是不是来自NAT后面。flags的值是可以相加的位掩码理解了每个位代表什么你就能根据自己的场景组合出合适的判断条件。flags值检测内容1Via头域中是否包含RFC1918私网地址2Contact头域中是否包含RFC1918私网地址4Via头域中是否缺少rport参数8Via头域中是否缺少received参数16Route头域中是否包含RFC1918私网地址实际使用中最常见的组合是“3”和“19”。“3”等于1加2只检测Via和Contact头有没有私网地址“19”等于1加2加16把Route头也加进来了。为什么我推荐用19而不是3因为有些UA尤其是老款硬话机既不在Via里带内网IP也不在Contact里带内网IP但Route字段能露出马脚。比如某些话机在注册时会在Route头写上自己的私网地址这就成了判断NAT的直接证据。再多说一句检测到NAT后要立即处理不能拖到路由逻辑的后半段再做因为中间可能被别的模块改过报文原始信息就丢了。2.2 识别之后的第一轮修补检测到NAT之后最基本的三件事是force_rport、fix_nated_contact、fix_nated_register。force_rport的作用是强制要求下游响应沿着请求进来的路径返回它会在Via头中补充rport参数Kamailio后续在构造响应时会用这个信息作为目的地址。fix_nated_contact会根据请求的实际来源IP和端口重写Contact头域中的IP和端口。fix_nated_register则是专门针对REGISTER请求的加强版它除了修Contact头还会处理Path等细节确保后续请求能够正确到达那个NAT后面的UA。这里要说一个关键逻辑fix_nated_contact只是改了内存里Contact头的文本真正要把修改后的地址存入位置表必须依赖registrar模块的save()操作。所以顺序一定是先修复再save。如果顺序反了存进去的还是旧的内网地址。另外force_rport只是保证响应能回来它不解决后续新会话的INVITE怎么发出去的问题所以一定要配合fix_nated_contact一起用两者缺一不可。注意很多老教程里会提到fix_nated_sdp()这个函数它可以直接修改SDP里的IP。但在有rtpengine的情况下我不建议再用它因为SDP里的RTP地址应该统一交给rtpengine去替换而不是由Kamailio手写一个NAT后的IP。两套逻辑混用容易把SDP改坏。3. 信令号令与媒体转发分工3.1 信令侧NAT修复的核心操作这里先给总体的思路信令一定要让后续的请求能到达NAT后的UA媒体一定要通过RTP代理中转。信令侧的工作可以拆成四步检测、添加rport和received、修复Contact、保存位置时标记NAT。检测我们已经说过了用nat_uac_test(19)。添加rport和received其实force_rport()和fix_nated_contact()函数内部会一起处理。修复Contact就是那个fix函数。至于保存位置时标记NAT这就得靠usrloc模块的nat_bflag参数了。在配置里你可以定义一个自定义标志位比如FLB_NAT然后在处理REGISTER时如果检测到NAT就调用setbflag(FLB_NAT)打上标记。这样后续用kamcmd ul.dump查看位置表时能清清楚楚看到哪些UA是待在有问题的NAT后面的。还有一个进阶操作值得关注add_contact_alias()和handle_uri_alias()。这一对函数在复杂网络里特别有用比如服务器有多个网口、或者需要经过多跳代理。add_contact_alias会把实际来源IP、端口和传输协议以alias参数的形式附加到Contact头中。之后当服务器要向这个UA发送请求时handle_uri_alias会把带有alias的URI重新翻译回原来的地址和端口。这个机制比单纯依赖received字段更灵活因为它是在URI层面做映射跨模块都能生效。如果你在部署Kamailio集群或者有多网卡环境建议把它用上。3.2 媒体侧怎么用rtpengine接管RTP媒体穿越的关键就是RTP代理。业界主流就两套方案rtpproxy和rtpengine。我强烈建议你用rtpengine功能和性能都比rtpproxy好而且在持续维护。rtpengine的工作逻辑是Kamailio在SDP经过时把里面的私网IP和端口替换成rtpengine自己的公网IP和端口然后rtpengine根据映射信息把后续收到的RTP包原样转发给真正的UA。这样一来两个NAT后面的UA之间建的RTP连接中间人变成了rtpengine媒体路径上没有不可达的私网地址。核心调用函数是rtpengine_manage()它会自动识别当前SDP是offer还是answer并且分别做相应处理。生产环境里我建议这样调if (has_body(application/sdp)) { rtpengine_manage(RTP/AVP ICEremove); }参数“RTP/AVP”指定媒体套件ICEremove表示把SDP里的ICE候选去掉因为我们在用rtpengine做媒体中转时不需要终端之间再走ICE探测了。去掉ICE可以避免一些终端在ICE失败后纠结切换媒体路径的问题让RTP直接走rtpengine干净利落。注意rtpengine_manage()的调用时机很关键。INVITE请求路由中调用一次处理offer被叫方的响应到达Kamailio时必须在onreply_route中再调用一次处理answer。只处理一头媒体就会出问题。3.3 keepalive机制保住NAT映射NAT后面的UAUDP映射是有寿命的。多数家用路由器的UDP映射在30秒到2分钟没有任何流量后就会静默失效。如果UA注册完之后就不说话了映射一过期服务器再给它发消息就发不到了。解决方案有两个方向一是UA自己定时发OPTIONS或keepalive包二是服务器端定时ping。Kamailio的nathelper模块提供了natping功能配置方式很直接modparam(nathelper, natping_interval, 30) modparam(nathelper, ping_nated_only, 1) modparam(nathelper, natping_processes, 1)natping_interval是发送间隔的秒数一般建议30秒比大多数路由器UDP映射超时短一个数量级稳妥。ping_nated_only设置为1表示只对打了NAT标志的contact发送keepalive避免浪费冤枉包。natping_processes控制用几个进程来发包默认1就够。服务器发送的keepalive报文通常是SIP OPTIONS很多UA收到后会回复200这样NAT映射就被续命了。不过有些老话机对OPTIONS响应不积极或者干脆不响应这时候映射依然会断。遇到这种设备就得把客户端的掉线检测时间调短让UA自己多说话。很多话机设置里都有“注册有效期”和“keepalive间隔”把这两个值设短一点比如注册周期120秒、保活间隔30秒可以最大程度避免NAT映射过期。4. 一份可直接落地的Kamailio NAT配置4.1 环境与模块准备这里给出一份基于Debian/Ubuntu Kamailio 5.6的部署配置。假设环境是服务器有公网IPSIP信令走UDP 5060RTP媒体端口范围是10000到20000rtpengine和Kamailio装在同一台服务器上。如果你的服务器在云上还需要在安全组里放行UDP 5060和UDP 10000到20000端口这一步经常被遗漏导致信令通但媒体不通。安装包很简单apt update apt install kamailio kamailio-extra-modules kamailio-json-modules rtpenginekamailio-extra-modules里面包含了nathelper、rtpengine这些常用模块一定要装。装完之后先确认Kamailio能正常启动再逐步调试配置别一上来就把生产流量切过去。我习惯先在测试环境把整个流程跑通再把配置文件同步到生产这样能把影响范围控制在最小。4.2 核心路由逻辑配置下面这份配置把NAT处理逻辑独立放在最前面任何请求进来先过NAT检查和修复再走后续的认证、路由。模块参数这里只保留和NAT相关的核心部分方便你对照理解#!KAMAILIO # ----------- 全局变量 ---------- #!define FLB_NAT 1 # ----------- 模块参数 ---------- modparam(usrloc, db_mode, 0) modparam(usrloc, nat_bflag, FLB_NAT) modparam(registrar, received_avp, $avp(received)) modparam(registrar, use_path, 1) modparam(registrar, path_mode, 1) modparam(nathelper, received_avp, $avp(received)) modparam(nathelper, natping_interval, 30) modparam(nathelper, ping_nated_only, 1) modparam(rtpengine, rtpengine_sock, udp:127.0.0.1:2223) # ----------- 请求路由 ---------- request_route { # 第一步NAT预检测和修复 if (nat_uac_test(19)) { force_rport(); if (is_method(REGISTER)) { fix_nated_register(); } else { fix_nated_contact(); } } # 第二步带SDP的请求统一过rtpengine if (has_body(application/sdp)) { rtpengine_manage(RTP/AVP ICEremove); } # REGISTER处理 if (is_method(REGISTER)) { if (nat_uac_test(19)) { setbflag(FLB_NAT); } save(location); exit; } # INVITE处理 if (is_method(INVITE)) { t_on_reply(NAT_REPLY); if (!t_relay()) { sl_reply_error(); } exit; } # 其余请求透传 if (!t_relay()) { sl_reply_error(); } } # ----------- 响应路由 ---------- onreply_route[NAT_REPLY] { if (has_body(application/sdp)) { rtpengine_manage(RTP/AVP ICEremove); } }需要特别提醒几个细节。第一setbflag(FLB_NAT)必须放在save(location)之前执行因为usrloc保存时会读取这个标志位决定是否把该contact标记为NAT类型。第二onreply_route中处理SDP时必须再次调用rtpengine_manage处理被叫方答应的SDP。第三这份配置没有包含认证和出站路由生产环境需要在REGISTER前加www_authenticate在INVITE前做授权检查但NAT处理逻辑本身是通用的主要看我们怎么组织路由顺序。4.3 rtpengine安装对接rtpengine安装好后默认监听UDP 2223端口但这个默认值不是绝对的需要跟Kamailio里的rtpengine_sock参数保持一致。rtpengine的配置文件通常在/etc/rtpengine/rtpengine.conf重点设置这几项interface 203.0.113.10 listen-ng 127.0.0.1:2223 port-min 10000 port-max 20000interface必须写服务器真正的公网IP这是rtpengine用于通告给终端的媒体地址写错了终端之间RTP就建不起来。listen-ng是控制接口Kamailio通过这里跟rtpengine通信。port-min和port-max是RTP端口范围需要跟前面安全组放开的范围一致。改完配置重启rtpenginesystemctl restart rtpengine检查运行状态最重要的一步是用rtpengine-ctl查看当前会话状态rtpengine-ctl all这个命令会显示所有活动的call的媒体信息包括源IP、目的IP、端口映射等排障时非常有用。如果执行报错多半是rtpengine没启动起来或者配置文件里的参数有误。4.4 常见客户端与服务器的配合设置再往下走就是客户端配合的问题。软电话比如MicroSIP、Zoiper、Linphone通常会自动处理NAT穿透只要注册地址填对服务器端修复好了一般就能通。但硬话机比如Yealink、Fanvil、Cisco就不一定了它们大多需要在网络设置里开启NAT穿透相关选项。硬话机里常见的选项包括NAT类型有/无/STUN、STUN服务器地址、NAT keepalive间隔。如果话机附近能提供STUN服务建议直接把STUN服务器配上。但话机即使开了STUN也只能解决它自己感知到公网地址的问题服务器端的NAT修复和rtpengine媒体中转逻辑不能省。我在一个项目里遇到的情况就是这样话机开了STUN后注册正常了但呼叫时SDP里偶尔还是会带出内网地址最终还是靠rtpengine把媒体拉回来。实际测试时我推荐用MicroSIP做软电话验证它的SIP日志非常详细能直接看到发送和接收的报文。用软电话先验证整个链路通不通再用硬话机接入排障效率会高很多。5. 现场排查实录解决NAT问题的定位思路5.1 单向通话怎么定位单向通话是NAT问题最常见的临床表现。虽然我们已经在服务器端做了完整的NAT处理但生产环境总会有意外比如某个SDP没有过rtpengine、防火墙端口没放行、或者终端自己改了媒体地址。定位步骤我习惯从信令入手。tcpdump -i eth0 -n -s 0 -w /tmp/call.pcap udp port 5060 or udp portrange 10000-20000抓完包之后重点看INVITE和200 OK里的SDP内容。如果某一方的SDP地址是私网地址那就说明rtpengine没有正确处理这一侧的媒体。如果两边的SDP地址都是公网地址但还是单通那就要看RTP端口能不能通。大多数云服务器默认安全组只放行了5060RTP端口范围没放行的话媒体包就进不来。单通还有一种隐蔽场景SDP里的地址是对的但RTP的源端口和SDP写的不一致。某些NAT设备或者Linux内核的高端口策略会改掉本地端口导致对端无法匹配RTP会话。这种情况在抓包里表现为SDP写了7078但实际发的RTP源端口是50000多。遇到这种还是得靠rtpengine中转并且确保中转后的端口固定。5.2 注册掉线怎么定位“注册成功过一会儿就掉”这个现象常见原因就两个NAT映射超时或者客户端没有按时发keepalive。排查时先看Kamailio日志里有没有周期性REGISTER刷新进来。如果客户端一直在发REGISTER但位置表里的记录还是过期那就要检查保存位置时有没有被标记成NAT以及natping有没有正常工作。kamcmd ul.dump这个命令会打印整个用户位置表。重点看两个字段flags里有没有NAT标记以及expires字段。如果flags里没有NAT标记说明保存位置时没执行setbflag那后续的natping自然也不会给这个UA发keepalive。还有一种情况是UA在NAT后面但它的NAT映射很稳定比如企业出口防火墙的UDP映射超时是5分钟而UA每30秒就发一个保活包这种情况基本不会掉线。如果UA不支持自己发保活包就靠natping两边一个都不做掉线是必然的。5.3 常用的调试命令和日志排查NAT问题时我最常用的一套命令组合是kamcmd ul.dump查看用户位置表确认regsitrar里存的contact是否正确、有没有NAT标记kamcmd rtpengine.show all查看rtpengine当前所有媒体会话的状态kamcmd nathelper.stats查看nathelper模块的统计信息比如natping发了多少、回了多少kamcmd tm.list查看当前事务的状态确认INVITE有没有处于挂起状态ngrep -d any port 5060实时抓SIP信令比tcpdump更直接直接过滤出SIP报文日志方面Kamailio默认日志路径可能在/var/log/kamailio.log如果找不到就用journalctl -u kamailio查看。排障时可以把日志级别调到debug然后重现问题。调试完一定记得改回去否则生产环境日志量会爆炸。我在一次排障时忘了改结果一晚上日志文件涨了几个GB直接把磁盘写满了这点一定要当心。5.4 NAT问题避坑速查表现象可能原因处理方向注册成功但来电无响应Contact头未修复后续INVITE发到私网地址确认fix_nated_contact/register已调用单向通话主叫听到被叫反向没声音被叫侧SDP没过rtpengine或防火墙RTP端口未放行抓包确认两侧SDP检查onreply_route逻辑注册经常掉线NAT映射超时natping未生效开启natping检查NAT标记是否设置媒体延迟抖动严重RTP路径绕路或rtpengine性能不足检查rtpengine配置确认用的是UDP还是内核模块通话建立后几秒断开RE-INVITE触发了新的NAT问题检查re-INVITE流程中rtpengine_manage是否被调用一接电话只有单侧声音且很快挂断SIP ALG干扰在出口路由器上关闭SIP ALG功能这个表里最后一行特别值得展开。很多家用路由器默认开启了SIP ALG它试图“帮”你修改SIP报文里的地址信息结果经常帮倒忙把服务器端正确修复过的地址又改回错误的了。遇到莫名其妙的单通、断话或者注册时好时坏第一件事就是去路由器里把SIP ALG关掉。这个操作成本最低收益却很大。我个人在实际操作中的体会是NAT问题不是一次配置就万事大吉的随着网络环境变化比如换路由器、运营商启用CGNAT、公司出口策略调整问题都可能重新冒出来。建议在服务器上把日志和rtpengine的监控做好遇到问题能快速定位。如果条件允许客户端开启STUN能明显降低服务器端压力但服务器端的NAT修复和rtpengine媒体中转逻辑是保底方案一定不能省。最后再分享一个小技巧每次改完Kamailio配置都用kamcmd cfg.sync保存一下再kamailio -c检查配置语法确保万无一失再reload能少踩很多发布事故的坑。