接續上一篇,這裡要看 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。
- Peer 1000 -> INVITE -> Proxy
From 192.168.1.159:39132 -> 192.168.1.201:5060INVITE 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. ...
- Proxy -> INVITE -> Peer 1001
From 192.168.1.201:5060 -> 192.168.1.159:5060INVITE 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. ...
- Peer 1001 -> 200 OK -> Proxy
From 192.168.1.159:5060 -> 192.168.1.201:5060SIP/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. ...
- Proxy -> 200 OK -> Peer 1000
From 192.168.1.201:5060 -> 192.168.1.159:39132SIP/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 或其他方式。
- Peer 1000 -> INVITE -> Proxy
From 192.168.1.159:39132 -> 192.168.1.201:5060INVITE sip:1001@192.168.1.201 SIP/2.0. Contact: <sip:1000@192.168.1.159:39132>. ...
- Proxy -> INVITE -> Peer 1001
From 192.168.1.201:5060 -> 192.168.1.159:5060INVITE 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>. ...
- Peer 1001 -> 200 OK -> Proxy
From 192.168.1.159:5060 -> 192.168.1.201:5060SIP/2.0 200 OK. Record-Route: <sip:192.168.1.201;lr>. Contact: <sip:1001@192.168.1.159:5060;rinstance=a1d5fa7ecfde6278;transport=UDP>.
- Proxy -> 200 OK -> Peer 1001
From 192.168.1.201:5060 -> 192.168.1.159:39132SIP/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 這樣處理速度會比較快。
- Peer 1000 -> ACK -> Proxy
From 192.168.1.159:39132 -> 192.168.1.201:5060ACK sip:1001@192.168.1.159:5060;rinstance=a1d5fa7ecfde6278;transport=UDP;nat=yes SIP/2.0. Route: <sip:192.168.1.201;lr>. ...
- Proxy -> ACK -> Peer 1001
From 192.168.1.201:5060 -> 192.168.1.159:5060ACK 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");
}
}
沒有留言:
張貼留言