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
24 KiB
shater
Управляемый интернет-шлюз для роутеров на OpenWrt. Одна коробка превращает домашнюю или офисную сеть в прозрачный VPN-шлюз, сетевой блокировщик рекламы, трекеров и вредоносных доменов, средство родительского контроля по устройствам и живую панель аналитики трафика — всё локально, всё self-hosted, всё настраивается из богатой веб-панели.
Что это
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>), совместимость с BananaWRT25.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-secretKEY_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.