顯示具有 sip 標籤的文章。 顯示所有文章
顯示具有 sip 標籤的文章。 顯示所有文章

2020/9/7

sippts

sippts 是用 perl 開發的一組測試 SIP Protocol 的工具。

安裝

依照 [PERL] 使用CPAN安裝模組 的說明,安裝 CPAN

yum install gcc* perl-CPAN

第一次進入 CPAN Shell 要經過一些設定,根據上面文章的說明,都是按 Enter 用預設值即可

# perl -MCPAN -e shell

根據 sippts 的說明,要安裝 CPAN modules

cpan -i IO:Socket:Timeout
cpan -i NetAddr:IP
cpan -i String:HexConvert
cpan -i Net::Address::IP::Local
cpan -i DBD::SQLite

在安裝 pcap module 前,要先安裝 libpcap

yum install libpcap libpcap-devel
cpan -f -i Net:Pcap

因為用 cpan -i Net:Pcap 安裝時,會卡在 t/04-loop.t ................ 1/195 測試,所以直接用 -f 強制安裝 pcap module

下載 sippts

wget https://github.com/Pepelux/sippts/archive/v2.0.3.tar.gz
tar zxvf v2.0.3.tar.gz
cd sippts-2.0.3

直接用 perl 執行看看,發生這樣的 error

# perl sipexten.pl
Can't locate Digest/MD5.pm

安裝 perl-Digest-MD5

yum -y install perl-Digest-MD5

使用

  • Sipscan 以 multithread 掃描 SIP services,可檢測多個 IPs, port ranges,可用在 TCP/UDP
  • Sipexten 可找到 SIP server 可運作的分機號碼。
  • Sipcracker 是 remote password cracker,可測試 SIP 密碼
  • Sipinvite 可檢測外撥電話是否需要認證,如果 SIP server 沒有做好設定,就可以外撥電話。且可以測試外撥電話後,再轉接另一個電話
  • Sipsniff 是簡單的 SIP sniffer 工具,可用 SIP method type 過濾 SIP packet
  • Sipspy 是一個簡單的 SIP server,可顯示 digest auth requests, responses
  • SipDigestLeak 可檢測是否有 Sandro Gauci 找到的 SIP digest leak vulnerability
# perl sipexten.pl -h 192.168.0.1 -e 100-200 -m REGISTER
Found match: 192.168.0.1/udp - User: 100 - Require authentication
Found match: 192.168.0.1/udp - User: 101 - Require authentication
Found match: 192.168.0.1/udp - User: 102 - Require authentication
Found match: 192.168.0.1/udp - User: 103 - Require authentication
....

sippts 預設的 useragent 為

User-Agent: pplsip

References

pplsip.SIP.Scanner

sippts v2.0.3 releases: Set of tools to audit SIP based VoIP Systems

Voip packages

2016/4/25

如何使用 sipp 播放自訂的 RTP 語音


sipp 是 linux 上用來產生 SIP traffic 的測試工具,它可以使用 xml 描述一通電話的所有 SIP 封包的內容,並利用 pcap 播放工具,將 pcap 裡面的
RTP 語音直接送到受話端。sipp 安裝後,預設只會有一個 g711a.pcap 也就是 alaw 的 RTP 語音檔,如果要測試其他 codec 的語音檔,就要自己錄製。


tcpdump


首先我們準備好一個 SIP Client 當作發話端,在 SIP server 上先建立一個 conference room,當然 SIP Client 必須要設定只使用我們要錄製的 ilbc codec。


執行 tcpdump 將封包錄製下來。


tcpdump -i eth0 -w audio.pcap

接下來就用 SIP Client 撥打電話到 conference room 裡面,然後就講幾句話之後掛斷電話。


wireshark 處理 audio.pcap


用 wireshark 將 audio.pcap 打開之後,可以看到這一段時間錄製的所有封包,這時候,我們用 filter 將 RTP 封包擷取出來。


在 filter 的地方填寫以下的字串,就可以取出 RTP 的語音封包,192.168.1.11 是發話端的 IP,192.168.1.5 是受話端的 IP。


((ip.src == 192.168.1.11) and (ip.dst == 192.168.1.5)) and rtp

在 File -> Export Specific Packet 的選單中,選擇將 Displayed All Packets 另存到另一個 ilbc.pcap 檔案中。


sipp scenario


準備 sipp call scenario 檔案,內容就是撥打電話進去,接通後,播放 ilbc.pcap 檔案。


SIPP scenario: test.xml


<?xml version="1.0" encoding="ISO-8859-1" ?>
<scenario name="test">

<send>
    <![CDATA[
      INVITE sip:[field2]@[remote_ip] SIP/2.0
      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch];rport
      Max-Forwards: 70
      To: "[field2]" <sip:[field2]@[remote_ip]>
      From: "[field0]" <sip:[field0]@[remote_ip]>;tag=[call_number]
      Call-ID: [call_id]
      CSeq: [cseq] INVITE
      Contact: <sip:[field0]@[local_ip]:[local_port]>
      Content-Type: application/sdp
      Content-Length: [len]

      v=0
      o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]:[local_port]
      s=-
      c=IN IP[media_ip_type] [media_ip]
      t=0 0
      m=audio [auto_media_port] RTP/AVP 97 101
      a=rtpmap:97 ILBC/8000
      a=rtpmap:97 mode=20
      a=rtpmap:101 telephone-event/8000
      a=fmtp:101 0-11,16
    ]]>
</send>
<recv response="401" auth="true" />

<send>
    <![CDATA[
      INVITE sip:[field2]@[remote_ip] SIP/2.0
      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch];rport
      Max-Forwards: 70
      To: "[field2]" <sip:[field2]@[remote_ip]>
      From: "[field0]" <sip:[field0]@[remote_ip]>;tag=[call_number]
      Call-ID: [call_id]
      CSeq: [cseq] INVITE
      Contact: <sip:[field0]@[local_ip]:[local_port]>
      Content-Type: application/sdp
      [field1]
      Allow: INVITE,ACK,BYE,CANCEL,OPTIONS,PRACK,REFER,NOTIFY,SUBSCRIBE,INFO,MESSAGE
      Supported: replaces,norefersub,100rel
      Content-Length: [len]

      v=0
      o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]:[local_port]
      s=-
      c=IN IP[media_ip_type] [media_ip]
      t=0 0
      m=audio [auto_media_port] RTP/AVP 97 101
      a=rtpmap:97 ILBC/8000
      a=rtpmap:97 mode=20
      a=rtpmap:101 telephone-event/8000
      a=fmtp:101 0-11,16
    ]]>
</send>

<recv response="100" optional="true" />
<recv response="180" optional="true" />
<recv response="200" />

<send>
    <![CDATA[
      
      ACK sip:[field2]@[remote_ip] SIP/2.0
      Via: SIP/2.0/[transport] [local_ip];rport;branch=[branch]
      From: "[field0]" <sip:[field0]@[remote_ip]>;tag=[call_number]
      To: "[field2]" <sip:[field2]@[remote_ip]>[peer_tag_param]
      Call-ID: [call_id]
      CSeq: [cseq] ACK
      Content-Length: 0
       
    ]]>
</send>

<pause milliseconds="2000"/>

<nop>
    <action>
        <exec play_pcap_audio="pcap/dtmf_2833_star.pcap" />  
    </action>
</nop>
 <nop>
    <action>
       <exec play_pcap_audio="pcap/ilbc.pcap"/>
    </action>
 </nop>
 
<pause milliseconds="1000"/>

<send retrans="500">
    <![CDATA[
      
      BYE sip:[field2]@[remote_ip] SIP/2.0
      Via: SIP/2.0/[transport] [local_ip]:[local_port];rport;branch=[branch]
      Max-Forwards: 70
      From: "[field0]" <sip:[field0]@[remote_ip]>;tag=[call_number]
      To: "[field2]" <sip:[field2]@[remote_ip]>[peer_tag_param]
      Call-ID: [call_id]
      Cseq: 3 BYE
      Subject: Performance Test
      Content-Length: 0
      
    ]]>
</send>

<recv response="200" crlf="true" />

<ResponseTimeRepartition value="10, 20, 30, 40, 50, 100, 150, 200"/>

<CallLengthRepartition value="10, 50, 100, 500, 1000, 5000, 10000"/>

</scenario>

準備另一個搭配 test.xml call scenario 的 sip 帳號及密碼的 csv 檔案: test.csv


SEQUENTIAL
2106;[authentication username=2106 password=test];2105

執行 sipp


因為通話是設定讓 sipp 以門號 2106 撥打到 2105,所以我們利用 SIP Client 註冊到 2105 之後,執行以下的 sipp 指令,就可以在 2105 接聽到 sipp 撥打過來的電話,然後聽到剛剛錄製的 ilbc 語音。


sipp -sf test.xml -inf test.csv -r 1 -rp 1 -i 192.168.1.11 -m 1 192.168.1.5

2014/11/17

kamailio installation step by step

先前已經測試過 opensips,雖然可以使用,但還是遇到一些問題,例如網頁使用者界面的套件一直沒辦法運作地很順利,我們試著改成版本更新速度比較快的 kamailio,接下來就是安裝的過程。

準備工作

安裝 kamailio 之前,必須把 CentOS 基本的套件裝好,通常我們會把開發者工具、kernel 的開發套件都裝上去,還會裝上 EPEL、rpmforge 這兩個 package repository。

因為 kamailio 的 dialplan 需要,所以必須要安裝 pcre,因為 rtpengine 的需要,所以要安裝 xmlrpc-c-devel iptables-devel。

yum -y install pcre pcre-devel libpcap libpcap-devel libunistring libunistring-devel xmlrpc-c-devel iptables-devel

安裝 kamailio

kamailio 才剛在 2014/10/16 發布 kamailio 4.2.0 版,我們可以到 kamailio download page 下載 kamailio-4.2.0_src.tar.gz。

把原始程式碼放在 /usr/local/src 資料夾中。

cd /usr/local/src
tar zxvf kamailio-4.2.0_src.tar.gz

cd kamailio-4.2.0
make cfg

修改 modules.lst 一行資料

vi modules.lst

include_modules= db_mysql websocket tls dialplan

編譯並安裝 kamailio

make all
make install

現在安裝完成的 kamailio

設定檔在 /usr/local/etc/kamailio
執行擋在 /usr/local/sbin

這裡面有4個執行檔

kamailio - Kamailio SIP server
kamdbctl - script to create and manage the Databases
kamctl - script to manage and control Kamailio SIP server
sercmd - CLI - command line tool to interface with Kamailio SIP server

modules在 /usr/local/lib64/kamailio/modules/
文件在 /usr/local/share/doc/kamailio/
man page 在 /usr/local/share/man/man5/ 以及 /usr/local/share/man/man8/

產生 mysql database

編輯 /usr/local/etc/kamailio/kamctlrc

vi /usr/local/etc/kamailio/kamctlrc
把這一行設定的註解移掉
DBENGINE=MYSQL

修改 DB 預設的密碼

DBRWPW="dbpassword"
DBROPW="dbpassword"

執行

/usr/local/sbin/kamdbctl create

結果會產生兩個 mysql users,預設密碼的部份剛剛有改過了,應該會變成 dbpassword。

kamailio 預設密碼 kamailiorw
    有 'kamailio' database 完整權限
kamailioro 預設密碼 kamailioro
    有 'kamailio' database read-only 權限

雖然 已經改過 /usr/local/etc/kamailio/kamctlrc 的密碼,需要再一次覆寫資料庫的密碼。

mysql -u root -p
use mysql;
UPDATE user SET Password=PASSWORD("dbpassword") WHERE User='kamailio';
UPDATE user SET Password=PASSWORD("dbpassword") WHERE User='kamailioro';
flush privileges;

製作啟動服務的 script

先把 kamailio 核心的設定檔改好。

cp /usr/local/src/kamailio-4.2.0/pkg/kamailio/centos/6/kamailio.init /etc/init.d/kamailio
mkdir -p /etc/kamailio
cp /usr/local/etc/kamailio/kamailio.cfg /etc/kamailio/

修改 DB 的設定

vi /etc/kamailio/kamailio.cfg

在檔案前面增加三行
#!define WITH_MYSQL
#!define WITH_AUTH
#!define WITH_USRLOCDB
修改 DBURL 密碼
#!ifndef DBURL
#!define DBURL "mysql://kamailio:max168kit@localhost/kamailio"
#!endif

以 kamailio 的 init script sample 把啟動服務的 script 做好。

cp /usr/local/src/kamailio-4.2.0/pkg/kamailio/centos/6/kamailio.sysconfig /etc/sysconfig/kamailio

chmod 755 /etc/init.d/kamailio

修改 script 內容

vi /etc/init.d/kamailio
修改這2行
KAM=/usr/local/sbin/kamailio
RUN_KAMAILIO=yes


# 最後面增加 -f $KAMCFG 
OPTIONS="-P $PID_FILE -m $SHM_MEMORY -M $PKG_MEMORY -u $USER -g $GROUP $EXTRA_OPTIONS -f $KAMCFG "

修改執行 kamailio 的 user 權限

mkdir -p /var/run/kamailio
adduser --system --shell "/sbin/nologin" --home /var/run/kamailio kamailio
chown kamailio:kamailio /var/run/kamailio

設定 SIP Domain 變數, 有二種方式

1.
export SIP_DOMAIN=192.168.1.24
2.
vi /root/.kamctlrc
SIP_DOMAIN=192.168.1.24

執行 script 時有一些錯誤。

which: no greadlink in (/usr/lib64/qt-3.3/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin:/)

修改第 19 行可以解決這個問題

vi /usr/local/sbin/kamctl

which greadlink > /dev/null 2>&1

使用獨立的log檔案

檢查 kamailio.cfg 設定

vi /etc/kamailio/kamailio.cfg
debug=3 #此值控制日誌輸出的詳細程度,3為普通,4為詳細(會產生很多日誌)。
log_stderror=no #設置為no表示將日誌輸出到文件,否則輸出到控制台(應該是以前台方式啟動opensips服務時才有用)。
log_facility=LOG_LOCAL0 #應該是用於在syslog服務的配置文件裡區分opensips產生的日誌(見下面"使用獨立的log文件")。
fork=yes #設置為yes表示在後台啟動opensips服務,設置為no表示在前台啟動。

kamailio 使用syslog服務,在沒有作任何設定的狀況下,log 會進入/var/log/message這個檔案,如果希望使用獨立的log檔案,可以這樣設定

touch /var/log/kamailio.log
vi /etc/rsyslog.conf
增加一行
local0.* /var/log/kamailio.log

/etc/init.d/rsyslog restart

一併把 logrotate 設定好

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

安裝 網頁界面 siremis

把 siremis 準備好

cd /usr/local/src
tar zxvf siremis-4.1.0.tgz
mv siremis-4.1.0 /var/www/html/siremis

產生 apache conf file

cd /var/www/html/siremis
make apache-conf

依照內容建議,編寫 apache httpd config for siremis

vi /etc/httpd/conf.d/siremis.conf
Alias /siremis "/var/www/html/siremis/siremis"
<Directory "/var/www/html/siremis/siremis">
    Options Indexes FollowSymLinks MultiViews
    AllowOverride All
    Order allow,deny
    Allow from all
    <FilesMatch "\.xml$">
        Order deny,allow
        Deny from all
    </FilesMatch>
    <FilesMatch "\.inc$">
        Order deny,allow
        Deny from all
    </FilesMatch>
</Directory>

修改資料夾權限,重新啟動 httpd

make prepare
chown -R apache:apache /var/www/html/siremis

service httpd restart

建立 siremis DB

mysqladmin create siremis -p

mysql -uroot -p
GRANT ALL PRIVILEGES ON siremis.* TO siremis@localhost IDENTIFIED BY 'dbpassword';

連上 siremis 網頁,一開始會是一個設定的 wizard,我們必須把資料庫的密碼,改成剛剛修改後的 dbpassword。

http://localhost/siremis/

在產生 DB 的畫面最下面,把這三個項目打勾

Import Default Data 打勾
Update sip database 打勾
Replace DB Config 打勾

最後只要看到 Installation Completed 的畫面,就完成安裝程序了。

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

    }
}

2014/8/25

opensips 簡介 1/2

opensips

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