What this ser.cfg will do:
Introduce forwarding concepts known as serial forking and redirection
Implement blind call forwarding
Implement forward on busy
Implement forward on no answer
Call forwarding can be achieved using one of two methods, namely serial forking and redirection. Both methods have advantages. Serial forking, as the name implies creates a new leg of the call (after the first fails) and sends a new INVITE message to the forwarded destination. On the other hand, redirection sends a reply message back to the caller and gives them the forwarded location. The caller then creates a brand new INVITE message with the forwarded destination in the R-URI and places the call.
Redirection must be used with caution if a PSTN gateway is accessible. The reason is that it is conceivable that SER could send a redirection response back to a PSTN gateway and the PSTN gateway could then send a new INVITE message to the forwarded destination and bypass SER, which could be an international call for which SER is unaware. This would result in toll charges that are not billable. The reason that SER could be unaware of the new call is that redirection is accomplished by replying with a 302 Temporarily Moved message. In the IP world, the intention is that the IP phone shall decide what to do, possibly even ask the user should I go ahead and make the call to the new destination? This way the user will acknowledge potential charges. However, there is no standard way 302 messages are handling in the interface between PSTN and IP and non-billable calls is a possibility.
Serial forking does not have this possible security exposure because SER will record route all messages, which means that even if the forwarded destination is a PSTN phone, SER will be able to account for the call and the toll charges, if any, would be billable to the subscriber.
This section implements three types of call forwarding:
Blind Call Forwarding All INVITE messages sent to the phone will be intercepted at the SIP router. The SIP router will create a new call leg and send the INVITE message to the forwarded destination. This means that the phone with blind call forwarding set will not even ring. This also means that it doesnt matter if the phone is registered with SER.
Forward On Busy If a phone replies to an INVITE message with a 486 Busy, SER will intercept the 486 response and create a new call leg, which will send an INVITE message to the forwarded destination.
Forward No Answer If a phone replies to an INVITE message with a 408 Request Timeout, SER will intercept the 408 message and create a new call leg, which will send an INVITE message to the forwarded destination.
Blind call forwarding is handled somewhat differently than forward no answer and forward on busy because blind call forwarding is altering an INVITE message for which the destination set has yet to be processed. This means that we can simply change the R-URI and relay the message.
The other two types of call forwarding require much more effort to implement because we must catch the error replies, alter the R-URI, and then append the forwarded contact location to the destination set and finally relay the message.
All call forwarding preferences are stored in the MySQL usr_preferences table. We will use the AVPOPS module to read a subscribers call forwarding settings. AVPOPS will also be used to alter the call sequence and perform serial forking that we require.
PLEASE NOTE! This configuration cannot recursive forwarding, i.e. forwarding to somebody who has forwarded. Adding support for this is complex, but is possible and is left as an exercise for the reader ;-)
In addition we have included an appendix describing the Call processing Language that allows more comprehensive control of a call.
debug=3
fork=yes
log_stderror=no
listen=192.0.2.13 # INSERT YOUR IP ADDRESS HERE
port=5060
children=4
dns=no
rev_dns=no
fifo="/tmp/ser_fifo"
fifo_db_url="mysql://ser:heslo@localhost/ser"
loadmodule "/usr/local/lib/ser/modules/mysql.so"
loadmodule "/usr/local/lib/ser/modules/sl.so"
loadmodule "/usr/local/lib/ser/modules/tm.so"
loadmodule "/usr/local/lib/ser/modules/rr.so"
loadmodule "/usr/local/lib/ser/modules/maxfwd.so"
loadmodule "/usr/local/lib/ser/modules/usrloc.so"
loadmodule "/usr/local/lib/ser/modules/registrar.so"
loadmodule "/usr/local/lib/ser/modules/auth.so"
loadmodule "/usr/local/lib/ser/modules/auth_db.so"
loadmodule "/usr/local/lib/ser/modules/uri.so"
loadmodule "/usr/local/lib/ser/modules/uri_db.so"
loadmodule "/usr/local/lib/ser/modules/domain.so"
loadmodule "/usr/local/lib/ser/modules/mediaproxy.so"
loadmodule "/usr/local/lib/ser/modules/nathelper.so"
loadmodule "/usr/local/lib/ser/modules/textops.so"
loadmodule "/usr/local/lib/ser/modules/avpops.so"
loadmodule "/usr/local/lib/ser/modules/permissions.so"
modparam("auth_db|permissions|uri_db|usrloc",
"db_url", "mysql://ser:heslo@localhost/ser")
modparam("auth_db", "calculate_ha1", 1)
modparam("auth_db", "password_column", "password")
modparam("nathelper", "rtpproxy_disable", 1)
modparam("nathelper", "natping_interval", 0)
modparam("mediaproxy","natping_interval", 30)
modparam("mediaproxy","mediaproxy_socket", "/var/run/mediaproxy.sock")
modparam("mediaproxy","sip_asymmetrics","/usr/local/etc/ser/sip-clients")
modparam("mediaproxy","rtp_asymmetrics","/usr/local/etc/ser/rtp-clients")
modparam("usrloc", "db_mode", 2)
modparam("registrar", "nat_flag", 6)
modparam("rr", "enable_full_lr", 1)
modparam("tm", "fr_inv_timer", 27)
modparam("tm", "fr_inv_timer_avp", "inv_timeout")
modparam("permissions", "db_mode", 1)
modparam("permissions", "trusted_table", "trusted")
modparam("avpops", "avp_url", "mysql://ser:heslo@localhost/ser")
modparam("avpops", "avp_table", "usr_preferences")
route {
# -----------------------------------------------------------------
# Sanity Check Section
# -----------------------------------------------------------------
if (!mf_process_maxfwd_header("10")) {
sl_send_reply("483", "Too Many Hops");
break;
};
if (msg:len > max_len) {
sl_send_reply("513", "Message Overflow");
break;
};
# -----------------------------------------------------------------
# Record Route Section
# -----------------------------------------------------------------
if (method=="INVITE" && client_nat_test("3")) {
# INSERT YOUR IP ADDRESS HERE
record_route_preset("192.0.2.13:5060;nat=yes");
} else if (method!="REGISTER") {
record_route();
};
# -----------------------------------------------------------------
# Call Tear Down Section
# -----------------------------------------------------------------
if (method=="BYE" || method=="CANCEL") {
end_media_session();
};
# -----------------------------------------------------------------
# Loose Route Section
# -----------------------------------------------------------------
if (loose_route()) {
if (!has_totag()) {
sl_send_reply("403", "Forbidden");
break;
};
if (method=="INVITE") {
if ((method=="INVITE" || method=="REFER") && !has_totag()) {
if (!proxy_authorize("","subscriber")) {
proxy_challenge("","0");
break;
} else if (!check_from()) {
sl_send_reply("403", "Use From=ID");
break;
};
consume_credentials();
};
if (client_nat_test("3")||search("^Route:.*;nat=yes")){
setflag(6);
use_media_proxy();
};
};
route(1);
break;
};
# ----------------------------------------------------------
# Call Type Processing Section
# ----------------------------------------------------------
if (!is_uri_host_local()) {
if (is_from_local() || allow_trusted()) {
route(4);
route(1);
} else {
sl_send_reply("403", "Forbidden");
};
break;
};
if (method=="ACK") {
route(1);
break;
} if (method=="CANCEL") {
route(1);
break;
} else if (method=="INVITE") {
route(3);
break;
} else if (method=="REGISTER") {
route(2);
break;
};
lookup("aliases");
if (!is_uri_host_local()) {
route(4);
route(1);
break;
};
if (!lookup("location")) {
sl_send_reply("404", "User Not Found");
break;
};
route(1);
}
route[1] {
# ----------------------------------------------------------
# Default Message Handler
# ----------------------------------------------------------
t_on_reply("1");
if (!t_relay()) {
if (method=="INVITE" || method=="ACK") {
end_media_session();
};
sl_reply_error();
};
}
route[2] {
# ----------------------------------------------------------
# REGISTER Message Handler
# ----------------------------------------------------------
sl_send_reply("100", "Trying");
if (!search("^Contact:[ ]*\*") && client_nat_test("7")) {
setflag(6);
fix_nated_register();
force_rport();
};
if (!www_authorize("","subscriber")) {
www_challenge("","0");
break;
};
if (!check_to()) {
sl_send_reply("401", "Unauthorized");
break;
};
consume_credentials();
if (!save("location")) {
sl_reply_error();
};
}
route[3] {
# ----------------------------------------------------------
# INVITE Message Handler
# ----------------------------------------------------------
if (client_nat_test("3")) {
setflag(7);
force_rport();
fix_nated_contact();
};
if (!allow_trusted()) {
if (!proxy_authorize("","subscriber")) {
proxy_challenge("","0");
break;
} else if (!check_from()) {
sl_send_reply("403", "Use From=ID");
break;
};
consume_credentials();
};
if (uri=~"^sip:1[0-9]{10}@") {
strip(1);
};
lookup("aliases");
if (!is_uri_host_local()) {
route(4);
route(1);
break;
};
if (uri=~"^sip:011[0-9]*@") {
route(4);
route(5);
break;
};
if (avp_db_load("$ruri/username", "s:callfwd")) {
setflag(22);
avp_pushto("$ruri", "s:callfwd");
route(6);
break;
};
if (!lookup("location")) {
if (uri=~"^sip:[0-9]{10}@") {
route(4);
route(5);
break;
};
sl_send_reply("404", "User Not Found");
break;
};
if (avp_db_load("$ruri/username", "s:fwdbusy")) {
if (!avp_check("s:fwdbusy", "eq/$ruri/i")) {
setflag(26);
};
};
if (avp_db_load("$ruri/username", "s:fwdnoanswer")) {
if (!avp_check("s:fwdnoanswer", "eq/$ruri/i")) {
setflag(27);
};
};
t_on_failure("1");
route(4);
route(1);
}
route[4] {
# ----------------------------------------------------------
# NAT Traversal Section
# ----------------------------------------------------------
if (isflagset(6) || isflagset(7)) {
if (!isflagset(8)) { (11)
setflag(8);
use_media_proxy();
};
};
}
route[5] {
# ----------------------------------------------------------
# PSTN Handler
# ----------------------------------------------------------
rewritehost("192.0.2.245"); # INSERT YOUR PSTN GATEWAY IP ADDRESS
avp_write("i:45", "inv_timeout");
t_on_failure("1");(12)
route(1);
}
(13)route[6] {
# ----------------------------------------------------------
# Call Forwarding Handler
#
# This must be done as a route block because sl_send_reply() cannot be
# called from the failure_route block
# ----------------------------------------------------------
if (uri=~"^sip:1[0-9]{10}@") { (14)
strip(1);
};
lookup("aliases");(15)
if (!is_uri_host_local()) { (16)
if (!isflagset(22)) {
append_branch();
};
route(4);
route(1);
break;
};
if (uri=~"^sip:011[0-9]*@") {
route(4); (17)
route(5);
break;
};
if (!lookup("location")) { (18)
if (uri=~"^sip:[0-9]{10}@") {
route(4);
route(1);
break;
};
sl_send_reply("404", "User Not Found");
};
route(4);
route(1);
}
onreply_route[1] {
if ((isflagset(6) || isflagset(7)) &&
(status=~"(180)|(183)|2[0-9][0-9]")) {
if (!search("^Content-Length:[ ]*0")) {
use_media_proxy();
};
};
if (client_nat_test("1")) {
fix_nated_contact();
};
}
(19)failure_route[1] {
if (t_check_status("487")) { (20)
break;
};
if (isflagset(26) && t_check_status("486")) { (21)
if (avp_pushto("$ruri", "s:fwdbusy")) {(22)
avp_delete("s:fwdbusy");(23)
resetflag(26);
route(6);(24)
break;
};
};
if (isflagset(27) && t_check_status("408")) { (25)
if (avp_pushto("$ruri", "s:fwdnoanswer")) { (26)
avp_delete("s:fwdnoanswer");(27)
resetflag(27);
route(6); (28)
break;
};
};
end_media_session();(29)
}
| Call forwarding is dependent on the avpops module. This module needs to access the MySQL database in order to read a subscribers call forwarding preferences from the usr_preferences table. So, here we specify the MySQL database and table for call forwarding preferences. |
| Here is where we implement the blind call forwarding functionality. When processing an INVITE message we need to lookup any row in the MySQL usr_preferences table to see if we can find a record that has the name portion of the R-URI and an attribute of callfwd. If the avp_db_load() function returns TRUE then the AVP named s:callfwd will have the blind call forwarding destination set. |
| If we are about to serial fork the call because of blind call forwarding, then we set flag 22 for future reference in route[6]. This is important because this flag determines whether or not we need to call the append_branch() function which actually forks the call. A subtle item to understand here is that blind call forwarding can only happen when processing the original INVITE. This means that the destination set of the call has not been processed yet, so we can safely alter the R-URI and the call to append_branch() should not be done. Forward on busy and forward no answer however, only occur after the original INVITE has failed, which means the destination set has been processed. In order to fork the call at this point the append_branch() function must be called.Therefore, flag 22 will be set only for blind call forwarding. |
| Since avp_db_load() found a blind call forwarding record in MySQL we need to write that new destination to the R-URI in order to send the INVITE message to the correct destination. Avp_pushto() does just this. This statement copies the value in AVP s:callfwd to the R-URI of the actual message. |
| Send the processing to route(6) which handles all call forwarding. |
| This section loads the subscribers preferences for forward on busy and forward no answer. Load the forward on busy setting from the MySQL usr_preferences table. Here we look for records in the database table where the attribute column value is fwdbusy and the username part (before @) of the R-URI can be found in the tables username column. avp_db_load() returns true if one or more records were found. |
| We do not allow users to forward to their now SIP phone, which would cause looping problems for SER. So we use avp_check() to make sure the forward on busy destination is not the same as the R-URI destination. If forward on busy is set then we need to remember this for future processing in the failure_route of this configuration script. If the subscriber in the R-URI has forward on busy enabled, then flag 26 will be set, and we will be able to detect this in the failure_route. |
| Load the forward no answer setting from the MySQL usr_preferences table. Here we look for records where the username port of the R-URI has an attribute column value of fwdnoanswer. |
| We do not allow users to forward to their own SIP phone, which would cause looping problems for SER. So we use avp_check() to make sure the forward no answer destination is not the same as the R-URI destination. If forward on busy is set then we need to remember this for future processing in the failure_route of this configuration script. If the subscriber in the R-URI has forward no answer enable then flag 27 will be set, and we will be able to detect this in the failure_route. |
| t_on_failure() informs SER that we want to perform special handling when a failure condition occurs. Failure conditions in this context refer to 4xx and 5xx response codes. For example, 486 is the response code for a busy answer. By setting t_on_failure(1) before calling t_relay(), SER will pass control to the code block defined as failure_route[1] which appears at the end of the configuration script. |
| (11) | We must take special precautions not to call use_media_proxy() more than once because by doing so the c= field in the SDP will get corrupted. This can happen if the original INVITE was NATed in route(4), get called during the call set up, and a 486 Busy is received. This will trigger the forward on busy functionality. Once the call is forked, use_media_proxy() must not be called because it was already called during the original INVITE processing logic. |
| (12) | Enable the failure route for all PSTN calls. |
| (13) | Here we introduce the call forwarding handler. This handler will create the new call leg for all three types of call forwarding. |
| (14) | In North America we dial 1+ ten digits to call a long distance number. If the R-URI contains this prefix we strip it off before processing. |
| (15) | Find any aliases. The call forwarding number in the MySQL usr_preferences table could very well be an alias in our database. |
| (16) | If the R-URI is not served by our domain then call append_branch() for forward on busy and forward no answer before enabling the NAT processing code in route(4) and relaying the cal from route(1). |
| (17) | If the R-URI is an international PSTN destination then call the NAT processing code in route(4) and relay to the PSTN via route(5). |
| (18) | Find the R-URI in the user location cache. If a user is not found, then determine if it is a domestic PSTN destination. If so, enable the NAT processing logic in route(4) and relay the call to the PSTN via route(5). Enable NAT traversal code and relay the message. Messages hitting this line are INVITE messages being forwarded to another SIP phone that is registered on this SIP router. |
| (19) | Here we introduce the failure route. If we call t_on_failure() before t_relay(), then SER will execute this code block when a 4xx or 5xx reply message is received. |
| (20) | If we entered the failure route because the caller cancelled the call (ie, response code 487), then we simply stop processing. |
| (21) | If forward on busy (flag 26) is set and the reply message is a 486, then enter the forward on busy code block. |
| (22) | Here we retrieve the previously loaded forward on busy destination URI. The AVP named s:fwdbusy is copied to the R-URI. |
| (23) | Since we've now copied the forward on busy URI to the R-URI we can safely delete the AVP. We could omit this line altogether and SER would free the AVP when the transaction is freed. We now clear the forward on busy flag to prevent accidentally entering this code block on future 486 messages. |
| (24) | Here we pass control to the call forwarding route block and exit. |
| (25) | If forward no answer (flag 27) is set and the reply message is a 408 then enter the forward no answer code block. |
| (26) | Here we retrieve the previously loaded forward no answer destination URI. The AVP named s:fwdnoanswer is copied to the R-URI. |
| (27) | Since we've now copied the forward no answer URI to the R-URI we can safely delete the AVP. We could omit this line altogether and SER would free the AVP when the transaction is freed. We now clear the forward no answer flag to prevent accidentally entering this code block on future 408 messages. |
| (28) | Here we pass control to the call forwarding route block and exit. |
| (29) | Disable mediaproxy if it was enabled during the call set up. |