Как я научил карманный роутер 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 я разбираюсь посредственно и во многом опирался на ИИ. То, что работает, я проверял на живом железе и настоящих звонках, но в объяснениях наверняка есть неточности. Если заметите ошибку — смело поправляйте меня в комментариях. Пакет тоже можно дорабатывать: исходники открыты, а удачные доработки присылайте пулреквестами в репозиторий.
Коротко: поставить и позвонить
Если хочешь просто попробовать — три шага:
Подключи компьютер к Mudi 7 и войди на роутер по SSH. Пароль — тот же, что от веб-панели роутера:
ssh root@192.168.8.1Запусти установщик:
wget -O- https://github.com/xarvel/MudiVoLTE/releases/latest/download/install.sh | shОн проверит совместимость, скачает пакет, переключит модем в нужный режим и перезагрузит роутер (Wi-Fi пропадёт на 2–3 минуты).
Подключи телефон к Wi-Fi роутера и установи Linphone (Android или iPhone). Когда роутер перезагрузится, снова зайди на него по SSH и выведи QR-код аккаунта:
mudi-volte phone show phonephone — имя первого аккаунта, его создаёт установщик. Отсканируй код в 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 есть долговременный секрет, который знает и оператор. Задача клиента — передать вызов аутентификации карте и вернуть корректный ответ, не извлекая этот секрет.
Процедура выглядит так:
Демон отправляет SIP
REGISTER: хочу зарегистрировать абонента, вот мои параметры.Сеть отвечает
401 Unauthorizedи присылает вызов AKA — случайное значениеRANDи токен аутентификацииAUTN.Демон передаёт их SIM. В текущей реализации это APDU-команда
AUTHENTICATE, доставленная черезAT+CSIM. APDU — формат запросов и ответов смарт-карты.SIM проверяет подлинность сети и возвращает
RES— ответ, а также сессионный ключ целостностиIKи ключ шифрованияCK.Демон настраивает защиту соединения и повторяет
REGISTERс SIP Digest-аутентификациейAKAv1-MD5, где при расчёте ответа используетсяRES.Приходит
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 ---------->| |
|<----- 100 Trying -------|------ INVITE ---------->|
| |<----- 180 Ringing ------|
|<----- 180 Ringing ------| |
| |<----- 200 OK -----------|
| |------ ACK ------------->|
|<----- 200 OK -----------| |
|------ ACK ------------->| |
| | |
|<========= RTP =========>|<========= RTP =========>|
| | |
|------ 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.10
m=audio 40000 RTP/AVP 101 116
a=rtpmap:101 AMR-WB/16000
a=fmtp:101 mode-set=0,1,2
a=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 status
mudi-volte missed
mudi-volte sms
mudi-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— подробный разбор реализации.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.
Tailscale: subnet routers — как роутер пускает устройства из Tailscale в свою сеть.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.