团队博客

Kamailio UAC 模块简述

韩小仿  2024-04

Kamailio 是一款非常强大的 SIP 代理服务器,Kamailio 一般转发 SIP 信令,不主动产生和发送 SIP 信令。但有时您可能希望 Kamailio 向 IPPBX 注册、主动发 SIP 消息,等等,也就是让 Kamailio 起到客户端的作用,这就需要用到 UAC 模块。

UAC 模块包含的内容很多,但官方手册给的例子比较少,想要熟练掌握就需要做很多练习。本文分几个方面进行说明,希望起到抛砖引玉的效果。

uac_replace、uac_restore

先看下面的流程:

UAC                   Kamailio                 UAS
     <----->                      <----->
   原始主被叫保持不变             新的主叫和被叫

例如 1001 呼叫 1002,通过下面的路由脚本就可以把主叫号码修改成 alice,被叫号码修改成 bob:

uac_replace_from('"alice"', "sip:" + "alice" + "@" + $fd);
uac_replace_to('"bob"', "sip:" + "bob" + "@" + $td);

但问题是如何处理后续的 SIP 请求(比如ACK、Re-Invite、BYE等),UAC 跟 Kamailio 之间始终保持 1001 是主叫,1002 是被叫,Kamailio 跟 UAS 之间始终都是 alice 是主叫,bob 是被叫。

这涉及到下面三个问题:

  • 需要保存新的主被叫号码
  • 主被叫号码保存到哪里,rr 头 或者对话变量
  • 恢复主被叫号码的方式有自动和手工

先看下面的模块参数配置:

modparam("rr", "append_fromtag", 1)                    # 必须为 1
modparam("uac", "restore_mode", "auto")                # 自动恢复方式
modparam("uac", "restore_dlg", 0)                      # 不从对话变量中恢复
modparam("uac", "rr_from_store_param", "vsf")          # 利用 rr 头的 vsf 参数来进行 from 的保存和恢复
modparam("uac", "rr_to_store_param", "vst")            # 利用 rr 头的 vst 参数来进行 to 的保存和恢复
modparam("uac", "restore_passwd", "my_secret_passwd")  # 密码保存到 rr 头的 my_secret_passwd, 密码加密后保存

用 sngrep 跟踪呼叫,下面是 Kamailio 收到的 INVITE 包

INVITE sip:1002@192.168.0.103 SIP/2.0
Via: SIP/2.0/UDP 192.168.0.100:22040;branch=z9hG4bK-d87543-8a6dd8262a483446-1--d87543-;rport
Max-Forwards: 70
Contact: <sip:1001@192.168.0.100:22040>
To: "1002"<sip:1002@192.168.0.103>
From: <sip:1001@192.168.0.103>;tag=4955d54f
...

经过 Kamailio 路由处理之后,发出来下面这个包:

INVITE sip:1002@192.168.0.100:5066;ob SIP/2.0
Record-Route: <sip:192.168.0.103;lr;ftag=4955d54f;vsf=bXlfczU/KzdRLnhqb2xwantnQWxkYEE-;vst=bXlfczY8IBcFV3t9bHR5cnNnQHJmUA-->
Via: SIP/2.0/UDP 192.168.0.103;branch=z9hG4bKdeb8.9ad64c208ecf50f14f8fff328827fc07.0
Via: SIP/2.0/UDP 192.168.0.100:22040;received=192.168.0.100;branch=z9hG4bK-d87543-8a6dd8262a483446-1--d87543-;rport=22040
Max-Forwards: 69
Contact: <sip:1001@192.168.0.100:22040>
To: "bob"<sip:bob@192.168.0.103>
From: "alice" <sip:alice@192.168.0.103>;tag=4955d54f
...

很明显,rr 头多了 vsf 和 vst 参数。

下面尝试把主被叫号码保存到对话变量,模块参数配置如下:

modparam("rr", "append_fromtag", 1)                    # 必须为 1
modparam("dialog", "db_mode", 0)                       # dialog 的对话变量不保存到数据库
modparam("uac", "restore_mode", "auto")                # 自动恢复方式
modparam("uac", "restore_dlg", 1)                      # 从对话变量中恢复

现在发起一个呼叫,被叫应答之后,运行 kamcmd dlg.list,输出为:

{
	h_entry: 4021
	h_id: 10506
	ref: 2
	call-id: MGUwYjRiNjEzMTA5MGUxOGZhMDljYWZjNDdkOTQyYTM.
	from_uri: sip:1001@192.168.0.103
	to_uri: sip:1002@192.168.0.103
	state: 4
	start_ts: 1726108372
	init_ts: 1726108372
	end_ts: 0
	duration: 2
	timeout: 1726151572
	lifetime: 43200
	dflags: 643
	sflags: 0
	iflags: 0
	caller: {
		tag: 024f2837
		contact: sip:1001@192.168.0.100:25946
		cseq: 1
		route_set:
		socket: udp:192.168.0.103:5060
	}
	callee: {
		tag: 42bca8c3647e483d926a125b9ddc671b
		contact: sip:1002@192.168.0.100:5066;ob
		cseq: 0
		route_set:
		socket: udp:192.168.0.103:5060
	}
	profiles: {
	}
	variables: {
		{
				_uac_tdpnew: "bob"
		}
		{
				_uac_tdp: "1002"
		}
		{
				_uac_tonew: sip:bob@192.168.0.103
		}
		{
				_uac_to: sip:1002@192.168.0.103
		}
		{
				_uac_fdpnew: "alice"
		}
		{
				_uac_fdp:
		}
		{
				_uac_funew: sip:alice@192.168.0.103
		}
		{
				_uac_fu: sip:1001@192.168.0.103
		}
	}
}

可以看到,原始的主被叫号码和修改后的主被叫号码都记录到了对话变量,方便以后做恢复处理。

顺便提下,如果把 dialog 模块的 db_mode 的值从 0 改成 2,那么 kamailio 重启时会自动从数据库里面读入对话变量的值。

uac_reg_send

一般用 uac_reg_send 发送 OPTIONS 或者 MESSAGE,下面是一段路由代码(任意路由都可以执行):

loadmodule "uas.so"
loadmodule "jansson.so"
...

route[UAC_MESSAGE] {
	$var(aor) = "1001@192.168.0.103";            # 发送 MESSAGE 到 1001 这个注册用户
	$var(text) = "Hello, World!\r\n";            # 待发送的文本内容
	$var(from) = "sip:admin@192.168.0.103";

	# rpc 请求报文
	$var(req) = $_s({"jsonrpc":"2.0", "method":"ul.lookup","params":["location","$var(aor)"], "id":1});
	jsonrpc_exec("$var(req)");
	if ($jsonrpl(code) != 200) return;

	# 暂时只处理一个 contact
	jansson_get("result.Contacts[0].Contact.Address", "$jsonrpl(body)", "$var(address)");
	if (jansson_get("result.Contacts[0].Contact.Received", "$jsonrpl(body)", "$var(received)")) {
		$var(outbound_proxy) = 0;
	} else {
		$var(outbound_proxy) = 1;
	}

	# uac 伪变量可参考这里: https://www.kamailio.org/wikidocs/cookbooks/devel/pseudovariables/#uac_reqkey
	$uac_req(method) = "MESSAGE";
	if ($var(outbound_proxy)) {
		$uac_req(ouri) = $var(received);
	}
	$uac_req(ruri) = $var(address);
	$uac_req(furi) = $var(from);
	$uac_req(turi) = $var(address);
	$uac_req(hdrs) = "Subject: Emergency Alert\r\n";
	$uac_req(hdrs) = $uac_req(hdrs) + "Content-Type: text/plain\r\n";
	$uac_req(body) = $var(text);
	$uac_req(evroute) = 1;                       # 触发 event_route[uac:reply]
	uac_req_send();                              # 发送
}

event_route[uac:reply]
{
	xinfo("===uac reply received, callid = $uac_req(callid), tu = $uac_req(turi), code = $uac_req(evcode)\n");
}

event_route [tm:local-request] {
	if ($rm == "MESSAGE") {
		xinfo("$ci|Routing locally generated $rm to $ru, callid = $ci\n");
		t_set_fr(1000, 10000);
	}
}

早期版本有个 BUG, $uac_req(callid) 最多只能到 128 字节,超过了就会崩溃,但早已修复。

uac_reg、uac_auth

uac_reg 是 Kamailio 作为 SIP 客户端 向 IPPBX(例如 FreeSWITCH)或者 SIP 代理服务器(例如 OpenSIPS)注册。

uac_auth 是 Kamailio 自己完成 SIP 认证(而不是转发 UAC 认证请求)。

《Kamailio实战》的第八章的第十一节对此已有很详细的介绍,这里仅补充几点:

  • l_uuid 是 uacreg 表的主键,不能包含逗号(“,”)、艾特(“@”) 等符号,但可以有下划线。逗号在 SIP 协议里面有专门的含义,不能用。艾特是 UAC 模块不让用
  • uacreg 表的 realm 字段可以是默认值(默认值是''),这样就会采用收到的 SIP 包里面的 realm 字段(一般是 401/407),减少发生冲突的可能性
  • uacreg 表的 contact_addr 字段可以是默认值(默认值是''),如果有特殊考虑,也可以进行配置,以便覆盖 UAC 模块的 reg_contact_addr 参数
  • UAC 模块发出的 REGISTER 请求,其 contact 头一般是 l_uuid@contact_addr,contact_addr 支持 ;transport=tcp

本人专门做过 uac_reg 的压力测试,稳定且效率高,值得您一试。

如何通过VRRP技术完成路由主备切换继续数据传送