关于Kamailio registrar、auth、usrloc等模块的补充说明

2023-02-28 12:30:29 浏览数 (2)

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版本,早期版本可能略有差异。

0 人点赞