- PVSM.RU - https://www.pvsm.ru -
Встала задача избавиться от старого хлама в стойке и реализовать программную версию, слегка забытой, но до сих пор существующей технологии для оказания, как правило, междугородней/международной связи для абонентов других операторов посредством звонка на специальный номер доступа и вводом ПИН кода. Авторизация абонентов проходит через биллинг посредством RADIUS, записи о звонках складываются туда же.
Сама по себе платформа мало кому интересна, но когда я писал конфиги, мне очень не хватало примеров использования, надеюсь, этот пример кому-нибудь пригодится.
Моя работа лишь поверхностно связана с телефонией, поэтому с Астериском я не очень то и знаком, поэтому при выборе базовой платформы у меня не было каких либо ограничений и предрассудков. Вполне логично было бы реализовать всё на Астериске, особенно учитывая его активное использование в компании, но по горькому опыту, его падения происходят в самый неподходящий момент и простой перезапуск службы не помогает. Поэтому, начитавшись позитивных отзывов и обзоров, платформой был выбран FreeSWITCH. Документации на него конечно гораздо меньше и еще меньше на русском языке, но это не испугало, ведь очень хорошо помнится, как Астериск на закате h323 собирался из нескольких пакетов, в строгом соответствии версий и примеров инсталляций были единицы. Перед началом и в процессе настройки был тщательно изучен Wiki [1].
Есть некоторое количество людей желающих звонить по межгороду выгоднее, чем по тарифам особенно сотового оператора, без каких либо приложений на телефоне или просто с рабочего номера, где нет выхода на «8-ку». Для этого организуется номер доступа (или несколько), куда клиент звонит (далее 555555), проходит авторизацию (по АОН или ПИН коду), слышит свой текущий остаток средств и набирает номер куда хотел позвонить, данные о звонке должны попасть в биллинг для обсчета. Собственно все это уже работало с давних лет (и было мега популярной услугой) на огромной и страшной Cisco AS5300. Предвосхищая критику авторизации по АОН: система предоплатная и больших балансов ни у кого нет – риски минимальные, клиентов мало – трудно догадаться под каким АОН можно звонить бесплатно, звонить через VoIP и подменять номер бесполезно – профит от такого звонка минимальный, местных операторов отследить легко.
Что-то вроде схемы сервиса, номера справа это expression для extension’ов:
Ничего сложного, но повозиться пришлось.
Установку 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/
Нажмите здесь для печати.