Как мы переводили MIKOPBX с chan_sip на PJSIP

от автора

Предыстория

Материал изначально готовился как доклад для asterconf 2020. Теперь постараюсь описать все более подробно в этой статье.

MIKOPBX — это бесплатная АТС с открытым исходным кодом на базе Asterisk 16. Год назад мы взялись за переход на PJSIP.

Основные причины:

  • PJSIP поддерживает «множественную регистрацию«. На одном аккаунте можно без проблем регистрировать несколько конечных UAC

  • Корректная работа входящей маршрутизации при настройке регистрации нескольких учетных записей провайдера на одном адресе (IP+PORT)

  • PJSIP более гибок в настройке

  • chan_sip не развивается и объявлен deprecated в Asterisk 17

Далее опишу с какими сложностями мы столкнулись и какие выгоды получили.


Основная причина — необходимость в поддержке «множественной регистрации«. Крайне удобно подключить к аккаунту несколько софтфонов / телефонов и не беспокоится, входящий вызов поступит где бы ты не находился.

Лично у меня подключены следующие устройства:

  • Аппаратный телефон на рабочем столе в офисе

  • Софтфон на ноутбуке

  • Софтфон на смартфоне

При поступлении входящего звонка на добавочный, все устройства звонят одновременно.

С чего начать?

В нашем случае был готовый файл конфигурации sip.conf. Стало интересно, возможно ли как то конвертировать старый конфиг в новый формат (структура pjsip.conf отличается значительно).

Готовый скрипт был найден в исходниках asterisk. Найти можно по пути:

contrib/scripts/sip_to_pjsip/sip_to_pjsip.py

Из встроенной справки:

Usage: sip_to_pjsip.py [options] [input-file [output-file]] Converts the chan_sip configuration input-file to the chan_pjsip output-file. The input-file defaults to 'sip.conf'. The output-file defaults to 'pjsip.conf'.

Скрипт позволяет получить рабочий конфиг и начать его тестировать и дорабатывать напильником.

Настройка множественной регистрации

После конвертации конфигурационного фала потребовалось увеличить количество контактов, которые могут подключаться к учетной записи (далее endpoint).

Каждую входящую регистрацию Asterisk рассматривает как contact.

Параметр «max_contacts» позволяет ограничить количество устройств, которые могут подключиться к endpoint.

;pjsip.conf [226]  type = aor max_contacts = 5

Количество подключенных контактов можно посмотреть в CLI консоли Asterisk:

mikopbx*CLI> pjsip show contacts    Contact:  <Aor/ContactUri..............................> <Hash....> <Status> <RTT(ms)..> ==========================================================================================    Contact:  201/sip:201@172.16.156.1:60616;ob              418d36496b Avail         3.793   Contact:  201/sip:201@172.16.156.1:60616;ob              ba56853d54 Avail         2.189   Contact:  203/sip:203@172.16.156.1:60616;ob              2cd641799f Avail         0.988  Objects found: 3 

Для того, чтобы при входящем звонили сразу все контакты, потребовалось доработать dialplan.

Пример c комментариями:

;extensions.conf [internal-users]  ; контекст для набора 3х значных внутренних номеров ; PJSIP_DIAL_CONTACTS - функция возвращает Dial-совместимую строку с контактами ; Контакты разделены символом & ; В качестве параметра функции необходимо передать ID endpoint exten => _XXX,1,Set(dialContacts=${PJSIP_DIAL_CONTACTS(${EXTEN})})   ; Перед Dial обязательно необходимо проверить  ; заполнена ли переменная "dialContacts" ; если нет, то на endpoint никто не зарегистрировался same => n,ExecIf($["${dialContacts}x" != "x"]?Dial(${DC},,Tt))

После правки dialplan началось интересное поведение системы.

Наши ожидания не оправдались. Мы предполагали, что при таком звонке, asterisk будет оперировать двумя каналами «Кто звонит» и «Кому звонит«. На практике, все оказалось иначе.

О природе каналов и их происхождении

Каждый канал SIP и PJSIP непосредственно связан с SIP диалогом «PBX — UAC«.

Проще говоря один INVITE = один канал вида SIP/104-0000XX.

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

Зная это, можно сделать следующие выводы:

  • Чем больше каналов, тем больше событий в AMI

  • Каждый канал пройдет определенный для него dialplan

  • Каждый канал повлияет на CDR записи

Если кратко подвести итог, то, после включения множественной регистрации, мы видим влияние на все основные модули наших продуктов:

  • История звонков на АТС

  • Функция записи разговоров

  • Работа CTI приложений, завязанных на AMI

Автоподъем. Paging. Intercom

Это крайне интересные функции. Все они завязаны на функцию «Автоответ«. Может работать как с настольными телефонами, так и с многими софтфонами.

Принцип работы многих UAC схож. Чтобы «поднять трубку» достаточно в INVITE передать дополнительный заголовок. Пример:

Call-Info:\;answer-after=0

В случае с аппаратным телефоном будет включена либо громкая связь, либо произойдет ответ в гарнитуре.

При работе с chan_sip при originate достаточно было установить переменную SIPADDHEADER:

Action: Originate Channel: SIP/104 Context: from-internal Exten: 74952293042 Priority: 1 Callerid: 104 Variable: SIPADDHEADER="Call-Info:\;answer-after=0"

Работа с этой переменной была описана в chan_sip.с и при звонке заголовок добавлялся автоматически в INVITE.

В случае с PJSIP подход отличается. Упрощенный пример extensions.conf:

[internal-users]  exten => 204,1,Dial(${PJSIP_DIAL_CONTACTS(204)},,Ttb(dial_create_chan,s,1)))  [dial_create_chan]  exten => s,1,Set(PJSIP_HEADER(add,Call-Info)=\;answer-after=0)  same => n,return 

Опция «b» в команде «Dial» позволяет созданный канал назначения с помощью Gosub направить в дополнительный контекст «dial_create_chan«.

Только в этом месте есть возможность управлять SIP заголовками ДО отправки INVITE.

Интересный вывод: «dial_create_chan» — место в dialplan, где канал еще существует, но НЕ связан с SIP диалогом.

Теперь более правильный пример установки заголовка:

[internal-users]  ; Получаем контактны: exten => _XXX,1,Set(dС=${PJSIP_DIAL_CONTACTS(${EXTEN})})   ; Считаем количество контактов:   same => n,ExecIf($["${FIELDQTY(dС,&)}"!="1"]?Set(__SIPADDHEADER=${EMPTY}))    same => n,ExecIf($["${dС}x" != "x"]?Dial(${DC},,Ttb(dial_create_chan,s,1)))  [dial_create_chan]  exten => s,1,ExecIf($["${SIPADDHEADER}x" == "x"]?return)   same => n,Set(header=${CUT(SIPADDHEADER,:,1)})   same => n,Set(value=${CUT(SIPADDHEADER,:,2)})   same => n,Set(PJSIP_HEADER(add,${header})=${value})   same => n,Set(__SIPADDHEADER=${EMPTY})    same => n,return 

С помощью функции «FIELDQTY» мы анализируем количество контактов, подключенных к endpoint. Если контактов несколько, то функцию лучше отключить, ведь сложно предугадать, на каком из телефонов сработает ответ на вызов.

С помощью функции «CUT» происходит разбор строки «SIPADDHEADER«, выделяем имя заголовка и его значение.

Обязательно, после PJSIP_HEADER очищаем значение переменной SIPADDHEADER. Это страховка от случайного срабатывания «ответа» на вызов при переадресациях.

Получение значения UserAgent

Для выборка корректного SIP заголовка необходимо понимать какое конечное устройство подключено к endpoint. В случае с pjsip ситуация несколько изменилась. Пример:

[get-user-agent] exten => 300,1,NoOp(--- Incoming call ---)   same => n,Set(vContact=${PJSIP_AOR(300,contact)})   same => n,Set(vUserAgent=${PJSIP_CONTACT(${vContact},user_agent)})   same => n,NoOp(--- ${vContact} & ${vUserAgent} ---)   ... ... ...    same => n,Hangup()

Пример в одну строчку для AOR с ID 300. Для упрощения ID endpoint = ID AOR и = EXTEN:

; ${PJSIP_CONTACT(${PJSIP_AOR(${EXTEN},contact)},user_agent)}

В функцию «PJSIP_AOR» передаем ID AOR, и в качестве опции указываем, что вернуть нам следует поле «contact«.

В функцию «PJSIP_CONTACT» передаем полученный контакт, и в качестве опции указываем, что вернуть следует поле «user_agent«.

Обратите внимание, PJSIP_AOR(300,contact) вернет ID контакта, но это не тоже самое, что можно увидеть в CLI.

Пример результата PJSIP_AOR:

201;@e758f5661420b391e239386a94edbefe

Пример вывода в CLI:

pjsip show contacts 201/sip:201@172.16.156.1:57130;ob Contact:  201/sip:201@172.16.156.1:57130;ob

Исходящая регистрация

Согласно документации Asterisk, разработчики выделяют два основных вида проблем регистрации:

Временные (temporary) проблемы

  • No Response

  • 408 Request Timeout

  • 500 Internal Server Error

  • 502 Bad Gateway

  • 503 Service Unavailable

  • 504 Server Timeout

  • Некоторые 6xx ответы

Постоянные (Permanent) проблемы

  • 401 Unauthorized

  • 403 Forbidden

  • 407 Proxy Authentication Required

  • Прочие 4xx, 5xx, 6xx ошибки

В pjsip.conf при настройке исходящей регистрации обязательно необходимо описать опции для повторной попытки регистрации:

[74952293042]  type = registration  ; Временные неудачи ; Интервал для повторных попыток регистрации retry_interval = 30 ; Максимальное количество попыток max_retries = 100  ; "Постоянные" неудачи ; Интервал используется при получении 403 Forbidden ответа. forbidden_retry_interval = 300 ; Интервал используется при получении Fatal ответов (non-temporary 4xx, 5xx, 6xx) fatal_retry_interval = 300

Если sip_to_pjsip.py для конвертации конфигурации, то эти опции придется описать вручную.

Идентификация провайдера

Для рада провайдеров телефонии может наблюдаться следующая картина:

  • Успешно проходит регистрация по адресу sip.test.ru

  • Допустим sip.test.ru резолвится в 10.10.10.10

  • Входящие вызовы поступают с 11.11.11.11

  • Входящие могут поступать и с 10.10.10.10

Вызовы могут не пройти авторизацию и будут завершены.

В PJSIP есть возможность идентификации по IP адресу:

[74952293042] type = identify ; ... ... ... match=sip.test.ru,185.45.152.0/24,185.45.155.0/24; ; ... ... ...

В параметре «match«, через запятую, можно описать все IP адреса провайдера. В этом случае входящий будет корректно сопоставлен с нужным endpoint.

Кроме того, следует обратить внимание на опцию «endpoint_identifier_order«.

Значение по умолчанию:

endpoint_identifier_order=ip,username,anonymous

Если у вас есть несколько учетных записей одного провайдера, которые регистрируются на одном и том же адресе IP:PORT, то имеет смысл поменять порядок идентификации:

endpoint_identifier_order=username,ip,anonymous

Пример, есть три транка:

  • 99999 — подключается к 10.10.10.10:5060

  • 88888 — подключается к 10.10.10.10:5060

  • 77777 — подключается к 10.10.10.10:5060

Если не настроить «endpoint_identifier_order«, то:

  • все входящие будут направлены в контекст произвольного endpoint (идентификация пройдет по адресу IP:PORT), к примеру в контекст endpoint «99999» .

  • канал, созданный при входящем будет всегда ассоциироваться с одним и тем же endpoint, к примеру PJSIP/99999-0000XXX, на какой внешний номер бы ни звонил клиент

Входящие без регистрации SIP URI

Для ряда случаев удобно направлять входящие на АТС без регистрации.

Обязательно следует подгрузить модуль «res_pjsip_endpoint_identifier_anonymous.so«.

Пример настройки pjsip.conf

[anonymous]  type = endpoint allow = alaw timers = no context = public-direct-dial

Пример extensions.conf

[public-direct-dial] exten => 74952293042,NoOp(--- Incoming call to ${EXTEN} ---) 	same => n,Dial(PJSIP/204,,TKg)); 	same => n,Hangup()

Контекст public-direct-dial должен быть изолирован от исходящих dialplan.

В качестве exten описываются все DID номера и логика маршрутизации.

Подведу итоги

  • Переход на PJSIP состоялся. С chan_pjsip АТС работает стабильно, надежно

  • Нами был получен огромный опыт работы с PJSIP

  • PJSIP более гибок в настройке, предоставляет больше возможностей

  • Функция множественной регистрации крайне удобна и порой незаменима

  • chan_pjsip живой, активно развивается и поддерживается сообществом

Из минусов перехода на chan_pjsip стоит отметить:

  • Требуется модернизация dialplan

  • Изменение поведения AMI, что отражается на CTI клиентах

  • Меняется поведение CDR, требуется доработка легирования истории звонков

  • chan_pjsip активно развивается, в свежих релизах asterisk встречаются грубые ошибки. не стоит гнаться за новыми версиями, лучше выждать появления «certified» версий

Полезные ссылки

ссылка на оригинал статьи https://habr.com/ru/post/521236/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *