«Интеграция с телефонией» — фраза, за которой может стоять что угодно. От «раз в сутки подтягиваем список звонков» до «менеджер разговаривает с клиентом во вкладке браузера, а карточка клиента открылась сама, ещё до того как он сказал „алло“».

Мы делаем FARA CRM и прошли этот путь целиком. Сначала перенесли рабочий модуль Asterisk. Потом привели к одному контракту облачные АТС. Потом сделали трубку прямо в браузере. А под конец обнаружили, что без собственного TURN‑релея половина этой красоты «то соединяется, то нет» — и добавили релей в поставку.

В статье три вещи:

  • какие бывают конфигурации телефонии у компаний — их ровно четыре, и они смешиваются;

  • что именно можно интегрировать в CRM — пять степеней, они накапливаются;

  • как это устроено у нас внутри.

Основные примеры — на Asterisk/FreePBX: это наш эталонный провайдер, проверенный в бою. Облачные АТС (Sipuni, МегаФон, Билайн, Манго, МТС) устроены по тому же контракту, отличия называю отдельно.


Часть 1. Четыре архитектуры: чем люди звонят

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

1. Все телефоны стационарные

Классика. В офисе АТС — Asterisk/FreePBX на своём сервере или облачная. У каждого сотрудника аппарат Yealink или Grandstream с внутренним номером, городские номера заведены транками, входящие раскидываются ring group’ами и очередями.

  • За: качество связи, привычка, ничего не «отваливается» при закрытии вкладки.

  • Против: сотрудник привязан к столу. Аппараты на 50 человек — это деньги. Удалёнка требует VPN или проброса.

2. Корпоративные симки на менеджеров

Компания покупает симки и подключает «мобильную АТС» оператора. Звонки идут по обычной сотовой сети, а оператор отдаёт CRM события и статистику по API.

Как это выглядит в жизни. Менеджеру выдают телефон — или просто вторую симку в личный. Он больше не привязан к столу и может ответить откуда угодно. Разговор идёт через трубку, но в момент ответа в CRM прилетает событие «идёт разговор с таким‑то клиентом». Менеджер смотрит на экран и сразу видит, кто звонит, сколько у него заказов и что обсуждали в прошлый раз. Экран при этом любой: компьютер дома, ноутбук, тот же телефон.

  • За: работает везде, где ловит сеть. Оборудования не надо, если обходитесь второй симкой. Администрирования тоже: интеграцию настраивают один раз.

  • Против: возможности ограничены тем, что отдаёт оператор — обычно событие о звонке и запись, иногда с задержкой и во времени UTC. Своей логики маршрутизации не написать. Внутренние звонки между сотрудниками — обычные сотовые.

3. Софтфон на устройстве сотрудника

Тот же внутренний номер АТС, но поднят не аппаратом, а программой. MicroSIP на рабочем компьютере, Zoiper или Linphone на смартфоне. АТС видит ровно такой же SIP‑endpoint, как у железного телефона. Устройство любое: корпоративное или личное.

  • За: оборудование не нужно, сотрудник мобилен, вся логика АТС остаётся на месте.

  • Против: у каждого своя гарнитура и свои проблемы со звуком. Софтфон на смартфоне в фоне засыпает — нужны push‑уведомления (отдельная возня) или постоянный VPN до АТС. Настраивать звонилки на десятке разных устройств тоже отнимает время.

4. Звонки прямо из CRM, без оборудования и без SIP‑приложений

Ни аппаратов, ни установленных программ. Внутренний номер регистрируется прямо во вкладке браузера по WebRTC, микрофон — обычная гарнитура. Логин и пароль линии лежат в самой CRM и там же меняются. По сути SIP‑телефон встроен в CRM, а звук идёт по каналу WebRTC.

Частный случай, который встречается чаще, чем кажется: тот же логин и пароль сотрудник вбивает в MicroSIP на своём телефоне. Учётка линии одна, а поднять её можно чем угодно — браузером, софтфоном, аппаратом.

  • За: развернуть = завести сотрудника в CRM. Всё под рукой: набор, книжка, история, карточка клиента.

  • Против: жёстче требования к АТС (WebRTC, DTLS‑SRTP, WSS) и к сети — про сеть будет отдельная глава. И чуть больше возни с настройкой: на extension нужно включить шифрование и задать SIP‑пароль, а потом вбить этот пароль в CRM. Если у сотрудника уже стоит стационарный аппарат, для браузера придётся завести отдельный extension: один и тот же аппарату и вкладке не подойдёт (почему — в части 4). Дело нескольких минут, но пропустить этот шаг нельзя.

Главный вывод: CRM не должна знать про устройства

FARA поддерживает все четыре конфигурации, и это не четыре интеграции, а одна. Потому что во всех четырёх случаях в CRM существует одна и та же сущность — линия: внутренний номер, привязанный к сотруднику. Аппарат, симка, софтфон и вкладка браузера — это лишь способ её поднять.

                ┌──────── одна сущность в CRM ────────┐
                │   phone_number: линия и сотрудник   │
                └──────────────────┬──────────────────┘
                                   │
        ┌──────────────┬───────────┴──────────┬──────────────┐
        │              │                      │              │
   аппарат на      симка в              софтфон на       вкладка
   столе (1)      телефоне (2)         смартфоне (3)   браузера (4)

Ровно поэтому в FARA есть отдельная модель phone_number — реестр линий, который синхронизируется с АТС. От него дальше считается всё: направление звонка, наша линия, оператор, признак внутреннего звонка. Об этом в третьей части.


Часть 2. Пять степеней интеграции

Степени накапливаются: каждая следующая имеет смысл, только если сделана предыдущая.

Степень 1. История звонков

Журнал: кто, кому, когда, сколько говорили, чем закончилось.

Если телефония облачная, история берётся одним HTTP‑запросом. Так у всех провайдеров, тут всё просто.

С Asterisk сложнее, и вариантов два.

CDR — таблица в базе АТС, обычно MySQL. Плюс: можно запросить историю за любой период. Минус: в ней лежат не «звонки», а события — переадресации, перехваты, каждая нога отдельной строкой. Чтобы получить один звонок, строки надо схлопывать по uniqueid или linkedid.

Живые события — AMI или ARI. Слушаем АТС и сами видим, когда звонок начался и когда закончился. Плюс: всё в реальном времени. Минус: историю за прошлый месяц так не получишь, а событий в десятки раз больше, чем звонков — хранить их все, чтобы посчитать потом, дорого.

В FARA работают оба. ARI даёт живые события: по ним показываем менеджеру всплывающую карточку. А когда звонок закончился, идём в CDR за точными данными.

история звонков без записей
история звонков без записей

Степень 2. Записи разговоров

Запись лежит на АТС или у оператора. CRM должна её забрать и положить рядом со звонком, чтобы слушать прямо из браузера. Для Asterisk мы написали отдельный агент с открытым кодом: он живёт на сервере АТС, имеет доступ к файлам записей и отдаёт их в CRM по запросу.

Формат записи определяем по содержимому файла, а не по расширению: FreePBX пишет MixMonitor в WAV, облачные провайдеры обычно отдают mp3. Иначе браузерный плеер получает неверный Content‑Type и молча ничего не играет.

история звонков с записями разговоров
история звонков с записями разговоров

Степень 3. Связь звонка с сущностями CRM

Звонок сам по себе — строчка в журнале. Ценность появляется, когда он привязан к партнёру, лиду, сделке. Сюда же лидогенерация: незнакомый номер превращается в нового партнёра и лид.

У нас звонок проходит тот же путь, что и входящее сообщение из мессенджера: номер → контакт → партнёр → лид. Буквально тот же код, см. часть 3.

Честно про маркетинговую атрибуцию. Звонок хранит нашу линию (phone_number_id), поэтому отчёт «сколько звонков пришло на какой номер» строится напрямую по таблице: номер с сайта, номер из Яндекса и номер с визиток вы увидите отдельно. А динамической подмены номера — call tracking в чистом виде, с UTM на конкретный звонок — мы не делаем.

Степень 4. Онлайн‑события: карточка на экране

Менеджер снял трубку — на экране всплыла карточка: кто звонит, какой партнёр, какой лид, ссылки на них. Это не «удобство». Это 10–15 секунд на каждом звонке и отсутствие «а вы, простите, кто?».

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

Карточку надо показывать на ответе, а не на дозвоне. Пока идёт дозвон, неизвестно, кто возьмёт трубку: ring group звонит пятерым. Показать всем пятерым и потом погасить четверым — мигание, которое раздражает сильнее, чем отсутствие карточки.

Карточка идёт только тому, чья линия ответила — не всем менеджерам сразу. Никакого «распределения звонков» в CRM при этом нет и не нужно: кто ответил, решает АТС, а CRM про это просто узнаёт.

всплывающая карточка звонка (на примере внутреннего звонка в чате FARA)
всплывающая карточка звонка (на примере внутреннего звонка в чате FARA)

Степень 5. Звонилка в CRM

Исходящие и входящие со звуком прямо в браузере: набор номера, книжка контактов, приём входящего, mute, DTMF, плашка разговора.

Ограничение честное, и назову его сразу: это возможно только там, где АТС умеет WebRTC. У нас это Asterisk/FreePBX. Облачные АТС отдают события и историю, то есть степени 1–4, но своей трубки в браузере через них нет — WebRTC‑плечо они не дают.

набор номера в браузере — звонок на сотовый через Asterisk
набор номера в браузере — звонок на сотовый через Asterisk

Сводка

Степень

Asterisk / FreePBX

Sipuni, МегаФон, Билайн, Манго и прочие облачные

1. История звонков

✅ CDR + живые ARI‑события

✅ API истории

2. Записи разговоров

✅ в объёме, который отдаёт провайдер

3. Партнёр / лид / линия

4. Живая карточка на ответе

5. Трубка в браузере

✅ WebRTC

Внутренние сотрудник↔сотрудник

✅ и вообще без АТС

✅ и вообще без АТС

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


Часть 3. Как это устроено внутри

3.1. Транспорт: агент рядом с АТС

   ┌──────────┐   ARI-события (POST на webhook)   ┌──────────────┐
   │ Asterisk │ ────────────────────────────────> │              │
   │ FreePBX  │                                   │   FARA CRM   │
   │          │ <──────────────────────────────── │              │
   └────┬─────┘   REST: CDR, записи, номера       └──────────────┘
        │              (HTTP Basic-auth)
   ┌────┴───────────┐
   │ asterisk-agent │  FastAPI рядом с АТС: слушает ARI по веб-сокету,
   │  (отдельный)   │  ходит в базу CDR, читает файлы записей с диска
   └────────────────┘

Почему агент, а не прямое подключение CRM к АТС. ARI‑веб‑сокет требует постоянного соединения. База CDR — доступа к MySQL FreePBX. Записи — доступа к каталогу на диске АТС. Держать всё это хозяйство внутри CRM мы пробовали и отказались.

В итоге транспорт у Asterisk стал таким же, как у облачных провайдеров — мы как будто сделали его облачным. Событие приходит на тот же универсальный webhook, что у Манго или Билайна. Телефония перестала быть особенной.

3.2. Контракт провайдера — четыре метода

Стратегия провайдера — это только транспорт: разобрать формат и сходить в API. Решений «что делать со звонком» в ней нет.

class PhoneStrategyBase(ChatStrategyBase):
    """Стратегия — ТРАНСПОРТ. Решения по событию принимает IncomingCallPipeline."""

    async def fetch_numbers(self, connector) -> list[dict]:
        """Линии провайдера в унифицированном виде."""

    async def fetch_call_history(self, connector, start_date, end_date) -> list[dict]:
        """История за период — в том же виде, что и webhook-события."""

    async def final_call_records(self, connector, env, adapter) -> list[dict] | None:
        """None — событие самодостаточно. Asterisk до-запрашивает CDR."""

    async def _download_call_record(self, connector, adapter) -> bytes | None:
        """Скачать запись разговора."""

Главное здесь: история и живое событие разбираются одним и тем же адаптером. Строка CDR приводится к той же форме, что и webhook‑событие, поэтому отдельного кода «для истории» просто нет.

Всё остальное живёт в базовом классе: кнопки «Проверить соединение», «Синхронизировать номера» и «Прочитать историю за период», фоновый крон и живая карточка. Добавить нового провайдера = написать адаптер события и два‑четыре хука.

3.3. Реестр линий: какие номера наши

phone_number — это то, что настроено в АТС: extension, транк, ring group, очередь. Синхронизируется кнопкой и кроном. Линии всех провайдеров приводятся к одному виду:

records.append({
    "external_id": resource,   # ключ upsert-а вместе с connector_id
    "kind": "number",          # number / trunk / group / queue
    "number": resource,
    "extension": resource,
    "name": resource,
    "user_key": resource,      # чем искать контакт сотрудника
    "raw": rec,
})

К линии привязан сотрудник. Дальше CRM знает, что 201 — это Иванов, а +7 918 — кто‑то снаружи.

3.4. Звонок — это не сообщение

В FARA уже были чаты и мессенджеры, и звонок напрашивался как сообщение особого типа в чате партнёра. Красиво: вся история общения с клиентом в одной ленте.

Не получилось по простой причине. Сообщения живут в чатах, значит на каждый звонок надо создавать чат. В том числе на внутренний 201 → 100 и на «позвонили с незнакомого номера и сразу бросили трубку». Это избыточно.

А в истории клиент хочет видеть 100% звонков, включая мусорные: по ним считают пропущенные.

Поэтому звонок — самостоятельная таблица, и все связи в ней необязательные.

class Call(AuditMixin, DotModel):
    __table__ = "call"

    connector_id: "ChatConnector" = Many2one(...)
    uniqueid: str = Char(index=True)                # ключ дедупликации

    direction: str = Selection([("incoming", "Входящий"), ("outgoing", "Исходящий")])
    is_internal: bool = Boolean(default=False)      # сотрудник↔сотрудник
    disposition: str = Selection([...])             # отвечен / пропущен / занято / …

    number_from: str | None = Char()
    number_to: str | None = Char()
    started_at: datetime | None = Datetime()
    duration: int = Integer(default=0)
    duration_talk: int = Integer(default=0)

    # Все связи опциональны — звонок пишется даже без привязки:
    phone_number_id: "PhoneNumber" = Many2one(...)  # наша линия
    partner_id: "Partner" = Many2one(...)           # клиент
    lead_id: "Lead" = Many2one(...)
    record_id: "Attachment" = Many2one(...)         # запись разговора

В ленту чата партнёра звонок всё‑таки попадает, но подмешивается уже на чтении: Call.list_for_chat отдаёт его как виртуальное сообщение. Хранение — в своей таблице, показ — там, где нужно.

3.5. Что происходит, когда звонок закончился

Простой пример: клиент с номера +7 918 111–22-33 позвонил на внутренний 201, поговорили 40 секунд.

  1. АТС присылает событие «звонок завершён».

  2. Смотрим обе стороны в реестре линий. 201 наша, +7 918 чужая — значит звонок входящий, наша линия 201, клиент +7 918.

  3. Ищем контакт +7 918 и его партнёра. Не нашли — заводим нового.

  4. Заводим лид или переиспользуем свежий.

  5. Пишем строку в call, а запись разговора забираем в фоне.

Шаги 3 и 4 — буквально те же методы, что у входящего сообщения из Telegram. Телефонный адаптер прикидывается сообщением (номер клиента = автор), поэтому пайплайн звонка наследует пайплайн сообщений. Один раз чиним правило «новый лид или старый» — чинится везде.

Направление считаем сами, от своей линии. В Asterisk нет понятия «входящий/исходящий»: это свойство точки зрения, а не звонка. Правило простое — смотрим, где оказался наш номер:

Звонок

Наш номер

Что записываем

8918 → 201

вызываемый

входящий, линия 201

201 → 8918

звонящий

исходящий, линия 201

201 → 100

обе стороны наши

внутренний

8918 → 8925

ни одной нашей

входящий, линии нет (транзит через АТС)

Клиент — всегда та сторона, которой нет в реестре линий.

История приезжает по тому же пути. Кнопка «Прочитать историю за период» и ночной крон гоняют строки CDR через тот же пайплайн, с двумя отличиями: карточку не показывают (звонок давно закончился) и лиды по старым звонкам заводят, только если попросили. Повторный импорт безопасен — звонок пишется upsert‑ом по uniqueid.

3.6. Живая карточка

Транспорт карточки — три строки, и это правильно: он ничего не решает и в базу не ходит.

class CallCard:
    """Карточка эфемерная: в БД не пишется, звонок попадёт в реестр отдельно."""

    @classmethod
    async def show(cls, env, user_id: int, call: dict):
        await env.apps.chat.chat_manager.send_to_user(
            user_id, {"type": "call.incoming", "call": call})

    @classmethod
    async def hide(cls, env, user_id: int, number: str):
        await env.apps.chat.chat_manager.send_to_user(
            user_id, {"type": "call.ended", "call": {"number": number}})

Карточка едет по тому же веб‑сокету, что и сообщения чата: отдельного канала для телефонии нет. На фронте минимум состояния — подписка на два события, минимальное время показа 10 секунд (чтобы короткий звонок не мигнул) и авто‑скрытие через 30 минут, если call.ended потерялся.


Часть 4. Трубка в браузере

Библиотека

JsSIP 3.13.8 (MIT). Альтернатива — SIP.js, но его последний релиз 0.21.2 датирован 27 октября 2022 года, а у JsSIP коммиты этого года. Типы отдельно ставить не надо: @types/jssip помечен deprecated именно потому, что библиотека везёт свои.

Топология: SIP‑сокет проксирует сам бэкенд

   браузер ──wss://crm/ws/sip──> FARA ──ws://10.0.0.5:8088/ws──> Asterisk
      │                     (только SIP-сигналинг)                  │
      └──────────────── RTP/DTLS: звук идёт НАПРЯМУЮ ───────────────┘

Почему не напрямую из браузера в АТС:

  • CSP. У проекта connect-src 'self' wss://$host — браузер просто заблокирует соединение на домен АТС;

  • не через nginx — тогда адрес АТС пришлось бы хардкодить в шаблон конфига, который собирается на деплое. В бэкенде адрес живёт в настройках коннектора и меняется из интерфейса;

  • не через агент — RTP/DTLS согласуется в SDP напрямую с Asterisk и идёт мимо любого прокси, то есть проксируется только сигналинг. Зато агент из «поставщика данных» стал бы точкой отказа для самих разговоров.

Сам прокси — это две задачи, перекладывающие кадры:

@router_public.websocket("/ws/sip")
async def sip_ws_proxy(websocket: WebSocket):
    ...
    # Каким коннектором регистрируемся — говорит браузер. Проверяем, что линия
    # сотрудника в нём действительно есть: иначе через нас можно было бы
    # достучаться до любой чужой АТС.
    connector_id = int(websocket.query_params.get("connector") or 0)
    lines = await _my_lines(env, sessions[0].user_id.id)
    ...
    # JsSIP и Asterisk договариваются по субпротоколу 'sip' — эхо ОБЯЗАТЕЛЬНО,
    # иначе браузер разорвёт соединение сразу после рукопожатия.
    await websocket.accept(subprotocol="sip")
    async with ws_connect(url, subprotocols=[Subprotocol("sip")]) as pbx:
        await _pipe(websocket, pbx)

Трафика тут мало: SIP‑сообщения текстовые, и их единицы на звонок. Звук через CRM не идёт.

Что настроить на FreePBX

Чек‑лист мы вывели прямо в форму коннектора, чтобы не пересказывать его в переписке:

  • на extension включить WebRTC: webrtc=yes. Это разом включает rtcp_mux, use_avpf, ice_support, use_received_transport и ставит media_encryption=dtls;

  • WSS‑транспорт с доверенным сертификатом (самоподписанный wss не поднимет), TCP 8089 наружу;

  • UDP 10000–20000 с АТС наружу — всегда;

  • сама CRM по https, иначе navigator.mediaDevices в браузере просто не существует;

  • сотруднику завести контакт со значением его extension и синхронизировать номера, иначе звонки из браузера не привяжутся к нему.

Отдельный extension для браузера нужен, если у сотрудника есть ещё и аппарат на том же номере. Причина не в max_contacts, как обычно пишут, а в том, что медиа‑профиль задаётся на эндпоинте: обычная трубка на plain RTP/AVP не поднимет endpoint с DTLS‑SRTP. Вторая причина слабее, но тоже реальна. При max_contacts > 1 штатный Dial(PJSIP/${EXTEN}) во FreePBX звонит только на последнее зарегистрировавшееся устройство. Чтобы звонили все, нужен Dial(${PJSIP_DIAL_CONTACTS(${EXTEN})}), то есть правка диалплана.


Часть 5. Внутренние звонки — вообще без АТС

Приятная штука: сотрудники могут звонить друг другу, не задействуя телефонию вообще. Никакой АТС, никаких extension. WebRTC один на один, сигналинг идёт по тому же веб‑сокету, что и чат, а кто сейчас в сети — оттуда же. Работает даже у клиента, у которого телефонии нет в принципе.

В трубке в шапке пункт «Внутренний (сотруднику)» есть всегда, а телефонные каналы добавляются, если настроены. Сделано это отдельно от SIP‑звонилки, и намеренно: там адресат — номер, здесь — пользователь CRM. Общего кода почти нет, а объединять их значило бы переписать работающую логику ради красоты.


Часть 6. Встроенный TURN — то, без чего половина этого не соединяется

Зачем

WebRTC собирает соединение перебором кандидатов. В дружественной сети хватает своих адресов и STUN. Не хватает в двух очень частых случаях:

  • симметричный NAT — адрес, который узнал STUN, не подходит для другого участника;

  • офис с закрытым UDP наружу — совершенно типовая корпоративная политика.

В обоих случаях нужен релей: сервер с публичным адресом, через который идёт звук. Без него звонки «то соединяются, то нет». Это худший вид проблемы, потому что воспроизводится не у всех и не всегда.

Вариант «а вы поднимите себе coturn» мы отвергли: это перекладывание на клиента задачи, которую он не умеет диагностировать. Релей должен ехать в коробке.

Почему coturn, а не свой на Python

Мы всерьёз считали свой. По нагрузке проходило: около 6k pps на 30 одновременно говорящих, для Python это немного. Но:

  • TCP/TLS‑плечо — это половина работы, и именно оно закрывает главный кейс, офис с закрытым UDP. UDP‑релей без него бесполезен ровно там, где релей и нужен;

  • релей смотрит в интернет, и написать его самому — это своими руками сделать дыру во внутреннюю докер‑сеть.

coturn: примерно день против недели с лишним. Тот случай, когда «написать самим» — соблазн, а не решение.

Единая точка выдачи ICE

Было плохо: у внутренних звонков захардкоженный Google STUN, у SIP‑звонилки строка в настройках коннектора. Отсюда классическое «у одних работает, у других нет» и починка в двух местах по‑разному.

Стало: одна ручка GET /ice/servers и один хук useIceConfig() на оба сценария.

Схема кредов — TURN REST API. Сервер релея не знает про пользователей вообще, ему достаточно общего секрета:

def make_credentials(secret: str, ttl: int, user_id: int):
    """username   = "<unixtime истечения>:<user_id>"
       credential = base64(HMAC-SHA1(secret, username))"""
    expires_at = int(time.time()) + max(ttl, 60)
    username = f"{expires_at}:{user_id}"
    digest = hmac.new(secret.encode(), username.encode(), hashlib.sha1).digest()
    return username, base64.b64encode(digest).decode("ascii"), expires_at

user_id идёт в username открытым текстом. Это не секрет, зато в логах релея видно, чья аллокация. Заводить и удалять сотрудников на релее не нужно вообще.

Транспорты отдаём все сразу, браузер сам переберёт и возьмёт первый рабочий:

urls = [f"turn:{host}:{port}?transport=udp",
        f"turn:{host}:{port}?transport=tcp"]
if settings.tls_port:
    urls.append(f"turns:{host}:{tls_port}?transport=tcp")

Что нужно сделать руками

Почти ничего. Релей едет в том же docker‑compose и стартует вместе с остальными сервисами. Адрес по умолчанию берётся из домена, на котором открыта CRM: релей стоит на той же машине, второй раз его прописывать незачем. Адрес, порты и срок жизни кредов правятся в системных настройках и применяются со следующего звонка, без перезапуска.

Единственный обязательный шаг — открыть порты в фаерволе: 3478/udp, 3478/tcp и диапазон релей‑портов. Релей работает в host‑сети, а она правила ufw не обходит.

В форме коннектора есть кнопка «Проверить релей». Она делает настоящую аллокацию настоящими кредами, а не просто стучится в порт. Зелёный ответ означает «релей жив и секрет совпал».

Безопасность релея

Релей смотрит в интернет и по определению умеет пересылать трафик. Два правила, без которых он превращается в подарок:

# Релей ЧУЖОГО TCP (RFC 6062) не нужен никому: WebRTC передаёт медиа по UDP,
# а TURN-over-TCP — это транспорт ДО релея, он остаётся. Без этой строки любой
# сотрудник с выданными кредами превращает сервер CRM в открытый TCP-прокси.
no-tcp-relay

denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.168.0.0-192.168.255.255
...
# Те же приватные сети, записанные как IPv4-mapped IPv6 (::ffff:10.0.0.1).
# Если сервер слушает IPv6, такой адрес — обходной путь к тем же машинам.
denied-peer-ip=::ffff:10.0.0.0-::ffff:10.255.255.255

Второе правило тонкое: запретить 10.0.0.0/8 и забыть про ::ffff:10.0.0.0 значит не запретить ничего. Иначе любой, кто получил креды — а получает их каждый сотрудник, — дотянется через релей до postgres и до соседей по докер‑сети.

Ещё одна вещь, которую часто считают неправильно: диапазон релей‑портов считается не по звонкам, а по аллокациям. Браузер берёт отдельную аллокацию на каждый turn‑URL, а udp, tcp и tls — это три разных. Значит один участник разговора может занять до трёх портов. 500 портов — это примерно 160 одновременных участников с релеем.

И отдельно: если АТС стоит в той же приватной сети, её адрес надо вернуть обратно через --allowed-peer-ip, иначе браузер не сможет говорить с ней через релей.


Коротко

  • Реестр линий — единственное, что CRM обязана знать про телефонию. Аппарат, симка, софтфон и вкладка браузера — это способы поднять линию, а не четыре разные интеграции.

  • Звонок — самостоятельная сущность, все связи необязательные. В историю он попадает всегда, в ленту чата подмешивается на чтении.

  • Обработка звонка — наследник обработки сообщения. Путь «незнакомый номер → партнёр → лид» не должен существовать в двух экземплярах.

  • Стратегия провайдера — только транспорт. Четыре хука, а история и живое событие идут одним и тем же путём.

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

Если у вас есть свои грабли по этой теме — особенно по WebRTC за корпоративным NAT — пишите в комментариях. Это ровно та область, где чужой опыт экономит недели.

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