- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

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

Есть некоторое количество людей желающих звонить по межгороду выгоднее, чем по тарифам особенно сотового оператора, без каких либо приложений на телефоне или просто с рабочего номера, где нет выхода на «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 (архив с файлами [2]). В файле 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

Источник [3]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/ip-telefoniya/40346

Ссылки в тексте:

[1] Wiki: http://wiki.freeswitch.org

[2] архив с файлами: http://files.freeswitch.org/freeswitch-sounds-ru-RU-elena-8000-1.0.13.tar.gz

[3] Источник: http://habrahabr.ru/post/189148/