Реализация VoIP карточной платформы на FreeSWITCH с использованием RADIUS

в 14:54, , рубрики: freeswitch, ip-телефония, radius, метки: ,

Встала задача избавиться от старого хлама в стойке и реализовать программную версию, слегка забытой, но до сих пор существующей технологии для оказания, как правило, междугородней/международной связи для абонентов других операторов посредством звонка на специальный номер доступа и вводом ПИН кода. Авторизация абонентов проходит через биллинг посредством RADIUS, записи о звонках складываются туда же.

Сама по себе платформа мало кому интересна, но когда я писал конфиги, мне очень не хватало примеров использования, надеюсь, этот пример кому-нибудь пригодится.

Моя работа лишь поверхностно связана с телефонией, поэтому с Астериском я не очень то и знаком, поэтому при выборе базовой платформы у меня не было каких либо ограничений и предрассудков. Вполне логично было бы реализовать всё на Астериске, особенно учитывая его активное использование в компании, но по горькому опыту, его падения происходят в самый неподходящий момент и простой перезапуск службы не помогает. Поэтому, начитавшись позитивных отзывов и обзоров, платформой был выбран FreeSWITCH. Документации на него конечно гораздо меньше и еще меньше на русском языке, но это не испугало, ведь очень хорошо помнится, как Астериск на закате h323 собирался из нескольких пакетов, в строгом соответствии версий и примеров инсталляций были единицы. Перед началом и в процессе настройки был тщательно изучен Wiki.

Постановка задачи

Есть некоторое количество людей желающих звонить по межгороду выгоднее, чем по тарифам особенно сотового оператора, без каких либо приложений на телефоне или просто с рабочего номера, где нет выхода на «8-ку». Для этого организуется номер доступа (или несколько), куда клиент звонит (далее 555555), проходит авторизацию (по АОН или ПИН коду), слышит свой текущий остаток средств и набирает номер куда хотел позвонить, данные о звонке должны попасть в биллинг для обсчета. Собственно все это уже работало с давних лет (и было мега популярной услугой) на огромной и страшной Cisco AS5300. Предвосхищая критику авторизации по АОН: система предоплатная и больших балансов ни у кого нет – риски минимальные, клиентов мало – трудно догадаться под каким АОН можно звонить бесплатно, звонить через VoIP и подменять номер бесполезно – профит от такого звонка минимальный, местных операторов отследить легко.

Что-то вроде схемы сервиса, номера справа это expression для extension’ов:

Реализация VoIP карточной платформы на FreeSWITCH с использованием RADIUS

Ничего сложного, но повозиться пришлось.

  1. Абсолютно не привычный для админа формат конфигов в xml, к тому же логика их инклудов по началу кажется очень запутанной.
  2. Очень не привычная логика условий condition, в частности меня все время сбивало с толку break=«on-true» (как же так прервать выполнение программы по истине), советую очень внимательно подойти к изучению этого вопроса и всё становится очень даже логично. Если кратко, то break влияет только на процесс охоты (об охоте чуть ниже) и прерывает обработку conditions в текущем extension если условие совпало (true), не совпало (false по умолчанию) или вообще не прерывает и обрабатывает следующее условие в любом случае (never).
  3. Все вычисления, кроме примитивных для которых можно установить inline=«true», выполняются только после transfer или execute_extension. Суть в том что FS обрабатывает XML_Dialplan в два этапа — охота и выполнение (hunting и executing). Во время охоты выполняются conditions, actions and anti-actions и выбираются приложения которые надо выполнить. Поэтому когда надо получить результаты какого либо сложного приложения надо переходить в другой extension.
  4. Нельзя просто так взять и подставить что-то в строку без регулярных выражений и очень смущает отсутствие просто элементарных алгебраических функций.
  5. Документация, особенно на модули очень скудная (было эпичным описание функции, которое в апреле исправили: If you don't know what this app does then you should not be using it! :) ).

Решение

Установку FS описывать не буду, там все просто, к тому же стандартно, дополнительно надо только скомпилировать недостающие модули. Все пути буду указывать относительно папки, где установлен FS.

Для настройки был выделен отдельный номер и направлен на FS (есть особенность использования нестандартного порта при звонке с внешних серверов – 5080). Создаем профиль для исходящих звонков (для приема достаточно правильного extension в контексте public) через сервер с названием sipgate (IP адрес 10.10.10.10) в conf/sip_profiles/external/sipgate.xml, в моем случае достаточно без авторизации:

<include>
  <gateway name="sipgate">
  <param name="username" value="<тестовый номер доступа>"/>
  <param name="proxy" value="10.10.10.10"/>
  <param name="register" value="false"/>
  <param name="caller-id-in-from" value="true"/>
  </gateway>
</include>

Для дальнейшей работы необходимо русифицировать FS и была наполнена папка sounds/ru/RU/elena звуковыми файлами нужного битрейта, в моем случае 8000 (архив с файлами). В файле freeswitch.xml меняем en на ru:

<section name="languages" description="Language Management">
<!--    <X-PRE-PROCESS cmd="include" data="lang/en/*.xml"/>-->
    <X-PRE-PROCESS cmd="include" data="lang/ru/*.xml"/>
  </section>

Голосовые файлы использовались стандартные и их недостаточно, но с записью пока проблемы.
Конфиг dialplan/public.xml был максимально урезан:

<include>
  <context name="public">
    <extension name="unloop">
      <condition field="${unroll_loops}" expression="^true$"/>
      <condition field="${sip_looped_call}" expression="^true$">
        <action application="deflect" data="${destination_number}"/>
      </condition>
    </extension>
    <X-PRE-PROCESS cmd="include" data="public/*.xml"/>
  </context>
</include>

А в файле conf/dialplan/public/voip_public.xml пишем extension для входящих вызовов, где сразу же пытаемся его авторизовать по номеру:

<include>
  <extension name="voip_platform_pub_step1">
    <condition field="destination_number" expression="^(555555)$">
        <!-- Еще не ответив абоненту пытаемся его авторизовать по АОНу -->
        <action application="log" data="INFO pub/1 RAD_AUTH STEP1"/>
        <action application="set" data="process_cdr=b_only"/><!-- отключает отправку Stop accounting пакета для А ноги, т.к. нам не надо тарифицировать входящий звонок, но почему то Start пакеты все-равно отправляются и их много... -->
        <action inline="true" application="set" data="pin_auth_count=0"/><!-- флаг повторной авторизации по ПИНу, чтобы предотвратить повторные попытки в одном сеансе - перебор паролей -->
        <!-- устанавливаем параметры запроса к radius серверу -->
        <action inline="true" application="set" data="CALLID=${uuid}"/>
        <action inline="true" application="set" data="CALLINGNUMBER=${caller_id_number}"/>
        <action inline="true" application="set" data="USERNAME=${caller_id_number}"/>
        <action inline="true" application="set" data="STEP=fs1"/><!-- Первый шаг авторизации по АОНу (для однозначного определения Сервиса сети биллингом) -->

        <action application="auth_function" data="in ${CALLEDNUMBER}, in ${USERNAME}, in ${PASSWD}, out AUTH_RESULT"/><!-- вызов функции авторизации radius, параметры запроса в ../../autoload_configs/rad_auth.conf.xml -->

        <action application="log" data="INFO pub/1 AUTH_RESULT=${AUTH_RESULT}: credit_amount=${credit_amount}; return_code=${return_code}"/>
        <action application="set" data="domain_name=$${domain}"/>
        <action application="transfer" data="10 XML voip"/><!-- далее вся обработка вызова будет в специальном контексте -->
    </condition>
  </extension>
</include>

Отдельное внимание «STEP=fs1» – в моем случае удобнее было сказать билингу что авторизация по АОНу с флагом fs1, а авторизация по ПИН fs1pin.

В комментариях упоминается conf/autoload_configs/rad_auth.conf.xml (IP адрес RADIUS сервера 10.20.20.20):

<configuration name="rad_auth.conf" description="radius authentification module">
  <settings>
  </settings>

  <client>
    <param name="authserver" value="10.20.20.20:1812:radiussecret"/>
    <param name="dictionary" value="/usr/local/etc/radiusclient/dictionary.all"/>
    <param name="seqfile" value="/var/run/radius.seq"/>
    <param name="mapfile" value="/usr/local/etc/radiusclient/port-id-map"/>
    <param name="default_realm" value=""/>
    <param name="radius_timeout" value="3"/>
    <param name="radius_retries" value="2"/>
    <param name="radius_deadtime" value="0"/>
    <param name="bindaddr" value="*"/>
  </client>

  <vsas>
    <!--name=радиус атрибут, id=его номер согласно dictionary, value=подставляется переменная откуда брать значение, pec=тоже согласно dictionary, expr=говорит о необходимости посчитать или просто взять значение, direction=надеюсь понятно -->
    <param name="Acct-Session-Id" id="44" value="CALLID" pec="0" expr="1" direction="in"/>
    <param name="Freeswitch-Ani" id="8" value="CALLINGNUMBER" pec="27880" expr="1" direction="in"/>
    <param name="Freeswitch-Dst" id="5" value="CALLEDNUMBER" pec="27880" expr="1" direction="in"/>
    <param name="NAS-Port-Type" id="61" value="0" pec="0" expr="0" direction="in"/>
    <param name="Connect-Info" id="77" value="STEP" pec="0" expr="1" direction="in"/>

    <param name="CREDIT_AMOUNT" id="101" value="credit_amount" pec="9" expr="0" direction="out"/>
    <param name="CREDIT_TIME" id="102" value="credit_time" pec="9" expr="0" direction="out"/>
    <param name="RADIUS_RETURN_CODE" id="103" value="return_code" pec="9" expr="0" direction="out"/>  
  </vsas>
 </configuration>

А теперь вся основная логика в файле conf/dialplan/voip.xml, согласно схеме в начале статьи:

<?xml version="1.0" encoding="utf-8"?>
<include>
 <context name="voip">

  <extension name="unloop">
    <condition field="${unroll_loops}" expression="^true$"/>
    <condition field="${sip_looped_call}" expression="^true$">
      <action application="deflect" data="${destination_number}"/>
    </condition>
  </extension>

  <extension name="voip_10">
    <condition field="destination_number" expression="^10$" break="on-false"/>
    <condition field="${AUTH_RESULT}" expression="^OK$" break="on-true">
      <!-- отправляем в IVR для считывания dtmf номера -->
      <action application="log" data="INFO voip_10 AUTH_RESULT=${AUTH_RESULT} => Read DTMF"/>
      <action application="answer"/>
      <action application="sleep" data="1000"/>
      <action application="play_and_get_digits" data="6 20 5 30000 # phrase:voip_get_digits voicemail/vm-fail_auth.wav digits ^**(d{6}|d{10,20})**$ 5000"/><!-- <min> <max> <tries> <timeout> <terminators> <file> <invalid_file> <var_name> <regexp> <digit_timeout> -->
      <action application="transfer" data="20 XML voip"/>
    </condition>
    <condition field="${return_code}" expression="^h323-return-code=6$" break="on-true">
      <!-- Баланс отрицательный или подключение приостановленно => надо сказать что недостаточно средств -->
      <action application="log" data="INFO voip_10 RETURN_CODE = 6 => Closed account"/>
      <action application="answer"/>
      <action application="sleep" data="1000"/>
      <!--TODO!!! подобрать нормальный ответ про баланс -->
      <action application="playback" data="voicemail/vm-not_available.wav"/>
      <action application="hangup" data="NORMAL_CLEARING"/>
    </condition>
      <!--TODO!!! попытаться обработать больше ошибок  -->
    <condition field="${pin_auth_count}" expression="^0$" break="on-true"><!-- Проверяем что клиент еще не пытался авторизоваться -->
      <!-- По каким то причинам авторизация по АОНу не прошла, спрашиваем PIN  -->
      <action inline="true" application="set" data="pin_auth_count=1"/>
      <action application="log" data="INFO voip_10 RETURN_CODE = OTHER"/>
      <action application="answer"/>
      <action application="sleep" data="1000"/>
      <action application="play_and_get_digits" data="10 10 5 30000 # phrase:voip_get_pin conference/conf-bad-pin.wav pin ^(d{10})$ 5000"/><!-- <min> <max> <tries> <timeout> <terminators> <file> <invalid_file> <var_name> <regexp> <digit_timeout> -->
      <action application="transfer" data="15 XML voip"/>
    </condition>
    <condition>
      <!-- если не прошла авторизация по АОНу,
           если радиус отбил не по причине закрытого аккаунта,
           если клиент не попал на проверку PIN-кода,
           значит лузер уже вводил PIN-код и не прошел авторизацию -->
      <action application="log" data="INFO voip_10 Prevent second PIN authentification"/>
      <action application="answer"/>
      <action application="sleep" data="1000"/>
      <!--TODO!!! подобрать нормальный файл с прощанием с наилучшими пожеланиями -->
      <action application="playback" data="voicemail/vm-not_available.wav"/>
      <action application="hangup" data="NORMAL_CLEARING"/>
    </condition>
  </extension>

  <extension name="voip_15">
    <condition field="destination_number" expression="^15$"/>
    <condition field="${pin}" expression="^(d{6})(d{4})$">
      <!-- Пытаемся авторизовать клиента по введенному PIN-коду -->
      <action application="log" data="INFO voip_15 pin=($1+$2) => RAD_AUTH STEP1/PIN"/>
      <action inline="true" application="set" data="CALLINGNUMBER=${caller_id_number}"/>
      <action inline="true" application="set" data="USERNAME=$1"/>
      <action inline="true" application="set" data="PASSWD=$2"/>
      <action inline="true" application="set" data="STEP=fs1pin"/>
      <action application="log" data="INFO voip_15 CALLID=${CALLID}; CALLINGNUMBER=${CALLINGNUMBER}; USERNAME=${USERNAME}"/>

      <action application="auth_function" data="in ${CALLEDNUMBER}, in ${USERNAME}, in ${PASSWD}, out AUTH_RESULT"/>

      <action application="log" data="INFO voip_15 AUTH_RESULT=${AUTH_RESULT}: credit_amount=${credit_amount}; return_code=${return_code}"/>
      <action application="transfer" data="10 XML voip"/><!-- повторно отправляем на предыдущий шаг для проверки результата запроса к радиусу уже по ПИНу -->
    </condition>
  </extension>

  <extension name="voip_20">
    <condition field="destination_number" expression="^20$"/>
    <condition field="${digits}" expression="^**(d+)**$">
      <!-- Спрашиваем у радиуса сколько секунд клиент может поговорить -->
      <action inline="true" application="set" data="digits=$1"/>
      <action inline="true" application="set" data="digits=${regex(${digits}|^(d{6})$|83532%1)}"/><!-- Подставляем 85555 – код города для вызовов на локальные номера если шаблон ^(d{6})$ не подходит, то значение переменной остается неизменным -->

      <action application="log" data="INFO voip_20 DTMF digits=${digits} => RAD_AUTH STEP2"/>
      <action application="log" data="INFO voip_20 CALLID=${CALLID}; CALLINGNUMBER=${CALLINGNUMBER}; USERNAME=${USERNAME}"/>

      <action inline="true" application="set" data="CALLEDNUMBER=${digits}"/>
      <!-- если авторизация была по ПИНу устанавливаем fs2pin иначе fs2 -->
      <action inline="true" application="set" data="STEP=${regex(${STEP}|^fsd(.*)$|fs2%1)}"/>
      <action application="auth_function" data="in ${CALLEDNUMBER}, in ${USERNAME}, in ${PASSWD}, out AUTH_RESULT"/>

      <action application="log" data="INFO voip_20 AUTH_RESULT=${AUTH_RESULT}: credit_amount=${credit_amount}; credit_time=${credit_time}; return_code=${return_code}"/>
      <!-- запланированная задача для точного ограничения времени звонка согласно секундам выданным радиусом -->
      <action application="export" data="nolocal:api_on_answer=sched_hangup +${credit_time} ${uuid} alloted_timeout" />
      <action application="transfer" data="30 XML voip"/>
    </condition>
  </extension>

  <extension name="voip_30">
    <condition field="destination_number" expression="^30$" break="on-false"/>
    <condition field="${AUTH_RESULT}" expression="^OK$" break="on-true">
      <action application="log" data="INFO voip_30 AUTH_RESULT=${AUTH_RESULT} => Call number"/>
      <action inline="true" application="set" data="effective_caller_id_number=35555555555"/>
      <!-- для определения Сервиса сети биллингом (АОН|ПИН) выдаем последний шаг (fs2|fs2pin), значение подставится в поле Freeswitch-CLID аккаунтинг пакета -->
      <action inline="true" application="set" data="effective_caller_id_name=${STEP}"/>
      <!-- экспортируем для Б ноги USERNAME под которым была авторизация, чтобы модифицированный mod_radius_cdr заменил этим значением стандартный АОН в поле User-Name аккаунтинг пакета -->
      <action application="export" data="nolocal:acc_username=${USERNAME}"/>
<!--      <action application="set_profile_var" data="Caller-Username=${USERNAME}"/> по идее этим можно заменить юзернэйм для оригинального mod_radius_cdr, но сходу не сработало и модуль уже модифицирован -->
      <action application="set" data="hangup_after_bridge=true"/><!-- после !успешного соединения закончится обработка звонка -->
      <action application="bridge" data="sofia/gateway/sipgate/${digits}"/>
      <!--TODO!!! обработать неудачный вызов -->
      <action application="log" data="INFO voip_30 AFTER BRIDGE"/>
    </condition>
    <!--TODO!!! Здесь должны быть другие ошибки про неправильное направление, нет цены и т.п. -->
    <!--TODO!!! h323-return-code=9 Access denied - если биллинг не подобрал ТПТ, может быть отправить на повторный ввод номера -->
    <condition>
      <action application="log" data="INFO voip_30 RETURN_CODE = OTHER"/>
      <action application="answer"/>
      <action application="sleep" data="1000"/>
      <action application="playback" data="zrtp/zrtp-status_error.wav"/>
      <action application="hangup" data="NORMAL_CLEARING"/>
    </condition>
  </extension>

 </context>
</include>

Упоминается phrase:voip_get_digits и phrase:voip_get_pin – «фразы» которые будут говориться клиенту во время ожидания ввода, причем второй можно было сделать и без этого. Хранятся в файле conf/lang/ru/viop.xml:

<include>
  <macro name="voip_get_digits" pause="250">
    <input pattern="(.*)">
      <match>
        <action function="play-file" data="ivr/ivr-account_balance_is.wav"/>
        <action function="say" data="${credit_amount}" method="pronounced" type="currency"/>
        <action function="play-file" data="ivr/ivr-please_enter_the_phone_number.wav"/>
      </match>
    </input>
  </macro>
  <macro name="voip_get_pin" pause="250">
    <input pattern="(.*)">
      <match>
        <action function="play-file" data="ivr/ivr-please_enter_pin_followed_by_pound.wav"/>
        <action function="execute" data="sleep(1000)"/>
      </match>
    </input>
  </macro>
</include>

И еще конфиг conf/autoload_configs/mod_radius_cdr.conf.xml, где практически ничего не настраивается и по факту вся логика жестко написана в коде:

<configuration name="mod_radius_cdr.conf" description="RADIUS CDR Configuration">
        <settings>
                <param name="dictionary" value="/usr/local/etc/radiusclient/dictionary.all"/>
                <param name="seqfile" value="/var/run/radius.seq"/>
                <param name="acctserver" value="10.20.20.20:1813:radiussecret"/>
                <param name="radius_retries" value="2"/>
                <param name="radius_timeout" value="3"/>
                <param name="radius_deadtime" value="0"/>
        </settings>
</configuration>

Мне пришлось немного модифицировать код этого модуля, так как статистика должна сваливаться или по номеру или по логину, а как через этот модуль штатными средствами пропустить свою переменную с флагом используемой авторизации я не нашел.

В итоге, когда уже все работало, был жестоко кастрирован общий конфиг FS, дабы сократить итоговый конфиг log/freeswitch.xml.fsxml.
По тексту есть тудушки, без них все работает, но если их сделать будет красивее.

Автор: vitux

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js