Ключевые слова:sip, voip, asterisk, openser, balance, cluster, kamailio, (найти похожие документы)
From: Сергей Лагута
Date: Mon, 21 Feb 2010 17:02:14 +0000 (UTC)
Subject: Создание отказоустойчивого сервера Asterisk с поддержкой балансировкой нагрузки
Оригинал: http://asteriskpbx.ru/blog/2009/08/17http://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 балансером. Но это лишь мизер всех его возможностей.
Данная схема с изменениями работает все еще.
Сейчас 3000 постоянно зарегистрированных абонентов. Прирост примерно по 1000 в год. Схема нуждается в обновлении, планируется кластеризация kamailio части.
Статья тоже нуждается в обновлении, подумываю написать ее на habrahabr.
Также моя компания оказывает услуги по настройке подобных схем для коммерческого использования, на данный момент схема запущена у еще двух VoIP операторов.
Я правильно понимаю что первый пункт -
1. Безотказную систему, желательно с полным резервирование и без единой точки отказа
У Вас все таки не реализован, т.к. sipbalancer один единственный, и никаких ha media proxy также не реализовано?
Вы правильно понимаете. kamailio можно резервировать разными способами.
например dns srv, правда не всегда удобно, т.к. у разных клиентов есть лаг в обновлении srv записей. я склонен очень трепетно относиться к подобным проблемам у клиентов и хотелось бы чтобы у них заработало быстро.
Сейчас разрабатывается вариант переезда IP адреса на другой сервер, но не средствами ha
Не описан остался момент, каким образом оба астериска (или того более - все четыре), видят у себя зарегистрированными всех пользователей kamailio одновременно? И если пользователь А зареган на Астериск2, и он звонит пользователю Б, который зареган на Астериск2, а оба они зареганы на всех астерисках, то не получается ли петли, когда вызов с А на Б проходит через kamailio? Очевидно, что нет, если схема работает - но мне непонятно как, она работает) Я только собираю информацию, чтобы понять и сделать подобное.
> Не описан остался момент, каким образом оба астериска (или того более -
> все четыре), видят у себя зарегистрированными всех пользователей kamailio одновременно?
> И если пользователь А зареган на Астериск2, и он звонит пользователю
> Б, который зареган на Астериск2, а оба они зареганы на всех
> астерисках, то не получается ли петли, когда вызов с А на
> Б проходит через kamailio? Очевидно, что нет, если схема работает -
> но мне непонятно как, она работает) Я только собираю информацию, чтобы
> понять и сделать подобное.
REGISTER сообщения до asterisk не доходят, asterisk отправляет звонки на kamailio, kamailio уже разруливает их зареганным на нем пользователям
У меня вопрос про балансировку и сбор статистики с очередей.
Каким образом происходит синхронизация очередей разных Asteriskов? Ведь в одну очередь направляется поток звонков и как вы его балансируете между несколькими Asterisk, ведь статистика будет не правильной?
Пытаюсь понять, чем подобные схемы будут отличаться от балансера на yate/fs/asterisk с включённым SDP forward и перебором нижестоящих станций по round-robin? В яте вообще конфиг будет занимать ровно две строки, sdp_forward=yes и fork станция1 | станция2