Русский
Русский
English
Статистика
Реклама

Mikopbx

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

30.09.2020 14:10:05 | Автор: admin

Предыстория

Материал изначально готовился как доклад для 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 записи

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

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

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

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

Автоподъем. 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) проблемы

  • 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 = 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" версий

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

Подробнее..
Категории: Asterisk , *nix , Pbx , Mikopbx

Asterisk. Оповещение о записи разговора

13.05.2021 12:17:12 | Автор: admin

Занимаюсь разработкой MikoPBX - простой в настойке АТС на базе Asterisk 16.

Недавно решили добавить функционал оповещения о записи разговора. Задачу сформулировали следующим образом:

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


Подключение модулей

Было принято решение использовать функционал приложения ChanSpy.

Для начала следует убедиться, что необходимые модули подгружается при начале работы asterisk. Добавим в modules.conf :

load => app_chanspy.soload => app_originate.so

Реализация dialplan

Путь к файлу записи оповещения опишем в extensions.conf, в секции global:

[globals] PBX_REC_ANNONCE=/var/mikopbx/media/custom/alert

Опишем dialplan для оповещений

[annonce-spy]exten => _.!,1,ExecIf($[ "${EXTEN}" == "h" ]?Hangup()  same => n,Set(chan=PJSIP/${EXTEN})  ; Проверка на существование канала.  same => n,ExecIf($["${CHANNELS(${chan})}x" != "x"]?Chanspy(${chan},uBq))  same => n,Hangup()[annonce-play]exten => annonce,1,Answer()  ; Воспроизведение медиа файла  same => n,Playback(${PBX_REC_ANNONCE})  same => n,Hangup()

Теперь, чтобы запустить оповещения достаточно знать имя канала и выполнить:

Originate(Local/${chan}@annonce-spy,exten,annonce-playback-in,annonce,1,10,a); 
  • опция "a" - указывает на то, что приложение будет выполнено асинхронно

  • "chan" - в переменной необходимо описать имя канала, без описания технологии.

  • Local/${chan}@annonce-spy - запускаем приложение Chanspy

  • "10" - как долго пытаться дозвониться до ${chan}@annonce-spy, в данном примере не имеет значения

  • exten,annonce-playback-in,annonce,1 - направляем в контекст с приложением Playback

Дополним dialplan входящих. В приложении Dial задействуем опцию "U" для перехвата момента соединения абонентов:

[incoming]exten => _XXX,1,Dial(${PJSIP_DIAL_CONTACTS(${EXTEN})},60,U(dial-answer))[dial-answer]exten => _[0-9*#+]!,1,Set(chan=${CUT(CHANNEL,/,2)})  same => n,Originate(Local/${chan}@annonce-spy,exten,annonce-play,annonce,1,2,a);  same => n,return

Теперь осталось протестировать входящие. Аналогично можно реализовать оповещение и для исходящих звонков.

Заключение

Относительно просто, без использования AGI, только на основе приложений dialplan возможно реализовать оповещение о записи разговоров.

Как ни странно, в сети довольно мало информации на эту тему.

Надеюсь эта статья будет полезна читателю :)

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

Подробнее..
Категории: Asterisk , *nix , Pbx , Mikopbx

Asterisk. И снова AMI Originate

17.05.2021 18:06:47 | Автор: admin

Ранее я уже писал "AMI. Разносторонний Originate. Применение в CTI приложении". На тот момент мне казалось, что тема раскрыта, исчерпана. Но оказалось, есть куда стремиться.

Классический Originate

Action: OriginateChannel: PJSIP/201Context: all-peersExten: 203Priority: 1Callerid: 201

Пример контекста all-peers

[all-peers]exten => _X!,1,Dial(${PJSIP_DIAL_CONTACTS(${EXTEN})},30,Tt)same => n,Hangup()
  • 201 совершит вызов на 203

  • callerid у 201 отобразиться как 201 (значение параметра "Callerid" в команде Originate)

  • callerid у 203 отобращиться как 201 - все красиво

В истории звонков отобразится

2021-05-17 16:34:17|2021-05-17 16:34:20|2021-05-17 16:34:32|201|PJSIP/201-0000000a|203|PJSIP/203-0000000b

В истории все хорошо.

Чего не хватает:

  • 201 видит не корректный номер телефона, в идеале, отобразить номер, с кем планируем разговаривать

  • Не учитывается случай множественной регистрации, для 201 будет звонить последний зарегистрированный contact. Необходимо, чтобы звонили все зарегистрированные на 201 устройства одновременно


Учитываем множественную регистрацию

Опишем дополнительный контекст в extensions.conf

[internal-orig]exten => _X!,1,Set(DST_CONTACT=${PJSIP_DIAL_CONTACTS(${EXTEN})})  same => n,ExecIf($["${DST_CONTACT}x" != "x"]?Dial(${DST_CONTACT},30,Tt))

Теперь Originate примет вид:

Action: OriginateChannel: Local/201@internal-origContext: all-peersExten: 203Priority: 1Callerid: 201

Проблема множественной регистрации решена, звонят все телефоны одновременно. Но, в CDR теперь каша:

2021-05-17 17:01:09|2021-05-17 17:01:12|2021-05-17 17:01:17|203|Local/201@internal-orig-00000006;2|201|PJSIP/201-000000122021-05-17 17:01:09|2021-05-17 17:01:12|2021-05-17 17:01:17|201|Local/201@internal-orig-00000006;1|203|PJSIP/203-00000013
  • Теперь две записи

  • Появились Local каналы

Это все решается, но перейдем к этому вопросу позже.

Корректный CallerID для звонящего

Добавим в AMI команду дополнительную переменную origCid=203:

Action: OriginateChannel: Local/201@internal-origContext: all-peersExten: 203Priority: 1Callerid: 201Variable: origCid=203

Значение переменной будет совпадать с параметром "Exten" нашего запроса AMI.

Теперь дополним dialplan:

[internal-orig]exten => _X!,1,Set(D_CONT=${PJSIP_DIAL_CONTACTS(${EXTEN})})  same => n,Set(CALLERID(num)=${origCid})  same => n,ExecIf($["${D_CONT}x" != "x"]?Dial(${D_CONT},${ringlength},Tt))

Теперь на каждый увидит корректный callerid.

Исправляем информацию в CDR

Эта задача оказалась наиболее сложной и интересной. К сожалению в интернет на эту тему информации крайне мало.

Основная идея по порядку:

  • Как можно раньше убить Local каналы (hangup)

  • Local каналы НЕ должны создавать CDR записи

  • Необходимо перехватить канал звонящего при создании и направить его в контекст назначения

Теперь на примерах.

Для определения вновь созданного "реального канала" (НЕ Local) следует использовать опцию U(orig-answer-channel) в приложении Dial

Дополнительный контекст orig-answer-channel примет вид:

[orig-answer-channel]exten => s,1,Set(MASTER_CHANNEL(O_SRC_CHAN)=${CHANNEL})  same => n,return

Основная его задача - получить имя реального канала.

Дополним контекст назначения. В нем мы завершим все Local каналы и "Реальный канал" в контекст назначения:

[all-peers]exten => _X!,1,ExecIf($[ "${O_SRC_CHAN}x" != "x" ]?ChannelRedirect(${O_SRC_CHAN},${CONTEXT},${EXTEN},1))  same => n,ExecIf($[ "${O_SRC_CHAN}x" != "x" ]?Hangup())  same => n,Dial(${PJSIP_DIAL_CONTACTS(${EXTEN})},30,Tt)  same => n,Hangup()

В "all-peers" первым попадет Local канал. Это хорошо видно в verbose логе:

Executing [203@all-peers:1] ExecIf("Local/201@internal-orig-00000009;1", "1?NoCDR()")Executing [203@all-peers:2] ExecIf("Local/201@internal-orig-00000009;1", "1?ChannelRedirect(PJSIP/201-00000016,all-peers,203,1)")Executing [203@all-peers:3] ExecIf("Local/201@internal-orig-00000009;1", "1?Hangup()")

Было использовано приложение "ChannelRedirect" реальный канал будет переадресован в контекст назначения, а Local каналы будут завершены.

Executing [203@all-peers:1] ExecIf("PJSIP/201-00000016", "0?NoCDR()")Executing [203@all-peers:2] ExecIf("PJSIP/201-00000016", "0?ChannelRedirect(,all-peers,203,1)")Executing [203@all-peers:3] ExecIf("PJSIP/201-00000016", "0?Hangup()")Executing [203@all-peers:4] Dial("PJSIP/201-00000016", "PJSIP/203/sip:203@172.16.156.1:59442;ob,30,Tt")

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

Осталось только добавить NoCDR, чтобы убрать cdr записи Local каналов.

Итоговый вариант

Команда AMI примет вид:

Action: OriginateChannel: Local/201@internal-origContext: all-peersExten: 203Priority: 1Callerid: 201Variable: origCid=203

Итоговый dialplan, обратите в нем внимание на два вызова NoCDR:

[internal-orig]exten => _X!,1,NoCDR()same => n,Set(MASTER_CHANNEL(O_DST_CHAN)=${origCid})same => n,Set(CALLERID(num)=${origCid})same => n,Set(DST_CONTACT=${PJSIP_DIAL_CONTACTS(${EXTEN})})same => n,ExecIf($["${DST_CONTACT}x" != "x"]?Dial(${DST_CONTACT},${ringlength},TtU(orig-answer-channel),s,1)))[orig-answer-channel]exten => s,1,Set(MASTER_CHANNEL(O_SRC_CHAN)=${CHANNEL})  same => n,return[all-peers]exten => _X!,1,ExecIf($[ "${O_SRC_CHAN}x" != "x" ]?NoCDR())same => n,ExecIf($[ "${O_SRC_CHAN}x" != "x" ]?ChannelRedirect(${O_SRC_CHAN},${CONTEXT},${O_DST_CHAN},1))same => n,ExecIf($[ "${O_SRC_CHAN}x" != "x" ]?Hangup())same => n,Dial(${PJSIP_DIAL_CONTACTS(${EXTEN})},30,Tt)same => n,Hangup()

В CDR будет сохранена одна запись

2021-05-17 17:29:07|2021-05-17 17:29:10|2021-05-17 17:29:14|201|PJSIP/201-00000018|203|PJSIP/203-00000019

То, что надо! :)

Итоги

Я не утверждаю, что это единственное решение. Также не утверждаю, что оно для всех приемлемо и является верным. Реализация мне нравится, остался доволен полученным опытом и результатом.

Описанный прием мы успешно применили в нашей бесплатной АТС с открытым исходным кодом MikoPBX.

Полезный ресурсы

Подробнее..
Категории: Asterisk , Api , Ami , Pbx , Mikopbx

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru