2014年9月1日

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");

    }
}