В ту же реку
Относительно недавно я написал себе шпаргалку по настройке 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, то есть косяк на моей стороне. Плюс ко всему, я вдруг совершенно завис с задачей переадресации вызовов. А вишенкой на торте стал вынос
На этом этапе я осознал, что достиг предела своей компетенции в области телефонии и взял тайм-аут на то, чтобы все в голове утряслось и уложилось. Этому решению так же очень могуче способствовало общее утомление после совершенно диких двух рабочих недель без выходных и с ненормированным рабочим графиком, которые последовали сразу после заезда на новое место размещения организации.
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 недвусмысленно указывается, что при выполнении настроек поля, выделенные жирным текстом, обязательны для заполнения.
Однако я, по непонятной мне причине, жирность поля Proxy не углядел и значение ему не выставил. В итоге получил работающие входящие внешние вызовы, но не работающие исходящие наружу. Командаsofia status gateway ffffffff-ffff-ffff-ffff-ffffffffffff
не показывала аномалий настройки и даже показывала назначенное значение поля Proxy, соответствующее значению Gateway. Точно такой же вывод команды при точно таких же настройках демонстрировал «голый» FreeSWITCH в предыдущей инсталляции, и при этом совершенно беспроблемно позволял совершать исходящие вызовы наружу.
FusionPBX же заработал только после явного указания значения Proxy.
*ffffffff-ffff-ffff-ffff-ffffffffffff
— UUID шлюза
Настройка ACL
Выполнил настройки в соответствии со шпаргалкой и тут же получил сломавшиеся внутренние вызовы. Логи показывали, что аппараты почему-то оказались в контексте external, соответственно, обрабатывались «не своим» диалпаном, от чего вызов завершался ошибкой ROUTE_NOT_FOUND.
Как выяснилось, настройка 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 Address
external_rtp_ip: 172.16.160.154
external_sip_ip: 172.16.160.154
Раздел SIP Profile: Internal
internal_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-значного номера без всяких префиксов в виде нулей, девяток и прочего). - Сотовые:
^(89d{2}d{7})$
(звонок на сотовый с префиксом 8, что является стандартом де-факто) - Межгород:
^(8d{10})$
(междугородний звонок, так же привычные: 8, код населенного пункта, номер абонента) - Международный:
^(810d+)$
(стандартный же префикс 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'а, пусть и с «графическим лицом», считаю, что текст самодостаточный и является неким форком, и имеет право на самостоятельную жизнь. Предыдущая часть так же сохраняет некоторую ценность благодаря описанной настройке сетевого оборудования. Некорректные настройки в том тексте будут исправлены и приведены к тем, что используются в данной статье.
Автор: LazyFao