telegram-cloud-photo-size-2-5238234503703109519-y.jpg
GL.iNet Mudi 7

Пару статей назад я устал возиться с кучей VPN и поставил Xray на роутер. Дома проблема закрылась: все устройства сами ходят куда надо — локальные и российские сервисы напрямую (без лишнего крюка и потери скорости), остальное через мой собственный сервер, реклама режется на лету. Красота.

А потом я поехал. И выяснилось, что весь этот уют остаётся дома.

В отеле — чужой Wi‑Fi, которому я не доверяю ни на грамм. Симка в роуминге даёт IP, от которого у банка дёргается глаз, и он на всякий случай просит «подтвердить вход» третий раз за день. Хочется то полностью в туннель, то наоборот напрямую — чтобы пройти captive‑портал в аэропорту. Таскать с собой ноутбук ради ssh в роутер — так себе развлечение в очереди на посадку.

Мне нужен был карманный роутер, у которого режимы переключаются пальцем, без веб‑морды и SSH. Я взял GL.iNet Mudi 7 (GL‑E5800) — и, забегая вперёд, довёл его до состояния «достал из кармана, ткнул нужный профиль на экране, поехал». Семь профилей‑тумблеров, сквозной kill switch и даже выход через домашний IP без белого IP дома.

Но сначала — обзор самого железа, потому что на Хабре я его не нашёл, а зверь любопытный.

Сразу оговорюсь, о чём эта статья и о чём — нет. Всё ниже — про приватность и безопасность собственного трафика в недоверенных сетях (публичный Wi‑Fi в отелях и аэропортах), про управление своей маршрутизацией и про доступ к своим же ресурсам: домашней сети и сервисам, стабильному собственному IP для личных кабинетов и банков вместо случайного роумингового. Это не про доступ к каким‑либо ограниченным ресурсам и не инструкция такого рода. Инструменты вроде VPN, прокси и Tor легальны сами по себе; пользуйтесь ими ответственно и в рамках законодательства своей юрисдикции.

Что за зверь Mudi 7

Mudi — это отдельная линейка GL.iNet: не «роутер к розетке», а автономная коробка со своей батареей, сотовым модемом и экраном. Первый Mudi (GL‑E750) вышел ещё в 2019-м как 4G‑хотспот, потом был Mudi V2 — а «7» в имени третьей модели отсылает не к номеру поколения, а к Wi‑Fi 7, как у Slate 7. Теперь это 5G + Wi‑Fi 7. По сути карманный компьютер с тремя радиоканалами.

Ключевое из характеристик (EU‑вариант, GL‑E5800):

SoC

Qualcomm quad‑core @2.2 ГГц

RAM / ROM

2 ГБ LPDDR4X / 8 ГБ eMMC

Модем

Quectel RG650V, 5G NR (NSA/SA) sub-6, до 4.67 Гбит/с вниз

SIM

2× nano‑SIM + встроенная eSIM, dual standby / авто‑failover

Wi‑Fi

Wi‑Fi 7 tri‑band (в ритейле — класс BE5800): 688 (2.4) + 2882 (5) + 5765 (6 ГГц), одновременно активны два диапазона

Проводка

1× 2.5G Ethernet (WAN/LAN), 1× USB‑C data (USB 3.1, 10 Гбит/с) + 1× USB‑C питание

Экран

2.8" сенсорный LCD, кастомные обои

Батарея

5380 мА·ч, съёмная, ~13.5 ч, зарядка USB‑PD/PPS до 30 Вт

Антенны

8 внутренних + 2× TS-9 для внешних (в комплект не входят)

Габариты

157 × 75 × 22.8 мм, 300 г

ОС

OpenWrt (прошивка GL.iNet поверх OpenWrt 23.05)

Цена

~€425 / $420

Ремарка для читателя из России: с основным российским 5G у этого роутера не сложится никогда. Частоты под него ГКРЧ выделила в августе 2026-го в диапазоне 4,63–4,99 ГГц (по 90 МГц каждому из «большой четвёрки», с запуском в миллионниках к концу 2028-го) — а это band n79, которого у RG650V‑EU просто нет: его потолок — n77 (до 4,2 ГГц). Единственный реалистичный сценарий «пятёрки» в РФ на этом железе — 5G, рефармленный на LTE‑диапазоны (та самая технейтральность, которую ГКРЧ тоже одобрила): n1/n3/n7/n20/n38/n41 модем покрывает. Здесь и сейчас Mudi в РФ работает по LTE — но модем Cat 20 по downlink (теоретически до 2 Гбит/с), это сотни мегабит при хорошем сигнале, так что «дорожный» сценарий не страдает. А честный sub-6 5G он покажет в поездках по странам, где тот развёрнут в его диапазонах.

Что понравилось за первые дни:

  • Автономность. Это не «роутер, который можно взять с собой», а изначально мобильное устройство. 13 часов от батареи — реально проводишь на нём рабочий день, раздавая 5G, и не думаешь о розетке. Батарея съёмная (можно возить второй «банк» или работать вообще без неё от USB‑PD).

  • Dual SIM + eSIM. Две физические симки плюс eSIM, с авто‑переключением при потере сети. Для путешествий между странами это киллер‑фича: держишь местную и домашнюю, роутер сам выбирает живую.

  • Экран. Вот ради чего всё и затевалось. 2.8«, отзывчивый, показывает статус, батарею, трафик и — главное — переключатели VPN прямо на устройстве. Обои кастомизируются из веб‑панели.»

  • Быстрые штатные VPN. GL.iNet заявляет до 700 Мбит/с на OpenVPN и до 600 на WireGuard: OpenVPN работает через DCO (шифрование в ядре, а не в userspace), WireGuard в ядре живёт изначально. Штатные протоколы летают. Забегая вперёд: Xray, о котором вся статья, идёт в userspace и таких цифр не покажет, но для дорожного сценария их и не нужно.

  • Под капотом — OpenWrt. Прошивка GL.iNet 4.x собрана поверх OpenWrt 23.05, и никто это не прячет: SSH с root из коробки, uciopkg, штатный fw4/nftables, LuCI включается одной галочкой в веб‑панели. То есть красивая «потребительская» морда с экраном не мешает залезть внутрь и переделать всё под себя — именно это и позволило проделать всё, что описано ниже, не сдувая штатную прошивку.

Что не понравилось / на что закрыть глаза:

  • Цена. ~€425 — ощутимо для карманного роутера, это уже уровень флагманского телефона среднего эшелона. За автономность + 5G + Wi‑Fi 7 + экран платишь premium; окупается, если реально много ездишь, а для «раз в год в отпуск» ценник кусается. Хотя есть и второй сценарий, где цена оправдывается иначе: как основной домашний роутер для тех, кто часто переезжает. Снял квартиру в новой стране — воткнул местную симку/eSIM, и у тебя сразу полноценная домашняя сеть с Wi‑Fi 7, без возни с местным провайдером, договором на год и ожиданием монтажника. Для экспата или цифрового кочевника это не «второй гаджет в дорогу», а единственный роутер, который переезжает вместе с тобой.

  • 5 и 6 ГГц не работают одновременно. Ограничение чипсета: либо 5, либо 6 ГГц. На практике почти не мешает, но знать стоит.

  • GPS фактически нет. Модем RG650V многосистемный приёмник имеет, и AT‑команды GNSS даже отвечают, но на плате Mudi 7 антенна GNSS не разведена (это подтверждают и на форуме GL.iNet). Итог: фикса не получить даже под открытым небом, а сделать из роутера GPS‑трекер «положил в чемодан — вижу точку» без пайки нельзя. Обидно для «дорожного» девайса, которому геометка была бы к лицу. Подробнее — в разделе про открытые вопросы ниже.

  • Голосового тракта у модема нет (для энтузиастов): модуль RG650V voice‑capable, но в прошивке GL.iNet аудио‑стек вырезан — сделать из роутера «звонилку» без плясок не выйдет. Мелочь, но я проверял — вдруг кому важно.

Из приятных мелочей — два разъёма TS-9 под внешние антенны сотового модема (не Wi‑Fi — тот обслуживают 8 внутренних). В городе не нужны, но если окажешься там, где сотовый сигнал еле дышит (глушь, подвал, поезд), выносная антенна помогает вытянуть пару делений. В комплекте их нет, но это опция «на всякий», а не то, чего не хватает из коробки.

Общий вердикт по железу: это лучший на сегодня «дорожный сервер» GL.iNet, и единственный с экраном, который реально хочется хакнуть. Чем я и занялся.

Как устроены тумблеры VPN на экране GL.iNet

Сносить прошивку и ставить чистый OpenWrt я не стал — потерял бы ровно тот экран, ради которого брал девайс. Задача сузилась: не менять прошивку, а «угнать» её же экранные тумблеры под свои профили Xray. А для этого сначала надо понять, как они устроены.

Сразу про грабли с бинарями. В GUI никакого Xray/VLESS нет, и GL.iNet прямо говорит, что добавлять не планирует. При этом сам пакет в фиде opkg есть: opkg update && opkg install xray-core со стоковой прошивки отработает (в фиде под эту сборку ~9500 пакетов, включая sing‑box и v2raya). Но версия там заметно отстаёт от актуальной, поэтому я взял свежие статические сборки под aarch64/musl (Xray‑core релизы Xray-linux-arm64-v8a.zip с GitHub; hev-socks5-tunnel — из релизов проекта или собрать самому) и положил бинарники в /usr/bin/. Дальше — init‑скрипты и автозапуск руками. Держите копию: обновление прошивки их не трогает (они в /usr/bin, не системные), а вот сброс к заводским — сотрёт.

Если не хочется собирать всё с нуля — есть готовый воспроизводимый headless‑сетап под ровно этот девайс: ChiliApple/mudi7-xray‑reality (Xray VLESS+REALITY+Vision клиент + hev‑socks5-tunnel, on‑demand xray-on/xray-off). Он не про экранные тумблеры и мультипрофильность из этой статьи, но как база «поднять прозрачный туннель на Mudi 7» — отличная отправная точка, многое у меня сошлось именно с ним.

Немного реверса. Когда тычешь в карточку VPN на экране, происходит вот что:

  1. Экранное приложение (gl_screen) дёргает локальный RPC: /rpc обрабатывает nginx прямо в Lua (oui-rpc.lua из открытого фреймворка oui), раскидывая вызовы по хендлерам в /usr/lib/oui-httpd/rpc/.

  2. RPC вызывает модуль vpn-client, функции set_tunnel {tunnel_id, enabled} и get_status.

  3. Каждый «туннель» — запись в UCI‑конфиге route_policy (система policy‑based routing у GL.iNet), привязанная к WireGuard‑пиру.

То есть экран не знает ни про какой Xray. Он знает про «туннели» с номерами и умеет их включать/выключать. Значит, мне нужно:

  • создать N “туннелей” в route_policy, каждый под свой профиль;

  • перехватить вызов vpn-client, чтобы вместо WireGuard он дёргал мой скрипт переключения Xray;

  • а сам трафик завернуть в Xray.

Фантомные WireGuard‑пиры

Первая хитрость. Экран рисует туннели, привязанные к WG‑пирам определённой группы. Реального WireGuard не надо — надо, чтобы карточка рисовалась. Поэтому создаём группу пиров (я назвал Xray) и в ней N “фантомных” пиров: конфиг валиден на вид, но никуда не поднимается — WireGuard просто не стартует, и это нормально. Их работа — существовать, чтобы экран нарисовал тумблер.

config peers 'peer_10009'
    option name 'Full VPN'
    option group_id '10009'
    option address_v4 '10.99.99.2/32'
    option end_point '127.0.0.1:51899'   # в никуда, WG не поднимается
    ...

А в route_policy каждому пиру соответствует правило‑туннель со своим tunnel_id — это и есть то, что экран включает/выключает:

config rule
    option name 'Full VPN'
    option tunnel_id '10'
    option peer_id '10009'
    option enabled '0'

Связь «пир → какой профиль Xray применить» я держу в отдельном файле‑маппинге /etc/xray/profiles.map, чтобы шим не хардкодил её:

10009 full
10010 split
10011 ru
...

Lua‑шим поверх RPC

Вторая хитрость — перехват. Рядом с оригинальным vpn-client кладу свой Lua‑шим, а оригинал переименовываю в vpn-client.glorig. Шим держит белый список моих tunnel_id:

  • пришёл мой tid — вызываю xray-tunnel <tid> on|off (радиопереключение + применение профиля);

  • чужой — прозрачно проксирую в оригинал, чтобы не сломать штатную логику;

  • в get_status подмешиваю реальное состояние (поднят ли Xray, жив ли hev-socks5-tunnel), чтобы карточка честно горела.

local TIDS={["10"]=true,["11"]=true,["12"]=true,["13"]=true,
            ["14"]=true,["15"]=true,["16"]=true}
function M.set_tunnel(p)
  local tid=tostring(p and p.tunnel_id or "")
  if not TIDS[tid] then return orig.set_tunnel(p) end  -- чужой — не трогаем
  os.execute("/usr/sbin/xray-tunnel "..tid..
             (p.enabled and " on" or " off").." >>/tmp/xray-screen.log 2>&1")
  return { tunnel_id=tonumber(tid) }
end

⚠️ Грабля на будущее: обновление прошивки GL.iNet перетирает шим (системный файл). Весь набор — шим, скрипты, UCI‑экспорты — держите в бэкапе с инструкцией «накатить заново».

Куда реально едет трафик

Труба классическая для OpenWrt‑сетапа:

LAN-клиент → policy routing (ip rule from LAN → table 200 → tun0)
           → hev-socks5-tunnel (tun0)
           → Xray SOCKS 127.0.0.1:10808 (sniffing routeOnly)
           → роутинг Xray по активному профилю
           → outbound: reality-прокси / ru-прокси / direct / block

hev-socks5-tunnel делает из SOCKS полноценный tun‑интерфейс, Xray разруливает по правилам. Профиль — отдельный config.json, который скрипт xray-profile кладёт в активный и рестартует Xray.

Тут — главная засада платформы, из‑за которой домашний рецепт с прозрачным TPROXY на Mudi 7 не работает. Qualcomm SDX72 гоняет форвардинг LAN↔5G через аппаратный datapath‑offload (IPA), который проходит мимо netfilter. Классический nftables TPROXY просто не видит форвардящихся пакетов — правило есть, а трафик мимо. Спасение — как раз software‑tun0: аппаратный ускоритель не умеет офлоадить программный интерфейс, поэтому загон трафика в tun0 силой возвращает его на нормальный Linux‑путь, где его уже ловит прокси. Плюс для tun0 обязательна отдельная firewall‑зона с masquerade и MSS‑clamping — без неё fw4 роняет обратный трафик бесхозного (вне зон) туннельного интерфейса. Вот почему тут именно hev+tun0, а не TPROXY.

Само policy routing — три команды: весь LAN гоним в отдельную таблицу, где дефолт смотрит в tun0, а помеченный mark 255 трафик самого Xray (его исходящие к proxy/direct) выпускаем в WAN мимо туннеля, чтобы не было петли:

ip route add default dev tun0 table 200
ip rule  add from 192.168.8.0/24 lookup 200 pref 200   # LAN -> в туннель
ip rule  add fwmark 255 lookup 201 pref 150             # egress Xray -> в WAN (table 201 = обычный дефолт)

Грабля: чтобы работали правила по geosite, на SOCKS‑inbound обязателен sniffing с routeOnly: true — иначе Xray видит только IP, и доменные правила проходят мимо. Потерял на этом вечер, отлаживая через временный access.log.

Вторая грабля — DNS утечёт мимо туннеля, если не подстраховаться. LAN‑клиенты обычно спрашивают DNS у роутера, а тот резолвит через WAN напрямую — и провайдер/сеть отеля видят, куда ты ходишь, даже когда весь HTTP уже в туннеле. Лечится тем, что upstream‑DNS роутера тоже загоняется в tun0 (пиннинг IP резолверов в таблицу 200: ip route add 1.1.1.1/32 dev tun0 table 200 и так далее), а лучше — DoH/DoT‑резолвер, который сам ходит только через прокси. В Tor‑профиле (ниже) это решено радикально — весь DNS завёрнут внутрь Tor.

Профили

Базовый набор повторяет домашнюю логику, только теперь каждый — отдельная кнопка на экране:

  • Full VPN — вообще всё через прокси.

  • Split RU — российское напрямую, остальное через VPN (обычный ежедневный режим).

  • RU sites — наоборот: только российское гоню через ru‑прокси (когда сервис хочет российский IP), остальное напрямую.

  • AdBlock — только резать рекламу (geosite:category-ads-all → blackhole), без проксирования.

  • Full RU — всё через ru‑прокси.

telegram-cloud-photo-size-2-5238234503703109517-y.jpg

Каждый профиль — это отдельный config.json Xray. Вот, для примера, full (всё через прокси, реклама в blackhole) — остальные отличаются только блоком routing.rules:

{
  "inbounds": [{
    "tag": "socks-in", "listen": "127.0.0.1", "port": 10808,
    "protocol": "socks", "settings": { "udp": true },
    // sniffing с routeOnly обязателен, иначе geosite-правила не сработают
    "sniffing": { "enabled": true, "destOverride": ["http","tls","quic"], "routeOnly": true }
  }],
  "outbounds": [
    {
      "tag": "proxy", "protocol": "vless",
      "settings": { "vnext": [{ "address": "proxy.example.com", "port": 443,
        "users": [{ "id": "<UUID>", "encryption": "none", "flow": "xtls-rprx-vision" }] }] },
      "streamSettings": {
        "network": "tcp", "security": "reality",
        "realitySettings": { "serverName": "www.apple.com", "fingerprint": "chrome",
          "publicKey": "<REALITY_PUBKEY>", "shortId": "<SHORT_ID>" },
        "sockopt": { "mark": 255 }          // mark 255 -> egress мимо tun0, без петли
      }
    },
    { "tag": "direct", "protocol": "freedom", "streamSettings": { "sockopt": { "mark": 255 } } },
    { "tag": "block",  "protocol": "blackhole" }
  ],
  "routing": { "domainStrategy": "AsIs", "rules": [
    { "type": "field", "domain": ["geosite:category-ads-all"], "outboundTag": "block" },
    { "type": "field", "inboundTag": ["socks-in"], "outboundTag": "proxy" }
  ]}
}

Профиль split — тот же файл, только перед правилом «всё в proxy» добавлены исключения на direct:

{ "type": "field", "domain": ["geosite:category-ru","domain:ru","regexp:.*\\.su$"], "outboundTag": "direct" },
{ "type": "field", "ip": ["geoip:ru","geoip:private"], "outboundTag": "direct" }

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

Kill switch, который не течёт

Что это. Kill switch — защита от утечки: если туннель умер, трафик не должен молча вывалиться в открытую сеть отеля. Лучше пусть интернет отвалится совсем (это видно сразу), чем ты думаешь, что в туннеле, а на самом деле светишь всё напрямую.

Как обычно ломается: весь LAN policy‑routing'ом заворачивается в tun0. Падает tun0 — маршрут исчезает, пакеты сваливаются в общую таблицу, а там правило lan → wan радостно выпускает их наружу. Утечка.

Моё решение — durable, в UCI, а не ad‑hoc в nftables. Пока туннель поднят, выключаю форвардинг lan → wan целиком:

uci set firewall.@forwarding[<lan→wan>].enabled=0
uci commit firewall && fw4 reload

Теперь LAN‑клиент физически достучится только до tun0 (через постоянную firewall‑зону). Умер tun0 — форвард‑политика fw4 drop глушит пакеты. Выключаешь туннель штатно — форвардинг возвращается, captive‑портал и прямой доступ снова работают.

Почему в UCI, а не «дроп‑правило в nft»: на LTE фаервол периодически передёргивается (fw4 reload прилетает от обновления DHCP‑аренды на WAN), и любые ad‑hoc nft‑вставки при этом смываются — дырка открывается снова. Запись в UCI переживает reload, потому что fw4 её сам восстанавливает.

Приятный побочный эффект: Split/RU‑профили не ломаются. Их «прямой» трафик к российским сайтам идёт от самого роутера (Xray помечает исходящие fwmark 255 и отправляет отдельной таблицей), а не форвардом lan → wan. Блокировка форварда его не задевает — проверено, а не на словах.

Как убедиться, что не течёт. Три быстрые проверки:

# 1. egress-IP клиента — должен быть прокси/домашний, а не роуминговый.
#    Только --resolve: у многих geo-сервисов несколько A-записей, обычный curl ловит не тот.
curl --resolve ipinfo.io:443:34.117.59.81 https://ipinfo.io/ip

# 2. при активном туннеле в forward-цепочке НЕТ пути lan->wan (только lan->tun0):
nft list chain inet fw4 forward_lan

# 3. kill switch: роняем tun0 и проверяем, что клиент теряет инет, а не утекает напрямую:
ip link set tun0 down   # трафик LAN должен встать колом, а не пойти в обход

Выход через домашний IP — без белого IP дома

Моя любимая часть. Идея: в поездке хочу, чтобы банк видел мой домашний IP, а не роуминговый и не отельный. Дома стоит Raspberry Pi (тот самый, с прошлых статей) — пусть он и будет точкой выхода.

Проблема: дома нет белого IP и проброшенного порта (обычная история — CGNAT/серый адрес). Классический WireGuard “извне домой” не заведётся: некуда стучаться.

Решение — то, что и так уже крутится для доступа к домашним сервисам: Cloudflare Tunnel. Он даёт «вход снаружи» без белого IP: cloudflared на Pi сам держит исходящее соединение до Cloudflare, а снаружи публичный хостнейм проксируется внутрь.

Собираем так:

  1. На Pi поднимаю сервер Xray в Docker: VLESS‑inbound по WebSocket, слушает только 127.0.0.1 (наружу не торчит — единственный вход через Cloudflare). TLS тут не нужен — его терминирует сам Cloudflare:

// сервер на Pi (за Cloudflare-туннелем)
{ "inbounds": [{
    "listen": "127.0.0.1", "port": 23456, "protocol": "vless",
    "settings": { "clients": [{ "id": "<UUID>" }], "decryption": "none" },
    "streamSettings": { "network": "ws", "security": "none",
      "wsSettings": { "path": "/hp-<random>" } }
  }],
  "outbounds": [{ "protocol": "freedom" }] }   // выход в интернет домашним аплинком Pi
  1. В Cloudflare‑туннель добавляю public hostname home.example.mehttp://localhost:23456Без Cloudflare Access: браузерное SSO VLESS‑клиент не пройдёт (строго говоря, Access с service‑токеном возможен — Xray умеет слать CF-Access-Client-Id/Secret через wsSettings.headers, — но городить его тут незачем); аутентификация по UUID.

  2. На Mudi — профиль home: VLESS + WebSocket + TLS на home.example.me:443. TLS терминирует Cloudflare, WebSocket он проксирует из коробки:

// outbound профиля home на Mudi
{ "tag": "home", "protocol": "vless",
  "settings": { "vnext": [{ "address": "home.example.me", "port": 443,
    "users": [{ "id": "<UUID>", "encryption": "none" }] }] },
  "streamSettings": {
    "network": "ws", "security": "tls",
    "tlsSettings": { "serverName": "home.example.me" },
    "wsSettings": { "path": "/hp-<random>", "headers": { "Host": "home.example.me" } },
    "sockopt": { "mark": 255 }
  }}

Путь получается такой:

Mudi (VLESS+WS+TLS:443) → Cloudflare edge → cloudflared-туннель
   → Xray-сервер на Pi → интернет через домашний аплинк Pi

Выход — с домашнего IP. Никакого белого адреса, никаких проброшенных портов, вся труба идёт через 443-й порт и снаружи выглядит как обычный HTTPS к своему домену.

Грабля с Cloudflare: если туннель управляется токеном (cloudflared … run --token), ingress и DNS живут в облачном дашборде, а не в локальном конфиге. Добавить хостнейм — либо руками в дашборде (30 секунд), либо через API: PUT /accounts/{acc}/cfd_tunnel/{tun}/configurations (прочитать текущий ingress, вставить своё правило перед catch‑all, записать) плюс DNS CNAME home → {tunnel-id}.cfargotunnel.com (proxied).

Бонус: Tor одной кнопкой

Раз уж на экране появились свободные тумблеры — грех не добавить Tor как инструмент приватности для собственного трафика (скрыть свой IP от аналитики и трекеров, работа с чувствительными данными и тому подобное). У Mudi 7 Tor вообще есть штатно: с прошивки 4.8.4 в веб‑панели появился раздел APPLICATIONS → Tor. Но GUI‑режим взаимоисключающ с VPN (включение Tor гасит VPN, DNS‑override и IPv6) — а мне Tor нужен как ещё один профиль среди прочих. Поэтому иду в обход GUI: бинарь tor лежит в прошивке, хватило поднять его демоном через uci и init‑скрипт и создать каталог под лог, без которого он падал на старте. Дальше — профиль Xray, который весь TCP отправляет в Tor:

"dns": { "tag":"dns-internal", "queryStrategy":"UseIPv4",
         "servers":["tcp://1.1.1.1"] },
"outbounds": [
  { "tag":"tor", "protocol":"socks",
    "settings":{"servers":[{"address":"127.0.0.1","port":9050}]} },
  { "tag":"dns-out", "protocol":"dns",
    "settings":{"address":"1.1.1.1","port":53,"network":"tcp"},
    "proxySettings":{"tag":"tor"} },
  { "tag":"block", "protocol":"blackhole" }
],
"routing": { "rules": [
  { "inboundTag":["dns-internal"], "outboundTag":"tor" },  // резолвы встроенного DNS — тоже в Tor
  // ... port 53 → dns-out, UDP → block, весь TCP → tor
] }

Два нюанса, без которых Tor‑режим дырявый:

  1. DNS не должен утекать мимо Tor — и тут две ловушки подряд. Запросы клиентов на 53-й порт заворачиваю в dns-out (protocol dns, TCP). Ловушка первая: dns‑outbound не проходит через роутинг повторно — без явного proxySettings он дозвонится до резолвера напрямую, мимо Tor, и молча сольёт имена сайтов провайдеру. Ловушка вторая, коварнее: A/AAAA‑запросы dns‑outbound вообще не форвардит — их перехватывает встроенный DNS‑модуль Xray, а тот по умолчанию резолвит системным резолвером, снова напрямую. Лечится связкой из конфига выше: встроенному DNS назначаем TCP‑резолвер и тег ("tag":"dns-internal"), а правилом роутинга по inboundTag отправляем его запросы в Tor. Проверял по латентности и логам: без этих строк резолв укладывался в ~26 мс (провайдерский DNS), с ними — честные ~280 мс и taking detour [tor] в логе.

  2. UDP/QUIC — в блок. Tor не носит UDP. Не заблокируешь — браузер полезет в QUIC и либо потечёт, либо затупит. Блокируем, браузеры сами откатываются на TCP.

Проверка выхода банальная: curl --socks5-hostname через профиль на https://check.torproject.org/api/ip → IsTor: true. Полноценный Tor‑режим в кармане, одним тапом по экрану.

Грабля напоследок: TTL против раздачи

Раз уж роутер раздаёт мобильный интернет — нельзя не вспомнить про давнюю операторскую подляну. Некоторые сотовые провайдеры детектят раздачу (tethering) по TTL и режут за неё скорость или требуют доплату.

Механика простая. Каждый IP‑пакет несёт TTL (Time To Live) — счётчик хопов, который уменьшается на 1 на каждом роутере по пути. Само устройство шлёт пакеты с «родным» TTL (у Android/Linux это 64). А вот трафик, который прошёл через твой роутер, уже уменьшен до 63. Оператор видит в одном потоке смесь 64 (сам роутер) и 63 (клиенты за ним) — и понимает: это раздача.

Лечится нормализацией TTL на выходе: заставляем роутер переписывать TTL всех исходящих в сотовый WAN пакетов в одно «эталонное» значение. На fw4 (OpenWrt 23.05) это durable‑инклюд в /etc/nftables.d/ — он подхватывается на каждом fw4 reload и после ребута, в отличие от ad‑hoc‑правила, которое смоет первый же reload:

# /etc/nftables.d/10-ttl-normalize.nft
chain ttl_normalize {
    type filter hook postrouting priority 110; policy accept;
    oifname "rmnet_data0" ip  ttl      set 64
    oifname "rmnet_data0" ip6 hoplimit set 64
}

rmnet_data0 — это интерфейс сотового модема (у Mudi модем сидит прямо на SoC, так что имя стабильное; на другом железе подставьте свой WAN). Значение 64 — потому что симка стоит в самом роутере, и «эталон» — это TTL Linux/Android. Если бы вы тезерили сам Mudi за телефоном (симка в телефоне, ещё один хоп), ставили бы 65.

Проверить, что правило живёт и ловит трафик: nft list chain inet fw4 ttl_normalize — счётчик пакетов должен расти.

Отдельно приятно, что для проксируемых профилей (Full VPN, Home IP) эта возня вообще не нужна: оператор видит только шифрованное соединение до одного хоста с TTL самого роутера — раздачу за туннелем по TTL не отличить в принципе. Фикс важен именно для «прямых» режимов, где часть трафика уходит в WAN в открытую.

Открытые вопросы: что ещё умеет модем, а прошивка молчит

Пока копался в модеме, заодно прощупал пару смежных идей — звонки и GPS. Общий вывод один: модуль RG650V куда богаче, чем то, что отдаёт прошивка GL.iNet (она собрана как чистый дата‑модем). Оставляю находки как зацепку для тех, кто захочет докрутить.

Звонки?

Раз в Mudi стоит Quectel RG650V, у которого в даташите значится VoLTE/VoNR (как опция SKU), нельзя ли сделать из роутера ещё и «звонилку» — принимать звонки и SMS на симку, живущую в роутере. Полноценно — нет, но накопал я вот что:

  • SMS работает. Модем в PDU‑режиме читает и шлёт SMS через AT‑порт (/dev/at_mdm0, есть и atcmd/ubus‑обёртки). SMS→Telegram‑форвардер собирается за вечер — реально полезно для банковских OTP на симку, оставленную дома.

  • Голос на уровне сигналинга — есть, аудио — нет. Модем зарегистрирован в CS‑домене сети (AT+CREG: 0,1), определитель номера отдаётся (AT+CLIP), при входящем прилетают URC RING + номер. То есть «ловить пропущенные» и слать уведомления «звонил такой‑то» — вполне реально. А вот самого звука нет: все аудио‑команды Quectel (AT+QPCMVAT+QDAIAT+QAUDMOD …) отвечают ERROR, в системе нет ни одной звуковой карты (/proc/asound пуст), а IMS не сконфигурирован и VoLTE выключен (AT+QCFG="ims" → 0,0). GL.iNet собрала прошивку как чисто дата‑модем и аудио‑тракт из неё выпилила.

  • Что мешает и куда копать. Голосовой DSP у Qualcomm‑платформы физически на кристалле есть — вырезан именно софт: нет ALSA/ASoC‑драйверов (в ASoC только заглушка snd-soc-dummy), нет узлов в device tree, скорее всего нет и аудио‑firmware для DSP. Вернуть теоретически можно, но это полноценный bring‑up голосового тракта Qualcomm (SDK/исходники ядра под эту сборку, device tree, QMI‑сервисы) — недёшево по времени и с риском окирпичить основной роутер. Плюс есть чисто операторский путь без всякого аудио: переадресация (AT+CCFC) выполняется на стороне сети — можно командой с роутера включать переброс входящих звонков на другой номер, роутер тут просто «пульт».

Если кто‑то на GL.iNet‑форуме или тут в комментариях доведёт голос до рабочего состояния (или подтвердит, что аудио‑firmware в прошивке нет в принципе) — очень интересно послушать. Пока мой практический вывод: SMS‑шлюз и «ловля пропущенных» — да, разговоры — нет.

GPS и «как найти роутер, если потерял»

Та же история, но с аппаратным нюансом. GNSS у RG650V в даташите заявлен как опция SKU, и AT‑команды отвечают (AT+QGPS=1 включает движок) — но фикса не будет: сотрудник GL.iNet на форуме подтверждает, что у Mudi 7 антенный пин GNSS не разведён («floating»), — мой AT+QGPSLOC вернул +CME ERROR: 516 (“not fixed”). Тут даже не софт, а железо: без пайки GPS не оживить.

Но потерянный роутер это найти не мешает — у него есть то, чего нет у GPS‑трекера: своя SIM и постоянный интернетAT+QENG="servingcell" отдаёт «адрес» соты, геосервисы вышек (Google Geolocation, OpenCellID) превращают его в координаты с точностью «район, а не квартира», а простой heartbeat раз в N минут шлёт это на домашний сервер по уже готовому туннелю. Для «роутер ещё в городе или уехал» — хватает; для «в какой сумке лежит» — проще кинуть внутрь обычную метку.

Итог

Теперь мой дорожный роутер — не «коробка с одним VPN», а пульт на семь режимов, который живёт в кармане и переключается пальцем:

  • обычная сплит‑маршрутизация на каждый день;

  • полный туннель для недоверенных сетей — со сквозным kill switch, который не течёт при падении канала;

  • выход через домашний IP, когда наружу надо показать «я как будто дома» — и всё это без белого IP дома;

  • Tor одной кнопкой, с завёрнутым внутрь DNS.

Само железо — лучший на сегодня «дорожный сервер» GL.iNet: автономный, с двумя симками и eSIM, 5G и Wi‑Fi 7. А самое приятное — я не тронул штатную прошивку и не потерял экран. Я просто убедил его показывать мои тумблеры. Дома Xray избавил меня от зоопарка VPN‑клиентов; в дороге — от необходимости держать в голове, куда сейчас на самом деле уходит трафик. Достал, ткнул, поехал.