Преодоление NAT с помощью RTPproxy.

В приведённом ниже ser.cfg показаны необходимые настройки для связи SIP клиентов, находящихся за устройствами с NAT вроде DSL маршрутизаторов или корпоративных брандмауэров.

RTPproxy является одним из двух решений SER для прохождения через NAT, второе - это mediaproxy , которое описывалось в предыдущем разделе. Оба этих решения называются удалёнными, т.к. решают задачу прохождения NAT не на клиентской стороне, а на SIP сервере. Удалённое решение имеет много преимуществ, главное из которых - гораздо более простая настройка SIP клиентов. Важными свойствами RTPproxy являются:

  1. Вызывается с помощью nathelper - самым гибким в настройках модуля прохождения NAT (другой модуль - это mediaproxy).
  2. Обрабатывает вдвое больше вызовов, чем mediaproxy, на том же оборудовании.
  3. Написана на языке C и доступна для улучшений и исправлений большинству программистов.
  4. Может быть установлена и работать на другом сервере, нежели SER.

RTPproxy - это отдельная программа, не входящая в состав SER, но её дистрибутив rtpproxy доступен вместе с SER в том же CVS repository. SER содержит в себе только функционал для взаимодействия с RTPproxy, и им является модуль nathelper.

ЗАМЕЧАНИЕ: Для правильного функционирования RTPproxy необходимо, чтобы она работала через публичный IP адрес. Кроме того, на практике RTPproxy чаще всего устанавливают на отдельный сервер, нежели на котором работает SER. См. приложение по описанию установки rtpproxy.

#------------------------------ ser.cfg example ------------------------------
debug=3
fork=yes #1
log_stderror=no #2

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
  # ----------------------------------------------------------------

  if (!search("^Contact:[ ]*\*") && nat_uac_test("19")) { #17
    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
}

route[4] { #22

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

  # -----------------------------------------------------------------

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

onreply_route[1] { #23

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

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

Анализ ser.cfg для прохождения NAT с помощью RTPproxy.


1) До сих пор мы запускали SER как foreground процесс. С этого момента он будет работать у нас в фоновом режиме, для этого используется команда fork=yes. Это требование запуска с помощью init.d скрипта, описанного в приложении.

2) Поскольку мы запускаем SER в фоновом режиме, то должны запретить вывод сообщений в stderr.

3) Здесь добавили модуль uri, чтобы вызывать функцию has_totag, она необходима для обработки re-INVITE сообщений и описана ниже.

4) Здесь загружается модуль nathelper, который используется для модификации SIP сообщений и взаимодействия с RTPproxy. Убедитесь, что RTPproxy запускается перед началом работы SER. Запустите RTPproxy без параметров, и с помощью команды 'ps -ax | grep rtpproxy' убедитесь в наличии её процесса. Если SER не сможет взаимодействовать с RTPproxy, то соответствующие сообщения об ошибках будут выведены в /var/log/messages при старте SER.

5) Модуль textops содержит функции обработки строк, поиска вхождения одних строк в другие и проверки наличия особых полей в заголовках сообщений.

6) В отличие от mediaproxy RTPproxy не имеет собственных средств поддержания активности подключений (т.е. регулярной посылки клиентам специальных пакетов - пингов, чтобы NAT не отключал соединения по неактивности, и SIP сообщения или RTP потоки достигали клиента). Но эти средства ест в nathelper. Самый важный параметр - natping_interval, который определяет как часто SER будет пинговать SIP клиента. Чаще всего устройства NAT сохраняют неактивные подключения в течении одной-двух минут, поэтому мы зададим здесь 30 сек. Это заставит SER посылать SIP клиенту UDP пакет из 4 байт каждые 30 сек - этого достаточно, чтобы сохранить соединения через NAT активными.
ЗАМЕЧАНИЕ: Некоторые NAT устройства не реагируют на входящие пинги для поддержания соединений, но у многих SIP клиентов есть собственный функционал для этого. Поэтому, если вы столкнулись с односторонней слышимостью спустя какое-то время после начала сеанса, то, возможно, у вас именно эта проблема. Решить её поможет включение клиентских возможностей для поддержания активности соединений.

7) Nathelper может пинговать либо всех SIP клиентов, либо только тех, которые помечены специальным nat-флажком (см. далее). Нам надо пинговать только клиентов за NAT, у остальных мы полагаем наличие публичного IP адреса и пинговать их не требуется.

8) SER и RTPproxy по умолчанию обмениваются информацией через unix-сокет, путь к которому здесь указан.
ЗАМЕЧАНИЕ: Если изменить здесь описание сокета, то надо не забыть запустить RTPproxy с указанием нового сокета (ключ запуска -s). RTPproxy также можно запустить в foreground режиме - для этого используется ключ запуска -f.

9) Когда SIP клиент регистрируется на нашем SIP сервере, нам надо сообщить модулю registrar зафиксировать NAT информацию про этого клиента. Мы делаем это установкой флажка 6 - это значение выбрано произвольно (но уже определено заранее в разделе параметров модулей). Можно было бы использовать флажок с другим значением, но 6 уже закрепилось за nat_flag. Если nat_flag установлен до вызова функции save("location"), сохраняющей информацию о клиенте, то SER сохранит её в MySQL вместе со значением nat_flag. В дальнейшем, обрабатывая следующие сообщения и вызывая lookup("location"), мы восстанавливаем значение флажка 6 для клиентов за NAT.

10) Если мы решили, что некоторый звонок будет проходить через наш RTPproxy, то мы должны позаботиться о завершении проксирования при поступлении сигнала окончания (BYE) и обрыва (CANCEL) соединения.

11) Наше решение прохождения NAT требует особой обработки сообщений re-INVITE, чтобы при их поступлении не обрывались уже работающие RTP соединения, и мы делаем это здесь. Функция has_totag возвращает true, если SIP сообщение содержит заголовок To, а, значит, сообщение является частью диалога. force_rtp_proxy имеет параметр 'l' ('lookup'), и когда он указан, RTPproxy будет пропускать потоки, только если они уже проксируются. Данный метод необходим, т.к. RTPproxy не знает, что пришёл именно re-INVITE, а мы хотим избежать ненужного проксирования. Все сообщения re-INVITE будут вызывать force_rtp_proxy("l"), но продолжены будут только уже запущенные сессии. Помещать вызов force_rtp_proxy("l") внутри блока проверки nat_uac_test нельзя, т.к. это помешает проксированию, когда re-INVITE придёт от публичного клиента к клиенту за NAT (поскольку в этом случае nat_uac_test вернёт false). Если мы обнаружили, что INVITE прислал клиент за NAT, то устанавливаем nat_flag, чтобы пометить данный вызов (setflag(6)), добавляем номер порта, с которого пришёл вызов, к самому первому заголовку Via (force_rport) и изменяем заголовок Contact, чтобы там фигурировал публичный адрес и порт NAT сервера клиента (fix_nated_contact).

12) В случае, когда полученное сообщение уже не нуждается в обработке нашим SIP сервером, мы вызываем блок NAT обработки, где, если требуется, используется RTPproxy, а затем пересылаем сообщение по назначению.

13) Сообщение CANCEL нужным образом обрабатывается при вызове t_relay(), т.к. SER автоматически определяет соответствие CANCEL и первоначального INVITE. Поэтому сообщение просто передаётся стандартному обработчику.

14) Если требуется, перед отсылкой сообщения по назначению вызывается RTPproxy.

15) Когда мы работаем с NAT клиентами, необходимо правильно обрабатывать ответные сообщения. В SER это происходит в блоке reply_route. SER позволяет определить несколько reply_route блоков, каждый из которых может выполнять несколько задач. Здесь мы задаём, что все ответные сообщения должны проходить через reply_route1, расположенный в конце ser.cfg. Необходимый reply_route должен вызываться перед обращением к t_relay, как это сделано здесь.

16) Если произошла ошибка, а мы уже воспользовались force_rtp_proxy, надо заставить RTPproxy прекратить проксирование, вызвав unforce_rtp_proxy.

17) Когда SIP клиент регистрируется на нашем SIP сервере, нам надо сообщить модулю registrar зафиксировать NAT информацию про этого клиента. Мы делаем это установкой флажка 6 - это значение выбрано произвольно (но уже определено заранее в разделе параметров модулей). Можно было бы использовать флажок с другим значением, но 6 уже закрепилось за nat_flag. Если nat_flag установлен до вызова функции save("location"), сохраняющей информацию о клиенте, то SER сохранит её в MySQL вместе со значением nat_flag. В дальнейшем, обрабатывая следующие сообщения и вызывая lookup("location"), мы восстанавливаем значение флажка 6 для клиентов за NAT. Чтобы определить, находится ли SIP клиент за NAT, мы используем функцию nat_uac_test модуля nathelper, которая имеет целочисленный аргумент. Он указывает, какой тест используется для определения. Весь набор тестов вызывается при значении аргумента 19, и мы рекомендует использовать именно его.
ЗАМЕЧАНИЕ: Список тестов функции nat_uac_test в порядке их выполнения:
  1. (16) Порт источника сообщения отличается от порта, указанного в самом первом заголовке Via (это может использоваться для обнаружения некорректной работы STUN).
  2. (2) IP адрес источника сообщения отличается от IP адреса в самом первом заголовке Via.
  3. (1) IP адрес в заголовке Contact принадлежит одной из частных сетей, определённых в RFC1918 (10.0.0.0/24, 172.16.0.0.0/20 и 192.168.0.0/16).
  4. (8) IP адрес в SDP части сообщений INVITE или OK принадлежит одной из частных сетей, определённых в RFC1918.
  5. (4) IP адрес в самом первом заголовке Via принадлежит одной из частных сетей, определённых в RFC1918.
Число в скобках - это значение аргумента nat_uac_test для выполнения указанного теста. Чтобы выполнить сразу несколько тестов, надо указать сумму соответствующих значений. Поэтому, когда мы вызываем в скрипте nat_uac_test(19), это означает, что будут выполнены тесты #1(16)+#2(2)+#3(10)=19.
ПРЕДУПРЕЖДЕНИЕ: Меняя набор используемых тестов, вы должны чётко осознавать, что делаете. Следует изучить SIP сообщения от ваших клиентов, чтобы убедиться в правильном срабатывании тестов.
Обратите внимание, что сначала мы вызываем функцию search для поиска звёздочки (*) в заголовке Contact, и если её нет, только тогда проводим тесты на наличие NAT.
ЗАМЕЧАНИЕ: Если заголовок Contact состоит из звёздочки, а не SIP URI, это означает, что SIP клиент хочет отменить свою регистрацию на SIP сервере. В этом случае функция save удалит все соответствующие записи из SER. И надо понимать, что отсутствие необходимой информации в заголовке Contact уменьшает точность определения, что клиент находится за NAT.

18) Мы проводим тестирование, находится ли клиент за NAT, и если да, то выставляем nat_flag (6), который используется далее.

19) Запускаем проксирование перед отсылкой сообщения получателю.

20) Запускаем проксирование перед отсылкой сообщения получателю.

21) Теперь, когда мы позаботились обо всём, что касается NAT, мы можем спокойно пересылать INVITE получателю.

22) Route4 - это удобная процедура для запуска проксирования. Если клиент находится за NAT, мы должны сделать следующее: добавить порт источника сообщения в самый первый заголовок Via (force_rport), изменяем заголовок Contact, чтобы там фигурировал публичный адрес и порт NAT сервера клиента (fix_nated_contact), и запускаем проксирование вызовом force_rtp_proxy. Модуль nathelper связывается с RTPproxy, которая выделяет необходимые RTP (UDP) порты, и соответственно переписывает SDP часть сообщения INVITE (см. вводную часть задачи работы с NAT).

23) Здесь добавлена процедура reply_route, которая оформлена точно так же, как и любой другой блок скрипта SER, только называться она должна onreply_route. Любое сообщение, переданное в этот блок, будет возвращено тому, кто его послал. Такие сообщения являются по сути ответами на запросы того, кто их послал. Типы таких ответных сообщения представляют собой целочисленный код подобно HTTP результатам, например, 200, 401, 403 и 404.

24) В рассматриваемом ser.cfg для клиентов за NAT нас интересуют только ответные коды 180, 183 и 2xx. Мы можем проверить содержание ответа с помощью указанного регулярного выражения, которое вернёт true на любой из нужных нам кодов. Важно отметить, что мы можем проверять состояние флажков, которые были установлены в других блоках обработки, и поэтому нам доступны NAT флажки вызывающего и вызываемого клиентов.

25) Мы можем вызывать force_rtp_proxy только для тех SIP сообщений, которые имеют корректный заголовок Contact в SDP части. Для этого мы здесь просто проверяем размер SDP части, и если он больше нуля, то полагаем, что там всё прописано как надо, и можем вызывать force_rtp_proxy.

26) Перед завершением reply_route обработки мы делаем ещё одну проверку на наличие NAT - убеждаемся, что заголовок Contact не содержит IP адрес из частных сетей RFC1918. Если это не так, то мы меняем его на публичный IP адрес и порт NAT сервера.