OpenSIPS 入门:一台 SIP 服务器,一个可编程的呼叫路由引擎

Posted on 一 14 9月 2026 in Tech

Abstract OpenSIPS 入门:一台 SIP 服务器,一个可编程的呼叫路由引擎
Authors Walter Fan
Category Tech
Version v1.0
Updated 2026-09-14
License CC-BY-NC-ND 4.0

大纲

展开看看
  • 它是什么:一个 GPL 的 SIP 服务器,本质是"可编程的 SIP 信令路由引擎",不碰媒体
  • 和 FreeSWITCH 什么关系:一个管信令和路由(前台),一个管媒体和业务(后台)
  • 怎么编译:源码 + make,理解 Makefile.conf、模块机制、TLS=1
  • 进程与路由模型:多进程 worker + 一套自己的脚本语言,route / branch_route / onreply_route / failure_route
  • 典型流程:一次 REGISTER 和一次 INVITE 在 opensips.cfg 里到底走了哪几步
  • 代码结构:core 干什么、193 个模块怎么分类、tm/rr/usrloc 是骨架
  • 最佳实践:事务、record_route、NAT、限流、MI 管理接口
  • 常见坑:483 死循环、prefix 不一致、忘了 t_relay、NAT 媒体不通
  • 上手清单:从零到打通第一路电话

一、OpenSIPS 是什么

先说结论:OpenSIPS 是一台可编程的 SIP 服务器。它不处理语音数据(RTP),只处理 SIP 信令——也就是那些 REGISTERINVITEBYE 消息。它的核心价值是:你可以用一门专门的脚本语言,决定每一条 SIP 消息该怎么处理、往哪里转。

一句话类比:如果 SIP 呼叫是快递,OpenSIPS 就是那个巨大的分拣中心——它自己不送货(不传语音),但每个包裹到底发往哪个网点、要不要验身份、要不要限流、走不走加密通道,全由它说了算。而真正"送货"(收发 RTP 语音流、放音、录音、会议混音)的活,交给 FreeSWITCH、Asterisk 或 rtpproxy/rtpengine 这类媒体服务器。

几个需要先对齐的名词:

  • SIP(Session Initiation Protocol):发起和管理通话的信令协议,长得像 HTTP,纯文本。
  • 信令 vs 媒体:信令是"喂,我要给你打电话"这套约定;媒体是接通后真正传输的声音。OpenSIPS 只管前者。
  • SIP proxy:把 SIP 请求从一方转发到另一方的中间人。OpenSIPS 主要就是干这个,外加注册、鉴权、路由等一堆增强。

OpenSIPS 起源于 SER(SIP Express Router),2005 年从 SER 分叉出来,主打"更开放的项目管理和更快的特性合入"。本文对照的是仓库里的开发版 4.1.0-dev(GPLv2 许可)。

它和 FreeSWITCH 到底谁干什么

写过前面几篇 FreeSWITCH 的读者会问:FreeSWITCH 不也能当 SIP proxy 吗?能,但定位不同。这是最容易混淆的地方,用一张表说清:

维度 OpenSIPS FreeSWITCH
本质 SIP 信令路由引擎(SIP proxy / registrar) 媒体服务器 + 呼叫控制平台(B2BUA)
碰不碰媒体 不碰 RTP(可借 rtpproxy/rtpengine 中继) 收发、转码、放音、录音、会议
扩展性 极强,单机可扛几万到几十万并发信令 单机媒体并发受 CPU/编解码限制
编程方式 自己的路由脚本(opensips.cfg) Dialplan / mod_lua / ESL
典型角色 运营商级前端、负载均衡、注册代理 IVR、会议、录音、转码、业务逻辑

生产里常见的搭配是:OpenSIPS 站在最前面当注册中心和负载均衡,把呼叫分发给后面一排 FreeSWITCH 干重活。前台一个人挡住海量请求,后台一群人干细活。


二、怎么编译

OpenSIPS 是 C 写的,构建系统是一套手写的 GNU Makefile,不用 CMake。核心思路:先编 core,再编模块,模块按需选

依赖

最小依赖不多:gccbison(或 yacc)、flex、GNU make(>= 3.79)、sedtr。可选依赖按模块来:要 TLS 就装 openssl,要 MySQL 就装 libmysqlclient,要 PostgreSQL 就装 libpq,要 XML-RPC 管理接口就装 libxmlrpc-c3,以此类推。

Debian/Ubuntu 上装齐基础依赖:

sudo apt-get install gcc make bison flex libssl-dev \
     libmysqlclient-dev libpq-dev

三条命令跑起来

git clone https://github.com/OpenSIPS/opensips.git
cd opensips

# 只编 core(等价于 make opensips)
make

# 编所有默认模块
make modules

# 或者一步到位:core + 模块
make all

# 安装(默认装到 /usr/local/)
sudo make install

关键在于理解模块选择机制,这是新手最容易懵的地方。构建配置存在 Makefile.conf 里(首次 make 会从 Makefile.conf.template 拷一份),几个开关记住就够用:

  • exclude_modules:默认不编的模块(通常是有外部依赖的)。
  • include_modules:强制编进去,哪怕它在 exclude 列表里。
  • skip_modules:这次构建临时跳过某些模块。

举几个实战例子:

# 只编 textops 一个模块(调试单模块时很有用)
make modules=modules/textops modules

# 默认模块里跳过 textops 和 db_mysql
make skip_modules="textops db_mysql" modules

# 把默认不编的 db_mysql 拉进来编
make include_modules="db_mysql" modules

# 想编 TLS / SCTP 支持,用编译期开关
make TLS=1 all
make SCTP=1 all

两个必须知道的编译期约定

第一,prefix 要前后一致。 默认安装前缀是 /usr/local/,而默认配置文件路径是编译期硬编码进二进制的。如果你 make all 用默认 prefix,却 make prefix=/ install 装到别处,OpenSIPS 启动时会去 /usr/local/etc/opensips/opensips.cfg 找配置,结果文件明明在 /etc/opensips/ ——直接找不到。正解是所有 make 命令都带同一个 prefix,或者干脆写进 Makefile.conf

第二,改了 DEFS、编译器/链接器 flag 之后要 make proper 再重编。 否则依赖没重新生成,你会遇到各种玄学错误。

除了源码,官方也提供 Debian/RPM 包和 Docker 镜像(仓库 docker/ 下有 CI 用的 Dockerfile),不想折腾编译可以直接用包管理器装。


三、进程与路由模型:OpenSIPS 的灵魂

要真正会用 OpenSIPS,得理解两件事:它怎么跑(进程模型),以及你怎么指挥它(路由脚本)

多进程 worker 模型

OpenSIPS 启动后 fork 出一组常驻进程。核心是一批 worker 进程(比如 UDP worker、TCP worker),每个 worker 独立完整地处理一条 SIP 消息——收包、跑脚本、转发,一气呵成。配置里那句 udp_workers=4 就是说开 4 个 UDP worker。除此之外还有 timer 进程、管理接口进程等。

进程间共享状态靠共享内存(shm),这是 OpenSIPS 高性能的关键,也是踩坑的高发区:worker 之间的私有内存(pkg)彼此看不见,只有放进 shm 的数据(比如注册信息、事务状态)才是全局可见的。

一门专门的路由脚本语言

OpenSIPS 最独特的地方:配置文件 opensips.cfg 不是 key-value,而是一段用专门语言写的程序。它由词法/语法分析器(cfg.lex / cfg.y)解析,每来一条 SIP 消息,就执行对应的"路由块"。

路由块分几种类型(定义在 route.h 里),各自在不同时机被触发:

路由块 触发时机
route{} 收到一条 SIP 请求(主入口)
branch_route[] 请求即将往某个分支发出前(每个分支各触发一次)
onreply_route[] 收到一条 SIP 应答时
failure_route[] 某个事务收到失败应答(4xx/5xx/6xx)时
local_route[] OpenSIPS 自己生成请求时
startup_route[] 进程启动时执行一次(适合初始化)
timer_route[] 按设定周期定时执行
event_route[] 订阅的内部事件发生时

理解这套"事件 → 路由块"的映射,是读懂任何一份 OpenSIPS 配置的钥匙。下面我们就顺着官方自带的 residential 配置(etc/opensips.cfg),把两个最典型的流程走一遍。


四、典型用例与流程

流程一:一次注册(REGISTER)

用户的 SIP 话机开机,要告诉服务器"我现在这个 IP/端口能被找到",这就是注册。在脚本里,它落到主 route{} 的这几行:

if (is_method("REGISTER")) {
    # 保存注册信息到 usrloc(location 表)
    if (!save("location"))
        xlog("failed to register AoR $tu\n");
    exit;
}

一次 REGISTER 的完整旅程:

  1. 进主 route:先 mf_process_maxfwd_header(10) 检查 Max-Forwards(防死循环)。
  2. 没有 To-tag,说明是新请求(不是对话内的续传)。
  3. 判断方法是 REGISTER
  4. save("location"):由 registrar 模块把这条注册写进 usrloc 模块管理的 location 表(内存里,可选落库),同时自动生成 200 OK 应答给话机。
  5. exit 结束。

生产环境里,REGISTER 之前通常还要插一段鉴权:用 auth / auth_db 模块做 digest 认证(www_challenge 发起挑战、www_authorize 校验),确认这个人真是他自己,防止别人冒名注册。默认配置为了免依赖把鉴权关掉了——这在生产上绝对不能照抄

流程二:一次呼叫(INVITE)

A 要打给 B。核心难点在于:SIP 是逐跳(hop-by-hop)的,OpenSIPS 得处理好"新请求"和"对话内后续请求"两种情况。我把 residential 配置的呼叫路径拆成几步:

第一步,区分新请求还是对话内请求。 有 To-tag 说明是已建立对话内的后续消息(ACK、BYE、re-INVITE):

if (has_totag()) {
    # 对话内请求,按 record-route 定下的路径走
    if (!loose_route()) { ... }
    if (is_method("BYE")) {
        do_accounting("log","failed");
    }
    route(relay);
    exit;
}

第二步,新 INVITE 要做事务和录路(record-route)。 这两个概念是骨架:

# 吸收重传,不重复建事务
t_check_trans();

# 让后续的对话内请求还能回到本机
if (!is_method("REGISTER|MESSAGE"))
    record_route();

# 只对 INVITE 记账
if (is_method("INVITE")) {
    do_accounting("log");
}

第三步,查被叫在哪,然后转发。 从 usrloc 里查 B 注册时留下的地址:

# 从 location 表查被叫的当前联系地址
if (!lookup("location","method-filtering")) {
    t_reply(404, "Not Found");
    exit;
}
route(relay);

第四步,relay 路由真正把请求发出去:

route[relay] {
    if (is_method("INVITE")) {
        t_on_branch("per_branch_ops");   # 每个分支发出前的钩子
        t_on_reply("handle_nat");        # 收到应答时的钩子
        t_on_failure("missed_call");     # 失败时的钩子
    }
    if (!t_relay()) {
        send_reply(500,"Internal Error");
    }
    exit;
}

t_relay() 是整份配置里最重要的一个函数——它由 tm(transaction module,事务模块)提供,负责有状态地把请求转发出去、管理重传、匹配应答。忘了调 t_relay(),呼叫就石沉大海,这是新手第一大坑。

配上前面注册的 record_route(),后续的 ACK、BYE 才能沿着同一条路径回到 OpenSIPS,整个对话才闭环。

用一张图把这条呼叫链路画清楚:

sequenceDiagram
    participant Alice as Alice (UAC)
    participant OS as OpenSIPS (proxy)
    participant Bob as Bob (UAS)

    Alice->>OS: INVITE
    OS->>OS: lookup(location) 查 usrloc
    OS->>OS: record_route() 记录路径
    OS->>Bob: INVITE (t_relay())
    Bob-->>OS: 200 OK
    OS-->>Alice: 200 OK (onreply_route)
    Alice->>OS: ACK
    OS->>Bob: ACK (loose_route) 对话内路由
    Note over Alice,Bob: RTP 媒体流(不经过 OpenSIPS,除非用 rtpproxy)
    Alice->>OS: BYE
    OS->>Bob: BYE

注意最后那条:语音流(RTP)默认直接在 Alice 和 Bob 之间跑,不经过 OpenSIPS。这正是它能扛高并发的原因——它只处理小小的信令包。只有当双方在 NAT 后面互相看不见时,才需要 rtpproxy/rtpengine 出来中继媒体。


五、代码结构

克隆下来的仓库,粗看文件很多,但脉络清晰。分三层理解:

Core(仓库根目录的那堆 .c/.h)

这是引擎本身,不涉及具体业务。挑几个关键的:

  • main.c:进程入口,负责启动、fork worker、装载配置。
  • cfg.lex / cfg.y:那门路由脚本语言的词法和语法分析器(flex + bison)。
  • route.c / action.c:执行路由脚本、跑每个 action 的核心。
  • msg_translator.c / parser/:解析和重建 SIP 消息。parser/ 是个独立目录,专管把文本 SIP 拆成结构体。
  • mem/:内存管理,区分 pkg(进程私有)和 shm(共享)两套分配器——这是理解 OpenSIPS 并发模型的关键。
  • net/:网络传输层,TCP/UDP 收发、io_wait 事件循环。
  • pt.c:进程表,管理那一组 worker 进程。
  • sr_module.c:模块加载和 API 注册机制(dlopen 动态加载 .so)。
  • pvar.c / transformations.c:脚本里 $ru$fu 这类伪变量(pseudo-variable)和 {s.len} 这类变换的实现。

Modules(modules/,193 个模块)

OpenSIPS 的功能几乎全在模块里,core 只是骨架。193 个模块按用途大致分几类:

类别 代表模块 干什么
骨架(几乎必用) tmrrslmaxfwdsipmsgops 事务、录路、无状态回复、跳数、消息操作
注册与定位 usrlocregistrarmid_registrar 用户位置存储、注册处理
鉴权 authauth_dbauth_jwtauth_aka digest / JWT / AKA 认证
数据库 db_mysqldb_postgresdb_sqlitedb_text 各种 DB 后端(统一 DB API)
缓存 cachedb_rediscachedb_mongodbcachedb_local 分布式/本地缓存
路由策略 dispatcherdroutingload_balancercarrierroute 负载均衡、动态路由、运营商路由
对话与状态 dialogtopology_hiding 对话跟踪、拓扑隐藏
NAT / 媒体 nathelperrtpproxyrtpengine NAT 穿透、RTP 中继
传输协议 proto_udpproto_tcpproto_tlsproto_wsproto_wss 各种 SIP 传输层(含 WebRTC 用的 WSS)
集群与高可用 clusterer 多节点数据同步
安全 fraud_detectionpermissions 防欺诈、权限控制
管理接口 mi_fifomi_httpmi_html 运行时管理(MI)

每个模块目录下都有 README(由 docbook XML 生成),写清了它导出的函数、参数、伪变量。这是查 API 的第一手资料。

Lib(lib/)与其他

lib/ 放公共库(JSON、CSV、URL、digest 鉴权、hash 等),db/ 是数据库抽象层,mem/ 内存,evi/ 事件接口,mi/ 管理接口框架。理解了 "core 提供机制、module 实现策略" 这条主线,整个仓库就不再是一团乱麻。


六、最佳实践

真正跑生产,下面几条比语法更重要:

  • 一切转发都走事务(t_relay)。 除非你非常清楚自己在干无状态转发,否则新请求一律用 tm 模块的 t_relay(),让它替你管重传和应答匹配。别自己 forward() 硬转。
  • 对所有需要闭环的对话做 record_route() 不然 ACK/BYE 找不到回来的路,对话半路断裂。记账、计费、拓扑隐藏也都依赖它。
  • 鉴权别省。 默认配置关了鉴权是为了免依赖,生产必须开 auth_db + digest,否则任何人都能冒名注册、盗打。
  • NAT 要专门处理。 大量话机在 NAT 后面。用 nathelper 检测、fix_nated_contact/fix_nated_sdp 修正,媒体不通时上 rtpproxy/rtpengine 中继。这是实战里最耗时的一块。
  • 限流与防欺诈前置。pike(流控)、fraud_detection(异常呼叫模式)在最前面挡住攻击和话费盗刷——SIP 服务器天天被扫。
  • 善用 MI 管理接口。 mi_fifo / mi_http 让你运行时查状态、看统计、reload 数据,不用重启。配 opensips-cli 工具更顺手(老的 opensipsctl 已被移除)。
  • 配置模块化。route(name) 把逻辑拆成命名子路由,别把几百行塞进一个 route{}examples/templates/ 下有 M4 模板可以起步。
  • 持久化和高可用。 注册、对话状态默认在内存,重启即失。生产用 DB 落库或 clusterer 做多节点同步。

七、常见坑

老程序员的经验是:框架的坑,往往长得都一样。OpenSIPS 这几个几乎人人踩过:

  • 483 Too Many Hops / 513 Message Too Large 的死循环。 十有八九是本域匹配没配对:OpenSIPS 没认出"这条请求是发给我自己的",于是又把它转给了自己,直到 Max-Forwards 归零。解法:配好 alias=你的域名,让 is_myself() 能正确判断。这是官方 INSTALL 文档里专门列出来的头号问题。
  • prefix 前后不一致,启动就找不到配置。 前面编译章节说过:makemake install 的 prefix 必须一致,否则二进制里硬编码的配置路径和实际安装路径对不上。
  • 忘了 t_relay(),呼叫石沉大海。 脚本逻辑跑完了却没真正转发,对方永远等不到 INVITE。
  • 忘了 record_route(),对话中途断。 INVITE 通了,但 BYE 挂不掉、re-INVITE 失败,因为后续请求回不到 OpenSIPS。
  • NAT 后媒体不通:信令通,听不到声音。 典型症状是"电话接通了但双方都是哑巴"。因为 SDP 里写的是内网 IP。要用 nathelper 修 SDP,必要时用 rtpproxy 中继。
  • shm 内存耗尽。 对话/事务状态都在共享内存,并发一高、又没设合理超时(fr_timeout、对话生命周期),shm 会被撑爆。上线前用 -m(shm 大小)、-M(pkg 大小)算好容量。
  • 改了 DEFSmake proper 直接重编。 会遇到莫名其妙的运行时错误,清理重编即可。
  • 把默认配置直接搬上生产。 residential 配置是教学用的最小示例,关了鉴权、没落库、没 NAT 处理、没限流——照抄上线等于裸奔。

八、上手清单

想从零打通第一路电话,按这个顺序来:

  1. 装依赖:gcc make bison flex libssl-dev(要 DB 再加对应 lib)。
  2. 拉源码编译:git clonemake allsudo make install(注意 prefix 一致)。
  3. 改配置:编辑 /usr/local/etc/opensips/opensips.cfg,把 socket=udp:127.0.0.1:5060 里的 IP 换成你的真实网卡地址,设好 alias=你的域名
  4. 启动:opensips -f /usr/local/etc/opensips/opensips.cfg,先前台带 -D 看日志。
  5. 两个软电话注册:用 Linphone/Zoiper 注册两个账号到你的服务器(默认无鉴权,随便填用户名)。
  6. 互拨验证:A 拨 B,看能不能接通。
  7. 看状态:用 opensips-climi_fifo 查在线注册、事务统计。
  8. 加鉴权和 NAT:验证通过后,再逐步加 auth_dbnathelper,向生产靠拢。

总结:先分清信令和媒体,再谈路由

OpenSIPS 不难,难的是一开始就把它和 FreeSWITCH、和媒体服务器混为一谈。记住这条主线:它是一台只管 SIP 信令、不碰语音数据的可编程路由引擎;它的全部威力,来自那门"每来一条消息就执行一段路由脚本"的语言,和背后 193 个各司其职的模块。

理解了 core 提供机制、module 实现策略、脚本编排流程这三层,再回头看任何一份 opensips.cfg,你都能顺着 route{} 一步步读懂它在干什么。剩下的,就是在真实流量里,把鉴权、NAT、限流、高可用一块块补齐。

行动清单

  • [ ] 编译时确认 makemake install 的 prefix 一致
  • [ ] 配好 alias,避免 483 死循环
  • [ ] 所有需要闭环的对话都 record_route()
  • [ ] 所有转发都走 t_relay()(有状态)
  • [ ] 生产环境务必开启 auth_db digest 鉴权
  • [ ] NAT 场景配 nathelper,媒体不通上 rtpproxy/rtpengine
  • [ ] 前置 pike 限流 + fraud_detection 防盗刷
  • [ ] 用 DB 或 clusterer 解决状态持久化和高可用
  • [ ] 上线前用 -m/-M 算好 shm/pkg 容量

参考