多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Kamailio dispatcher网源判断与分组路由配置实战

Kamailio dispatcher网源判断与分组路由配置实战 很多做Kamailio的朋友一开始接触路由逻辑都是写if ($si 192.168.1.100)这种硬编码判断规则少的时候挺爽规则一多就变成一场噩梦。后来在某套VoIP网关项目里我需要根据呼叫来源的网段、IP、甚至来源组做差异化路由才真正把dispatcher模块的“判断网源”能力吃透。今天这篇就把这个玩法彻底讲清楚从模块原理到配置文件写法再配合可直接复制的路由代码和排障经验一次性讲明白。这个内容适合谁正在用Kamailio做SIP信令路由、SBC、呼叫中心网关的运维和开发工程师。哪怕你之前只听过大名没用过dispatcher看完也能上手。核心价值可以概括成一句话把“这个呼叫从哪来”变成“这个呼叫该走哪条路”并且用一套可维护的配置管理几百上千个来源IP而不是写几百行if-else。1. dispatcher模块的底层逻辑为什么能用来“判断网源”1.1 模块本职是地址池管理分组能力是关键dispatcher模块最先为人熟知的能力是负载均衡和故障转移。它维护着一个目的地列表每个地址可以配置权重、优先级、健康状态Kamailio收到呼叫后从这个列表里按某种算法挑一个地址出来转发。这个机制本身处理的是“往哪走”的问题。但注意一个细节这个模块不只给了你一个平坦的地址列表它引入了setid分组的概念。也就是说地址不是散装在一个大池子里而是可以分成多个组。例如组1放主用SIP服务器组2放备份SIP服务器组10放客户A的网关组20放客户B的接入地址。这个分组能力就是“按网源判断”的基础设施。判断本质上是分类而setid天然提供了分类的容器。你不需要额外造一套映射表直接复用dispatcher的分组机制就能把来源IP归入某个业务组。1.2 “网源”到底指的是什么数据SIP请求进入Kamailio的时候能从网络层直接读取几个关键量源IP$si、源端口$sp、传输协议$proto。这些就是“网源”最原始的定义。再往上一点还可以看SIP头里的From域名、User-Agent、Contact地址等业务标识。在实际项目里“判断网源”通常有这么几种含义判断请求来自内网还是公网决定是否做鉴权。判断来电来自哪个客户网关决定转发到哪条中继。判断来源是否在白名单中决定是否允许访问管理接口或特殊号码段。判断来源所在区域决定选择哪一组本地出口。dispatcher模块处理的是“地址集合”的匹配所以它能承担第一、第二、第四类判断。第三类也可以做但要谨慎设计因为它本质上是访问控制更专业的做法是结合数据库鉴权。1.3 跟一堆if判断相比优势在哪里硬编码IP判断最大的问题是规则和数据耦合在一起。你有30个客户来源IP就得写30个if分支如果客户换了一个IP或者新增一个来源就得改配置然后reload整个Kamailio。用dispatcher来源IP是放在dispatcher.list文件里的新增一个来源就是加一行用kamcmd dispatcher.reload热加载主进程都不需要重启。性能上也有差异。if ($si x.x.x.x)是线性比较100条规则就要顺序比对100次。dispatcher模块内部对地址做了哈希索引匹配的速度跟列表规模的关系没那么大。这一点在来源IP上千之后会变得非常明显。还有一个隐性优势团队协作。负责业务的人可以只维护dispatcher.list这份“数据文件”不需要动路由脚本降低了误操作概率。2. 判断网源的典型业务场景2.1 按来源中继分组选路这是最常见的场景。一台Kamailio作为汇聚层同时对接了几个上游中继或者几个大客户的接入网关。每个来源过来的呼叫出局线路不同。例如A客户的呼叫要送到中继组AB客户送到中继组B。没有dispatcher的时候你需要在路由脚本里写if($si A客户网关IP) { route(TO_MEDIATOR_A); exit; }有了dispatcher你把这些来源IP分别放进不同的setid然后用ds_is_from_list识别再用ds_select_dst选路。来源管理变成了数据配置脚本逻辑统一成一套模式后续维护轻松很多。2.2 内外网差异化策略企业自建语音系统经常会遇到这种情况内网办公分机注册到Kamailio走内部SIP服务器不经公网外部远程分机或出差软终端从公网注册需要先过NAT处理、鉴权、反欺诈检查。把内网网段拆成一组dispatcher列表判断命中后直接放行进内部路由跳过公网那一堆处理。公网来源统一走另一条路由逻辑。这样既减少内网呼叫的时延也让安全策略的分层变得直观。2.3 限流与安全准入判断网源之后除了决定路由还可以顺势决定“给不给资源”。例如呼叫源在白名单允许访问的组别内才允许使用转码资源、才允许发起呼叫不在列表内的陌生发起方直接回403或进入人工审核流程。这类场景的核心思路是先识别来源再绑定策略。dispatcher负责识别策略绑定用AVP或数据库完成。判断出来的结果可以存到一个AVP里后面的路由分支统一读取这个AVP而不是反复调用ds_is_from_list这样脚本更清晰也更容易调试。3. 两条实现主线的详细拆解3.1 直接用源地址变量做精确或正则匹配最简单粗暴的方式if($si 192.168.1.100) { route(INTERNAL_CALL); exit; }稍微灵活一点用正则匹配网段if($si ~ ^192\.168\.) { route(INTERNAL_CALL); exit; }这种写法在规则很少的时候没问题但有几个明显的坑正则表达式写多了之后肉眼根本看不出哪条匹配哪段排障很痛苦。性能上每条都要做正则编译和匹配规则一多CPU开销直线上升。新来源IP必须改脚本无法热加载。所以这条主线只适合“永远只有三五个固定来源”的场景。如果你的来源列表过阵子就要变不要用这个方案。3.2 用 ds_is_from_list() 做集合归属判定dispatcher模块提供了一个专门做集合判断的函数ds_is_from_list。签名大致是这样的ds_is_from_list([setid [, mode [, uri]]])三个参数的作用第一个参数setid指定匹配哪一组地址。如果省略则匹配所有组。第二个参数mode匹配模式。常用的是0表示只匹配IP地址部分如果传1表示匹配IP:端口。第三个参数uri要拿去匹配的字符串。通常传$si也就是请求的源IP。返回值方面1表示命中0表示未命中负数表示模块内部错误或参数问题。所以在配置文件里最常见的写法是if(ds_is_from_list(10, 0, $si)) { # 来源属于组10 } if(!ds_is_from_list(10, 0, $si)) { # 来源不属于组10 }这个函数的优势是内部走哈希查找性能远好于自己写一长串if。而且它判断的是“来源是否命中某个地址集合”天然适合做网源归属判断。有一点容易踩坑第三个参数如果传$si那匹配的就是纯IP如果你希望来源端口也参与匹配比如某些来源固定从5060端口发出就需要传$si:$sp同时把mode设为1。否则端口上的差异会导致判断结果不符合预期。3.3 两条路线的取舍用表格直观对比一下对比维度if变量匹配ds_is_from_list规则数量少直观简单多一层配置文件规则数量多难以维护集中管理清晰性能线性比较规则多时差哈希索引性能稳定规则热更新需要改脚本并reload改dispatcher.list热加载精确控制完全可控写法随意需要理解mode参数细节与选路联动需自己写选择逻辑可与ds_select_dst联动我的建议很明确只要来源规则超过十条或者预期会持续增长直接用ds_is_from_list。这个函数存在的意义就是解决“地址集合判断”这一类问题硬用if反而要付出更多维护成本。4. 完整落地配置从零搭建按网源路由4.1 模块加载与关键参数先加载模块并设置几个重要参数loadmodule dispatcher.so modparam(dispatcher, list_file, /etc/kamailio/dispatcher.list) modparam(dispatcher, hash_size, 10) modparam(dispatcher, ds_ping_interval, 60) modparam(dispatcher, ds_ping_method, OPTIONS) modparam(dispatcher, ds_ping_reply_codes, class2;code403;code404;code480;code486) modparam(dispatcher, ds_probe_mode, 1)逐个说下为什么这么配。list_file是地址列表文件路径默认在/etc/kamailio/dispatcher.list。这个文件决定了所有来源组的定义是整套方案的“数据库”。hash_size是内部哈希表的大小单位是2的幂。10代表2的10次方也就是1024个槽位。对于一个来源IP数量在几百到一两千的项目这个值够用。如果你管理的来源IP上万建议调成12或13。哈希槽越大冲突越少匹配越快但也稍微多吃一点内存。ds_ping_interval是健康检查探测间隔单位秒。60秒一次兼顾时效性和资源消耗。ds_ping_method设置探活请求方法OPTIONS不会产生呼叫费用也不会触发业务逻辑是标准的探活方式。ds_ping_reply_codes设置哪些响应码算“存活”。这里加了403和404因为有些网关在禁止OPTIONS请求时会回403或者干脆回404表示路径不存在但这些网关实际是能转发呼叫的。如果你不把这些code加进去它们会被误判为故障地址导致呼叫无法切换。ds_probe_mode设置为1表示只探测当前非活动状态的地址。这样能减少不必要的探测流量也有助于快速恢复故障地址。4.2 dispatcher.list 文件设计文件内容格式是setid, destination, [weight, priority, ...]每一行代表一条记录。比如# 内网来源组 1, sip:192.168.10.10:5060 1, sip:192.168.10.11:5060 # 客户A来源组 10, sip:10.20.30.40:5060 10, sip:10.20.30.41:5060 # 客户A出局中继组 11, sip:203.0.113.100:5060 11, sip:203.0.113.101:5060 # 客户B来源组 20, sip:10.20.40.50:5060 # 客户B出局中继组 21, sip:203.0.114.100:5060这个设计里面有一个关键点来源组和出局中继组是分开的。为什么因为dispatcher模块在ds_select_dst的时候会把匹配到的组里的地址作为目的地址来使用。如果你把来源IP和真正要转发的目的地址放在同一个组里Kamailio会把来源IP本身当成目的服务器可能把呼叫又转回呼叫方形成环路。所以我强烈建议在规划setid的时候就做好分区。10-19这个区间用于来源判断20-29用于对应业务的中继转发。来源组只用来做匹配中继组只用来做出局选择互不干涉。这个文件还支持注释行建议把组用途标注清楚。半年之后回头看清晰的分区注释比什么都管用。4.3 核心路由逻辑代码下面是一套可以直接粘贴到Kamailio配置里试用的骨架逻辑request_route { # 网源识别先做识别结果存AVP if(ds_is_from_list(10, 0, $si)) { xlog(L_INFO, 来源命中客户A组源IP$si\n); $avp(s:source_group) customer_a; } else if(ds_is_from_list(20, 0, $si)) { xlog(L_INFO, 来源命中客户B组源IP$si\n); $avp(s:source_group) customer_b; } else { xlog(L_WARN, 未知来源拒绝源IP$si\n); sl_send_reply(403, Unknown Source); exit; } # 按来源组选择对应的出局中继组 if($avp(s:source_group) customer_a) { if(!ds_select_dst(11, 0)) { sl_send_reply(503, No Destination); exit; } } else if($avp(s:source_group) customer_b) { if(!ds_select_dst(21, 0)) { sl_send_reply(503, No Destination); exit; } } t_on_failure(FAILURE_RT); if(!t_relay()) { sl_reply_error(); } exit; } failure_route[FAILURE_RT] { if(t_is_canceled()) { exit; } # 尝试组内下一个目的地 if(ds_next_dst()) { t_on_failure(FAILURE_RT); t_relay(); exit; } }这段代码有几个细节值得说明。ds_is_from_list的三个参数里第三个参数是字符串$si是运行时的源IP变量。模块内部会把$si展开成实际IP进行匹配。所以你在配置里不需要显式赋值直接写$si就行。xlog可以输出当前命中的分组信息这个日志在排障时非常关键。生产环境建议保留但注意控制日志级别避免高并发下刷爆磁盘。$avp(s:source_group)用来存储来源组标识。后面所有路由决策都读这个AVP而不是反复调用ds_is_from_list。这样做的原因一是性能更好二是语义更清晰——来源识别只做一次后面纯逻辑判断。ds_select_dst会把选中的目的地写入$du然后t_relay就会把请求发往这个目的地。ds_next_dst在主叫地址不可达时自动跳到组里下一个地址实现故障转移。4.4 上线验证与测试方式配置文件改完后先用命令检查语法kamailio -c看到Syntax OK再重启。如果是新加载dispatcher列表不重启也可以用kamcmd dispatcher.reload。然后查看列表内容是否正常kamcmd dispatcher.list你会看到每个setid下有哪些地址状态是否活跃。这个命令是排查网源判断问题的第一站。再用SIP工具做功能性验证。可以用sipp构造请求分别使用列表内的源IP和列表外的源IP发起呼叫列表内来源应能看到呼叫成功转发到对应中继。列表外来源应收到403拒绝。手动把一条中继地址停掉再发呼叫应能看到自动切换到组内另一条备用中继。抓包辅助定位可以用tcpdump -i eth0 -n -s 0 -w test.pcap port 5060观察信令流向确认请求从哪个IP进来又往哪个IP转发。这一步能快速确认ds_select_dst是否选对了目标。5. 从测试到生产常见问题与排查实录5.1 高频问题速查表现象可能原因解决办法ds_is_from_list 一直返回0setid写错或者列表没加载先kamcmd dispatcher.list确认dispatcher.list加载失败文件格式错误逗号或换行不对检查文件末尾换行空格注释符来源识别成功但转发地址不对来源组和中继组用了同一个setid立即拆分来源组和出局组修改列表后无效果没有执行reloadkamcmd dispatcher.reload中继正常但总被标记故障探活响应码不在允许列表调整ds_ping_reply_codes用固定端口匹配却不中mode和uri参数不匹配传$si:$sp且mode设为1公网来源IP不准前面还有一层SBC/NAT确认实际看到的$si来源5.2 三个特别值得说透的坑第一个坑是来源组和中继组复用。我有一次在客户环境调试来源判断倒是很顺利呼叫却打到了来源IP本身。查了半天发现dispatcher.list里同一行既配了客户来源地址又配了出局中继地址。ds_select_dst直接选中了“来源地址”当作目的地结果就是A呼叫BB又被当成目的地信令环路。拆分setid后瞬间解决。第二个坑是文件编码。dispatcher.list在Windows上编辑过再传回Linux出现了\r\n的问题。模块解析时把回车当成了目标地址的一部分导致匹配失败。排查时看kamcmd dispatcher.list的输出地址后面多了一个诡异字符。处理方式是统一用Unix换行传文件之前先dos2unix过一遍。第三个坑是ds_is_from_list的mode理解不到位。第一次用这个函数时我传的uri是$si但是mode用了1结果怎么都不命中。后来看文档才明白mode为1时模块预期的是地址加端口的组合只给IP匹配不上。如果你是纯IP匹配用mode 0如果确实需要IP加端口就传$si:$sp并保持mode 1。5.3 性能调优与运维心得来源判断放在路由入口最前面。不要在做完一堆DB查询、鉴权、NAT处理之后再判断网源那样浪费计算资源。理想顺序是收到请求 → 网源识别 → 基础合法性检查 → 按来源组路由。hash_size不要盲目调大。假设你的来源IP就300个hash_size设10已经绰绰有余。调大会增加内存占用但匹配性能不会有感知级别的提升。只有在来源IP数量上万时才值得把hash_size调到13或14。热加载有一个小技巧先准备一个全新的dispatcher.list文件确认格式无误后再执行kamcmd dispatcher.reload。如果文件写坏了reload不会生效但旧列表还在运行不会中断线上呼叫。这比直接重启Kamailio安全得多。日志里如果看到大量ds_is_from_list匹配失败的记录先不要急着加规则。先抓包看看来源IP到底是什么。很多项目前面还挂了SBC或NAT设备Kamailio实际看到的$si可能和业务方提供的“客户网关IP”不一样。确认清楚实际源地址再往列表里加否则加了一堆规则永远不命中。最后分享一个我的习惯无论项目多小总会在dispatcher.list顶部写清楚setid的分配方案注释类似这样# 1-9: 内网地址分组 # 10-19: 外部来源识别组 # 20-29: 出局中继组这套划分让配置表本身具备自解释能力后续维护的人不会因为分不清“这个组是拿来匹配还是拿来转发”而踩坑。我自己吃过分组的亏之后就再也没省过这几行注释。
返回列表