2014/9/29

Scala vs Groovy

當全世界都在走向 functional programming 的時候,身為一個傳統的 OOP Java Programmer 也必須要面對現實,不僅僅是 JDK 本身,慢慢地加入了 FP 的元素,還有其他跟 Java 相容的新語言,可以讓我們選擇如何踏入 FP 的世界。

Java 的優勢,是將近二十年來累積而成的龐大函式庫,如果說要以跟 Java 相容為前題,跨足進入 FP 的世界,我們可以選擇的是 GroovyScala,至於 Jython 或是 JRuby,都只能算是另一個語言的 Porting,就不需要去考慮使用他們。

在面對技術趨勢的選擇時,我們可以使用 Google Trend: Groovy vs Scala 觀察搜尋的趨勢,Groovy 跟 Scala 比較起來,還是 Scala 比較吸睛,重點還是在 Scala 天生支援的 Actor 功能。

以往 Java 一直都是在 Server Side 佔據著重要的地位,當我們撰寫 Server 程式時,最重要的就是要解決多人共用的運算環境,這是 Scala 從 erlang 借來的 concurrent 運算的 Actor 所要作的事情,由於 mailbox 並行運算在 erlang 已經被驗證過是個絕佳的實做方式,我們也更容易接受,選擇 Scala 作為 Java 的下一步,會是比較好的決定。

在 google 搜尋 groovy vs scala的時候,可以看到很多人的爭執。

  1. Scala? Groovy? Why Java is the right programming language for 2014 and beyond
  2. What are the key differences between Scala and Groovy?
  3. To Scala or Groovy? Which is better for a 'mathematical' approach?
  4. Java.next(): Groovy vs. Scala

在討論時,首先被提出來的就是 static type/dynamic type 的差異,再來是談到 learning curve 的差別,通常認為學習 scala 需要花多一些時間。

如果不深入談技術本質上的差異,我們再來看看 Java 爸爸 James Gosling 的代言

During a meeting in the Community Corner (java.net booth) with James Gosling, a participant asked an interesting question:

"Which Programming Language would you use now on top of JVM, except Java?".

The answer was surprisingly fast and very clear: - Scala.

再來看看 Groovy 的爸爸 James Strachan,在 blog 文章 Scala as the long term replacement for java/javac? 所說的話:

I can honestly say if someone had shown me the Programming in Scala book by by Martin Odersky, Lex Spoon & Bill Venners back in 2003 I'd probably have never created Groovy.

聽爸爸的話準沒錯!

2014/9/22

opensips - dr(dynamic routing)

上一次是將 routing 訊息直接填寫到 opensips.cfg 設定檔,但有另一個方式可以達到這個功能,就是使用 dynamic routing,因為 dynamic routing 是將設定好的 routing 資訊寫到 DB 裡面,再以 MI command(dr_reload) 在不停止 opensips 的狀況下,重新載入 routing 資訊。

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 負責。

DB tables

跟 dynamic routing 有關的 3 個 DB tables

  1. dr_groups
    從 opensips.conf 中呼叫 dr 的 table
  2. dr_gateways
    route endpoints,也就是要填 asterisk 的 ips
  3. dr_rules
    存放 inbound DID 或 default routes 的 rules

填寫 table 資料

dr_groups

在這裡我們不管 inbound 的 username 與 domain,所以設定為 * ,最重要的是 groupid 0 這個資訊,這個 groupid 在後面用來設定 dynamic routes。

mysql> use opensips;
mysql> INSERT INTO dr_groups(username,domain,groupid,description) VALUES(".*",".*","0","INBOUND");

dr_gateway

address 的地方填寫 gateway ip:port,strip 欄位決定要去除電話號碼的幾位,probe_mode 這個欄位設定為 2 代表我們要 enable probe,使用一個 active gw 的列表。

mysql> INSERT INTO dr_gateways(type,gwid,address,strip,probe_mode,description) VALUES("0","1","192.168.1.5:5060","1","2","asterisk 5");

mysql> INSERT INTO dr_gateways(type,gwid,address,strip,probe_mode,description) VALUES("0","2","192.168.1.17:5060","0","2","asterisk 17");

dr_rules

dr_rules 設定撥號規則,groupid 填 0 就是剛剛填寫的 dr_groups "INBOUND",prefix 1 是電話號碼的前置碼,也可以填上完整的 DID 號碼。
gwlist 欄位是參考到 dr_gateways 的 gwid 資料

mysql> INSERT INTO dr_rules(groupid,prefix,priority,gwlist,description) VALUES("0","1","0","1","My Number");
mysql> INSERT INTO dr_rules(groupid,prefix,priority,gwlist,description) VALUES("0","2","0","2","My Number 2");

修改 opensips.conf

modules 裡面要加上

loadmodule "drouting.so"
loadmodule "db_mysql.so"

modparam("drouting", "db_url", "mysql://opensips:opensipsrw@localhost/opensips")
modparam("drouting", "probing_interval", 60)
modparam("drouting", "probing_from", "sip:probe@URI")
modparam("drouting", "probing_method", "OPTIONS")
modparam("drouting", "probing_reply_codes", "501, 403, 404")
modparam("drouting", "use_domain", 1)

在 #### INITIAL REQUESTS 這一行的後面,加上封包判斷,INVITE/CANCEL 時,就呼叫 route[gw]。
do_routing("0") 就是呼叫 groupid 為 0 的路由

if (method == "INVITE") {
    setflag(1);
    record_route();
    xlog("INBOUND CALL,$dd,$ru,$ci,$fn,$fu");
    route(gw);
    exit;
} else if ( is_method("CANCEL") ) {
    xlog("!!CANCEL\n");
    setflag(ACC_DO);
    setflag(ACC_FAILED);

    xlog("CANCEL CALL,$dd,$ru,$ci,$fn,$fu");
    route(gw);
    exit;
}

route[gw] {
    if (!do_routing("0")) {
        xlog("do_routing: No rules matching the URI\n");
        send_reply("503","No rules matching the URI");
        exit;
    }

    if (is_method("INVITE")) {
        t_on_failure("GW_FAILOVER");
    }
    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");
    }
}

opensips.cfg

完整的 opensips.cfg

####### 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"


####### 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
modparam("drouting", "probing_interval", 60)
modparam("drouting", "probing_from", "sip:probe@URI")
modparam("drouting", "probing_method", "OPTIONS")
modparam("drouting", "probing_reply_codes", "501, 403, 404")
modparam("drouting", "use_domain", 1)


####  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 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
    xlog("initial requests\n");
if ( is_method("INVITE") ) {
    xlog("!!INVITE\n");
    setflag(1);
    record_route();
    xlog("INBOUND CALL,$dd,$ru,$ci,$fn,$fu");
    route(gw);
    exit;
} else if ( is_method("CANCEL") ) {
    xlog("!!CANCEL\n");
    setflag(ACC_DO);
    setflag(ACC_FAILED);

    xlog("CANCEL CALL,$dd,$ru,$ci,$fn,$fu");
    route(gw);
    exit;
}

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

    # CANCEL processing
    if (is_method("CANCEL")) {
        xlog("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;
    }


    # 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("0") ) {
        send_reply("404","No Route found");
        exit;
    }

    t_on_failure("GW_FAILOVER");

    route(RELAY);
}


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


route[gw] {
    if (!do_routing("0")) {
        xlog("do_routing: No rules matching the URI\n");
        send_reply("503","No rules matching the URI");
        exit;
    }

    if (is_method("INVITE")) {
        t_on_failure("GW_FAILOVER");
    }
    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

用這個指令,重新啟動 opensips

service opensips restart

如果 opensips 已經啟動了,但修改了 DB table 裡面的 routing 資訊,就可以用這個指令讓 opensips 重新由 DB 載入 routing 資訊。

opensipsctl fifo dr_reload

測試

在 192.168.1.5 的分機,撥打 2000 到 opensips (192.168.1.24) 時,opensips 會自動將電話轉送到 192.168.1.17 的號碼 2000

在 192.168.1.17 的分機,撥打 1106 時,opensips 會自動將第一碼 1 去掉,電話轉送到 192.168.1.5 的號碼 106。

參考網頁

OpenSIPS Dynamic Routing

2014/9/15

在工作團隊中,我們都該期待自己可以成為做事快、狠、準的空條承太郎嗎?

荒木飛呂彥的作品「JoJo的奇妙冒險」中,第三部JoJo 星塵鬥士 在最近這幾個月的時間裡,動畫版本持續的播映當中,這部作品受人矚目的原因,當然是主角「空條承太郎」,以及「替身使者」的出現。也因為JoJo這第三部的作品,奠定了JoJo奇妙冒險在歷史上的地位。

先看一下OP片頭動畫,看到承太郎快速的白金之星,反差的慢動作鏡頭時,就會開始熱血起來。




這也讓人想起,Matrix 的 bullet time 經典畫面,用高速攝影的方式,讓大家看清楚動作的細節。




星塵鬥士的團隊成員是由五個人組成

  1. 紫色隱者:喬瑟夫.喬斯達
  2. 白金之星:空條承太郎
  3. 綠色法皇:花京院典明
  4. 紅色魔術師:穆罕默德.阿布德爾
  5. 銀色戰車:J.P .波魯那雷夫

這五個人個性、能力都不同,在團隊中,自然就得扮演著不同的角色,負責處理不同的事務,如果讓星塵鬥士的團隊成員,進到一個軟體公司,接手技術開發的工作,這個團隊在遇到不同的專案開發事務時,也得要選擇最合適的人,去處理最適合的工作,這樣才能達到適才適所,降低搞砸事情的機率。

對於一個技術人員來說,從進入開發團隊一開始,可能就一直在想,我該好好地加強自己的技術能力,成為最強的替身使者白金之星,畢竟技術就是一切,但實際上,白金之星只有一個,這並不表示其他人就是沒有用處的,因為不管是紫色隱者、綠色法皇、紅色魔術師、銀色戰車都是團隊的必要成員,每一個人都有最適合要扮演的角色。

產品前期銷售要給誰處理?

喬瑟夫.喬斯達是團隊中年紀最大,替身功能最弱的成員,他是承太郎的爺爺、荷莉·喬斯達的父親,手裡掌握著史比特·瓦根財團的資源。

在遇到需要與其他路人交涉的情況時,通常都是由喬瑟夫.喬斯達負責協調,有時候也會介紹當地的特殊人文習慣,前期銷售需要一位略懂技術的人員,在基本的技術能力輔助下,進行業務活動,作為業務人員,也需要有足夠的社會經驗,喬瑟夫.喬斯達理所當然得要承擔前期銷售的工作。

研發前期雛型實作要給誰處理?

空條承太郎的替身白金之星擁有速度、準確度、破壞力,簡單來說做事情總是快、狠、準,甚至還透過學習,學會了凍結時間,可說是最強的替身使者。

承太郎最適合處理雛型實作的工作,因為他在如此強大的能力條件下,可用各種他所知道的方式,以最快的方式達成結果,完成雛型,做出一個可以展示的成品,在這個階段裡,成品不需要太多包裝與實際使用的考量,只要能展示出使用的概念就算是完成這項工作了。

專案測試要給誰處理?

穆罕默德.阿布德爾外表看起來,年紀是次於喬瑟夫.喬斯達的成員,除了個性上是團隊成員中最沉穩的這個優點之外,阿布德爾還有占星的能力,知道每一個替身使者代表的塔羅牌。

測試工作,基本上就跟算命沒什麼兩樣,算命師要先有塔羅占星的基本知識,然後根據牌面,計算(猜)出每個人的個性運勢,如果可以用命盤算一算就知道專案的 bug 會出現在哪裡,對團隊來說,等於幫了一個大忙。

客製專案要給誰處理?

穆罕默德.阿布德爾跟J.P .波魯那雷夫都算是不錯的選擇,阿布德爾的沉穩跟波魯那雷夫的輕佻,是天平的兩端,在決定分派客製專案時,可以根據客戶承辦的特性,決定要給誰處理。

比較正派,中規中矩的就交給阿布德爾,比較喜歡玩樂的,個性活潑的,就交給波魯那雷夫,客製專案基本上就只有一個目的,就是滿足客戶的專案需求,盡一切的可能,讓專案順利結案,結案的方式,會因為承辦的不同,而有不同的要求與處置,配合客戶的個性指定不同的人處理,才是最佳的選擇。

創新研發專案要給誰處理?

花京院典明在團隊中屬於智囊團的角色,花京院是最能運用思考的方式,得到處理方案的角色。團隊曾經在沙漠中遭遇到一個小嬰兒(死神13)的追擊,團隊中有些人甚至在事件解決後,還完全不曉得小嬰兒就是替身使者,由此可見花京院是最能靠腦力搭配自己替身能力解決問題的成員。

創新研發專案是在許多的未知中,需要多方的嘗試,才能完成的專案,負責的人得盡一切的努力,找到技術門檻的解決方式,這也不是隨便什麼人都能夠解決的事情,指定了錯誤的人負責這種專案,很有可能花了時間跟金錢,還是沒辦法得到什麼成果。

結語

星塵鬥士的五人團隊是個防禦型團隊,一邊解決來犯的敵人,一邊前進,其實本質跟躲藏在公司深處的研發團隊有些不一樣。這篇文章的用意,只是要提醒大家,基於同一個目的(打倒 DIO)而形成一個團隊,團隊成員為獨立個體,每一個人都有自己的個性、特殊專長(替身),當團隊面對不同任務(替身使者)時,必須各自發揮自己的能力,才能解決各式各樣不同的問題,如果五個人都是白金之星,團隊或許已經因為內鬨互毆而解散了。

每個人都該了解自己,才能為自己在團隊中定位,確認自己的價值,但或者應該反過來說,老闆必須要了解團隊成員中每一個人,才能為每一個人在團隊中定位,確認每一個人都能適才適所,並對團隊產生價值,五隻手指頭握起來,才是一個拳頭。

最後看一下片尾曲,這是英文老歌 Walk Like An Egyptian




2014/9/9

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

    }
}