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.

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:

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

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).

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.

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).

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.

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 # usefull 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 then 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 branchif(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 errorif(method=="INVITE"&& strempty(@to.tag)){
t_on_failure("FAILURE_ROUTE");}# send it out now; use stateful forwarding as it works reliably# even for UDP2TCPif(!t_relay()){
sl_reply_error();}
drop;}
route[INIT]{# initial sanity checks -- messages with# max_forwards==0, or excessively long requestsif(!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 localhostif((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-routingif(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);}elseif(!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 purposeif(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 relayif(strempty($t.did)&& strempty($f.did)){
sl_reply("403","Relaying Forbidden");
drop;}}
route[REGISTRAR]{# if the request is a REGISTER lets take care of itif(method=="REGISTER"){# check if the REGISTER if for one of our local domainsif(strempty($t.did)){
sl_reply("403","Register forwarding forbidden");
drop;}# we want only authenticated users to be registeredif(!www_authenticate("$fd.digest_realm","credentials")){if($?==-2){
sl_reply("500","Internal Server Error");}elseif($?==-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 userif(!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 bindingif(!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 methodsbreak;}# requests from non-local to local domains should be permitted# remove this if you want a walled gardenif(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");}elseif($?==-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 gardenif($f.did!=""&& $t.did==""){
append_hf("P-hint: outbound\r\n");
route(FORWARD);}}
route[INBOUND]{# lets see if know the calleeif(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 exampleif($tu.fwd_always_target!=""){
attr2uri("$tu.fwd_always_target");
route(FORWARD);}# native SIP destinations are handled using our USRLOC DBif(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 belowif($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 headerif($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 possibleif(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 URIif($tu.fwd_busy_target!=""){
attr2uri("$tu.fwd_busy_target");
route(FORWARD);}# alternatively you could forward the request to SEMS/voicemail here}elseif(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 URIif($tu.fwd_noanswer_target!=""){
attr2uri("$tu.fwd_noanswer_target");
route(FORWARD);}# alternatively you could forward the request to SEMS/voicemail here}}

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.