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

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

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 12

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

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

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

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

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

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

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


            0
            подскажите, как подключить ДомРу, поскольку voip.domru.ru желает видеть в регистрации длинный номер (UserID), а идентифицирует по короткому (AuthID), без напильника не работает (руками правлю pjsip строчку client_uri=sip:@voip.domru.ru), как это сделать через web интерфейс?
              0
              Подобрал :)
              в дополнительные параметры:
              [registration]
              client_uri = sip: номер_с_кодом_города@voip.domru.ru:5060
                0
                Все верно подобрали ))
                По сути это переопределение параметров для секции с «type=registration»
            +1
            Драйвер не влияет на взаимодействие с провайдерами (с другими PBX), т.к. под капотом остается SIP, тут уже, кто как соблюдает rfc
              0
              Очень даже влияет. Половина, если не больше, кода драйвера — это всякие обработки «не по rfc».
                0
                О да :( Из-за того, что в АТС Билайна работа с BLF реализована через не пойми какое место, нам пришлось отказаться от их услуг.
                До этого работали с тремя операторами телефонии — у всех именно BLF работало как надо.
            0
            Последний раз, когда пробывал использовать pjsip в высоконагруженной системе — висло от 2 до трех раз в сутки. Там же chan_sip работает буквально годами(на других машинах того же кластера, естественно).
            Надо, конечно, попробывать еще раз(полгода прошло), но непонятно как они с такими приколами chan_sip в depricated записали.
              0

              А chan_dongle можно прикрутить ?

                0
                В теории можно. Но на текущий момент соответствующий модуль не собран на АТС. Нет функционала генерации dialplan для этого канала.

              Only users with full accounts can post comments. Log in, please.