CallerID FreePBX: база имён звонящих — варианты, ограничения и настройка на MariaDB

CallerID FreePBX: сравнение Phonebook, CID Lookup, Superfecta и func_odbc. База на MariaDB с нормализацией номеров и кириллицей.
Быстрый ответ
CallerID FreePBX подтягивает имя звонящего из четырёх типов баз:
  • Asterisk Phonebook — до пары сотен номеров, хранится в AstDB, номер должен совпасть символ в символ.
  • CallerID Lookup Sources с источником MySQL — основной вариант для офиса: тысячи записей, своя таблица, кеш в AstDB.
  • CID Superfecta — цепочка из нескольких источников, фильтр спама, уведомления. Модуль нужен версии 17.0.7 и новее, в старых есть RCE.
  • func_odbc плюс модуль Set CallerID — полный контроль, экранирование SQL, минимум зависимостей от модулей.

Для 90% офисов хватает отдельной таблицы MariaDB, где номер приведён к 10 цифрам, и источника MySQL в CallerID Lookup Sources. Ниже — как это поднять за 30-40 минут.

Диагноз: поднял FreePBX, а на трубке только номер

CallerID FreePBX по умолчанию показывает ровно то, что прислал провайдер. В России это почти всегда голый номер: 79161234567 и ни одной буквы. Знакомо?

Звонит клиент. Трубку берёт новый менеджер. Он не знает, что это крупнейший заказчик, который третий день ждёт счёт. В CDR потом тоже одни цифры — отчёт по клиентам приходится сводить руками.

Секретарь помнила наизусть номера сорока ключевых клиентов. Секретарь ушла в декрет. Справочник ушёл вместе с ней.

Смотри, в чём корень. В США операторы передают CNAM — имя абонента прямо в сигнализации. У нас такой услуги по факту нет. Значит, имя должна подставить сама АТС из своей базы. FreePBX это умеет, причём четырьмя разными способами. И у каждого свои грабли.

Что получишь на выходе

  • Понимание, какой вариант базы CallerID подходит под твой размер и задачи.
  • Рабочую таблицу в MariaDB, которая находит номер в любом формате: +7, 8, 7 или 10 цифр.
  • Кириллицу на трубках и в CDR без кракозябр.
  • Импорт контактов из CSV, который можно повесить на таймер.
  • Проверку, troubleshooting, бэкап, мониторинг и безопасное обновление.
  • Готовые альтернативы: Phonebook, Superfecta, HTTP API и func_odbc.
  • FAQ по типовым вопросам, которые задают в Яндексе.

Время: 30-40 минут на основной рецепт. Ещё 20 минут, если будешь ставить автосинхронизацию и мониторинг.

Что нужно: SSH-доступ к серверу FreePBX под root или через sudo, доступ в веб-интерфейс администратора, CSV с номерами и именами. Если CSV пока нет — сделаем тестовый на две строки.

Системные требования

Компонент Версия Комментарий
ОС Debian 12 Bookworm Единственная официально поддерживаемая ОС для FreePBX 17
FreePBX 17.x На 16.x рецепт тоже работает, отличия отмечены в тексте
Asterisk 22 LTS по умолчанию, поддерживаются 20 и 21 Начиная с 19 в Asterisk нет app_mysql
СУБД MariaDB 10.11 из репозитория Debian 12 Та же, что использует сам FreePBX
Модуль cidlookup 15.0.25+ / 16.0.12+ / любой 17.x С этих версий MySQL-запрос идёт через AGI, а не через app_mysql
Модуль superfecta 16.0.40+ / 17.0.7+ Только если выберешь Superfecta. Ниже этих версий — CVE-2026-54710
Python 3.11 из Debian 12 Для скрипта импорта и HTTP API

На момент публикации актуальна версия FreePBX 17 с Asterisk 22 LTS по умолчанию. Перед установкой проверь свежие релизы на странице FreePBX 17 в базе знаний Sangoma и в сообществе FreePBX.

Что будет в статье

  • Причины, почему имя не появляется даже после настройки.
  • Сравнение вариантов базы CallerID и их ограничения.
  • Рецепт: таблица MariaDB, импорт, источник MySQL, привязка к входящему маршруту.
  • Проверка и осложнения с командами.
  • Альтернативы: Phonebook, Superfecta, HTTP, func_odbc.
  • Профилактика, безопасность, бэкап, обновление.
  • FAQ.

Причины: почему имя звонящего в FreePBX не появляется

Прежде чем строить базу, пойми, что ломается. Половина вопросов на форумах звучит как «настроил CallerID Lookup, не работает». А не работает по одной из семи причин ниже. Все они лечатся, но по-разному.

Причина Почему ломает
Провайдер передаёт только номер CNAM в российской телефонии практически не встречается. Имени просто неоткуда взяться, пока его не подставит АТС.
Номер приходит в другом формате В базе записано 8 916 123-45-67, провайдер шлёт 79161234567 или +79161234567. Точное сравнение не находит совпадения.
Источник не привязан к входящему маршруту Lookup Source создан, но в Inbound Route на вкладке Other выбран None. FreePBX не делает запрос.
Кодировка latin1 по умолчанию Поле Character Set в источнике пустое — используется latin1. Кириллица превращается в вопросики.
Устаревший кеш в AstDB С включённым кешем имя, найденное однажды, возвращается всегда. Даже если в базе его давно поменяли.
Старый модуль cidlookup на новом Asterisk Старые версии модуля ходили в MySQL через app_mysql. Его удалили в Asterisk 19, и запрос молча не выполняется.
Телефон не умеет показать имя Старые трубки обрезают имя до 12-16 символов или не понимают UTF-8. В CDR имя есть, на экране нет.

Вот тут важно. Причины 2 и 4 дают 80% проблем на российских АТС. Формат номера и кодировка. Поэтому рецепт ниже построен вокруг них: номер нормализуем в самой базе, кодировку задаём явно.

Про причину 6 отдельно. Это подробно разобрано в задаче FREEPBX-23206: модуль полагался на app_mysql, после его удаления в Asterisk 19 источники MySQL перестали работать. Позже запрос переписали на AGI-скрипт. Если у тебя FreePBX 17 с обновлёнными модулями — проблемы нет. Если сервер не обновлялся с 2021 года — она есть.

Варианты базы CallerID в FreePBX: сравнение и ограничения

Теперь по существу запроса. В FreePBX нет одной «базы CallerID». Есть несколько механизмов, которые решают задачу с разной глубиной. Выбирать нужно по объёму, по источнику данных и по тому, кто будет эту базу поддерживать.

Вариант Где хранит Разумный объём Формат номера Главное ограничение
Asterisk Phonebook AstDB, семейство cidname до 200-300 записей Только точное совпадение Нет нормализации, неудобное массовое редактирование
CallerID Lookup: Internal AstDB через Phonebook как у Phonebook Точное совпадение По сути тот же Phonebook
CallerID Lookup: MySQL Любая база MySQL/MariaDB десятки тысяч и больше Любая логика в SQL Один запрос, одна колонка, кодировка по умолчанию latin1
CallerID Lookup: HTTP/HTTPS Твой веб-сервис не ограничен Решает сервис Задержка сервиса добавляется к каждому входящему
CallerID Lookup: ENUM DNS-зоны из enum.conf — E.164 В России для имён почти не применяется
CallerID Lookup: OpenCNAM Внешний сервис — Номера США Для российских номеров бесполезен, бесплатный тариф 60 запросов в час
CID Superfecta Много источников по цепочке не ограничен Правила и регулярки Сложнее в настройке, история уязвимостей
func_odbc + Set CallerID Любая база через ODBC не ограничен Любая логика в SQL Ручные конфиги вне GUI

Источники типов cidlookup подробно описаны в официальном руководстве по CallerID Lookup Sources. Там же предупреждение, которое все пропускают: поиск имени может замедлить АТС. Запрос выполняется синхронно, пока звонок висит на входящем маршруте.

Как выбрать без долгих раздумий — схема ниже.

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#ffffff',
    'primaryTextColor': '#1e293b',
    'primaryBorderColor': '#94a3b8',
    'lineColor': '#64748b',
    'fontSize': '15px',
    'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
  },
  'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
    A["Нужна база имён звонящих"] --> B{"Больше 300 номеров?"}
    B -->|Нет| C["Asterisk Phonebook"]
    B -->|Да| D{"Данные уже в CRM с API?"}
    D -->|Да| E["Lookup Source HTTP"]
    D -->|Нет| F{"Нужны спам-фильтр и уведомления?"}
    F -->|Да| G["CID Superfecta 17.0.7+"]
    F -->|Нет| H{"Нужна полная гибкость SQL?"}
    H -->|Да| I["func_odbc и Set CallerID"]
    H -->|Нет| J["Lookup Source MySQL"]
    style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
    style B fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#9a3412
    style D fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#9a3412
    style F fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#9a3412
    style H fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#9a3412
    style C fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
    style E fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
    style G fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
    style I fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
    style J fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d

Почему основным рецептом выбран MySQL-источник. Он штатный, настраивается в GUI, переживает обновления FreePBX и попадает в стандартный бэкап конфигурации. Таблицу при этом контролируешь ты: можешь наполнять её из CRM, 1С, Active Directory или руками. Superfecta мощнее, но для задачи «показать имя клиента» это танк для поездки в булочную.

Архитектура решения

Вот как пойдёт звонок после настройки. Провайдер присылает номер, входящий маршрут вызывает Lookup Source, тот проверяет кеш, потом идёт в MariaDB. Найденное имя записывается в CALLERID(name) — его видят телефоны и CDR.

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#ffffff',
    'primaryTextColor': '#1e293b',
    'primaryBorderColor': '#94a3b8',
    'lineColor': '#64748b',
    'fontSize': '15px',
    'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
  },
  'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
    A["Входящий от провайдера"] --> B["Inbound Route по DID"]
    B --> C["CID Lookup Source"]
    C --> D{"Номер есть в кеше AstDB?"}
    D -->|Да| E["Имя из кеша"]
    D -->|Нет| F["AGI cidlookup_mysql"]
    F --> G["MariaDB cid_directory"]
    G --> H["CALLERID name"]
    E --> H
    H --> I["Ring Group или IVR"]
    I --> J["Экран телефона"]
    H --> K["CDR поле cnam"]
    style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
    style J fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
    style D fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#9a3412
    style G fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
    style H fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d

Таблица портов

Порт Протокол Назначение Доступен снаружи?
5060 UDP/TCP SIP, PJSIP Только с IP провайдера и офисных сетей
5061 TCP SIP over TLS Только если используешь TLS
10000-20000 UDP RTP, голос Да, для провайдера и удалённых абонентов
80/443 TCP Веб-интерфейс FreePBX Нет, только из админской сети
3306 TCP MariaDB Нет. Слушает только 127.0.0.1
8085 TCP HTTP API для CallerID, если выберешь этот вариант Нет. Только с IP АТС

Рецепт: база CallerID для FreePBX на MariaDB

Договоримся об именах, чтобы ничего не перепутать. Сервер FreePBX называю pbx01. База живёт там же, в той MariaDB, которую уже использует FreePBX, но в отдельной базе данных cid_directory. Если хочешь вынести базу на отдельный сервер — всё то же самое, разница в одном поле Host и в правилах firewall.

Подготовка: проверь среду

Сначала убедись, что версии те, о которых речь. Пять секунд сейчас экономят час потом.

Хост: pbx01, сервер FreePBX, под root.


cat /etc/debian_version
asterisk -rx "core show version"
mariadb --version
fwconsole ma list | grep -Ei 'framework|core|cidlookup|superfecta|phonebook|setcid'

Результат: Debian 12.x, Asterisk 20, 21 или 22, MariaDB 10.11. В списке модулей framework и core версии 17.x. Модуль cidlookup может отсутствовать — поставим. Если superfecta стоит с версией ниже 17.0.7 — сначала прочитай блок про безопасность в разделе альтернатив.

Перед любыми изменениями сделай снимок. Если FreePBX крутится в виртуалке на Proxmox, это одна команда.

Хост: узел Proxmox, где запущена ВМ pbx01. Вместо 105 подставь VMID своей АТС.


qm snapshot 105 pre-callerid-db --description "Before CallerID DB setup"

Результат: снапшот виден в веб-интерфейсе Proxmox на вкладке Snapshots. Откат — qm rollback 105 pre-callerid-db.

Шаг 1. Поставь или обнови модуль CallerID Lookup

Зачем: источник MySQL в свежих версиях модуля работает через AGI-скрипт. В старых — через app_mysql, которого в Asterisk 22 нет. Обновление закрывает причину номер 6 из таблицы выше.

Хост: pbx01, под root.


fwconsole ma downloadinstall cidlookup
fwconsole ma upgrade cidlookup
fwconsole reload
ls -l /var/lib/asterisk/agi-bin/ | grep -i cidlookup

Результат: модуль в статусе Enabled, в каталоге agi-bin есть файл cidlookup_mysql.agi или похожий по имени. Если файла нет, а модуль свежий — проверь, не лежит ли AGI в каталоге модуля: find /var/www/html/admin/modules/cidlookup -name «*.agi».

Шаг 2. Создай базу и двух пользователей

Зачем: отдельная база не смешивается с таблицами FreePBX и не ломается при его обновлениях. Пользователей два, и это не паранойя. FreePBX подставляет номер звонящего прямо в текст SQL-запроса. Значит, у пользователя, под которым работает поиск, должно быть право только на SELECT и только на одну таблицу. Писать будет второй пользователь — для импорта.

Хост: pbx01, под root. В Debian 12 root заходит в MariaDB через unix_socket без пароля.


mariadb

Внутри клиента MariaDB выполни. Пароли замени на свои, длинные и случайные. Сгенерировать можно командой openssl rand -base64 24 в соседнем окне.


CREATE DATABASE cid_directory CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE USER 'cid_ro'@'localhost' IDENTIFIED BY 'ЗАМЕНИ_НА_ДЛИННЫЙ_ПАРОЛЬ_RO';
CREATE USER 'cid_rw'@'localhost' IDENTIFIED BY 'ЗАМЕНИ_НА_ДЛИННЫЙ_ПАРОЛЬ_RW';

GRANT SELECT ON cid_directory.* TO 'cid_ro'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON cid_directory.* TO 'cid_rw'@'localhost';

FLUSH PRIVILEGES;

Результат: каждая команда отвечает Query OK. Проверить права можно так: SHOW GRANTS FOR ‘cid_ro’@’localhost’; — в ответе должен быть только SELECT.

Шаг 3. Создай таблицу с нормализацией номера

Это ключевой шаг всей статьи. Не перепутай.

Проблема: в одной базе номера записаны как «+7 (916) 123-45-67», «89161234567» и «7 916 1234567». Провайдер присылает «79161234567». А завтра сменишь провайдера — и он будет слать «+79161234567». Сравнивать строки как есть бессмысленно.

Решение: хранить рядом с исходным номером вычисляемую колонку — последние 10 цифр. Для российских мобильных и городских номеров это и есть уникальная часть. Колонка STORED, на ней уникальный индекс. MariaDB пересчитывает её сама при каждой вставке и правке.

Хост: pbx01, в том же клиенте MariaDB.


USE cid_directory;

CREATE TABLE contacts (
    id          INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    number      VARCHAR(32)  NOT NULL,
    num10       VARCHAR(10)  AS (RIGHT(REGEXP_REPLACE(number, '[^0-9]', ''), 10)) STORED,
    name        VARCHAR(64)  NOT NULL,
    source      VARCHAR(32)  NOT NULL DEFAULT 'manual',
    updated_at  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uq_num10 (num10)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT INTO contacts (number, name) VALUES
    ('+7 (916) 123-45-67', 'Иванов И. Ромашка'),
    ('8 (343) 222-33-44',  'Бухгалтерия Ромашки');

SELECT number, num10, name FROM contacts;

Результат: SELECT показывает две строки, в колонке num10 значения 9161234567 и 3432223344. Скобки, пробелы, плюсы и восьмёрки исчезли.

Почему 10 цифр, а не 11. Первая цифра у российских номеров бывает 7 или 8, а иногда её вообще нет. Последние 10 цифр стабильны во всех вариантах записи. Если работаешь с международными номерами, где важна страна, — храни полный E.164 и сравнивай по нему. Но для внутреннего российского трафика 10 цифр закрывают задачу.

Про длину имени. VARCHAR(64) с запасом, но телефоны покажут меньше. Большинство настольных трубок выводят 16-24 символа в строке входящего. Поэтому «Иванов И. Ромашка» лучше, чем «Общество с ограниченной ответственностью Ромашка, Иванов Иван Иванович, главный инженер».

Шаг 4. Залей контакты из CSV

Рождённый в Excel телефонный справочник нормализации не боится. Он о ней просто не слышал. Поэтому импорт делаем скриптом, а не руками.

Формат CSV: два поля через точку с запятой, номер и имя, кодировка UTF-8. Excel умеет сохранять «CSV UTF-8» — выбирай именно этот тип. Пример файла:


+7 (916) 123-45-67;Иванов И. Ромашка
8 343 222-33-44;Бухгалтерия Ромашки
79035556677;Петров П. Вектор

Сначала поставь драйвер MariaDB для Python и положи учётку пользователя cid_rw в файл с правами только для root. Пароль в коде скрипта держать не будем.

Хост: pbx01, под root.


apt update && apt install -y python3-pymysql

cat > /root/.cid_rw.cnf <<'EOF'
[client]
user=cid_rw
password=ЗАМЕНИ_НА_ДЛИННЫЙ_ПАРОЛЬ_RW
host=localhost
EOF
chmod 600 /root/.cid_rw.cnf

Теперь сам скрипт. Он читает CSV, пропускает пустые строки и номера короче 10 цифр, обрезает имя до 64 символов и делает upsert: новые номера добавляет, существующие обновляет.

Хост: pbx01, файл /usr/local/sbin/cid-import.py.


#!/usr/bin/env python3
"""Импорт контактов CallerID из CSV в cid_directory.contacts.
Формат строки: номер;имя  (UTF-8, допускается BOM)."""
import csv
import sys
import pathlib
import pymysql

CNF = "/root/.cid_rw.cnf"
STAMP = pathlib.Path("/var/lib/cid-sync/last_ok")

SQL = (
    "INSERT INTO contacts (number, name, source) VALUES (%s, %s, 'csv') "
    "ON DUPLICATE KEY UPDATE number = VALUES(number), "
    "name = VALUES(name), source = 'csv'"
)


def load(path):
    rows, skipped = [], 0
    with open(path, newline="", encoding="utf-8-sig") as f:
        for rec in csv.reader(f, delimiter=";"):
            if len(rec) < 2:
                skipped += 1
                continue
            number, name = rec[0].strip(), rec[1].strip()[:64]
            digits = "".join(c for c in number if c.isdigit())
            if len(digits) < 10 or not name:
                skipped += 1
                continue
            rows.append((number, name))
    return rows, skipped


def main():
    if len(sys.argv) != 2:
        sys.exit("usage: cid-import.py contacts.csv")
    rows, skipped = load(sys.argv[1])
    conn = pymysql.connect(read_default_file=CNF, database="cid_directory",
                           charset="utf8mb4")
    with conn:
        with conn.cursor() as cur:
            cur.executemany(SQL, rows)
        conn.commit()
    STAMP.parent.mkdir(parents=True, exist_ok=True)
    STAMP.touch()
    print(f"ok: {len(rows)} upserted, {skipped} skipped")


if __name__ == "__main__":
    main()

Хост: pbx01, под root. Сделай скрипт исполняемым и прогони на файле. Файл заранее скопируй на сервер, например в /srv/cid/contacts.csv.


chmod 750 /usr/local/sbin/cid-import.py
mkdir -p /srv/cid
/usr/local/sbin/cid-import.py /srv/cid/contacts.csv

Результат: строка вида ok: 3 upserted, 0 skipped. Если skipped больше нуля — открой CSV и посмотри, какие строки без имени или с коротким номером. Обычно это внутренние трёхзначные номера, им в этой таблице не место.

Обрати внимание на одну тонкость. Если в CSV два разных написания одного номера, последнее перезапишет первое — уникальный индекс на num10 не даст создать дубль. Это фича, а не баг: один номер — одно имя.

Шаг 5. Проверь запрос руками до GUI

Зачем: если запрос не работает в консоли, он не заработает и в FreePBX. А в FreePBX ошибку искать дольше. Проверяем ровно под тем пользователем и ровно тем запросом, что пойдут в Lookup Source.

Хост: pbx01. Пароль cid_ro введёшь интерактивно.


mariadb -u cid_ro -p --default-character-set=utf8mb4 cid_directory \
  -e "SELECT name FROM contacts WHERE num10 = RIGHT('+79161234567', 10) LIMIT 1;"

mariadb -u cid_ro -p --default-character-set=utf8mb4 cid_directory \
  -e "SELECT name FROM contacts WHERE num10 = RIGHT('89161234567', 10) LIMIT 1;"

Результат: оба запроса возвращают «Иванов И. Ромашка» нормальной кириллицей. Два разных формата — одно имя. Именно этого мы и добивались.

Почему RIGHT в самом запросе, а не только в колонке. Номер от провайдера тоже приходит в непредсказуемом виде. Мы нормализуем обе стороны сравнения. Индекс на num10 при этом используется: функция применяется к константе, а не к колонке.

Шаг 6. Создай источник в CallerID Lookup Sources

Зачем: это штатная точка, через которую FreePBX подставляет имя во входящий вызов. Открой веб-интерфейс: Admin → CallerID Lookup Sources → Add CID Lookup Source. Заполни поля так:

Поле Значение Комментарий
Source Description CID MariaDB Это имя увидишь в выпадающем списке входящего маршрута
Source type MySQL Работает и с MariaDB
Cache Results Нет на старте Включишь позже, когда база устоится. Подробно ниже
Host localhost Именно localhost, а не 127.0.0.1 — пользователь создан для localhost
Database cid_directory
Query запрос из блока ниже [NUMBER] FreePBX заменит на номер звонящего
Username cid_ro Только SELECT
Password пароль cid_ro
Character Set utf8mb4 Не оставляй пустым, иначе latin1 и кракозябры

Запрос для поля Query, одной строкой:


SELECT name FROM contacts WHERE num10 = RIGHT('[NUMBER]', 10) LIMIT 1

Нажми Submit, потом Apply Config. Запрос должен возвращать ровно одну колонку. FreePBX берёт первое значение первой строки и пишет его в имя звонящего.

Шаг 7. Привяжи источник к входящему маршруту

Зачем: сам по себе Lookup Source ничего не делает. Его нужно включить на конкретном Inbound Route. Это самая частая причина вопроса «всё настроил, ничего не происходит».

Открой Connectivity → Inbound Routes → нужный маршрут → вкладка Other. В поле CID Lookup Source выбери «CID MariaDB». Сохрани и примени конфиг. Если входящих маршрутов несколько, повтори для каждого, где нужно имя. Маршрут «по умолчанию» с пустым DID тоже не забудь.

После этого перегенерируй диалплан с консоли — так надёжнее, чем кнопка в браузере, и сразу видны ошибки.

Хост: pbx01, под root.


fwconsole reload
asterisk -rx "dialplan show cidlookup"

Результат: в контексте cidlookup есть расширение вида cidlookup_1 с вызовом AGI. Номер после подчёркивания — ID источника. Если контекст пустой, источник не привязан ни к одному маршруту или конфиг не применился.

Ниже — как это выглядит по времени. Обрати внимание: пока идёт запрос, у звонящего ещё не начались гудки. Поэтому быстрый запрос — это не эстетика, а качество обслуживания.

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#f8fafc',
    'primaryTextColor': '#1e293b',
    'primaryBorderColor': '#94a3b8',
    'noteBkgColor': '#fefce8',
    'noteTextColor': '#713f12',
    'noteBorderColor': '#fbbf24',
    'actorBkg': '#f8fafc',
    'actorBorder': '#94a3b8',
    'actorTextColor': '#1e293b',
    'fontSize': '15px',
    'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
  },
  'sequence': {
    'mirrorActors': false,
    'messageAlign': 'center',
    'actorMargin': 120,
    'width': 160,
    'noteMargin': 12
  }
}}%%
sequenceDiagram
    participant P as Провайдер
    participant A as Asterisk
    participant G as AGI cidlookup
    participant D as MariaDB
    participant T as Телефон
    P->>A: INVITE с номером
    A->>A: Проверка кеша AstDB
    A->>G: Номер звонящего
    G->>D: SELECT name
    D->>G: Иванов И. Ромашка
    G->>A: CALLERID name
    Note over A,D: Пауза до гудков
    A->>T: Вызов с именем

Шаг 8. Реши, включать ли кеш

Галочка Cache Results в источнике заставляет FreePBX сохранять найденное имя в AstDB. Следующий звонок с того же номера вообще не пойдёт в базу. Быстро. Но есть подвох.

Закешированное имя возвращается всегда. Даже если ты поменял его в таблице. Даже если клиент ушёл к конкурентам и номер теперь принадлежит другому человеку. Кеш сам не протухает.

Ситуация Кеш Почему
База локальная, на том же сервере Выключен Запрос по индексу занимает миллисекунды, выигрыша нет, а риск устаревших имён есть
База на удалённом сервере в той же сети По ситуации Включай, если задержка до базы заметна, и чисти кеш после каждой синхронизации
HTTP API медленного внешнего сервиса Включён Иначе каждый входящий ждёт ответа сервиса
Имена часто меняются Выключен Иначе люди увидят старые имена и перестанут доверять системе

Для рецепта из этой статьи кеш не нужен. Локальная MariaDB с уникальным индексом отвечает быстрее, чем ты успеешь об этом подумать.

Проверка: как убедиться, что CallerID FreePBX подставляет имя

Проверяем в трёх местах: живой лог Asterisk, CDR и экран телефона. Если имя есть в CDR, но нет на трубке — проблема в телефоне. Если нет и в CDR — в базе или маршруте.

Живой лог во время тестового звонка

Хост: pbx01, под root. Открой консоль, позвони на городской номер с мобильного, который есть в базе.


asterisk -rvvvvv

В выводе ищи строки с cidlookup. Должен быть вызов AGI, а после него Set с CALLERID(name). Выйти из консоли — exit или Ctrl+C, Asterisk при этом не остановится.

Если звонок уже прошёл, смотри в файле лога:


grep -iE 'cidlookup|CALLERID\(name\)' /var/log/asterisk/full | tail -n 30

Результат: видно, какой номер ушёл в запрос и какое имя вернулось. Если номер в логе выглядит неожиданно, например с префиксом маршрута или с лишним плюсом, — вот она, причина несовпадения.

Проверка через CDR

CDR хранит имя в поле cnam. Это удобно: видно сразу последние звонки и что по ним нашлось.

Хост: pbx01, в клиенте mariadb под root.


SELECT calldate, src, cnam, did, disposition
FROM asteriskcdrdb.cdr
WHERE calldate > NOW() - INTERVAL 1 HOUR
ORDER BY calldate DESC
LIMIT 10;

Результат: у тестового звонка в cnam стоит имя из твоей таблицы. Если там номер или пусто — lookup не сработал. Колонка src заодно покажет, в каком формате провайдер присылает номер.

Проверка кеша AstDB

Даже если кеш выключен, полезно знать, что в AstDB лежит. Phonebook и кеш cidlookup используют одно и то же семейство cidname.


asterisk -rx "database show cidname" | head -n 20

Результат: список ключей вида /cidname/79161234567 со значением-именем. Если там старое имя для нужного номера — именно оно и показывается, а твоя таблица даже не опрашивается.

Отладка FreePBX

Если AGI молчит или возвращает пусто, включи отладочный лог FreePBX. Окно держи открытым и сделай тестовый звонок во втором окне.


fwconsole dbug

Лог пишется в /var/log/asterisk/freepbx_dbug. Там видны ошибки подключения к базе из PHP, которые в консоль Asterisk не попадают.

Осложнения: типовые ошибки и как их лечить

Обычно в этом месте пишут: «если делал строго по инструкции, проблем не будет». Проблемы будут. Инструкцию писал человек, а сервер настраивал тоже человек.

Ошибка Причина Решение
Вместо имени вопросики или кракозябры Пустой Character Set, используется latin1 Указать utf8mb4 в источнике, проверить кодировку таблицы
Имя не меняется после правки в базе Включён кеш, значение лежит в AstDB Удалить ключ из cidname или выключить кеш
Имя не находится, в консоли всё работает Провайдер шлёт номер в другом формате Посмотреть src в CDR, проверить нормализацию
Access denied for user Хост в источнике 127.0.0.1, а пользователь создан для localhost Поставить localhost или создать пользователя для 127.0.0.1
Вместо номера unknown, когда имени в базе нет Старый баг модуля cidlookup 15.0.24 Обновить модуль до свежей версии
Предупреждение про app_mysql, MySQL нельзя выбрать Старый cidlookup на Asterisk 19+ Обновить модуль cidlookup
Пауза перед гудками Медленная удалённая база или HTTP API Индекс, локальная база, кеш
Имя есть в CDR, на телефоне нет Телефон не показывает UTF-8 или обрезает Прошивка, язык трубки, короткие имена

Кракозябры вместо кириллицы

Причина: поле Character Set в источнике пустое. Подсказка в самом интерфейсе модуля честно пишет, что по умолчанию используется latin1. Кириллица из utf8mb4-таблицы в latin1 не помещается.

Решение: открой источник, впиши utf8mb4, сохрани, примени конфиг. Потом проверь, что сама таблица действительно в utf8mb4 — если её создавали без явной кодировки на старом сервере, могла получиться latin1.

Хост: pbx01, клиент mariadb под root.


SELECT TABLE_NAME, TABLE_COLLATION
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'cid_directory';

ALTER TABLE cid_directory.contacts CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

ALTER нужен только если в TABLE_COLLATION не utf8mb4. Если данные уже залиты в неправильной кодировке, конвертация их не спасёт — перезалей из CSV.

Имя устарело и не обновляется

Причина: в источнике включён кеш. Имя лежит в AstDB и возвращается раньше, чем дело дойдёт до базы.

Решение: удали ключ конкретного номера. Номер пиши в том формате, в каком он приходит от провайдера, — смотри вывод database show cidname.

Хост: pbx01, под root.


asterisk -rx "database del cidname 79161234567"
Не сноси всё семейство cidname
Команда удаления дерева cidname целиком выглядит заманчиво, но в том же семействе живёт модуль Asterisk Phonebook. Если ты им пользуешься, вместе с кешем исчезнет весь телефонный справочник. Перед массовой чисткой сделай выгрузку семейства в файл, команда ниже.

asterisk -rx "database show cidname" > /root/cidname-backup-$(date +%F).txt
asterisk -rx "database deltree cidname"

Номер не находится, хотя в базе он есть

Причина: формат. Чаще всего провайдер присылает номер с префиксом, а то и вовсе с «+» и кодом страны, которого нет в твоей записи. Или наоборот: в базе 10 цифр, провайдер шлёт 7 цифр городского номера без кода. Тогда RIGHT отдаст 7 цифр, а сравнение провалится.

Решение: возьми реальный src из CDR и подставь его в ручной запрос из шага 5. Если не находит — смотри, сколько цифр приходит. Для городских без кода есть вариант: в поле Query дописать код города через CONCAT. Например, для Екатеринбурга с кодом 343:


SELECT name FROM contacts WHERE num10 = RIGHT(CONCAT('343', '[NUMBER]'), 10) LIMIT 1

Это костыль под конкретного провайдера. Сначала попробуй попросить провайдера передавать номер в полном формате — многие делают это по заявке за день.

Access denied for user ‘cid_ro’

Причина: в MariaDB пользователь определяется связкой имя плюс хост. Подключение к localhost идёт через сокет, к 127.0.0.1 — через TCP, и это могут оказаться разные учётные записи.

Решение: в источнике укажи Host = localhost. Если по каким-то причинам нужен именно 127.0.0.1, создай второго пользователя.

Хост: pbx01, клиент mariadb под root.


CREATE USER 'cid_ro'@'127.0.0.1' IDENTIFIED BY 'ТОТ_ЖЕ_ПАРОЛЬ_RO';
GRANT SELECT ON cid_directory.* TO 'cid_ro'@'127.0.0.1';
FLUSH PRIVILEGES;

Вместо номера показывается unknown

Причина: в 2022 году вышла версия cidlookup 15.0.24, которая при отсутствии имени в базе затирала исходный CallerID. Это обсуждалось в теме на форуме FreePBX и исправлено в 15.0.25 и 16.0.12.

Решение: обнови модуль. На FreePBX 17 этой проблемы быть не должно, но если сервер переносили бэкапом со старого — проверь версию.

Хост: pbx01, под root.


fwconsole ma list | grep cidlookup
fwconsole ma upgrade cidlookup
fwconsole reload

Пауза перед гудками

Причина: lookup выполняется до того, как звонок пойдёт на телефоны. Если запрос занимает две секунды, звонящий две секунды слушает тишину. Хуже всего с HTTP-источниками и удалёнными базами через VPN.

Решение: замерь время запроса. Для локальной базы с индексом это единицы миллисекунд.

Хост: pbx01.


time mariadb -u cid_ro -p cid_directory \
  -e "SELECT name FROM contacts WHERE num10 = RIGHT('79161234567', 10) LIMIT 1;"

mariadb -u cid_ro -p cid_directory \
  -e "EXPLAIN SELECT name FROM contacts WHERE num10 = RIGHT('79161234567', 10);"

В EXPLAIN в колонке key должен стоять uq_num10, а type — const. Если там ALL — индекс не используется, перепроверь шаг 3. Для удалённой базы или HTTP включай кеш и смотри на сеть.

Имя в CDR есть, на трубке нет

Причина: телефон. Старые модели не умеют UTF-8 в заголовке From, некоторые показывают только номер, если не включена опция отображения имени. Плюс обрезка по длине экрана.

Решение: обнови прошивку, проверь язык интерфейса трубки и опцию вида «Display Name» или «Caller ID Display». Если в Inbound Route задан CID name prefix, например «Продажи:», помни, что он съедает место на экране. Иногда проще убрать префикс, чем потерять фамилию клиента.

Superfecta не шлёт уведомления

Причина: функции Send to у Superfecta, будь то email, Telegram или URL, срабатывают только когда какой-то источник нашёл имя. Нет имени — нет уведомления. Это поведение, описанное разработчиками, а не баг.

Решение: для теста добавь в схему источник Abandon Lookup последним в цепочке — тогда имя найдётся всегда, и ты увидишь, работает ли отправка.

Альтернативы: Phonebook, Superfecta, HTTP API и func_odbc

Основной рецепт закрывает типовую задачу. Но бывает, что база маленькая, или данные живут в CRM, или нужна логика, которую в одно поле Query не впихнуть. Разберём каждый вариант с настройкой.

Asterisk Phonebook: когда номеров меньше пары сотен

Самый простой путь. Модуль Asterisk Phonebook хранит пары номер-имя прямо в AstDB, в семействе cidname. В источниках CallerID Lookup он подключается как тип Internal. Никакой внешней базы, никаких паролей.

Ограничения жёсткие. Совпадение только точное — «79161234567» и «89161234567» для него разные номера. Массовое редактирование неудобное. Нет истории изменений. Для отдела из десяти человек с тридцатью постоянными клиентами — нормально. Для отдела продаж с базой на пять тысяч — нет.

Записи можно добавлять в GUI, импортом CSV из интерфейса модуля, или прямо из консоли. Формат CSV для импорта у разных версий модуля отличался, поэтому сначала сделай экспорт из интерфейса и посмотри, какие колонки он ожидает.

Хост: pbx01, под root. Пример добавления записи напрямую в AstDB — номер в том формате, в каком его шлёт провайдер.


asterisk -rx 'database put cidname 79161234567 "Иванов И. Ромашка"'
asterisk -rx 'database get cidname 79161234567'

Если провайдер шлёт номера в двух форматах, придётся класть две записи. Вот почему при росте базы Phonebook быстро становится неудобным.

CID Superfecta: когда нужно несколько источников

CID Superfecta — это оркестратор. Он опрашивает несколько источников по очереди или параллельно: свою MySQL, Contact Manager, Phonebook, внешние сервисы. Умеет помечать спам, подставлять текст для подозрительных номеров, отправлять уведомления на email, в Telegram, Pushover и на произвольный URL. Схемы можно вешать на разные входящие маршруты.

Где это оправдано. Колл-центр, где нужно проверить номер в CRM, потом в общей базе, потом пометить спам. Офис, где руководитель хочет уведомление в Telegram о звонке от VIP-клиента. Если тебе нужно просто показать имя из одной таблицы — Superfecta избыточна.

Критично: уязвимость в Superfecta
В модуле Superfecta версий до 16.0.40 на FreePBX 16 и до 17.0.7 на FreePBX 17 найдена уязвимость CVE-2026-54710. Аутентифицированный пользователь может выполнить произвольный PHP-код на сервере с правами веб-сервера через обработчик AJAX модуля. Если Superfecta у тебя установлена, проверь версию прямо сейчас и обнови. Если не используешь — удали модуль целиком.

Подробности уязвимости — в карточке CVE-2026-54710 на OpenCVE. Проверка и обновление:

Хост: pbx01, под root.


fwconsole ma list | grep -i superfecta
fwconsole ma upgrade superfecta
fwconsole reload
fwconsole ma list | grep -i superfecta

Результат: версия 17.0.7 или новее. Если модуль не нужен:


fwconsole ma uninstall superfecta
fwconsole ma remove superfecta
fwconsole reload

Свои источники для Superfecta пишутся на PHP и кладутся в каталог /var/www/html/admin/modules/superfecta/sources/. Это мощно, но каждый такой файл — код, который выполняется веб-сервером. Учитывай это, когда тащишь источники с GitHub без чтения.

HTTP API: когда данные живут в CRM

Если контакты лежат в CRM, 1С или самописной системе, тащить их в отдельную таблицу не всегда разумно. Источник HTTP или HTTPS в CallerID Lookup делает GET-запрос, подставляя номер вместо токена [NUMBER] в строке запроса, и берёт тело ответа как имя.

Минимальный сервис на Flask, который отвечает именем по номеру. Для примера он читает ту же таблицу, но вместо SQL ты подставишь запрос в API своей CRM.

Хост: pbx01 или отдельный сервер приложений, под root. Пакеты из репозитория Debian 12.


apt install -y python3-flask python3-pymysql gunicorn
useradd --system --no-create-home --shell /usr/sbin/nologin cidapi
install -d -o cidapi -g cidapi -m 750 /opt/cidapi

Учётка для API — отдельный файл, читаемый только пользователем сервиса. Используй cid_ro, писать сервису не нужно.


cat > /opt/cidapi/db.cnf <<'EOF'
[client]
user=cid_ro
password=ЗАМЕНИ_НА_ДЛИННЫЙ_ПАРОЛЬ_RO
host=localhost
EOF
chown cidapi:cidapi /opt/cidapi/db.cnf
chmod 600 /opt/cidapi/db.cnf

Хост: там же, файл /opt/cidapi/app.py.


import re
import pymysql
from flask import Flask, request, Response

app = Flask(__name__)
SQL = "SELECT name FROM contacts WHERE num10 = %s LIMIT 1"


def lookup(number):
    digits = re.sub(r"\D", "", number or "")
    if len(digits) < 10:
        return ""
    conn = pymysql.connect(read_default_file="/opt/cidapi/db.cnf",
                           database="cid_directory", charset="utf8mb4",
                           connect_timeout=1, read_timeout=1)
    with conn:
        with conn.cursor() as cur:
            cur.execute(SQL, (digits[-10:],))
            row = cur.fetchone()
    return row[0] if row else ""


@app.get("/cid")
def cid():
    name = lookup(request.args.get("number", ""))
    return Response(name, mimetype="text/plain; charset=utf-8")

Тут номер передаётся в запрос параметром, а не склейкой строки. Это закрывает SQL-инъекцию полностью, в отличие от поля Query в GUI. Таймауты в одну секунду — чтобы зависшая база не держала звонок.

Юнит systemd, чтобы сервис поднимался сам. Слушаем только localhost, если API на том же сервере.

Хост: там же, файл /etc/systemd/system/cidapi.service.


[Unit]
Description=CallerID lookup HTTP API
After=network-online.target mariadb.service

[Service]
User=cidapi
Group=cidapi
WorkingDirectory=/opt/cidapi
ExecStart=/usr/bin/gunicorn --workers 2 --bind 127.0.0.1:8085 app:app
Restart=on-failure
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes

[Install]
WantedBy=multi-user.target

chown cidapi:cidapi /opt/cidapi/app.py
systemctl daemon-reload
systemctl enable --now cidapi
curl -s "http://127.0.0.1:8085/cid?number=%2B79161234567"; echo

Результат: curl печатает «Иванов И. Ромашка». В CallerID Lookup Sources создай источник типа HTTP: Host 127.0.0.1, Port 8085, Path /cid, Query number=[NUMBER]. Привяжи к маршруту так же, как в шаге 7.

Если API живёт на отдельном сервере — слушай на внутреннем IP и закрой порт firewall-ом для всех, кроме IP АТС. Для HTTP через недоверенную сеть бери тип HTTPS и нормальный сертификат.

func_odbc и Set CallerID: максимум контроля

Вариант для тех, кому мало одного поля Query. ODBC — штатный путь Asterisk к внешним базам, именно он заменил удалённый app_mysql. FreePBX 17 уже использует ODBC для записи CDR, так что драйвер MariaDB на сервере стоит.

Схема такая. Описываем DSN, в Asterisk заводим соединение res_odbc и функцию func_odbc. Функцию вызываем из модуля Set CallerID, который ставим назначением входящего маршрута перед группой вызова. Получается без правок генерируемого диалплана, всё в GUI плюс три конфига.

Главный плюс — экранирование. В func_odbc есть функция SQL_ESC, и номер звонящего попадает в запрос безопасно.

Хост: pbx01, под root. Сначала узнай имя драйвера и посмотри, какие файлы custom подключаются в конфиги Asterisk.


odbcinst -q -d
grep -n include /etc/asterisk/res_odbc.conf /etc/asterisk/func_odbc.conf 2>/dev/null
fwconsole ma downloadinstall setcid

Добавь DSN в /etc/odbc.ini. Значение Driver возьми из вывода odbcinst — на FreePBX 17 это обычно MySQL или MariaDB.


[cid-directory]
Description = CallerID directory
Driver      = MySQL
Server      = localhost
Database    = cid_directory
Port        = 3306
Socket      = /run/mysqld/mysqld.sock
Option      = 3
Charset     = utf8mb4

Проверь DSN до Asterisk — утилитой isql из unixODBC.


echo "SELECT name FROM contacts LIMIT 1;" | isql -v cid-directory cid_ro 'ПАРОЛЬ_RO'

Соединение в Asterisk. Если в res_odbc.conf подключается res_odbc_custom.conf — пиши туда. Если нет — допиши в res_odbc.conf, но помни, что этот файл может перезаписаться при обновлении.


[cid]
enabled => yes
dsn => cid-directory
username => cid_ro
password => ЗАМЕНИ_НА_ДЛИННЫЙ_ПАРОЛЬ_RO
pre-connect => yes
max_connections => 5

Функция в func_odbc — аналогично, в custom-файл, если он подключён.


[CIDNAME]
dsn=cid
readsql=SELECT name FROM contacts WHERE num10 = RIGHT('${SQL_ESC(${ARG1})}', 10) LIMIT 1
synopsis=CallerID name lookup in cid_directory

asterisk -rx "module reload res_odbc.so"
asterisk -rx "module reload func_odbc.so"
asterisk -rx "odbc show cid"
asterisk -rx "core show function ODBC_CIDNAME"

Результат: odbc show cid показывает соединение в статусе Connected, функция ODBC_CIDNAME описана. Теперь в Admin → Set CallerID создай запись: CallerID Name — ${ODBC_CIDNAME(${CALLERID(num)})}, CallerID Number — ${CALLERID(num)}, назначение — твоя группа вызова. Во входящем маршруте поставь назначением эту запись Set CallerID, а CID Lookup Source оставь None.

Одна тонкость: если имя не найдено, функция вернёт пустую строку, и на трубке будет пусто вместо номера. Лечится выражением IF в поле имени: ${IF($[«${ODBC_CIDNAME(${CALLERID(num)})}» != «»]?${ODBC_CIDNAME(${CALLERID(num)})}:${CALLERID(name)})}. Выглядит страшно, работает надёжно. Запрос при этом выполнится дважды, но по индексу это незаметно.

Синтаксис func_odbc и res_odbc подробно описан в документации Asterisk.

Итоговое сравнение альтернатив

Критерий Phonebook Lookup MySQL Lookup HTTP Superfecta func_odbc
Настройка 5 минут 30 минут 1 час 1-2 часа 1 час
Нормализация номера Нет Да, в SQL Да, в коде Да, правила Да, в SQL
Защита от SQL-инъекций Не нужна Только правами пользователя Параметры в коде Зависит от источника SQL_ESC
Несколько источников Нет Нет Сколько напишешь Да Через диалплан
Уведомления Нет Нет Сколько напишешь Да Через диалплан
Только GUI Да Да Плюс сервис Да Нет

Профилактика: чтобы база CallerID не протухла

База имён полезна, пока она актуальна. Через полгода без синхронизации она начинает врать, а через год ей перестают верить. Поэтому синхронизация, мониторинг и бэкап — не опции.

Автосинхронизация через systemd timer

Одна строка в crontab без комментария живёт дольше, чем сотрудник, который её написал. И знает о системе больше него. Поэтому вместо crontab — таймер systemd: он виден в списке, пишет лог в journal и не теряется при переезде.

Схема: CRM или скрипт выгрузки кладёт свежий CSV в /srv/cid/contacts.csv, таймер раз в час запускает импорт.

Хост: pbx01, под root. Файл /etc/systemd/system/cid-sync.service.


[Unit]
Description=Import CallerID contacts from CSV
ConditionPathExists=/srv/cid/contacts.csv

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/cid-import.py /srv/cid/contacts.csv

Файл /etc/systemd/system/cid-sync.timer.


[Unit]
Description=Hourly CallerID contacts import

[Timer]
OnCalendar=hourly
RandomizedDelaySec=300
Persistent=true

[Install]
WantedBy=timers.target

systemctl daemon-reload
systemctl enable --now cid-sync.timer
systemctl list-timers cid-sync.timer
systemctl start cid-sync.service && journalctl -u cid-sync.service -n 5 --no-pager

Результат: таймер в списке с временем следующего запуска, в журнале строка ok с количеством записей. Если включал кеш в источнике — добавь в сервис вторую строку ExecStart с удалением устаревших ключей, иначе синхронизация бессмысленна.

Скрипт импорта только добавляет и обновляет. Удалённые из CRM контакты останутся в таблице. Если это важно, раз в неделю чисти записи источника csv, которые давно не обновлялись, или перезаливай таблицу целиком в транзакции.

Мониторинг

Что мониторить: что синхронизация прошла недавно, что таблица не опустела, что MariaDB жива. Скрипт импорта трогает файл-метку /var/lib/cid-sync/last_ok при каждом успешном прогоне. Возраст этой метки — лучший индикатор.

Пример для агента Zabbix. Возраст метки берёт штатный ключ vfs.file.time[/var/lib/cid-sync/last_ok,modify], триггер — если прошло больше трёх часов. Число записей — через UserParameter.

Хост: pbx01, файл /etc/zabbix/zabbix_agent2.d/cid.conf. Учётку cid_ro положи в /etc/zabbix/.cid_ro.cnf с владельцем zabbix и правами 600.


UserParameter=cid.rows,mariadb --defaults-extra-file=/etc/zabbix/.cid_ro.cnf -Nse "SELECT COUNT(*) FROM cid_directory.contacts"

systemctl restart zabbix-agent2
zabbix_agent2 -t cid.rows

Триггер на cid.rows: резкое падение больше чем на 20% за прогон. Это значит, что кто-то выгрузил из CRM пустой или обрезанный файл.

Резервное копирование

Вот тут важно. Штатный модуль Backup в FreePBX сохраняет свою конфигурацию и свои базы. Твою cid_directory он не знает и не бэкапит. После восстановления АТС из бэкапа источник будет на месте, а таблицы не будет.

Что бэкапить Как часто Где хранить Как восстановить
База cid_directory Ежедневно Локально 30 дней плюс копия вне сервера mariadb cid_directory из дампа
Конфигурация FreePBX Ежедневно, модуль Backup Удалённое хранилище модуля Backup Модуль Restore
Скрипты и юниты При изменении Git или вместе с дампом Копирование файлов
Семейство cidname AstDB Еженедельно, если используешь Phonebook Вместе с дампом database put по списку

Хост: pbx01, под root. Скрипт /usr/local/sbin/cid-backup.sh.


#!/bin/bash
set -euo pipefail
DIR=/var/backups/cid
mkdir -p "$DIR"
mysqldump --single-transaction --default-character-set=utf8mb4 cid_directory \
  | gzip > "$DIR/cid_directory_$(date +%F).sql.gz"
asterisk -rx "database show cidname" > "$DIR/cidname_$(date +%F).txt"
find "$DIR" -type f -mtime +30 -delete

chmod 750 /usr/local/sbin/cid-backup.sh
/usr/local/sbin/cid-backup.sh && ls -lh /var/backups/cid/

Запуск по расписанию — так же, через systemd timer с OnCalendar=daily. Каталог /var/backups/cid забирай на внешнее хранилище тем же инструментом, которым бэкапишь остальное: rsync, borg, rclone.

Восстановление. Проверь его хотя бы раз до того, как оно понадобится.


gunzip -c /var/backups/cid/cid_directory_2026-10-25.sql.gz | mariadb cid_directory
mariadb -e "SELECT COUNT(*) FROM cid_directory.contacts;"

Автозапуск

Всё, от чего зависит поиск, должно подниматься само после перезагрузки. Проверь одной командой.


systemctl is-enabled mariadb asterisk cid-sync.timer
systemctl is-enabled cidapi 2>/dev/null || true

Результат: enabled для всех. Asterisk на FreePBX 17 обычно запускается через юнит freepbx, поэтому статус asterisk может быть другим — смотри systemctl is-enabled freepbx.

Безопасность

База CallerID — это список клиентов с телефонами. Персональные данные в чистом виде. Относись к ней соответственно.

Пароли и права
Пользователь, под которым работает источник MySQL, должен иметь только SELECT и только на cid_directory. Номер звонящего подставляется в текст запроса, а CallerID в SIP подделывается за минуту. Права root на MariaDB в источнике — прямой путь к утечке всей базы АТС, включая пароли SIP-учёток.

MariaDB должна слушать только localhost. Проверь:

Хост: pbx01.


ss -ltnp | grep -E ':3306|:8085'

Результат: адрес 127.0.0.1, а не 0.0.0.0. Если база слушает все интерфейсы и ты не выносил её на отдельный сервер — поправь bind-address в /etc/mysql/mariadb.conf.d/50-server.cnf и перезапусти MariaDB.

Firewall. Если используешь модуль Firewall самого FreePBX — работай в нём и не ставь параллельно UFW, два менеджера правил iptables друг друга не любят. Если модуля нет, минимальный UFW выглядит так. Вместо 203.0.113.10 — IP провайдера, вместо 10.0.0.0/24 — офисная сеть.

Хост: pbx01, под root.


ufw default deny incoming
ufw default allow outgoing
ufw allow from 10.0.0.0/24 to any port 22 proto tcp
ufw allow from 10.0.0.0/24 to any port 443 proto tcp
ufw allow from 10.0.0.0/24 to any port 5060 proto udp
ufw allow from 203.0.113.10 to any port 5060 proto udp
ufw allow 10000:20000/udp
ufw enable
ufw status numbered
Не отрежь себе SSH
Правило для SSH из твоей сети добавляй до включения UFW. Если работаешь удалённо, держи открытой вторую сессию или консоль гипервизора, пока не убедишься, что вход работает.

fail2ban. Проверь, установлен ли он и есть ли тюрьма для Asterisk.


systemctl is-active fail2ban
fail2ban-client status

Если fail2ban нет и FreePBX не закрывает перебор своими средствами — поставь его и включи тюрьму asterisk. Фильтр в Debian уже есть, путь к логу укажи на /var/log/asterisk/full.


apt install -y fail2ban
cat > /etc/fail2ban/jail.d/asterisk.local <<'EOF'
[asterisk]
enabled  = true
logpath  = /var/log/asterisk/full
maxretry = 5
findtime = 600
bantime  = 3600
EOF
systemctl restart fail2ban
fail2ban-client status asterisk

SSH hardening — отдельный файл, чтобы не трогать основной конфиг. Вместо admin — твой пользователь.

Хост: pbx01, файл /etc/ssh/sshd_config.d/10-hardening.conf.


PermitRootLogin prohibit-password
PasswordAuthentication no
MaxAuthTries 3
AllowUsers admin

sshd -t && systemctl reload ssh

sshd -t проверит синтаксис до перезагрузки. Ошибка — перезагрузка не произойдёт, и доступ не потеряешь. Заходить по ключу проверь заранее.

Обновление без сюрпризов

Обновления FreePBX трогают модуль cidlookup, Asterisk и PHP. Твоя таблица от них не зависит, но AGI-скрипт и поведение источника — зависят.

Что проверить до обновления:

  • Свежий снапшот ВМ или полный бэкап FreePBX плюс дамп cid_directory.
  • Список доступных обновлений: что именно поменяется.
  • Release notes модулей cidlookup и superfecta, если они в списке.
  • Время: обновляй вне рабочих часов, звонки во время fwconsole reload могут оборваться.

Хост: pbx01, под root.


/usr/local/sbin/cid-backup.sh
fwconsole ma showupgrades
fwconsole ma upgradeall
fwconsole reload
fwconsole ma list | grep -Ei 'cidlookup|superfecta'

После обновления сделай тестовый звонок и проверь CDR, как в разделе «Проверка». Откат — снапшот ВМ на Proxmox: qm rollback с ID машины и именем снапшота, на узле Proxmox.

Про будущее. Сообщество FreePBX называет 1 января 2027 года датой выхода FreePBX 18. Перед мажорным переходом проверь, как в новой версии работает источник MySQL, — один раз app_mysql уже убрали без особых предупреждений.

FAQ: частые вопросы про CallerID в FreePBX

Почему CallerID Lookup в FreePBX не работает после настройки?

В девяти случаях из десяти источник не выбран во входящем маршруте на вкладке Other, или номер от провайдера в другом формате, чем в базе. Проверь src в CDR и выполни запрос с этим номером вручную под пользователем источника. Если на старой системе модуль cidlookup не обновлялся, он может пытаться использовать удалённый из Asterisk app_mysql.

Как проверить, что база CallerID работает правильно?

Сделай тестовый звонок с номера из базы и посмотри поле cnam в таблице asteriskcdrdb.cdr. Параллельно в консоли asterisk -rvvvvv видно вызов AGI cidlookup и подставленное имя. Если имя в CDR есть, а на трубке нет — проблема в телефоне, а не в базе.

Что делать, если вместо имени кракозябры?

Впиши utf8mb4 в поле Character Set источника MySQL и примени конфиг. По умолчанию модуль использует latin1, кириллица в нём не живёт. Заодно проверь, что таблица создана в utf8mb4, и что CSV был сохранён в UTF-8.

Чем CID Superfecta отличается от CallerID Lookup Sources?

CallerID Lookup — один источник на маршрут: одна база, один HTTP-сервис или Phonebook. Superfecta опрашивает цепочку источников, умеет помечать спам и отправлять уведомления. Для простого показа имени из одной базы хватает Lookup Sources, Superfecta нужна для сложной логики. И держи её версии 17.0.7 или новее.

Как сбросить кеш CallerID в FreePBX?

Кеш лежит в AstDB в семействе cidname. Удаляй конкретный номер командой database del через asterisk -rx. Удаление всего семейства сотрёт и записи модуля Phonebook, так что сначала выгрузи его в файл.

Можно ли брать имена из Active Directory?

Напрямую из LDAP модуль cidlookup не умеет. Рабочая схема — скрипт, который выгружает пользователей с атрибутом telephoneNumber или mobile в CSV, и импорт из этой статьи на таймере. Для внутренних звонков между добавочными это не нужно — там имена берутся из настроек самих добавочных.

Сколько номеров выдержит база CallerID?

Phonebook в AstDB комфортен до пары сотен записей. Таблица MariaDB с уникальным индексом спокойно держит сотни тысяч номеров, время поиска остаётся в пределах миллисекунд. Узким местом становится не объём, а сеть до базы, если она удалённая.

Работает ли OpenCNAM с российскими номерами?

Для практических задач нет. OpenCNAM опирается на американскую систему CNAM, а у бесплатного тарифа ограничение 60 кешированных запросов в час. Для российских номеров базу имён придётся вести самому.

Прогноз

Что сделали. Разобрали все варианты базы CallerID в FreePBX и их ограничения. Подняли отдельную таблицу в MariaDB, которая находит номер в любом формате записи. Залили контакты скриптом, подключили источник MySQL с правильной кодировкой, привязали к входящему маршруту. Закрыли права, настроили синхронизацию, мониторинг и бэкап, который штатный модуль FreePBX сам бы не сделал.

Что теперь работает. Менеджер видит на трубке «Иванов И. Ромашка» до того, как снимет трубку. В CDR рядом с номером стоит имя — отчёт по клиентам строится одним запросом. А когда секретарь уйдёт в отпуск, справочник останется на сервере. Где ему и место.

Не заработало?
Если имя так и не появилось — пиши в комментарии, разберёмся. Приложи версию FreePBX и Asterisk, формат номера из колонки src в CDR и кусок лога с cidlookup. С этими тремя вещами диагноз ставится за пять минут.

Андрей А.
Author: Андрей А.

25 years in IT — from a field engineer at MegaFon to head of department. I rebuild IT infrastructure that’s falling apart and turn it into something predictable. I know what chaos costs. I help businesses get out of a crisis: cleaning up legacy systems, stabilizing what’s breaking, and setting up processes you can actually plan around.

Оставайтесь на связи

Рецепты от IT-боли. Без воды, без рекламы, без маркетинговой шелухи.

Подписаться на IT-Аптеку →
Поделитесь:

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх