Fixing SIP header addresses – Via headers

In part 2, Via headers are put under the microscope. I examine how the address in the Via header is set by each node in the path; how and why it may differ from the source address. I will look at the functions available in OpenSIPS to detect and handle situations where the address in the received Via differs from the source address of the request. I touch on the options available within OpenSIPS to select the address it will add when it is acting as a Proxy.

The role of the Via header

Via headers define the path or route that SIP responses should follow. Consider, for example, when a SIP Client device (UAC) such as an VoIP phone, sends an INVITE request to a Proxy server which passes the request to a gateway or other endpoint (UAS) as shown in the diagram below. Before the UAC sends the INVITE request it will add a Via header. Each Proxy server the request passes through will also add a Via header above any that are already present in the request.

Via headers are added to a SIP Request by each node. The request could be, for example, an INVITE

The response (for example, 200 OK) is sent back using the stack of Via headers
Each header defines the address of one node in the return path
The topmost header is stripped off as the response passes through the matching intermediate node

The impact of NAT on the Via address

When a SIP device is behind NAT, there is a risk that it will insert a private non-routable LAN address into the Via header where it should have actually used the public address on the external interface of the NAT router.

The Via header needs to contain an address and port where responses can be received from downstream nodes. In the example above, Address-A and Port-X are only accessible within the LAN environment and are meaningless for access via the Internet. The only way that Proxy P would be able to send responses back to the UAC would be if it used Address-B and Port-Y or, possibly, Address-B and a pre-defined port configured to forward packets to the UAC.

Client-side solutions

UAC devices (IP phone, softphone, ATA, etc) may not know the correct address to use to populate the Via header because they simply do not have adequate knowledge of their own network environment. Very often, there will be options to set user-defined configuration parameters or to enable an automated discovery mechanism such as STUN. Even if the device is configured with the correct external IP address (address B in the diagram), it may not be able to discover the external port (port Y) that the router chose because most NAT routers will use Port Address Translation (PAT) as well as NAT. Therefore, it is not uncommon for the SIP request to arrive at the next node with a non-routable IP address, the wrong port or both.

Server-side solutions

Symmetric communication and the behaviour of NAT routers

Most server-side solutions work on the principle of “symmetric communication”. The idea being that a response sent back to the same source address and port that the request came from will be accepted by the NAT router. Most NAT routers and firewalls permit and support symmetric communication as part of their so-called “stateful packet inspection” mechanism. Depending on the make and model of NAT router, different constraints may apply with respect to port and address matching for outbound and inbound packets – terms such as “full cone”, “restricted cone” are used to describe this behaviour. These constraints do not normally have a significant impact for VoIP, but they can do occasionally so if you are interested the topic is explored in the following Wikipedia article:

Network address translation – Wikipedia

NAT routers also assign a timeout on each connection when it is opened, so the symmetric communication behaviour will only work for a fixed time after the initial request is sent. When using UDP, this time period may be as little as 30 seconds. However, most SIP devices and servers can be configured to keep connections open by sending periodic “keep-alive” packets. These keep-alive pings may be initiated from the client device or the server (or both) and may be in the form of a SIP request such as OPTIONS or NOTIFY. Dummy UDP packets may be used in some instances. OpenSIPS supports a range of keep-alive ping options within the nethelper module. Look for the “natping” parameters.

The “received” parameter

The problem with incorrect Via addresses is sufficiently widespread and understood in the world of SIP that provision was made for it in the international standards, within the core RFC’s. Section 18.2.1 of RFC 3261 describes how a server must add a “received” parameter to the topmost Via header when it receives a request: “This parameter MUST contain the source address from which the packet was received“. This parameter was designed to assist routing of responses not only where NAT is involved but also where the upstream device has used a hostname rather than an IP address in the Via it inserted. The default behaviour for routing of SIP responses over UDP, as described in RFC 3261, is to use address and port information embedded in the relevant Via header – the address is taken from the “received” parameter value and the port is taken from the “sent-by” component. If there is no port, it defaults to port 5060 for UDP and TCP, or port 5061 for TLS. However, this results in a rather messy hybrid solution:

  • The IP address where the response will be sent was set by the server
  • The port where the response will be sent was set by the client (and it may be wrong)
  • It doesn’t use symmetric communication so instead requires port forwarding in the NAT router

This was recognised as being unsatisfactory, so an alternative solution was described in RFC 3581.

The “rport” parameter

The issues mentioned above, particularly in the case of SIP using the UDP transport protocol, were addressed in RFC 3581. It describes the “rport” parameter as follows:

When a server compliant to this specification (which can be a proxy or UAS) receives a
request, it examines the topmost Via header field value.  If this Via header field value
contains an "rport" parameter with no value, it MUST set the value of the parameter to
the source port of the request. This is analogous to the way in which a server will insert
the "received" parameter into the topmost Via header field value. In fact, the server
MUST insert a "received" parameter containing the source IP address that the request
came from, even if it is identical to the value of the "sent-by" component.

The addition of an “rport” parameter alongside the old “received” parameter means that SIP responses can be passed back to the source using symmetric routing, although strictly this will only be correct if the response is sent from the same address and port that the corresponding request was received on (which is what normally happens anyway).

Thus, we have a simple solution to the problem when using UDP – the inclusion of the “rport” parameter in the topmost Via header of a SIP request. This depends on the behaviour of the client device to insert the empty rport parameter, which means it is not strictly a server-side solution. However, OpenSIPS has a trick here that you need to know about.

OpenSIPS behaviour when receiving a SIP request

If your OpenSIPS script supports registrations and acts as a simple Registrar server, then it will behave as an endpoint when receiving a REGISTER request. In this role, it does not need to add a Via header. It will use the transport protocol, IP address and port specified in the topmost Via header to send a response back upstream. If “rport” behaviour has been activated, it will send the response back to the source of the request. There may be a number of other cases where OpenSIPS acts as an endpoint (as a UAS), but the use cases that are of most interest here are those where OpenSIPS acts as a SIP Proxy. In these cases, it must add a Via header before relaying the request to the next node.

When acting as a SIP Proxy, OpenSIPS conforms to both of the RFC’s mentioned above – it adds a “received” parameter and responds to the presence of the “rport” parameter as it should. However, it is also possible to instruct it to behave as if the “rport” parameter were present even if it is not. This is done by calling the core function force_rport().

…but under what circumstances is it correct to call force_rport() in OpenSIPS?

One approach that could be taken is to use nat_uac_test() to inspect the address and port in the topmost Via header, then call force_rport() if a mismatch exists in either. This requires test 2 and test 16 which can be combined in a single function call passing an argument of “18” as follows:

# Force rport behaviour if topmost Via doesn't match request source addr:port
if (nat_uac_test("18"))
    force_rport();

Is it ever wrong to call force_rport()?

By calling force_rport(), we are making OpenSIPS use symmetric response routing. This means that all SIP responses over UDP will be sent back to the source port – the same port that the SIP request came from. In most cases this will be fine, but there could be a few exceptional cases where that is not the preferred behaviour. Sometimes, the client device deliberately sends from one port while listening for replies on another. [Note: This is very likely to happen when the transport protocol is TCP or TLS, but the RFC’s indicate that “rport” is a solution for UDP (connectionless communication) and describe different behaviours for TCP]. It could also happen when a firewall at the customer’s premises has been configured to do port forwarding to a specific device on the LAN using a pre-configured static port on the outside of the firewall.

If you encounter one of these situations, it will be necessary to add lines to your OpenSIPS script that somehow recognise the exceptions. How you do this will vary from case to case. In one project I worked on, exceptions were made if the User-Agent header contained a particular substring. You could inspect the port specified in the topmost Via and assume that, if it is 5060 or 5061, then it should not be changed. If necessary, you could add a further test to inspect the transport protocol. Here is a code snippet I have constructed which may give you some ideas:

# Force rport behaviour if topmost Via addr:port doesn't match source, but
#    don't force if topmost Via port is less than 5099 and address is same as source
if (nat_uac_test("18")) {
     if (!nat_uac_test("2") && $(hdr(Via){via.port}) < 5099)
         xlog("L_WARN", "NOT forcing rport. Via port is $(hdr(Via){via.port})\n");
     else
         force_rport();
}

The screen shots below illustrate the effect of calling force_rport() for a call from a server behind a NAT firewall, using UDP. The INVITE request is being sent to an OpenSIPS proxy server at address 89.200.a.b. The firewall, which has an external address 86.4.x.y had been configured with port forwarding on port 5060 – so any inbound packet arriving on port 5060 of the external interface would be forwarded to port 5060 on the server. Even so, it uses a randomly selected port on the external interface (port 15203), to send outbound packets to the Internet.

INVITE does not have rport parameter. OpenSIPS does not call force_rport()
An almost identical INVITE request, but this time OpenSIPS calls force_rport()

OpenSIPS behaviour when receiving a SIP response

Fortunately, when handling SIP responses the default behaviour in OpenSIPS is often sufficient without you needing to write any additional code in your script. It will strip off the topmost Via header and then inspect the next Via header to determine where to send the response. It will use the “received” and “rport” parameters if they are present and it will send from the same socket that the request was received on. It will respect the transport protocol defined in the relevant Via header. I am not completely sure if all this behaviour is coded in the core or is part of the TM module. It probably doesn’t matter because you will almost always want to use the TM module – I have never written a script that didn’t use it.

If you are new to OpenSIPS, it may not be immediately obvious how and where SIP responses are handled. This is because the main bulk of your script only handles SIP requests. As mentioned, responses are handled correctly, behind the scenes, without you needing to add any lines of code. However, you will soon find that you want to be able to intercept responses and add your own custom code, even if it is just to be able to write diagnostic messages to the log file. This is done by calling the t_on_reply() function at the appropriate point in main script routes and by adding a new onreply_route at the end of your script. Check the OpenSIPS TM module documentation to see how this works in practice.

OpenSIPS behaviour when adding a Via header

When it is acting as a SIP Proxy, OpenSIPS must add a new Via header to the SIP request before sending the request on to the next node, but what address and port does it use to populate that header? This depends initially on the transport protocol, the socket/listen statements and the advertised address and port specified in the ‘global parameters’ section of the script. It can also be explicitly set at run time within the script using the core functions set_advertised_address() and set_advertised_port().

The content of the Via header may be influenced by the selection of the “send socket” if this causes the transport protocol to change (e.g. SIP request received over UDP, but relayed to the next node over TCP). The default address that OpenSIPS will use in the Via header will be the first “listen” socket with a matching transport protocol – “listen” sockets are defined at the start of your script. In v3 of OpenSIPS, they start with the keyword “socket” but in earlier versions the keyword was “listen”. You can override this default using the advertised address global parameter or using set_advertised_address() in the executable section of your script. A similar process applies to the port. It is also possible to set the advertised address and port as part of the socket/listen statement using the optional parameter “as ip:port”. For example:

/* OpenSIPS v2.x.x and earlier */
listen = udp:127.0.0.1:5060 as 99.88.44.33:5060
/* OpenSIPS v3.x.x onwards */
socket = udp:127.0.0.1:5060 as 99.88.44.33:5060

The “send socket” may be selected through data entries in the tables used by certain routing modules (e.g. the Dynamic Routing module) or it may be explicitly set within the executable section of your script by calling force_send_socket() or writing a value into $socket_out if using v3 of OpenSIPS or into $fs if using an earlier version. That is how you would change the transport protocol.

You can see from the above that several factors influence the construction of the Via header. 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 article in this series will discuss the theory and practice for constructing an OpenSIPS script that will interconnect different transport protocols – e.g. receiving TLS and sending UDP or receiving WebRTC and sending TCP. As soon as the article is ready, a link will be provided here.

Is Via handling different when using TCP?

The quick answer is yes and this arises from the requirements defined in section 18.2.2 of RFC 3261:

If the "sent-protocol" is a reliable transport protocol such as TCP or SCTP, or TLS over those,
the response MUST be sent using the existing connection to the source of the original request
that created the transaction, if that connection is still open. This requires the server transport to
maintain an association between server transactions and transport connections. If that connection
is no longer open, the server SHOULD open a connection to the IP address in the "received"
parameter, if present, using the port in the "sent-by" value, or the default port for that transport, if
no port is specified.

This requirement, sometimes referred to as connection persistence, means that the information in the topmost Via header may largely be disregarded by the proxy server – the proxy server only needs to be able to do the following:

  • match a response to an earlier request
  • have an internal record of all existing connections
  • keep connections open for sufficient time that responses can re-use them

Some of the above tasks are essentially the responsibility of the transport layer rather than the application, although the (global) core parameter tcp_connection_lifetime has a direct bearing on connection persistence because it determines how long a connection stays open after the initial request is received.

The mechanism used to match a response to an earlier request is not explicitly defined in the RFC’s. I assume it would still inspect the topmost Via header, but it could use other information such as the Call ID. From practical experiments I can confirm that OpenSIPS ignores the port specified in the “sent-by” component of the topmost Via when the original TCP connection is still open. This behaviour is virtually identical to that for UDP when the “rport” parameter is present (and has a value), but with TCP it requires the proxy to act in a stateful manner whereas for UDP it could, in theory, be stateless because the information is all contained in the header.

The “alias” parameter and RFC 5923

This is probably an appropriate point to mention the “alias” parameter. It is defined in RFC 5923 which is all about connection re-use for reliable connection-orientated transport protocols such as TCP or TLS. When the topmost Via header on a SIP request contains the “alias” parameter, the server should add the connection to an indexed internal list. The idea is that this connection can be found again later, allowing the same connection to be re-used. When I first read this, it seemed to overlap with the existing provisions of RFC 3261 section 18.2.2 as mentioned above. However, there is a subtle difference – the purpose of the “alias” parameter is to encourage efficient use of existing connections for requests outside the scope of section 18.2.2. For example, it permits an upstream SIP request to re-use the connection established during receipt of the downstream SIP request. This is nicely illustrated by the following two SIP sequence diagrams snapped using sngrep.

INVITE and BYE requests without “alias” behaviour
Identical INVITE and BYE but with force_tcp_alias()

It also allows re-use of a connection for requests that are completely unrelated to the original SIP dialogue – for example, multiple calls could all use the same TCP connection.

Why are TCP connections treated differently?

Establishing a TCP connection is quite resource intensive – i.e. it is relatively slow and it uses up server and network resources that are finite. There is a finite limit to the number of TCP connections that an application can open and to the total number that a server can open. While these constraints may not present any problem to a small company operating an IP-PBX with perhaps a hundred extensions, it can be a big headache for a VoIP service provider whose systems have to cater for hundreds of thousands of calls per hour. If connection persistence and re-use can halve the number of TCP connections opened by the server then that is a benefit worth winning. In this respect, when using OpenSIPS you should pay attention to the values assigned to the following (global) core parameters: tcp_max_connections, tcp_connection_lifetime.

Another important difference between UDP and TCP is that TCP connections will generally use an ephemeral port to connect to a remote destination. The concept, which is explained well in RFC 5923 and also in a Wikipedia article here, means that a node opening a new TCP connection will use a “random” high numbered port at the client-end of the connection while it is listening for incoming connections on a static pre-defined port such as 5060 or 5061. OpenSIPS uses ephemeral ports when it is setting up a new connection in TCP or TLS (as a client), but this behaviour can be modified for some SIP requests if force_tcp_alias() was called when some earlier connection was opened between the same two servers and that connection is still open.

Conclusion

When I started writing this article, I thought it would be really short and simple, but as I investigated it seemed like one of those fractal images where you can just keep digging deeper and deeper, finding endless details. Consequently, it was difficult to structure the discussion coherently, but it just about hangs together. I hope you found it useful and learnt something new. As usual, please show appreciation by clicking the Facebook Recommended button at the top or the Like button below, or leave me a comment.

Other reading:

+1

Leave a Comment