SIP transport protocol transcoding in OpenSIPS

Introduction

This is the final article in my series about fixing SIP header addresses. It broadly overlaps with the theme of the earlier articles. As a VoIP solutions designer, you may want your proxy server to deliberately transcode between different transport protocols. For example, WebRTC to TCP or TLS to UDP. This is possible with OpenSIPS, but getting every detail correct across a wide range of use-cases is quite difficult. It requires attention to detail in several areas, not least the construction of Record-Route headers. These were examined and discussed in a couple of my earlier articles, but those did not drill down and focus on issues specific to transport protocol transcoding. Hopefully, I can fill in the missing pieces of the jigsaw here.

Why transcode the transport protocol?

There are many situations where it is extremely useful to be able to use OpenSIPS to translate between one transport protocol and another. Here are some examples:

  1. Remote client devices connecting via TLS to a registrar proxy which interconnects with a local voice server or PBX using unencrypted UDP.
  2. Edge proxy for browser-based WebRTC connections over the Internet, passing the packets to a local voice server over UDP or TCP
  3. Teams SBC connecting to MS Teams proxies using TLS and passing the calls to and from a local voice server over UDP or TCP

In scenario 1, OpenSIPS terminates all the TLS connections and handles authentication of the remote client devices, thereby freeing up and simplifying the role of the voice server or PBX. It makes it easier to debug VoIP sessions because you can capture unencrypted SIP on the voice server.

Scenario 2 describes a typical WebRTC to SIP gateway. It allows existing VoIP infrastructure to be used with the relatively new WebRTC protocol that is now widely supported by most browsers, opening up a range of possibilities for users to make and receive calls from a web page.

Scenario 3 describes a typical Teams SBC solution based on OpenSIPS. Interconnection with MS Teams, using Direct Routing, has to be over a secure TLS link. The SBC requires valid certificates that identify the SBC host server with a fully qualified domain name and that FQDN must also be used in the Record-Route headers that are inserted by OpenSIPS. This means OpenSIPS can handle all the interconnection security issues allowing you to leverage the services and features present in your existing VoIP infrastructure – e.g. by offering a SIP trunk connection with support for your inbound DDI’s as well as for outbound traffic to the PSTN.

Configuring different sockets in OpenSIPS

If OpenSIPS is going to receive requests over one transport and send them onwards using a different transport, then it has to be configured to bind on different sockets – one per protocol. These are likely to be using different ports such as port 5060 for UDP and port 5061 for TLS. When TLS is used, the proxy server will usually be assigned one or more host names which should match the name stored in the Subject CN element of the certificate (or it may instead be in the Subject Alternative Name field).

This means you will require multiple “listen” statements in your opensips.cfg file (or “socket” statements if using a version of OpenSIPS later than v2.4.x). If your server is likely to use a hostname for identification, you will probably also want to insert some “alias” statements – one statement for each unique combination of protocol, hostname and port. Here is an example from a v2.4 Proxy server I built specifically to transcode between multiple protocols (the hostname and IP addresses have been obfuscated):

/* Disable getting home SIP domains based on reverse DNS lookups */
auto_aliases=no

/* Specific home SIP domains - host name or IP */
alias=udp:xproxy.smartvox.co.uk:5060
alias=tcp:xproxy.smartvox.co.uk:5060
alias=tls:xproxy.smartvox.co.uk:5061
alias=wss:xproxy.smartvox.co.uk:443

/* Bind to specific interface/port/protocol */
listen=udp:89.200.14.123:5060
listen=tcp:89.200.14.123:5060
listen=tls:89.200.14.123:5061
listen=wss:89.200.14.123:443

To complete the picture, let’s also include a snippet of code showing the modules loaded and some of the module parameter settings where they are relevant to interfaces and protocols:


#### PROTOCOL modules
loadmodule "proto_udp.so"
loadmodule "proto_tcp.so"
loadmodule "proto_tls.so"
loadmodule "proto_wss.so"
modparam("proto_wss", "wss_max_msg_chunks", 16)

#### TLS MANAGEMENT module
loadmodule "tls_mgm.so"
modparam("tls_mgm", "tls_method", "SSLv23")
modparam("tls_mgm", "ciphers_list", "AES256-GCM-SHA384,AES256-SHA256,AES256-SHA,CAMELLIA256-SHA,AES128-SHA,CAMELLIA128-SHA,RC4-SHA")
modparam("tls_mgm", "certificate", "/etc/letsencrypt/live/xproxy.smartvox.co.uk/cert.pem")
modparam("tls_mgm", "private_key", "/etc/letsencrypt/live/xproxy.smartvox.co.uk/privkey.pem")
modparam("tls_mgm", "ca_list", "/etc/letsencrypt/live/xproxy.smartvox.co.uk/fullchain.pem")
modparam("tls_mgm", "verify_cert", "0")
modparam("tls_mgm", "require_cert", "0")

In the above example, I chose TLS settings that are not especially strict. They were chosen for maximum compatibility with a wide range of remote devices, including some that only support older, less secure encryptions. You will also see that I selected to load the TLS certificates from local files rather than from a database table. That’s because it is easier to debug problems when the certs are loaded this way. However, for a production system it may be preferable to load them from a database table (see my earlier article about Using TLS in OpenSIPS v2 for more details).

How to switch the transport protocol?

OpenSIPS acts as a SIP Proxy – this means it receives a SIP Request, modifies it, then relays it onwards to another destination. If you know you want to use a specific protocol when relaying requests to a chosen destination, then your script will need to specify the protocol. One way of doing this is to set the “send socket”. Different approaches are available in your script to do this:

  • The “send socket” may be selected through data entries in the tables used by certain routing modules (e.g. the Dynamic Routing module, table dr_gateways, field ‘socket’ – example shown below). Obviously, this only works when you are using the chosen module to select the onward address
  • If, instead of using a routing module, you wish to hard code some routing rules directly into the script, then it is possible to set the transport protocol by calling force_send_socket()
  • Another way to hard code the onward transport protocol within the script would be to write a value into $socket_out if using v3 of OpenSIPS or into $fs if using an earlier version

Here are some real-World examples of all three approaches:

// Edited example of dr_gateways table using 'socket' to set transport protocol

gwid  type  address		strip	socket			description
21    1     192.168.20.10:5092	0	192.168.20.11		PBX
31    1	    192.168.20.19:5080	0	tcp:192.168.20.11	FreeSwitch
1     1	    gw.mydomain.net	0	192.168.20.11		Gateways
2     1	    51.153.251.45:5060	0	192.168.20.11		Asterisk



// Example of using the force_send_socket() function

    force_send_socket(tcp:192.168.20.11:5060);



// Example of using $fs core variable in v2.4.x

    $fs = "tls:192.168.20.11:5061";

The above techniques will set the “send socket”, but it is also necessary to ensure that the destination where you are sending the request supports the same transport protocol. You may choose to put the destination into the $du variable or you may simply want to rely on the R-URI, but either way make sure it is matched to your chosen transport protocol. In particular, make sure you are sending the request to the correct port – for example, the standard port for UDP is 5060 whereas for TLS it is 5061.

Another thing to be careful about is checking if the R-URI of the SIP request included a parameter like this:

;transport=tls

That is because this parameter, if present, needs to match the transport protocol being used in each connection leg. So if OpenSIPS is deliberately changing the protocol then it must either modify or remove the parameter. For this, I generally use a line of code in my script like this (in this case changing from TLS to TCP):

    subst_uri('/transport=tls/transport=tcp/i');

Letting OpenSIPS select the “send socket”

If your script does not specify the “send socket” when routing an initial request, OpenSIPS will select one automatically to match the transport protocol. As far as I can tell, the rules for selection work like this:

  1. If no transport protocol is specified, UDP is assumed
  2. If a transport protocol has been specified (e.g. by the presence of “;transport=tcp” in the Request URI) then it will select the first defined “listen” socket with the same protocol

This means it is possible to switch the transport protocol by doing nothing more than changing (or adding) the R-URI parameter “;transport=<proto>”. For many situations, that may be sufficient. It often pays to keep things simple if you can, but please make sure you test every possible use-case very thoroughly. Note also that loose-routed requests may be controlled by Route headers instead. These can also contain a parameter specifying the transport protocol, but it should not be necessary to change any part of the Route header as long as you inserted the correct Record-Route headers while handling the original SIP request.

Headers that may need fixing

Now I want to get back to the primary theme in this series of articles – fixing headers. The first step is to figure out which headers may require our attention.

Contact

Assuming that OpenSIPS is acting as a Proxy, we do not need to make additional changes to Contact headers just because we are changing the transport protocol. That is because Contact headers hold information about the endpoints and they are created and populated by the endpoints. So OpenSIPS might change the transport protocol for the onward leg of a request, but the protocol as seen by each endpoint remains constant throughout the dialogue. However, it may still be necessary to fix the Contact header if the sending device was behind NAT. This requirement does not change when our proxy is doing transport protocol transcoding, so you can just follow the advice given in my companion article here.

Via

What about Via headers? The ones you receive in a SIP request should not need altering because they will reflect the transport protocol used by the sender. Your OpenSIPS proxy is only changing the protocol on the onward leg; it cannot alter the protocol on the inbound leg. However, OpenSIPS will add a Via header just before it relays the request on to the next destination. The transport protocol shown in this new Via header will automatically reflect the protocol of the chosen “send socket”. The only part of it that you could explicitly change from within the script is the address and port – these can be overridden using “advertised address” and/or “advertised port”. However, that should only be necessary if your OpenSIPS proxy is behind NAT.

Record-Route

When we look at adding Record-Route headers to the initial SIP request, things start to get a bit more interesting. First, if the OpenSIPS proxy is changing the transport protocol, then it will almost certainly need to insert a double Record-Route header – one per interface/socket. These were explained in another companion article here and the principles are described in RFC 5658. Fortunately, OpenSIPS will quite often be able to do all the hard work for you, allowing you to do nothing more than add one line of code to your opensips.cfg script like this:

    record_route();

It could hardly be easier, could it. Yet that one line of code is good enough to cope with every possible combination of transport protocol transcoding in a server I configured to work with UDP, TCP, TLS and WebRTC. There are just a few caveats that need to be mentioned. They are:

  • In the modparams section for the RR module, make sure the option for using double RR headers is 1 (yes). That is actually the default option so you’d have to go out of your way to turn it off.
  • Make sure the call to the record_route() function is just about the last thing you do before calling t_relay(). At the very least, call record_route() after the script has set the destination and the “send socket”.
  • It is most likely to work if you only have one network interface on the host server and the server is not behind NAT.
  • Only insert RR headers on the initial request within a dialogue – they should not be added to a loose-routed requests

When you inspect the SIP request coming out of OpenSIPS, what should the double RR headers look like? Here are some examples collected on my test server:

// From a WebRTC client to a TCP gateway
Record-Route: <sip:89.200.14.123;transport=tcp;r2=on;lr;did=6dd.0baf4f2>
Record-Route: <sip:89.200.14.123:486;transport=wss;r2=on;lr;did=6dd.0baf4f2>

// From TLS to WebRTC
Record-Route: <sip:89.200.14.123:486;transport=wss;r2=on;lr;did=656.2d7f1a6>
Record-Route: <sip:89.200.14.123:5061;transport=tls;r2=on;lr;did=656.2d7f1a6>

So what can be done for those special cases where a simple call to record_route() just doesn’t give you the result you need? If the host server has two interfaces – perhaps one that connects to the Internet and another that connects to the LAN – or your server is behind NAT and your RR headers are incorrect, then first try using the options that alter the advertised address. The advertised address can be set by adding an extra string on the original listen/socket declaration statement or by calling the function set_advertised_address() at the appropriate point in your script.

// Example of a modified "listen" statement (in v3 it is a "socket" statement)
//   suitable for OpenSIPS server on a LAN using 1-to-1 NAT
listen=tls:192.168.20.11:5061 as 82.0.128.19:5061

// Example of explicitly setting the advertised address within the script
    if (isflagset(FROM_WAN))
        set_advertised_address(192.168.20.11);
    else
        set_advertised_address(82.0.128.19);

    t_relay()
    exit;

If everything else fails to give you the result you want, then you can always use the function record_route_preset() instead of the simpler record_route(). It allows you to specify exactly what you want in the new Record-Route header or headers. To create a double RR header, you pass it two arguments – the first defines the upper RR header (outbound) and the second defines the lower header (inbound/received). When you add double RR headers, it is important to also explicitly add the “;r2=on” parameter. To do this, use the function add_rr_param() – this is shown in examples below.

Here is a snippet of some code I wrote for a proxy server located behind 1-to-1 NAT. The server only has one interface (connected to the LAN), but it supports UDP, TCP or TLS connections from the Internet via the NAT router/firewall and relays the SIP requests to local servers which may use either UDP or TCP. The routing selection to the local servers is not shown in this snippet, but it has already been done before this section of the script is reached and so the “send socket” has been preset. The code inspects the following two core variables to determine the incoming and outgoing transport protocols:

$pr

$fs

protocol of the received message (udp, tcp, tls, sctp, ws)

details of the forced (send) socket, in the form proto:ip:port

    #
    # Add suitable RR header(s) depending on the source of the call
    #
    if ($fs =~ "tcp")
        $var(tailstring) = ":5060;transport=tcp";
    else
        $var(tailstring) = ":5060";

    if (isflagset(FROM_WAN)) {
        # From a trusted Internet address (WAN)
        if ($pr=="tls")
            record_route_preset("192.168.20.11$var(tailstring)","82.0.128.19:5061;transport=tls");
        else if ($pr=="tcp")
            record_route_preset("192.168.20.11$var(tailstring)","82.0.128.19:5060;transport=tcp");
        else
            record_route_preset("192.168.20.11$var(tailstring)","82.0.128.19:5060");
        add_rr_param(";r2=on");

    } else ...

The arguments passed to the record_route_preset() function can use a fully qualified domain name (FQDN) instead of an IP address if you want. This is recommended within the published solutions describing how to use OpenSIPS to build a Teams SBC. The examples used look somewhat like this:

    record_route_preset(“IP:port”, “SBC_FQDN:5061;transport=tls”);
    add_rr_param(“;r2=on”);

What about the RTP media?

Connections over TLS or WebRTC will mostly use encrypted media. If you are using OpenSIPS to interconnect with a PBX, gateway or media server that does not support encrypted media (or does not support it in the format offered) then it is possible to use rtpengine to transcode. This is quite likely to be necessary in a proxy that interconnects different transport protocols and I recommend that you design it in from the start and assume it will be required for every call.

In my design, I assumed the following and used these assumptions to construct a switch-case selection for the parameter string passed to rtpengine:

Protocol

UDP or TCP

TLS

WebRTC

Encryption required

No

Yes

Yes

Required media options

RTP/AVP

RTP/SAVP

RTP/SAVPF ICE rtcp-mux

I described how to use OpenSIPS with rtpengine, especially when interconnecting with WebRTC, in an earlier article: WebRTC using OpenSIPS and rtpengine

Debugging and diagnostics

If you are trying to do anything non-standard, it is very important to test all possible use-cases and inspect the resulting routing using an analysis tool such as sngrep. If using TLS or WebRTC where the packets are encrypted, I recommend using the OpenSIPS siptrace module to capture the raw packets and analyse the routing. Another option is to use Homer.

Here is a tip when looking at SIP capture diagnostics: The transport protocol within one leg of a call should not change over time. So if the connection between node A and node B starts as TLS (perhaps in the form of an INVITE request), it should not change to UDP at some later point in the same call (perhaps when the BYE request is sent to end the call). If the protocol does change it indicates that you are not really in control and you almost certainly need to fix something in your script.

For example, if your OpenSIPS proxy server is receiving a call over TCP and sending it on to an internal server using UDP, we would expect each connection between your proxy and the other nodes to continue to use the same protocol as it started with. If the transport protocol changes part way through the call on either leg then you should consider this a warning flag – something is not quite right. Also, if a response bypasses your proxy altogether and goes directly to the UAC, that is another red flag that needs investigating

A final word

I hope you found this useful. Please click the Like or Recommended buttons to show your appreciation. Leave me a comment or ask a question if you like. I’ll do my best to reply, but don’t expect a free consultancy service and bear in mind that I have retired now. These articles are my swan song – a final brain dump.

Leave a Comment