В ту же реку
Относительно недавно я написал себе шпаргалку по настройке FreeSWITCH. Описанный там процесс настройки привел к работоспособной в тестовых условиях конфигурации. Тест был необходим для составления предварительного представления о том, с чем придется иметь дело после переезда организации и запуске телефонии в продакшн. Однако, когда переезд состоялся и началось подключение в рабочем режиме, то первое же включение показало неработоспособность конфигурации: перестали ходить внутренние вызовы.
Это стало для меня полнейшим сюрпризом, поскольку с момента финальной настройки и проверки работоспособности, по мотивам которой была написана шпаргалка, по момент включения в рабочем режиме никаких изменений в конфиг не вносилось. Были лишь массово добавлены внутренние номера и маршруты для входящих и исходящих вызовов для тех сотрудников, за кем были закреплены прямые городские номера (порядка 60 с хвостиком номеров).
Был проведен дебаг, выявлен косяк, и все заработало. Однако, осталось ощущение костыля. Описывать его не стану, поскольку пребываю в уверенности, что примененное решение не верное, хоть и привело к искомому результату. Кроме того, выяснились нюансы: при исходящих вызовах изнутри наружу определялся только тот номер, что был указан в настройке SIP-транка в поле default_provider_username:
<X-PRE-PROCESS cmd="set" data="default_provider_username=3435555555"/>
а не тот, что указан в конфигурации абонентского номера:
<variable name="outbound_caller_id_name" value="3435555566"/>
Техподдержка провайдера сообщила, что все вызовы, прилетающие к ним от нас, в поле From имеют именно номер 3435555555, то есть косяк на моей стороне. Плюс ко всему, я вдруг совершенно завис с задачей переадресации вызовов. А вишенкой на торте стал вынос мозга аппаратами Ericsson Dialog 4422, отказавшимися выполнять трансфер вызова, и аппаратами Cisco 7945g, решившими, что их предел длительности соединения составляет 90-100 секунд при отсутствии малейшего намека на подобную настройку в конфиге. В то же время аппараты Yealink T21 E2 работали полностью без нареканий.
На этом этапе я осознал, что достиг предела своей компетенции в области телефонии и взял тайм-аут на то, чтобы все в голове утряслось и уложилось. Этому решению так же очень могуче способствовало общее утомление после совершенно диких двух рабочих недель без выходных и с ненормированным рабочим графиком, которые последовали сразу после заезда на новое место размещения организации.
FusionPBX
Не смотря на отсутствие у меня симпатий к графическим интерфейсам там, где правит бал консоль и текстовые конфиги, я все же стал смотреть в сторону решения с веб-мордой, именуемого FusionPBX. Первой причиной такой измены собственным принципам стало желание видеть весь объем настроек по каждому функциональному элементу, собранных в одном месте в виде работоспособной «из коробки» конфигурации. Именно такую возможность дает графический интерфейс. Дополнительным бонусом продуманного графического интерфейса является наглядное представление взаимосвязей между модулями и функциями. Для новичка (лично для меня) меньший уровень абстракции с конкретным способом реализации способствует более быстрому обучению и приходу к понимаю того, как эта штука работает. Второй причиной стал www.pbxforums.com, на который я попадал по ссылке через одну при поиске информации по FreeSWITCH, и попадал по иронии судьбы именно на скриншоты страниц настроек FusionPBX.
FusionPBX это FreeSWITCH с веб-мордой и с настройками, хранящимися в базе данных. Скрипт автоматической установки выполняет установку и FreeSWITCH'а, и Nginx'а, и PostgreSQL, и, собственно, веб-интерфейса самого FusionPBX. Останавливаться на этом моменте не стану, все без запинок ставится по инструкции из документации. Ставил все на рекомендуемую разработчиками 64-битную Debian 8.
Импорт абонентских номеров
Здесь не будет рассматриваться процесс настройки абонентских номеров и входящих маршрутов. Этот процесс описан в официальной документации.
Вместо него будет описана процедура импорта всего скопом. Описаний, мануалов и советов по выполнению данной процедуры мною найдено не было.
По окончании установки включаем автоматический вход в Adminer (аналог phpMyAdmin):
Advanced→Default settings:
auto_login
Value: true
Enabled: true После изменения значений на текущей странице нажимаем Save, на странице настроек по умолчанию Reload.Переходим в Adminer: Advanced→Adminer.
Интерес для нас представляют следующие таблицы:
v_extensions — абонентские номера.
v_destinations — маршруты для входящих вызовов на городские номера, закрепленные за внутренними абонентскими номерами.
v_dialplans — справочник диалпланов.
v_dialplan_details — настройки диалпланов входящих вызовов.
v_voicemails — настройки голосовой почты.
Формулировка задачи была следующей: выгрузить из AD ФИО сотрудников и их номера внутренних телефонов, сохранить выгрузку в CSV-файл и импортировать его в БД в таблицу абонентских номеров и настроек голосовой почты (голосовая почта должна быть отключена).
Используя справочник соответствия городских номеров внутренним, создать CSV-файлы для импорта в таблицы с маршрутами и диалпланами входящих вызовов.
Я не стану подробно рассматривать эту задачу, просто спрячу готовые скрипты под спойлер.
Внимание!
Предлагаемые скрипты вы используете на свой страх и риск, автор не несет ответственности за их неправильное использование или неожиданные побочные эффекты их правильного использования.
.
- Присвойте переменной $nums значения, соответствующие вашим номерам.
- Перед использованием скриптов необходимо везде заменить UUID домена на значение, присвоенное домену при установке (поле domain_uuid).
- Так же необходимо заменить IP-адрес домена (172.18.253.1) на ваш.
- Не забудьте откорректировать значение ключа -SearchBase, указав свою область выборки вместо «OU=Ekaterinburg,DC=dc,DC=domain,DC=local»
- UUID приложения Voicemail (поле app_uuid) так же заменить на UUID, присвоенный при установке.
- Значения UUID'ов можно посмотреть, например, в таблице v_dialplans.
- Всем абонентским номерам будет присвоен пароль для регистрации «12345», пароль на голосовую почту и прочие сервисы — совпадающий с абонентским номером.
- Скрипт дописывает файлы построчно! Поэтому не забывайте удалять файлы перед каждым запуском скрипта или очищать их содержимое!
Абонентские номера и голосовая почта
$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False $nums=@{"1111"="5555555";"1112"="5555566"} [System.IO.File]::AppendAllText("d:\v_extensions.csv", "extension_uuid;domain_uuid;extension;number_alias;password;accountcode;effective_caller_id_name;effective_caller_id_number;outbound_caller_id_name;outbound_caller_id_number;emergency_caller_id_name;emergency_caller_id_number;directory_full_name;directory_visible;directory_exten_visible;limit_max;limit_destination;missed_call_app;missed_call_data;user_context;toll_allow;call_timeout;call_group;call_screen_enabled;user_record;hold_music;auth_acl;cidr;sip_force_contact;nibble_account;sip_force_expires;mwi_account;sip_bypass_media;unique_id;dial_string;dial_user;dial_domain;do_not_disturb;forward_all_destination;forward_all_enabled;forward_busy_destination;forward_busy_enabled;forward_no_answer_destination;forward_no_answer_enabled;follow_me_uuid;enabled;description;forward_caller_id_uuid;absolute_codec_string;forward_user_not_registered_destination;forward_user_not_registered_enabled;force_ping`r`n", $Utf8NoBomEncoding) [System.IO.File]::AppendAllText("d:\v_voicemails.csv", "domain_uuid;voicemail_uuid;voicemail_id;voicemail_password;greeting_id;voicemail_alternate_greet_id;voicemail_mail_to;voicemail_sms_to;voicemail_attach_file;voicemail_file;voicemail_local_after_email;voicemail_enabled;voicemail_description;voicemail_name_base64`r`n", $Utf8NoBomEncoding) Get-ADUser -Filter * -SearchBase "OU=Ekaterinburg,DC=dc,DC=domain,DC=local" -Properties Telephonenumber,sn,initials,cn|%{ if(-not $_.Telephonenumber -eq ""){ if($nums.Get_Item($_.Telephonenumber) -eq $null) {$outn = "5555555"} else {$outn = $nums.Get_Item($_.Telephonenumber)} $extension_uuid = (New-Guid).Tostring() $domain_uuid = "ffffffff-ffff-ffff-ffff-ffffffffffff" ## Заменить!!! $extension = $_.Telephonenumber $number_alias = "" $password = "12345" $accountcode = "172.18.253.1" $effective_caller_id_name = $_.sn + " " + $_.initials $effective_caller_id_number = $extension $outbound_caller_id_name = $outn $outbound_caller_id_number = $outn $emergency_caller_id_name = $effective_caller_id_name $emergency_caller_id_number = $extension $directory_full_name = $_.cn $directory_visible = "true" $directory_exten_visible = "true" $limit_max = "1" $limit_destination = "error/user_busy" $missed_call_app = "" $missed_call_data = "" $user_context = "172.18.253.1" $toll_allow = "domestic,international,local" $call_timeout = "30" $call_group = "" $call_screen_enabled = "false" $user_record = "" $hold_music = "local_stream://default" $auth_acl = "" $cidr = "" $sip_force_contact = "" $nibble_account = "" $sip_force_expires = "3600" $mwi_account = "" $sip_bypass_media = "" $unique_id = "" $dial_string = "" $dial_user = "" $dial_domain = "" $do_not_disturb = "" $forward_all_destination = "" $forward_all_enabled = "" $forward_busy_destination = "" $forward_busy_enabled = "" $forward_no_answer_destination = "" $forward_no_answer_enabled = "" $follow_me_uuid = "" $enabled = "true" $description = $_.sn + " " + $_.initials $forward_caller_id_uuid = "" $absolute_codec_string = "" $forward_user_not_registered_destination = "" $forward_user_not_registered_enabled = "" $force_ping = "" $csv="$extension_uuid;$domain_uuid;$extension;$number_alias;$password;$accountcode;$effective_caller_id_name;$effective_caller_id_number;$outbound_caller_id_name;$outbound_caller_id_number;$emergency_caller_id_name;$emergency_caller_id_number;$directory_full_name;$directory_visible;$directory_exten_visible;$limit_max;$limit_destination;$missed_call_app;$missed_call_data;$user_context;`"$toll_allow`";$call_timeout;$call_group;$call_screen_enabled;$user_record;$hold_music;$auth_acl;$cidr;$sip_force_contact;$nibble_account;$sip_force_expires;$mwi_account;$sip_bypass_media;$unique_id;$dial_string;$dial_user;$dial_domain;$do_not_disturb;$forward_all_destination;$forward_all_enabled;$forward_busy_destination;$forward_busy_enabled;$forward_no_answer_destination;$forward_no_answer_enabled;$follow_me_uuid;$enabled;$description;$forward_caller_id_uuid;$absolute_codec_string;$forward_user_not_registered_destination;$forward_user_not_registered_enabled;`"$force_ping`"`r`n" [System.IO.File]::AppendAllText("d:\v_extensions.csv", $csv, $Utf8NoBomEncoding) $voicemail_uuid = (New-Guid).Tostring() $voicemail_id = $extension $voicemail_password = $extension $greeting_id $voicemail_alternate_greet_id $voicemail_mail_to = "" $voicemail_sms_to $voicemail_attach_file $voicemail_file = "" $voicemail_local_after_email = "true" $voicemail_enabled = "false" $voicemail_description = $description $voicemail_name_base64 [System.IO.File]::AppendAllText("d:\v_voicemails.csv", "$domain_uuid;$voicemail_uuid;$voicemail_id;$voicemail_password;$greeting_id;$voicemail_alternate_greet_id;$voicemail_mail_to;$voicemail_sms_to;$voicemail_attach_file;$voicemail_file;$voicemail_local_after_email;$voicemail_enabled;$voicemail_description;$voicemail_name_base64`r`n", $Utf8NoBomEncoding)}}
Маршруты и диалпланы
$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False [System.IO.File]::AppendAllText("d:\v_destinations.csv", "domain_uuid;destination_uuid;dialplan_uuid;fax_uuid;destination_type;destination_number;destination_number_regex;destination_caller_id_name;destination_caller_id_number;destination_cid_name_prefix;destination_context;destination_app;destination_data;destination_enabled;destination_description;destination_accountcode`r`n", $Utf8NoBomEncoding) [System.IO.File]::AppendAllText("d:\v_dialplans.csv", "domain_uuid;dialplan_uuid;app_uuid;dialplan_context;dialplan_name;dialplan_number;dialplan_continue;dialplan_order;dialplan_enabled;dialplan_description`r`n", $Utf8NoBomEncoding) [System.IO.File]::AppendAllText("d:\v_dialplan_details.csv", "domain_uuid;dialplan_uuid;dialplan_detail_uuid;dialplan_detail_tag;dialplan_detail_type;dialplan_detail_data;dialplan_detail_break;dialplan_detail_inline;dialplan_detail_group;dialplan_detail_order`r`n", $Utf8NoBomEncoding) $nums="1111=5555555;1112=5555566" $nums.Split(";")|%{ $innum = $_.Split("=")[0] $outnum = $_.Split("=")[1] $domain_uuid = "ffffffff-ffff-ffff-ffff-ffffffffffff" ## Заменить!!! $destination_uuid = (New-Guid).Tostring() $dialplan_uuid = (New-Guid).Tostring() $fax_uuid $destination_type = "inbound" $destination_number = "343$outnum" $destination_number_regex = "^(343$outnum)$" $destination_caller_id_name $destination_caller_id_number $destination_cid_name_prefix $destination_context = "public" $destination_app $destination_data $destination_enabled = "true" $destination_description = "$outnum-$innum" $destination_accountcode [System.IO.File]::AppendAllText("d:\v_destinations.csv", "$domain_uuid;$destination_uuid;$dialplan_uuid;$fax_uuid;$destination_type;$destination_number;$destination_number_regex;$destination_caller_id_name;$destination_caller_id_number;$destination_cid_name_prefix;$destination_context;$destination_app;$destination_data;$destination_enabled;$destination_description;$destination_accountcode`r`n", $Utf8NoBomEncoding) $app_uuid = "ffffffff-ffff-ffff-ffff-ffffffffffff" ## Заменить!!! $dialplan_context = "public" $dialplan_name = $destination_number $dialplan_number = $destination_number $dialplan_continue = "false" $dialplan_order = "100" $dialplan_enabled = "true" $dialplan_description = $destination_description [System.IO.File]::AppendAllText("d:\v_dialplans.csv", "$domain_uuid;$dialplan_uuid;$app_uuid;$dialplan_context;$dialplan_name;$dialplan_number;$dialplan_continue;$dialplan_order;$dialplan_enabled;$dialplan_description`r`n", $Utf8NoBomEncoding) $dialplan_detail_break $dialplan_detail_inline $dialplan_detail_group $dialplan_detail_uuid = (New-Guid).Tostring() $dialplan_detail_tag = "condition" $dialplan_detail_type = "destination_number" $dialplan_detail_data = "^(343$outnum)$" $dialplan_detail_order = 20 [System.IO.File]::AppendAllText("d:\v_dialplan_details.csv", "$domain_uuid;$dialplan_uuid;$dialplan_detail_uuid;$dialplan_detail_tag;$dialplan_detail_type;$dialplan_detail_data;$dialplan_detail_break;$dialplan_detail_inline;$dialplan_detail_group;$dialplan_detail_order`r`n", $Utf8NoBomEncoding) $dialplan_detail_uuid = (New-Guid).Tostring() $dialplan_detail_tag = "action" $dialplan_detail_type = "transfer" $dialplan_detail_data = "$innum XML 172.18.253.1" $dialplan_detail_order = 30 [System.IO.File]::AppendAllText("d:\v_dialplan_details.csv", "$domain_uuid;$dialplan_uuid;$dialplan_detail_uuid;$dialplan_detail_tag;$dialplan_detail_type;$dialplan_detail_data;$dialplan_detail_break;$dialplan_detail_inline;$dialplan_detail_group;$dialplan_detail_order`r`n", $Utf8NoBomEncoding) }
Проверка связи на рандомно выбранные номера показала работоспособность импорта.
Настройка шлюза
Accounts→Gateways
Gateway: 172.16.253.3
Username: 3435555555
Password: not-used
From User: 3435555555
From Domain: 172.16.253.3
Proxy: 172.16.253.3
Register: False
Caller ID In From: TrueОбратите внимание!В документации по FusionPBX недвусмысленно указывается, что при выполнении настроек поля, выделенные жирным текстом, обязательны для заполнения.Настройка ACL
Однако я, по непонятной мне причине, жирность поля Proxy не углядел и значение ему не выставил. В итоге получил работающие входящие внешние вызовы, но не работающие исходящие наружу. Командаsofia status gateway ffffffff-ffff-ffff-ffff-ffffffffffffне показывала аномалий настройки и даже показывала назначенное значение поля Proxy, соответствующее значению Gateway. Точно такой же вывод команды при точно таких же настройках демонстрировал «голый» FreeSWITCH в предыдущей инсталляции, и при этом совершенно беспроблемно позволял совершать исходящие вызовы наружу.
FusionPBX же заработал только после явного указания значения Proxy.
*ffffffff-ffff-ffff-ffff-ffffffffffff— UUID шлюза
Выполнил настройки в соответствии со шпаргалкой и тут же получил сломавшиеся внутренние вызовы. Логи показывали, что аппараты почему-то оказались в контексте external, соответственно, обрабатывались «не своим» диалпаном, от чего вызов завершался ошибкой ROUTE_NOT_FOUND.
Лирическое отступление
Юмор ситуации заключался еще и в том, что эта «беда» стряслась у меня до того, как я обнаружил обязательность заполнения поля Proxy в настройках шлюза. И стоило мне настроить ACL, как начинали приходить вызовы снаружи, но ломались внутренние. И как я ни играл с применением ACL и с их значениями, итог был один: или звонки снаружи внутрь, или внутренние звонки без звонков изнутри наружу и снаружи внутрь.
Как выяснилось, настройка ACL была выполнена неправильно!
Важно!
ACL-списки только для сетей и доменов провайдеров.
Ваших собственных сетей и доменов в них быть не должно.
Список domains должен быть по умолчанию deny.
Сами правила должны быть разрешающими и в них должен быть прописан IP-адрес шлюза провайдера с маской /32, поле domain заполнять не нужно.
Итак, выполняем настройку ACL: Advanced→Access Controls→domains. Удаляем существующие правила, создаем новое:
Type: allow
CIDR: 172.16.253.3/32
Domain:
Description: default SIP-trunkПо окончании жмем Save, далее чтобы новые ACL вошли в силу: Status→Sip Status и жмем Reload ACL.
Системные переменные
Advanced→Default Settings
Здесь мы укажем выданный нам провайдером внешний IP-адрес, который мы использовали при настройке 1:1 NAT в шпаргалке, укажем телефонный код региона, язык и голос для голосовых ответов, тип гудка.
Раздел Defaults:
default_areacode: 343
default_language: ru
default_dialect: RU
default_voice: elena
ringback: $${ru-ring}
transfer_ringback: $${ru-ring}Раздел IP Addressexternal_rtp_ip: 172.16.160.154
external_sip_ip: 172.16.160.154Раздел SIP Profile: Internalinternal_auth_calls: trueСобственно говоря, именно эта переменная в значении true отвечает за считывание настроек абонентского номера и передачу из него значений ${outbound_caller_id_number} и ${outbound_caller_id_name}. Чтобы эта переменная имела силу, необходимо, чтобы была отключена авторизация внутренних абонентских номеров по ACL. По умолчанию, из коробки, это сделано и так: ACL-авторизация отсутствует, вместо нее используется Digest (по абонентскому номеру и паролю): internal_auth_calls: true.Важно!Исходящие маршруты
Чтобы корректно определялись прямые городские номера, присвоенные внутренним в настройках через поля Outbound Caller ID Name и Outbound Caller ID Number, необходимо выполнение трех условий:
- Отсутствие ACL-авторизации внутренних абонентов
- Включенная Digest-авторизация в настройках SIP-профиля:
internal_auth_calls: true- Наличие в настройках шлюза:
Caller ID In From: True
Dialplan→Outbound Routes
Пожалуй, это единственный пункт настроек, не подвергшийся переосмыслению.
Подробно разбирать его не стану. Отмечу лишь, что были использованы следующие регулярные выражения для различных направлений:
- Внутригород:
^(\d{7})$(набор прямого городского 7-значного номера без всяких префиксов в виде нулей, девяток и прочего). - Внутригород с кодом города:
^(8343\d{7})$(набор городского 7-значного номера с префиксом 8343). - Сотовые:
^(89\d{9})$(звонок на сотовый с префиксом 8, что является стандартом де-факто) - Межгород:
^(8\d{10})$(междугородний звонок, так же привычные: 8, код населенного пункта, номер абонента) - Международный:
^(810\d+)$(стандартный же префикс 810, далее код страны, код территории, номер абонента).
Для всех маршрутов было отредактированы два тега action типа set:
effective_caller_id_name=${default_areacode}${outbound_caller_id_name} effective_caller_id_number=${default_areacode}${outbound_caller_id_number} таким образом, чтобы передаваемый оператору номер вызывающего абонента включал в себя код города.Лечим сброс вызова через 90-100 секунд на аппаратах Cisco
Как было отмечено выше, сюрпризом стал обрыв установленного соединения через 90-100 секунд на всех аппаратах Cisco 7945g. Подкручивание всех таймеров с более или менее релевантным названием переменной в конфиге аппаратов результата не дало. Курение логов в консоли FreeSWITCH выявило Session Expire.
Гуглинг, кроме матов в сторону нежелания аппаратов Cisco нормально работать хоть с кем-то, кроме Call Manager'а, выявил, что такое поведение вполне может быть вылечено отключением переменной
aggressive-nat-detection.Advanced→SIP Profile
aggressive-nat-detection
Value: true
Enabled: False Русификация голосового откликаНам потребуются файлы озвучки, созданные альтруистичными профессионалами.
Качаем:
files.freeswitch.org/releases/sounds/freeswitch-sounds-ru-RU-elena-48000-1.0.51.tar.gz
files.freeswitch.org/releases/sounds/freeswitch-sounds-ru-RU-elena-32000-1.0.51.tar.gz
files.freeswitch.org/releases/sounds/freeswitch-sounds-ru-RU-elena-16000-1.0.51.tar.gz
files.freeswitch.org/releases/sounds/freeswitch-sounds-ru-RU-elena-8000-1.0.51.tar.gz
Каждый из архивов содержит готовую структуру каталогов. Каждый из архивов распаковываем в /usr/share/freeswitch/sound/
Поскольку ранее мы уже выполнили настройку значений по умолчанию, с этого момента файлы русской озвучки подхватятся и начнут воспроизводиться без дополнительных движений. Единственное, что вам, возможно, придется сделать (мне пришлось), так это во всех четырех папках ru/RU/elena/voicemail/_bitrate_/ переименовать файл vm-not_available_no_voicemail.wav и дать ему новое имя vm-no_answer_no_vm.wav. Только после этой манипуляции я получил голосовой отклик на событие недоступности вызываемого абонента.
P.S.: Как и предыдущая часть, данный текст был написан исключительно с целью документирования возникающих сложностей и путей их решения. Несмотря на то, что текст так же освещает быстрый старт с нуля все того же FreeSWITCH'а, пусть и с «графическим лицом», считаю, что текст самодостаточный и является неким форком, и имеет право на самостоятельную жизнь. Предыдущая часть так же сохраняет некоторую ценность благодаря описанной настройке сетевого оборудования. Некорректные настройки в том тексте будут исправлены и приведены к тем, что используются в данной статье.
