Содержание статьи
SIP: краткое введение
Прежде всего стоит разобраться, как работает SIP в принципе и что это вообще за протокол. Итак, SIP — Session Initiation Protocol, служащий исключительно для целей установления и завершения сессии. Обмен пакетами от установления соединения до его завершения называется диалогом. Медиапоток по данному протоколу не передается — для этого есть протокол RTP. Инкапсулированный же в пакете SIP SDP (Session Description Protocol) определяет, в частности, и параметры медиапотока, такие как используемый кодек и тип данных.
Важно понимать, что прохождение этих двух потоков — SIP/SDP и RTP (или, как говорят связисты, сигнализации и данных) — совершенно независимо друг от друга. Посмотрим, что происходит при нормальном вызове (очень упрощенно, правда).
- Вызывающая сторона инициирует соединение, посылая SIP-пакет INVITE, в нем же инкапсулирован и SDP с информацией о желаемых параметрах RTP. Этот пакет может пройти через несколько SIP-прокси — о них чуть ниже.
- В зависимости от того, проходит ли вызов через SIP-прокси или нет, вызывающая сторона получает либо ответ от SIP-прокси TRYING (а уж прокси тем временем направляет INVITE вызываемой стороне) и следом RINGING, либо сразу RINGING. Пакет RINGING означает, что вызываемая сторона посылает сигнал абоненту «звоню» и теперь нужно ожидать, условно говоря, «поднятия трубки».
- Как только вызываемая сторона поднимает трубку, она посылает SIP-пакет с кодом 200 «OK». В этот пакет снова инкапсулирована нагрузка SDP, содержащая параметры, которые поддерживаются этой стороной. Вызывающая же сторона, если параметры совпадают, посылает ответный пакет ACK.
- Клиенты соединяются по протоколу RTP напрямую независимо от того, проходил ли предыдущий обмен пакетами SIP через SIP-прокси или нет. Происходит разговор.
- При отбое отбивающая сторона посылает SIP-пакет BYE, а вторая сторона посылает опять же пакет ACK. Соединение завершено.
Теперь стоит разобраться с устройством сети SIP — какие логические сущности в ней вообще имеются.
- UAC и UAS — User Agent Client и User Agent Server. Очень условно их можно назвать вызывающей и вызываемой стороной. В SIP-терминалах и программных клиентах реализуется как UAC, так и UAS.
- SIP-прокси занимается в основном маршрутизацией.
- SIP-регистратор сохраняет информацию о логическом местоположении UAC/UAS.
- SIP-редиректор предоставляет UAC/UAS информацию о маршрутизации и в случае необходимости перенаправляет вызов в другое место.
- Сервер местоположения на самом деле представляет собой базу данных, где указано, какой адрес или какие адреса соответствуют тому или иному SIP URI. Фактически это лишь удобная абстракция.
- B2BUA может находиться на месте SIP-прокси, но им не является. В случае с прокси терминал устанавливает соединение с другим терминалом напрямую (хоть оно и проходит через прокси, но лишь до определенного этапа. И прокси в общем случае не изменяет заголовки и тело SIP-пакета). В случае с B2BUA терминал сначала устанавливает с ним соединение, затем B2BUA устанавливает соединение с вызываемым терминалом, и в дальнейшем весь процесс происходит исключительно через него, с использованием двух независимых сессий — вызывающий терминал-B2BUA и B2BUA-вызывающий терминал. Применение B2BUA дает гораздо большую гибкость, нежели применение обычного SIP-прокси, — хотя бы потому, что он позволяет изменять тело SIP-пакета. Кроме того, по названным выше причинам его применяют и при тарификации вызовов. Но есть у него и недостаток — большее потребление памяти.
Стоит отметить, что зачастую логические сущности объединяются в одном ПО. Так, SIP-прокси чаще всего объединяется с регистратором и редиректором.
SIP-прокси существуют двух типов — stateless и stateful. Если ты знаком с iptables, думаю, объяснять их различия нет необходимости. А если нет... Stateful SIP-прокси хранит состояние транзакции, то есть знает, какому пакету INVITE соответствует тот или иной пакет ACK. Stateless-прокси же ничего этого не знает и тупо передает пакет куда следует.
И тут мы плавно переходим к вопросу, какие еще свободные средства для работы с SIP-протоколом (помимо Asterisk, о котором знают все) существуют в *nix-системах. На данный момент есть два проекта, предназначенные для этой цели, — Kamailio и OpenSIPS. Они имеют общего предка, так что подробно описывать их различия смысла нет. В статье будет рассмотрен OpenSIPS.
SEMS
OpenSIPS, в отличие от Asterisk, не поддерживает некоторые виды услуг — он занимается исключительно протоколом SIP. Для того же, чтобы на базе OpenSIPS собрать сервер конференций или, скажем, голосовую почту, потребуется использовать медиасервер, такой, например, как SEMS.
SEMS разработан теми же людьми, что делали SER («дедушку» OpenSIPS), и имеет следующие особенности:
- поддерживает создание приложений («железных теток») и конференций;
- может выступать в качестве B2BUA/SBC (OpenSIPS это тоже умеет, но SEMS более заточен под подобные вещи);
- крайне масштабируемый.
Если понадобится замена Asterisk не только как SIP-прокси, но и как медиасервера, SEMS выглядит неплохим вариантом.
Что такое OpenSIPS? Установка
OpenSIPS обеспечивает функциональность практически всех серверных компонентов, которые были упомянуты выше, — от SIP-прокси до B2BUA. Отличие же его от Asterisk’а заключается, во‑первых, в том, что OpenSIPS манипулирует пакетами и сессиями SIP на более низком уровне, чем это делает Asterisk, а во‑вторых, Asterisk по сути является комбайном, например содержит в себе, помимо SIP, медиасервер. Кроме того, Asterisk не поддерживает масштабирование.
OpenSIPS имеет давнюю историю: как минимум он (если считать и его предшественников) не младше Asterisk, а с учетом того, что SIP для последнего неродной, возможно, даже и старше. Он также поддерживает и модульную архитектуру, при этом модулей там предостаточно. В настоящий момент с нуля разрабатывается ветка 2.0, которая будет иметь другую архитектуру, — текущая закладывалась без учета части современных реалий, которые в те времена еще и не прогнозировались. Однако данная ветка находится в состоянии активной разработки, а я предпочитаю стабильные версии, поэтому здесь будет описана версия 1.11.3, которая, кстати, является LTS.
Итак, поехали устанавливать:
$ wget -qO - http://apt.opensips.org/key.asc | sudo apt-key add -
$ sudo sh -c "echo 'deb http://apt.opensips.org/ stable111 main' > /etc/apt/sources.list.d/opensips.list"$ sudo apt-get update
$ sudo apt-get install opensips opensips-console
Начальная настройка OpenSIPS
В файле /
нужно настроить имя или адрес SIP-домена:
SIP_DOMAIN=192.168.56.103
Имя SIP-домена не обязательно должно совпадать с DNS-именем хоста, на котором запущен OpenSIPS, однако некоторые SIP-терминалы не позволяют указывать его отдельно. В тестовом случае это не настолько критично, но, если сервер будет находиться в продакшене, этот момент лучше учесть. Также для нормальной работы все равно рекомендуется DNS-сервер со сконфигурированными полями NAPTR и SRV. В общем‑то, базовая настройка на этом закончена, пора переходить к написанию скрипта маршрутизации.
Скрипт маршрутизации
Основной конфигурационный файл OpenSIPS (в моем случае он был расположен по пути /etc/opensips/opensips.cfg) делится на три части:
- Глобальные параметры, такие как логирование с отладкой, адреса, на которых OpenSIPS слушает, настройка количества дочерних процессов.
- Параметры модулей. Сюда относятся путь к каталогу с модулями, список самих загружаемых модулей и их параметры, например для модуля транзакций tm предусмотрена настройка самых разнообразных таймеров.
- Наконец, логика маршрутизации. Именно здесь находится самая вкусная часть OpenSIPS. Каждый SIP-пакет, приходящий на (и проходящий через) OpenSIPS, обрабатывается в соответствии с данной логикой. Разумеется, именно поэтому она может быть достаточно сложной. Как правило, здесь имеются несколько маршрутов — основной, аналогичный точке входа в какую‑либо программу и несколько дополнительных, направление на которых вызывается из основного. Дополнительные маршруты аналогичны функциям в процедурных языках программирования.
В принципе, для создания базового конфигурационного файла можно использовать команду osipsconfig, но его, тем не менее, придется править ручками. Ниже будут приведены наиболее важные части из файла конфигурации с комментариями (к слову, допустимы как однострочные комментарии в *nix-стиле, так и многострочные в стиле C++). Итак, часть секции глобальных параметров:
# Указываем, на каком адресе и порте будет слушать OpenSIPSlisten=udp:192.168.56.103:5060
# Отключаем поддержку TCP и TLSdisable_tcp=yesdisable_tls=yes
А теперь посмотрим некоторые параметры модулей:
# Путь к модулямmpath="/usr/lib/opensips/modules"# Загружаем модули sl и tmloadmodule "sl.so"loadmodule "tm.so"# Настраиваем параметры модуля tmmodparam("tm", "fr_timeout", 5)
modparam("tm", "fr_inv_timeout", 30)
modparam("tm", "restart_fr_on_each_reply", 0)
modparam("tm", "onreply_avp_mode", 1)
# Загружаем модуль-обертку SIGNALINGloadmodule "signaling.so"<...>
Пожалуй, стоит обратить внимание на данные модули. Первым загружается модуль sl, который отвечает за stateless-прокси. Параметры его мы не трогаем. Следующая строчка загружает модуль tm, который отвечает за stateful-прокси, — и уж тут как раз некоторые параметры нужно изменить. Рассмотрим, что они означают:
- fr_timeeout — интервал между запросом и предварительным ответом (таким как trying). Транзакция на момент предварительного ответа еще не завершена. Этот тайм‑аут истекает, если нет никакого ответа, в случае же наличия предварительного ответа он сбрасывается. В секундах.
- fr_inv_timeout — интервал между предварительным и окончательным ответом. Тайм‑аут истекает, если с момента получения предварительного ответа окончательного так и не пришло. Опять же в секундах.
- restart_fr_on_each_reply — указывает, должен ли fr_timeout начинать отсчет сначала, если вызываемая сторона регулярно шлет предварительные ответы. Нулевое значение — false, любое другое — true.
- onreply_avp_mode — как будут обрабатываться AVP (Atribute-Value pair) в маршруте Reply. Если не вдаваться в подробности, 1 означает, что AVP, относящиеся к данному маршруту, можно будет видеть и использовать и в некоторых других ветвях скрипта маршрутизации. Это, тем не менее, снижает производительность.
Модуль SIGNALING представляет собой обертку вокруг модулей tm и sl для удобства работы.
Теперь наконец можно перейти к разбору структуры последней секции, которая, собственно, и является скриптом обработки запросов. Первым идет так называемый main route block, в который попадают все пакеты. Как уже говорилось выше, этот блок аналогичен точке входа в стандартные программы. Рассмотрим, что он делает:
- Пакет входит в данный блок, и происходят некоторые проверки.
- Если пакет не предназначен нам, мы его направляем в блок relay.
- А если же он пришел к нам, мы, в зависимости от нужд, совершаем какие‑то действия. Отмечу, что здесь можно направлять поток обработки в другие блоки, аналогично тому, как в программе на си из функции main() можно вызывать другие функции, описанные в коде программы.
Кроме блока типа route (к которому относится и названный выше и который обрабатывает входящие запросы), существует еще несколько типов блоков маршрутизации. Опишу некоторые из них:
- branch_route — позволяет задавать действия в пределах одной транзакции. Понятно, что это актуально лишь в случае stateful-маршрутизации.
- failure_route — обрабатывает полученные отрицательные (с кодом большим или равным 300) ответы — причем ответы как приходящие извне, так и генерируемые самим OpenSIPS. Опять же актуально только для stateful-маршрутизации.
- onreply_route — обрабатывает все нормальные ответы. Может быть как stateful — в случае если мы специально указываем, что ответ принадлежит какой‑либо транзакции, так и stateless — если указываем, что маршрутизация глобальная.
- error_route — обрабатывает ошибки при парсинге SIP-пакетов.
Поскольку даже самый простой скрипт маршрутизации, который почти ничем не занимается (реализует только базовый регистратор и редиректор), достаточно сложен, мы его разберем по косточкам:
# Точка входаroute{ # Следующий условный оператор осуществляет проверку поля заголовка MaxForwards — счетчик «прыжков» маршрутизации. Если его значение равно нулю, посылается ответ 483 и прекращается обработка данного пакета. Помимо этого, функция mf_process_maxfwd_header() смотрит, а имеется ли вообще такой заголовок в пакете, если нет, то он создается и устанавливается в заданное значение — в данном случае 10. if (!mf_process_maxfwd_header("70")) { sl_send_reply("483","Too Many Hops"); exit; } # Далее мы проверяем наличие тега у поля To, который показывает, относится ли данный пакет к какому-либо диалогу. if (has_totag()) { # Пакет OPTIONS, помимо собственно запроса доступных опций, обычно используется в качестве средства проверки соединения. Следующая проверка служит для того, чтобы удостовериться, действительно ли пакет предназначен именно нашему прокси. if (is_method("OPTIONS") && uri==myself && (! uri=~"sip:.*[@]+.*")) { options_reply(); exit; } # Смотрим, есть ли в пакете указание, куда его маршрутизировать дальше. Функция loose_route() сама по себе очень многозначная (как и многие другие функции), и, если таковое указание имеется, она действует в соответствии с секцией 16.12 RFC 3261 за некоторыми исключениями (о них лучше почитать в документации). if (loose_route()) { # В серьезных скриптах маршрутизации здесь и спрятана вся логика — например, осуществляется аккаунтинг. Однако, поскольку скрипт у нас исключительно простой, пакет мы просто маршрутизируем по направлению, которое в нем и задано. route(relay); } else { # В случае же, если пакет не содержит маршрута, мы смотрим, не является ли он пакетом ACK, пришедшим в ответ на сообщение об ошибке, и переправляем его куда следует. if (is_method("ACK")) { if ( t_check_trans() ) { t_relay(); exit; } else { # Если же данный запрос ACK не принадлежит никакой транзакции, мы просто его игнорируем exit; } } # В остальных же случаях мы отправляем сообщение с кодом "404", аналогичное подобному же в HTTP. sl_send_reply("404","Not here"); } exit; } # Обрабатываем запросы, не относящиеся к заданному диалогу. # Запрос CANCEL мы не трогаем и пересылаем дальше. if (is_method("CANCEL")) { if (t_check_trans()) { t_relay(); } exit; } # Функция t_check_trans() тоже имеет двойное назначение — если запрос не относится ни к ACK, ни к CANCEL, но относится к какой-то транзакции ретрансляции, она его ретранслирует дальше, что следующая строчка и делает. t_check_trans(); # Фильтруем пакеты, у которых есть поле Route, но не задано поле To (за исключением пакета ACK) и логируем о подобных попытках if (loose_route()) { xlog("L_ERR", "Attempt to route with preloaded Route's [$fu/$tu/$ru/$ci]"); if (!is_method("ACK")) { sl_send_reply("403", "Preloaded Route denied"); } } # Если запрос адресован не нам, добавляем поле Record-Route для принудительной маршрутизации SIP-трафика через наш прокси if (!is_method("REGISTER|MESSAGE")) { record_route(); } # Если в запросе не фигурирует URI, который хоть как-то относится к нашему серверу, мы его отправляем в route(relay) if (!uri==myself) { route(relay); } # Поддержку presence (сообщений о статусе присутствия абонента) тоже не реализуем, для чего отключаем методы PUBLISH и SUBSCRIBE if (is_method("PUBLISH|SUBSCRIBE")) { sl_send_reply("503", "Service Unavailable"); exit; } # Обработка запроса REGISTER. Для упрощения скрипта мы даем возможность регистрироваться всем, безо всякой аутентификации. Кроме того, база местоположений по тем же соображениям временная, в настоящую БД ничего не пишется. if (is_method("REGISTER")) { if (!save("location", "m")) { sl_reply_error(); } exit; } # Функция lookup() проверяет, есть ли у нас в базе местоположений данный пользователь. Если его нет, мы создаем новую транзакцию и возвращаем "404". Опять же в серьезных скриптах здесь еще и проверяются поддерживаемые клиентом методы, чего мы не делаем. if (!lookup("location")) { t_newtran(); t_reply("404", "Not Found"); exit; } route(relay);}# Блок relay, который и обрабатывает все проходящие пакетыroute[relay] { # В случае INVITE мы смотрим, есть ли отрицательный результат транзакции, и, если есть, отправляем в соответствующий блок if (is_method("INVITE")) { t_on_failure("fail"); } # Наконец, пропускаем пакет дальше и, если он почему-либо не проходит, выдаем ошибку "500" if (!t_relay()) { send_reply("500", "Internal Server Error"); }}# Блок fail, о котором было упомянуто вышеfailure_route[fail] { # Если транзакция была отменена, мы просто выходим из блока if (t_was_cancelled()) { exit; }}
По завершению конфигурирования нужно проверить конфиг на наличие синтаксических ошибок с помощью следующей команды:
# sudo opensips -C
Затем включаем возможность запуска OpenSIPS в файле /etc/default/opensips:
RUN_OPENSIPS=yes
И стартуем его:
$ sudo service opensips start
Можно приступать к тестированию.
info
Для управления OpenSIPS существует Web-GUI — OpenSIPS-CP.
Тестирование конфигурации
Для тестирования используем два клиента, запущенные на двух машинах. Я использовал Linphone и Twinkle. В первом для добавления учетной записи открываем настройки (Linphone → Preferences) и переходим на вкладку Manage SIP Accounts, где и добавляем с помощью кнопки Add. В поле Your SIP identity ставим SIP-идентификатор (вида sip:имя_пользователя@SIP-домен), а в поле SIP Proxy address пишем адрес (не SIP-домен!) SIP-прокси.
В случае же с Twinkle при первом запуске будет предложено создать учетную запись. Назови профиль и выбери тип Wizard для создания учетной записи. Далее вбиваем все нужное. Единственное отличие от Linphone состоит в том, что в Twinkle имя пользователя пишется отдельно от домена.
После регистрации, чтобы убедиться, что клиенты доступны, можно посмотреть список местоположений пользователей на сервере, для чего нужно набрать MI-команду:
# sudo opensipsctl fifo ul_dump
Эта команда вызывает функцию MI (интерфейса управления) модуля usrloc — ul_dump, которая и выводит список пользователей, фактически зарегистрированных на сервере.
После этого уже можно звонить. С этим не должно возникнуть особых проблем, но если они все‑таки будут — используй функцию xlog() для логирования и tcpdump/Wireshark для исследования пакетов.
Заключение
OpenSIPS позволяет манипулировать с пакетами SIP чрезвычайно гибко. Однако порог вхождения в него достаточно высокий — требуется не только досконально знать протокол, но и понимать, что делает та или иная функция, и иметь представление, для чего в общем может понадобиться какой‑либо модуль, даже сам список которых выглядит внушительно.
В статье, разумеется, был затронут лишь самый минимум из того, что умеет OpenSIPS, — не были рассмотрены ни авторизация при регистрации, ни аккаунтинг, ни, тем более, настройка OpenSIPS в качестве B2BUA. Тем не менее общее представление о возможностях OpenSIPS и о синтаксисе его файла конфигурации у тебя должно сложиться. Ну а в том случае, если тебе это нужно, — дальше сможешь разобраться и сам.