Kamailio跟注册、认证以及用户位置有关的模块,常见的就是registrar、auth、auth_db以及usrloc等,尽管有官方手册,但是要熟练掌握是需要一个过程的。笔者在这里分享下使用经验,希望起到一个抛砖引玉的效果。
- multi-contacts和one-contact -
代码语言:javascript复制modparam("registrar", "max_contacts", 10)
这个模块参数限制同时注册的contact个数,如果contact个数大于10,那么register模块就会拒绝当前的注册请求(回503 Service Unavailable)。
max_contacts最大能配置多大呢?答案是跟全局参数max_branches有关,max_branches的默认值是12,最大能配置成32,max_contacts必须小于等于max_branches。
multi-contacts可能会拒绝新的注册请求,有的人可能不喜欢这个特性,那么registrar模块是否支持one-contact,也就是只要通过了认证,就接受新的注册请求,并把旧的contact全部清除呢?
当然可以!
看下面的路由块:
代码语言:javascript复制save("location", "0x04");
save函数的第二个参数改成0x04就行,registrar模块会自动删除旧的Contact,达到one-contact的效果。
- ul.dump里面的Address和received -
代码语言:javascript复制#!define FLT_NATS 5
#!define FLB_NATB 6
#!define FLB_NATSIPPING 7
route[NATDETECT] {
if (nat_uac_test("19")) {
if (is_method("REGISTER")) {
fix_nated_register();
} else {
if(is_first_hop()) {
set_contact_alias();
}
}
setflag(FLT_NATS);
}
return;
}
route[REGISTRAR] {
if (!is_method("REGISTER")) return;
if(isflagset(FLT_NATS)) {
setbflag(FLB_NATB);
# do SIP NAT pinging
setbflag(FLB_NATSIPPING);
}
if (!save("location")) {
sl_reply_error();
}
exit;
}
经常看到这样的路由代码,其中一个路由块是检测NAT,如果检查到了NAT,那么就要修改注册请求,并设置FLT_NATS事务标志。另外一个路由块是如果检查到有FLT_NATS事务标志,那么就设置FLB_NATB和FLB_NATSIPPING这两个分支标志。
代码语言:javascript复制kamcmd ul.dump
上面这个命令可以看到已注册用户的位置信息,这是一个例子:
代码语言:javascript复制{
AoR: 1001
Contacts: {
Contact: {
Address: sip:1001@172.92.27.106:9999;transport=udp;alias=120.229.53.16~51177~1
Expires: 2949
Q: -1
Call-ID: 61A34281A03EAD774B7CE87E3D1C009B@192.168.100.173
CSeq: 2
User-Agent: Kapanga Softphone Desktop Windows 1.00/2180b 1654855613_88A4C2D0E069_0A002700000D_508492987BAA_508492987BAB_528492987BAA
Received: sip:120.229.53.16:51177
Path: [not set]
State: CS_SYNC
Flags: 0
CFlags: 192
Socket: udp:192.168.100.173:5060
Methods: 16383
Ruid: uloc-6350e6b4-70a-1
Instance: [not set]
Reg-Id: 0
Server-Id: 0
Tcpconn-Id: -1
Keepalive: 0
Last-Keepalive: 1667477575
KA-Roundtrip: 0
Last-Modified: 1667477575
}
}
}
这个客户端是藏在NAT后面的,我们可以看到Address指向的是客户端的内网地址(来自客户端的Contact头),而alias参数明显是通过路由增加的。
Received是客户端的位置信息。
CFlags是分支标志,192 = (1 << 6) (1 << 7),也就是执行了下面两句路由:
代码语言:javascript复制setbflag(FLB_NATB); # 6
setbflag(FLB_NATSIPPING); # 7
现在再看INVITE的路由处理:
代码语言:javascript复制route[INVITE] {
if (!is_method("INVITE")) return;
if (!lookup("location")) {
sl_send_reply("404", "Not Found");
exit;
}
xinfo("ru = $ru, du = $du, bf = $bfn");
reccord_route();
t_relay();
exit;
}
lookup()执行成功之后,du等于ul里面的Received(等于是设置了Outbound代理),ru就等于ul里面的Address,
如果客户端没有藏在NAT后面,那么是什么情况呢?
ul里面CFlags为0(没有NAT),也没有Received字段,在lookup()执行成功之后,ru等于ul里面的Address(Kamailio可直达),du为空,
值得注意的是,由于网络和终端的复杂性,nat_uac_test("19")做NAT检测不一定准确,笔者推荐另外一种方式:
代码语言:javascript复制mhomed=1
listen=udp:MY_IP4_ADDR:MY_SIP_WAN_PORT advertise MY_IP4_PUBLIC_ADDR:MY_SIP_WAN_PORT
listen=udp:MY_IP4_ADDR:MY_SIP_LAN_PORT
把MY_SIP_WAN_PORT和MY_SIP_LAN_PORT分开。
NATDETECT路由块是:
代码语言:javascript复制route[NATDETECT] {
if ($Rp == MY_SIP_WAN_PORT) {
if (is_method("REGISTER")) {
fix_nated_register();
} else {
if(is_first_hop()) {
set_contact_alias();
}
}
setflag(FLT_NATS);
}
return;
}
检查$Rp是否等于MY_SIP_WAN_PORT,如果是,那么设置NAT标志,否则,就不设置。
- 怎样处理aU不等于fU -
先看下面这个REGISTER包:
代码语言:javascript复制REGISTER sip:106.14.57.231 SIP/2.0
Via: SIP/2.0/UDP 172.92.27.106:9999;branch=z9hG4bK984DAFB95BEE2B8589944BD21A423D16;rport
From: "1001" <sip:1001@106.14.57.231>;tag=C0EE54A33277C190C1ED8A7BE22EF360
To: "1001" <sip:1001@172.19.176.216>
Contact: <sip:1001@172.92.27.106:9999;transport=udp>
Call-ID: FE56FEB8FB5061DF3C90258DD36A1E00@106.14.57.231
Authorization: Digest username="07551001", realm="172.19.176.216", nonce="d80ed367-d6e3-41a3-80ac-5da1c93718fe", uri="sip:106.14.57.231", response="6468e08ea57894f1f46b0fa9c790df01", algorithm=MD5, cnonce="6363ab86aa9cf775", qop=auth, nc=00000001
User-Agent: Kapanga Softphone Desktop Windows 1.00/2180b 1654855613_88A4C2D0E069_0A002700000D_508492987BAA_508492987BAB_528492987BAA
Supported: timer, replaces
CSeq: 2 REGISTER
Max-Forwards: 70
Event: registration
Allow-Events: message-summary, registration
Expires: 3000
Allow: INVITE, INFO, PRACK, ACK, BYE, CANCEL, OPTIONS, NOTIFY, REGISTER, SUBSCRIBE, REFER, PUBLISH, UPDATE, MESSAGE
Content-Length: 0
这里fU是1001,但aU是07551001,二者不相等。
我们一般见到的处理认证的路由代码是:
代码语言:javascript复制route[AUTH] {
if (is_method("REGISTER") || from_uri==myself) {
# authenticate requests
if (!auth_check("$fd", "subscriber", "1")) {
auth_challenge("$fd", "0");
exit;
}
# user authenticated - remove auth header
if(!is_method("REGISTER|PUBLISH"))
consume_credentials();
}
# if caller is not local subscriber, then check if it calls
# a local destination, otherwise deny, not an open relay here
if (from_uri!=myself && uri!=myself) {
sl_send_reply("403","Not relaying");
exit;
}
return;
}
上面的auth_check()需要适时地调整成:
代码语言:javascript复制auth_check("$fd", "subscriber", "0")
也就是说,auth_check()函数的第三个参数要设置为0,也就是不再做aU是否等于fU的检查。
有些VoIP网关(比如潮流)习惯只用一个账号注册到SIP代理服务器,遇到INVITE挑战也会采用这个账号信息进行认证。Kamailio碰到这种场景就可以完美处理了。
- 自动unregister掉线的sip客户端 -
usrloc模块有ka机制,也就是Kamailio周期性的发sip ping给sip客户端,如果对方掉线了Kamailio没有收到回应,那么就自动unregister。
代码语言:javascript复制#!substdef "!MY_IP4_PUBLIC_ADDR!200.200.200.200!g"
modparam("usrloc", "timer_interval", 120)
modparam("usrloc", "ka_mode", 1)
modparam("usrloc", "ka_method", "OPTIONS")
modparam("usrloc", "ka_from", "sip:server@MY_IP4_PUBLIC_ADDR")
modparam("usrloc", "ka_domain", "MY_IP4_PUBLIC_ADDR")
modparam("usrloc", "ka_timeout", 20)
ka_mode可以配置的值有:
- 0 不使能ka机制
- 1 给所有的contact发送sip ping
- 2 只发给有nat_bflag分支标志的contact
- 4 只发给transport是udp的contact
除了上面的配置之外,还可以配置下面两个参数,处理transport是tcp的contact:
代码语言:javascript复制modparam("usrloc", "handle_lost_tcp", 1)
modparam("usrloc", "close_expired_tcp", 1)
至于wss的contact,可以考虑配置websocket模块的参数,使能websocket的ping/pong机制。
到底采用哪个机制,ka_mode要怎样配置?建议您搭建环境测试下,一定能找到适合自己的解决方案。
- 把SIP注册请求转发到 -
- 第三方SIP代理服务器或者IPPBX -
通过path模块可以很方便地把REGISTER请求转发出去,下面是一个简单的例子:
代码语言:javascript复制loadmodule "path.so"
loadmodule "dispatcher.so"
modparam("dispatcher", "list_file", "/etc/kamailio/dispatcher.list")
modparam("dispatcher", "ds_probing_mode", 3)
modparam("dispatcher", "flags", 2)
modparam("dispatcher", "xavp_dst", "_dsdst_")
代码语言:javascript复制route[REGISTRAR] {
if (!is_method("REGISTER")) return;
add_path_received(); # 增加path头
ds_select_dst("1", "4");
xinfo("--- SCRIPT: going to ru:$ru du:$du attrs:$xavp(_dsdst_=>attrs)n");
t_relay();
exit;
}
更好的做法是,在dispatcher.list的属性里面增加domain配置,比如:
代码语言:javascript复制# setid(int) destination(sip uri) flags(int,opt) priority(int,opt) attributes(str,opt)
1 sip:192.168.1.100:7060;transport=udp 8 0 duid=1;socket=udp:192.168.1.200;domain=xswitch.cn
然后在路由里面取到属性的domain参数,再修改rd、fd、
代码语言:javascript复制route[REGISTRAR] {
if (!is_method("REGISTER")) return;
add_path_received(); # 增加path头
ds_select_dst("1", "4");
$var(atts) = $xavp(_dsdst_=>attrs);
$var(domain) = $(var(atts){param.value,domain});
if (($var(domain) == $null) || ($var(domain) == "")) { # 如果没有配置domain参数,那么就取$dd
$var(domain) = $dd;
}
xinfo("--- SCRIPT: going to ru:$ru du:$du attrs:$xavp(_dsdst_=>attrs) domain:var(domain)n");
$rd = $var(domain);
$fd = $var(domain);
$td = $var(domain);
t_relay();
exit;
}
但如果IP PBX不支持path规范的话,那只能换下面这种方式了:
代码语言:javascript复制#!substdef "!MY_IP4_ADDR!192.168.1.101!g"
#!substdef "!MY_SIP_PORT!5060!g"
route[REGISTRAR] {
if (!is_method("REGISTER")) return;
remove_hf("Contact");
append_hf("Contact: <sip:$var(user)@MY_IP4_ADDR:MY_SIP_PORT;transport=udp;lhst=$sut;lm=midreg;bf=$bf>rn");
ds_select_dst("1", "4");
t_relay();
exit;
}
就是把Contact换成Kamailio自己,同时增加下面三个参数:
- lhst 就是ua的位置信息
- lm
- bf 分支标志
IP PBX呼叫user的时候会自动把INVITE请求发到Kamailio,并带回lhst、lm和bf等参数。
- 把用户的注册信息发到MQ Server -
代码语言:javascript复制loadmodule "jansson.so"
loadmodule "http_async_client.so"
loadmodule "registrar.so"
modparam("registrar", "xavp_rcd", "ulrcd")
modparam("registrar", "xavp_rcd_mask", 0)
route[REGISTRAR] {
if (!is_method("REGISTER")) return;
if (!save("location")) {
sl_reply_error();
}
$var(rc) = save("location");
jansson_set("string", "nottify", "usrloc", "$var(body)");
jansson_set("integer", "rc", $var(rc), "$var(body)");
jansson_set("string", "ulrcd", "$xavp(ulrcd[0]=>ruid)", "$var(body)");
jansson_set("string", "contact", "$xavp(ulrcd[0]=>contact)", "$var(body)");
jansson_set("string", "received", "$xavp(ulrcd[0]=>received)", "$var(body)");
jansson_set("string", "expires", "$xavp(ulrcd[0]=>expires)", "$var(body)");
jansson_set("string", "path", "$xavp(ulrcd[0]=>path)", "$var(body)");
$http_req(method) = "POST";
$http_req(hdr) = "Content-Type: application/json";
$http_req(suspend) = 0;
$http_req(body) = $var(body);
http_async_query("http://192.168.1.200:8080/event", "HTTP_REPLY");
exit;
}
route[HTTP_REPLY] {
if ($http_ok) {
xinfo("route[HTTP_REPLY]: status $http_rsn");
xinfo("route[HTTP_REPLY]: body $http_rbn");
} else {
xinfo("route[HTTP_REPLY]: error $http_err)n");
}
}
上面的代码比较简单直观,无需过多解释,需要注意的是save()函数的返回值:
- -2,错误,太多的contact
- -1,一般性错误
- 1, 成功,新注册
- 2, 成功,续注册
- 3, 成功,注销
以上讨论都是基于Kamailio 5.5.x版本,早期版本可能略有差异。