Files
omarandClaude Opus 5 4869d62e02
test / go + panel tests (push) Successful in 1m39s
release / test gate (push) Successful in 1m39s
release / apk aarch64_cortex-a53 (push) Failing after 2m54s
release / apk x86_64 (push) Failing after 2m54s
release / release apk (push) Failing after 1m35s
feat(egress)!: remove byedpi — what it replaced was not weak, it was broken (D29)
The `byedpi` egress kind, the `openwrt/byedpi` package (`ciadpi`), the readiness
endpoint and the panel plate are gone. D13 is not deleted from DECISIONS.md; it
is REVERSED there, with the reason, because the reason is the whole point.

D13 adopted an external desync process on an observation: the engine's own
`tls_fragment`/`tls_record_fragment` were tried against a live ISP and did not
get through, so the method was judged too weak for anything past "just fragment
the ClientHello". The method was never tried. `common/tlsfragment` dropped a
number of labels equal to the number of DOTS in the name, and a name always has
one more label than it has dots — so the cut always landed inside the FIRST
label. `www.youtube.com` was split inside `www` and `youtube` went to the wire
in one piece, which is the word the DPI matches on. Of six blocked names exactly
one got through: `youtube.com`, the one whose first label IS the blocked word.
That defect is fixed (815011dfb, efb2177f4). With it fixed the built-in presets
do the job the external process was brought in to do, and the process is 100 KB
of binary, a second procd service, a second UCI file, a port that agreed with
our egress by hand-written comment only, a readiness prober, a five-state
service model and a panel plate — all to work around fifteen lines of ours.

So this is not "ByeDPI turned out to be bad". It is a good tool that turned out
not to be needed, and the reason we thought it was needed was ours.

A CONFIG THAT STILL SAYS `type 'byedpi'` IS THE PART THAT NEEDED WORK. Nothing
is migrated and nothing is rewritten: the kind stays unbuildable, therefore
fail-closed — no outbound, no mark, no `ip rule`, no routing table, so every
node, group and rule bound to it is blocked rather than released onto the plain
WAN. A migration to `direct` was considered and rejected: it is the only rewrite
that leaves the egress routing at all, and it would silently turn a blocked
egress into a live plain-WAN path with the router's real address — by an
upgrade, on a config nobody touched. `CurrentSchemaVersion` is therefore not
bumped either: no stored field changes meaning, and a bump would only make this
build's configs unreadable to an older daemon for no gain.

What changes is what the operator is TOLD. `model.RetiredEgressTypes` is a
closed, positive table read by BOTH `ValidateEgresses` and the generator (one
copy of the sentence, because two copies drift). It names the removal, denies
that it is a typo, says nothing is built and that the traffic is blocked rather
than leaked, names the replacement (`direct`/`interface` with `dpi 'record'`),
refuses to promise which preset defeats a given ISP, and says `apk del byedpi`.
The generic "unknown type" is still there and still says something different, on
purpose: "we took this kind away" and "you mistyped something" send an operator
to different places, and a value that was correct on the day it was written must
not be reported as a spelling mistake. The type list stays closed and positive —
`interface`, `direct`, the alias `tunnel` — and `EgressTypeKnown` does NOT admit
the retired kind: being told it was removed and having it work anyway is worse
than either alone.

`Egress.Port` goes with the kind: no surviving egress dials anything, so the
option is no longer parsed and drains out of /etc/config/shater on the next
render, the same way the deleted per-group probe_url/probe_interval did.

Tests, verified by mutation, each failing by name:
  - drop the retired branch in `ValidateEgresses` -> the retired kind is
    reported as "is not one of interface/direct" and
    TestRetiredEgressTypeIsReportedByTheValidator fails on both spellings;
  - drop it in the generator -> "unknown type \"byedpi\"" and
    TestRetiredEgressTypeIsReportedByTheGenerator fails;
  - the FAIL-OPEN mutation, which is the one that matters: let `byedpi` fall
    into the `direct` arm and be a known type -> four tests fail, including the
    two that check no outbound is emitted. A removal that quietly starts routing
    the traffic it used to block, under a reassuring message, is the failure with
    the worst consequence;
  - the panel half: empty RETIRED_EGRESS_TYPES -> two egressEdit tests fail.
Controls beside the claims: `interface`, `direct`, the `tunnel` alias and the
empty synonym must still resolve, warn about nothing and emit an outbound
(TestSupportedEgressTypesAreUntouched), and never-supported values — `proxy`,
`block`, `wireguard`, `byedpi2`, `bye dpi`, `sorcery` — must NOT draw the
removal sentence, which names a replacement for something that never existed.

CI and docs: the feed loses its fourth package everywhere the four were named —
`apk upgrade shaterd shater-core luci-app-shater`, in CLAUDE.md, both READMEs,
INSTALL.md, the release body and `shaterd`'s own diag bundle. The version
exception (byedpi carried upstream's version, ours come from the git tag) is
gone with it, so ci/version.sh and ci/sdk-build-apk.sh no longer have an
exception to remember and the "expected >=4 of OUR .apk" collect check is now 3.
INSTALL.md §5.3 gains the half a feed cannot do: dropping the package from the
feed does not take it off a router it is already on, so `apk del byedpi` is
written down, with what it removes and why it is safe.

Panel: 368 tests -> 339. Deleted with the mechanism they covered:
byedpiReady.test.ts, byedpiAge.test.ts, byedpiRefusal.test.ts (34 tests);
egressEdit.test.ts gains 5 for the retired-type sentence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BHw89tdWddzhjUc4bAH4tS
2026-07-27 17:13:50 +03:00

24 KiB
Raw Permalink Blame History

shater

Управляемый интернет-шлюз для роутеров на OpenWrt. Одна коробка превращает домашнюю или офисную сеть в прозрачный VPN-шлюз, сетевой блокировщик рекламы, трекеров и вредоносных доменов, средство родительского контроля по устройствам и живую панель аналитики трафика — всё локально, всё self-hosted, всё настраивается из богатой веб-панели.

License: GPL-3.0 targets: x86_64 · aarch64_cortex-a53 feed: apk 25.12+


Что это

shater — это сетевой прокси-стек для роутеров на OpenWrt / ImmortalWrt / BananaWRT (Banana Pi BPI-R3, BPI-R4 и совместимые). Он прозрачно (без настройки клиентов) заворачивает весь LAN-трафик через прокси с маршрутизацией по домену, гео и клиенту, фильтрует DNS, собирает статистику и управляется из встроенной веб-панели.

Ядро — форк движка sing-box через sing-box-lx — вкомпилировано в один Go-бинарь shaterd вместе с control-plane, DNS-фильтром, агрегатором статистики и самой веб-панелью. За счёт sing-box поддерживается широкий и актуальный набор протоколов: VLESS/VMess/Trojan/Shadowsocks, Reality/XTLS, WireGuard, AmneziaWG 2.0, Hysteria2, TUIC, XHTTP — ровно то, что умеет разобрать shater/parse и что регистрирует shater/registry в движке.

Интеграция в OpenWrt — тонкий LuCI-лаунчер: мини-дашборд и кнопка «Открыть панель», которая по одноразовому токену передаёт браузер в полноценную SPA-панель, поднятую демоном на собственном порту (по умолчанию :8088).


Ключевые возможности

Прозрачный прокси и маршрутизация

  • TPROXY data-plane для нескольких LAN-интерфейсов (TCP + UDP), сниффинг SNI/Host/QUIC, без утечек DNS.
  • Правила маршрутизации по источнику (IP/CIDR/MAC/интерфейс/зона), назначению (domain/suffix/keyword/geosite), спискам, порту, протоколу → outbound / selector / chain / direct / block.
  • Группы узлов с балансировщиком/обсерваторией (least-ping / failover / round-robin), мульти-хоп цепочки и выбор egress по правилу.

Надёжность («железно»)

  • Fail-closed kill-switch: мёртвая группа → block, а не тихая утечка мимо прокси; собственная nft-таблица inet shater и свои марки/таблицы, fw4 не трогаем.
  • Атомарный apply с валидацией движком и nft -c. Commit-confirm с авто-откатом к последней рабочей конфигурации есть, но на стоковой установке выключен: confirm_timeout поставляется нулём, и apply не вооружает ничего, пока вы не зададите окно (см. «Включение»).
  • Идемпотентный reconcile из hotplug/boot под flock; management-bypass (SSH/LuCI/LAN) всегда в обход.

DNS, фильтрация, блокировки

  • Перехват :53, DNS движка sing-box в процессе; резолверы DoH/DoT/plain/FakeIP, выбор резолвера по домену.
  • Блок-листы с гибкими источниками: inline / file / url (авто-обновление) / категория geosite; hosts-файл, plain-список или AdBlock-стиль ||domain^ компилируются в локальный .srs. Эффективный компилированный матчер вместо dnsmasq-мегасписков.
  • Block-DoH/DoT — не даёт устройствам обходить фильтр через свой шифрованный DNS.

Подписки и узлы

  • Подписки (VLESS/VMess/Trojan/SS/WG/AmneziaWG), форматы Clash/sing-box/Xray-JSON, интервал обновления + вручную + на загрузке; стабильная идентичность узла между обновлениями; квоты/срок из subscription-userinfo.
  • Ручные узлы: share-ссылки, импорт файла, wg-quick/AmneziaWG .conf.
  • Health board: TCP + реальная проба через прокси-путь, exit-IP, «протестировать все».

Контроль по устройствам

  • Авто-обнаружение устройств (dhcp.leases + ip neigh), имена, живой статус/трафик.
  • Тумблеры на устройство: прокси on/off, блок-листы on/off, страна/узел выхода; блок/allow домена для одного устройства или для всех; расписания.

Статистика и видимость

  • Топ доменов (запрошенные/заблокированные), allowed-vs-blocked, разбивка по устройствам, таймлайны — из DNS-событий движка в процессе (без скрейпинга логов).
  • Трафик по клиенту/узлу/правилу (байты) из nft-счётчиков; живой query-log.

Панель и профили

  • Встроенная SPA-панель (собственный порт, вшита в бинарь): overview, узлы и подписки, правила маршрутизации, DNS/блок-листы, устройства, apply/rollback.
  • Именованные профили/сцены и WAN-профили (условные оверрайды).

Полный список с тегами MVP/T1/T2 — docs-shater/FEATURES.md.


Архитектура

Один бинарь shaterd держит движок, control-plane, DNS-фильтр и веб-сервер панели в одном процессе; OpenWrt-обвязка (тонкий LuCI + procd/system glue) оборачивает его. Конфиг — UCI desired-state; демон рендерит его в конфиг движка и применяет; телеметрия течёт обратно в панель.

flowchart TB
  subgraph BIN["shaterd — один бинарь (форк sing-box-lx)"]
    ENG["движок sing-box\nпротоколы · Reality · AmneziaWG 2.0 · DNS · routing · stats"]
    CTRL["control-plane (shater/)\nUCI-модель · генерация конфига · apply/rollback · nft/routing"]
    FILT["DNS-фильтр + блок-листы + политика по устройствам (shater/)"]
    STAT["агрегатор статистики (shater/)"]
    PANEL["веб-сервер панели + вшитая SPA (свой порт, токен-auth)"]
  end
  subgraph WRT["OpenWrt-обвязка (openwrt/)"]
    LUCI["тонкий LuCI — мини-дашборд + кнопка «Открыть панель»"]
    PROCD["procd init · hotplug · uci-defaults · fw4/routing"]
  end
  LUCI -->|"ubus: mint token"| PANEL
  PROCD --> BIN
  CTRL --> ENG
  FILT --> ENG
  ENG --> STAT
  STAT --> PANEL

Путь трафика: LAN-клиент → nft tproxy (mark → tproxy-порт) → tproxy-inbound sing-box (сниффинг SNI/Host/QUIC) → маршрут по правилу → outbound/selector/chain (проксировано) · direct (обычный маршрут, без туннеля) · block. TPROXY несёт только TCP и UDP; ICMP и остальные протоколы — через отдельные опциональные механизмы (l3_tunnel, untunnelable_egress, ARCHITECTURE §3a). Подробные диаграммы (auth-handoff, data-plane, DNS-flow, apply-flow) — в docs-shater/ARCHITECTURE.md.


Установка

shater поставляется одним подписанным apk-фидом (OpenWrt / ImmortalWrt / BananaWRT 25.12+: .apk, индекс packages.adb, EC-ключ в /etc/apk/keys/). Старый opkg-фид (.ipk, 24.10) снят — оба наших роутера на 25.12 с apk-tools 3, бинаря opkg там просто нет (docs-shater/DECISIONS.md D22).

Пакеты ставятся по зависимостям: shaterd → shater-core → luci-app-shater. shaterd подтягивается автоматически как зависимость.

Фид apk

/etc/apk/arch сам выбирает нужный per-arch релиз (apk-релизы раздельны по арке):

# 1) доверяем ключу apk-фида (любое имя *.pem под /etc/apk/keys подходит).
wget -O /etc/apk/keys/shater-apk.pem \
  "https://git.qomar.pw/omar/shater/releases/download/apk-latest-$(cat /etc/apk/arch)/shater-apk.pem"

# 2) добавляем репозиторий — строка указывает на сам ФАЙЛ-ИНДЕКС packages.adb.
echo "https://git.qomar.pw/omar/shater/releases/download/apk-latest-$(cat /etc/apk/arch)/packages.adb" \
  > /etc/apk/repositories.d/shater.list

# 3) обновляемся и ставим (shaterd подтянется как зависимость).
apk update
apk add luci-app-shater        # -> shater-core -> shaterd

Обновление — перечисляйте пакеты явно, голый apk upgrade не запускайте: без аргументов apk пересобирает состояние ВСЕХ установленных пакетов по ВСЕМ подключённым репозиториям и может задеть (в т.ч. откатить) посторонние системные пакеты.

apk update
apk upgrade shaterd shater-core luci-app-shater
# эквивалент, дополнительно закрепляющий пакеты в world:
# apk add -u shaterd shater-core luci-app-shater

Документация apk-tools 3 про apk upgrade: «If list of packages is provided, only those packages are upgraded along with needed dependencies». Проверить установленные версии: apk list -I shaterd shater-core luci-app-shater.

Роллинг или фиксация — это выбор URL в shater.list. apk-latest-<arch> — движущийся указатель: каждый релизный прогон заменяет его ассеты, поэтому «поставил и забыл»: apk update сам видит новую сборку. apk-vX.Y.Z-<arch> — фиксация на конкретной сборке: роутер не получит ничего нового, пока /etc/apk/repositories.d/shater.list не отредактируют руками — на каждом роутере и на каждый релиз. На mini_router сознательно прописан версионированный URL, и ручная правка — его цена. Подробнее — docs-shater/INSTALL.md §5.1.

Версии пакетов CI берёт из git-тега (vX.Y.Z → X.Y.Z-r1, сборка вне тега → X.Y.Z-r<коммитов+1>), поэтому каждая новая сборка действительно видна менеджеру пакетов как новая. Подробности — docs-shater/INSTALL.md §2.1.

Полные инструкции — ручная установка из .apk, фиксация версии (apk-vX.Y.Z-<arch>), совместимость с BananaWRT 25.12-mtk-vendor — в docs-shater/INSTALL.md.

Включение

shater ставится инертным (globals выключены), чтобы установка не рвала связь. Настройте узлы/правила (через панель или uci), затем включите и примените:

uci set shater.globals.enabled=1
# Предохранитель: commit-confirm поставляется ВЫКЛЮЧЕННЫМ (confirm_timeout=0),
# и без этой строки apply ничем не подстрахован. 120 с — окно на проверку связи.
uci set shater.globals.confirm_timeout=120
uci commit shater
shaterd apply        # применить и вооружить авто-откат на 120 с
shaterd confirm      # подтвердить в пределах окна (отменяет авто-откат)

shaterd apply печатает, вооружил ли он что-нибудь, и почему нет: при confirm_timeout=0 он прямо говорит, что автоматического отката НЕТ. Оставить ноль — сознательный выбор: тогда apply, отрезавший вам SSH/LuCI, придётся откатывать руками.

/etc/init.d/shater enable && /etc/init.d/shater start поднимает демона под procd. Кнопка «Открыть панель» в LuCI чеканит одноразовый токен и передаёт браузер в панель (:8088 по умолчанию).

После первого же применённого включённого fail-closed конфига появляется загрузочная защита: /etc/init.d/shater-armor (START=21) грузит сохранённый fail-closed план ещё до старта демона, закрывая те секунды между поднятием LAN и первым apply, когда роутер форвардил трафик в WAN открытым. Форвардинг LAN→WAN заблокирован, пока shaterd не применит конфиг; SSH, LuCI и панель при этом доступны намеренно — цепочка вешается только на forward. Чем защита вооружается, когда отказывается вооружаться и как её снять — docs-shater/INSTALL.md §4.


Сборка из исходников

Ship-артефакт — бинарь shaterd со вшитой SPA. Собирается вне дерева SDK скриптом scripts/build-shaterd.sh:

scripts/build-shaterd.sh [VERSION] [--fast]

Что он делает: (1) собирает панель — cd panel && npm ci && npm run build (Vite → panel/dist); (2) копирует panel/dist/* в shater/panel/webroot/, откуда //go:embed вшивает реальную SPA в бинарь; (3) кросс-собирает под {amd64, arm64} с musl-static набором тегов (CGO_ENABLED=0 GOOS=linux), stripped/trimmed; (4) прогоняет UPX --lzma --best (~42 МБ → ~8–11 МБ); (5) стейджит артефакт в openwrt/shaterd/files/ для пакета.

Затем OpenWrt-пакеты из openwrt/ собираются каноническим путём SDK. Детали (набор build-тегов, почему shaterd — prebuilt-пакет, порядок CI) — в docs-shater/INSTALL.md.

Проверка

bash scripts/run-tests.sh            # полный гейт
bash scripts/run-tests.sh --no-race  # без -race, для локального цикла

Гейт гоняет весь набор под теми же build-тегами, с которыми собирается роутерный бинарь (scripts/router-tags.sh), на Linux (с не-Linux хоста — сам перезапускается в Docker), с -race, и содержит три машинные проверки против молчаливого скипа: набор тегов может только ДОБАВЛЯТЬ тест-файлы; каждый пакет с тестами обязан отчитаться ok поимённо; каждый TestIntegration* обязан выдать вердикт по имени. Причина такая: до 2026-07 релизный тракт не гонял почти ничего — 115 тест-файлов из 116 под shater/** в CI не исполнялись ни разу.

Отдельно scripts/check-router-tags.sh проверяет, что ни одна заявленная в FEATURES.md фича не потеряла нужный ей build-тег.

Зелёный гейт — необходимое, но не достаточное условие: он не видит стыков с ядром, procd и nftables. Это проверяется на стенде (см. docs-shater/CONTEXT.md).


Структура репозитория

Репозиторий — это оверлей продукта shater поверх дерева форка движка sing-box-lx (конфликт-фри: движок в апстрим-каталогах, продукт в своих).

Путь Что это
shater/ Go: control-plane, DNS-фильтр, агрегатор статистики, хост движка
panel/ Админ-SPA (Vite + React + TS) и её Go-сервер
openwrt/ Пакеты: shaterd, shater-core, luci-app-shater
docs-shater/ Документация продукта (см. таблицу ниже)
scripts/ build-shaterd.sh — сборка ship-артефакта
ci/ Скрипты сборки apk-фида и релизов (SDK, EC-подпись, Gitea API)
.gitea/workflows/ release.yml — CI: сборка пакетов + подписанный apk-фид
SPECS/ Конституция форка движка и спеки (Spec Kit)
docs-lx/ Справочник конфигурации фич движка (lx-config.md, .ru.md)
lx-test/, submodules/ Примеры конфигов движка и submodule AmneziaWG-рантайма
docs/, mkdocs.yml Апстрим документация sing-box (mkdocs) — как есть
adapter/ cmd/ dns/ route/ option/ protocol/ transport/ … Дерево движка sing-box-lx

CI и релизы

CI на Gitea Actions (.gitea/workflows/release.yml) собирает все 4 пакета и публикует подписанные фиды:

  • apk (25.12+) — единственный формат: по релизу на арку, индекс packages.adb подписан EC-ключом (публичный dist/shater-apk.pem; секрет — в Gitea-secret KEY_APK).

Триггеры: push тега vX.Y.Z → версионный релиз apk-vX.Y.Z-<arch>; workflow_dispatch → только роллинг. Роллинг apk-latest-<arch> обновляется на каждом прогоне, включая теговый, и после публикации проверяется через API: в нём обязаны лежать наши три пакета ровно собранной версии и ни одного ассета другой версии. Публикация — через Gitea API (ci/gitea-release.sh). Ключ никогда не перегенерируется — это инвалидировало бы доверие на всех развёрнутых роутерах.


Связь с upstream и движок

shater вкомпилирует форк движка sing-box-lx — тонкий downstream апстрима SagerNet/sing-box, добавляющий набор клиентских фич (XHTTP, AmneziaWG 2.0, MASQUE, расширения наблюдаемости) за build-тегами и живущий ребейзом на каждый upstream-тег, а не merge. Это набор самого форка, а не shater: MASQUE/CONNECT-IP мы намеренно не регистрируем — shater/generate его не порождает, а отказ от него и остального незадействованного зоопарка экономит ~6 МБ бинаря и столько же RAM на роутере (shater/registry). Форк разрабатывается по Spec Kit; неизменяемые принципы — в SPECS/CONSTITUTION.md, справочник фич движка — в docs-lx/lx-config.ru.md.

История: v0.1 (движок на xray-core, полностью рабочая и VM-проверенная версия) сохранена на ветке v0.1. v0.2 схлопнула runtime в один форкнутый бинарь.


Документация

Документ О чём
docs-shater/CONTEXT.md Начните здесь — контекст проекта, история v0.1→v0.2, testbed/инфра
docs-shater/INSTALL.md Сборка ship-артефакта и установка apk-фида (роллинг/фиксация)
docs-shater/ARCHITECTURE.md One-binary дизайн, auth-handoff, data/DNS/apply-потоки (диаграммы)
docs-shater/FEATURES.md Полный список фич с тегами MVP/T1/T2
docs-shater/ROADMAP.md Фазовый план
docs-shater/DECISIONS.md Почему sing-box, почему форк, split панели, лицензия
docs-shater/DESIGN.md Визуальная система панели — «Faceplate», токены, компоненты
docs-shater/PORTING.md Порт проверенных кусков из v0.1

Индекс папки — docs-shater/README.md.


Оборудование

Арка aarch64_cortex-a53 покрывает Banana Pi BPI-R3 (MT7986/Filogic 830) и BPI-R4 (MT7988/Filogic 880) — оба таргет OpenWrt mediatek/filogic. x86_64 — QEMU-стенд для тестов.


Лицензия

GPL-3.0 — как у upstream sing-box. Подробности — в docs-shater/DECISIONS.md (D6). Неофициальный форк, не аффилирован с SagerNet.