Ровно год назад к нам обратились бывшие коллеги, с предложением принять участие в модификации движка VoIP оператора связи. Задача сводилась к полной переделке личного кабинета, обеспечению масштабирования системы, создания системы биллинга, LCR, мониторинга расходов пользователей, контроля длительности разговоров, аналитики по звонкам. История закончилась печально, т.к. заложенный нами расширенный функционал системы якобы не соответствовал ТЗ, никак не формализованному на бумаге и находящемуся только в головах менеджеров оператора. В связи с тем, что за разработанный функционал, который заказчику очень понравился, менеджеры платить не захотели, отношения мы разорвали. NDA и договора у нас не было, поэтому посоветовавшись с коллегами мы решили часть наработок выложить в свободный доступ. Я думаю, что это будет серия статей. И начнём пожалуй с базовых вещей и архитектуры.


Каждый администратор, который хотя бы раз в жизни имел дело с IP телефонией знает, что услуги IP телефонии могут предоставляться конечному абоненту несколькими способами:


  • Аренда номера у оператора связи. В данном случае пользователю выдаётся личный логин/пароль к которому "привязан" телефонный номер. При использовании связки логин/пароль для подключения к оборудованию оператора, пользователь может принимать звонки на арендованный номер и совершать звонки другим абонентам с этого номера.
  • Аренда блока номеров и подключение через SIP Trunk(транк). При подключении через SIP Trunk, оператор связи отправляет на заранее согласованный IP адрес все вызовы, приходящие на арендованные у этого оператора номера. В зависимости от способа организации SIP транка, может использоваться или не использоваться парольная аутентификация. В данном случае установка правильного CallerID, который будет видеть вызываемая сторона при исходящих звонках лежит на плечах администратора IP АТС.
  • Покупка DID номеров у операторов связи без покупки исходящего трафика. Оператор просто перенаправляет вызов, пришедший на данный номер, на указанный сервер.
  • Покупка исходящего трафика. Есть довольно много операторов связи продающих исходящий трафик. При этом в зависимости от тарифного плана передаваемый вызываемой стороне CallerID может передаваться или теряться при прохождении через цепочку операторов терминирующих(доставляющих до конечного клиента) трафик нужного направления. Довольно часты случаи, когда звонок на московский номер через московского оператора может прийти скажем с лондонского номера или с бельгийского, потому что маршрут был дешевле.

Компании имеющие представительства в нескольких странах или желающие получить присутствие в другой стране, могут купить, для удобства своих клиентов, телефонный номер скажем в Великобритании и обрабатывать поступающие звонки в Москве. При этом оператор предоставивший им такой номер может не заниматься доставкой звонков в Великобританию. Некоторые страны, например республика Беларусь, не предоставляют номера нерезидентам.


Итак, исходим из следующих начальных требований:


  1. Сервер для приёма вызовов может стоять где угодно
  2. Количество серверов не ограничено
  3. Количество операторов связи не ограничено
  4. Приём вызова может происходить при любом типе аренды номера (логин/пароль или транк)
  5. Вызов с определенным CallerID должен происходить только с того сервера и только с той учётной записи, к которой на данный момент привязан этот CallerID, либо через оператора позволяющего менять CallerID
  6. Вызовы между соседними серверами должны проходить прозрачно без потери информации о вызываемом абоненте. Например переадресация лондонского номера на внутренний, мобильный или стационарный телефон в другом регионе
  7. Для исходящих вызовов, в зависимости от тарифного плана, должен выбираться наиболее дешевый маршрут с резервированием через более дорогие. В целях контроля качества связи необходимо обеспечить мониторинг ABR, ASR(статистические параметры, определяющие качество связи в заданном направлении через определённый узел телефонии)
  8. Если есть внешние пользователи использующие систему, они должны видеть в realtime свой текущий баланс и совершаемые на данный момент разговоры
  9. При достижении значения текущего баланса менее определенной суммы пользователь должен получать голосовое сообщение об отрицательном балансе с прекращением разговора
  10. Пользователь должен иметь возможность совершать звонки через веб браузер(WebRTC)
  11. Сервера обработки входящих и исходящих вызовов, а также сервера обслуживающие абонентов могут быть разделены
  12. Для аналитики и маршрутизации вызова нужна геолокация входящих и исходящих звонков

Небольшая ремарка: описываемая конфигурация является универсальной, но в большей степени подходит для сервисов имеющих клиенто-ориентированное направление, например call центров, в случаях когда необходима персонализация клиентов и привязка клиента к одному менеджеру или группе менеджеров. Большинство описываемых механизмов универсально и может эффективно использоваться в других конфигурациях.


Требований довольно много, так с чего начать? Принимать и совершать звонки будет Asterisk, подготовкой вызовов будет заниматься Application Server на python, все рабочие данные мы будем хранить в MariaDB, а большинство логики будет реализовано в виде процедур. Это позволит нам максимально дистанцировать логику работы asterisk от лишних правил в диалплане и обеспечить масштабируемость совместно с унификацией конфигурации.


Трафик


Определимся с нашим трафиком. Нам необходимо присутствие скажем на Кипре, Америке, Великобритании и России. С точки зрения Российского трафика выгоднее работать через Российских операторов связи. Например Весткол, IPPort и прочих. Российские(московские) операторы также по довольно приемлемой цене предоставляют в аренду телефонные номера в кодах 495 и 499. Присутствие на Кипре, в Америке и Великобритании мы можем обеспечить себе купив номера у Zadarma, Multilel или кого-то другого. Так как международные вызовы у Российских операторов довольно дороги, то можно купить трафик у зарубежных операторов для примера VoiceBuy или VoxBeam.


При подключении к оператору связи в варианте "логин/пароль" звонки на нашу АТС будут приходить с IP адреса сервера оператора, на котором происходит регистрация. Однако в случае исходящих звонков адрес сервера может быть другим.


При подключении к оператору через SIP транк входящие звонки могут приходить с нескольких серверов из пула оператора, а исходящие звонки осуществляются по единому DNS имени, на котором в большинстве случаев находится несколько IP адресов. Как мы уже определились ранее, оператор может иметь возможность замены CallerID для исходящего вызова, что может регулироваться тарифным планом либо договором. Замена CallerID(подмена номера) в России при исходящих вызовах запрещена. С зарубежными операторами дело обстоит намного проще и замена CallerID вполне себе поддерживается.


Создаём базу данных.


Большинство публикуемых в данной статье данных и таблиц являются выдержками из ядра реальной базы данных, за исключением номеров телефонов и IP адресов


В связи с тем, что у нас имеются мультивалютные операторы связи, в таблице операторов связи должна присутствовать ссылка на таблицу валют. Чуть позже мы рассмотрим возможность мультивалютного пересчёта тарифов операторов.


Таблица валют
CREATE TABLE `currency` (
    `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
    `iso` CHAR(3) NOT NULL DEFAULT '' COLLATE 'utf8mb4_unicode_ci',
    `name_en` VARCHAR(200) NOT NULL DEFAULT '' COLLATE 'utf8mb4_unicode_ci',
    `name_ru` VARCHAR(200) NOT NULL DEFAULT '' COLLATE 'utf8mb4_unicode_ci',
    `numcode` INT(3) UNSIGNED ZEROFILL NULL DEFAULT NULL COMMENT 'numcode for country',
    PRIMARY KEY (`id`),
    UNIQUE INDEX `iso` (`iso`)
)
COMMENT='Справочник валют, iso кодов и наименований'
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB
AUTO_INCREMENT=1
;

currency


id iso name_en name_ru numcode
96 RUB Russian Ruble Российский рубль 643
122 USD US Dollar Доллар США 840
156 EUR Euro Евро 978

Таблица операторов
CREATE TABLE `providers` (
    `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
    `providername` VARCHAR(50) NULL DEFAULT '0' COLLATE 'utf8mb4_unicode_ci',
    `currency_id` INT(10) UNSIGNED NOT NULL DEFAULT '0',
    `noncli_prefix` VARCHAR(10) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
    `cli_prefix` VARCHAR(10) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
    `premcli_prefix` VARCHAR(10) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
    `cli_allowed` ENUM('Y','N') NOT NULL DEFAULT 'N' COLLATE 'utf8mb4_unicode_ci',
    `dynamic_calls` CHAR(1) NULL DEFAULT 'N' COLLATE 'utf8mb4_unicode_ci',
    `append_plus` TINYINT(1) NOT NULL DEFAULT '1',
    `dynamic_calls_caller_id` VARCHAR(30) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
    PRIMARY KEY (`id`),
    INDEX `FK_providers_currency` (`currency_id`),
    CONSTRAINT `FK_providers_currency` FOREIGN KEY (`currency_id`) REFERENCES `currency` (`id`)
)
COMMENT='справочник операторов и префиксов для различных типов сервисов'
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB
AUTO_INCREMENT=1
;

providers


id providername currency_id noncli_prefix cli_prefix premcli_prefix cli_allowed dynamic_calls append_plus dynamic_calls_caller_id
3 Zadarma 96 N N 1
4 Multitel 122 N N 1
7 WestCall 96 Y N 0 74951815283
8 VoxBeam 122 0011103 0011101 0011102 Y Y 1
11 VoiceBuy 122 9991 9992 9993 Y Y 1
16 IPPort 96 N N 1

Итак. Операторы.


Обычно у зарубежных операторов 2-3 тарифа по которым они доставляют трафик до вызываемого абонента. Про различия CLI и NonCLI техник можно прочитать здесь.


Для выбора тарифного плана обычно используется префикс (noncli_prefix, cli_prefix, premcli_prefix) указываемый перед вызываемым номером в процессе формирования строки вызова.


Описание полей:


  • providername — наименование оператора
  • currency_id — валюта оператора
  • noncli_prefix — тариф без поддержки CLI
  • cli_prefix — тариф с поддержкой CLI
  • premcli_prefix — тариф премиального уровня
  • cli_allowed — указывает на то, что оператор позволяет менять CLI для исходящих вызовов
  • dynamic_calls — указывает, что оператор позволяет осуществлять звонки по любым направлениям с любым CLI.
  • append_plus — добавлять или не добавлять "+" перед вызываемым номером. Часть операторов требует "+", часть нет.
  • dynamic_calls_caller_id — CID для оператора по умолчанию.

В случае с WestCall и другими операторами предоставляющими подключение через SIP транк, вы покупаете пул номеров и осуществляя вызовы можете менять исходящий номер на любой номер из данного пула. IPPort как и прочие операторы, при использовании схемы логин/пароль с единственным купленным номером, не позволяют менять исходящий номер. Операторы VoxBeam и VoiceBuy используются для исходящих звонков для любых направлений и позволяют менять исходящий номер на любой другой. Правда здесь есть одно но! И заключается оно в том, что конечные операторы связи доставляющие звонок до вызываемого абонента, могут отклонить вызов или поменять номер вызывающего абонента на свой, если вызывающий номер соответствует внутреннему номеру страны или местности. Т.е. скажем при звонке из России в Украину(Киев) мы меняем CallerID на украинский номер в Киеве и оператор связи через которого идёт вызов может данный звонок просто "отбить", т.к. он не соответствует внутренней политике обработки звонков "Звонок через внешний входящий международный транк не может содержать внутренних номеров". Операторы Zadarma и Multitel у нас будут использоваться в качестве поставщиков номеров и исходящий трафик мы через них отправлять не будем, хотя при желании могли бы.


Важным аспектом обработки вызова является безопасность. Довольно часто встречаются ситуации, когда неверно настроенный сервер IP телефонии позволяет пропускать сквозь себя чужие звонки. Для обеспечения безопасности контуров обрабатывающих входящие и исходящие звонки, дополнительно к разделению операторов по различным контекстам, стоит создать базу IP адресов операторов связи предоставляющих нам услуги связи. Во первых это позволит настроить экспорт адресов в firewall и убрать лишние попытки "прозвона" через нас, во вторых даст более полную информацию о способах подключения к операторам связи.


Таблица IP адресов операторов связи
CREATE TABLE `providers_ips` (
    `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
    `provider_id` INT(11) UNSIGNED NOT NULL DEFAULT '0',
    `ipaddress` INT(11) UNSIGNED NOT NULL DEFAULT '2130706433',
    `domainname` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
    `direction` ENUM('IN','OUT','IN/OUT') NOT NULL DEFAULT 'IN' COLLATE 'utf8mb4_unicode_ci',
    `proto` ENUM('SIP','IAX2','H323') NOT NULL DEFAULT 'SIP' COLLATE 'utf8mb4_unicode_ci',
    PRIMARY KEY (`id`),
    INDEX `FK_providers_ips_providers` (`provider_id`),
    CONSTRAINT `FK_providers_ips_providers` FOREIGN KEY (`provider_id`) REFERENCES `providers` (`id`)
)
COMMENT='Список адресов операторов связи используемых в работе. direction указывает направление для входящих/исходящих звонков'
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB
AUTO_INCREMENT=1
;

providers_ips


id provider_id ipaddress domainname direction proto
1 8 2130706433 sbc.voxbeam.com OUT SIP
2 8 1607694320 95.211.119.240 IN SIP
7 3 2130706433 sip.zadarma.com OUT SIP
16 3 3106773121 proxy-1.fr.zadarma.com IN SIP
17 3 3106773122 proxy-2.fr.zadarma.com IN SIP
31 16 1506852360 89.208.190.8 IN SIP
33 16 1506852357 89.208.190.5 IN SIP
34 16 1506852354 sip.n1.ipport.net OUT SIP
35 11 2991415097 sip.voicebuy.com OUT SIP
42 4 3514573416 209.124.34.104 OUT SIP
44 7 3277775106 195.94.225.2 IN/OUT SIP
51 4 3514573417 209.124.34.105 IN SIP
52 4 3514573446 209.124.34.134 IN SIP

Описание полей:


  • provider_id указывает привязку адреса к оператору из таблицы providers
  • ipaddress является Int представлением fqdn адреса сервера оператора для конвертации при помощи встроенной функции INET_NTOA.
  • domainname указывает fqdn имя сервера оператора осуществляющего обработку вызова.
  • direction указывает направление вызова для указанного IP адреса(OUT только на выход, IN только на вход, IN/OUT и на вход и на выход)
  • proto указывает протокол по которому происходит взаимодействие с оператором связи(SIP/IAX2/H323)

Таким образом связка оператора и адресов будет выглядеть следующим образом.


vw_providers


id providername proto aa direction
16 IPPort SIP 89.208.190.5 [89.208.190.5] IN
16 IPPort SIP sip.n1.ipport.net [89.208.190.2] OUT
16 IPPort SIP 89.208.190.8 [89.208.190.8] IN
4 Multitel SIP 80.97.55.105 [80.97.55.105] IN
4 Multitel SIP 209.124.34.104 [209.124.34.104] OUT
4 Multitel SIP 41.218.96.199 [41.218.96.199] IN
11 VoiceBuy SIP sip.voicebuy.com [178.77.95.57] OUT
8 VoxBeam SIP sbc.voxbeam.com [127.0.0.1] OUT
8 VoxBeam SIP 95.211.119.240 [95.211.119.240] IN
7 WestCall SIP 195.94.225.2 [195.94.225.2] IN/OUT
3 Zadarma SIP proxy-1.fr.zadarma.com [185.45.152.129] IN
3 Zadarma SIP proxy-3.ri.zadarma.com [195.122.19.11] IN
3 Zadarma SIP siplv.zadarma.com [195.122.19.17] IN
3 Zadarma SIP proxy-8.fr.zadarma.com [185.45.152.136] IN
3 Zadarma SIP sip.zadarma.com [127.0.0.1] OUT
3 Zadarma SIP siplv1.zadarma.com [195.122.19.17] IN
3 Zadarma SIP mediarelay-1.zadarma.com [185.45.152.162] IN

Для чего это нужно ?


  1. Входящий вызов на номер телефона, арендованного нами у конкретного оператора, должен приходить из пула IP адресов серверов этого оператора. Поэтому в момент входящего вызова стоит проверить номер привязанный к оператору с таблицей IP адресов оператора
  2. Оператор может иметь несколько серверов обрабатывающих исходящие вызовы от нашего сервера, поэтому это нужно учесть при формировании очереди дозвона
  3. Данная схема позволяет мгновенно получать строку вызова. При этом замена domainname на имя транка определенного в sip.conf или iax.conf не нарушает работу системы
  4. Номера могут быть "размазаны" по нескольким серверам для отказоустойчивости, а могут быть жёстко закреплены за каждым сервером. Нет смысла обслуживать вызов на сервере, к которому этот номер не прикреплён.

Попробуем получить строку вызова для номера 74957777777.


Запрос строки вызова
SELECT CONCAT_WS('/',
    pips.proto,
    pips.domainname, CONCAT(IF(p.append_plus IS TRUE,'+',''), IFNULL(p.cli_prefix,''),74957777777)) AS dial_string
FROM providers_ips AS pips
LEFT JOIN providers AS p ON p.id=pips.provider_id
WHERE pips.direction IN ('OUT', 'IN/OUT')

Запрос строки дозвона


dial_string
SIP/sip.zadarma.com/74957777777
SIP/westcall/74957777777
SIP/sbc.voxbeam.com/001110174957777777
SIP/sip.voicebuy.com/999274957777777
SIP/sip.n1.ipport.net/+74957777777

Как мы видим, мы получили готовые данные для дозвона, которые нужно передать приложению Dial сервера Asterisk. На этом пока остановимся.


Для привязки арендованных у оператора связи номеров, нам необходимо задать список серверов на которых данные номера будут обслуживаться.


Таблица серверов
CREATE TABLE `servers` (
    `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
    `servername` VARCHAR(50) NULL DEFAULT '0' COLLATE 'utf8mb4_unicode_ci',
    `location` INT(11) NOT NULL DEFAULT '0' COMMENT 'country code where server is located',
    `ipaddress` INT(11) NOT NULL DEFAULT '2130706433' COMMENT 'localhost by default',
    `comment` VARCHAR(50) NULL DEFAULT '0' COLLATE 'utf8mb4_unicode_ci',
    `sip` VARCHAR(50) NULL DEFAULT NULL COMMENT 'SIP URI' COLLATE 'utf8mb4_unicode_ci',
    `iax2` VARCHAR(50) NULL DEFAULT NULL COMMENT 'IAX2 address and user binding if rsa keys used' COLLATE 'utf8mb4_unicode_ci',
    `iax2control` VARCHAR(50) NULL DEFAULT NULL COMMENT 'IAX2 address and control user binding for pair and route control' COLLATE 'utf8mb4_unicode_ci',
    `protocol` ENUM('SIP','IAX2') NOT NULL DEFAULT 'SIP' COLLATE 'utf8mb4_unicode_ci',
    PRIMARY KEY (`id`)
)
COMMENT='Информация о серверах системы'
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB
AUTO_INCREMENT=1
;

servers


id servername location ipaddress comment sip iax2 iax2control protocol
0 undefined 0 2130706433 0 SIP
1 asterisk-macomnet 189 3557129729 0 SIP/212.5.126.1 IAX2/macomnet@macomnet IAX2/macomnetcontrol@macomnet IAX2
2 asterisk-msm 189 1506807809 0 SIP/89.208.16.1 IAX2/msm@msm IAX2/msmcontrol@msm IAX2
5 asterisk-corbina 189 1399234817 0 SIP/83.102.161.1 IAX2/corbina@corbina IAX2/corbinacontrol@corbina IAX2

Описание полей:


  • servername — внутреннее имя сервера
  • location — географическая зона в которой находится сервер. В данном случае Россия
  • ipaddress — реальный IP адрес сервера
  • sip — SIP URL для соединения с сервером
  • iax2 — Связка RSA ключа с идентификатором сервера для переадресации звонков между серверами
  • iax2control — специальный канал команд для удалённого управления серверами и межсерверного взаимодействия. Длина команды до 80 символов
  • protocol — информационное поле для сортировки по типу межсерверной связи

Для чего нужна эта таблица и почему она такая странная? Для межсерверного соединения серверов asterisk удобнее всего использовать протокол IAX. Причин несколько:


  1. весь транзитный трафик между серверами можно зашифровать
  2. в отличие от SIP протокола — более простое прохождение через Firewall операторов связи(для работы нужен всего 1 UDP порт)
  3. невозможность установки соединения злоумышленником с сервером без наличия ключей шифрования
  4. возможность передавать данные между серверами. например сессионные переменные. В SIP протоколе можно добавлять данные в заголовки сессионных пакетов, но зачастую лишние заголовки просто "вычищаются" промежуточными серверами из пакетов. В IAX протоколе можно перед установкой соединения передать необходимые переменные в потоке
  5. чёткая привязка хоста и связки пользователей/ключей к контексту на сервере

примерный конфиг iax.conf на одном из серверов
[macomnet]
type=user
username=macomnet
auth=rsa
inkeys=asterisk-corbina:asterisk-msm
context=incoming_dialer
encryption=yes
qualify=yes
disallow=all
allow=gsm
allow=ulaw

[msm]
type=peer
host=89.208.16.1
username=msm
auth=rsa
outkey=asterisk-macomnet
encryption=yes
qualify=yes
disallow=all
allow=gsm
allow=ulaw
trunk=yes

[corbina]
type=peer
host=83.102.161.1
username=corbina
auth=rsa
outkey=asterisk-macomnet
encryption=yes
qualify=yes
disallow=all
allow=gsm
allow=ulaw
trunk=yes

С серверами на начальном этапе разобрались. Теперь необходимо сделать привязку арендуемых номеров к определенному серверу. Это необходимо для корректной маршрутизации вызовов.


Таблица номеров

CREATE TABLE numbers_pool (
id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
number VARCHAR(50) NULL DEFAULT '0' COMMENT 'Арендуемый номер' COLLATE 'utf8mb4_unicode_ci',
provider_id INT(11) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'Прикрепление номера к оператору',
server_id INT(11) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'Прикрепление номера к серверу',
enabled BIT(1) NOT NULL DEFAULT b'1' COMMENT 'Включение/отключение номера',
direction ENUM('IN','OUT','IN/OUT') NULL DEFAULT 'IN' COMMENT 'Направление звонка по умолчанию' COLLATE 'utf8mb4_unicode_ci',
virtualflag BIT(1) NOT NULL DEFAULT b'1' COMMENT 'Виртуальный или реальный номер',
echotest TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'Тестирование номера 0/1 — выключено/включено',
PRIMARY KEY (id),
UNIQUE INDEX number (number),
INDEX FK_number_assignment_copy_providers (provider_id),
INDEX FK_number_assignment_servers (server_id),
INDEX number btree (number),
CONSTRAINT FK_provider FOREIGN KEY (provider_id) REFERENCES providers (id),
CONSTRAINT FK_server FOREIGN KEY (server_id) REFERENCES servers (id)
)
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB
AUTO_INCREMENT=1
;


numbers_pool


id number provider_id server_id enabled direction virtualflag echotest
125 37167859001 4 1 1 IN 1 0
126 37167859002 4 1 1 IN 1 0
278 4971122954000 4 1 0 IN 1 0
279 74951815000 7 1 1 IN/OUT 0 0
280 74951815001 7 1 1 IN/OUT 0 0
281 74951815002 7 1 1 IN/OUT 0 0
426 74951339501 3 2 0 IN/OUT 1 0
427 74951339502 3 1 0 IN/OUT 1 0
515 74957952301 16 1 1 IN/OUT 0 0
516 74957952302 16 2 1 IN/OUT 0 0
529 442038070121 11 1 1 IN 1 0
531 442038070123 11 1 1 IN 1 0

Описание полей:


  • number — номер в международном формате
  • provider_id — привязка номера к оператору
  • server_id — привязка номера к серверу
  • enabled — обслуживать вызовы на данном номере или нет
  • direction — обслуживаемые номером направления вызова
  • virtualflag — номер является "реальным" или "виртуальным". "Реальные" номера привязаны к диалпирам, "виртуальные" не имеют привязки
  • echotest — включение режима тестирования номера. если режим включен, то вызов переадресовывается на приложение Echo() сервера Asterisk

На момент разработки системы количество арендованных номеров было около 400 штук, количество серверов 4. Для тестирования маршрутизации вызовов даже добавляли Мультифон от Мегафона. При использовании "реальных" номеров, привязанных к логину/паролю, необходимо обеспечить осуществление вызова через правильный диалпир. Так как asterisk не может знать через какой диалпир должен маршрутизироваться вызов, необходимо создать таблицу связей.


Таблица связей номеров и dialpeer
CREATE TABLE `gates` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `numbers_pool_id` INT(11) UNSIGNED NOT NULL DEFAULT '0',
    `serverid` INT(11) UNSIGNED NOT NULL DEFAULT '0',
    `gatename` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
    `gatenumber` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
    `cid_support` ENUM('Y','N') NULL DEFAULT 'Y' COLLATE 'utf8mb4_unicode_ci',
    `contextname` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
    `comment` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
    PRIMARY KEY (`id`),
    UNIQUE INDEX `name_number` (`gatename`, `gatenumber`),
    INDEX `FK_gates_servers` (`serverid`),
    INDEX `FK_gates_numbers_pool` (`numbers_pool_id`),
    CONSTRAINT `FK_gates_numbers_pool` FOREIGN KEY (`numbers_pool_id`) REFERENCES `numbers_pool` (`id`),
    CONSTRAINT `FK_gates_servers` FOREIGN KEY (`serverid`) REFERENCES `servers` (`id`)
)
COMMENT='List of gates on servers'
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB
AUTO_INCREMENT=1
;

gates


id numbers_pool_id serverid gatename gatenumber cid_support contextname comment
1 516 1 sip_peer_msm_2302 74957952302 Y sip_msm_74957952302
2 515 1 sip_peer_msm_2301 74957952301 Y sip_msm_74957952301
8 279 1 westcall 74951815000 Y westcall
9 280 1 westcall 74951815001 Y westcall
10 281 1 westcall 74951815002 Y westcall

Описание полей:


  • numbers_pool_id — идентификатор номера из таблицы numbers_pool
  • serverid — привязка гейта к серверу
  • gatename — имя гейта
  • gatenumber — номер подключенный к гейту
  • cid_support — поддерживается ли установка callerid при исходящем вызове
  • contextname — контекст на сервере, который обслуживает исходящие вызовы по данному номеру

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


В связи с тем, что основная логика обработки и маршрутизации вызова вынесена из Asterisk в Application Server, а процедуры находятся в MariaDB, необходимо обеспечить механизмы отладки и контроля работы процедур. Для этого можно использовать следующую схему.
Для хранения отладочной информации создаём дополнительную базу данных debug и создаём там таблицу debug_records


CREATE TABLE `debug_records` (
    `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
    `logtime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `procedure_name` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_unicode_ci',
    `debug_text` TEXT NOT NULL COLLATE 'utf8mb4_unicode_ci',
    PRIMARY KEY (`id`)
)
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB
AUTO_INCREMENT=1
;

Для записи в данную таблицу данных создаём процедуру debug


CREATE DEFINER=`root`@`localhost` PROCEDURE `debug`(
    IN `ProcedureName` VARCHAR(50),
    IN `DebugText` TEXT
)
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT 'Save debug info to log'
BEGIN
DECLARE DebugEnabled TINYINT DEFAULT False;
SET DebugEnabled=True;
IF DebugEnabled THEN
    INSERT INTO debug_records (procedure_name,debug_text) VALUES (ProcedureName,DebugText);
    COMMIT;
END IF;
END

Теперь отладка и контроль работы процедуры на этапе разработки превращается в удовольствие. В начале каждой процедуры мы определяем две переменные, первая — это название процедуры, вторая — это флаг определяющий включена отладка или нет. И далее в различных местах, где процедура может повести себя некорректно — предварительно добавляем запись в отладочный журнал.


CREATE DEFINER=`root`@`localhost` PROCEDURE `usp_gettypeofcall`(
    IN `IncomingPhoneNumber` VARCHAR(30),
    IN `TargetGateNumber` VARCHAR(30),
    IN `CHANID` VARCHAR(60),
    IN `SYSTEMNAME` VARCHAR(30)
)
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT 'Получение типа звонка и маршрута на основе входящих номеров'
BEGIN
DECLARE ProcedureName VARCHAR(50) DEFAULT 'usp_gettypeofcall';
DECLARE ProcedureDebug TINYINT DEFAULT True;
IF ProcedureDebug THEN CALL debug.debug(ProcedureName,CONCAT_WS(' ','Call from ',IncomingPhoneNumber,'to',TargetGateNumber,'at server',SYSTEMNAME,'with channelid',CHANID)); END IF;
END

Обращаю Ваше внимание на то, что использование данной логики не рекомендовано!!! в продакшн режиме на больших нагрузках, так как вывод отладочной информации может сильно снизить быстродействие.


Настройка Asterisk


Перейдём к настройке Asterisk сервера. Стандартная процедура обработки вызовов попадающих в контекст public, в большинстве существующих конфигураций, выглядит следующим образом


[public]
exten => 74951815000,1,NoOp(Incoming Call to ${EXTEN})
same => n,Dial(SIP/1000,60)
same => n,Hangup()

обработка исходящего вызова из контекста dialout выглядит так


[dialout]
exten => _X.,1,NoOp(Outgoing Call to ${EXTEN})
same => n,Dial(SIP/dialpeer_name/${EXTEN},60)
same => n,Hangup()
или
[dialout]
exten => _X.,1,NoOp(Outgoing Call to ${EXTEN})
same => n,Dial(SIP/sip.n1.ipport.net/${EXTEN},60)
same => n,Hangup()
или
[dialout]
exten => _X.,1,NoOp(Outgoing Call to ${EXTEN})
same => n,Dial(SIP/${EXTEN}@sip.n1.ipport.net,60)
same => n,Hangup()

В общем кто как привык и кому как нравится. Как мы видим при такой организации исходящих вызовов практически нет информации о состоянии вызова. Мы модифицировали Dialplan обработки звонков и добавили в него сервисные функции


[service]
; GetIP subroutine
exten => getip,1,Set(TESTAT=${CUT(SIP_HEADER(From),@,2)})
same =>  n,GotoIf($["${TESTAT}" != ""]?hasat)
same =>  n,Set(FROM_IP=${CUT(CUT(SIP_HEADER(From),>,1),:,2)})
same =>  n,Goto(gotip)
same =>  20(hasat),Set(FROM_IP=${CUT(CUT(CUT(SIP_HEADER(From),@,2),>,1),:,1)})
same =>  n(gotip),NoOp(Incoming Server IP is ${FROM_IP})
same =>  n,Return()

exten => set_handler,1,Set(CHANNEL(hangup_handler_push)=service,outbound_handler,1)
same  => n,AGI(/usr/local/etc/asterisk_scripts/create_channel_record.py)
same  => n,Return()

; Set Hangup handler for channel
exten => outbound_handler,1,NoOp(Hungup handler python started)
same  => n,AGI(/usr/local/etc/asterisk_scripts/hangup.py)
same  => n,HangupCauseClear()
same  => n,Return()

exten => no_more_paths,1,NoOp(No more dial paths)
same  => n,Hangup()

[predial]
exten => s,1,NoOp(PreDial handler python started)
same  => n,AGI(/usr/local/etc/asterisk_scripts/predial.py)
same  => n,Return()

[public]
exten => _X.,1,GoSub(service,getip,1)
same => n,AGI(/usr/local/etc/asterisk_scripts/incoming.py)

exten => _+X.,1,GoSub(service,getip,1)
same => n,AGI(/usr/local/etc/asterisk_scripts/incoming.py)

[users_context]
exten => _X.,1,NoOp()
same => n,AGI(/usr/local/etc/asterisk_scripts/make_a_route.py)

[make_a_call]
exten => h,1,NoOp(Hangup)

exten => _.,1,NoOp(${EXTEN})
same  => n,SET(__LoopCount=1)
same  => n(try),AGI(/usr/local/etc/asterisk_scripts/incoming_dialer.py)
same  => n,Dial(${DIALSTRING},60,b(service^set_handler^1)U(predial))
same  => n,SET(__LoopCount=${IF($[${HANGUPCAUSE}=17]?10:${LoopCount})})
same  => n,Set(__LoopCount=${INC(LoopCount)})
same  => n,NoOp(Current LoopCount ${LoopCount})
same  => n,GotoIf($["${LoopCount}" < 10]?try)
same  => n,Hangup()

[redirect]
exten => h,1,NoOp(Hangup)

exten => _.,1,NoOp(${ForwardPath})
same  => n,Dial(${ForwardPath}/${EXTEN},60,b(service^set_handler^1))
same  => n,Hangup()

Контекст [service] содержит в себе сервисные функции такие как:


  • getip — определение IP адреса входящего сервера
  • set_handler — установка обработчиков на текущий исходящий вызов. Данный обработчик вызывается перед осуществлением вызова при создании канала
  • outbound_handler — обработчик завершения вызова. Вызывается во всех случаях отбоя со стороны вызываемого абонента
  • no_more_paths — используется если исчерпаны все доступные варианты вызова абонента

Контекст [predial] вызывается перед соединением абонентов при успешном поднятии трубки на вызываемой стороне.
В контекст [public] попадают все внешние звонки.
В контекст [redirect] попадают все звонки, которые необходимо переадресовать с сервера на сервер.
Контекст [users_context] является основным контекстом пользователей для подготовки маршрутов для вызовов.
Контекст [make_a_call] является основным контекстом при осуществлении исходящего вызова.


Рассмотрим более подробно процедуру обработки входящего вызова:


  1. Вызов поступает с сервера оператора и попадает в контекст [public]
  2. Вызывается процедура определения IP адреса сервера с которого пришёл вызов на основе заголовков SIP пакета
  3. Вызывается AGI приложение incoming.py осуществляющее начальную обработку входящего вызова.
  4. Внутренняя логика приложения подключается к базе данных и отправляет в процедуру проверки подключений начальные имеющиеся параметры: IP адрес сервера, вызываемый номер, идентификатор сервера. Идентификатор сервера является уникальным, соответствует таблице серверов и определяется в файле asterisk.conf.
  5. Если с идентификацией номера всё нормально, то производится поиск ранее использованных звонков пользователей звонящему абоненту. В случае успешного поиска вызов переадресовывается на инициатора предыдущего звонка. В случае неуспешного поиска вызов переадресовывается на дежурного оператора. Если с идентификацией номера возникли проблемы, то производится голосовая "отбивка".

Все внутренние и внешние исходящие вызовы происходят через контексты [users_context] и [make_a_call].


Рассмотрим подробнее процедуру исходящего вызова:


  1. Внутренний абонент набирает номер вызываемого абонента
  2. Вызов попадает в контекст [users_context]
  3. Исходные данные для осуществления вызова передаются в AGI приложение make_a_route.py, внутренняя логика которого, используя данные базы данных, формирует очередь маршрутов для осуществления вызова. На основе полученного списка маршрутов, который состоит из:
    • оператора через которого происходит вызов
    • исходящего диалпира для осуществления вызова
    • CallerID для исходящего вызова
    • стоимости вызова через оператора

    • приложение make_a_route.py меняет контекст на [make_a_call] или [redirect] и передаёт в них идентификатор сформированной очереди
  4. Контекст [redirect] используется в случаях, когда жёстко привязанный к серверу номер, с которого необходимо сделать вызов, находится на другом сервере.
  5. Контекст [make_a_call] представляет собой цикл дозвона с мониторингом состояния. В нашей конфигурации мы ограничиваем количество итераций в цикле дозвона до 10 штук. Поэтому при обращении в контекст дозвона, мы устанавливаем в 1 внутреннюю переменную итераций дозвона, действующую только в текущей сессии.
  6. Данные очереди полученные из предыдущего контекста и номер текущей итерации дозвона передаются в AGI приложение incoming_dialer.py, которое возвращает сформированный заранее, в AGI приложении make_a_route.py, DialString для осуществления вызова.
  7. Для осуществления мониторинга вызова, используются дополнительные опции приложения Dial, а именно U и b. Опция b позволяет вызвать подпрограмму после создания канала связи, но перед самим вызовов. В данном случае эта опция используется для установки AGI приложения, которое будет вызвано при разрыве соединения. Одновременно с вызовом данной опции, в базу данных, заносится информация о текущем осуществляемом вызове. Опция U позволяет вызвать подпрограмму при установлении соединения с абонентом, т.е. в момент поднятия вызываемым абонентом трубки. Это позволяет гарантированно отследить начало разговора и начать отсчёт времени.
  8. Если соединение с абонентом не удалось и код возврата не равен 17(Занято), следует новая итерация цикла.
  9. Вся информация о завершении соединения через текущий канал связи заносится в базу данных. Что позволяет считать ABR, ASR и прочую аналитику путём обычного запроса к базе данных
  10. Если количество циклов дозвона превысило имеющееся в базе данных, то происходит переход в контекст [no_more_paths], который может осуществлять информирование клиента о невозможности соединения с абонентом

На этом логика работы сервера Asterisk заканчивается. Используя данную связку базы данных и приложений, добавление серверов в обслуживающий кластер происходит путём копирования конфигурационной директории сервера Asterisk, сменой имени сервера, генерацией новых RSA ключей для IAX соединений и прописыванием идентификационных данных сервера в одной таблице. Вся остальная логика работы формируется через веб интерфейс путём привязки номеров и операторов к серверам.


Использование AGI приложений на Python обусловлено единообразием среды разработки при проектировании Application сервера. Начальная версия AGI приложений использовала прямое подключение к базе. После запуска системы и отладки работы ядра, AGI приложения были переписаны под использование протокола HTTP и Application Server на uwsgi. Часть операций не требующих проверки условий и вычислений, например проверка категорий номеров, была переписана для скорости на использование CURL в диалплане.


Следующая статья посвящена тарификатору, LCR и определению геопозиции вызываемых или вызывающих абонентов.


© Aborche 2017
Aborche

Поделиться с друзьями
-->

Комментарии (9)


  1. vazhnov
    16.01.2017 15:46

    Здорово было бы увидеть это где-нибудь на GitHub под свободной лицензией!


    1. aborche
      16.01.2017 15:47

      Целиком проект врядли, но определённые модули наверное можно.


  1. IgorDimitrov
    16.01.2017 18:01

    Кстати проект Zadarma также поддерживает SIP транк и для исходящих и для входящих вызовов.

    Для исходящих в разделе «SIP настройки» нажмите кнопку «добавить SIP транк» при этом будут пропускаться CallerID всех приобретенных/подтвержденных номеров. При звонке с другим CallerID будет установлен CallerID по умолчанию.

    Для входящих в настройке номеров вместо SIP логина установите галочку «Внешний сервер (SIP URI)» и укажите свой SIP URI.


    1. aborche
      16.01.2017 18:18

      по какой-то причине наши заказчики не захотели терминировать через Вас трафик. Увы. Но не нам решать. Хотя телефонов у Вас было куплено оооочень много.


      1. IgorDimitrov
        16.01.2017 18:32

        Я ни в коем случае не рекламирую «покупайте у нас».

        Исключительно добавил о технических параметрах. Что через Zadarma можно и через sip trunk все принимать/звонить.
        Кстати если работать без транка, рекомендуем использовать DNS SRV, ведь еще одно дополнительное резервирование никогда не будет лишним :)
        И по поводу IP серверов, в скором времени обновим список и будет просто несколько сетей вместо списка IP, что намного удобней для настройки файрвола.


        1. aborche
          16.01.2017 19:18

          да уж. это было бы не лишним. когда мы тестировались появилась новая нода в lv. а в списке её не было. Долго искали почему звонки не ходят :)
          А так, всё отлично, вам бы правда немного кабинет переделать. Просто так фиг найдёшь купленные номера. Помню долго мучался искал.


  1. g613
    16.01.2017 21:41

    same => n,Set(FROM_IP=${CUT(CUT(SIP_HEADER(From),>,1),:,2)})

    вообще, написать в хидере From произвольный ip адрес делов 1 минуты, и использовать это для проверки доступа совсем не айс.

    с таким количеством AGI ваш сервер умрет даже при не сильно большом cps. FastAGI хотябы используйте.


    1. aborche
      16.01.2017 22:45
      +1

      Я даже спорить не буду по поводу заголовков :)


      По поводу CPS и AGI. Почему-то все уверены и считают, что они станут магистральщиками и к ним сразу в моменте единовременно придёт 100500 клиентов в секунду и положит сервер и не один, а весь кластер. Для больших нагрузок используется другое оборудование, программное обеспечение и архитектура. В данном случае на 2500 клиентов сервиса было не более 500 звонков в сутки. Скажем так, сервис позиционировался на 90% для международных звонков. Сколько может потратить среднестатистический пользователь на звонки за рубеж ?


      А теперь давайте применим данную архитектуру для офиса в 200-500 человек. Какой CPS будет у такого офиса за 8 рабочих часов, если средняя продолжительность разговора с клиентом 5-10 минут? Если это международные звонки, то какой объём нужен клиенту? Сколько линий? Сколько одновременных разговоров разрешено? А если это только локальные звонки, то зачем нужен LCR?
      Поэтому про умирание сервера от AGI вопрос можно поднимать при наличии нагрузки. Да. Можно уйти на FastAGI, можно уйти на B2B решения, на платные B2B сервисы. Всё зависит от конкретных задач.
      Строить систему на 100000 CPS, вкладывая в неё серьёзные деньги(которых к слову у заказчика не было) при 5000 тысячах клиентов совершающих в сумме 1000 звонков в сутки смысла нет. Согласны ?


      1. g613
        17.01.2017 08:46

        ну в начале статьи просто про оператора связи говорили. я подрузамевал там больше 1000 звонков в день.