Как мы переводили 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" версий

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

    Средняя зарплата в IT

    111 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 7 268 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 7

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

      как приятно слышать эти слова
        0

        Осталось убедить в необходимости этого перехода провайдеров телефонии, недавно Яндекс телефонию не смог подключить к своему астеру по pjsip, может, конечно, я не гуру, но только по классике все заработало.
        Вопрос: одну из основных причин можно устранить несколькими внутренними номерами и ринг группой? Разве нет?

          +1
          К Yandex проблем не было с подключением.
          Даже инструкции одно время описал:
          wiki.mikopbx.com/providers:yandex:telephony

          можно устранить несколькими внутренними номерами и ринг группой

          Попробовать можно, но проблемы кроются в деталях. Меня не устраивала история звонков в этом случае. Усложняется анализ / отборы в отчетах. Усложняется администрирование этого «огорода» учетных записей. «Не зашло» такое решение. В PJSIP все выглядит более красиво и правильно.

          Неразрешимых проблем с подключением провайдеров не встретил, хотя проектов выполнил не мало.


            +1
            Драйвер не влияет на взаимодействие с провайдерами (с другими PBX), т.к. под капотом остается SIP, тут уже, кто как соблюдает rfc
              0
              Очень даже влияет. Половина, если не больше, кода драйвера — это всякие обработки «не по rfc».
                0
                О да :( Из-за того, что в АТС Билайна работа с BLF реализована через не пойми какое место, нам пришлось отказаться от их услуг.
                До этого работали с тремя операторами телефонии — у всех именно BLF работало как надо.
            0
            Последний раз, когда пробывал использовать pjsip в высоконагруженной системе — висло от 2 до трех раз в сутки. Там же chan_sip работает буквально годами(на других машинах того же кластера, естественно).
            Надо, конечно, попробывать еще раз(полгода прошло), но непонятно как они с такими приколами chan_sip в depricated записали.

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

            Самое читаемое