I'm trying to set up a SIP infrastructure using OpenSIPS as a load balancer for multiple backend SIP servers. The goal is to route incoming INVITE requests to a backend server and have OpenSIPS manage the call's media using rtpproxy.
Here's a quick overview of my setup:
OpenSIPS: Handles registration with an upstream provider, load balances
INVITEs, and usesrtpproxyfor media.Backend Servers: Custom Python SIP servers built with PJSUA2.
rtpproxy: Handles media streams.
My issue is with the ACK message. After an INVITE is sent to a backend and the backend replies with a 200 OK, OpenSIPS receives the final ACK from the provider but never forwards it to the backend server. This causes the call to fail.
The call flow looks like this:
Provider -> OpenSIPS -> Backend (INVITE)
Provider <- OpenSIPS <- Backend (200 OK)
Provider -> OpenSIPS (ACK)
... The ACK is lost here. The backend never receives it.
suspect the problem lies in my OpenSIPS configuration, specifically how I'm handling stateful routing and dialog management. I've tried tweaking the Record-Route and Contact headers, but nothing seems to work.
Has anyone encountered a similar issue? What OpenSIPS configuration parameters or routing logic could cause a final ACK to be dropped instead of being routed to the correct backend server?
Any guidance on how to properly set up a stateful proxy with load_balancer and rtpproxy would be a great help! 🙏
You can find my full opensips.cfg :
#
# OpenSIPS loadbalancer script
# by OpenSIPS Solutions <[email protected]>
#
# This script was generated via "make menuconfig", from
# the "Load Balancer" scenario.
# You can enable / disable more features / functionalities by
# re-generating the scenario with different options.
#
# Please refer to the Core CookBook at:
# https://opensips.org/Resources/DocsCookbooks
# for a explanation of possible statements, functions and parameters.
#
####### Global Parameters #########
/* uncomment the following lines to enable debugging */
debug_mode=yes
log_level=2
xlog_level=2
stderror_enabled=no
syslog_enabled=yes
syslog_facility=LOG_LOCAL0
udp_workers=4
# CUSTOMIZE ME
listen=udp:0.0.0.0:5060 as [OPENSIPS_PUBLIC_IP]:5060
# listen=udp:172.31.0.10:5060
####### Modules Section ########
#set module path
mpath="/usr/lib/x86_64-linux-gnu/opensips/modules/"
#### UAC module
loadmodule "uac.so"
loadmodule "uac_auth.so"
loadmodule "uac_registrant.so"
modparam("uac_registrant", "db_url", "mysql://opensips:opensipsrw@mysql/opensips")
modparam("uac_registrant", "timer_interval", 30)
modparam("uac_registrant", "hash_size", 2)
#### HTTPD module
loadmodule "httpd.so"
modparam("httpd", "port", 8888)
#### SIGNALING module
loadmodule "signaling.so"
#### StateLess module
loadmodule "sl.so"
#### Transaction Module
loadmodule "tm.so"
modparam("tm", "fr_timeout", 5)
modparam("tm", "fr_inv_timeout", 30)
modparam("tm", "restart_fr_on_each_reply", 0)
modparam("tm", "onreply_avp_mode", 1)
#### Record Route Module
loadmodule "rr.so"
/* do not append from tag to the RR (no need for this script) */
modparam("rr", "append_fromtag", 0)
#### MAX ForWarD module
loadmodule "maxfwd.so"
#### SIP MSG OPerationS module
loadmodule "sipmsgops.so"
#### FIFO Management Interface
loadmodule "mi_fifo.so"
modparam("mi_fifo", "fifo_name", "/tmp/opensips_fifo")
modparam("mi_fifo", "fifo_mode", 0666)
#### MYSQL module
loadmodule "db_mysql.so"
#### SQLOPS module
loadmodule "sqlops.so"
modparam("sqlops","db_url","mysql://opensips:opensipsrw@mysql/opensips") # CUSTOMIZE ME
#### ACCounting module
loadmodule "acc.so"
/* what special events should be accounted ? */
modparam("acc", "early_media", 0)
modparam("acc", "report_cancels", 0)
/* by default we do not adjust the direct of the sequential requests.
if you enable this parameter, be sure to enable "append_fromtag"
in "rr" module */
modparam("acc", "detect_direction", 0)
#### DIALOG module
loadmodule "dialog.so"
modparam("dialog", "dlg_match_mode", 1)
modparam("dialog", "default_timeout", 21600) # 6 hours timeout
modparam("dialog", "db_mode", 2)
modparam("dialog", "db_url", "mysql://opensips:opensipsrw@mysql/opensips") # CUSTOMIZE ME
#### LOAD BALANCER module
loadmodule "load_balancer.so"
modparam("load_balancer", "db_url", "mysql://opensips:opensipsrw@mysql/opensips") # CUSTOMIZE ME
modparam("load_balancer", "probing_method", "OPTIONS")
modparam("load_balancer", "probing_interval", 30)
#### MI_HTTP module
loadmodule "mi_http.so"
loadmodule "proto_udp.so"
#### Proxy modules
loadmodule "usrloc.so"
loadmodule "nathelper.so"
modparam("nathelper", "natping_interval", 10)
loadmodule "rtpproxy.so"
modparam("rtpproxy", "rtpproxy_sock", "unix:/var/run/rtpproxy/rtpproxy.sock")
modparam("rtpproxy", "rtpproxy_autobridge", 1)
####### Routing Logic ########
# main request routing logic
route{
if (!mf_process_maxfwd_header(10)) {
send_reply(483,"Too Many Hops");
exit;
}
if (has_totag()) {
# handle hop-by-hop ACK (no routing required)
if ( is_method("ACK") && t_check_trans() ) {
xlog("ACK HOP BY HOP \n");
t_relay();
exit;
}
# sequential request withing a dialog should
# take the path determined by record-routing
if ( !loose_route() ) {
# we do record-routing for all our traffic, so we should not
# receive any sequential requests without Route hdr.
send_reply(404,"Not here");
exit;
}
if (is_method("BYE")) {
# do accounting even if the transaction fails
rtpproxy_unforce();
do_accounting("log","failed");
}
# route it out to whatever destination was set by loose_route()
# in $du (destination URI).
route(RELAY);
exit;
}
#### INITIAL REQUESTS
# CANCEL processing
if (is_method("CANCEL")) {
if (t_check_trans())
t_relay();
exit;
} else if (!is_method("INVITE")) {
send_reply(405,"Method Not Allowed");
exit;
}
if ($rU==NULL) {
# request with no Username in RURI
send_reply(484,"Address Incomplete");
exit;
}
t_check_trans();
# preloaded route checking
if (loose_route()) {
xlog("L_ERR",
"Attempt to route with preloaded Route's [$fu/$tu/$ru/$ci]");
if (!is_method("ACK"))
send_reply(403,"Preload Route denied");
exit;
}
if (has_body("application/sdp")) {
xlog("L_INFO", ">>> Calling rtpproxy_offer for $ci\n");
rtpproxy_offer();
}
# record routing
xlog("Record route \n");
record_route();
do_accounting("log");
if ( !lb_start(1,"channel")) {
send_reply(500,"No Destination available");
exit;
}
t_on_failure("GW_FAILOVER");
xlog("Going to route RELAY \n");
route(RELAY);
}
route[RELAY] {
if (has_body("application/sdp")) {
xlog("has body app sdp \n");
xlog("L_INFO", ">>> Calling rtpproxy_answer for $ci\n");
xlog("answering withrtpproxy \n");
rtpproxy_answer();
}
xlog("fixing nated contact \n");
fix_nated_contact();
xlog("calling t_relay \n");
if (!t_relay()) {
xlog("Error on t_relay \n");
sl_reply_error();
}
exit;
}
failure_route[GW_FAILOVER] {
if (t_was_cancelled()) {
exit;
}
# failure detection with redirect to next available trunk
if (t_check_status("(408)|([56][0-9][0-9])")) {
xlog("Failed trunk $rd/$du detected \n");
if ( lb_next() ) {
t_on_failure("GW_FAILOVER");
t_relay();
exit;
}
send_reply(500,"All GW are down");
}
}
local_route {
if (is_method("BYE") && $DLG_dir=="UPSTREAM") {
acc_log_request("200 Dialog Timeout");
}
}