History: 9 Years SER-Kamailio

Date: Sep 3, 2010
Data compiled by: Daniel-Constantin Mierla

Overview

SIP Express Router (aka SER) is a high-performance, configurable, free Session Initiation Protocol (SIP) server licensed under the open-source GNU license, offering a large set of features. Started before the publishing of RFC3261 (SIP v2.0), SER pioneered the development of many SIP extensions and pushed further the real-time communications over IP.

Initial project web site was:

Now the web sites are:

It is the oldest and most robust open source SIP server, routing billions of VoIP minutes every month world wide, being used from Telcos and Carriers to ITSP and SOHO environments. If you haven't heard of it so far, it is very likely because your VoIP provider routes the calls fast and reliable with SER-based SIP servers, so you don't need to build your own system.

First source code commit of SER was done 9 years ago: Sep 3, 2001. According to GIT log, first three commits were:

git log --pretty=format:"%h%x09%an%x09%ad%x09%s" --reverse | head -3

512dcd9 Andrei Pelinescu-Onciul Mon Sep 3 21:27:11 2001 +0000   Initial revision
888ca09 Andrei Pelinescu-Onciul Tue Sep 4 01:41:39 2001 +0000   parser seems to work
e60a972 Andrei Pelinescu-Onciul Tue Sep 4 20:55:41 2001 +0000   First working release

That is 3286 days of continuous development, with over 70 registered developers and hundreds of contributors, estimated cost of development: over 8 000 000 USD.

Summary of Evolution

  • September 2001 - initial commit, SER was then developed for about one year internally at FhG FOKUS Institute, Berlin, Germany
  • Autumn 2002 - SER was released as GPL, code published on BerliOS: http://developer.berlios.de/projects/ser/
  • June 2005 - OpenSER forked from SER, code hosted by Source Forge: http://sourceforge.net/projects/openser/
  • July 2008 - OpenSER was renamed to Kamailio
  • November 2008 - SER and Kamailio teams decide to join development efforts and merge the source code trees of the two applications
  • development portal for both changed to: http://sip-router.org
  • January 2010 - version 3.0.0 is released, from a source code tree containing both SER and Kamailio
  • September 2010 - expect next major release, version 3.1.0

http://sip-router.org/pub/img/sip-router-evolution.png

SER code and architecture was and still is the foundation for other projects that forked over years from it or from its forks, which still keep majority of inherited code untouched.

As you can notice, SER and Kamailio are now same application (completely the same source code). The difference is made by what modules are you using for same purpose (e.g., user authentication, location, accounting) because the variants have a different database structure (you can notice later the existence of modules with same name, but located in different folders).

http://www.kamailio.org/wp-images/kamailio-rock-logo.jpg

Of course you can combine to some extent, for example use Kamailio-specific accounting module with SER-specific database user authentication module. The limitation comes to modules that have dependencies, for example registrar module depends on usrloc module -- you have to use both from one side.

Public Releases

There were 11 distinct numbering versions used over the years (0.9.x was overlapping, counted once), internal VCS branching representations were:

SER Only Versions

  • 2002-09-25: SER 0.8.x
  • 2005-07-04: SER 0.9.x
  • 2008-08-06: SER 2.0.x
  • 2009-02-28: SER 2.1.x

Kamailio (OpenSER) Only Versions

Combined Versions

Next Combined Version

SER Arguments

The command line arguments of SER changed over the years, but you can notice that it has IPv6 support since 2002 (well, we still wait for ISPs...).

Output of ser -h and ser -V at very young age (recompiled today):

# ser -h
version: ser 0.8.7-99 (i386/linux)
Usage: ser -l address [-p port] [-l address [-p port]...] [options]
Options:
    -f file      Configuration file (default /usr/local/etc/ser/ser.cfg)
    -p port      Listen on the specified port (default: 5060)
                 applies to the last address in -l and to all
                 following that do not have a corespponding -p
    -l address   Listen on the specified address (multiple -l mean
                 listening on more addresses). The default behaviour
                 is to listen on the addresses returned by uname(2)

    -n processes Number of child processes to fork per interface
                 (default: 8)

    -r           Use dns to check if is necessary to add a "received="
                 field to a via
    -R           Same as `-r` but use reverse dns;
                 (to use both use `-rR`)

    -v           Turn on "via:" host checking when forwarding replies
    -d           Debugging mode (multiple -d increase the level)
    -D           Do not fork into daemon mode
    -E           Log to stderr
    -V           Version number
    -h           This help message
    -b nr        Maximum receive buffer size which will not be exceeded by
                 auto-probing procedure even if  OS allows
    -m nr        Size of shared memory allocated in Megabytes
    -w  dir      change the working directory to "dir" (default "/")
    -t  dir      chroot to "dir"
    -u uid       change uid
    -g gid       change gid
    -P file      create a pid file
    -i fifo_path create a fifo (useful for monitoring ser)


# ser -V
version: ser 0.8.7-99 (i386/linux)
flags: STATS:Off, USE_IPV6, DNS_IP_HACK, SHM_MEM, SHM_MMAP, PKG_MALLOC, F_MALLOC, FAST_LOCK-ADAPTIVE_WAIT
ADAPTIVE_WAIT_LOOPS=1024, MAX_RECV_BUFFER_SIZE 262144, MAX_LISTEN 16, MAX_URI_SIZE 1024, BUF_SIZE 3040
@(#) $Id: main.c,v 1.119 2002-09-25 19:20:26 andrei Rel $
main.c compiled on 17:11:29Sep  3 2010 with gcc 4.4

Output of ser -h and ser -V nowadays:

# ser -h
version: ser 3.0.99-dev1 (i386/linux) b72876
Usage: ser [options]
Options:
    -f file      Configuration file (default: /usr/local/etc/ser/ser.cfg)
    -L path      Modules search path (default: /usr/local/lib/ser/modules:/usr/local/lib/ser/modules_s:/usr/local/lib/ser/modules_k)
    -c           Check configuration file for errors
    -l address   Listen on the specified address/interface (multiple -l
                  mean listening on more addresses).  The address format is
                  [proto:]addr_lst[:port], where proto=udp|tcp|tls|sctp,
                  addr_lst= addr|(addr, addr_lst) and
                  addr= host|ip_address|interface_name.
                  E.g: -l locahost, -l udp:127.0.0.1:5080, -l eth0:5062,
                  -l "sctp:(eth0)", -l "(eth0, eth1, 127.0.0.1):5065".
                  The default behaviour is to listen on all the interfaces.
    -n processes Number of child processes to fork per interface
                  (default: 8)
    -r           Use dns to check if is necessary to add a "received="
                  field to a via
    -R           Same as `-r` but use reverse dns;
                  (to use both use `-rR`)
    -v           Turn on "via:" host checking when forwarding replies
    -d           Debugging mode (multiple -d increase the level)
    -D no        1..do not fork (almost) anyway, 2..do not daemonize creator
                  3..daemonize (default)
    -E           Log to stderr
    -T           Disable tcp
    -N           Number of tcp child processes (default: equal to `-n')
    -W type      poll method (depending on support in OS, it can be: poll,
                  epoll_lt, epoll_et, sigio_rt, select, kqueue, /dev/poll)
    -V           Version number
    -h           This help message
    -b nr        Maximum receive buffer size which will not be exceeded by
                  auto-probing procedure even if  OS allows
    -m nr        Size of shared memory allocated in Megabytes
    -w dir       Change the working directory to "dir" (default: "/")
    -t dir       Chroot to "dir"
    -u uid       Change uid
    -g gid       Change gid
    -P file      Create a pid file
    -G file      Create a pgid file
    -O nr        Script optimization level (debugging option)
    -a mode      Auto aliases mode: enable with yes or on,
                  disable with no or off
    -A define    Add config pre-processor define (e.g., -A WITH_AUTH)

# ser -V
version: ser 3.0.99-dev1 (i386/linux) b72876
flags: STATS: Off, USE_IPV6, USE_TCP, USE_TLS, TLS_HOOKS, USE_RAW_SOCKS,
DISABLE_NAGLE, USE_MCAST, DNS_IP_HACK, SHM_MEM, SHM_MMAP, PKG_MALLOC, DBG_QM_MALLOC,
USE_FUTEX, FAST_LOCK-ADAPTIVE_WAIT, USE_DNS_CACHE, USE_DNS_FAILOVER, USE_NAPTR, USE_DST_BLACKLIST, HAVE_RESOLV_RES
ADAPTIVE_WAIT_LOOPS=1024, MAX_RECV_BUFFER_SIZE 262144, MAX_LISTEN 16, MAX_URI_SIZE 1024, BUF_SIZE 65535, PKG_SIZE 4MB
poll method support: poll, epoll_lt, epoll_et, sigio_rt, select.
id: b72876
compiled on 17:16:21 Sep  3 2010 with gcc 4.4.3

Source Code Repository Statistics

Commits

Nine years later since first one, the number of new development commits is 14200:

git log --pretty=format:"%h%x09%an%x09%ad%x09%s" --reverse | wc -l

14200

This number represents only the commits done in development branch (GIT master branch). Over all, the number of commits is far more, since every release had its own branch. However, the number includes the commits done during 2005-2008 within Kamailio (OpenSER) project in SVN development branch (SVN trunk).

Lines in repository

The total number of files and lines used in repository (code, comments, ...):

Total files: 4701
Total lines: 941217

Current lines of code statistics:

ansic:       392801 (73.38%)
xml:         124390 (23.24%)
sh:            8965 (1.67%)
yacc:          3393 (0.63%)
perl:          3314 (0.62%)
python:        1372 (0.26%)
php:           1047 (0.20%)
awk:             43 (0.01%)

Modules

Current development branch counts over 170 modules (extensions)

ls modules | wc -w
41

app_lua        cfg_rpc       debugger     matrix      pipelimit  tm
app_python     counters      dialplan     mediaproxy  privacy    topoh
auth           ctl           enum         mi_rpc      sanity     utils
auth_identity  db_berkeley   geoip        mqueue      sl         xhttp
avpops         db_flatstore  iptrtpproxy  mtree       sms        xmlops
carrierroute   db_mysql      lcr          pdb         textopsx   xmlrpc
cfg_db         db_postgres   malloc_test  peering     tls


ls modules_k | wc -w
82

acc            domainpolicy  nathelper            pua_usrloc  sqlops
acc_radius     drouting      nat_traversal        pua_xmpp    sst
alias_db       exec          osp                  purple      statistics
auth_db        group         path                 pv          textops
auth_diameter  h350          pdt                  qos         tmx
auth_radius    htable        perl                 ratelimit   uac
benchmark      imc           perlvdb              regex       uac_redirect
call_control   jabber        permissions          registrar   uri_db
cfgutils       kex           pike                 rls         userblacklist
cpl-c          ldap          presence             rr          usrloc
db_oracle      maxfwd        presence_dialoginfo  rtimer      xcap_client
db_text        memcached     presence_mwi         rtpproxy    xcap_server
db_unixodbc    mi_datagram   presence_xml         seas        xlog
dialog         mi_fifo       pua                  siptrace    xmpp
dispatcher     misc_radius   pua_bla              siputils
diversion      mi_xmlrpc     pua_dialoginfo       snmpstats
domain         msilo         pua_mi               speeddial

ls modules_s | wc -w
51

acc_db       bdb         domain   maxfwd       pike          speeddial   usrloc
acc_radius   blst        eval     msilo        prefix_route  textops     xcap
acc_syslog   cpl-c       exec     nathelper    presence_b2b  timer       xlog
auth_db      db_ops      fifo     options      print         uac
auth_radius  dbtext      gflags   oracle       print_lib     unixsock
avp          dialog      jabber   osp          ratelimit     uri
avp_db       dispatcher  ldap     pdt          registrar     uri_db
avp_radius   diversion   mangler  permissions  rr            uri_radius

Version 0.8.8 had 22, some of them no longer available:

acc   cpl    ext  jabber  msilo   pike   radius_acc   registrar    sl    textops  usrloc
auth  cpl-c  exec im      maxfwd  mysql  print        radius_auth  rr    sms      tm

Commit Statistics

Next table presents the most productive time frames in number of commits and percentage.

Description Value Commits Percent
Top Hour 17:00-18:00 1259 8.87
Top Day Thursday 2775 19.54
Top Month March 1727 12.16
Top Year 2009 2267 15.96

Here are the screenshots with detailed representation over the time of commits:

http://sip-router.org/pub/img/9-years/ser-commits-hour-of-day.png

http://sip-router.org/pub/img/9-years/ser-commits-hour-of-day-graph.png

http://sip-router.org/pub/img/9-years/ser-commits-hour-of-week.png

http://sip-router.org/pub/img/9-years/ser-commits-day-of-week.png

http://sip-router.org/pub/img/9-years/ser-commits-month-of-year.png

http://sip-router.org/pub/img/9-years/ser-commits-by-year.png

http://sip-router.org/pub/img/9-years/ser-commits-by-year-table.png

Ohloh Statistics

Many other statistics can be found at:

For example, estimated cost to develop the project from scratch is over 8 millions USD:

http://sip-router.org/pub/img/9-years/ser-ohloh-project-cost.png

Config Files

One of most interesting evolutions inside the projects was the default configuration file. Started with a completely different format, based on regular expression matching, changed quickly in a programmable language, format that continues today.

First Ever Config

If you ever wondered how was the first config file published, then here it is:

# $Id: sip_router.cfg,v 1.1.1.1 2001/09/03 21:27:11 andrei Exp $

# format:
#  method_re   sip_uri_re      dest_host
# (warning: re cannot contain space)

^R.*        ^sip:.*@dorian.*   ekina.fokus.gmd.de
^INVITE     .*                 ape             # my laptop
.           .                  192.168.46.55
.*          .*andrei           helios.fokus.gmd.de


# end

Probably didn't make it in any production system with this format. The config language was completely different than what is today.

First Release Config

One of first public official releases was 0.8.8 (burned on CD). The config language structure was pretty much the same like today.

Here is the config:

#
# $Id: ser.cfg,v 1.10 2002/09/26 10:54:48 janakj Rel $
#
# simple quick-start config script
#

# ----------- global configuration parameters ------------------------

debug=3          # debug level (cmd line: -dddddddddd)
fork=yes
log_stderror=no # (cmd line: -E)
check_via=no    # (cmd. line: -v)
dns=no           # (cmd. line: -r)
rev_dns=no      # (cmd. line: -R)
port=5060
children=4
fifo="/tmp/ser_fifo"

# ------------------ module loading ----------------------------------

# Uncomment this if you want to use SQL database
#loadmodule "/usr/lib/ser/modules/mysql.so"

loadmodule "/usr/lib/ser/modules/sl.so"
loadmodule "/usr/lib/ser/modules/print.so"
loadmodule "/usr/lib/ser/modules/tm.so"
loadmodule "/usr/lib/ser/modules/rr.so"
loadmodule "/usr/lib/ser/modules/maxfwd.so"
loadmodule "/usr/lib/ser/modules/usrloc.so"
loadmodule "/usr/lib/ser/modules/registrar.so"

# Uncomment this if you want digest authentication
# mysql.so must be loaded !
#loadmodule "/usr/lib/ser/modules/auth.so"

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

# -- usrloc params --

modparam("usrloc", "db_mode",   0)

# Uncomment this if you want to use SQL database
# for persistent storage and comment the previous line
#modparam("usrloc", "db_mode", 2)

# -- auth params --
# Uncomment if you are using auth module
#
#modparam("auth", "secret", "alsdkhglaksdhfkloiwr")
#modparam("auth", "calculate_ha1", yes)
#
# If you set "calculate_ha1" parameter to yes (which true in this config),
# uncomment also the following parameter)
#
#modparam("auth", "password_column", "password")

# -------------------------  request routing logic -------------------

# main routing logic

route{

    # initial sanity checks -- discard local ACKs, messages with
    # max_forwars==0, or excessively long requests
    sl_filter_ACK();
    if (!mf_process_maxfwd_header("10")) {
        sl_send_reply("483","Too Many Hops");
        break;
    };
    if (len_gt( max_len )) {
        sl_send_reply("513", "Message too big");
        break;
    };

    # Do strict routing if pre-loaded route headers present
    rewriteFromRoute();

    # if the request is for other domain use UsrLoc
    # (in case, it does not work, use the following command
    # with proper names and addresses in it)
    if (uri==myself) {

        if (method=="REGISTER") {

# Uncomment this if you want to use digest authentication
#           if (!www_authorize("iptel.org", "subscriber")) {
#               www_challenge("iptel.org", "0");
#               break;
#           };

            save("location");
            break;
        };

        # native SIP destinations are handled using our USRLOC DB
        if (!lookup("location")) {
            sl_send_reply("404", "Not Found");
            break;
        };
    };
    # forward to current uri now
    if (!t_relay()) {
        sl_reply_error();
    };

}

Default Configs Over Years

Nowadays we build two applications from same source code: Kamailio and SER.

Next sections show the latest versions of default config files. If you want to see the evolution for each major version, then follow the links:

The Configs Today

SER Default Config

You can notice in this config the modularity with sub-routes and the usage of string names for routes (e.g., route[REGISTRAR]), first ever introduced by SER in 2007.

#
# $Id$
#

# First start SER sample config script with:
#   database, accounting, authentication, multi-domain support
#   PSTN GW section, named flags, named routes, global-,
#   domain- and user-preferences with AVPs
# Several of these features are only here for demonstration purpose
# what can be achieved with the SER config script language.
#
# If you look for a simpler version with a lot less dependencies
# please refer to the ser-basic.cfg file in your SER distribution.

# To get this config running you need to execute the following commands
# with the new serctl (the capital word are just place holders)
# - ser_ctl domain add DOMAINNAME
# - ser_ctl user add USERNAME@DOMAINNAME -p PASSWORD
# If you want to have PID header for your user
# - ser_attr add uid=UID asserted_id="PID"
# If you want to have gateway support
# - ser_db add attr_types name=gw_ip rich_type=string raw_type=2 description="The gateway IP for the default ser.cfg" default_flags=33
# - ser_attr add global gw_ip=GATEWAY-IP

# ----------- global configuration parameters ------------------------

debug=2         # debug level (cmd line: -dddddddddd)
#memdbg=10 # memory debug log level
#memlog=10 # memory statistics log level
#log_facility=LOG_LOCAL0 # sets the facility used for logging (see syslog(3))

/* Uncomment these lines to enter debugging mode
fork=no
log_stderror=yes
*/

check_via=no    # (cmd. line: -v)
dns=no          # (cmd. line: -r)
rev_dns=no      # (cmd. line: -R)
#port=5060
#children=4
#user=ser
#group=ser
#disable_core=yes #disables core dumping
#open_fd_limit=1024 # sets the open file descriptors limit
#mhomed=yes  # useful for multihomed hosts, small performance penalty
#disable_tcp=yes
#tcp_accept_aliases=yes # accepts the tcp alias via option (see NEWS)
enable_tls=yes

#

# ------------------ module loading ----------------------------------

#loadpath "modules:modules_s"
loadpath "/usr/lib/ser/modules:/usr/lib/ser/modules_s"

# load a SQL database for authentication, domains, user AVPs etc.
loadmodule "db_mysql"

loadmodule "sl"
loadmodule "tm"
loadmodule "rr"
loadmodule "maxfwd"
loadmodule "usrloc"
loadmodule "registrar"
loadmodule "xlog"
loadmodule "textops"
loadmodule "ctl"
loadmodule "cfg_rpc"
loadmodule "auth"
loadmodule "auth_db"
loadmodule "gflags"
loadmodule "domain"
loadmodule "uri_db"
loadmodule "avp"
loadmodule "avp_db"
loadmodule "acc_db"
loadmodule "xmlrpc"
#loadmodule "tls"

# ----------------- setting script FLAGS -----------------------------
flags
  FLAG_ACC          : 1,  # include message in accounting
  FLAG_FAILUREROUTE : 2;  # we are operating from a failure route

avpflags
  dialog_cookie;        # handled by rr module

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

# specify the path to you database here
modparam("acc_db|auth_db|avp_db|domain|gflags|usrloc|uri_db", "db_url", "mysql://ser:heslo@127.0.0.1/ser")

# -- usrloc params --

# as we use the database anyway we will use it for usrloc as well
modparam("usrloc", "db_mode", 1)

# -- auth params --
modparam("auth_db", "calculate_ha1", yes)
modparam("auth_db", "plain_password_column", "password")

# -- rr params --
# add value to ;lr param to make some broken UAs happy
modparam("rr", "enable_full_lr", 1)
#
# limit the length of the AVP cookie to only necessary ones
modparam("rr", "cookie_filter", "(account)")
#
# you probably do not want that someone can simply read and change
# the AVP cookie in your Routes, thus should really change this
# secret value below
modparam("rr", "cookie_secret", "MyRRAVPcookiesecret")

# -- gflags params --
# load the global AVPs
modparam("gflags", "load_global_attrs", 1)

# -- domain params --
# load the domain AVPs
modparam("domain", "load_domain_attrs", 1)

# -- ctl params --
# by default ctl listens on unixs:/tmp/ser_ctl if no other address is
# specified in modparams; this is also the default for sercmd
modparam("ctl", "binrpc", "unixs:/tmp/ser_ctl")
# listen on the "standard" fifo for backward compatibility
modparam("ctl", "fifo", "fifo:/tmp/ser_fifo")
# listen on tcp, localhost
#modparam("ctl", "binrpc", "tcp:localhost:2046")

# -- acc_db params --
# failed transactions (=negative responses) should be logged to
modparam("acc_db", "failed_transactions", 1)

# comment the next line if you dont want to have accounting to DB
modparam("acc_db", "log_flag", "FLAG_ACC")

# -- tm params --
# uncomment the following line if you want to avoid that each new reply
# restarts the resend timer (see INBOUND route below)
#modparam("tm", "restart_fr_on_each_reply", "0")

# -- xmlrpc params --
# using a sub-route from the module is a lot safer than relying on the
# request method to distinguish HTTP from SIP
modparam("xmlrpc", "route", "RPC");

# -------------------------  request routing logic -------------------

# main routing logic

route{
    # if you have a PSTN gateway just un-comment the follwoing line and
    # specify the IP address of it to route calls to it
    #$gw_ip = "1.2.3.4"

    # first do some initial sanity checks
    route(INIT);

    # bypass the rest of the script for CANCELs if possible
    route(CATCH_CANCEL);

    # check if the request is routed via Route header or
    # needs a Record-Route header
    route(RR);

    # check if the request belongs to our proxy
    route(DOMAIN);

    # handle REGISTER requests
    route(REGISTRAR);

    # from here on we want to know you is calling
    route(AUTHENTICATION);

    # check if we should be outbound proxy for a local user
    route(OUTBOUND);

    # check if the request is for a local user
    route(INBOUND);

    # here you could for example try to do an ENUM lookup before
    # the call gets routed to the PSTN
    #route(ENUM);

    # lets see if someone wants to call a PSTN number
    route(PSTN);

    # nothing matched, reject it finally
    sl_reply("404", "No route matched");
}

route[FORWARD]
{
    # here you could decide wether this call needs a RTP relay or not

    # if this is called from the failure route we need to open a new branch
    if (isflagset(FLAG_FAILUREROUTE)) {
        append_branch();
    }

    # if this is an initial INVITE (without a To-tag) we might try another
    # (forwarding or voicemail) target after receiving an error
    if (method=="INVITE" && strempty(@to.tag)) {
        t_on_failure("FAILURE_ROUTE");
    }

    # send it out now; use stateful forwarding as it works reliably
    # even for UDP2TCP
    if (!t_relay()) {
        sl_reply_error();
    }
    drop;
}

route[INIT]
{
    # initial sanity checks -- messages with
    # max_forwards==0, or excessively long requests
    if (!mf_process_maxfwd_header("10")) {
        sl_reply("483","Too Many Hops");
        drop;
    }

    if (msg:len >=  4096 ) {
        sl_reply("513", "Message too big");
        drop;
    }

    # you could add some NAT detection here for example

    # or you cuold call here some of the check from the sanity module

    # lets account all initial INVITEs
    # further in-dialog requests are accounted by a RR cookie (see below)
    if (method=="INVITE" && strempty(@to.tag)) {
        setflag(FLAG_ACC);
    }
}

route[RPC]
{
    # allow XMLRPC from localhost
    if ((method=="POST" || method=="GET") &&
        src_ip==127.0.0.1) {

        if (msg:len >= 8192) {
            sl_reply("513", "Request to big");
            drop;
        }

        # lets see if a module wants to answer this
        dispatch_rpc();
        drop;
    }
}

route[RR]
{
    # subsequent messages withing a dialog should take the
    # path determined by record-routing
    if (loose_route()) {
        # mark routing logic in request
        append_hf("P-hint: rr-enforced\r\n");

        # if the Route contained the accounting AVP cookie we
        # set the accounting flag for the acc_db module.
        # this is more for demonstration purpose as this could
        # also be solved without RR cookies.
        # Note: this means all in-dialog request will show up in the
        # accounting tables, so prepare your accounting software for this ;-)
        if ($account == "yes") {
            setflag(FLAG_ACC);
        }

        # for broken devices which overwrite their Route's with each
        # (not present) RR from within dialog requests it is better
        # to repeat the RRing
        # and if we call rr after loose_route the AVP cookies are restored
        # automatically :)
        record_route();

        route(FORWARD);
    } else if (!method=="REGISTER") {
        # we record-route all messages -- to make sure that
        # subsequent messages will go through our proxy; that's
        # particularly good if upstream and downstream entities
        # use different transport protocol

        # if the inital INVITE got the ACC flag store this in
        # an RR AVP cookie. this is more for demonstration purpose
        if (isflagset(FLAG_ACC)) {
            $account = "yes";
            setavpflag($account, "dialog_cookie");
        }

        record_route();
    }
}

route[DOMAIN]
{
    # check if the caller is from a local domain
    lookup_domain("$fd", "@from.uri.host");

    # check if the callee is at a local domain
    lookup_domain("$td", "@ruri.host");

    # we dont know the domain of the caller and also not
    # the domain of the callee -> somone uses our proxy as
    # a relay
    if (strempty($t.did) && strempty($f.did)) {
        sl_reply("403", "Relaying Forbidden");
        drop;
    }
}

route[REGISTRAR]
{
    # if the request is a REGISTER lets take care of it
    if (method=="REGISTER") {
        # check if the REGISTER if for one of our local domains
        if (strempty($t.did)) {
            sl_reply("403", "Register forwarding forbidden");
            drop;
        }

        # we want only authenticated users to be registered
        if (!www_authenticate("$fd.digest_realm", "credentials")) {
            if ($? == -2) {
                sl_reply("500", "Internal Server Error");
            } else if ($? == -3) {
                sl_reply("400", "Bad Request");
            } else {
                if ($digest_challenge != "") {
                    append_to_reply("%$digest_challenge");
                }
                sl_reply("401", "Unauthorized");
            }
            drop;
        }

        # check if the authenticated user is the same as the target user
        if (!lookup_user("$tu.uid", "@to.uri")) {
            sl_reply("404", "Unknown user in To");
            drop;
        }

        if ($f.uid != $t.uid) {
            sl_reply("403", "Authentication and To-Header mismatch");
            drop;
        }

        # check if the authenticated user is the same as the request originator
        # you may uncomment it if you care, what uri is in From header
        #if (!lookup_user("$fu.uid", "@from.uri")) {
        #   sl_reply("404", "Unknown user in From");
        #   drop;
        #}
        #if ($fu.uid != $tu.uid) {
        #   sl_reply("403", "Authentication and From-Header mismatch");
        #   drop;
        #}

        # everything is fine so lets store the binding
        if (!save_contacts("location")) {
            sl_reply("400", "Invalid REGISTER Request");
            drop;
        }
        drop;
    }
}

route[AUTHENTICATION]
{
    if (method=="CANCEL" || method=="ACK") {
        # you are not allowed to challenge these methods
        break;
    }

    # requests from non-local to local domains should be permitted
    # remove this if you want a walled garden
    if (strempty($f.did)) {
        break;
    }

    # as gateways are usually not able to authenticate for their
    # requests you will have trust them base on some other information
    # like the source IP address. WARNING: if at all this is only safe
    # in a local network!!!
    #if (src_ip==a.b.c.d) {
    #   break;
    #}

    if (!proxy_authenticate("$fd.digest_realm", "credentials")) {
        if ($? == -2) {
            sl_reply("500", "Internal Server Error");
        } else if ($? == -3) {
            sl_reply("400", "Bad Request");
        } else {
            if ($digest_challenge != "") {
                append_to_reply("%$digest_challenge");
            }
            sl_reply("407", "Proxy Authentication Required");
        }
        drop;
    }

    # check if the UID from the authentication meets the From header
    $authuid = $uid;
    if (!lookup_user("$fu.uid", "@from.uri")) {
        del_attr("$uid");
    }
    if ($fu.uid != $fr.authuid) {
        sl_reply("403", "Fake Identity");
        drop;
    }
    # load the user AVPs (preferences) of the caller, e.g. for RPID header
    load_attrs("$fu", "$f.uid");
}

route[OUTBOUND]
{
    # if a local user calls to a foreign domain we play outbound proxy for him
    # comment this out if you want a walled garden
    if ($f.did != ""  && $t.did == "") {
        append_hf("P-hint: outbound\r\n");
        route(FORWARD);
    }
}

route[INBOUND]
{
    # lets see if know the callee
    if (lookup_user("$tu.uid", "@ruri")) {

        # load the preferences of the callee to have his timeout values loaded
        load_attrs("$tu", "$t.uid");

        # if you want to know if the callee username was an alias
        # check it like this
        #if (strempty($tu.uri_canonical)) {
            # if the alias URI has different AVPs/preferences
            # you can load them into the URI track like this
            #load_attrs("$tr", "@ruri");
        #}

        # check for call forwarding of the callee
        # Note: the forwarding target has to be full routable URI
        #       in this example
        if ($tu.fwd_always_target != "") {
            attr2uri("$tu.fwd_always_target");
            route(FORWARD);
        }

        # native SIP destinations are handled using our USRLOC DB
        if (lookup_contacts("location")) {
            append_hf("P-hint: usrloc applied\r\n");

            # we set the TM module timers according to the prefences
            # of the callee (avoid too long ringing of his phones)
            # Note1: timer values have to be in ms now!
            # Note2: this makes even more sense if you switch to a voicemail
            #        from the FAILURE_ROUTE below
            if ($t.fr_inv_timer != 0) {
                if ($t.fr_timer != 0) {
                    t_set_fr("$t.fr_inv_timer", "$t.fr_timer");
                } else {
                    t_set_fr("$t.fr_inv_timer");
                }
            }

            route(FORWARD);
        } else {
            sl_reply("480", "User temporarily not available");
            drop;
        }
    }
}

route[PSTN]
{
    # Only if the AVP 'gw_ip' is set and the request URI contains
    # only a number we consider sending this to the PSTN GW.
    # Only users from a local domain are permitted to make calls.
    # Additionally you might want to check the acl AVP to verify
    # that the user is allowed to make such expensives calls.
    if ($f.did != "" && $gw_ip != "" &&
        uri=~"sips?:\+?[0-9]{3,18}@.*") {
        # probably you need to convert the number in the request
        # URI according to the requirements of your gateway here

        # if an AVP 'asserted_id' is set we insert an RPID header
        if ($asserted_id != "") {
            xlset_attr("$rpidheader", "<sip:%$asserted_id@%@ruri.host>;screen=yes");
            replace_attr_hf("Remote-Party-ID", "$rpidheader");
        }

        # just replace the domain part of the RURI with the
        # value from the AVP and send it out
        attr2uri("$gw_ip", "domain");
        route(FORWARD);
    }
}

route[CATCH_CANCEL] {
    # check whether there is a corresponding INVITE to the CANCEL,
    # and bypass the rest of the script if possible

    if (method == CANCEL) {
        if (!t_relay_cancel()) { # implicit drop if the INVITE was found

            # INVITE was found but some error occurred
            sl_reply("500", "Internal Server Error");
            drop;
        }
        # bad luck, no corresponding INVITE was found,
        # we have to continue with the script
    }
}

failure_route[FAILURE_ROUTE]
{
    # mark for the other routes that we are operating from here on from a
    # failure route
    setflag(FLAG_FAILUREROUTE);

    if (t_check_status("486|600")) {
        # if we received a busy and a busy target is set, forward it there
        # Note: again the forwarding target has to be a routeable URI
        if ($tu.fwd_busy_target != "") {
            attr2uri("$tu.fwd_busy_target");
            route(FORWARD);
        }
        # alternatively you could forward the request to SEMS/voicemail here
    }
    else if (t_check_status("408|480")) {
        # if we received no answer and the noanswer target is set,
        # forward it there
        # Note: again the target has to be a routeable URI
        if ($tu.fwd_noanswer_target != "") {
            attr2uri("$tu.fwd_noanswer_target");
            route(FORWARD);
        }
        # alternatively you could forward the request to SEMS/voicemail here
    }
}

Kamailio Default Config

You can notice here the usage of config defines (#!define XYZ,

!ifdef XYZ, ...) which makes very easy to enable/disable features as

well as defining values for tokens that are replaced later in config (e.g., DBURL). The config provides advanced features such as NAT traversal with RTPProxy or presence server.

#!KAMAILIO
#
# Kamailio (OpenSER) SIP Server v3.1 - default configuration script
#     - web: http://www.kamailio.org
#     - git: http://sip-router.org
#
# Direct your questions about this file to: <sr-users@lists.sip-router.org>
#
# Refer to the Core CookBook at http://www.kamailio.org/dokuwiki/doku.php
# for an explanation of possible statements, functions and parameters.
#
# Several features can be enabled using '#!define WITH_FEATURE' directives:
#
# *** To run in debug mode:
#     - define WITH_DEBUG
#
# *** To enable mysql:
#     - define WITH_MYSQL
#
# *** To enable authentication execute:
#     - enable mysql
#     - define WITH_AUTH
#     - add users using 'kamctl'
#
# *** 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
#
# *** To enable nat traversal execute:
#     - define WITH_NAT
#     - install RTPProxy: http://www.rtpproxy.org
#     - start RTPProxy:
#        rtpproxy -l _your_public_ip_ -s udp:localhost:7722
#
# *** To enable PSTN gateway routing execute:
#     - define WITH_PSTN
#     - set the value of pstn.gw_ip
#     - check route[PSTN] for regexp routing condition
#
# *** 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 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 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

# *** Value defines - IDs used later in config
#!ifdef WITH_MYSQL
# - database URL - used to connect to database server by modules such
#       as: auth_db, acc, usrloc, a.s.o.
#!define DBURL "mysql://openser:openserrw@localhost/openser"
#!endif

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

#!ifdef WITH_DEBUG
debug=4
log_stderror=yes
#!else
debug=2
log_stderror=no
#!endif

memdbg=5
memlog=5

log_facility=LOG_LOCAL0

fork=yes
children=4

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

/* 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 */
#alias="sip.mydomain.com"

port=5060

/* uncomment and configure the following line if you want Kamailio to
   bind on a specific interface/port/proto (default bind on all available) */
#listen=udp:10.0.0.10:5060


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


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

#set module path
mpath="/usr/local/lib/kamailio/modules_k/:/usr/local/lib/kamailio/modules/"

/* uncomment next line for MySQL DB support */
#!ifdef WITH_MYSQL
loadmodule "db_mysql.so"
#!endif
loadmodule "mi_fifo.so"
loadmodule "kex.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 "uri_db.so"
loadmodule "siputils.so"
loadmodule "xlog.so"
loadmodule "sanity.so"
loadmodule "ctl.so"
loadmodule "mi_rpc.so"
loadmodule "acc.so"
#!ifdef WITH_AUTH
loadmodule "auth.so"
loadmodule "auth_db.so"
#!ifdef WITH_IPAUTH
loadmodule "permissions.so"
#!endif
#!endif
/* uncomment next line for aliases support
   NOTE: a DB (like db_mysql) module must be also loaded */
#loadmodule "alias_db.so"
/* uncomment next line for multi-domain support
   NOTE: a DB (like db_mysql) module must be also loaded
   NOTE: be sure and enable multi-domain support in all used modules
         (see "multi-module params" section ) */
#loadmodule "domain.so"
#!ifdef WITH_PRESENCE
loadmodule "presence.so"
loadmodule "presence_xml.so"
#!endif

#!ifdef WITH_NAT
loadmodule "nathelper.so"
loadmodule "rtpproxy.so"
#!endif

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


# ----- mi_fifo params -----
modparam("mi_fifo", "fifo_name", "/tmp/kamailio_fifo")


# ----- 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 -----
# add value to ;lr param to cope with most of the UAs
modparam("rr", "enable_full_lr", 1)
# do not append from tag to the RR (no need for this script)
modparam("rr", "append_fromtag", 0)


# ----- rr 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)


# ----- uri_db params -----
/* by default we disable the DB support in the module as we do not need it
   in this configuration */
modparam("uri_db", "use_uri_table", 0)
modparam("uri_db", "db_url", "")


# ----- acc params -----
/* what sepcial events should be accounted ? */
modparam("acc", "early_media", 1)
modparam("acc", "report_ack", 1)
modparam("acc", "report_cancels", 1)
/* 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", "failed_transaction_flag", 3)
modparam("acc", "log_flag", 1)
modparam("acc", "log_missed_flag", 2)
modparam("acc", "log_extra",
    "src_user=$fU;src_domain=$fd;dst_ouser=$tU;dst_user=$rU;dst_domain=$rd")
/* enhanced DB accounting */
#!ifdef WITH_ACCDB
modparam("acc", "db_flag", 1)
modparam("acc", "db_missed_flag", 2)
modparam("acc", "db_url", DBURL)
modparam("acc", "db_extra",
    "src_user=$fU;src_domain=$fd;dst_ouser=$tU;dst_user=$rU;dst_domain=$rd")
#!endif

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

# ----- auth_db params -----
/* enable the DB based authentication */
#!ifdef WITH_AUTH
modparam("auth_db", "calculate_ha1", yes)
modparam("auth_db", "password_column", "password")
modparam("auth_db", "db_url", DBURL)
modparam("auth_db", "load_credentials", "")

#!ifdef WITH_IPAUTH
modparam("permissions", "db_url", DBURL)
modparam("permissions", "db_mode", 1)
#!endif

#!endif

# ----- alias_db params -----
/* uncomment the following lines if you want to enable the DB based
   aliases */
#modparam("alias_db", "db_url", DBURL)


# ----- domain params -----
/* uncomment the following lines to enable multi-domain detection
   support */
#modparam("domain", "db_url", DBURL)
#modparam("domain", "db_mode", 1)   # Use caching


# ----- multi-module params -----
/* uncomment the following line if you want to enable multi-domain support
   in the modules (dafault off) */
#modparam("alias_db|auth_db|usrloc|uri_db", "use_domain", 1)


# ----- presence params -----
/* enable presence server support */
#!ifdef WITH_PRESENCE
modparam("presence|presence_xml", "db_url", DBURL)
modparam("presence_xml", "force_active", 1)
modparam("presence", "server_address", "sip:10.0.0.10:5060")
#!endif

#!ifdef WITH_NAT
# ----- rtpproxy -----
modparam("rtpproxy", "rtpproxy_sock", "udp:127.0.0.1:7722")
# ----- nathelper -----
modparam("nathelper", "natping_interval", 30)
modparam("nathelper", "ping_nated_only", 1)
modparam("nathelper", "sipping_bflag", 7)
modparam("nathelper", "sipping_from", "sip:pinger@kamailio.org")
modparam("registrar|nathelper", "received_avp", "$avp(i:80)")
modparam("usrloc", "nat_bflag", 6)
#!endif

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


# main request routing logic

route {

    # per request initial checks
    route(REQINIT);

    # NAT detection
    route(NAT);

    # handle requests within SIP dialogs
    route(WITHINDLG);

    ### only initial requests (no To tag)

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

    t_check_trans();

    # 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(1); # 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);

    route(RELAY);
}


route[RELAY] {
#!ifdef WITH_NAT
    if (check_route_param("nat=yes")) {
        setbflag("6");
    }
    if (isflagset(5) || isbflagset("6")) {
        route(RTPPROXY);
    }
#!endif

    /* example how to enable some additional event routes */
    if (is_method("INVITE")) {
        #t_on_branch("BRANCH_ONE");
        t_on_reply("REPLY_ONE");
        t_on_failure("FAIL_ONE");
    }

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

# Per SIP request initial checks
route[REQINIT] {
    if (!mf_process_maxfwd_header("10")) {
        sl_send_reply("483","Too Many Hops");
        exit;
    }

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

# Handle requests within SIP dialogs
route[WITHINDLG] {
    if (has_totag()) {
        # sequential request withing a dialog should
        # take the path determined by record-routing
        if (loose_route()) {
            if (is_method("BYE")) {
                setflag(1); # do accounting ...
                setflag(3); # ... even if the transaction fails
            }
            route(RELAY);
        } else {
            if (is_method("SUBSCRIBE") && uri == myself) {
                # in-dialog subscribe requests
                route(PRESENCE);
                exit;
            }
            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.\n");
                    exit;
                }
            }
            sl_send_reply("404","Not here");
        }
        exit;
    }
}

# Handle SIP registrations
route[REGISTRAR] {
    if (is_method("REGISTER"))
    {
        if(isflagset(5))
        {
            setbflag("6");
            # uncomment next line to do SIP NAT pinging
            ## setbflag("7");
        }
        if (!save("location"))
            sl_reply_error();

        exit;
    }
}

# USER location service
route[LOCATION] {
    # apply DB based aliases (uncomment to enable)
    ##alias_db_lookup("dbaliases");

    if (!lookup("location")) {
        switch ($rc) {
            case -1:
            case -3:
                t_newtran();
                t_reply("404", "Not Found");
                exit;
            case -2:
                sl_send_reply("405", "Method Not Allowed");
                exit;
        }
    }

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

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

#!ifdef WITH_PRESENCE
    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;
}

# Authentication route
route[AUTH] {
#!ifdef WITH_AUTH
    if (is_method("REGISTER"))
    {
        # authenticate the REGISTER requests (uncomment to enable auth)
        if (!www_authorize("$td", "subscriber"))
        {
            www_challenge("$td", "0");
            exit;
        }

        if ($au!=$tU)
        {
            sl_send_reply("403","Forbidden auth ID");
            exit;
        }
    } else {

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

        # authenticate if from local subscriber
        if (from_uri==myself)
        {
            if (!proxy_authorize("$fd", "subscriber")) {
                proxy_challenge("$fd", "0");
                exit;
            }
            if (is_method("PUBLISH"))
            {
                if ($au!=$tU) {
                    sl_send_reply("403","Forbidden auth ID");
                    exit;
                }
            } else {
                if ($au!=$fU) {
                    sl_send_reply("403","Forbidden auth ID");
                    exit;
                }
            }

            consume_credentials();
            # caller authenticated
        } else {
            # caller is not local subscriber, then check if it calls
            # a local destination, otherwise deny, not an open relay here
            if (!uri==myself)
            {
                sl_send_reply("403","Not relaying");
                exit;
            }
        }
    }
#!endif
    return;
}

# Caller NAT detection route
route[NAT] {
#!ifdef WITH_NAT
    force_rport();
    if (nat_uac_test("19")) {
        if (method=="REGISTER") {
            fix_nated_register();
        } else {
            fix_nated_contact();
        }
        setflag(5);
    }
#!endif
    return;
}

# RTPProxy control
route[RTPPROXY] {
#!ifdef WITH_NAT
    if (is_method("BYE")) {
        unforce_rtp_proxy();
    } else if (is_method("INVITE")){
        force_rtp_proxy();
    }
    if (!has_totag()) add_rr_param(";nat=yes");
#!endif
    return;
}

# Routing to foreign domains
route[SIPOUT] {
    if (!uri==myself)
    /* replace with following line if multi-domain support is used */
    ##if (!is_uri_host_local())
    {
        append_hf("P-hint: outbound\r\n");
        route(RELAY);
    }
}

# 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 rotuing 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;
    }

    $ru = "sip:" + $rU + "@" + $sel(cfg_get.pstn.gw_ip);

    route(RELAY);
    exit;
#!endif

    return;
}

# Sample branch router
branch_route[BRANCH_ONE] {
    xdbg("new branch at $ru\n");
}

# Sample onreply route
onreply_route[REPLY_ONE] {
    xdbg("incoming reply\n");
#!ifdef WITH_NAT
    if ((isflagset(5) || isbflagset("6")) && status=~"(183)|(2[0-9][0-9])") {
        force_rtp_proxy();
    }
    if (isbflagset("6")) {
        fix_nated_contact();
    }
#!endif
}

# Sample failure route
failure_route[FAIL_ONE] {
#!ifdef WITH_NAT
    if (is_method("INVITE")
            && (isbflagset("6") || isflagset(5))) {
        unforce_rtp_proxy();
    }
#!endif

    if (t_is_canceled()) {
        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;
    ##}

    # uncomment the following lines if you want to redirect the failed
    # calls to a different new destination
    ##if (t_check_status("486|408")) {
    ##  sethostport("192.168.2.100:5060");
    ##  append_branch();
    ##  # do not set the missed call flag again
    ##  t_relay();
    ##}
}