Предыстория
Материал изначально готовился как доклад для 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 = aormax_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.988Objects found: 3
Для того, чтобы при входящем звонили сразу все контакты,
потребовалось доработать dialplan.
Пример c комментариями:
;extensions.conf[internal-users]; контекст для набора 3х значных внутренних номеров; PJSIP_DIAL_CONTACTS - функция возвращает Dial-совместимую строку с контактами; Контакты разделены символом &; В качестве параметра функции необходимо передать ID endpointexten => _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 записи
Если кратко подвести итог, то, после включения множественной
регистрации, мы видим влияние на все основные модули наших
продуктов:
Автоподъем. Paging. Intercom
Это крайне интересные функции. Все они завязаны на функцию
"Автоответ". Может работать как с настольными
телефонами, так и с многими софтфонами.
Принцип работы многих UAC схож. Чтобы "поднять
трубку" достаточно в INVITE передать
дополнительный заголовок. Пример:
Call-Info:\;answer-after=0
В случае с аппаратным телефоном будет включена либо громкая
связь, либо произойдет ответ в гарнитуре.
При работе с chan_sip при
originate достаточно было установить переменную
SIPADDHEADER:
Action: OriginateChannel: SIP/104Context: from-internalExten: 74952293042Priority: 1Callerid: 104Variable: 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;obContact: 201/sip:201@172.16.156.1:57130;ob
Исходящая регистрация
Согласно документации Asterisk, разработчики выделяют два
основных вида проблем регистрации:
Временные (temporary) проблемы
Постоянные (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 = endpointallow = alawtimers = nocontext = 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" версий
Полезные ссылки