Создание отказоустойчивого сервера Asterisk с поддержкой балансировкой нагрузки (sip voip asterisk openser balance cluster kamailio)

Оригинал: http://asteriskpbx.ru/blog/2009/09/13

Часть 1.

Пролог

Отличное приложение Asterisk, но свои косяки в нем тоже имеются, от утечек памяти появляющихся под большой нагрузкой, до багов которые еще никто не заметил. В итоге случается так, что до бесконечности Call центр на asterisk на одной машине масштабировать нельзя, рано или поздно утыкаемся в потолок производительности и система начинает периодически падать. Одна беда если просто звонок оборвался, но как правило Call центр у большой компании постепенно обрастает различным функционалом, по нему начинают вести статистику обращений по различным вопросам, по длительности вызовов на различные службы делают выводы о лояльности клиентов, по записанным разговорам решают конфликтные ситуации, по статистике очередей считают зарплаты операторам. 

В общем: если компания завязана на общение с многочисленными клиентами, то нестабильная работа Call центра нарушает слаженное взаимодействие разных отделов, приводит к уменьшению эффективности работы компании и как следствие уменьшению прибыли. Это все конечно банальные вещи, но просто захотелось излить душу, потому что с данной проблемой я столкнулся в полной мере. Для специалиста по телефонии в большой компании с количеством абонентов > 100000 нестабильность Call центра приводит к появлению стойкой головной боли :) и появлению нервного тика :) , не говоря уже о том что даже банальный выезд за город происходит с задней мыслью, а что если Call центр опять свалится, а рядом меня не будет :)

Как быть дальше?

Свелось все к тому, что так больше работать никто не мог. Решили принимать какие-то меры. Первая самая очевидная мера - это разделить Call центр на несколько машин. Да это работает, но не избавляет от всех прелестей по увеличению количества работы после каждой переконфигурации в системе, причем это количество работы растет с увеличением абонентов. Начали думать о каком-нибудь адски крутом проприетарном Call центре за много денег, надо сказать что я был категорически против, вспомнив только однажды увиденный прайс от Nec, с его кучей лицензий и непонятных "кабинетов" становилось плохо:). Слава ктулху мой голос был услышан и сомнительное решение за много денег покупать передумали, а решили попытаться построить решение на уже проверенном и знакомом Asterisk PBX.

Что же мы хотим?

Чего хотелось бы от Call центра, чего на данный момент у нас не было?

1. Безотказную систему, желательно с полным резервирование и без единой точки отказа

2. Легкую реализацию load balansing между машинами Call центра

3. Масштабируемую систему. Чтобы для обработки увеличившейся нагрузки необходимо было бы только добавить еще одну машину, причем чтобы это не требовало перенастройки всей системы, необходимо чтобы это мог сделать обычный инженер по эксплуатации (да у нас и такие есть :))

4. Единую точку хранения учетных записей системы, у нас все сервисы для абонентов привязаны к логину и паролю, авторизацию для пользования сервисам осуществляет Radius, в общем - идеальный вариант, если Call центр будет работать с Radius

5. Единая точка хранения статистики работы Call центра, ну тут и к бабке не ходи, asterisk-addons и mysql :)

6. Гибкое API для разработки собственных интерфейсов управления и просмотра статистики работы Call центра и тут Asterisk нам подходит: AMI, CDR, queue_log

В общем необходимо было реализовать Load Balansing, Radius, масштабируемость и безотказность. Всего ничего :) , не говоря уже о том что в процессе разработки наверняка всплывет еще куча всяких ньюансов.

Как же все это сделать?

Краем уха я слышал где-то про SIP софтсвитч на OpenSer?, когда начал искать как же он работает наткнулся на одном из любимых форумов по asterisk на книженцию Building Telephony Systems with OpenSER. Благодаря ей был освоен очень нелегкий в понимании OpenSer?. Изучив документацию стало ясно, что авторизация через Radius это не проблема, балансировка тоже. Решено было строить систему такого вида:

Т.к. мы предоставляем сервис телефонии то имеем стык с PSTN с несколькими операторами по E1. В VoIP загоняем его через 2 Cisco AS5350. Которые гонят трафик по SIP на sipbalanser, в качестве которого используется софтсвитч kamailio, бывший проект OpenSer? (все наверное в курсе что этот проект форкнулся на kamailio и opensips, вроде оба проекта развиваются но документацию мне больше по душе у kamailio). На sipbalanser-е регистрируюся сотрудники Call центра, их авторизует Radius, kamailio для связи с Radius использует radiusclient-ng. 

Логины и пароли хранятся в общей базе данных компании, в качестве сервера Radius используется Free Radius 2. Сведения о зарегистрированных клиентах хранятся в локальной базе данных на MySql?. Запросы на установление соединения маршрутизируются на sipbalanser, который затем с помощью модуля dispatcher распределяет их на ноды asterisk по алгоитму round robin. Asterisk обрабатывает звонок, если надо проигрывает музыку, помещает вызов в [112]IVR или делает любое свойственное asterisk-у действие с звонком, затем если нужно соединить с реальным человеком, то звонок отправляется на нужного нам сотрудника обратно на sipbalanser, тот ищет у себя в локальной базе зарегистрированного User Agent-а и если находит отдает ему вызов.   

Часть 2

В этой части я расскажу как настроить Kamailio чтобы его можно было использовать как SIP Proxy, на который будет возложена функции:

  1. Регистрации SIP User Agent-ов (авторизация возложена на Radius сервер)
  2. Преодоления NAT, с помощью media-proxy
  3. Маршрутизации вызовов
  4. Балансировки вызовов на ноды Asterisk

Как видно на схеме на sipbalanser-е запущены приложения:

1. Kamailio - Наш SIP Proxy

2. MySQL - в моем случае kamailio использует только одну таблицу из целой кучи таблиц разного назначения, я же использую только таблицу openser.locations, в ней храняться зарегистрированные UA, MySQL может находиться на отдельном сервере, у меня пока все на одном сервере

3. Media-dispatcher - управляющий модуль mediaproxy, kamailio подключается к нему через UNIX сокет, служит для управления media-relay и вывода информации для мониторинга, написан на Python

4. Media-relay - релей RTP трафика, написан на Python, но сам трафик релеит Linux ядро, (любителям FreeBSD не рекомендую использовать для релея RTP трафика предыдущие версии mediaproxy, они там работают, но при большом количестве звонков валится mediaproxy), используется механизм contrack, media-relay может находиться на отдельном сервер, и их может быть сколько угодно, очень удобно в плане масштабирования, сигнализацию на одном сервере обрабатываем, а RTP трафик на другом

5. Radiusclient-ng - через него мы контактируем с radius

6. Asterisk - :) о нем наверное сами все знаете

Установка

Установка всего этого добра весьма банальна, все кроме Kamailio и mediaproxy можно поставить из портов, пакетов и репозитариев, как удобно, а ключевые для нас компоненты лучше ставить из исходников, чтобы и версии последние были и весь процесс прочувствовать и понять что для этих компонентов нужно. Я описывать процесс установки не буду, т.к. сам ставил давно, и уже не помню всех подробностей, а ставить заного ради статьи - лень J Все что нужно знать о установке подробно расписано в README. Единственное для mediaproxy почему-то нет init файлов, но это не беда, их легко написать самостоятельно.

Конфигурирование openser

Перейдем к основному, как настроить kamailio. Где-то я возможно повторюсь с статьей о openser с voip.rus.net, но там описан процесс настройки применительно к старым версиям Openser, в новых версиях kamailio многое изменилось и процесс настройки слегка изменился, особенно это касается работы c NAT. Конфигурационный файл я аттачить не буду, а буду приводить куски в статье, если эти куски собрать во едино, то получится нужный конфигурационный файл. Делаю это я осознанно, чтобы не возникало желания пролистать статью, ибо "много букв" J залить конфигурационный файл себе и получить все косяки из-за слабого представления что где зачем.

Я буду описывать не каждую строчку, т.к. предназначение некоторых банально, а некоторые я и сам не знаю зачем нужны, но в мануалах пишут что они нужны :)

Настоятельно рекомендую тем кто не знаком с процедурой установления соединения SIP протокола прочитать соответствующие мануалы. Не повредит знать структуру SIP пакетов т.к. kamailio для маршрутизации звонков использует те или иные поля SIP пакета.

Пожалуй начнем.

     debug=3
     log_stderror=no

отправлять или нет log сообщения в stdout, если стоит нет, есть возможность отправлять лог сообщения на syslog

     fork=yes

директива fork заставляет kamailio работать в режиме демона, иначе все сообщения будут попадать в stdout

     children=8
     disable_dns_blacklist=yes

disable_dns_blacklist=yes без этой опции kamailio может коряво работать с серверами на которые будем маршрутизировать сообщения

     auto_aliases=no
     port=5060
     listen=udp:192.168.0.1:5060

listen - понятен наверное для чего, параметров listen может быть несколько, но надо быть осторожным с ip маршрутами, чтобы сообщение поступив через один интерфейс не уходило обратно через другой, если конечно именно не это требуется :)

     alias=voip.telecom.ru:5060
     alias= sip.telecom.ru:5060
     alias=192.168.0.1:5060

список alias-ов, синонимом которых является сервер

     mpath="/usr/local/lib/kamailio/modules/"

Путь до папки с модулями kamailio

     loadmodule "pv.so"
     loadmodule "db_mysql.so"
         modparam("db_mysql", "auto_reconnect", 1)

загружаем модуль работы с mysql и включаем авто реконнект.

     loadmodule "sl.so"
     loadmodule "tm.so"
         modparam("tm", "fr_timer", 10)
         modparam("tm", "fr_inv_timer", 120)
         modparam("tm", "wt_timer", 5)
         modparam("tm", "delete_timer", 2)
         modparam("tm", "ruri_matching", 1)
         modparam("tm", "via1_matching", 1)
         modparam("tm", "unix_tx_timeout", 2)
         modparam("tm", "restart_fr_on_each_reply", 1)
         modparam("tm", "pass_provisional_replies", 0)
     loadmodule "rr.so"
         modparam("rr", "enable_full_lr", 1)
         modparam("rr", "append_fromtag", 0)
     loadmodule "maxfwd.so"
         modparam("maxfwd", "max_limit", 256)
     loadmodule "usrloc.so"
         modparam("usrloc", "db_mode",1)
         modparam("usrloc", "timer_interval",120)
         modparam("usrloc", "db_url", "mysql://openser:openserrw@localhost/openser")
         modparam("usrloc", "nat_bflag", 6)
         modparam("usrloc", "expires_column", "expires")

задаем параметры базы данных, куда конектится, какую базу использовать, выбираем для флага сигнализирующего принадлежность клиента к клиентам за натом bflag 6, подробнее о флагах можно почитать на сайте kamailio

     loadmodule "registrar.so"
         modparam("registrar", "method_filtering", 1)
         modparam("registrar", "max_expires", 50)
         modparam("registrar", "default_q", 0)
         modparam("registrar", "append_branches", 1)
         modparam("registrar", "case_sensitive", 0)
         modparam("registrar", "max_contacts", 0)
         modparam("registrar", "retry_after", 0)
         modparam("registrar", "method_filtering", 0)
       

модуль отвечающий за обработку сообщений Register, у меня выставлен таймер максимального времени перерегистрации в 50 секунд, это связано с тем что мы используем у себя переделанный клиент QuteCom?, который если регается реже начинает терять регистрацию и валится в корку. max_expires заставляет всех клиентов регаться не реже чем раз в 50 секунд.

     loadmodule "textops.so"
     loadmodule "xlog.so"
     loadmodule "mi_fifo.so"
         modparam("mi_fifo", "fifo_name", "/tmp/kamailio_fifo")
         modparam("mi_fifo", "fifo_mode", 0660)
         modparam("mi_fifo", "fifo_group", "openser")
         modparam("mi_fifo", "fifo_user", "openser")
         modparam("mi_fifo", "reply_dir", "/tmp/")
         modparam("mi_fifo", "reply_indent", "\t")

/tmp/kamailio_fifo - это сокет для управления и мониторинга kamailio, управляет и мониторит приложении kamctl

     loadmodule "uri_db.so"
         modparam("uri_db", "use_uri_table", 0)
         modparam("uri_db", "db_url", "")
     loadmodule "siputils.so"
     loadmodule "nathelper.so"
         modparam("nathelper", "rtpproxy_disable", 1)
         modparam("nathelper", "natping_interval", 10)
         modparam("nathelper", "received_avp", "$avp(i:42)")

Т.к. будем использовать mediaproxy, то выключаем поддержку rtpproxy, задаем интервал <<пингания>> UA, чтобы роутеры держали открытыми порты.

     loadmodule "avpops.so"
     loadmodule "auth.so"
     loadmodule "auth_db.so"
     loadmodule "dispatcher.so"
         modparam("dispatcher", "flags", 2 )
         modparam("dispatcher", "list_file", "/usr/local/etc/kamailio/dispatcher.list")
         modparam("dispatcher", "dst_avp", "$avp(i:271)")
         modparam("dispatcher", "grp_avp", "$avp(i:272)")
         modparam("dispatcher", "cnt_avp", "$avp(i:273)")

модуль dispatcher служит для реализации load balancing-а, в list file хранятся адреса различных серверов на которые мы будем распределять вызовы.

     loadmodule "auth_radius.so"
         modparam("auth_radius", "radius_config","/etc/radiusclient-ng/radiusclient.conf")
         modparam("auth_radius", "service_type",1)
         modparam("auth_radius", "use_ruri_flag", 22)

включаем поддержку radius, указываем путь до конфига радиус сервера

     loadmodule "mediaproxy.so"
         modparam("mediaproxy","mediaproxy_socket", "/var/run/mediaproxy/dispatcher.sock")

включаем работу с mediaproxy, задаем путь до сокета media-dispatcher

     loadmodule "domain.so"
         modparam("domain", "db_url", "mysql://openser:openserrw@localhost/openser")
         modparam("domain", "db_mode", 1)
         modparam("domain", "domain_table", "domain")
         modparam("domain", "domain_col", "domain")
     loadmodule "presence.so"
         modparam("presence", "db_url", "mysql://openser:openserrw@localhost/openser")
         modparam("presence", "max_expires", 3600)
         modparam("presence", "server_address", "sip:sippalanser.is74.ru:5060")
     loadmodule "dialog.so"
         modparam("dialog", "dlg_flag", 4)
     loadmodule "nat_traversal.so"
         modparam("nat_traversal", "keepalive_interval", 90)
         modparam("nat_traversal", "keepalive_method", "OPTIONS")

Kamailio читает конфигурационный файл от начала до конца, соответственно диалплан исполняется как обычная программа. Существует несколько разновидностей блоков маршрутизации, которые зовутся: route - основной блок маршрутизации, route[x] - что-то типа процедур, только без параметров, все параметры передаются с помощью флагов и переменных, t_on_reply - блок для обработки различных ответов, failure_route - для обработки ошибок

     route
     {

Основной блок маршрутизации, по аналогии с С/C++ main() {}

     if (method=="OPTIONS")
         {
             exit;
         };
         if (method=="PUBLISH")
         {
             exit;
         };
         if (method=="SUBSCRIBE")
         {
             exit;
         };

Я не использую у себя эти сообщения пока, чтобы не мешались оправляю их в dev/null :)

     if (!mf_process_maxfwd_header("10"))
         {
             sl_send_reply("483","Too Many Hops");
             exit;
         };
         if (msg:len > max_len )
             {
             sl_send_reply("513", "Message Overflow");
             exit;
         };
       

Небольшая защита от больших пакетов и зацикленных вызовов.

     # -----------------------------------------------------------------
     # Record Route Section
     # -----------------------------------------------------------------
     if (method=="INVITE" && nat_uac_test("2"))
     {
             xlog("L_INFO", "record route section | INVITE & nat test: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
             record_route_preset("192.168.0.1:5060;nat=yes");
     }

Если получено сообшение INVITE и процедура проверки ната вернула нам истину, то явно указываем заголовочное поле Record-Route. (nat_uac_test может по разному определять ваш нат, для этого у него есть несколько методов определения ната, советую почитать документацию и опытно теоретическим путем выяснить какой аргумент этой функции вам подойдет).

Функция xlog отсылает указанное сообщение в аргументе на syslog или на stdout

     else if (method!="REGISTER")
     {
             record_route();
     }

Если от SIP клиента, находящегося за маршрутизатором с NAT, получено сообщение не INVITE и не REGISTER типа, то только тогда мы вызываем функцию record_route(), для гарантии того, чтобы сообщения проходили через наш SIP прокси сервер с вышестоящих и нижестоящих SIP прокси серверов или со шлюзов в публичную телефонную сеть (PSTN).

     if (method=="BYE" || method=="CANCEL")
     {
             xlog("L_INFO", "Call Tear Down | end_media_session: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
             end_media_session();
     };

Отмена или конец вызова, вызывается end_media_session, это фунцкия модуля mediaproxy, даже если у нас не был mediaproxy задействован для установления вызова совершенно безопасно на всякий случай вызывать эту функцию чтобы наверняка разорвать соединение.

     # -----------------------------------------------------------------
     # Loose Route Section
     #Секция свободной маршрутизации
     # -----------------------------------------------------------------

Эта секция включается когда сообщение адресовано не нашему серверу, а проходит через него транзитом

     if (loose_route())
     {
     xlog("L_INFO", "loose route\n");
             if ((method=="INVITE" || method=="REFER") && !has_totag())
             {
                 sl_send_reply("403", "Forbidden");
                 return;
             };

Мы должны особым образом обрабатывать сообщения Re-Invite, дабы предотвратить разрыв потоков по RTP протоколу, во время обработки этих сообщений. Таким образом, в этом месте мы отдельно обрабатываем эти сообщения для клиентов, находящихся за NAT.

Для гарантии того, что мы получили действительно повторное сообщение INVITE (re-INVITE), мы должны убедиться, что функция has_totag() и loose_route() вернула TRUE. Причина в том, что возможно оригинальное сообщение INVITE содержит предопределенные заголовочные поля для маршрутов, что заставило бы loose_route() вернуть TRUE. Поэтому производим проверку функцией has_totag(), т.к. только для уже установленных соединений будет содержаться флаг "tag=" в заголовочном поле <To> (т.е., только для вызовов, где, вызываемым абонентом, был подтвержден запрос на установку соединения сообщением с кодом "200 OK").

Другими словами, эта новая проверка безопасности основывается на том факте, что уже установленные SIP вызовы будут содержать "totag", тогда как еще не установленные - его не содержат. Для гарантии того, что наша логика "свободной маршрутизации" не будет использована в злонамеренных целях, мы проверяем тот факт, что эти сообщения INVITE и REFER, приняты в рамках уже установленного соединения.

             if (method=="INVITE")
             {
                 if (nat_uac_test("2") || search("^Route:.*;nat=yes"))
                 {

Теперь мы проверяем NAT статус отправителя сообщения re-INVITE, вызывая функцию nat_uac_test("3"). Также ищем заголовочное поле <Route>, содержащий тег ";nat=yes", который будет вставлен ранее, обсуждаемой ранее, функцией record_route_preset(). Если найден тэг ";nat=yes", тогда вызывающий абонент находиться за маршрутизатором с NAT.

                     setbflag(6);

Если отправитель сообщения находиться за маршрутизатором с NAT или INVITE сообщение содержит флаг "nat=yes", тогда мы устанавливаем флаг 6 для использование его в дальнейшем.

                     use_media_proxy();

Для начала проксирования RTP потоков, мы вызываем функцию use_media_proxy(). Она будет общаться с внешним mediaproxy сервером, заставляя это открыть UDP порты для обоих клиентов, или управлять существующим сеансом RTP проксирования для уже установленного вызова, в зависимости от заголовочного поля <Call-ID>. Вызов use_media_proxy() вызывает перезапись содержимого SDP, в части IP адреса и портов, которые выделил mediaproxy сервер для RTP потоков

                 }
                 else
                 {
                     xlog("L_INFO", "loose route |  Re-INVITE from NO NAT: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
                     route(4);
                 }
             }
             route(1);
             exit;
         }

Если NAT -а нет, то просто отправляем звонок на route[4] для проверки находится ли абонент за NAT и на route[1] для доставки сообщения по назначению, что будет происходить там, мы выясним чуть позже.

     # -----------------------------------------------------------------
     # Call Type Processing Section
     # Секция, обрабатывающая различные типы вызовов.
     # -----------------------------------------------------------------
     if (uri!=myself)
     {
             xlog("L_INFO", "MESSAGE NOT MYSELF: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
             route(4);
             route(1);
             exit;
     };

Если сообщение больше не нуждается в обработке нашим SIP серверов, то отправляем его на route[4] route[1]

     if (method=="ACK")
     {
             xlog("L_INFO", "route ACK detect: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
             route(1);
             exit;
     }

Получено сообщение ACK, отдельное правило создано для нужд отладки, такие сообщение мы должны сразу на route[1] отдать.

     else if (method=="CANCEL")
     {
             xlog("L_INFO", "route CANCEL detect: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
             end_media_session();
             route(1);
             exit;
     }

Получено команда отмены вызова, вызываем end_media_session, чтобы наверняка закрыть UDP порты на mediaproxy, даже если UA не за NAT и mediaproxy не использовался, совершенно безопасно вызвать эту команду и отправляем Cancel по назначению чтобы другой UA тоже закончил процедуру установления сессии.

     else if (method=="INVITE")
     {
              route(3);
              exit;
     }

Если получено сообщение INVITE то отдаем его сразу на route[3], там содержится вся логика установления соединения.

     else if (method=="REGISTER")
     {
             xlog("L_INFO", "route REGISTER detect: M=$rm RURI=$ru F=$fu T=$tu IP=$si $mb\n");
             route(2);
             exit;
     }

Если получено сообщение REGISTER то будем его обрабатывать в route[2]

     route(1);
       

все остальные сообщения отправляем по назначению.

     # -----------------------------------------------------------------
     # Default Message Handler
     # -----------------------------------------------------------------
     route[1]
     {
         xlog("L_INFO", "route[1] default handler: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
         t_on_reply("1");

При работе с UA находящимися за NAT мы должны корректно обрабатывать сообщения возвращающиеся к UA, к этим сообщениям можно получить доступ через блок reply_route

         if (!t_relay())

вызываем функцию t_relay, это statefull функция, т.е. с сохранением состояния транзакции. Т.е. если после начала транзакции Invite сообщением отправить ACK или BYE то это сообщение будет отправлено именно тому UA который это сообщение ждет.

         {
             xlog("L_INFO", "route[1] | ERROR: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
             if (method=="INVITE" || method=="ACK")
             {
                 xlog("L_INFO", "route[1] | INVITE or ACK error: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
                 xlog("L_INFO", "route[1] | end_media_session: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
                 end_media_session();
             }
             sl_reply_error();

Ошибка доставки сообщения

         }
     }
     # ------------------------------------------------------------------------
     # Обработка REGISTER
     # ------------------------------------------------------------------------
     route[2]
     {
         sl_send_reply("100", "Trying");
         xlog("L_INFO", "route[2] REGISTER Message Handler M=$rm RURI=$ru F=$fu T=$tu IP=$si");
         if (!search("^Contact:[ ]*\*") && nat_uac_test("2"))
         {
             setbflag(6);
             fix_nated_register();
       

Fix_nated_register() специально используется для обработки сообщений REGISTER от клиентов, находящихся за NAT

             force_rport();

функция Force_rport () добавляет полученный IP порт в самое начало заголовочных полей "via" SIP сообщения. Это дает возможность направлять последующие SIP сообщения на нужный порт для последующих SIP транзакций.

         fix_contact();

переписываем IP и порт в заловке Contact, чтобы в таблице зарегистрированных UA был не локальный адрес за натом, а адрес ната и порт через который можно достучаться до UA

         }

Проверяем UA от которого пришло сообщение, за NAT он или нет, если проверка истина то выставляем bflag 6, этот флаг будет сохранен в таблице location, kamailio всегда будет знать какой UA за Nat или нет, чтобы иметь возможность задействовать mediaproxy для установления RTP сессии

         if(is_method("REGISTER") && is_present_hf("Expires") && $(hdr(Expires){s.int})==0)
         {
             xlog("L_INFO", "UNREGISTER: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
         }

Если поле Expires=0 значит UA отрегивается, не будем уточнять его параметры авторизации.

         else
         {
             if(!radius_www_authorize(""))
             {
                 xlog("L_INFO", "radius_www_authorize() error M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
                 www_challenge("","1");
                 exit;
             }
             else
             {
                  if (!check_to())
                 {
                     sl_send_reply("401", "Unauthorized");
                     exit;
                     }
               consume_credentials();
             }
         };

В противном случае авторизуем его. У нас авторизация реализована через Radius, только не спрашивайте как настраивать Radius сервер какие атрибуты надо править. У нас отдельный человек занимается Radius-ом, и настройка всего этого его рук дело. Наш Radius сервер претерпел множественные изменения исходников для нужд нашей компании, поэтому его конфиги вам совершенно не будут интересны. В книге про openser есть пример как использовать аккаунты в базе для регистрации, на сайте есть туториал по работе с radius в нете полно примеров разных конфигураций, так что в этом у вас есть полная свобода.

         if (!save("location"))
         {
             sl_reply_error();
         }
     }
       

Сохраняем информацию о UA в базе kamailio, в таблице locations.

Секция обработки INVITE

     route[3]
     {
         xlog("L_INFO", "route[3] invite handler: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
        if (nat_uac_test("2"))
        {
              setbflag(7);

здесь мы выставляем bflag 7, т.е. когда исходящий вызов сигнализатором NAT-а будет флаг 7, а при входящем вызове на абонента за NAT-ом флаг 6

             force_rport();
             fix_contact();
        }

Если INVITE от клиента за NAT делаем с ним то же что сделали до этого в route[2] Здесь было бы неплохо сделать авторизацию INVITE, дабы кто попало не позвонил и не поговорил на халяву, если у вас конечно этот сервер будет обслуживать клиентов, которым надо насчитать денег. Но на практике kamailio не до конца справляется с авторизацией INVITE. 20-30 % звонков завершаются аварийно, т.к. kamailio решает что авторизация не пройдена. Товарищ tma c форума asterisk-support.ru пишет Проблема в том, что INVITE может придти без необходимых для Digest авторизации данных, в результате биллинг/radius "отшивает" запрос, а Kamailio/SER/OpenSER, как следствие, его рвет."

На практике авторизация работает примерно так, что для REGISTER что для INVITE запросов. Приходит первоначальный запрос, если в нем нет необходимой Digest информации для авторизации запроса, то сервер должен вернуть false, потом он отправляет 401 Unauthorized, на что клиент должен попробовать еще раз отправить этот запрос но уже с необходимыми Digest данными, для запросов REGISTER это прокатывает, а вот для INVITE-ов почему-то не всегда, сложный вопрос почему это так, я не знаю причин и пока решил вызовы не проверять, есть идеи как такую проверку реализовать, но это уже отдельная тема

Lookup("location") заставляет сервер проверить есть ли UA которому хочет позвонить клиент отправивший INVITE в списке зареганных, если он есть то функция возвращает его реквизиты, а также значение bflag 6, установлен или нет

         if (!lookup("location"))
         {

UA не найден, значит либо он не зареган либо звонят не UA находящемуся на этом сервере

             xlog("L_INFO", "route[3] | not local client: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
             route(5);
             exit;

если нужного номера нет на этом сервере, то поищем его на asterisk сервере, route[5] отвечает за перенаправление вызова на asterisk

          }
         else
         {
             xlog("L_INFO", "route[3] | local client: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
         }

клиент найден на сервер, отправляем его на route[4], который в случае необходимости включит mediaproxy и собственно по назначению на route[1]

         route(4);
         route(1);
     }

Включаем mediaproxy

     route[4]
     {
         #xlog("L_INFO", "route[4] nat traversal: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
         if (isbflagset(6) || isbflagset(7))
         {

Проверяем, если установлен bflag 6, то вызов на UA который за NAT, если установлен bflag 7, то вызов от UA который за NAT

             if (!isbflagset(8))
             {
                 setbflag(8);
                 xlog("L_INFO", "route[4] | use_media_proxy: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
                 use_media_proxy();
             }

Включаем mediaproxy, простенькая защита от многократного включения mediaproxy для одного вызова, реализована посредством конструкции c bflag 8

         }
     }

Вот собственно секция для выбора нужной ноды asterisk, работает модуль dispatcher

     route[5]
     {
          t_on_reply("2");
          t_on_failure("1");

обработка ошибок и сообщений

         xlog("L_INFO", "route[5]->asterisk node: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
         ds_select_domain("1","4");

выбираем группу серверов 1 и механизм распределения вызовов 4 - round robin

         route(4);
         route(1);

отправляем вызов куда следует

     }

В файле dispatcher.lis который мы указали здесь modparam("dispatcher", "list_file", "/usr/local/etc/kamailio/dispatcher.list") мы прописываем необходимые сервера куда будем балансировать нагрузку.

     1 sip:192.168.0.1:5060
     1 sip:192.168.0.2:5060

Вот на эти два сервера и пойдут запросы

     2 sip:192.168.0.3:5060
     2 sip:192.168.0.4:5060

А сюда бы они пошли если бы мы указали в конфиге kamailio

        ds_select_domain("2","4");

Обработка ошибок и сообщений.

     onreply_route[1]
     {
          if ((isbflagset(6) || isbflagset(7)) && (status=~"(180)|(183)|2[0-9][0-9]")
     )
         {
             if (!search("^Content-Length:[ ]*0"))
             {
                 use_media_proxy();
             };
             if (nat_uac_test("2"))
             {
                 fix_contact();
             };
         };
     }
     onreply_route[2]
     {
         if (status=~"[12][0-9][0-9]")
         {
             fix_nated_contact();
             exit;
         };
     }
     failure_route[1]
     {
         if( t_check_status("408") )
         {
             xlog( "L_NOTICE", "[$Tf] FR: $ci -- TIMEOUT for Gateway $rd\n" );
         }
         else
         {
             xlog( "L_NOTICE", "[$Tf] FR: $ci -- $rs reason $rr\n" );
         };
         if( t_check_status("403") )
         {
             xlog("L_NOTICE", "[$Tf] FR: $ci -- SIP-$rs Forbidden -> ISDN Cause Code1\n" );
             return;
         };
         if( t_check_status("486") )
         {
             xlog("L_NOTICE", "[$Tf] FR: $ci -- SIP-$rs Destination BUSY \n" );
             return;
         };
         if( t_check_status("487") )
         {
             xlog("L_NOTICE", "[$Tf] FR: $ci -- SIP-$rs Request Cancelled\n" );
             return;
         };
         if( ds_next_domain() )
         {
             t_on_reply("2");
             xlog( "L_NOTICE", "[$Tf] FR: $ci Next gateway $fU -> $tU via $rd\n" );
             if( !t_relay() )
             {
                 xlog( "L_INFO", "[$Tf] FR: $ci -- ERROR - Can not t_relay()\n" );
                 return;
             };
             return;
         }
         else
         {
                  xlog( "L_INFO", "[$Tf] FR: $ci No more buscuits in the gateways" );
             t_reply("503", "Service unavailable -- no more gateways" );
             exit;
         };
         xlog("L_INFO", "failure_route[1]->: M=$rm RURI=$ru F=$fu T=$tu IP=$si\n");
     }

Вот в принципе минимальный конфиг для того чтобы ваш kamailio стал полноценным load балансером. Но это лишь мизер всех его возможностей.

Получите бесплатную консультацию

Мы очень любим общаться и не жалеем на это времени.
Напишите нам - задайте интересующий Вас вопрос, поделитесь идеей.
Мы постараемся ответить Вам как можно быстрее.
Каждое сообщение директор читает лично.