2014/09/09

opensips - connect to gateway

如果整個 SIP 環境要跟傳統電話交換機連接,必須要有 voice gateway 處理這個問題,而 opensips 要做的,就是提供 SIP 界面跟其他 voice gateway 整合。

我們設想一個情境,有兩個 edge 端分別用 asterisk 當作 voice gateway,這兩個端點之間,可以直接互相設定 sip trunk。但最好的方式,是在中間增加一個 opensips,充當 SIP Proxy Server 的功能,只針對受話號碼的 prefix 形式來判斷,要將電話話務轉送給那一個 gateway 處理。

在中間的 SIP Proxy Server 單純地只處理 SIP 的封包, 最重要的就是 INVITE,在轉送話務後,讓兩個端點直接對送 RTP 資料。

osipsconfig

先前我們使用 opensips 承擔了 registration server 的工作,但在這個情境中,並不需要註冊的功能,所以我們先使用 osipsconfig 產生新的設定檔。

我們選擇了 Trunking Script,並只勾選 USE_DIALPLAN, USE_DIALOG 這兩個項目。

為了要對 script 除錯,我們可以使用 xlog() 這個函數,當 opensips 收到 SIP 封包,就會執行 routing script,並將 xlog 的資訊列印到 log file 裡面。

xlog 的第一個參數可有有無,可填上以下的 log level,將來就能直接在 opensips.cfg 的 debug=3 裡面決定 log 的資訊等級。

L_ALERT - log level -3
L_CRIT - log level -2
L_ERR - log level -1
L_WARN - log level 1
L_NOTICE - log level 2
L_INFO - log level 3
L_DBG - log level 4

獨立的 log file

opensips使用syslog服務,預設安裝的情況下,log內容會寫入 /var/log/message 這個文件,如果希望使用獨立的log文件,可用以下的指令設定。

touch /var/log/opensips.log
vi /etc/rsyslog.conf –> 增加一行:local0.* /var/log/opensips.log
/etc/init.d/rsyslog restart

針對 opensips.log 檔案,我們再用 logrotate 避免 log file 太大。

> vi /etc/logrotate.d/opensips.logrotate
/var/log/opensips.log {
   missingok
   rotate 5
   daily
   create 0640 root root
}

senario

asterisk: 192.168.1.5
asterisk: 192.168.1.17
opensips: 192.168.1.24

兩個 asterisk 分別設定 siptrunk 指定 host 為 192.168.1.24,另外再以電話號碼 prefix 來區分,prefix 為 2 的分機是由 192.168.1.17 負責,prefix 為 1 是由 192.168.1.5 負責。

osipsconfig - Trunking Script

我們只單純需要 opensips 扮演 SIP Proxy 的角色,因此就先在 osipsconfig 的 Trunking Script,只勾選 USE_DIALPLAN, USE_DIALOG 兩個選項,然後就 Generate Trunking Script 產生設定檔,檔名的形式為 opensips_trunking_2014-5-26_10:6:51.cfg,中間的日期及時分秒就是產生檔案的時間。

把該設定檔替換為 opensips.cfg,然後就能再進行下一步的設定修改,設定檔分為以下三個部份。

global configuration

要修改 listen=udp:192.168.1.24:5060,並增加一行 db_default_url="mysql://opensips:opensipsrw@localhost/opensips" 。

####### Global Parameters #########

debug=3
log_stderror=no
log_facility=LOG_LOCAL0

fork=yes
children=4

/* uncomment the following lines to enable debugging */
#debug=6
#fork=no
#log_stderror=yes

/* uncomment the next line to enable the auto temporary blacklisting of 
   not available destinations (default disabled) */
#disable_dns_blacklist=no

/* uncomment the next line to enable IPv6 lookup after IPv4 dns 
   lookup failures (default disabled) */
#dns_try_ipv6=yes

/* comment the next line to enable the auto discovery of local aliases
   based on revers DNS on IPs */
auto_aliases=no


listen=udp:192.168.1.24:5060   # CUSTOMIZE ME


disable_tcp=yes

disable_tls=yes

db_default_url="mysql://opensips:opensipsrw@localhost/opensips"

module configuration

修改 module path
mpath="/usr/lib64/opensips/modules/"

然後注意 db_url 的密碼的地方,根據自己的設定修改密碼
modparam("drouting", "db_url",
"mysql://opensips:opensipsrw@localhost/opensips") # CUSTOMIZE ME

####### Modules Section ########

#set module path
mpath="/usr/lib64/opensips/modules/"


#### SIGNALING module
loadmodule "signaling.so"

#### StateLess module
loadmodule "sl.so"

#### Transaction Module
loadmodule "tm.so"
modparam("tm", "fr_timer", 5)
modparam("tm", "fr_inv_timer", 30)
modparam("tm", "restart_fr_on_each_reply", 0)
modparam("tm", "onreply_avp_mode", 1)

#### Record Route Module
loadmodule "rr.so"
/* do not append from tag to the RR (no need for this script) */
modparam("rr", "append_fromtag", 0)

#### MAX ForWarD module
loadmodule "maxfwd.so"

#### SIP MSG OPerations module
loadmodule "sipmsgops.so"

#### FIFO Management Interface
loadmodule "mi_fifo.so"
modparam("mi_fifo", "fifo_name", "/tmp/opensips_fifo")
modparam("mi_fifo", "fifo_mode", 0666)

#### URI module
loadmodule "uri.so"
modparam("uri", "use_uri_table", 0)

#### MYSQL module
loadmodule "db_mysql.so"

#### AVPOPS module
loadmodule "avpops.so"

####  DYNAMIC ROUTING module
loadmodule "drouting.so"
modparam("drouting", "db_url",
    "mysql://opensips:opensipsrw@localhost/opensips") # CUSTOMIZE ME

####  PERMISSIONS module
loadmodule "permissions.so"
modparam("permissions", "db_url",
    "mysql://opensips:opensipsrw@localhost/opensips") # CUSTOMIZE ME

#### ACCounting module
loadmodule "acc.so"
/* what special events should be accounted ? */
modparam("acc", "early_media", 0)
modparam("acc", "report_cancels", 0)
/* by default we do not adjust the direct of the sequential requests.
   if you enable this parameter, be sure the enable "append_fromtag"
   in "rr" module */
modparam("acc", "detect_direction", 0)
modparam("acc", "failed_transaction_flag", "ACC_FAILED")
/* account triggers (flags) */
modparam("acc", "log_flag", "ACC_DO")
modparam("acc", "log_missed_flag", "ACC_MISSED")


#### DIALOG module
loadmodule "dialog.so"
modparam("dialog", "dlg_match_mode", 1)
modparam("dialog", "default_timeout", 21600)  # 6 hours timeout
modparam("dialog", "db_mode", 2)
modparam("dialog", "db_url",
    "mysql://opensips:opensipsrw@localhost/opensips") # CUSTOMIZE ME

####  DIALPLAN module
loadmodule "dialplan.so"
modparam("dialplan", "db_url",
    "mysql://opensips:opensipsrw@localhost/opensips") # CUSTOMIZE ME

routing configuration

把 if ( check_source_address("1","$avp(trunk_attrs)") ) { ... } 這個部份加上 # 註解掉。

把 if ( !isflagset(IS_TRUNK) ) { ... } 這個部份加上 # 註解掉。

在 record_routing 的前面,增加一段 URI 的檢查

    # 增加 URI 的檢查
    if ( uri == myself ) {
        if(is_method("INVITE") && !has_totag() && uri=~"sip:.*") {
            route(home);
        }
    }

增加subroute: route[home],就是以 prefix 號碼來決定要不要 rewritehostport,如果機器的 port 不是預設的 5060,而是 5070,就改成 192.168.1.17:5070。

# 增加 route[home]
route[home] {
    if (uri=~"^sip:2[0-9]{3}@") {
        # uri 開頭為 2,共 4 碼
        rewritehostport("192.168.1.17");
    } else if(uri=~"^sip:[1][0-9].*") {
        # uri 開頭為 1
        rewritehostport("192.168.1.5");
    }
    route(RELAY);
}

完整的 script 內容如下

####### Routing Logic ########

# main request routing logic

route{

    if (!mf_process_maxfwd_header("10")) {
        sl_send_reply("483","Too Many Hops");
        exit;
    }

#    if ( check_source_address("1","$avp(trunk_attrs)") ) {
#        # request comes from trunks
#        setflag(IS_TRUNK);
#    } else if ( is_from_gw() ) {
#        # request comes from GWs
#    } else {
#        send_reply("403","Forbidden");
#        exit;
#    }

    if (has_totag()) {
        # sequential request withing a dialog should
        # take the path determined by record-routing
        if (loose_route()) {

            # validate the sequential request against dialog
            if ( $DLG_status!=NULL && !validate_dialog() ) {
                xlog("In-Dialog $rm from $si (callid=$ci) is not valid according to dialog\n");
                ## exit;
            }

            if (is_method("BYE")) {
                setflag(ACC_DO); # do accounting ...
                setflag(ACC_FAILED); # ... even if the transaction fails
            } else if (is_method("INVITE")) {
                # even if in most of the cases is useless, do RR for
                # re-INVITEs alos, as some buggy clients do change route set
                # during the dialog.
                record_route();
            }

            # route it out to whatever destination was set by loose_route()
            # in $du (destination URI).
            route(RELAY);
        } else {
            if ( is_method("ACK") ) {
                if ( t_check_trans() ) {
                    # non loose-route, but stateful ACK; must be an ACK after 
                    # a 487 or e.g. 404 from upstream server
                    t_relay();
                    exit;
                } else {
                    # ACK without matching transaction ->
                    # ignore and discard
                    exit;
                }
            }
            sl_send_reply("404","Not here");
        }
        exit;
    }

    #### INITIAL REQUESTS

#    if ( !isflagset(IS_TRUNK) ) {
#        ## accept new calls only from trunks
#        send_reply("403","Not from trunk");
#        exit;
#    }

    # CANCEL processing
    if (is_method("CANCEL")) {
        if (t_check_trans())
            t_relay();
        exit;
    } else if (!is_method("INVITE")) {
        send_reply("405","Method Not Allowed");
        exit;
    }

    if ($rU==NULL) {
        # request with no Username in RURI
        sl_send_reply("484","Address Incomplete");
        exit;
    }

    t_check_trans();

    # preloaded route checking
    if (loose_route()) {
        xlog("L_ERR",
        "Attempt to route with preloaded Route's [$fu/$tu/$ru/$ci]");
        if (!is_method("ACK"))
            sl_send_reply("403","Preload Route denied");
        exit;
    }

    # 增加 URI 的檢查
    if ( uri == myself ) {
        if(is_method("INVITE") && !has_totag() && uri=~"sip:.*") {
            route(home);
        }
    }

    # record routing
    record_route();

    setflag(ACC_DO); # do accounting


    # create dialog with timeout
    if ( !create_dialog("B") ) {
        send_reply("500","Internal Server Error");
            exit;
    }





    # apply transformations from dialplan table
    dp_translate("0","$rU/$rU");

    # route calls based on prefix
    if ( !do_routing("1") ) {
        send_reply("404","No Route found");
        exit;
    }

    t_on_failure("GW_FAILOVER");

    route(RELAY);
}


route[RELAY] {
    if (!t_relay()) {
        sl_reply_error();
    };
    exit;
}

# 增加 route[home]
route[home] {
    if (uri=~"^sip:2[0-9]{3}@") {
        # uri 開頭為 2,共 3 碼
        rewritehostport("192.168.1.17");
    } else if(uri=~"^sip:[1][0-9].*") {
        # uri 開頭為 1
        rewritehostport("192.168.1.5");
    }
    route(RELAY);
}

failure_route[GW_FAILOVER] {
    if (t_was_cancelled()) {
        exit;
    }

    # detect failure and redirect to next available GW
    if (t_check_status("(408)|([56][0-9][0-9])")) {
        xlog("Failed GW $rd detected \n");

        if ( use_next_gw() ) {
            t_on_failure("GW_FAILOVER");
            t_relay();
            exit;
        }

        send_reply("500","All GW are down");
    }
}


local_route {
    if (is_method("BYE") && $DLG_dir=="UPSTREAM") {

        acc_log_request("200 Dialog Timeout");

    }
}

小結

這個方式,很單純地直接修改 opensips.cfg,並直接將 routing 的條件寫在設定檔中,接下來應該要嘗試使用 dynamic routing module。

2014/09/01

opensips 簡介 2/2

接續上一篇,這裡要看 opensips.cfg 裡面的 routing 區塊該怎麼寫。如何處理 routing 是 opensips 裡面最難懂的部份。

Routing Basics

Routing requests and replies

opsnsips script 裡面處理的 routing 機制,通常是用來處理 inter-domain calls,我們可用 DNS server 來尋找 destination address,而 intra-domain calls 則是使用 user location table 來處理 routing。replies 是利用 request 裡面的 VIA header。對 statefule routing 來說,transaction 是使用 VIA 裡面的 branch 參數來做對應。

範例:sip proxy server 為 192.168.1.201:5060,Peer 1000 為 192.168.1.159:39132,Peer 1001 為 192.168.1.159:5060。

在 VIA header 裡面就有足夠的資訊,可以將 reply 送回去,另一個重要的參數是 branch,這是用來在 stateful mode識別 transaction 的資訊,received 與 rport 用在 RFC 3581 處理 NAT traversal。

  1. Peer 1000 -> INVITE -> Proxy
    From 192.168.1.159:39132 -> 192.168.1.201:5060
     INVITE sip:1001@192.168.1.201 SIP/2.0.
     Via: SIP/2.0/UDP 192.168.1.159:39132;branch=z9hG4bK-d87543-f467f33a206c333a-1--d87543-;rport.
     ...
  2. Proxy -> INVITE -> Peer 1001
    From 192.168.1.201:5060 -> 192.168.1.159:5060
     INVITE sip:1001@192.168.1.159:5060;rinstance=a1d5fa7ecfde6278;transport=UDP SIP/2.0.
     Via: SIP/2.0/UDP 192.168.1.201;branch=z9hG4bKf5b7.34401122.0.
     Via: SIP/2.0/UDP 192.168.1.159:39132;received=192.168.1.159;branch=z9hG4bK-d87543-f467f33a206c333a-1--d87543-;rport=39132.
     ...
  3. Peer 1001 -> 200 OK -> Proxy
    From 192.168.1.159:5060 -> 192.168.1.201:5060
     SIP/2.0 200 OK.
     Via: SIP/2.0/UDP 192.168.1.201;branch=z9hG4bKf5b7.34401122.0.
     Via: SIP/2.0/UDP 192.168.1.159:39132;received=192.168.1.159;branch=z9hG4bK-d87543-f467f33a206c333a-1--d87543-;rport=39132.
     ...
  4. Proxy -> 200 OK -> Peer 1000
    From 192.168.1.201:5060 -> 192.168.1.159:39132
     SIP/2.0 200 OK.
     Via: SIP/2.0/UDP 192.168.1.159:39132;received=192.168.1.159;branch=z9hG4bK-d87543-f467f33a206c333a-1--d87543-;rport=39132.
     ...
Initial and sequential requests

要區分 initial 與 sequential requests 的差異,這兩種 request 的 routing logic 不同。

initial requests: routed based on discovery mechanism,通常是 location table 或 DNS,initial request 會紀錄相關的 SIP proxy hops。

initial request 的 TO header 裡面不會有 TAG parameter。

根據 caller, callee 的不同,使用的 routing mechanism 可能是下列幾項中的一項:enum, aliases, dns, user location 或其他方式。

  1. Peer 1000 -> INVITE -> Proxy
    From 192.168.1.159:39132 -> 192.168.1.201:5060
     INVITE sip:1001@192.168.1.201 SIP/2.0.
     Contact: <sip:1000@192.168.1.159:39132>.
     ...
  2. Proxy -> INVITE -> Peer 1001
    From 192.168.1.201:5060 -> 192.168.1.159:5060
     INVITE sip:1001@192.168.1.159:5060;rinstance=a1d5fa7ecfde6278;transport=UDP SIP/2.0.
     Record-Route: <sip:192.168.1.201;lr>.
     Contact: <sip:1000@192.168.1.159:39132>.
     ...
  3. Peer 1001 -> 200 OK -> Proxy
    From 192.168.1.159:5060 -> 192.168.1.201:5060
     SIP/2.0 200 OK.
     Record-Route: <sip:192.168.1.201;lr>.
     Contact: <sip:1001@192.168.1.159:5060;rinstance=a1d5fa7ecfde6278;transport=UDP>.
  4. Proxy -> 200 OK -> Peer 1001
    From 192.168.1.201:5060 -> 192.168.1.159:39132
     SIP/2.0 200 OK.
     Record-Route: <sip:192.168.1.201;lr>.
     Contact: <sip:1001@192.168.1.159:5060;rinstance=a1d5fa7ecfde6278;transport=UDP;nat=yes>.

sequential requests: routed based on initial requests 上的資訊,收集到的 routes 資訊稱為 route set,在 script 中,可使用 loose_route() 函數來利用 route set 處理 routing。

可利用 TO header 裡面的 TAG 參數來區分 initial 與 sequential requests。

sequential requests 是利用 Route header 與 URI 來做 routing,換句話說,就是 UAC 收到由 Record-Route 與 Contact headers 產生的 route set。

當 client 發現了 route set,就會 mirror Contact header 的 request URI 還有 Route header 的 Record-Route。對Proxy server 來說,使用 loose_route function 重新利用 location table 或 DNS 尋找 destination 這樣處理速度會比較快。

  1. Peer 1000 -> ACK -> Proxy
    From 192.168.1.159:39132 -> 192.168.1.201:5060
     ACK sip:1001@192.168.1.159:5060;rinstance=a1d5fa7ecfde6278;transport=UDP;nat=yes SIP/2.0.
     Route: <sip:192.168.1.201;lr>.
     ...
  2. Proxy -> ACK -> Peer 1001
    From 192.168.1.201:5060 -> 192.168.1.159:5060
     ACK sip:1001@192.168.1.159:5060;rinstance=a1d5fa7ecfde6278;transport=UDP;nat=yes SIP/2.0.
Sample route script

整個 routing script 是包括在 route{ } 區塊裡面。

第一個部份是檢查是不是超過 10 個 SIP Proxy hops。mf_process_maxfwd_header這個 function 是由 maxfwd.so 這個 module 提供(script 的 modules 區塊必須要 loadmodule "maxfwd.so"),可避免 SIP message loops。

sl_send_reply 是由 stateless (sl.so) 提供,用來傳送 stateless request 給 SIP client,這表示 opensips 並不會等待 message ack。

exit 是告訴 opensips 結束 request processing。

    if (!mf_process_maxfwd_header("10")) {
        sl_send_reply("483","Too Many Hops");
        exit;
    }

第二個部份是處理 TO header。這表示這是一個 sequential request,通常會用 loose_route 提供 routing,如果遇到 BYE, CANCEL 就會 forward 此訊息。

有 To 但是沒有 ;lr 將會被認為是 error message 而被丟棄。

如果遇到 ACK 不符合任何一個 transaction,也會直接被丟棄。

    if (has_totag()) {
        # sequential request withing a dialog should
        # take the path determined by record-routing
        if (loose_route()) {

            # validate the sequential request against dialog
            if ( $DLG_status!=NULL && !validate_dialog() ) {
                xlog("In-Dialog $rm from $si (callid=$ci) is not valid according to dialog\n");
                ## exit;
            }

            if (is_method("BYE")) {
                setflag(ACC_DO); # do accounting ...
                setflag(ACC_FAILED); # ... even if the transaction fails
            } else if (is_method("INVITE")) {
                # even if in most of the cases is useless, do RR for
                # re-INVITEs alos, as some buggy clients do change route set
                # during the dialog.
                record_route();
            }



            # route it out to whatever destination was set by loose_route()
            # in $du (destination URI).
            route(relay);
        } else {

            if ( is_method("ACK") ) {
                if ( t_check_trans() ) {
                    # non loose-route, but stateful ACK; must be an ACK after 
                    # a 487 or e.g. 404 from upstream server
                    t_relay();
                    exit;
                } else {
                    # ACK without matching transaction ->
                    # ignore and discard
                    exit;
                }
            }
            sl_send_reply("404","Not here");
        }
        exit;
    }

第三部份是處理 CANCEL,我們不需要自己處理 routing,因為 CANCEL 是屬於某個 INVITE transaction,所以就只要 t_check_trans(),然後直接 t_relay()。

接下來獨立的 t_check_trans() 是為了要檢查是否屬於某個 INVITE transaction,這個檢查可以在 request restransmission 時,停止繼續執行 script。

    # CANCEL processing
    if (is_method("CANCEL"))
    {
        if (t_check_trans())
            t_relay();
        exit;
    }

    t_check_trans();

這個部份是處理 non-register requests。

    if ( !(is_method("REGISTER")  ) ) {

        if (from_uri==myself)

        {

            # authenticate if from local subscriber
            # authenticate all initial non-REGISTER request that pretend to be
            # generated by local subscriber (domain from FROM URI is local)
            if (!proxy_authorize("", "subscriber")) {
                proxy_challenge("", "0");
                exit;
            }
            if (!db_check_from()) {
                sl_send_reply("403","Forbidden auth ID");
                exit;
            }

            consume_credentials();
            # caller authenticated

        } else {
            # if caller is not local, then called number must be local

            if (!uri==myself) {
                send_reply("403","Rely forbidden");
                exit;
            }
        }

    }

這個部份是處理沒有 TO header 但卻有 Route 的 request,如果發現這種封包,除了 ACK 之外,就直接丟棄。

    # preloaded route checking
    if (loose_route()) {
        xlog("L_ERR",
        "Attempt to route with preloaded Route's [$fu/$tu/$ru/$ci]");
        if (!is_method("ACK"))
            sl_send_reply("403","Preload Route denied");
        exit;
    }

如果 request 目標 server 不是自己,就用 record_route() 紀錄 routes。

    # record routing
    if (!is_method("REGISTER|MESSAGE"))
        record_route();

把 INVITE request 貼上 ACC_DO 要處理 accounting 的標籤。

    # account only INVITEs
    if (is_method("INVITE")) {

        # create dialog with timeout
        if ( !create_dialog("B") ) {
            send_reply("500","Internal Server Error");
            exit;
        }

        setflag(ACC_DO); # do accounting
    }

處理不是由自己服務的 domain 的 request,這裡預設是以 open relay 的方式運作。

將 request forward 到其他 proxy 的處理過程,將會在後續章節裡面討論。

    ## replace with following line if multi-domain support is used
    ##if (!is_uri_host_local())
    if (!uri==myself) {
        append_hf("P-hint: outbound\r\n"); 
        # if you have some interdomain connections via TLS
        ##if($rd=="tls_domain1.net") {
        ## t_relay("tls:domain1.net");
        ## exit;
        ##} else if($rd=="tls_domain2.net") {
        ## t_relay("tls:domain2.net");
        ## exit;
        ##}
        route(relay);
    }

可以自己決定要不要提供 presence 功能,改成 route(2) 就能提供 presense agent 的功能。

    ## uncomment this if you want to enable presence server
    ## and comment the next 'if' block
    ## NOTE: uncomment also the definition of route[presence] from below
    ##if( is_method("PUBLISH|SUBSCRIBE"))
    ## route(2);
    if (is_method("PUBLISH|SUBSCRIBE"))
    {
        sl_send_reply("503", "Service Unavailable");
        exit;
    }

如果是 REGISTER request,就可用 www_authorize, db_check_to 進行使用者驗證,驗證通過後,就儲存 AOR 至 location table。

    if (is_method("REGISTER"))
    {

        # authenticate the REGISTER requests
        if (!www_authorize("", "subscriber"))
        {
            www_challenge("", "0");
            exit;
        }

        if (!db_check_to()) 
        {
            sl_send_reply("403","Forbidden auth ID");
            exit;
        }

        if (   0 ) setflag(TCP_PERSISTENT);

        if (!save("location"))
            sl_reply_error();

        exit;
    }

丟棄沒有完整 URI 的 requests。
alias_db_lookup 可查詢 alias,例如 1000@mydomain.com 會跟 boss@mydomain.com 一樣。

    if ($rU==NULL) {
        # request with no Username in RURI
        sl_send_reply("484","Address Incomplete");
        exit;
    }

    # apply DB based aliases (uncomment to enable)
    ##alias_db_lookup("dbaliases");

在 localtion db 尋找 AOR(address of record),參數 m 是條件過濾。不存在時,就回傳 404 Not Found。進一步搜尋 URI,不存在時,就回傳 420 Bad Entension。

    # do lookup with method filtering
    if (!lookup("location","m")) {
        if (!db_does_uri_exist()) {
            send_reply("420","Bad Extension");
            exit;
        }

        t_newtran();
        t_reply("404", "Not Found");
        exit;
    }

到最後,就進行 relay 的 subroute。

    # when routing via usrloc, log the missed calls also
    setflag(ACC_MISSED);
    route(relay);

這個部份是 relay subroute。t_relay 可基於 request URI 將 forward request statefully,這是由 TM (tm.so) module 提供,負責發送 request 與處理 resneds, responses。如果 t_relay 沒有成功送到 destination,就會回傳 error。

route[relay] {
    # for INVITEs enable some additional helper routes
    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;
}

這是 presence agent 的範例。

# Presence route
/* uncomment the whole following route for enabling presence
NOTE: do not forget to enable the call of this route from the main
route */
##route[presence]
##{
##    if (!t_newtran())
##    {
##        send_reply("500","Internal Error");
##        exit;
##    };
##
##    if(is_method("PUBLISH"))
##    {
##        handle_publish();
##        t_release();
##    }
##    else
##    if( is_method("SUBSCRIBE"))
##    {
##        handle_subscribe();
##        t_release();
##    }
##
##    exit;
##}

branch_route, onreply_route, failure_route, local_route 最後是這四個 subroutes。

branch_route 是由 TM module 提供,在 route 裡面呼叫 t_on_branch[1],就可以進入 branch_route[1]。

failure_route 是由 TM module 提供,也就是 failed transaction routing block,當 transaction 在任一個 branch 收到 >=300 的 reply 時,就會進入 failure_route。

onreply_route: reply routing block,這會在收到任一個 reply 時執行,他會處理 reply 的訊息,預設會發送 reply 給 caller side。

local_route: 當 TM 內部產生一個新的 SIP request (internal UAC request) 時就會 trigger local_route。

branch_route[per_branch_ops] {
    xlog("new branch at $ru\n");
}

onreply_route[handle_nat] {

    xlog("incoming reply\n");
}


failure_route[missed_call] {
    if (t_was_cancelled()) {
        exit;
    }

    # uncomment the following lines if you want to block client 
    # redirect based on 3xx replies.
    ##if (t_check_status("3[0-9][0-9]")) {
    ##t_reply("404","Not found");
    ##    exit;
    ##}
}

local_route {
    if (is_method("BYE") && $DLG_dir=="UPSTREAM") {

        acc_db_request("200 Dialog Timeout", "acc");

    }
}

2014/08/25

opensips 簡介 1/2

opensips 在SIP 環境的定位是提供 registrar, proxy, redirect, location services。最重要的是,可高速處理 sip headers 並提供封包的 routing 功能。其他的部份,例如 NAT traversal, IMS, load balancing 等等功能則是由 3rd party modules 提供。

registrar 可管理某個 domain 下的 SIP UA,讓這些 UA 註冊並受該 server 管理。

proxy server的用途是,接收 UA 的 request,當受話端不在同一個 domain 時,就把 request 轉送到另一個 SIP proxy server。

redirect server 接收 request 之後,直接以 302 Moved Temporarily 回應給 UA。

opensips history

在 2004 年,德國 FhG Fokus research institute 以 GPL 發布了 SER,其後 OpenSERs fork 該專案,開發 SER 的單位輾轉成立了 Voice System 公司,繼續維護 OpenSER。

OpenSERs 在 2008 年又經歷了一次分裂,成了兩個專案:Kamailio 與 OpenSIPs。opensips 核心非常小,但以上百個 modules 來延伸其功能,目前可以用來提供 SIP firewals, session border controllers(SBCs), load balancers 等等功能。

以下列出重要的 module 與功能

  1. dispatcher, path: 這兩個模組用來提供 load balancing 的功能
  2. mediaproxy, rtpproxy, nathelper: 提供 NAT traversal 的功能
  3. presence: 處理 presense server
  4. IMC, XMPP: instant messaging

安裝 opensip 的程序,可參閱這篇OpenSIPS and Control Panel Install Guide,雖然版本有點差異,安裝過程可套用在目前的版本 opensips 1.11.1, opensips-cp 5.0 上。

opensips.cfg

該設定檔可分成三大區塊: Global Configuration, Modules, Routing Logic。

# Global Configurations
debug=3
log_stderror=no
log_facility=LOG_LOCAL0

fork=yes
children=4

# Modules Section
mpath="/usr/lib64/opensips/modules/"

### Module Loading: 用 loadmodule 載入 modules
loadmodule "signaling.so"
loadmodule "sl.so"
loadmodule "tm.so"

### Module Specific Parameters
modparam("tm", "fr_timer", 5)
modparam("tm", "fr_inv_timer", 30)
modparam("tm", "restart_fr_on_each_reply", 0)
modparam("tm", "onreply_avp_mode", 1)

# Routing Logic
### Main routing block: SIP request 處理的起點,每個 request 都會經過這裡

### Secondary routing blocks: 可定義 route(),用起來類似 subroutines

### Reply routing blocks: 處理 reply messages, 包含 provisional, successfule final replies, negative final replies,通常是 200 OK

### Failure routes blocks: 處理錯誤狀況, 例如 busy or timeout

### Branch route blocks: 在把 request forward 出去之前,每一條 branch 都可執行一段 logic 

### Local routing blocks: 當 opensips 內部產生 request (通常只作為 UAS),就使用 Transaction Module(TM) 

### Error routing block: 當 sip request parsing error 發生時,就執行這個

Sessions, dialogs, and transactions

在處理 opensips processing 之前,我們要先知道這三個名詞的意思

  1. SIP transaction: A SIP request,包含了 resends 與 direct responses,也就是 REGISTER 與 200 OK

  2. SIP dialog: 兩個 SIP entities 在某個時間點,存在的相互關係,換句話說,就是在兩個 UACs 之間,從 INVITE 到 BYE 之間,建立的一個對話

  3. SIP Session: 在兩個 SIP entities 之間傳送的 media flow,可能是 audio/video/text

SIP proxy: stateless or stateful

stateless 的作法,proxy server 會在 forward 訊息之後,把所有 message 的 internal information 丟棄。單純地只會把資料轉送到 request 裡面提供的下一個節點。

如果需要提供 billing, call forwarding, voicemail 功能,就需要使用 stateful mode,每個 transaction 都需要存在記憶體中,並處理 failure, response, retransmissions 等等狀況。這是 Transaction Module(TM) module 提供的功能。

要注意,stateful 的狀態維護是針對 transaction 處理而不是 dialog,因此從 INVITE 到 200 OK response 的處理是 stateful,而不是從 INVITE 到 BYE request,這個是 dialog。

stateful operation 的處理過程

Script and Routing Basics

一般安裝好 opensips 之後,沒有調整什麼特別的設定的時候,設定檔會放在 /usr/etc/opensips 這個目錄中,我們可以自己做個 link,改用 /etc/opensips 這個目錄。

根據上面 opensips.cfg 那個段落的內容,這個設定檔裡面可分為 (1) Global Parameters (2) Load Modules (3) Module Parameters (4) Routing Script 這四個部份。

將 opensips 設定完成後,最基本可以達成以下任務。

  1. 建立一個 SIP Server
  2. 可讓內部網路的 UACs 連接
  3. 在不同 UACs 之間互相撥打電話
  4. 話機不需要驗證,不使用資料庫
  5. 不支援 PSTN,只能在話機之間通話

Global Parameters

Listen Interfaces

不設定 listen 時,系統將會自動綁定所有的網路界面。

port 參數只是針對 SIP Server 設定 UDP service port

listen=udp:192.168.152.148:5060
listen=tcp:192.168.152.148:5061
listen=tls:192.168.152.148:5062
port=5060
Logging

在正式環境,就設定 debug=3,在 debug 環境,就設定 debug=9。

可以用這個 MI command 調整 log level

opensipsctl fifo debug 1

在 opensips.cfg 裡面的 log 相關設定範例如下

debug=3
log_stderror=no
log_facility=LOG_LOCAL0

Number of processes

opensips process 可運作在 foreground 或background,可設定 fork=yes,指定運作在 background。如果設定運作在 foreground,opensips就無法 listen 多個網路界面,tcp與tls 也會自動 disabled。

children 是設定每一個網路界面有幾個 processes 提供服務。

fork=yes
children=4
tcp_children=6
disable_tcp=no
disable_tls=no
Daemon options
gid/group=sip # unix group
uid/user=sip # unix user
wdir="/" # working directory
chroot="/usr/local/opensips-1.6"
SIP identity

設定 SIP request/response 裡面的標準參數。

server_header="Server: My openSIpS 
#default is "openSIpS (<version> (<arch>/<os>))"
server_signature = yes
user_agent_header="User-Agent: My openSIpS
其他

alias 很重要,這是設定 SIP server 支援的 domain name。

alias="mydomain.sip" # to set alias hostnames for the server
auto_aliases=no # discover aliases via reversed DNS
disable_dns_failover = yes
sip_warning=yes #add a debugging header in replies
global parameters 區塊
####### Global parameters #########
debug=3      # set the debug leve to 3
log_stderror=no    # log to syslog
log_facility=LoG_LoCAL0  # Log to facility LoG_LoCAL0
fork=yes      # Run as a daemon
children=4     # open 4 child process for 
each UDpaddress

/* uncomment the next line to disable TCp(default on) */
#disable_tcp=yes

/* uncomment the next line to enable the auto temporary blacklisting of not available destinations (default disabled) */
#disable_dns_blacklist=no

/* uncomment the next line to enable Ipv6 lookup after Ipv4 dns lookup failures (default disabled) */
#dns_try_ipv6=yes

/* uncomment the next line to disable the auto discovery of local aliases based on revers DNS on Ips (default on) */
#auto_aliases=no

/* uncomment the following lines to enable TLS support (default off) */
#disable_tls = no
#listen = tls:your_Ip:5061
#tls_verify_server = 1
#tls_verify_client = 1
#tls_require_client_certificate = 0
#tls_method = TLSv1
#tls_certificate = "//etc/opensips/tls/user/user-cert.pem"
#tls_private_key = "//etc/opensips/tls/user/user-privkey.pem"
#tls_ca_list = "//etc/opensips/tls/user/user-calist.pem"
port=5060 # Run on port 5060

/* uncomment and configure the following line if you want opensips to bind on a specific interface/port/proto (default bind on all  available) */
#listen=udp:192.168.1.2:5060

Modules

指定 modules 的路徑

#set module path
mpath="/usr/lib64/opensips/modules/"

設定 FIFO file 的路徑,用來處理與暫存外部指令。

#### FIFO Management Interface
loadmodule "mi_fifo.so"
modparam("mi_fifo", "fifo_name", "/tmp/opensips_fifo")
modparam("mi_fifo", "fifo_mode", 0666)

Scripting Basics

opensips 使用類似 C 語言的 scripting language,這可以用來 routing SIP requests 與處理 SIP replies。

除了 core functions/values 之外,其他 functions 都是由 modules 提供。

Core functions
  1. forward();
    Route the request "stateless" (based on R-URI)
  2. drop();
    停止執行 script
  3. exit();
    立即終止 script processing

其他 core functions:
seturi()
setflag()
isflagset()
strip()
prefix()
rewritehostport()

Core values

script 裡面預先定義了一些 values 可以使用

  1. INET/INET6
    IPv4 or IPv6
  2. TCP/TLS/UDP
    protocol
  3. myself
    a reference to the list of local IP addresses, hostnames, and aliases
Core keywords

可用來識別 SIP message裡面的特殊欄位的值

  1. af
    Address family(INET/INET6)
  2. proto
    Protocol(TCP/TLS/UDP)
  3. dst_ip
    在 SIP message 中收到的 local interface 的 IP
  4. method
    SIP method of this message
  5. Status
    用在 onreply_route 時,這個變數會參考到 reply 的 status code
  6. retcode
    上一個 function 處理的結果
  7. uri
    request URI
  8. from_uri
    FROM header 裡面的 URI
  9. to_uri
    TO header 裡面的 URI

使用範例

if(af==INET6) {
log("Message received over Ipv6 link\n");
};
if(is_method("INVITE") && from_uri=~".*@opensips.org")
{
log("the caller is from opensips.org\n");
};
Pseudo-variables

就是 system variables,例如 SIP Messages 可取得 headers, R-URI, source IP。從 opensips 可取得 time, process ID。

Pseudo-variables 是以 $ 開頭,如果 script 裡面要使用該符號,就要加上 esacape $$,完整的 Pseudo-variables 可在這個網頁取得。

Script variables

可在 config script 裡面使用的變數,這些變數只存在於 script 的執行期,執行結束後,就會被刪除。該變數可儲存數字或字串。

$var(name)

範例

$var(b)=1;
$var(b)="1";
$var(b)="$fu"+"$tu";
$var(b)=1+2;

可使用以下的 operations

+
-
/
*
%: modulo division
|: Bitwise OR
&: Bitwise AND
^: Bitwise XOR
~: Bitwise NOT

也可使用下列的字串處理方法

{s.len}
{s.int}
{s.substr,offset,length}
{s.select,index,separator}
{uri.user}
{uri.host}
{uri.params}
{param.value,name} - returns the value of parameter "name"

範例

"a=1;b=2;c=3"{param.value,c} = "3"
Attribute-Value Pair (AVP)

這個變數是在 statefule mode,附屬在 SIP message 的變數,所以 AVP 是 transaction-persistent variables。AVP 會在 transaction 開始時被配置,而在 transaction 結束時被釋放。

AVP 的格式:
$avp(id[N])

id
(1) si:name
    AVP identifier name, s 與 i 是字串或數字
(2) name
    AVP alias name, 字串或數字

範例:
$avp(i:700)
$avp(s:blacklist)

AVPs 相關的函數

  1. avp_db_load: 由 DB 將 AVPs 載入至記憶體
  2. avp_db_store: 將 AVPs 存到 DB
  3. avp_db_delete
  4. avp_db_query: 進行 DB 查詢,並將結果存至 AVP
  5. avp_delete: 刪除記憶體中的 AVP
  6. avp_pushto: 將 AVP 放入 SIP message 中
  7. avp_check: 用 operator(equal, greater than, and a value) 取得值,例如 avp_check("i:500", "lt/i:501");
  8. avp_copy: 複製 avp
  9. avp_printf: 格式化 AVP
  10. avp_subst: 在 AVP 內尋找並取代某個值
  11. avp_op: 對 AVPs 做 math operations
  12. is_avp_set: check if this AVP's name is set
  13. avp_print: 列印記憶體中的 AVP

從 DB 中取得 user_preference table 變成 AVPs。

範例:在 call forward 時,可取得該 user 的 user_preference 資料。

user_preference table structure:

  1. id: auto-increment field
  2. uuid: unique user id
  3. username: username
  4. domain: domain
  5. attribute: AVP name
  6. type: 0–Avp str|Val Str,1–Avp str|Val Int,2–Avp int|Val Str,3-Avp int|Val int
  7. value: AVP value
  8. last modified: 上次修改的時間

Flags

用來 trigger some processes 例如 accouting, dialog control, NAT handling。有三種 flags: message, script, branch flags。

Type Persistence Function Purpose
Message flag transaction setflag(flag_idx) 在 transaction level 啟動 some functions
Branch flag Branch setbflag(flag_idx) 在 branch level 啟動 some functions
Script flag Top-level Routing setsflag(flag_idx) 儲存其他 flags

pseudo-variables: $mf (message flags), $bf (branch flags), $sf (script flags)

module GFLAGS

只能用在 external flags,這是用在 MI commands,可在 MI interface 的 external program 中,使用 set_gflag(), is_gflag(), reset_gflag() 這些 functions。

Statements

if-else
if ( t_check_trans() ) {
    t_relay();
    exit;
} else {
    exit;
}
Switch
switch($retcode)
{
    case -1:
        log("process INVITE requests here\n");
        break;
    case 1:
        log("process REGISTER requests here\n");
        break;
    case 2:
    case 3:
        log("process SUBSCRIBE and NoTIFY requests here\n");
        break;
    default:
        log("process other requests here\n");
}
Subroutes
route[1]{
    if(is_method("INVITE")) 
    {
        return(-1);
    };
    if(is_method("REGISTER"))
        return(1);
    }
    if(is_method("SUBSCRIBE"))
        return(2);
    }
    if(is_method("NoTIFY"))
        return(3);
    }
    return(-2);
}