Handling of NAT using RTPproxy

What this ser.cfg will do:

  1. Introduce specialized processing which is necessary when dealing with SIP clients that are behind a NAT device such as a DSL router or corporate firewall

RTPproxy is one of two NAT traversal solutions for SER, the other is mediaproxy just descussed in the previous section. Both RTPproxy and mediaproxy are known as far-end NAT traversal solutions which means that they handle all aspects of NAT at the SIP proxy location rather than at the SIP client location.

There are many advantages to handling NAT issues at the SIP proxy server, the primary benefit being that SIP client configuration is much simpler. Some of the important features of RTPproxy are:

  1. Is called by nathelper, the most configurable NAT helper modules (mediaproxy module is the other one)

  2. Can handle almost twice as many calls as mediaproxy on the same hardware

  3. Developed in C and is thus easily extendable for most programmers

  4. RTPproxy can be installed on a remote server which is not running SER

RTPproxy is a standalone software and is thus not a part of the sip_router CVS and distribution. However, it can be found in the same CVS repository as SER (on the same level as sip_router). Just replace the sip_router directory name with rtpproxy (or use the ONsip.org Getting Started source package). The SER distribution only includes the glue which gives SER the ability to communicate with a running instance of RTPproxy. This glue is known as the nathelper module.

NOTE: In order for RTPproxy to function properly it must be configured to listen on a public IP address. Also, in most real world configurations, RTPproxy will not be installed on the SER server, but on a remote machine. Refer to the appendix for information on installing rtpproxy.

debug=3
fork=yes1
log_stderror=no2

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"3
loadmodule "/usr/local/lib/ser/modules/uri_db.so"
loadmodule "/usr/local/lib/ser/modules/nathelper.so"4
loadmodule "/usr/local/lib/ser/modules/textops.so"5

modparam("auth_db|uri_db|usrloc", "db_url", "mysql://ser:heslo@localhost/ser")
modparam("auth_db", "calculate_ha1", 1)
modparam("auth_db", "password_column", "password")

modparam("nathelper", "natping_interval", 30)6 
modparam("nathelper", "ping_nated_only", 1)7   
modparam("nathelper", "rtpproxy_sock", "unix:/var/run/rtpproxy.sock")8

modparam("usrloc", "db_mode", 2)

modparam("registrar", "nat_flag", 6)9

modparam("rr", "enable_full_lr", 1)

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!="REGISTER") {
    record_route();
  };

  if (method=="BYE" || method=="CANCEL") { 10
    unforce_rtp_proxy();
  } 

  # -----------------------------------------------------------------
  # Loose Route Section
  # -----------------------------------------------------------------
  if (loose_route()) { (11)

    if ((method=="INVITE" || method=="REFER") && !has_totag()) {
      sl_send_reply("403", "Forbidden");
      break;
    };

    if (method=="INVITE") {

      if (!proxy_authorize("","subscriber")) {
        proxy_challenge("","0");
        break;
      } else if (!check_from()) {
        sl_send_reply("403", "Use From=ID");
        break;
      };

      consume_credentials();

      if (nat_uac_test("19")) {
        setflag(6);
        force_rport();
        fix_nated_contact();
      };
      force_rtp_proxy("l");
    };

    route(1);
    break;
  };

  # -----------------------------------------------------------------
  # Call Type Processing Section
  # -----------------------------------------------------------------
  if (uri!=myself) {
    route(4);(12)
    route(1);
    break;
  };

  if (method=="ACK") {
    route(1);
    break;
  } if (method=="CANCEL") { (13)
    route(1);
    break;
  } else if (method=="INVITE") {
    route(3);
    break;
  } else  if (method=="REGISTER") {
    route(2);
    break;
  };

  lookup("aliases");
  if (uri!=myself) {
    route(4); (14)
    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"); (15)

  if (!t_relay()) { (16)
    if (method=="INVITE" && isflagset(6)) {
      unforce_rtp_proxy();
    };
    sl_reply_error();
  };
}

route[2] {

  # -----------------------------------------------------------------
  # REGISTER Message Handler
  # ----------------------------------------------------------------

  (17)if (!search("^Contact:[ ]*\*") && nat_uac_test("19")) {
    setflag(6);
    fix_nated_register();
    force_rport();
  };

  sl_send_reply("100", "Trying");

  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 (!proxy_authorize("","subscriber")) {
    proxy_challenge("","0");
    break;
  } else if (!check_from()) {
    sl_send_reply("403", "Use From=ID");
    break;
  };

     consume_credentials();

  if (nat_uac_test("19")) { (18)
    setflag(6);
     }

  lookup("aliases");
  if (uri!=myself) {
    route(4); (19)
    route(1);
    break;
  };

  if (!lookup("location")) {
    sl_send_reply("404", "User Not Found");
    break;
  };

  route(4); (20)
  route(1); (21)
}

(22)route[4] {

  # -----------------------------------------------------------------
  # NAT Traversal Section
  # -----------------------------------------------------------------

  if (isflagset(6)) {
    force_rport();
    fix_nated_contact();
    force_rtp_proxy();
  }
}

(23)onreply_route[1] {

  if (isflagset(6) && status=~"(180)|(183)|2[0-9][0-9]") { (24)
      if (!search("^Content-Length:[ ]*0")) { (25)
      force_rtp_proxy();
    };
  };

  (26)if (nat_uac_test("1")) {
    fix_nated_contact();
  };
}

RTPproxy Transparent NAT Traversal ser.cfg Analysis

1

Up until now we have run SER as a foreground process. From this point forward we will run SER as a daemon. The fork directive tells the SER daemon to run in the background. This is a requirement for using the init.d start script shown in the appendix.

2

Since we are running SER as a background process we must set the log_stderror directive equal to no in order to not keep SER in the foreground.

3

The uri module is introduced here to access the has_totag() function. This function is necessary for processing re-INVITEs and is described below.

4

Here we load the nathelper module. Nathelper has functionality for rewriting SIP messages. It also communicates with rtpproxy, which is a standalone program. Make sure that you have rtpproxy started before you start SER. It can be found in the same cvs repository as SER or you can use the SER package provided on http://www.onsip.org/ under Downloads. Start rtpproxy without parameters and make sure it is running using the 'ps -ax | grep rtpproxy' command. If SER cannot communicate with rtpproxy, you will get errors in /var/log/messages when staring SER."

5

The textops module provide utility functions for manipulating text, searching for substrings, and checking for the existence of specific header fields.

6

Unlike mediaproxy, rtpproxy does not have a built-in ping keep-alive capability (i.e. sending packets regularly to the clients to make sure the NAT binding is kept in place and thus incoming SIP messages or RTP streams can come in). However, this functionality is instead built into nathelper.

The natping_interval is a very crucial setting which controls how often our ser proxy will ping registered SIP UAs. Most NAT devices will only hold port bindings open for a minute or two, so we specify 30 seconds here.

This causes ser to send a 4-byte UDP packet to each SIP UA every 30 seconds. This is all that is required to keep NATed clients alive.

NOTE: Some NAT devices have been reported to not allow incoming keep-alives. Thus, many UAs have their own implementations of keep-alive. If you experience one-way audio problems after a while, you may have run into this problem. The only solution is to turn on user client keep-alives.

7

Nathelper can ping all UAs or only the ones that have been flagged with a special nat flag (see further below). We only need to ping NATed clients as we assume all other clients have public addresses and keep-alive is not needed.

8

Ser and rtpproxy communicate via a standard Unix socket, with the default socket path specified here.

NOTE: If you change the socket path here, you must be sure to start rtpproxy with the parameter: s unix:socketpath

You can also start rtpproxy with f to let it run without forking. It will then show you what is happening.

9

When SIP clients attempt to REGISTER with our SIP proxy we need a way to tell the registrar module to store NAT information for the UA. We do this by using flag 6, which has been arbitrarily chosen (but defined earlier in the moduel parameter section). We could have specified another integer here, but 6 has become the accepted default for nat_flag.

When nat_flag is set before calling the save() function to store contact information, then ser will also preserve the NAT contact information as well as set the flags column in the MySQL location table. By doing so, we can call lookup(location) when processing messages and flag 6 will be set for NATed clients.

10

When we decide that a particular call must be proxied through our proxy server, we must also ensure that the call is torn down when a call is hung up (BYE) or cancelled (CANCEL).

(11)

Our NAT traversal requirements must handle re-INVITE messages in a special manner to prevent RTP media streams from dropping off during a re-INVITE. So we do special re-INVITE NAT processing here. The has_totag() function will return true if the SIP message has a To header field. If it has, the message be will be in-dialog.

force_rtp_proxy() has an l (lookup) parameter. When this parameter is specified, rtpproxy will only proxy the call if a session already exists. The reason for doing this is that rtpproxy cannot really know that this is a re-INVITE and we want to avoid unnecessary proxying calls that do not need it. All re-INVITEs will call force_rtp_proxy(l), but only existing proxied sessions will be re-proxied. We cannot put the force_rtp_proxy() call inside the nat_uac_test check, as this will prevent proxying to happen if the re-INVITE is sent from a non-NATed UA to a NATed UA (because nat_uac_test() will be false).

The other things we do here are: if NAT is detected for the UA sending the INVITE, set the NAT flag, so we know this is a NATed call (setflag(6)), add the received port to top-most via header (force_rport), as well as rewrite the Contact header with the public IP address of the user client (fix_nated_contact).

(12)

In the event that our message is no longer to be handled by our SIP router, we call our NAT handling route block to enable rtpproxy if needed before sending the message to its destination.

(13)

We now explicitly handle CANCEL messages. CANCEL messages can be safely processed with a simple call to t_relay() because SER will automatically match the CANCEL message to the original INVITE message. So here we just route the message to the default message handler.

(14)

Enable rtpproxy if needed before sending the message to its destination.

(15)

When dealing with NATed clients, we must correctly handle response messages that are destined back to the client. These response messages are accessible in ser by using a reply_route block.

Ser allows you to specify multiple reply_route blocks, which can perform many tasks. Here we specify that any reply messages must be passed to reply_route["1"], which is defined at the end of the ser.cfg file.

In order to invoke a reply_route, you simply need to set the handler prior to calling t_relay(), as we have done here.

(16)

If something goes wrong and we have called force_rtp_proxy (so that rtpproxy set up a new proxy call), we need to tell rtpproxy to tear it down again using unforce_rtp_proxy().

(17)

When SIP clients attempt to REGISTER with our SIP proxy we need a way to tell the registrar module to store NAT information this particular UA. We do this by using flag 6, which has been arbitrarily chosen (but defined earlier in the module parameter section). We could have specified another integer here, but 6 seems to be the accepted standard for nat_flag.

When nat_flag is set before calling the save() function to store contact information, then ser will also preserve the NAT contact information as well as set the flags column in the MySQL location table. By doing so, we can later call lookup(location) when processing messages and flag 6 will be set for NATed clients.

In order to determine whether a SIP client is NATed we use the nathelper function nat_uac_test(), which accepts an integer as a parameter. This parameter specifies which part of the SIP message to examine for determining the NAT status of the client. 19 indicates a common subset of tests, and are the recommended tests.

NOTE: These are the tests you can run using nat_uac_test (in the order they are employed):

  1. (16) Is received port (source port of the packet) different from the port specified in the top-most Via header? (this can be used to detect some broken STUN implementations)

  2. (2) Is the received IP address (source IP of the packet) different from the IP address in the top-most Via header?

  3. (1) Does the Contact header contain an RFC1918 address? RFC1918 addresses are 10.0.0.0/24, 172.16.0.0.0/20, and 192.168.0.0/16

  4. (8) Does the SDP payload of an INVITE or OK message contain an RFC1918 address?

  5. (4) Does the top-most Via header contain an RFC1918 address?

The numbers in paranthesis indicate the number to be used to invoke the test in nat_uac_test(). To invoke more than one test, add up the numbers of the tests you want to run. So, as we use nat_uac_test(19) in our script, it means that test #1 (16) + #2 (2) + #3 (1) = 19, are run.

WARNING: Before changing the tests, you should know what you are doing. You should scrutinize the SIP message coming from your UAs to determine which tests will be true.

You will notice that we first call the search() function to find out if the <Contact:> header contains an asterisk or not. We only test for NAT if the <Contact:> header is not an asterisk.

NOTE: If the <Contact:> header is an asterisk rather than a SIP URI, then this indicates that the SIP client is attempting to unregister with the SIP proxy. The ser save() function has special provisions to remove all binds for the AOR in this case. It is important to understand that we cannot accurately determine the NAT status of a client when the contact information is missing.

(18)

We test to see if the user client is behind NAT, if so, we set the NAT flag (6). This flag is used further below.

(19)

Enable rtpproxy if needed before sending the message to its destination.

(20)

Enable rtpproxy if needed before sending the message to its destination.

(21)

Now that we have taken care of all the NAT related items, we can safely send the INVITE message to its destination.

(22)

Route[4] is a convenience route to enable rtpproxy.

If a client is NATed, we must do the following things: Add the received (public) port to the top-most Via header (force_rport), rewrite the Contact header with the public IP address and port of the NAT in front of the user client (fix_nated_contact), and set up the proxying using force_rtp_proxy(). Nathelper will then communicate to rtpproxy, which will allocate RTP (UDP) ports and the SDP payload of the INVITE will be rewritting (see the introductory section on NATing for details).

(23)

Here we introduce a reply_route. A reply_route is defined just like any other block in ser. The only difference is that it is called onreply_route.

Any message that is passed to this block will be returning to the original sender. You can think of these messages as the response to the original request that the caller made. The types of messages that will appear here will have an integer response code, much like HTTP response codes. Examples here would be 200, 401, 403, and 404.

(24)

In this ser.cfg we are only interested in response codes of 180, 183, and all 2xx messages for NATed clients. We can check the status as shown with a regular expression. If any of these response codes are found then this statement will be TRUE.

An important thing to note is that we can check flags set in the other route blocks because their scope is still valid. So our caller and callee NAT flags are still accessible.

(25)

We can only call force_rtp_proxy() for SIP messages that have a valid <contact> parameter in the SDP payload. So here we test to make sure the c= parameter is valid by simply checking the SDP payload length. We assume that if we have an SDP payload then we will have a c= parameter and can call force_rtp_proxy ().

(26)

Before our reply_route processing is complete we have one more NAT test to try. Here we make sure the <Contact:> header does not contain an RFC1918 IP address. If it does, then we replace it with the public IP address and port of the NAT in front of the user client.