WebRTC+Kamamilio+rtpengine+Websocket SIP服务器搭建教程

发现国内使用SIP的资料真的很少,由于公司需要使用语音视频电话,在网上找了一大圈也没找到相关案例,最后自己折腾出一个方案,故此分享出来。
开始之前不得不介绍一下这五个东西。

Kamamilio 是 OpenSER 的前身,是C语言写的一个Sip服务,支持在 在Linux/UNIX系统上运行,优点就是快,可配置性强,可直接写C语言配置,其它的我就不多做介绍了,可自行google

FreeSWITCH 也是sip服务器,只是相对于Kamamilio 我感觉要复杂,而且它的信令用户管理我感觉不友好,每个用户就得搞一个配置文件,那100000个用户不得崩溃,这个看个人需求吧,我觉得,也许是我理解不够深入,它也有它的优势。

RTPProxy 就是流媒体(Stream) NAT穿透的一个代理组件
Rtpengine 这个和RTPProxy 其实是一个东西,只是 Rtpengine 加入了更多东西,可以说是一个RTPProxy 的升级版这么理解吧。
Websocket 这个不需要解释了吧,http/s 的长连接服务

为什么要写这篇文章呢?需求是这样的,这里先列出几大sip的使用方案吧

1、SDP -> Kamamilio/FreeSWITCH -> RTPProxy
这种方案适用于 android/ios/pc 的基于SDP协议的方案,支持的框架有,linphone,MicroSIP 等
2、Websocket -> WebRTC -> Kamamilio/FreeSWITCH -> WebsocketServer -> Rtpengine
这种方案适用于 JsSIP、Flutter、react-native、VUE 等前端框架使用
3、SDP/Websocket -> server -> 硬件交换机
这个没啥不好的,就是成本高一点
4、SRS to WebRTC
这个也没啥不好的,但是毕竟是做直播推流的,很多通话逻辑处理还得自己实现,比如信令、计费、分组、对接运营商等。

有关直播推流拉流可以关注我后续文章,会有讲到。

1
2
3
4
--- ---
5
6

我使用的系统是 debian 10,之前用的是Centos 7,但是由于 Rtpengine 引擎在 Centos 不好装,直接给我把系统玩坏了,嗯,还是少用root为妙,所以换成 debian
所以如果你的系统是 Centos 那就使用方案1会少折腾一点。

如果你使用的是阿里、百度、腾讯云,那么估计我这个配置还不一定行,因为我这个服务器是直接绑定公网IP的,而这些大平台都内置了一个NAT路由,把外网IP隔离了,所以如果NAT是内网IP,请开放网络安全组/防火墙的端口。

而我需要的就是第二种,好了,废话不多说了,直接上代码,注意Kamamilio 依赖MYSQL或其它数据库服务,使用前请确认你的数据库类型,我这里以mysql服务为例。

之前有玩过第一种方案的并且也用的挺好,最近又在玩flutter,发现这个方案玩的人还是比较少的,但是web玩的人却是很多,而这个方案就比较适合web了,所以出一篇文章记录一下,也好让喜欢折腾的道友有个参照。

安装 Kamamilio

# 添加 Kamamilio 源公钥
wget http://deb.kamailio.org/kamailiodebkey.gpg
sudo apt-key add kamailiodebkey.gpg
sudo vim /etc/apt/sources.list

编辑 /etc/apt/sources.list 添加如下 源地址,这里我直接贴上我的配置信息

# deb cdrom:[Debian GNU/Linux 11.0.0 _Bullseye_ - Official amd64 DVD Binary-1 20210814-10:04]/ bullseye contrib main

# deb cdrom:[Debian GNU/Linux 11.0.0 _Bullseye_ - Official amd64 DVD Binary-1 20210814-10:04]/ bullseye contrib main

deb http://security.debian.org/debian-security bullseye-security main contrib
deb-src http://security.debian.org/debian-security bullseye-security main contrib

# bullseye-updates, to get updates before a point release is made;
# see https://www.debian.org/doc/manuals/debian-reference/ch02.en.html#_updates_and_backports
# A network mirror was not selected during install.  The following entries
# are provided as examples, but you should amend them as appropriate
# for your mirror of choice.
#
# deb http://deb.debian.org/debian/ bullseye-updates main contrib
# deb-src http://deb.debian.org/debian/ bullseye-updates main contrib

# aliyun
deb https://mirrors.aliyun.com/debian/ bullseye main non-free contrib
deb-src https://mirrors.aliyun.com/debian/ bullseye main non-free contrib
deb https://mirrors.aliyun.com/debian-security/ bullseye-security main
deb-src https://mirrors.aliyun.com/debian-security/ bullseye-security main
deb https://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib
deb-src https://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib
deb https://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib
deb-src https://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib

# kamailio
deb     http://deb.kamailio.org/kamailio55 jessie  main
deb-src http://deb.kamailio.org/kamailio55 jessie  main
deb     http://deb.kamailio.org/kamailio55 stretch main
deb-src http://deb.kamailio.org/kamailio55 stretch main
deb     http://deb.kamailio.org/kamailio55 buster main
deb-src http://deb.kamailio.org/kamailio55 buster main
deb     http://deb.kamailio.org/kamailio55 bullseye main
deb-src http://deb.kamailio.org/kamailio55 bullseye main
deb     http://deb.kamailio.org/kamailio55 trusty main
deb-src http://deb.kamailio.org/kamailio55 trusty main
deb     http://deb.kamailio.org/kamailio55 xenial main
deb-src http://deb.kamailio.org/kamailio55 xenial main
deb     http://deb.kamailio.org/kamailio55 bionic main
deb-src http://deb.kamailio.org/kamailio55 bionic main
deb     http://deb.kamailio.org/kamailio55 focal main
deb-src http://deb.kamailio.org/kamailio55 focal main

更新源和安装,kamailio-mysql-modules kamailio-websocket-modules这两个模块是必须装的,不然会找不到so文件

sudo apt update
sudo apt-get install vim kamailio kamailio-mysql-modules kamailio-websocket-modules

如果kamailio-mysql-modules报错Unable to locate package libmysql dev,请安装mysql服务和客户端,至于mysql服务这个不用我教了吧,玩服务器这个基本少不了的,不想折腾就直接安装个宝塔,或单独安装 libmysqlclient21

sudo apt-get install libmysqlclient21
# 如果没有找到包,请到官网单独下载 然后使用
# https://packages.debian.org/sid/amd64/libmysqlclient21/download
# sudo apt-get install ./libmysqlclient21_8.0.23-3+b1_amd64.deb

安装完成 kamailio 后就是配置文件了,整个kamailio服务器只需要关注这两个文件就可以了

root@debian:~# ls -l /etc/kamailio/
total 44
-rw-r--r-- 1 root root 35805 Apr 30 00:09 kamailio.cfg
-rw-r--r-- 1 root root  4254 Apr 22 17:45 kamctlrc

vim /etc/kamailio/kamctlrc

配置 kamctlrc

下面直接贴出我的配置,这个文件只要是配置 kamctl 命令行工具使用的数据库

## The Kamailio configuration file for the control tools.
##
## Here you can set variables used in the kamctl and kamdbctl setup
## scripts. Per default all variables here are commented out, the control tools
## will use their internal default values.

## the SIP domain
SIP_DOMAIN=你的域名或IP

## chrooted directory
# CHROOT_DIR="/path/to/chrooted/directory"

## database type: MYSQL, PGSQL, ORACLE, DB_BERKELEY, DBTEXT, or SQLITE
## by default none is loaded
##
## If you want to setup a database with kamdbctl, you must at least specify
## this parameter.
DBENGINE=MYSQL

## database host
DBHOST= localhost #你数据库的ip或域名一般 localhost|127.0.0.1

## database port
DBPORT=3306 #你数据库的端口

## database name (for ORACLE this is TNS name)
DBNAME=kamailio #数据库的名称

## database path used by dbtext, db_berkeley or sqlite
# DB_PATH="/usr/local/etc/kamailio/dbtext"

## database read/write user
DBRWUSER="kamailio" #数据库的用户名

## password for database read/write user
DBRWPW="xxx" #数据库的密码

## database read only user
# DBROUSER="kamailioro"

## password for database read only user
# DBROPW="kamailioro"

## database access host (from where is kamctl used)
# DBACCESSHOST=192.168.0.1

## database super user (for ORACLE this is 'scheme-creator' user)
DBROOTUSER="root" #数据库root用名名

## password for database super user
## - important: this is insecure, targeting the use only for automatic testing
## - known to work for: mysql
DBROOTPW="xxx" #数据库root密码

## option to ask confirmation for all database creation steps
# DBINITASK=yes

## database character set (used by MySQL when creating database)
CHARSET="utf8" #数据库字符集

## user name column
# USERCOL="username"


## SQL definitions
## If you change this definitions here, then you must change them
## in db/schema/entities.xml too.
## FIXME

# FOREVER="2030-05-28 21:32:15"
# DEFAULT_Q="1.0"


## Program to calculate a message-digest fingerprint
# MD5="md5sum"

## awk tool
# AWK="awk"

## gdb tool
# GDB="gdb"

## If you use a system with a grep and egrep that is not 100% gnu grep compatible,
## e.g. solaris, install the gnu grep (ggrep) and specify this below.
##
## grep tool
# GREP="grep"

## egrep tool
# EGREP="egrep"

## sed tool
# SED="sed"

## tail tool
# LAST_LINE="tail -n 1"

## expr tool
# EXPR="expr"


## Describe what additional tables to install. Valid values for the variables
## below are yes/no/ask. With ask (default) it will interactively ask the user
## for an answer, while yes/no allow for automated, unassisted installs.

## If to install tables for the modules in the EXTRA_MODULES variable.
# INSTALL_EXTRA_TABLES=ask

## If to install presence related tables.
# INSTALL_PRESENCE_TABLES=ask

## If to install uid modules related tables.
# INSTALL_DBUID_TABLES=ask

## Define what module tables should be installed.
## If you use the postgres database and want to change the installed tables, then you
## must also adjust the STANDARD_TABLES or EXTRA_TABLES variable accordingly in the
## kamdbctl.base script.

## Kamailio standard modules
# STANDARD_MODULES="standard acc lcr domain group permissions registrar usrloc msilo
#                   alias_db uri_db speeddial avpops auth_db pdt dialog dispatcher
#                   dialplan"

## Kamailio extra modules
# EXTRA_MODULES="imc cpl siptrace domainpolicy carrierroute userblocklist htable purple sca"


## type of aliases used: DB - database aliases; UL - usrloc aliases
## - default: none
# ALIASES_TYPE="DB"

## control engine: RPCFIFO
## - default RPCFIFO
# CTLENGINE="RPCFIFO"

## path to FIFO file for engine RPCFIFO
# RPCFIFOPATH="/run/kamailio/kamailio_rpc.fifo"

## check ACL names; default on (1); off (0)
# VERIFY_ACL=1

## ACL names - if VERIFY_ACL is set, only the ACL names from below list
## are accepted
# ACL_GROUPS="local ld int voicemail free-pstn"

## check if user exists (used by some commands such as acl);
## - default on (1); off (0)
# VERIFY_USER=1

## verbose - debug purposes - default '0'
# VERBOSE=1

## do (1) or don't (0) store plaintext passwords
## in the subscriber table - default '1'
# STORE_PLAINTEXT_PW=0

## Kamailio START Options
## PID file path - default is: /run/kamailio/kamailio.pid
# PID_FILE=/run/kamailio/kamailio.pid

## Extra start options - default is: not set
## example: start Kamailio with 64MB share memory: STARTOPTIONS="-m 64"
# STARTOPTIONS=

配置 kamailio.cfg

接下来才是重点 kamailio.cfg 这个文件才是最复杂,里面包含很多逻辑处理,如果你只是单纯的使用,不想去研究太多逻辑的,可以直接把我这个配置把IP地址改掉换成你的就好,我也就不做过多解释了(开玩笑我解释个锤子我解释,1500行配置,解释起来天都黑了,我是花了整整两天时间配出来的),有兴趣的可以自己一行行代码去解读研究。

找到 #!substdef "!MY_IP_ADDR! 这一行,把IP地址改掉
找到 #!define DBURL 这一行,把xxx改成你的root密码或数据库密码

#!KAMAILIO
#
# Kamailio SIP Server v5.5 - default configuration script
#     - web: https://www.kamailio.org
#     - git: https://github.com/kamailio/kamailio
#
# Direct your questions about this file to: 
#
# Refer to the Core CookBook at https://www.kamailio.org/wiki/
# for an explanation of possible statements, functions and parameters.
#
# Note: the comments can be:
#     - lines starting with #, but not the pre-processor directives,
#       which start with #!, like #!define, #!ifdef, #!endif, #!else, #!trydef,
#       #!subst, #!substdef, ...
#     - lines starting with //
#     - blocks enclosed in between /* */
# Note: the config performs symmetric SIP signaling
#     - it sends the reply to the source address of the request
#     - remove the use of force_rport() for asymmetric SIP signaling
#
# Several features can be enabled using '#!define WITH_FEATURE' directives:
#
# *** To run in debug mode:
#     - define WITH_DEBUG
#     - debug level increased to 3, logs still sent to syslog
#     - debugger module loaded with cfgtrace endabled
#
# *** To enable mysql:
#     - define WITH_MYSQL
#
# *** To enable authentication execute:
#     - enable mysql
#     - define WITH_AUTH
#     - add users using 'kamctl' or 'kamcli'
#
# *** To enable IP authentication execute:
#     - enable mysql
#     - enable authentication
#     - define WITH_IPAUTH
#     - add IP addresses with group id '1' to 'address' table
#
# *** To enable persistent user location execute:
#     - enable mysql
#     - define WITH_USRLOCDB
#
# *** To enable presence server execute:
#     - enable mysql
#     - define WITH_PRESENCE
#     - if modified headers or body in config must be used by presence handling:
#     - define WITH_MSGREBUILD
#
# *** To enable nat traversal execute:
#     - define WITH_NAT
#     - option for NAT SIP OPTIONS keepalives: WITH_NATSIPPING
#     - install RTPProxy: http://www.rtpproxy.org
#     - start RTPProxy:
#        rtpproxy -l _your_public_ip_ -s udp:localhost:7722
#
# *** To use RTPEngine (instead of RTPProxy) for nat traversal execute:
#     - define WITH_RTPENGINE
#     - install RTPEngine: https://github.com/sipwise/rtpengine
#     - start RTPEngine:
#        rtpengine --listen-ng=127.0.0.1:2223 ...
#
# *** To enable PSTN gateway routing execute:
#     - define WITH_PSTN
#     - set the value of pstn.gw_ip
#     - check route[PSTN] for regexp routing condition
#
# *** To enable database aliases lookup execute:
#     - enable mysql
#     - define WITH_ALIASDB
#
# *** To enable speed dial lookup execute:
#     - enable mysql
#     - define WITH_SPEEDDIAL
#
# *** To enable multi-domain support execute:
#     - enable mysql
#     - define WITH_MULTIDOMAIN
#
# *** To enable TLS support execute:
#     - adjust CFGDIR/tls.cfg as needed
#     - define WITH_TLS
#
# *** To enable JSONRPC over HTTP(S) support execute:
#     - define WITH_JSONRPC
#     - adjust event_route[xhttp:request] for access policy
#
# *** To enable anti-flood detection execute:
#     - adjust pike and htable=>ipban settings as needed (default is
#       block if more than 16 requests in 2 seconds and ban for 300 seconds)
#     - define WITH_ANTIFLOOD
#
# *** To block 3XX redirect replies execute:
#     - define WITH_BLOCK3XX
#
# *** To block 401 and 407 authentication replies execute:
#     - define WITH_BLOCK401407
#
# *** To enable VoiceMail routing execute:
#     - define WITH_VOICEMAIL
#     - set the value of voicemail.srv_ip
#     - adjust the value of voicemail.srv_port
#
# *** To enhance accounting execute:
#     - enable mysql
#     - define WITH_ACCDB
#     - add following columns to database
#!ifdef ACCDB_COMMENT
  ALTER TABLE acc ADD COLUMN src_user VARCHAR(64) NOT NULL DEFAULT '';
  ALTER TABLE acc ADD COLUMN src_domain VARCHAR(128) NOT NULL DEFAULT '';
  ALTER TABLE acc ADD COLUMN src_ip varchar(64) NOT NULL default '';
  ALTER TABLE acc ADD COLUMN dst_ouser VARCHAR(64) NOT NULL DEFAULT '';
  ALTER TABLE acc ADD COLUMN dst_user VARCHAR(64) NOT NULL DEFAULT '';
  ALTER TABLE acc ADD COLUMN dst_domain VARCHAR(128) NOT NULL DEFAULT '';
  ALTER TABLE missed_calls ADD COLUMN src_user VARCHAR(64) NOT NULL DEFAULT '';
  ALTER TABLE missed_calls ADD COLUMN src_domain VARCHAR(128) NOT NULL DEFAULT '';
  ALTER TABLE missed_calls ADD COLUMN src_ip varchar(64) NOT NULL default '';
  ALTER TABLE missed_calls ADD COLUMN dst_ouser VARCHAR(64) NOT NULL DEFAULT '';
  ALTER TABLE missed_calls ADD COLUMN dst_user VARCHAR(64) NOT NULL DEFAULT '';
  ALTER TABLE missed_calls ADD COLUMN dst_domain VARCHAR(128) NOT NULL DEFAULT '';
#!endif

####### Include Local Config If Exists #########
import_file "kamailio-local.cfg"

####### Defined Values #########
#!substdef "!MY_IP_ADDR!这里填公网IP地址!g"
#!substdef "!MY_DOMAIN!这里填公网IP地址!g"
#!substdef "!MY_WS_PORT!8880!g"
#!substdef "!MY_WSS_PORT!8443!g"
#!substdef "!MY_MSRP_PORT!8881!g"
#!substdef "!MY_WS_ADDR!tcp:MY_IP_ADDR:MY_WS_PORT!g"
#!substdef "!MY_WSS_ADDR!tls:MY_IP_ADDR:MY_WSS_PORT!g"
#!substdef "!MY_MSRP_ADDR!tcp:MY_IP_ADDR:MY_MSRP_PORT!g"
#!substdef "!MSRP_MIN_EXPIRES!1800!g"
#!substdef "!MSRP_MAX_EXPIRES!3600!g"

#!define WITH_WEBSOCKETS
# - !define WITH_TLS
# - !define WITH_MSRP

#!define WITH_MYSQL
#!define WITH_AUTH
#!define WITH_USRLOCDB

#!define WITH_NAT
#!define WITH_NATSIPPING
#!define WITH_ANTIFLOOD
#!define WITH_RTPENGINE

#!define WITH_DEBUG
#!define WITH_JSONRPC
#!define WITH_IMC

#!define DBURL "mysql://root:[email protected]/kamailio"

# *** Value defines - IDs used later in config
#!ifdef WITH_DEBUG
#!define DBGLEVEL 3
#!else
#!define DBGLEVEL 2
#!endif

#!ifdef WITH_MYSQL
# - database URL - used to connect to database server by modules such
#       as: auth_db, acc, usrloc, a.s.o.
#!trydef DBURL "mysql://kamailio:kamailiorw@localhost/kamailio"
#!endif

#!ifdef WITH_MULTIDOMAIN
# - the value for 'use_domain' parameters
#!define MULTIDOMAIN 1
#!else
#!define MULTIDOMAIN 0
#!endif

# - flags
#   FLT_ - per transaction (message) flags
#!define FLT_ACC 1
#!define FLT_ACCMISSED 2
#!define FLT_ACCFAILED 3
#!define FLT_NATS 5

#       FLB_ - per branch flags
#!define FLB_NATB 6
#!define FLB_NATSIPPING 7
#!define FLB_RTPWS 8
#!define FLB_BRIDGE 11

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

/* LOG Levels: 3=DBG, 2=INFO, 1=NOTICE, 0=WARN, -1=ERR, ... */
debug=DBGLEVEL

/* set to 'yes' to print log messages to terminal or use '-E' cli option */
log_stderror=no

memdbg=5
memlog=5

log_facility=LOG_LOCAL0
log_prefix="{$mt $hdr(CSeq) $ci} "

/* number of SIP routing processes for each UDP socket
 * - value inherited by tcp_children and sctp_children when not set explicitely */
children=8

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

/* number of SIP routing processes for all TCP/TLS sockets */
# tcp_children=8

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

/* add local domain aliases - it can be set many times */
# alias="sip.mydomain.com"
alias=MY_DOMAIN

/* listen sockets - if none set, Kamailio binds to all local IP addresses
 * - basic prototype (full prototype can be found in Wiki - Core Cookbook):
 *      listen=[proto]:[localip]:[lport] advertise [publicip]:[pport]
 * - it can be set many times to add more sockets to listen to */
# listen=udp:10.0.0.10:5060
listen=udp:MY_IP_ADDR:5060
listen=tcp:MY_IP_ADDR:5060

#!ifdef WITH_WEBSOCKETS
listen=MY_WS_ADDR
#!ifdef WITH_TLS
listen=MY_WSS_ADDR
#!endif
#!endif
#!ifdef WITH_MSRP
listen=MY_MSRP_ADDR
#!endif

/* life time of TCP connection when there is no traffic
 * - a bit higher than registration expires to cope with UA behind NAT */
tcp_connection_lifetime=3605

/* upper limit for TCP connections (it includes the TLS connections) */
tcp_max_connections=2048

#!ifdef WITH_JSONRPC
tcp_accept_no_cl=yes
#!endif

#!ifdef WITH_TLS
enable_tls=yes

/* upper limit for TLS connections */
tls_max_connections=2048
#!endif

/* set it to yes to enable sctp and load sctp.so module */
enable_sctp=no

####### Custom Parameters #########

/* These parameters can be modified runtime via RPC interface
 * - see the documentation of 'cfg_rpc' module.
 *
 * Format: group.id = value 'desc' description
 * Access: $sel(cfg_get.group.id) or @cfg_get.group.id */

#!ifdef WITH_PSTN
/* PSTN GW Routing
 *
 * - pstn.gw_ip: valid IP or hostname as string value, example:
 * pstn.gw_ip = "10.0.0.101" desc "My PSTN GW Address"
 *
 * - by default is empty to avoid misrouting */
pstn.gw_ip = "" desc "PSTN GW Address"
pstn.gw_port = "" desc "PSTN GW Port"
#!endif

#!ifdef WITH_VOICEMAIL
/* VoiceMail Routing on offline, busy or no answer
 *
 * - by default Voicemail server IP is empty to avoid misrouting */
voicemail.srv_ip = "" desc "VoiceMail IP Address"
voicemail.srv_port = "5060" desc "VoiceMail Port"
#!endif

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

/* set paths to location of modules */
# mpath="/usr/lib/x86_64-linux-gnu/kamailio/modules/"

#!ifdef WITH_MYSQL
loadmodule "db_mysql.so"
#!endif

#!ifdef WITH_JSONRPC
loadmodule "xhttp.so"
#!endif
loadmodule "jsonrpcs.so"
loadmodule "kex.so"
loadmodule "corex.so"
loadmodule "tm.so"
loadmodule "tmx.so"
loadmodule "sl.so"
loadmodule "rr.so"
loadmodule "pv.so"
loadmodule "maxfwd.so"
loadmodule "usrloc.so"
loadmodule "registrar.so"
loadmodule "textops.so"
loadmodule "textopsx.so"
loadmodule "siputils.so"
loadmodule "xlog.so"
loadmodule "sanity.so"
loadmodule "ctl.so"
loadmodule "cfg_rpc.so"
loadmodule "acc.so"
loadmodule "counters.so"
#!ifdef WITH_IMC
loadmodule "imc.so"
#!endif

#!ifdef WITH_AUTH
loadmodule "auth.so"
loadmodule "auth_db.so"
#!ifdef WITH_IPAUTH
loadmodule "permissions.so"
#!endif
#!endif

#!ifdef WITH_ALIASDB
loadmodule "alias_db.so"
#!endif

#!ifdef WITH_SPEEDDIAL
loadmodule "speeddial.so"
#!endif

#!ifdef WITH_MULTIDOMAIN
loadmodule "domain.so"
#!endif

#!ifdef WITH_PRESENCE
loadmodule "presence.so"
loadmodule "presence_xml.so"
#!endif

#!ifdef WITH_NAT
loadmodule "nathelper.so"
#!ifdef WITH_RTPENGINE
loadmodule "rtpengine.so"
#!else
loadmodule "rtpproxy.so"
#!endif
#!endif

#!ifdef WITH_TLS
loadmodule "tls.so"
#!endif

#!ifdef WITH_ANTIFLOOD
loadmodule "htable.so"
loadmodule "pike.so"
#!endif

#!ifdef WITH_DEBUG
loadmodule "debugger.so"
#!endif

#!ifdef WITH_MSRP
loadmodule "msrp.so"
loadmodule "cfgutils.so"
#!endif

# ----- websocket -----
#!ifdef WITH_WEBSOCKETS
loadmodule "websocket.so"
#!endif


# ----------------- setting module-specific parameters ---------------

#!ifdef WITH_IMC
modparam("imc", "db_url", DBURL)
#!endif

# ----- jsonrpcs params -----
modparam("jsonrpcs", "pretty_format", 1)
/* set the path to RPC fifo control file */
# modparam("jsonrpcs", "fifo_name", "/run/kamailio/kamailio_rpc.fifo")
/* set the path to RPC unix socket control file */
# modparam("jsonrpcs", "dgram_socket", "/run/kamailio/kamailio_rpc.sock")
#!ifdef WITH_JSONRPC
modparam("jsonrpcs", "transport", 7)
#!endif

# ----- ctl params -----
/* set the path to RPC unix socket control file */
# modparam("ctl", "binrpc", "unix:/run/kamailio/kamailio_ctl")

# ----- sanity params -----
modparam("sanity", "autodrop", 0)

# ----- tm params -----
# auto-discard branches from previous serial forking leg
modparam("tm", "failure_reply_mode", 3)
# default retransmission timeout: 30sec
modparam("tm", "fr_timer", 30000)
# default invite retransmission timeout after 1xx: 120sec
modparam("tm", "fr_inv_timer", 120000)

# ----- rr params -----
# set next param to 1 to add value to ;lr param (helps with some UAs)
modparam("rr", "enable_full_lr", 0)
# do not append from tag to the RR (no need for this script)
modparam("rr", "append_fromtag", 0)

# ----- registrar params -----
modparam("registrar", "method_filtering", 1)
/* uncomment the next line to disable parallel forking via location */
# modparam("registrar", "append_branches", 0)
/* uncomment the next line not to allow more than 10 contacts per AOR */
# modparam("registrar", "max_contacts", 10)
/* max value for expires of registrations */
modparam("registrar", "max_expires", 3600)
/* set it to 1 to enable GRUU */
modparam("registrar", "gruu_enabled", 0)
/* set it to 0 to disable Path handling */
modparam("registrar", "use_path", 1)
/* save Path even if not listed in Supported header */
modparam("registrar", "path_mode", 0)

# ----- acc params -----
/* what special events should be accounted ? */
modparam("acc", "early_media", 0)
modparam("acc", "report_ack", 0)
modparam("acc", "report_cancels", 0)
/* by default ww 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)
/* account triggers (flags) */
modparam("acc", "log_flag", FLT_ACC)
modparam("acc", "log_missed_flag", FLT_ACCMISSED)
modparam("acc", "log_extra",
        "src_user=$fU;src_domain=$fd;src_ip=$si;"
        "dst_ouser=$tU;dst_user=$rU;dst_domain=$rd")
modparam("acc", "failed_transaction_flag", FLT_ACCFAILED)
/* enhanced DB accounting */
#!ifdef WITH_ACCDB
modparam("acc", "db_flag", FLT_ACC)
modparam("acc", "db_missed_flag", FLT_ACCMISSED)
modparam("acc", "db_url", DBURL)
modparam("acc", "db_extra",
        "src_user=$fU;src_domain=$fd;src_ip=$si;"
        "dst_ouser=$tU;dst_user=$rU;dst_domain=$rd")
#!endif

# ----- usrloc params -----
modparam("usrloc", "timer_interval", 60)
modparam("usrloc", "timer_procs", 1)
modparam("usrloc", "use_domain", MULTIDOMAIN)
/* enable DB persistency for location entries */
#!ifdef WITH_USRLOCDB
modparam("usrloc", "db_url", DBURL)
modparam("usrloc", "db_mode", 2)
#!endif

# ----- auth_db params -----
#!ifdef WITH_AUTH
modparam("auth_db", "db_url", DBURL)
modparam("auth_db", "calculate_ha1", yes)
modparam("auth_db", "password_column", "password")
modparam("auth_db", "load_credentials", "")
modparam("auth_db", "use_domain", MULTIDOMAIN)

# ----- permissions params -----
#!ifdef WITH_IPAUTH
modparam("permissions", "db_url", DBURL)
modparam("permissions", "load_backends", 1)
#!endif

#!endif

# ----- alias_db params -----
#!ifdef WITH_ALIASDB
modparam("alias_db", "db_url", DBURL)
modparam("alias_db", "use_domain", MULTIDOMAIN)
#!endif

# ----- speeddial params -----
#!ifdef WITH_SPEEDDIAL
modparam("speeddial", "db_url", DBURL)
modparam("speeddial", "use_domain", MULTIDOMAIN)
#!endif

# ----- domain params -----
#!ifdef WITH_MULTIDOMAIN
modparam("domain", "db_url", DBURL)
/* register callback to match myself condition with domains list */
modparam("domain", "register_myself", 1)
#!endif

#!ifdef WITH_PRESENCE
# ----- presence params -----
modparam("presence", "db_url", DBURL)

# ----- presence_xml params -----
modparam("presence_xml", "db_url", DBURL)
modparam("presence_xml", "force_active", 1)
#!endif

#!ifdef WITH_NAT
#!ifdef WITH_RTPENGINE
# ----- rtpengine params -----
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")
modparam("rtpengine", "extra_id_pv", "$avp(extra_id)")
#!else
# ----- rtpproxy params -----
modparam("rtpproxy", "rtpproxy_sock", "udp:127.0.0.1:7722")
#!endif
# ----- nathelper params -----
modparam("nathelper", "natping_interval", 30)
modparam("nathelper", "ping_nated_only", 1)
modparam("nathelper", "sipping_bflag", FLB_NATSIPPING)
modparam("nathelper", "sipping_from", "sip:[email protected]")

# params needed for NAT traversal in other modules
modparam("nathelper|registrar", "received_avp", "$avp(RECEIVED)")
modparam("usrloc", "nat_bflag", FLB_NATB)
#!endif

#!ifdef WITH_TLS
# ----- tls params -----
modparam("tls", "config", "/etc/kamailio/tls.cfg")
#!endif

#!ifdef WITH_ANTIFLOOD
# ----- pike params -----
modparam("pike", "sampling_time_unit", 2)
modparam("pike", "reqs_density_per_unit", 16)
modparam("pike", "remove_latency", 4)

# ----- htable params -----
/* ip ban htable with autoexpire after 5 minutes */
modparam("htable", "htable", "ipban=>size=8;autoexpire=300;")
#!endif

#!ifdef WITH_DEBUG
# ----- debugger params -----
modparam("debugger", "cfgtrace", 1)
modparam("debugger", "log_level_name", "exec")
#!endif

# ----- websocket -----
#!ifdef WITH_WEBSOCKETS
# ----- nathelper params -----
modparam("nathelper|registrar", "received_avp", "$avp(RECEIVED)")
# Note: leaving NAT pings turned off here as nathelper is _only_ being used for
#       WebSocket connections.  NAT pings are not needed as WebSockets have
#       their own keep-alives.
#!endif

#!ifdef WITH_MSRP
# ----- htable params -----
modparam("htable", "htable", "msrp=>size=8;autoexpire=MSRP_MAX_EXPIRES;")
#!endif

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


/* Main SIP request routing logic
 * - processing of any incoming SIP request starts with this route
 * - note: this is the same as route { ... } */
request_route {

        #!ifdef WITH_IMC
        if(is_method("MESSAGE") && (uri=~"sip:chat-[0-9]+@" || uri=~"sip:chat-manager@")){
        if(imc_manager()){
                        sl_send_reply("200", "ok");
        } else {
                        sl_send_reply("500", "command error");
        }
        exit;
        }
        #!endif

        # per request initial checks
        route(REQINIT);

        xlog("L_INFO", "START: $rm from $fu (IP:$si:$sp)\n");
        # ----- websocket -----
        #!ifdef WITH_WEBSOCKETS
        if (nat_uac_test(64)) {
                # Do NAT traversal stuff for requests from a WebSocket
                # connection - even if it is not behind a NAT!
                # This won't be needed in the future if Kamailio and the
                # WebSocket client support Outbound and Path.
                force_rport();
                if (is_method("REGISTER")) {
                        fix_nated_register();
                } else {
                        if (!add_contact_alias()) {
                                xlog("L_ERR", "Error aliasing contact <$ct>\n");
                                sl_send_reply("400", "Bad Request");
                                exit;
                        }
                }
        }
        #!endif

        # NAT detection
        route(NATDETECT);

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

        # handle retransmissions
        if (!is_method("ACK")) {
                if(t_precheck_trans()) {
                        t_check_trans();
                        exit;
                }
                t_check_trans();
        }

        # handle requests within SIP dialogs
        route(WITHINDLG);

        ### only initial requests (no To tag)

        # authentication
        route(AUTH);

        # record routing for dialog forming requests (in case they are routed)
        # - remove preloaded route headers
        remove_hf("Route");
        if (is_method("INVITE|SUBSCRIBE")) {
                record_route();
        }

        # account only INVITEs
        if (is_method("INVITE")) {
                setflag(FLT_ACC); # do accounting
        }

        # dispatch requests to foreign domains
        route(SIPOUT);

        ### requests for my local domains

        # handle presence related requests
        route(PRESENCE);

        # handle registrations
        route(REGISTRAR);

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

        # dispatch destinations to PSTN
        route(PSTN);

        # user location service
        route(LOCATION);
}

# Wrapper for relaying requests
route[RELAY] {

        # enable additional event routes for forwarded requests
        # - serial forking, RTP relaying handling, a.s.o.
        if (is_method("INVITE|BYE|SUBSCRIBE|UPDATE")) {
                if(!t_is_set("branch_route")) t_on_branch("MANAGE_BRANCH");
        }
        if (is_method("INVITE|SUBSCRIBE|UPDATE")) {
                if(!t_is_set("onreply_route")) t_on_reply("MANAGE_REPLY");
        }
        if (is_method("INVITE")) {
                if(!t_is_set("failure_route")) t_on_failure("MANAGE_FAILURE");
        }

        if (!t_relay()) {
                sl_reply_error();
        }
        exit;
}

# Per SIP request initial checks
route[REQINIT] {
        # no connect for sending replies
        set_reply_no_connect();
        # enforce symmetric signaling
        # - send back replies to the source address of request
        force_rport();

        #!ifdef WITH_ANTIFLOOD
        # flood detection from same IP and traffic ban for a while
        # be sure you exclude checking trusted peers, such as pstn gateways
        # - local host excluded (e.g., loop to self)
        if(src_ip!=myself) {
                if($sht(ipban=>$si)!=$null) {
                        # ip is already blocked
                        xdbg("request from blocked IP - $rm from $fu (IP:$si:$sp)\n");
                        exit;
                }
                if (!pike_check_req()) {
                        xlog("L_ALERT","ALERT: pike blocking $rm from $fu (IP:$si:$sp)\n");
                        $sht(ipban=>$si) = 1;
                        exit;
                }
        }
        #!endif

        if($ua =~ "friendly|scanner|sipcli|sipvicious|VaxSIPUserAgent") {
                # silent drop for scanners - uncomment next line if want to reply
                # sl_send_reply("200", "OK");
                exit;
        }

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

        if(is_method("OPTIONS") && uri==myself && $rU==$null) {
                sl_send_reply("200","Keepalive");
                exit;
        }

        if(!sanity_check("17895", "7")) {
                xlog("Malformed SIP request from $si:$sp\n");
                exit;
        }
}

# Handle requests within SIP dialogs
route[WITHINDLG] {
        if (!has_totag()) return;

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

                #!ifdef WITH_WEBSOCKETS
                if ($du == "") {
                        if (!handle_ruri_alias()) {
                                xlog("L_ERR", "Bad alias <$ru>\n");
                                sl_send_reply("400", "Bad Request");
                                exit;
                        }
                }
                #!endif

                route(DLGURI);
                if (is_method("BYE")) {
                        setflag(FLT_ACC); # do accounting ...
                        setflag(FLT_ACCFAILED); # ... even if the transaction fails
                } else if ( is_method("ACK") ) {
                        # ACK is forwarded statelessly
                        route(NATMANAGE);
                } else if ( is_method("NOTIFY") ) {
                        # Add Record-Route for in-dialog NOTIFY as per RFC 6665.
                        record_route();
                }
                route(RELAY);
                exit;
        }

        if (is_method("SUBSCRIBE") && uri == myself) {
                # in-dialog subscribe requests
                route(PRESENCE);
                exit;
        }
        if ( is_method("ACK") ) {
                if ( t_check_trans() ) {
                        # no loose-route, but stateful ACK;
                        # must be an ACK after a 487
                        # or e.g. 404 from upstream server
                        route(RELAY);
                        exit;
                } else {
                        # ACK without matching transaction ... ignore and discard
                        exit;
                }
        }
        sl_send_reply("404","Not here");
        exit;
}

# Handle SIP registrations
route[REGISTRAR] {
        if (!is_method("REGISTER")) return;

        if(isflagset(FLT_NATS)) {
                setbflag(FLB_NATB);
                #!ifdef WITH_NATSIPPING
                # do SIP NAT pinging
                setbflag(FLB_NATSIPPING);
                #!endif
        }
        if (!save("location")) {
                sl_reply_error();
        }
        exit;
}

# User location service
route[LOCATION] {

        #!ifdef WITH_SPEEDDIAL
        # search for short dialing - 2-digit extension
        if($rU=~"^[0-9][0-9]$") {
                if(sd_lookup("speed_dial")) {
                        route(SIPOUT);
                }
        }
        #!endif

        #!ifdef WITH_ALIASDB
        # search in DB-based aliases
        if(alias_db_lookup("dbaliases")) {
                route(SIPOUT);
        }
        #!endif

        $avp(oexten) = $rU;
        if (!lookup("location")) {
                $var(rc) = $rc;
                route(TOVOICEMAIL);
                t_newtran();
                switch ($var(rc)) {
                        case -1:
                        case -3:
                                send_reply("404", "Not Found");
                                exit;
                        case -2:
                                send_reply("405", "Method Not Allowed");
                                exit;
                }
        }

        # when routing via usrloc, log the missed calls also
        if (is_method("INVITE")) {
                setflag(FLT_ACCMISSED);
        }

        route(RELAY);
        exit;
}

# Presence server processing
route[PRESENCE] {
        if(!is_method("PUBLISH|SUBSCRIBE")) return;

        if(is_method("SUBSCRIBE") && $hdr(Event)=="message-summary") {
                route(TOVOICEMAIL);
                # returns here if no voicemail server is configured
                sl_send_reply("404", "No voicemail service");
                exit;
        }

        #!ifdef WITH_PRESENCE
        #!ifdef WITH_MSGREBUILD
        # apply changes in case the request headers or body were modified
        msg_apply_changes();
        #!endif
        if (!t_newtran()) {
                sl_reply_error();
                exit;
        }

        if(is_method("PUBLISH")) {
                handle_publish();
                t_release();
        } else if(is_method("SUBSCRIBE")) {
                handle_subscribe();
                t_release();
        }
        exit;
        #!endif

        # if presence enabled, this part will not be executed
        if (is_method("PUBLISH") || $rU==$null) {
                sl_send_reply("404", "Not here");
                exit;
        }
        return;
}

# IP authorization and user authentication
route[AUTH] {
        #!ifdef WITH_AUTH

        #!ifdef WITH_IPAUTH
        if((!is_method("REGISTER")) && allow_source_address()) {
                # source IP allowed
                return;
        }
        #!endif

        if (is_method("REGISTER") || from_uri==myself) {
                # authenticate requests
                if (!auth_check("$fd", "subscriber", "1")) {
                        auth_challenge("$fd", "0");
                        exit;
                }
                # user authenticated - remove auth header
                if(!is_method("REGISTER|PUBLISH"))
                        consume_credentials();
        }
        # if caller is not local subscriber, then check if it calls
        # a local destination, otherwise deny, not an open relay here
        if (from_uri!=myself && uri!=myself) {
                sl_send_reply("403","Not relaying");
                exit;
        }

        #!else

        # authentication not enabled - do not relay at all to foreign networks
        if(uri!=myself) {
                sl_send_reply("403","Not relaying");
                exit;
        }

        #!endif
        return;
}

# Caller NAT detection
route[NATDETECT] {
        force_rport();
        #!ifdef WITH_NAT
        if (nat_uac_test("19")) {
                if (is_method("REGISTER")) {
                        fix_nated_register();
                } else {
                        if(is_first_hop()) {
                                set_contact_alias();
                        }
                }
                setflag(FLT_NATS);
        }
        #!endif
        return;
}

# RTPProxy control and signaling updates for NAT traversal
route[NATMANAGE] {
        #!ifdef WITH_NAT
        if (is_request()) {
                if(has_totag()) {
                        if(check_route_param("nat=yes")) {
                                setbflag(FLB_NATB);
                        }

                        #!ifdef WITH_RTPENGINE
                        if (check_route_param("rtp=bridge")) {
                                xlog("L_INFO", "route-param rtp=bridge \n");
                                setbflag(FLB_BRIDGE);
                        }

                        if (check_route_param("rtp=ws")) {
                                xlog("L_INFO", "route-param rtp=ws \n");
                                setbflag(FLB_RTPWS);
                        }
                        #!endif
                }
        }
        #!ifdef WITH_RTPENGINE

        #if (!isbflagset(FLB_BRIDGE)) {
        #       xlog("L_INFO", " bridge flag not set , return \n");
        #       return;
        #}

        if ( !( isflagset(FLT_NATS) || isbflagset(FLB_NATB) || isbflagset(FLB_RTPWS)) ) {
                xlog("L_INFO", " neither flag set , NATS , NATB , FLB_RTPWS , return \n");
                return;
        }

        $xavp(r=>$T_branch_idx) = "replace-origin replace-session-connection";

        if (!nat_uac_test("8")) {
                xlog("L_INFO", " trust-address \n ");
                $xavp(r=>$T_branch_idx) = $xavp(r=>$T_branch_idx) + " trust-address";
        }

        if (is_request()) {
                if (!has_totag()) {
                        if (!t_is_failure_route()) {
                                $avp(extra_id) = @via[1].branch + $T_branch_idx;
                                $xavp(r=>$T_branch_idx) = $xavp(r=>$T_branch_idx) + " via-branch=extra";
                        }
                }
        }

        if (is_reply()) {
                $avp(extra_id) = @via[2].branch + $T_branch_idx;
                $xavp(r=>$T_branch_idx) = $xavp(r=>$T_branch_idx) + " via-branch=extra";
        }

        if (isbflagset(FLB_RTPWS)) {
                if ($proto =~ "ws") {
                        # webrtc to sip
                        xlog("L_INFO", " webrtc to sip \n ");
                        $xavp(r=>$T_branch_idx) = $xavp(r=>$T_branch_idx) + " rtcp-mux-demux DTLS=off SDES-off ICE=remove RTP/AVP";
                } else {
                        # sip to webrtc
                        $xavp(r=>$T_branch_idx) = $xavp(r=>$T_branch_idx) + " rtcp-mux-offer generate-mid DTLS=passive SDES-off ICE=force RTP/SAVPF";
                }
        } else {
                if ($proto =~ "ws") {
                        # doesnt need bridging, and protcol is ws , thus webrtc to webrtc call
                        $xavp(r=>$T_branch_idx) = $xavp(r=>$T_branch_idx) + " generate-mid DTLS=passive SDES-off ICE=force";
                }
        }

        xlog("L_INFO", "NATMANAGE branch_id:$T_branch_idx ruri: $ru, method:$rm, status:$rs, extra_id: $avp(extra_id), rtpengine_manage: $xavp(r=>$T_branch_idx)\n");

        rtpengine_manage($xavp(r=>$T_branch_idx));

        #!else

        if (!(isflagset(FLT_NATS) || isbflagset(FLB_NATB))) return;

        if(nat_uac_test("8")) {
                rtpproxy_manage("co");
        } else {
                rtpproxy_manage("cor");
        }

        #!endif

        if (is_request()) {
                if (!has_totag()) {
                        if(t_is_branch_route()) {
                                add_rr_param(";nat=yes");
                        }

                        #!ifdef WITH_RTPENGINE
                        if (isbflagset(FLB_BRIDGE)) {
                                add_rr_param(";rtp=bridge");
                        }

                        if (isbflagset(FLB_RTPWS)) {
                                add_rr_param(";rtp=ws");
                        }
                        #!endif
                }
        }
        if (is_reply()) {
                if(isbflagset(FLB_NATB)) {
                        if(is_first_hop())
                                set_contact_alias();
                }
        }

        if(isbflagset(FLB_NATB)) {
                # no connect message in a dialog involving NAT traversal
                if (is_request()) {
                        if(has_totag()) {
                                set_forward_no_connect();
                        }
                }
        }
        #!endif
        return;
}

#!ifdef WITH_RTPENGINE
route[BRIDGING] {
        # if traffic is from ws and going to not ws or vice versa , do bridging by setting flag
        if (!has_totag()) {
                if ($proto =~ "ws" && !($ru =~ "transport=ws")) {
                        # incoming protoocl ws , outgoing protocl is not ws , need bridging
                        setbflag(FLB_RTPWS);
                } else if (!($proto =~ "ws") && $ru =~ "transport=ws") {
                        # incoming protocl not ws , outgoing protocl ws , need bridging
                        setbflag(FLB_RTPWS);
                }
        }
}
#!endif

# URI update for dialog requests
route[DLGURI] {
        #!ifdef WITH_NAT
        if(!isdsturiset()) {
                handle_ruri_alias();
        }
        #!endif
        return;
}

# Routing to foreign domains
route[SIPOUT] {
        if (uri==myself) return;

        append_hf("P-Hint: outbound\r\n");
        route(RELAY);
        exit;
}

# PSTN GW routing
route[PSTN] {
        #!ifdef WITH_PSTN
        # check if PSTN GW IP is defined
        if (strempty($sel(cfg_get.pstn.gw_ip))) {
                xlog("SCRIPT: PSTN routing enabled but pstn.gw_ip not defined\n");
                return;
        }

        # route to PSTN dialed numbers starting with '+' or '00'
        #     (international format)
        # - update the condition to match your dialing rules for PSTN routing
        if(!($rU=~"^(\+|00)[1-9][0-9]{3,20}$")) return;

        # only local users allowed to call
        if(from_uri!=myself) {
                sl_send_reply("403", "Not Allowed");
                exit;
        }

        # normalize target number for pstn gateway
        # - convert leading 00 to +
        if (starts_with("$rU", "00")) {
                strip(2);
                prefix("+");
        }

        if (strempty($sel(cfg_get.pstn.gw_port))) {
                $ru = "sip:" + $rU + "@" + $sel(cfg_get.pstn.gw_ip);
        } else {
                $ru = "sip:" + $rU + "@" + $sel(cfg_get.pstn.gw_ip) + ":"
                                        + $sel(cfg_get.pstn.gw_port);
        }

        route(RELAY);
        exit;
        #!endif

        return;
}

# Routing to voicemail server
route[TOVOICEMAIL] {
#!ifdef WITH_VOICEMAIL
        if(!is_method("INVITE|SUBSCRIBE")) return;

        # check if VoiceMail server IP is defined
        if (strempty($sel(cfg_get.voicemail.srv_ip))) {
                xlog("SCRIPT: VoiceMail routing enabled but IP not defined\n");
                return;
        }
        if(is_method("INVITE")) {
                if($avp(oexten)==$null) return;

                $ru = "sip:" + $avp(oexten) + "@" + $sel(cfg_get.voicemail.srv_ip)
                                + ":" + $sel(cfg_get.voicemail.srv_port);
        } else {
                if($rU==$null) return;

                $ru = "sip:" + $rU + "@" + $sel(cfg_get.voicemail.srv_ip)
                                + ":" + $sel(cfg_get.voicemail.srv_port);
        }
        route(RELAY);
        exit;
#!endif

        return;
}

# Manage outgoing branches
branch_route[MANAGE_BRANCH] {

        xlog("L_INFO", "MANAGE_BRANCH: New branch [$T_branch_idx] to $ru\n");
        #!ifdef WITH_RTPENGINE
        t_on_branch_failure("rtpengine");
        route(BRIDGING);
        #!endif

        route(NATMANAGE);
}

# Manage incoming replies
reply_route {
        if(!sanity_check("17604", "6")) {
                xlog("Malformed SIP response from $si:$sp\n");
                drop;
        }
}

# Manage incoming replies in transaction context
onreply_route[MANAGE_REPLY] {
        xdbg("incoming reply\n");
        if(status=~"[12][0-9][0-9]") {
                route(NATMANAGE);
        }
}

# Manage failure routing cases
failure_route[MANAGE_FAILURE] {
        route(NATMANAGE);

        if (t_is_canceled()) exit;


        #!ifdef WITH_BLOCK3XX
        # block call redirect based on 3xx replies.
        if (t_check_status("3[0-9][0-9]")) {
                t_reply("404","Not found");
                exit;
        }
        #!endif

        #!ifdef WITH_BLOCK401407
        # block call redirect based on 401, 407 replies.
        if (t_check_status("401|407")) {
                t_reply("404","Not found");
                exit;
        }
        #!endif

        #!ifdef WITH_VOICEMAIL
        # serial forking
        # - route to voicemail on busy or no answer (timeout)
        if (t_check_status("486|408")) {
                $du = $null;
                route(TOVOICEMAIL);
                exit;
        }
        #!endif
}

# ----- websocket -----

# JSONRPC over HTTP(S) routing
#!ifdef WITH_JSONRPC
event_route[xhttp:request] {
        set_reply_close();
        set_reply_no_connect();

        #!ifdef WITH_WEBSOCKETS
        if ($Rp != MY_WS_PORT
        #!ifdef WITH_TLS
                        && $Rp != MY_WSS_PORT
        #!endif
        ){

                xlog("L_WARN", "HTTP request received on $Rp\n");
                xhttp_reply("403", "Forbidden", "", "");
                exit;

        }

        xlog("L_DBG", "HTTP Request Received\n");

        if ($hdr(Upgrade)=~"websocket"
                && $hdr(Connection)=~"Upgrade"
                && $rm=~"GET") {

                # Validate Host - make sure the client is using the correct
                # alias for WebSockets
                if ($hdr(Host) == $null || !is_myself("sip:" + $hdr(Host))) {
                        xlog("L_WARN", "Bad host $hdr(Host)\n");
                        xhttp_reply("403", "Forbidden", "", "");
                        exit;
                }

                # Optional... validate Origin - make sure the client is from an
                # authorised website.  For example,
                #
                # if ($hdr(Origin) != "http://communicator.MY_DOMAIN"
                #     && $hdr(Origin) != "https://communicator.MY_DOMAIN") {
                #       xlog("L_WARN", "Unauthorised client $hdr(Origin)\n");
                #       xhttp_reply("403", "Forbidden", "", "");
                #       exit;
                # }

                # Optional... perform HTTP authentication

                # ws_handle_handshake() exits (no further configuration file
                # processing of the request) when complete.
                if (ws_handle_handshake())
                {
                        # Optional... cache some information about the
                        # successful connection
                        exit;
                }
        }

        xhttp_reply("404", "Not Found", "", "");
        exit;

        #!endif

        if(src_ip!=127.0.0.1) {
                xhttp_reply("403", "Forbidden", "text/html",
                                "Not allowed from $si");
                exit;
        }
        if ($hu =~ "^/RPC") {
                jsonrpc_dispatch();
                exit;
        }

        xhttp_reply("200", "OK", "text/html",
                                "Wrong URL $hu");
    exit;
}
#!endif

#!ifdef WITH_WEBSOCKETS
onreply_route {
        if (($Rp == MY_WS_PORT || $Rp == MY_WSS_PORT)&& !(proto == WS || proto == WSS)) {
                xlog("L_WARN", "SIP response received on $Rp\n");
                drop;
        }

        if (nat_uac_test(64)) {
                # Do NAT traversal stuff for replies to a WebSocket connection
                # - even if it is not behind a NAT!
                # This won't be needed in the future if Kamailio and the
                # WebSocket client support Outbound and Path.
                add_contact_alias();
        }
}

event_route[websocket:closed] {
        xlog("L_INFO", "WebSocket connection from $si:$sp has closed\n");
}
#!endif

#!ifdef WITH_RTPENGINE
event_route[tm:branch-failure:rtpengine] {
        xlog("L_INFO", "BRANCH FAILED: $sel(via[1].branch) + $T_branch_idx");
        $avp(extra_id) = @via[1].branch + $T_branch_idx;
        rtpengine_delete("via-branch=extra");
}
#!endif

#!ifdef WITH_MSRP
event_route[msrp:frame-in] {
        msrp_reply_flags("1");

        if ((($Rp == MY_WS_PORT || $Rp == MY_WSS_PORT)
                && !(proto == WS || proto == WSS)) && $Rp != MY_MSRP_PORT) {
                xlog("L_WARN", "MSRP request received on $Rp\n");
                msrp_reply("403", "Action-not-allowed");
                exit;
        }

        if (msrp_is_reply()) {
                msrp_relay();
        } else if($msrp(method)=="AUTH") {
                if($msrp(nexthops)>0) {
                        msrp_relay();
                        exit;
                }

                if (!www_authenticate("MY_DOMAIN", "subscriber",
                                        "$msrp(method)")) {
                        if (auth_get_www_authenticate("MY_DOMAIN", "1",
                                                        "$var(wauth)")) {
                                msrp_reply("401", "Unauthorized",
                                                        "$var(wauth)");
                        } else {
                                msrp_reply("500", "Server Error");
                        }
                        exit;
                }

                if ($hdr(Expires) != $null) {
                        $var(expires) = (int) $hdr(Expires);
                        if ($var(expires) < MSRP_MIN_EXPIRES) {
                                msrp_reply("423", "Interval Out-of-Bounds",
                                        "Min-Expires: MSRP_MIN_EXPIRES\r\n");
                                exit;
                        } else if ($var(expires) > MSRP_MAX_EXPIRES) {
                                msrp_reply("423", "Interval Out-of-Bounds",
                                        "Max-Expires: MSRP_MAX_EXPIRES\r\n");
                                exit;
                        }
                } else {
                        $var(expires) = MSRP_MAX_EXPIRES;
                }

                $var(cnt) = $var(cnt) + 1;
                pv_printf("$var(sessid)", "s.$(pp).$(var(cnt)).$(RANDOM)");
                $sht(msrp=>$var(sessid)::srcaddr) = $msrp(srcaddr);
                $sht(msrp=>$var(sessid)::srcsock) = $msrp(srcsock);
                $shtex(msrp=>$var(sessid)) = $var(expires) + 5;
                # - Use-Path: the MSRP address for server + session id
                $var(hdrs) = "Use-Path: msrps://MY_IP_ADDR:MY_MSRP_PORT/"
                                        + $var(sessid) + ";tcp\r\n"
                                        + "Expires: " + $var(expires) + "\r\n";
                msrp_reply("200", "OK", "$var(hdrs)");
        } else if ($msrp(method)=="SEND" || $msrp(method)=="REPORT") {
                if ($msrp(nexthops)>1) {
                        if ($msrp(method)!="REPORT") {
                                msrp_reply("200", "OK");
                        }
                        msrp_relay();
                        exit;
                }
                $var(sessid) = $msrp(sessid);
                if ($sht(msrp=>$var(sessid)::srcaddr) == $null) {
                        # one more hop, but we don't have address in htable
                        msrp_reply("481", "Session-does-not-exist");
                        exit;
                } else if ($msrp(method)!="REPORT") {
                        msrp_reply("200", "OK");
                }
                msrp_relay_flags("1");
                msrp_set_dst("$sht(msrp=>$var(sessid)::srcaddr)",
                                "$sht(msrp=>$var(sessid)::srcsock)");
                msrp_relay();
        } else {
                msrp_reply("501", "Request-method-not-understood");
        }
}
#!endif

保存好配置文件,接下来就是启动服务了。

#创建数据库
kamdbctl create
# 创建用户 1001 为登录用户名 123456 为sip登录密码
kamctl add 1001 123456
# 也可以使用下面的 sh脚本批量创建用户
vim create_user.sh
# 复制以下代码保存到文件
#!/bin/bash
for((i=1;i<=2000;i++));
do
kamctl add $((1000+$i)) 'admin888..'
done
chmod +x create_user.sh
sh ./create_user.sh

systemctl start kamailio.service # 没有报错说明你成功了
systemctl status kamailio.service  #查看服务状态
# 开启 kamailio debug 日志
vim /etc/rsyslog.conf
# 在该文件的结尾添加如下代码
local0.* -/var/log/kamailio.log
systemctl restart rsyslog #重启日志服务
tail -f /var/log/kamailio.log #实时输出日志

客户端的使用我在文末面贴上代码

完成上面这些步骤,基本上就可以使用 jssip、dart_sip_ua、flutter sip_ua 等工具连接服务器了
但是这并没有完成,因为音视频通话你会发现 本地同一个WIFI局域网通话没有任何问题,一但不在一个局域网就会没有声音和视频,这就涉及到公网NAT转发的问题,大致意思就是 A设备注册的地址是192.168.1.2、B设备注册的地址是192.168.1.3,这两个地址都是手机的内网地址,他们不在一个内网是无法通讯的,就需要Rtpengine 或 RTPProxy作为中间件把这两个地址的网络打通,由于 RTPProxy 对websocket支持貌似不友好,所以这里我就把它抛弃了。

安装 Rtpengine

vim /etc/apt/sources.list
#在结尾处添加如下一行
# sipwise
deb https://dfx.at/rtpengine/10.2/ stable main
sudo apt update
:wq #保存

wget https://dfx.at/rtpengine/latest/pool/main/r/rtpengine-dfx-repo-keyring/rtpengine-dfx-repo-keyring_1.0_all.deb
sudo dpkg -i rtpengine-dfx-repo-keyring_1.0_all.deb
sudo apt-key add /usr//share/keyrings/dfx.at-rtpengine-archive-keyring.gpg
sudo apt update
sudo apt install rtpengine

至此 rtpengine 安装完成
启动服务

vim /etc/rtpengine/rtpengine.sample.conf
[rtpengine]
table = -1
# no-fallback = false
### for userspace forwarding only:
# table = -1

### a single interface:
# interface = 123.234.345.456
### separate multiple interfaces with semicolons:
# interface = internal/12.23.34.45;external/23.34.45.54
### for different advertised address:
interface = 这里换成你的公网IP

listen-ng = 127.0.0.1:2223
#listen-tcp = 8880
#listen-udp = 5060

### interface for HTTP, WS and Prometheus
# listen-http = 9101

timeout = 60
silent-timeout = 3600
tos = 184
#control-tos = 184
# delete-delay = 30
# final-timeout = 10800

# foreground = false
# pidfile = /run/ngcp-rtpengine-daemon.pid
# num-threads = 16

port-min = 30000
port-max = 60000
# max-sessions = 5000

# recording-dir = /var/spool/rtpengine
# recording-method = proc
# recording-format = raw

# redis = 127.0.0.1:6379/5
# redis-write = [email protected]:6379/42
# redis-num-threads = 8
# no-redis-required = false
# redis-expires = 86400
# redis-allowed-errors = -1
# redis-disable-time = 10
# redis-cmd-timeout = 0
# redis-connect-timeout = 1000

b2b-url = http://127.0.0.1:8090/
# xmlrpc-format = 0

# 6 为 debug 具体请参照官网
log-level = 6
# log-stderr = false
# 系统日志匹配前缀
log-facility = local5
# log-facility-cdr = local0
# log-facility-rtcp = local1

# graphite = 127.0.0.1:9006
# graphite-interval = 60
# graphite-prefix = foobar.

# homer = 123.234.345.456:65432
# homer-protocol = udp
# homer-id = 2001

# sip-source = false
# dtls-passive = false

[rtpengine-testing]
table = -1
interface = 这里换成你的公网IP
listen-ng = 2223
foreground = true
log-stderr = true
log-level = 7

# 配置日志
vim /etc/rsyslog.conf
# 在该文件的结尾添加如下代码
local5.* -/var/log/rtpengine.log
systemctl restart rsyslog #重启日志服务
tail -f /var/log/rtpengine.log #实时输出日志

rtpengine --config-file=/etc/rtpengine/rtpengine.sample.conf
# 配置好后,需要重启一下  kamailio 
systemctl restart kamailio.service

至此 rtpengine 配置完成,可以进行公网视频通话了,有声音也有视频。

jssip客户端的使用

这个使用的方法就比较多了,可以使用npm安装和单独使用,这里就不一一演示了,可以直接看官方文档

npm install jssip --save
#使用文档请看 https://www.npmjs.com/package/jssip

flutter ua_sip 的使用

# 运行命令
flutter pub add sip_ua
# 创建文件
sip_util.dart

# 以下是 sip_util.dart 代码
import 'package:flutter/material.dart';
import 'package:sip_ua/sip_ua.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:audioplayers/audioplayers.dart' as audio;

class SipUtil implements SipUaHelperListener{

  final SIPUAHelper sipHelper = SIPUAHelper();
  bool isRegister = false;
  //是否正在通话
  bool isInCall = false;
  String serverIp = "xxx";
  String wsPort = "8880";

  Map saveUserData = {};
  Map suUserData = {};
  Map ruUserData = {};

  bool isVideo = false;
  int callMsgId = 0;

  MediaStream? mediaStream;

  audio.AudioPlayer? audioPlayer;

  void init(var userData){
    saveUserData = userData;
    if(isRegister) return;
    sipHelper.addSipUaHelperListener(this);
    UaSettings settings = UaSettings();

    settings.webSocketUrl = 'ws://$serverIp:$wsPort';
    settings.webSocketSettings.extraHeaders = {};
    settings.webSocketSettings.allowBadCertificate = true;

    settings.uri = "${userData['sip_user']}@$serverIp";
    settings.authorizationUser = "${userData['sip_user']}";
    settings.password = "${userData['sip_passwd']}";
    settings.displayName = "${userData['nickname']}";
    settings.userAgent = 'Dart SIP Client v1.0.0';
    settings.dtmfMode = DtmfMode.RFC2833;

    sipHelper.start(settings);
  }

  dynamic callSip(suUser,ruUser,{BuildContext? context,bool toIsVideo = false}) async{

    //通话中禁止新开
    if(sipUtil.isInCall){
      print("通话中禁止新开");
      return;
    }

    if(sipUtil.mediaStream!=null){
      sipUtil.mediaStream!.dispose();
      sipUtil.mediaStream = null;
    }

    if(!isRegister){
      sipUtil.init(saveUserData);
      while(!sipUtil.isRegister){
        await Future.delayed(const Duration(milliseconds: 1500));
      }
      util.hideLoading();
    }

    sipUtil.isVideo = toIsVideo;

    try{
      final mediaConstraints = {'audio': true, 'video': sipUtil.isVideo};
      MediaStream mediaStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
      bool result = await sipHelper.call(
          ruUser['sip_user'], voiceonly: !sipUtil.isVideo,mediaStream: mediaStream
      );
      //拨打失败
      if(!result){
        return print("拨打失败,未知原因");
      }
    }catch(e){
      print(e.toString());
      return;
    }

    sipUtil.suUserData = suUser;
    sipUtil.ruUserData = ruUser;
  }

  @override
  void callStateChanged(Call call, CallState state) async{
   print("触发事件:${EnumHelper.getName(state.state)}");

    //这里第一次通知事件传不到通话视图,所以需要储存一下
    if(state.state==CallStateEnum.STREAM&&state.originator=="local"){
      mediaStream = state.stream;
      Logger.w("流事件类型:${state.originator}");
    }

    //处理音乐,这里的音乐自己添加
    if(call.direction=="INCOMING"&&state.state==CallStateEnum.PROGRESS){
      audioPlayer = await util.audioPlayAssetsToLocal("assets/mp3/call_inc.mp3");
    }
    if(call.direction!="INCOMING"&&state.state==CallStateEnum.PROGRESS){
      audioPlayer = await util.audioPlayAssetsToLocal("assets/mp3/call_wait.mp3");
    }

    if(audioPlayer!=null&&state.state==CallStateEnum.ACCEPTED){
      audioPlayer!.stop();
    }

    if(state.state==CallStateEnum.FAILED||state.state==CallStateEnum.ENDED){

      sipUtil.isInCall = false;
      if(sipUtil.mediaStream!=null){
        sipUtil.mediaStream!.dispose();
        sipUtil.mediaStream = null;
      }

      if(audioPlayer!=null){
        audioPlayer!.stop();
      }
      //播放音乐,这里的音乐自己添加到 assets
      util.audioPlayAssetsToLocal("assets/mp3/call_end.mp3");
    }

    //唤起视频界面 打入 打出 都是 CALL_INITIATION
    if (state.state == CallStateEnum.CALL_INITIATION) {

      print("收到电话呼叫:is back ${publicValUtil.isBackstage}");
      if(publicValUtil.isBackstage){}
      BuildContext context = routeUtil.navigatorKey.currentContext!;

      //INCOMING 为对方打进来
      if(call.direction=="INCOMING"){

        //通话中禁止呼叫
        if(sipUtil.isInCall){
          call.hangup();return;
        }

        sipUtil.isInCall = true;
        //对方打过来
        var result = await apiUtil.apiChatContactsDialogGetSipUser(
            data: {"sip_user":"${call.remote_identity}"});

        Map userData;
        if(result['code'] != 200){
          userData = {};
        }else{
          userData = result['data']['user'];
        }

        bool isVideo = call.remote_has_video;
        //这里可以放一些唤起通话界面的逻辑
        //sheetsUtil.showCallScreenModalSheet(context, userData,call,isIncoming: true,isVideo: isVideo);
      }else{
        sipUtil.isInCall = true;
        //这里可以放一些唤起通话界面的逻辑
        //sheetsUtil.showCallScreenModalSheet(context, sipUtil.ruUserData, call,isVideo:sipUtil.isVideo);
      }
    }
  }

  @override
  void onNewMessage(SIPMessageRequest msg) {
    // TODO: implement onNewMessage
  }

  @override
  void registrationStateChanged(RegistrationState state) {
    print("收到注册服务:${EnumHelper.getName(state.state)}");
    if(state.state!=RegistrationStateEnum.REGISTERED){
      isRegister = false;
    }else{
      isRegister = true;
    }
  }

  @override
  void transportStateChanged(TransportState state) {
    // TODO: implement transportStateChanged
  }
}

//初始化视频
sipUtil.init({"sip_user":"xxx","sip_passwd":"xxx","nickname":"xxx"});

你可能感兴趣的:(WebRTC+Kamamilio+rtpengine+Websocket SIP服务器搭建教程)