Как я научил карманный роутер Mudi 7 звонить через SIM-карту

В обзоре Mudi 7 я разбирал этот карманный роутер: начинку, экран, батарею и то, что в нём не понравилось. В списке недостатков мельком написал: голосового тракта у модема нет, сделать звонилку без плясок не получится. Про «без плясок» я не ошибся.

Внутри роутера стоит SIM-карта. У неё есть номер, на этот номер можно позвонить. Но пользоваться им как обычной телефонной линией роутер из коробки не умеет. Интернет раздаёт, SMS показывает, а для разговора — переставляй симку в телефон.

Мне захотелось иначе: SIM остаётся в Mudi, а звонки приходят на телефоны в его Wi-Fi. С них же можно набрать обычный номер и позвонить через эту SIM. Без отдельного сервера и без покупки ещё одного GSM-шлюза.

В итоге получился MudiVoLTE — пакет для OpenWrt, который делает именно это. На телефонах работает Linphone — бесплатный SIP-клиент для Android и iPhone, на роутере — мой демон, со стороны оператора это VoLTE-абонент. Проверено на Mudi 7 с реальной SIM европейского оператора, Android и iPhone, со звуком в обе стороны. Через Tailscale звонить можно и вне дома. Ниже — рабочая архитектура и путь звонка: от SIM и сети оператора до обычного SIP-клиента.

В России это не заработает. В прошивке модема есть региональная блокировка: в российских сетях модем Mudi 7 не регистрируется, и штатными средствами это не обойти. Всё, что ниже, — для SIM-карт и сетей за пределами РФ.

Я не специалист по телекому. В VoLTE, IMS и устройстве модемов Qualcomm я разбираюсь посредственно и во многом опирался на ИИ. То, что работает, я проверял на живом железе и настоящих звонках, но в объяснениях наверняка есть неточности. Если заметите ошибку — смело поправляйте меня в комментариях. Пакет тоже можно дорабатывать: исходники открыты, а удачные доработки присылайте пулреквестами в репозиторий.

Коротко: поставить и позвонить

Если хочешь просто попробовать — три шага:

  1. Подключи компьютер к Mudi 7 и войди на роутер по SSH. Пароль — тот же, что от веб-панели роутера:

ssh root@192.168.8.1
  1. Запусти установщик:

wget -O- https://github.com/xarvel/MudiVoLTE/releases/latest/download/install.sh | sh

Он проверит совместимость, скачает пакет, переключит модем в нужный режим и перезагрузит роутер (Wi-Fi пропадёт на 2–3 минуты).

  1. Подключи телефон к Wi-Fi роутера и установи Linphone (Android или iPhone). Когда роутер перезагрузится, снова зайди на него по SSH и выведи QR-код аккаунта:

mudi-volte phone show phone

phone — имя первого аккаунта, его создаёт установщик. Отсканируй код в Linphone, и аккаунт настроится сам. Тот же QR на 90 секунд можно вывести на экран самого Mudi: mudi-volte phone screen phone. QR заодно настраивает шифрование: SIP пойдёт по TLS, звук — по SRTP.

Всё — входящие звонки на SIM будут приходить в Linphone, с него же можно звонить наружу. Команда mudi-volte status покажет, что линия зарегистрирована. Звук проще всего проверить звонком из Linphone на эхо-номер 600: он не использует сотовую сеть, и в трубке слышен собственный голос.

Для надёжных входящих в фоне можно дополнительно подключить бесплатный push-сервис Linphone. Настройка описана в README проекта; для первого звонка она не нужна.

Для начала: что мы вообще пытаемся заставить звонить

Mudi 7, он же GL-E5800, — карманный роутер с 5G, Wi-Fi 7, аккумулятором и экраном. В моём экземпляре модуль Quectel RG650V-EU, прошивка GL.iNet 4.10.0 на OpenWrt 23.05.4, ядро Linux 5.15.170-perf.

Модем здесь не подключён к роутеру — он сам и есть роутер. Linux с OpenWrt работает прямо на чипе модема — Qualcomm SDX72 внутри модуля RG650V. В привычной схеме модем — отдельное устройство вроде USB-свистка: его можно перезапустить, а компьютер продолжит работать. Здесь команда перезагрузки модема AT+CFUN=1,1 перезагружает весь роутер — вместе с Wi-Fi, SSH и моим сеансом отладки.

В обзоре я винил в отсутствии голоса прошивку GL.iNet, а дело в самом модуле: Quectel позиционирует RG650V как модуль для передачи данных. Но «нет поддерживаемой голосовой функции» и «внутри вообще ничего не знает о звонках» — разные вещи. Как выяснилось, сигнализация там есть. Готового пути от разговора к микрофону и динамику — нет. И вот это различие съело большую часть исследования.

VoLTE на пальцах: сигнализация и звук

Обычный телефонный звонок кажется одной операцией: нажал кнопку — говоришь. Внутри это две довольно независимые задачи.

Первая — договориться о звонке. Кто кому звонит, где находится абонент, свободен ли он, какие кодеки поддерживает, когда поднял трубку и когда положил.

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

В VoLTE обе задачи работают поверх IP. Но не в смысле «открыл произвольный SIP-сервер в интернете». Оператор использует IMS — IP Multimedia Subsystem, свою инфраструктуру мультимедийной связи. VoLTE — голосовая услуга через IMS поверх LTE.

Обычно модем поднимает отдельное подключение к операторской сети для IMS. Обычный интернет идёт своим путём, телефонная сигнализация и голос — своим. Если вы встречали APN ims в настройках Android, это как раз оттуда.

Если читали мою статью про WebRTC, разделение на сигналинг, SDP и медиапоток уже знакомо. Только вместо браузеров, ICE и TURN здесь SIM-карта и операторский IMS.

И ещё: значок 5G на роутере не значит, что и разговор идёт по 5G. Голос поверх 5G называется VoNR — Voice over New Radio, и ему нужна «самостоятельная» сеть 5G SA. В моём стенде была 5G NSA: там 5G ускоряет интернет, а управление и голос остаются на LTE. Поэтому исследовал я именно VoLTE, а VoNR не проверял.

Почему я не использовал встроенный голосовой тракт

Начал я с очевидного: поискать способ включить голос в самом модеме. AT-команды — старый текстовый интерфейс управления модемами. Современные Quectel всё ещё понимают команды вида AT+..., только теперь за ними стоят LTE, 5G, SIM-приложения и довольно сложная прошивка.

На форуме Quectel нашлась подсказка для родственного модуля: параметр voice_disable со значением 2. Да, чтобы включить голос, нужно записать двойку в параметр с названием «отключить голос». Ноль, который хотелось попробовать первым, не помог. Ох уж эти проприетарные протоколы.

Встроенный IMS-клиент после этого действительно звонил, но устойчивого двустороннего доступа к его аудио из Linux не было. Короткие эксперименты с DSP и диагностическими логами позволили увидеть сигнализацию и получить запись входящего голоса, но не дали интерфейса для отправки речи обратно. Этого было достаточно для вывода: не вытаскивать звук из закрытого тракта, а обойти его.

Рабочая архитектура: внешний IMS-стек

Правильный путь оказался проще попыток встроиться в закрытый голосовой тракт Qualcomm. Модем остаётся радиочастью и даёт Linux доступ к SIM и отдельной IMS-сети. Сам IMS-клиент работает в OpenWrt, а микрофон и динамик остаются на телефоне с Linphone. У Quectel этот режим называется External VoLTE Stack.

Установка один раз переводит модем во внешний режим, и эта настройка остаётся в нём. Дальше демон работает с SIM первого слота и поднимает для IMS интерфейс rmnet_dataN. После этого сигнализация и голос становятся обычными IP-пакетами, которые можно обработать в Linux. Закрытый голосовой DSP-тракт модема при этом не используется.

Как попасть в IMS

Откуда берётся операторский SIP-сервер

Демон находит профиль с APN ims и поднимает для него отдельное PDN-подключение. PDN — Packet Data Network: в данном случае IP-подключение к операторской сети для IMS.

После подключения AT+CGCONTRDP возвращает адрес, DNS и адреса P-CSCF — Proxy Call Session Control Function. Это первый SIP-узел оператора, с которым общается наш клиент. Свой IMS-сервер я не строю: для сети оператора демон — просто абонент.

Всё, что идёт к IMS, должно уходить через это PDN-подключение. На одном из живых звонков звука не было: RTP к медиашлюзу оператора уходил маршрутом по умолчанию, через интернет-подключение. Помогла привязка сокета к IMS-интерфейсу (SO_BINDTODEVICE), после неё звук пошёл в обе стороны.

SIM вместо пароля

Обычный SIP-аккаунт часто выглядит как сервер, логин и пароль. Для операторского IMS пароль от SIM никто не выдаёт.

Здесь работает AKA — Authentication and Key Agreement. У SIM есть долговременный секрет, который знает и оператор. Задача клиента — передать вызов аутентификации карте и вернуть корректный ответ, не извлекая этот секрет.

Процедура выглядит так:

  1. Демон отправляет SIP REGISTER: хочу зарегистрировать абонента, вот мои параметры.

  2. Сеть отвечает 401 Unauthorized и присылает вызов AKA — случайное значение RAND и токен аутентификации AUTN.

  3. Демон передаёт их SIM. В текущей реализации это APDU-команда AUTHENTICATE, доставленная через AT+CSIM. APDU — формат запросов и ответов смарт-карты.

  4. SIM проверяет подлинность сети и возвращает RES — ответ, а также сессионный ключ целостности IK и ключ шифрования CK.

  5. Демон настраивает защиту соединения и повторяет REGISTER с SIP Digest-аутентификацией AKAv1-MD5, где при расчёте ответа используется RES.

  6. Приходит 200 OK. Абонент зарегистрирован.

Дальше SIP с оператором идёт внутри IPsec: ключи приходят из AKA, а пакеты защищает ядро Linux. Демон предлагает только защиту целостности, без шифрования (ealg=null), и сеть на это соглашается.

Что происходит, когда кто-то звонит

После регистрации сеть знает, куда доставить вызов. Сам звонок управляется тем же протоколом, что и регистрация, — SIP, Session Initiation Protocol.

SIP на пальцах

SIP текстовый и похож на HTTP: запрос начинается с метода и адреса, затем идут заголовки, пустая строка и тело. В звонке телом обычно будет SDP — описание параметров звука.

INVITE приглашает в разговор, 180 Ringing сообщает о вызове, 200 OK подтверждает ответ, а ACK завершает установление сеанса. BYE заканчивает начавшийся разговор, CANCEL отменяет вызов до ответа. Заголовок Call-ID связывает сообщения одного вызова.

Для входящего звонка упрощённая последовательность выглядит так:

Оператор MudiVoLTE Linphone | | | |------ INVITE ---------->| | || | |<----- 180 Ringing ------| |<----- 180 Ringing ------| | | || || | | | | ||| | | | |------ BYE ------------->|------ BYE ------------->| |<----- 200 OK -----------|<----- 200 OK -----------|

С оператором демон обменивается SIP по TCP, после аутентификации — внутри ESP. Локальные телефоны могут использовать UDP, TCP или TLS; настроенные по QR ходят по TLS. Голос на обоих участках идёт отдельным RTP-потоком по UDP.

В живой сети к этой последовательности добавляются PRACK, который подтверждает промежуточные ответы, и UPDATE, который меняет параметры уже начатого сеанса. А у разговора и регистрации разные таймеры: re-REGISTER продлевает присутствие абонента в IMS, а SIP session timer проверяет, что конкретный разговор ещё жив.

SDP: договориться о звуке

В теле SIP-сообщений передаётся SDP — Session Description Protocol. Название историческое: по сути это текстовый формат описания сеанса, а не ещё один транспорт.

Например, такой фрагмент может описывать аудио на стороне IMS:

c=IN IP4 192.0.2.10m=audio 40000 RTP/AVP 101 116a=rtpmap:101 AMR-WB/16000a=fmtp:101 mode-set=0,1,2a=rtpmap:116 telephone-event/16000

Здесь сказано: отправляй RTP на указанный IP и UDP-порт, для полезной нагрузки 101 используй AMR-WB с частотой дискретизации 16 кГц, поддерживаемые режимы — 0,1,2; 116 обозначает события тонального набора.

Число 101 само по себе не означает AMR-WB. Это динамический идентификатор, его смысл стороны определили в rtpmap. В другом звонке тот же кодек вполне может оказаться под другим номером. Попытка угадывать кодек по одному PT уже подарила мне неправильно декодированную запись.

Согласовать нужно не только название кодека. У AMR есть разные режимы битрейта и способы упаковки в RTP. В моих звонках сеть использовала в том числе bandwidth-efficient: поля упакованы по битам, не обязательно по границам байтов. «Почти правильный» парсер здесь выдаёт вполне настоящий мусор.

Куда наконец едет голос

После согласования параметров начинается RTP — Real-time Transport Protocol, в нашем случае поверх UDP. Каждый пакет несёт небольшой кусок закодированного звука, номер последовательности и временную метку. Это позволяет заметить потерю, восстановить порядок и понять, когда воспроизводить звук. RTP сам по себе не гарантирует доставку и не шифрует разговор.

RTCP — соседний протокол для отчётов о потере, джиттере и синхронизации. Сам голос он не переносит.

В текущем демоне на операторской стороне доступны AMR-WB и G.711, а на локальной — G.722 и G.711.

  • AMR-WB — широкополосная речь, основа многих HD Voice-звонков.

  • G.722 — широкополосный кодек, который хорошо поддерживают SIP-клиенты.

  • G.711, он же PCMA/PCMU, — простой узкополосный вариант для совместимости.

Поэтому роутер не просто пересылает одни и те же пакеты. Типичный путь выглядит так:

Linphone → G.722 → PCM 16 кГц → AMR-WB → сеть оператора

В обратную сторону — зеркально. PCM здесь просто несжатые отсчёты звука в памяти. Никакой физический аудиовыход на роутере не нужен.

Пакеты приходят не идеально каждые 20 мс: где-то задержались, где-то пришли сразу несколько. Между приёмом с одной стороны и отправкой на другую стоит jitter buffer: по умолчанию он накапливает три кадра, то есть около 60 мс. Это небольшая добавочная задержка ради более ровного звука. Если кадр потерялся, мост кратко повторяет предыдущий с затуханием, затем подставляет тишину.

Кнопки «нажмите 1» тоже требуют отдельной логики. DTMF можно передавать как события telephone-event по RFC 4733, а не надеяться, что звуковой тон хорошо переживёт перекодирование. Для локального клиента предусмотрен и SIP INFO.

Теперь превращаем это в телефонную линию

Телефонам в Wi-Fi нужен SIP-сервер, на котором они зарегистрируются и через который будут звонить. Поэтому на LAN-стороне появился обычный SIP-регистратор и B2BUA — Back-to-Back User Agent. За длинным названием простая идея: демон ведёт два отдельных разговора по SIP — один с оператором, второй с Linphone — и связывает их состояние и звук.

Телефон регистрируется на роутере по логину и паролю. Это локальная SIP Digest-аутентификация, не SIM AKA. Долговременный секрет SIM телефону не передаётся. По умолчанию телефон, настроенный по QR-коду, ходит к роутеру по TLS, а звук идёт по SRTP. Это защищает от прослушивания в той же Wi-Fi-сети, но не от того, кто в ней выдаёт себя за роутер: QR отключает в Linphone проверку сертификата.

Входящий звонок можно разослать подключённым телефонам. Кто первым ответил, тот забрал разговор; остальным уходит отмена. При исходящем Linphone набирает номер, а демон звонит на него через SIM.

Фан-факт: заодно получился интерком. Телефоны в Wi-Fi Mudi могут звонить друг другу по коротким номерам, например 201 и 202. Разговор остаётся внутри локальной сети: SIM в нём не участвует, минуты тарифа не тратятся. Хотел оживить SIM-карту, а собрал ещё и маленькую телефонную станцию на батарейке.

Ещё один фан-факт: наконец-то можно записывать звонки. Для Linphone разговор через SIM роутера — обычный SIP-звонок, а встроенная запись есть и на Android, и на iPhone. Запись сохраняется на телефоне: сам MudiVoLTE разговоры не записывает. Запись на живом звонке я пока не проверял.

Это не операторский VoWiFi. Телефон не строит собственное подключение к IMS через Wi-Fi и не использует SIM из роутера как свою. Он обычный SIP-клиент. Весь операторский VoLTE заканчивается на Mudi, который работает шлюзом.

В текущей реализации IMS-часть вынесена в библиотеку volte: SIP в ней разбирается с помощью sipgo, а IPsec выполняет ядро Linux. К SIM обращается сам демон, через библиотеку wwan-go. AMR-WB кодируется и декодируется готовыми библиотеками, G.722 реализован отдельно по стандарту.

А SMS тоже через SIP?

Не обязательно. SMS over IMS использует SIP MESSAGE, внутри которого лежит не просто строка текста, а структура SMS — RP-DATA и TPDU. Нужно разобрать кодировку, части длинного сообщения и отправить подтверждение на уровне SMS, а не только ответить SIP 200 OK.

Такой путь в библиотеке реализован. Но в моём случае живые SMS продолжили приходить через SGs — взаимодействие LTE-ядра со старой инфраструктурой SMS. При этом модему не обязательно уходить в 3G.

Поэтому SMS в MudiVoLTE идут двумя разными дорогами. Между сетью и роутером — штатным путём модема: входящие принимает и складывает SMS-сервис GL.iNet, исходящие уходят через его очередь отправки, так же, как из админки. А между роутером и телефонами это уже SIP MESSAGE. Новое SMS приходит в Linphone сообщением в чате от номера отправителя, а ответ в этом чате роутер отправляет обычным SMS. Сообщение на эхо-номер 600 возвращается обратно — так удобно проверить чат без сотовой сети.

Полный круг — сообщение с SIP-телефона, SMS в сеть, это же SMS обратно и снова сообщение на телефон — проверен на живой SIM. Те же SMS видны на странице MudiVoLTE и в mudi-volte sms. А вот приём SMS именно через IMS в живой сети — пока нет: мой оператор присылает их по SGs, и выдавать одно за другое я не хочу.

Звонки вне дома: Tailscale вместо белого адреса

В мобильной сети роутер обычно находится за CGNAT, поэтому открыть SIP-порты наружу не получится. В прошивке GL уже есть Tailscale: роутер объявляет в частную сеть свой LAN-маршрут, а телефон подключается к нему через приложение Tailscale.

После одобрения маршрута Linphone продолжает ходить на тот же адрес роутера, поэтому его не нужно настраивать заново. Подробные шаги есть в README. Качество разговора зависит от пути Tailscale: при прямом соединении задержка минимальна, через ретранслятор — выше.

Как это выглядит снаружи

Вся рабочая часть упакована в OpenWrt-пакет mudi-volte. Он ставит демон mudi-imsd, сервис procd, правила фаервола, CLI и две страницы: в штатной админке GL.iNet — Applications → Calls & SMS — и в LuCI → Services → Calls & SMS.

На странице — состояние линии, телефоны с QR-кодами, список пропущенных, SMS, настройки, аккаунт для push и журнал. Страница в админке GL опирается на то, как прошивка 4.x подгружает свои разделы, и обновление прошивки может её сломать. LuCI и командная строка от этого не зависят.

Установка переводит модем во внешний режим, создаёт аккаунт для первого телефона и сохраняет исходные настройки для отката. Ещё она выключает энергосбережение GL: иначе роутер, к которому долго никто не подключается, засыпает вместе с модемом, а через несколько часов выключается совсем. Сервис стартует вместе с роутером, следит за IMS-подключением и восстанавливает его при обрыве.

Для диагностики и отката — несколько коротких команд:

mudi-volte statusmudi-volte missedmudi-volte smsmudi-volte uninstall

Последняя команда — откат: она вернёт модему встроенный VoLTE и настройки сна, удалит пакет вместе с аккаунтами телефонов и перезагрузит роутер.

Что работает и где ещё шероховатости

На момент подготовки статьи проверены входящие и исходящие звонки с реальной SIM европейского оператора, широкополосный звук AMR-WB ↔ G.722, звонки между локальными телефонами, SMS в обе стороны, подключение по QR, фоновые входящие, звонки вне дома через Tailscale и восстановление IMS-подключения после обрыва.

Но это пока не универсальная замена любому телефонному шлюзу:

  • Один разговор одновременно, включая локальный звонок между телефонами.

  • На iPhone без push-аккаунта входящие звонки приходят, только пока Linphone открыт. iOS усыпляет SIP-приложения в фоне, и разбудить Linphone может только push-сервис его разработчиков, Belledonne Communications. Сервис бесплатный, но без гарантий. Push будит только для звонков: SMS на iPhone приходят, когда Linphone открыт. Android обычно держит соединение сам: QR включает фоновую службу Linphone, нужно только снять для него ограничения батареи.

  • Проверены конкретные модем, прошивка и оператор. Демон работает только по IPv4. У других сетей могут отличаться требования к регистрации, IP-версии, кодекам и SIM-профилю.

  • SMS — только текст: без MMS, картинок и групповых сообщений. Приём SMS через IMS в живой сети не проверен: мой оператор доставляет их по SGs.

  • Экстренные вызовы не поддерживаются. Номер 112 уйдёт как обычный звонок, без экстренной регистрации, и как его обработает сеть, я не проверял. На экспериментальный шлюз как на единственный телефон для них рассчитывать нельзя.

Итого

В начале казалось, что голос в модеме где-то есть и осталось найти правильную команду. Параметр действительно нашёлся — voice_disable=2, — и встроенный клиент даже начал звонить. Но вывести звук из закрытого тракта в Linux так и не получилось, и на эти попытки ушла большая часть времени. Заработал путь, который Quectel сам предлагает для модулей без голоса: модем отдаёт SIM и отдельное подключение к IMS, а звонками, кодеками и телефонами занимается Linux.

Остальные грабли нашлись в мелочах. Звук уходил к оператору не через тот интерфейс, кодек угадывался по номеру из SDP, а AMR в сети оказался упакован по битам, без выравнивания на байты. Снаружи всё это выглядело одинаково: звонок соединяется, а звука нет или вместо него мусор.

В обзоре это был карманный роутер для поездок. Теперь у него есть ещё и телефонная линия. Симка осталась там же. Просто звонок наконец перестал пропадать внутри коробки.

Ссылки для тех, кому хочется глубже

  • MudiVoLTE — проект; docs/PROTOCOLS.md — подробный разбор реализации.

  • SIP: RFC 3261, SDP: RFC 8866, RTP/RTCP: RFC 3550.

  • AKA для Digest: RFC 3310, SIP sec-agree: RFC 3329, IPsec ESP: RFC 4303.

  • AMR в RTP: RFC 4867, DTMF: RFC 4733, таймеры SIP-сеанса: RFC 4028.

  • 3GPP TS 24.229 — SIP/SDP в IMS; 3GPP TS 33.203 — защита доступа к IMS.

  • SRTP: RFC 3711.

  • Tailscale: subnet routers — как роутер пускает устройства из Tailscale в свою сеть.

Источник: habr.com

0 0 голоса
Рейтинг новости
1
0
Подписаться
Уведомить о
0 комментариев