Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24c5a1615d | ||
|
|
4492f0599c | ||
|
|
bc2b53069a | ||
|
|
c257d6c5cc | ||
|
|
225cce5397 | ||
|
|
84d2766592 |
+20
-14
@@ -1,28 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# mod from https://gist.github.com/pldubouilh/c5703052986bfdd404005951dee54683
|
||||
|
||||
set -e -o pipefail
|
||||
set -euo pipefail
|
||||
|
||||
ARCH=$1
|
||||
DEB_SRC=$2
|
||||
OUT_IPK=$3
|
||||
|
||||
PROJECT=$(dirname "$0")/../..
|
||||
TMP_PATH=`mktemp -d`
|
||||
cp $2 $TMP_PATH
|
||||
pushd $TMP_PATH
|
||||
TMP_PATH=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP_PATH"' EXIT
|
||||
|
||||
DEB_NAME=`ls *.deb`
|
||||
ar x $DEB_NAME
|
||||
cp "$DEB_SRC" "$TMP_PATH"/
|
||||
pushd "$TMP_PATH" >/dev/null
|
||||
|
||||
# Derive the name from the file we copied — do not glob-parse `ls *.deb`.
|
||||
DEB_NAME=$(basename "$DEB_SRC")
|
||||
ar x "$DEB_NAME"
|
||||
|
||||
mkdir control
|
||||
pushd control
|
||||
pushd control >/dev/null
|
||||
tar xf ../control.tar.gz
|
||||
rm md5sums
|
||||
sed "s/Architecture:\\ \w*/Architecture:\\ $1/g" ./control -i
|
||||
rm -f md5sums
|
||||
sed "s/Architecture:\\ \w*/Architecture:\\ $ARCH/g" ./control -i
|
||||
cat control
|
||||
tar czf ../control.tar.gz ./*
|
||||
popd
|
||||
popd >/dev/null
|
||||
|
||||
DEB_NAME=${DEB_NAME%.deb}
|
||||
tar czf $DEB_NAME.ipk control.tar.gz data.tar.gz debian-binary
|
||||
popd
|
||||
tar czf "$DEB_NAME.ipk" control.tar.gz data.tar.gz debian-binary
|
||||
popd >/dev/null
|
||||
|
||||
cp $TMP_PATH/$DEB_NAME.ipk $3
|
||||
rm -r $TMP_PATH
|
||||
cp "$TMP_PATH/$DEB_NAME.ipk" "$OUT_IPK"
|
||||
|
||||
@@ -36,8 +36,15 @@ nul
|
||||
# playwright MCP screenshots/snapshots
|
||||
.playwright-mcp/
|
||||
|
||||
# working-session screenshots dropped in the repo root (not shipped docs)
|
||||
/*.png
|
||||
|
||||
# throwaway build binaries / scratch staged under tmp/
|
||||
/tmp/
|
||||
|
||||
# -- upstream sing-box-lx -----------------------------------------
|
||||
/.idea/
|
||||
.idea/
|
||||
/vendor/
|
||||
/*.json
|
||||
/*.srs
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
## Правила делегирования
|
||||
|
||||
1. ЛЮБАЯ реализация (код, тесты, конфиги, рефакторинг, отладка) выполняется
|
||||
субагентами через инструмент Agent с `model: "opus"`. Сам ты правишь файлы
|
||||
субагентами через инструмент Agent с `model: "fable"`. Сам ты правишь файлы
|
||||
только в одном случае: тривиальная правка в 1–2 строки, где постановка
|
||||
задачи дороже самой правки.
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
названия скиллов прямо в текст задания.
|
||||
|
||||
4. Независимые задачи запускай ПАРАЛЛЕЛЬНО — несколько вызовов Agent в одном
|
||||
сообщении, каждый с `model: "opus"`. Зависимые — последовательно, передавая
|
||||
сообщении, каждый с `model: "fable"`. Зависимые — последовательно, передавая
|
||||
в следующее ТЗ результаты предыдущего.
|
||||
|
||||
5. Приёмка: результат каждого субагента ты проверяешь сам (читаешь diff
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
<!-- Language: [Русский](README.md) · **English** -->
|
||||
|
||||
# shater
|
||||
|
||||
**A self-hosted internet-control appliance for OpenWrt routers.** One box turns a
|
||||
home or office network into a transparent VPN gateway, a network-wide
|
||||
ad/tracker/malware blocker, per-device parental control, and a live traffic
|
||||
dashboard — all local, all configured from a rich built-in web panel.
|
||||
|
||||
> The primary README is Russian — [README.md](README.md). This is a condensed
|
||||
> English mirror.
|
||||
|
||||
[](LICENSE)
|
||||

|
||||
|
||||
## What it is
|
||||
|
||||
shater is a network proxy stack for **OpenWrt / ImmortalWrt / BananaWRT** routers
|
||||
(Banana Pi BPI-R3, BPI-R4 and compatible). It transparently routes all LAN traffic
|
||||
through a proxy (split by domain/geo/client), filters DNS, gathers statistics, and
|
||||
is managed from a built-in web panel.
|
||||
|
||||
The engine is a **fork of [sing-box](https://github.com/SagerNet/sing-box) via
|
||||
[sing-box-lx](https://github.com/Leadaxe/sing-box-lx)**, compiled into a single Go
|
||||
binary `shaterd` together with the control plane, DNS filter, stats aggregator and
|
||||
the web panel itself. Broad protocol set: VLESS/VMess/Trojan/Shadowsocks,
|
||||
Reality/XTLS, WireGuard, **AmneziaWG 2.0**, Hysteria2, TUIC, XHTTP, MASQUE/CONNECT-IP.
|
||||
|
||||
A thin **LuCI launcher** (mini-dashboard + "Open panel" button) hands the browser a
|
||||
single-use token into the standalone SPA the daemon serves on its own port
|
||||
(default `:8088`).
|
||||
|
||||
## Highlights
|
||||
|
||||
- Transparent **TPROXY** data plane (TCP + UDP), SNI/Host/QUIC sniffing, no DNS leaks.
|
||||
- First-match routing by source / destination / list / geo / client → outbound /
|
||||
selector / chain / direct / block; node groups with balancer/observatory;
|
||||
multi-hop chains; per-rule egress.
|
||||
- **Fail-closed kill-switch** (dead group → block, never a silent direct leak); own
|
||||
`inet shater` nft table; atomic apply with `nft -c` validation and commit-confirm
|
||||
auto-rollback.
|
||||
- **DNS filtering & blocklists** with flexible sources (inline / file / url /
|
||||
geosite), compiled `.srs` matcher; Block-DoH/DoT to stop filter bypass.
|
||||
- Subscriptions (Clash / sing-box / Xray-JSON) and manual nodes; node health board.
|
||||
- Per-device control (proxy/blocklist toggles, exit country, per-device block/allow,
|
||||
schedules) and per-domain/client/device statistics from in-process DNS events.
|
||||
|
||||
Full list with MVP/T1/T2 tags — [`docs-shater/FEATURES.md`](docs-shater/FEATURES.md).
|
||||
|
||||
## Install
|
||||
|
||||
Two signed feeds. Pick by the router's OpenWrt version. Verbatim commands and the
|
||||
manual `.ipk`/`.apk` install are in [`docs-shater/INSTALL.md`](docs-shater/INSTALL.md).
|
||||
|
||||
**opkg (OpenWrt 24.10):**
|
||||
|
||||
```sh
|
||||
wget -O /etc/opkg/keys/5ac4b177689cb8e0 \
|
||||
https://git.qomar.pw/omar/shater/releases/download/latest/shater-feed.pub
|
||||
echo "src/gz shater https://git.qomar.pw/omar/shater/releases/download/latest" \
|
||||
>> /etc/opkg/customfeeds.conf
|
||||
opkg update && opkg install luci-app-shater # -> shater-core -> shaterd
|
||||
```
|
||||
|
||||
**apk (OpenWrt / ImmortalWrt / BananaWRT 25.12+):**
|
||||
|
||||
```sh
|
||||
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"
|
||||
echo "https://git.qomar.pw/omar/shater/releases/download/apk-latest-$(cat /etc/apk/arch)/packages.adb" \
|
||||
> /etc/apk/repositories.d/shater.list
|
||||
apk update && apk add luci-app-shater # -> shater-core -> shaterd
|
||||
```
|
||||
|
||||
shater ships **inert** (globals off) so install never breaks connectivity. After
|
||||
configuring nodes/rules: `uci set shater.globals.enabled=1 && uci commit shater`,
|
||||
then `shaterd apply` and `shaterd confirm`.
|
||||
|
||||
## Build from source
|
||||
|
||||
`scripts/build-shaterd.sh [VERSION] [--fast]` builds the SPA (Vite), embeds it via
|
||||
`//go:embed`, cross-builds musl-static `{amd64, arm64}` and UPX-packs the artifact
|
||||
into `openwrt/shaterd/files/`. Details in
|
||||
[`docs-shater/INSTALL.md`](docs-shater/INSTALL.md).
|
||||
|
||||
## Repository layout
|
||||
|
||||
| Path | What |
|
||||
|------|------|
|
||||
| `shater/` | Go control plane, DNS filter, stats aggregator, engine host |
|
||||
| `panel/` | Admin SPA (Vite + React + TS) and its Go server |
|
||||
| `openwrt/` | Packages: `shaterd`, `shater-core`, `luci-app-shater`, `byedpi` |
|
||||
| `docs-shater/` | Product documentation |
|
||||
| `scripts/`, `ci/`, `.gitea/workflows/` | Build script, feed/release scripts, CI |
|
||||
| `SPECS/`, `docs-lx/` | Engine-fork constitution/specs and feature-config reference |
|
||||
| `docs/`, `mkdocs.yml` | **Upstream** sing-box docs (mkdocs) — kept as-is |
|
||||
| `adapter/ cmd/ dns/ route/ option/ protocol/ transport/ …` | sing-box-lx engine tree |
|
||||
|
||||
## CI, upstream & license
|
||||
|
||||
CI (`.gitea/workflows/release.yml`) builds all 4 packages and publishes signed
|
||||
feeds: opkg (usign, key `5ac4b177689cb8e0`) and apk (EC key `shater-apk.pem`). A
|
||||
`vX.Y.Z` tag → versioned release; `workflow_dispatch` → rolling `latest`.
|
||||
|
||||
The engine is the **sing-box-lx** fork — a thin downstream of upstream sing-box that
|
||||
lives by **rebase, never merge**; its constitution is
|
||||
[`SPECS/CONSTITUTION.md`](SPECS/CONSTITUTION.md). Licensed under
|
||||
[GPL-3.0](LICENSE), like upstream sing-box. Unofficial fork, not affiliated with
|
||||
SagerNet.
|
||||
@@ -1,71 +1,310 @@
|
||||
<!-- Язык: **Русский** · [English](README.en.md) -->
|
||||
|
||||
# shater
|
||||
|
||||
**A self-hosted internet-control appliance for OpenWrt.** One box turns your
|
||||
network into a transparent VPN gateway, a network-wide ad/tracker/malware blocker,
|
||||
per-device parental control, and a live traffic dashboard — configured from a rich
|
||||
web admin panel, all local.
|
||||
**Управляемый интернет-шлюз для роутеров на OpenWrt.** Одна коробка превращает
|
||||
домашнюю или офисную сеть в прозрачный VPN-шлюз, сетевой блокировщик рекламы,
|
||||
трекеров и вредоносных доменов, средство родительского контроля по устройствам
|
||||
и живую панель аналитики трафика — всё локально, всё self-hosted, всё
|
||||
настраивается из богатой веб-панели.
|
||||
|
||||
[](LICENSE)
|
||||

|
||||

|
||||

|
||||
|
||||
> ⚠️ **v0.2 is under active development on a new foundation.** The previous,
|
||||
> complete and VM-verified xray-based version lives on the **[`v0.1`](../../src/branch/v0.1)**
|
||||
> branch and still installs from the signed feed.
|
||||
---
|
||||
|
||||
## What v0.2 is
|
||||
## Что это
|
||||
|
||||
shater v0.2 is built as a **fork of [sing-box](https://github.com/SagerNet/sing-box)
|
||||
(via [sing-box-lx](https://github.com/Leadaxe/sing-box-lx))** with our whole
|
||||
product embedded in the one binary: the proxy engine, a control plane, a DNS
|
||||
filter, and a full admin panel. Riding sing-box gives a broad, up-to-date protocol
|
||||
set — VLESS/VMess/Trojan/Shadowsocks, Reality, **AmneziaWG 2.0**, Hysteria2, TUIC —
|
||||
without reinventing the anti-DPI arms race.
|
||||
**shater** — это сетевой прокси-стек для роутеров на **OpenWrt / ImmortalWrt /
|
||||
BananaWRT** (Banana Pi BPI-R3, BPI-R4 и совместимые). Он прозрачно (без настройки
|
||||
клиентов) заворачивает весь LAN-трафик через прокси с маршрутизацией по домену,
|
||||
гео и клиенту, фильтрует DNS, собирает статистику и управляется из встроенной
|
||||
веб-панели.
|
||||
|
||||
The UI is split for both integration and a great experience: a **thin LuCI app**
|
||||
(a small dashboard + an "Open panel" button) hands a short-lived token to a
|
||||
**standalone admin panel** the daemon serves on its own port — so panel auth is
|
||||
bootstrapped from LuCI's existing login, and the real UX is a modern SPA we fully
|
||||
own.
|
||||
Ядро — **форк движка [sing-box](https://github.com/SagerNet/sing-box) через
|
||||
[sing-box-lx](https://github.com/Leadaxe/sing-box-lx)** — вкомпилировано в один
|
||||
Go-бинарь `shaterd` вместе с control-plane, DNS-фильтром, агрегатором статистики и
|
||||
самой веб-панелью. За счёт sing-box поддерживается широкий и актуальный набор
|
||||
протоколов: VLESS/VMess/Trojan/Shadowsocks, Reality/XTLS, WireGuard,
|
||||
**AmneziaWG 2.0**, Hysteria2, TUIC, XHTTP, MASQUE/CONNECT-IP.
|
||||
|
||||
## Highlights (planned)
|
||||
Интеграция в OpenWrt — тонкий **LuCI-лаунчер**: мини-дашборд и кнопка «Открыть
|
||||
панель», которая по одноразовому токену передаёт браузер в полноценную SPA-панель,
|
||||
поднятую демоном на собственном порту (по умолчанию `:8088`).
|
||||
|
||||
- Transparent TPROXY proxy (TCP+UDP), split by domain/geo/client, no DNS leaks.
|
||||
- Broad protocols incl. **AmneziaWG 2.0**, Reality, Hysteria2, TUIC.
|
||||
- Network-wide **DNS blocklists** with flexible sources (inline / file / url /
|
||||
geosite) and an efficient matcher for million-entry lists.
|
||||
- **Per-domain, per-client, per-device statistics** — fed by the engine's DNS
|
||||
events in-process (no log scraping).
|
||||
- **Per-device control**: block a site for one device or everyone; per-device
|
||||
exit/proxy toggles; schedules; alerts.
|
||||
- Fail-closed kill-switch, atomic apply with commit-confirm rollback, signed opkg
|
||||
feed.
|
||||
---
|
||||
|
||||
See **[`docs-shater/FEATURES.md`](docs-shater/FEATURES.md)** for the full list.
|
||||
## Ключевые возможности
|
||||
|
||||
## Documentation
|
||||
**Прозрачный прокси и маршрутизация**
|
||||
- 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 по правилу.
|
||||
|
||||
| Doc | What |
|
||||
|-----|------|
|
||||
| [`docs-shater/CONTEXT.md`](docs-shater/CONTEXT.md) | **Start here** — project context, v0.1→v0.2 history, decisions in brief, testbed/infra |
|
||||
| [`docs-shater/ROADMAP.md`](docs-shater/ROADMAP.md) | Phased plan (Phase 1 = fork + embedding prototype) |
|
||||
| [`docs-shater/FEATURES.md`](docs-shater/FEATURES.md) | Full feature list with MVP/T1/T2 tags |
|
||||
| [`docs-shater/ARCHITECTURE.md`](docs-shater/ARCHITECTURE.md) | One-binary design, auth handoff, data/DNS/apply flow (diagrams) |
|
||||
| [`docs-shater/DECISIONS.md`](docs-shater/DECISIONS.md) | Why sing-box, why fork, why the panel split, license, etc. |
|
||||
| [`docs-shater/DESIGN.md`](docs-shater/DESIGN.md) | Admin-panel visual system — the "Faceplate" direction, tokens, components, north-star prototype |
|
||||
**Надёжность («железно»)**
|
||||
- **Fail-closed kill-switch**: мёртвая группа → block, а не тихая утечка мимо
|
||||
прокси; собственная nft-таблица `inet shater` и свои марки/таблицы, fw4 не
|
||||
трогаем.
|
||||
- Атомарный apply с валидацией движком и `nft -c`, **commit-confirm** с
|
||||
авто-откатом к последней рабочей конфигурации.
|
||||
- Идемпотентный reconcile из hotplug/boot под flock; management-bypass
|
||||
(SSH/LuCI/LAN) всегда в обход.
|
||||
|
||||
## Status
|
||||
**DNS, фильтрация, блокировки**
|
||||
- Перехват `:53`, DNS движка sing-box в процессе; резолверы DoH/DoT/plain/FakeIP,
|
||||
выбор резолвера по домену.
|
||||
- **Блок-листы с гибкими источниками**: `inline` / `file` / `url` (авто-обновление) /
|
||||
категория `geosite`; hosts-файл, plain-список или AdBlock-стиль `||domain^`
|
||||
компилируются в локальный `.srs`. Эффективный компилированный матчер вместо
|
||||
dnsmasq-мегасписков.
|
||||
- **Block-DoH/DoT** — не даёт устройствам обходить фильтр через свой шифрованный DNS.
|
||||
|
||||
Foundation reset complete: v0.1 preserved on its branch, `main` reset for v0.2.
|
||||
Next is **Phase 1** — fork sing-box-lx into `main` and stand up the embedding
|
||||
prototype (prove AmneziaWG 2.0, measure binary size). Follow `docs-shater/ROADMAP.md`.
|
||||
**Подписки и узлы**
|
||||
- Подписки (VLESS/VMess/Trojan/SS/WG/AmneziaWG), форматы Clash/sing-box/Xray-JSON,
|
||||
интервал обновления + вручную + на загрузке; стабильная идентичность узла между
|
||||
обновлениями; квоты/срок из `subscription-userinfo`.
|
||||
- Ручные узлы: share-ссылки, импорт файла, `wg-quick`/AmneziaWG `.conf`.
|
||||
- Health board: TCP + реальная проба через прокси-путь, exit-IP, «протестировать
|
||||
все».
|
||||
|
||||
## Hardware
|
||||
**Контроль по устройствам**
|
||||
- Авто-обнаружение устройств (dhcp.leases + `ip neigh`), имена, живой статус/трафик.
|
||||
- Тумблеры на устройство: прокси on/off, блок-листы on/off, страна/узел выхода;
|
||||
блок/allow домена для одного устройства или для всех; расписания.
|
||||
|
||||
`aarch64_cortex-a53` covers Banana Pi **BPI-R3** (MT7986/Filogic 830) and **BPI-R4**
|
||||
(MT7988/Filogic 880), both the OpenWrt `mediatek/filogic` target. `x86_64` is the
|
||||
QEMU test VM.
|
||||
**Статистика и видимость**
|
||||
- Топ доменов (запрошенные/заблокированные), allowed-vs-blocked, разбивка по
|
||||
устройствам, таймлайны — из DNS-событий движка в процессе (без скрейпинга логов).
|
||||
- Трафик по клиенту/узлу/правилу (байты) из nft-счётчиков; живой query-log.
|
||||
|
||||
## License
|
||||
**Панель и профили**
|
||||
- Встроенная SPA-панель (собственный порт, вшита в бинарь): overview, узлы и
|
||||
подписки, правила маршрутизации, DNS/блок-листы, устройства, apply/rollback.
|
||||
- Именованные профили/сцены и WAN-профили (условные оверрайды).
|
||||
|
||||
[GPL-3.0](LICENSE) (sing-box is GPL-3.0). See `docs-shater/DECISIONS.md` D6.
|
||||
Полный список с тегами MVP/T1/T2 — [`docs-shater/FEATURES.md`](docs-shater/FEATURES.md).
|
||||
|
||||
---
|
||||
|
||||
## Архитектура
|
||||
|
||||
Один бинарь `shaterd` держит движок, control-plane, DNS-фильтр и веб-сервер панели
|
||||
в одном процессе; OpenWrt-обвязка (тонкий LuCI + procd/system glue) оборачивает его.
|
||||
Конфиг — UCI desired-state; демон рендерит его в конфиг движка и применяет;
|
||||
телеметрия течёт обратно в панель.
|
||||
|
||||
```mermaid
|
||||
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 (flow-offload) · block. Подробные диаграммы (auth-handoff,
|
||||
data-plane, DNS-flow, apply-flow) — в [`docs-shater/ARCHITECTURE.md`](docs-shater/ARCHITECTURE.md).
|
||||
|
||||
---
|
||||
|
||||
## Установка
|
||||
|
||||
shater поставляется двумя подписанными фидами. Выберите по версии OpenWrt на роутере:
|
||||
|
||||
- **OpenWrt 24.10** → фид **opkg** (`.ipk`, `Packages.gz`, ключ usign).
|
||||
- **OpenWrt / ImmortalWrt / BananaWRT 25.12+** → фид **apk** (`.apk`, `packages.adb`,
|
||||
EC-ключ).
|
||||
|
||||
Пакеты ставятся по зависимостям: `shaterd` → `shater-core` → `luci-app-shater`
|
||||
(+ опциональный `byedpi`). `shaterd` подтягивается автоматически как зависимость.
|
||||
|
||||
### Путь A — фид opkg (OpenWrt 24.10)
|
||||
|
||||
```sh
|
||||
# 1) доверяем ключу фида — ИМЯ файла обязано равняться отпечатку usign-ключа.
|
||||
wget -O /etc/opkg/keys/5ac4b177689cb8e0 \
|
||||
https://git.qomar.pw/omar/shater/releases/download/latest/shater-feed.pub
|
||||
|
||||
# 2) добавляем фид (один URL обслуживает все арки).
|
||||
echo "src/gz shater https://git.qomar.pw/omar/shater/releases/download/latest" \
|
||||
>> /etc/opkg/customfeeds.conf
|
||||
|
||||
# 3) обновляемся и ставим (shaterd подтянется как зависимость).
|
||||
opkg update
|
||||
opkg install luci-app-shater # -> shater-core -> shaterd
|
||||
opkg install byedpi # опционально: ByeDPI desync-egress
|
||||
```
|
||||
|
||||
Обновление: `opkg update && opkg upgrade shaterd shater-core luci-app-shater byedpi`
|
||||
(обновляйте только эти четыре пакета, не системные).
|
||||
|
||||
### Путь B — фид apk (OpenWrt / ImmortalWrt / BananaWRT 25.12+)
|
||||
|
||||
`/etc/apk/arch` сам выбирает нужный per-arch релиз (apk-релизы раздельны по арке):
|
||||
|
||||
```sh
|
||||
# 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 add byedpi # опционально: ByeDPI desync-egress
|
||||
```
|
||||
|
||||
Обновление: `apk update && apk upgrade shaterd shater-core luci-app-shater byedpi`.
|
||||
|
||||
> Полные инструкции — раздельная установка из `.ipk`/`.apk` вручную, закрепление
|
||||
> версии (`vX.Y.Z` / `apk-vX.Y.Z-<arch>`), совместимость с BananaWRT
|
||||
> `25.12-mtk-vendor` — в [`docs-shater/INSTALL.md`](docs-shater/INSTALL.md).
|
||||
|
||||
### Включение
|
||||
|
||||
shater ставится **инертным** (globals выключены), чтобы установка не рвала связь.
|
||||
Настройте узлы/правила (через панель или `uci`), затем включите и примените:
|
||||
|
||||
```sh
|
||||
uci set shater.globals.enabled=1
|
||||
uci commit shater
|
||||
shaterd apply # apply + вооружить commit-confirm на живом демоне
|
||||
shaterd confirm # подтвердить (отменяет авто-откат)
|
||||
```
|
||||
|
||||
`/etc/init.d/shater enable && /etc/init.d/shater start` поднимает демона под procd.
|
||||
Кнопка «Открыть панель» в LuCI чеканит одноразовый токен и передаёт браузер в
|
||||
панель (`:8088` по умолчанию).
|
||||
|
||||
---
|
||||
|
||||
## Сборка из исходников
|
||||
|
||||
Ship-артефакт — бинарь `shaterd` со вшитой SPA. Собирается вне дерева SDK скриптом
|
||||
`scripts/build-shaterd.sh`:
|
||||
|
||||
```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`](docs-shater/INSTALL.md).
|
||||
|
||||
---
|
||||
|
||||
## Структура репозитория
|
||||
|
||||
Репозиторий — это оверлей продукта **shater** поверх дерева форка движка
|
||||
**sing-box-lx** (конфликт-фри: движок в апстрим-каталогах, продукт в своих).
|
||||
|
||||
| Путь | Что это |
|
||||
|------|---------|
|
||||
| `shater/` | Go: control-plane, DNS-фильтр, агрегатор статистики, хост движка |
|
||||
| `panel/` | Админ-SPA (Vite + React + TS) и её Go-сервер |
|
||||
| `openwrt/` | Пакеты: `shaterd`, `shater-core`, `luci-app-shater`, `byedpi` |
|
||||
| `docs-shater/` | Документация продукта (см. таблицу ниже) |
|
||||
| `scripts/` | `build-shaterd.sh` — сборка ship-артефакта |
|
||||
| `ci/` | Скрипты сборки фидов и релизов (SDK, usign/EC, Gitea API) |
|
||||
| `.gitea/workflows/` | `release.yml` — CI: сборка пакетов + подписанные фиды opkg/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 пакета и
|
||||
публикует **подписанные фиды**:
|
||||
|
||||
- **opkg (24.10):** один комбинированный релиз, подписан usign-ключом (публичный
|
||||
`dist/shater-feed.pub`, отпечаток `5ac4b177689cb8e0`; секрет — в Gitea-secret
|
||||
`KEY_BUILD`).
|
||||
- **apk (25.12+):** параллельная линия, **по релизу на арку**, подписан EC-ключом
|
||||
(`dist/shater-apk.pem`; секрет — `KEY_APK`).
|
||||
|
||||
Триггеры: push тега **`vX.Y.Z`** → версионный релиз; `workflow_dispatch` →
|
||||
плавающий `latest`/`apk-latest-<arch>` (всегда свежий фид). Публикация — через
|
||||
Gitea API (`ci/gitea-release.sh`). Ключи **никогда не перегенерируются** — это
|
||||
инвалидировало бы доверие на всех развёрнутых роутерах.
|
||||
|
||||
---
|
||||
|
||||
## Связь с upstream и движок
|
||||
|
||||
shater вкомпилирует **форк движка sing-box-lx** — тонкий downstream апстрима
|
||||
[SagerNet/sing-box](https://github.com/SagerNet/sing-box), добавляющий набор
|
||||
клиентских фич (XHTTP, AmneziaWG 2.0, MASQUE, расширения наблюдаемости) за
|
||||
build-тегами и живущий **ребейзом на каждый upstream-тег, а не merge**. Форк
|
||||
разрабатывается по Spec Kit; неизменяемые принципы — в
|
||||
[`SPECS/CONSTITUTION.md`](SPECS/CONSTITUTION.md), справочник фич движка — в
|
||||
[`docs-lx/lx-config.ru.md`](docs-lx/lx-config.ru.md).
|
||||
|
||||
История: **v0.1** (движок на xray-core, полностью рабочая и VM-проверенная версия)
|
||||
сохранена на ветке **[`v0.1`](../../src/branch/v0.1)**. v0.2 схлопнула runtime в
|
||||
один форкнутый бинарь.
|
||||
|
||||
---
|
||||
|
||||
## Документация
|
||||
|
||||
| Документ | О чём |
|
||||
|----------|-------|
|
||||
| [`docs-shater/CONTEXT.md`](docs-shater/CONTEXT.md) | **Начните здесь** — контекст проекта, история v0.1→v0.2, testbed/инфра |
|
||||
| [`docs-shater/INSTALL.md`](docs-shater/INSTALL.md) | Сборка ship-артефакта и установка обоих фидов (opkg/apk) |
|
||||
| [`docs-shater/ARCHITECTURE.md`](docs-shater/ARCHITECTURE.md) | One-binary дизайн, auth-handoff, data/DNS/apply-потоки (диаграммы) |
|
||||
| [`docs-shater/FEATURES.md`](docs-shater/FEATURES.md) | Полный список фич с тегами MVP/T1/T2 |
|
||||
| [`docs-shater/ROADMAP.md`](docs-shater/ROADMAP.md) | Фазовый план |
|
||||
| [`docs-shater/DECISIONS.md`](docs-shater/DECISIONS.md) | Почему sing-box, почему форк, split панели, лицензия |
|
||||
| [`docs-shater/DESIGN.md`](docs-shater/DESIGN.md) | Визуальная система панели — «Faceplate», токены, компоненты |
|
||||
| [`docs-shater/PORTING.md`](docs-shater/PORTING.md) | Порт проверенных кусков из v0.1 |
|
||||
|
||||
Индекс папки — [`docs-shater/README.md`](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](LICENSE) — как у upstream sing-box. Подробности — в
|
||||
[`docs-shater/DECISIONS.md`](docs-shater/DECISIONS.md) (D6). Неофициальный форк, не
|
||||
аффилирован с SagerNet.
|
||||
|
||||
+15
-235
@@ -1,239 +1,19 @@
|
||||
[English](README.md) · **Русский**
|
||||
# shater — этот файл переехал
|
||||
|
||||
# sing-box-lx
|
||||
Лицо этого репозитория — продукт **shater** (управляемый интернет-шлюз для
|
||||
роутеров на OpenWrt). Основной README на русском — **[README.md](README.md)**;
|
||||
краткая английская версия — **[README.en.md](README.en.md)**.
|
||||
|
||||
> **Тонкий downstream-форк [SagerNet/sing-box](https://github.com/SagerNet/sing-box).**
|
||||
> Небольшой набор клиентских фич поверх upstream — транспорт **XHTTP**, **AmneziaWG 2.0**, **MASQUE** (CONNECT-IP / Cloudflare WARP), расширения **наблюдаемости** (CommandClient) и балансировка нагрузки **round_robin** — каждая за своим build-tag.
|
||||
> Набор может расти, философия — нет: жить ребейзом на каждый upstream-тег, а не отдельной жизнью.
|
||||
Раньше здесь лежал README форка движка **sing-box-lx**, который shater
|
||||
вкомпилирует в свой бинарь. Документация именно движка-форка живёт в его слое:
|
||||
|
||||
> 📄 README самого upstream sing-box — **[на GitHub](https://github.com/SagerNet/sing-box/blob/main/README.md)** (всегда актуальный).
|
||||
- **[docs-lx/lx-config.ru.md](docs-lx/lx-config.ru.md)** — справочник конфигурации
|
||||
фич движка (XHTTP, AmneziaWG 2.0, MASQUE).
|
||||
- **[SPECS/CONSTITUTION.md](SPECS/CONSTITUTION.md)** — конституция тонкого форка
|
||||
(принципы, build-tag изоляция, ребейз-модель).
|
||||
- **[SPECS/README.md](SPECS/README.md)** — формат задач Spec Kit.
|
||||
- Апстрим-README самого sing-box —
|
||||
[на GitHub](https://github.com/Leadaxe/sing-box-lx).
|
||||
|
||||
Это не отдельный проект и не «улучшенный sing-box». Это upstream sing-box **плюс несколько фич**, реализованных так, чтобы их можно было переносить на новые версии sing-box годами, почти без конфликтов. Со временем фич может становиться больше — другие протоколы, новые возможности, — но каждая обязана жить по тем же правилам тонкого форка ([CONSTITUTION](SPECS/CONSTITUTION.md)).
|
||||
|
||||
---
|
||||
|
||||
## Уникальное позиционирование
|
||||
|
||||
В экосистеме sing-box форки, добавляющие XHTTP/AmneziaWG, делятся на два лагеря — и `sing-box-lx` не входит ни в один:
|
||||
|
||||
| Форк | Фичи | Подход | Синк с upstream |
|
||||
|------|------|--------|-----------------|
|
||||
| **SagerNet/sing-box** (upstream) | базовый | — | — |
|
||||
| **shtorm-7/sing-box-extended** | десятки (WARP, MASQUE, MTProxy, XHTTP, AWG2, …) | «комбайн», правки повсюду | отдельная ветка, без ребейза на теги |
|
||||
| **amnezia-vpn/amnezia-box**, **hoaxisr/amnezia-box** | только AWG | толстый форк, правки in-place | синк по веткам (`dev-next`/`stable-next`) |
|
||||
| **➡ sing-box-lx** (этот репозиторий) | **малый набор (XHTTP, AWG2, наблюдаемость, round_robin)** | **тонкий: новые файлы за build-tag, минимум касаний upstream** | **ребейз атомарных `// lx`-коммитов на upstream-теги** |
|
||||
|
||||
**Чем мы отличаемся:**
|
||||
|
||||
- **Минимальная дивергенция.** Новый код живёт в новых файлах. Существующие upstream-файлы трогаются только в крошечных помеченных швах `// lx:begin … // lx:end`. → дешёвые ребейзы.
|
||||
- **Изоляция за build-tag.** Фичи включаются тегами `with_xhttp` / `with_awg`. Сборка **без** них байт-в-байт повторяет поведение upstream — фичи ничего не ломают по умолчанию.
|
||||
- **Идентичность сохранена.** Go-модуль остаётся `github.com/sagernet/sing-box`, бинарь называется `sing-box`. Суффикс `-lx` есть только в строке версии (`1.13.13-lx.N`).
|
||||
- **Build-tag — родная конвенция sing-box**, а не наше изобретение (`with_quic`, `with_wireguard`, …). Мы просто применяем её с максимальной дисциплиной.
|
||||
|
||||
> Готовые форки-комбайны мы **не тянем как зависимость**, а используем только как референс wire-протокола.
|
||||
|
||||
---
|
||||
|
||||
## Фичи и статус
|
||||
|
||||
| # | Фича | Что это | Статус |
|
||||
|---|------|---------|--------|
|
||||
| **XHTTP** | клиентский транспорт | Xray-совместимый «splithttp» (режимы `auto`/`packet-up`/`stream-up`/`stream-one`) поверх Reality/TLS/h2c | ✅ **проверен живым Xray (3x-ui) сервером** (packet-up/auto): handshake + DNS + HTTPS + скачивание. `stream-one` — известный баг framing |
|
||||
| **AmneziaWG 2.0** | клиентский endpoint | обфускация WireGuard: `Jc/Jmin/Jmax`, `S1–S4`, `H1–H4` + **2.0**: `I1–I5` (CPS — кастомные пакеты-приманки) | ✅ собирается, проходит `check`; зависимость **активирована** ([Leadaxe/wireguard-go-awg2-lx](https://github.com/Leadaxe/wireguard-go-awg2-lx) — sagernet-база + обфускация); **проверено живым AWG2-сервером**: handshake + keepalive + трафик наружу |
|
||||
| **Маскировка `id/ip/ib`** | сахар над AWG | WireSock-стиль: декларативная маскировка поверх `I1` — домен (`id`) + протокол (`ip`: `quic`/`dns`/`stun`/`sip`) + браузер (`ib`), ядро строит клиент-инициированную `I1`-приманку: `quic` = out-of-order фрагментированный Initial (i1+i2), `dns`/`stun`/`sip` = query/Binding-Request/INVITE | ✅ **`ip=quic` device-проверен на реальном LTE/WARP DPI** (~330 мс, упрощает Cloudflare WARP); `dns`/`stun`/`sip` собираются и проходят `check`, но режутся как класс протокола к WARP-edge — для других провайдеров |
|
||||
| **Наблюдаемость** (расширения CommandClient) | live-стрим для UI | нативные расширения libbox gRPC за `with_lx_command`: `URLTestOutbound`, `GetRules`, `GetGroups`, `GetOutbounds`, `GetPool`, плюс `Connection.detourList` (хвост detour'а отдельным полем, SPEC 017) и `SubscribeDNSQueries` — структурный live-поток DNS (домен, qtype, rcode `-1`=ошибка, CNAME-цепочка, привязка к процессу, `dnsServer`/`dnsServerType`/`outbound`, SPEC 018) | ✅ в rc-серии, потребляется **LxBox**. SPEC 014–018: [`014`](SPECS/014-CLASH_API_TO_COMMANDCLIENT_MIGRATION/SPEC.md) · [`015`](SPECS/015-COMMAND_PROTOCOL_RPC_EXTENSIONS/SPEC.md) · [`017`](SPECS/017-CONNECTION_DETOUR_CHAIN/SPEC.md) · [`018`](SPECS/018-DNS_QUERY_STREAM/SPEC.md) |
|
||||
| **round_robin** (балансировка нагрузки) | режим `urltest` | пул-балансировка на `urltest` за `with_lx_command` (для `GetPool`): `mode` `least_test` (дефолт) \| `round_robin`; `balancer{pool (дефолт 3), pool_tolerance (0=держать живые / >0=топ по задержке), sticky_hash}`. Sticky-ключ: пропущен/`[]` → дефолт `["process","domain"]`, `["none"]` → выкл; компоненты `process`/`domain`/`source_ip`/`dest_ip`/`dest_port`. Фиксированные слоты `slot[hash(key)%pool]` (FNV-64a), замена в слоте; `GetPool` отдаёт слоты | ✅ локально равномерно (10/10/10, sticky off); rc.15 починил схлопывание `domain`-ключа (теперь читается `metadata.Domain`, переживающий resolve домен→IP, а не пустой `destination.Fqdn`) — на устройстве равномерность 0.27 → 0.95+. SPEC [`019`](SPECS/019-URLTEST_MODE_STICKY/SPEC.md), конфиг — [docs/.../urltest.md](docs/configuration/outbound/urltest.md) |
|
||||
| **MASQUE** (`type: masque`) | клиентский outbound | CONNECT-IP (RFC 9484) поверх HTTP/3 **или** HTTP/2 для **Cloudflare WARP** (SPEC 021): туннелирует целые IP-пакеты через userspace gVisor-стек; `profile` (`cloudflare`/`standard`), `network` (`h3`/`h2`), pinning ECDSA public key, idle-suspend + самовосстановление. h2 — ручной фреймер поверх `x/net/http2` (без доп. зависимостей); `connect-ip-go` вкопан | ✅ **device-verified на Wi-Fi и LTE** (`warp=on`, реальный трафик на `h3` и `h2`); на сетях, режущих входящий UDP:443, `h3`-handshake виснет — там `network: h2` (TCP:443) |
|
||||
|
||||
Подробные отчёты — в [`SPECS/002-…`](SPECS/002-XHTTP_CLIENT_TRANSPORT/IMPLEMENTATION_REPORT.md), [`SPECS/003-…`](SPECS/003-AWG2_CLIENT_ENDPOINT/IMPLEMENTATION_REPORT.md) и [`SPECS/009-…`](SPECS/009-WIRESOCK_MASQUERADE_PROFILES/IMPLEMENTATION_REPORT.md). Полный справочник конфига — **[docs-lx/lx-config.ru.md](docs-lx/lx-config.ru.md)**.
|
||||
|
||||
> **Не поддерживается (слой Reality, отложено):** post-quantum Reality (`pqv` / ML-DSA-65) и `spiderX` из Xray. Это Xray-специфичные фичи Reality, которых нет в sing-box, а Reality — upstream-слой TLS, который мы держим нетронутым (это не одна из наших фич). Классический X25519 Reality работает; сервер, который **требует** post-quantum Reality, не подключится. Это ограничение sing-box — правильнее решать в upstream (получим на ребейзе).
|
||||
|
||||
---
|
||||
|
||||
## Сборка
|
||||
|
||||
Сборка идёт через отдельный **`Makefile.lx`** (upstream `Makefile` не трогаем):
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/Leadaxe/sing-box-lx
|
||||
make -f Makefile.lx lx-build
|
||||
# → бинарь ./sing-box с версией вида 1.13.13-lx.1
|
||||
```
|
||||
|
||||
> `--recurse-submodules` обязателен для `with_awg`: рантайм AmneziaWG подключён submodule'ом `submodules/wireguard-go` → [Leadaxe/wireguard-go-awg2-lx](https://github.com/Leadaxe/wireguard-go-awg2-lx).
|
||||
|
||||
Под капотом — стандартный `go build` с набором тегов (единственный источник истины — `make -f Makefile.lx lx-print-tags`):
|
||||
|
||||
```
|
||||
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_clash_api,with_naive_outbound,with_purego,badlinkname,tfogo_checklinkname0,with_xhttp,with_awg
|
||||
```
|
||||
|
||||
Это клиентский feature-set upstream **минус** серверные/нерелевантные теги — `with_acme` (серверный выпуск сертов), `with_tailscale`, `with_ccm`/`with_ocm` (AI-прокси) — **плюс** `with_purego` (CGO-free кросс-сборка, чтобы `with_naive_outbound`/cronet собирался при `CGO=0` на любом desktop-таргете, кроме Windows 7 / 32-бит legacy-сборки, где naive выкинут — у `cronet-go` нет windows/386) и наши фичи `with_xhttp` / `with_awg`. Всё остальное — ровно как upstream.
|
||||
|
||||
Проверка конфигов:
|
||||
|
||||
```bash
|
||||
./sing-box check -c lx-test/config/xhttp_reality.json
|
||||
./sing-box check -c lx-test/config/awg2_basic.json
|
||||
```
|
||||
|
||||
> `lx-test/config/` — наши примеры (upstream `test/` — отдельный Go-модуль, его не используем).
|
||||
|
||||
**Android (`libbox.aar`).** `make lib_install && make lib_android` собирает gomobile-AAR — `libbox.aar` (SDK 23) + `libbox-legacy.aar` (SDK 21) — с зашитыми `with_xhttp`/`with_awg` (и без `tailscale`), для встраивания в Android-приложение-потребитель (нужны NDK r28 + OpenJDK 17). `Libbox.version()` отдаёт `…-lx.N`.
|
||||
|
||||
---
|
||||
|
||||
## Конфигурация фич
|
||||
|
||||
> Полные таблицы полей, дефолты и `awg-quick`→JSON маппинг — **[docs-lx/lx-config.ru.md](docs-lx/lx-config.ru.md)**. Здесь — кратко.
|
||||
|
||||
### XHTTP (outbound transport)
|
||||
|
||||
```jsonc
|
||||
"transport": {
|
||||
"type": "xhttp",
|
||||
"host": "example.com",
|
||||
"path": "/xhttp",
|
||||
"mode": "auto" // auto | packet-up | stream-up | stream-one
|
||||
}
|
||||
```
|
||||
|
||||
### AmneziaWG 2.0 (endpoint)
|
||||
|
||||
Поля AWG промотированы прямо в `WireGuardEndpointOptions`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"type": "wireguard",
|
||||
// … стандартные поля wireguard (private_key, address, peers, …) …
|
||||
"jc": 10, "jmin": 50, "jmax": 100,
|
||||
"s1": 20, "s2": 20, "s3": 60, "s4": 60,
|
||||
"h1": 1, "h2": 2, "h3": 3, "h4": 4,
|
||||
"i1": "<b 0x...><r 12>", "i2": "", "i3": "", "i4": "", "i5": "" // 2.0 CPS
|
||||
}
|
||||
```
|
||||
|
||||
> `I1–I5` — это конфиг (не согласуется по сети), значения должны **совпадать на клиенте и сервере**, регистрозависимы.
|
||||
|
||||
**Сахар-маскировка (`id`/`ip`/`ib`).** Вместо ручного `i1` задаёшь домен, протокол и
|
||||
браузер — ядро само собирает `I1`-приманку (стиль WireSock). Удобно для упрощения
|
||||
коннекта к **Cloudflare WARP**:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"type": "wireguard",
|
||||
// … стандартные поля wireguard …
|
||||
"id": "www.google.com", "ip": "quic", "ib": "chrome" // quic: id идёт как SNI в ClientHello
|
||||
// или: "ip": "dns", "id": "www.google.com" // dns/sip: id идёт как QNAME/host
|
||||
}
|
||||
```
|
||||
|
||||
`ip` ∈ `quic|dns|stun|sip`; `id` обязателен только для `quic` (SNI); для `dns`/`sip` опционален (без него генерится псевдо-имя), `stun` игнорирует. Где задан — идёт на провод (SNI / QNAME / host)
|
||||
и опционален для `sip` (без него генерится псевдо-host) и `stun`; `ib` ∈ `chrome|firefox|curl`
|
||||
(только quic, эффект минимальный — без JA3-fingerprint). Взаимоисключается с явным `i1`.
|
||||
|
||||
Для **`quic`** ядро генерит out-of-order фрагментированный QUIC Initial (RFC 9001) — реальный
|
||||
ClientHello, нарезанный на CRYPTO-фреймы в перемешанном порядке, так что line-rate DPI парсит
|
||||
мусор и пропускает. Раскладка рандомизируется на каждый вызов (нет межюзерной сигнатуры), и
|
||||
`ip=quic` теперь шлёт **два** независимых Initial (i1+i2) — поток читается как развивающаяся
|
||||
QUIC-сессия. Это **единственный профиль, device-проверенный на реальном LTE/WARP DPI** (~330 мс).
|
||||
`dns`/`stun`/`sip` реализованы как корректные клиент-инициированные запросы, но режутся как класс
|
||||
протокола к WARP-edge (raw DNS/STUN/SIP к дата-центровому IP сам по себе аномален) — сохранены
|
||||
для других провайдеров, чей DPI проверяет лишь корректность пакета. См.
|
||||
[docs-lx/lx-config.ru.md](docs-lx/lx-config.ru.md) и [примеры SPECS/009](SPECS/009-WIRESOCK_MASQUERADE_PROFILES/EXAMPLES.md).
|
||||
|
||||
### MASQUE (outbound — Cloudflare WARP)
|
||||
|
||||
Outbound `masque` туннелирует целые IP-пакеты через **CONNECT-IP (RFC 9484)**, HTTP/3 или HTTP/2,
|
||||
к **Cloudflare WARP**. Не путать с AWG-сахаром *masquerade* `id/ip/ib` выше — разные фичи, одно слово.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"type": "masque",
|
||||
"tag": "warp",
|
||||
"server": "162.159.198.2",
|
||||
"server_port": 443,
|
||||
"profile": "cloudflare", // cloudflare (WARP) | standard (RFC 9484)
|
||||
"network": "h3", // ТРАНСПОРТ: h3 (QUIC) | h2 (HTTP/2). НЕ tcp/udp — это network_list
|
||||
"sni": "www.microsoft.com", // domain-fronting; endpoint аутентифицируется пиннингом public key, не по SNI
|
||||
"private_key": "<base64 DER EC>",
|
||||
"public_key": "<base64 DER PKIX>",
|
||||
"ip": "172.16.0.2/32", "ipv6": "2606:4700:110:...::/128"
|
||||
}
|
||||
```
|
||||
|
||||
Ключевой материал (`private_key`/`public_key`/`ip`/`ipv6`) берётся готовым из конфига — регистрацию
|
||||
устройства в WARP делает клиент. На сетях, режущих входящий UDP:443, `h3`-handshake виснет —
|
||||
переключите узел на `network: h2` (TCP:443). Полный справочник —
|
||||
[docs-lx/lx-config.ru.md §4](docs-lx/lx-config.ru.md) и [SPECS/021](SPECS/021-MASQUE_CONNECT_IP_OUTBOUND/CONFIG.md).
|
||||
|
||||
---
|
||||
|
||||
## Модель сопровождения
|
||||
|
||||
```
|
||||
upstream tag (vX.Y.Z)
|
||||
│
|
||||
└─► ветка lx = upstream + N атомарных // lx-коммитов
|
||||
├─ FORK_BOOTSTRAP (Makefile.lx, CI, версия)
|
||||
├─ XHTTP client transport
|
||||
├─ AWG2 client endpoint
|
||||
└─ … (новые фичи — такими же атомарными // lx-коммитами)
|
||||
```
|
||||
|
||||
- **Только ребейз, никогда merge.** На новый upstream-тег ветка `lx` ребейзится поверх него.
|
||||
- Каждая фича — атомарный коммит(ы), помеченный `// lx`. Новые файлы конфликтов не дают; швы в upstream-файлах малы и переносятся вручную.
|
||||
- Разработка ведётся по **Spec Kit** (`SPECS/NNN-T-S-NAME/`: SPEC → PLAN → TASKS → IMPLEMENTATION_REPORT).
|
||||
|
||||
### Remotes
|
||||
|
||||
```bash
|
||||
origin git@github.com:Leadaxe/sing-box-lx.git # ветка по умолчанию: lx
|
||||
upstream https://github.com/SagerNet/sing-box.git
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Структура lx-специфики
|
||||
|
||||
| Путь | Назначение |
|
||||
|------|------------|
|
||||
| `Makefile.lx` | сборка с lx-тегами и версией `-lx` |
|
||||
| `.github/workflows/lx-ci.yml` | CI: матрица фич (baseline/xhttp/awg/full) + negative-check + кросс-платформа + android AAR |
|
||||
| `.github/workflows/lx-release.yml` | релиз на `v*-lx.*`: desktop ×6 + `libbox.aar` → GitHub Release |
|
||||
| `SPECS/` | Spec Kit (конституция, задачи, отчёты) |
|
||||
| `lx-test/config/` | примеры конфигов для `sing-box check` |
|
||||
| `transport/v2rayxhttp/` | XHTTP-клиент (новый пакет) |
|
||||
| `transport/wireguard/device_awg.go` | AWG IpcSet-параметры (за `with_awg`) |
|
||||
| `submodules/wireguard-go` | submodule: merged-форк AmneziaWG-рантайма ([Leadaxe/wireguard-go-awg2-lx](https://github.com/Leadaxe/wireguard-go-awg2-lx)) |
|
||||
| `option/v2ray_xhttp.go`, `option/wireguard_awg.go` | опции фич |
|
||||
| `include/v2rayxhttp.go` | регистрация транспорта за build-tag |
|
||||
|
||||
Поиск всех правок upstream-файлов: `grep -rn "// lx"`.
|
||||
|
||||
---
|
||||
|
||||
## Потребитель
|
||||
|
||||
Ядро собирается для десктоп-лаунчера **singbox-launcher** (бандлит `bin/sing-box`). На Android потребитель встраивает **`libbox.aar`** (gomobile) вместо бинаря — конфиг-JSON тот же. Маппинг `type=xhttp` и AWG-полей в визарде — задачи на стороне потребителя, не здесь.
|
||||
|
||||
---
|
||||
|
||||
## Ссылки
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Upstream | [SagerNet/sing-box](https://github.com/SagerNet/sing-box) · [документация](https://sing-box.sagernet.org/) |
|
||||
| Этот форк | [Leadaxe/sing-box-lx](https://github.com/Leadaxe/sing-box-lx) |
|
||||
| AmneziaWG-рантайм | [Leadaxe/wireguard-go-awg2-lx](https://github.com/Leadaxe/wireguard-go-awg2-lx) — sagernet-база + обфускация (3-way merge) |
|
||||
| AmneziaWG upstream | [amnezia-vpn/amneziawg-go](https://github.com/amnezia-vpn/amneziawg-go) · [docs.amnezia.org](https://docs.amnezia.org/documentation/amnezia-wg/) |
|
||||
| XHTTP (исток) | [XTLS/Xray-core](https://github.com/XTLS/Xray-core) — `transport/internet/splithttp` |
|
||||
| Конфиг фич | [docs-lx/lx-config.ru.md](docs-lx/lx-config.ru.md) |
|
||||
| Spec Kit | [SPECS/](SPECS/) — [README](SPECS/README.md) · [CONSTITUTION](SPECS/CONSTITUTION.md) · [IMPLEMENTATION_PROMPT](SPECS/IMPLEMENTATION_PROMPT.md) |
|
||||
|
||||
---
|
||||
|
||||
## Лицензия
|
||||
|
||||
Наследует лицензию upstream sing-box (**GPL-3.0**). Все правки помечены `// lx` и распространяются под той же лицензией. Это неофициальный форк, не аффилирован с SagerNet.
|
||||
> Файл оставлен как указатель, чтобы у репозитория был один основной русский
|
||||
> README (`README.md`), а не два конкурирующих.
|
||||
|
||||
Binary file not shown.
+1
-1
@@ -13,7 +13,7 @@
|
||||
# Feed format: opkg `src/gz` (.ipk + text Packages index, usign signature).
|
||||
# OpenWrt 24.10 (our SDK) still uses opkg; apk arrives at 25.12. The committed
|
||||
# trust anchor dist/shater-feed.pub is a usign (Ed25519) key, matching this.
|
||||
set -e
|
||||
set -euo pipefail
|
||||
OUT="${1:?feed dir required}"; cd "$OUT"
|
||||
: > Packages
|
||||
for ipk in *.ipk; do
|
||||
|
||||
@@ -133,6 +133,45 @@ fi
|
||||
echo "[apk-sdk] feeds install (prefer shater feed)"
|
||||
./scripts/feeds install -p shater shaterd shater-core byedpi luci-app-shater
|
||||
|
||||
# --- .config: build a MINIMAL one FROM SCRATCH (never append to the SDK's) ---
|
||||
# The ImmortalWrt SDK ships the buildbot's fully-expanded .config, and that file
|
||||
# carries CONFIG_ALL_KMODS=y + CONFIG_ALL_NONSHARED=y (verifiable without the
|
||||
# tarball: downloads.immortalwrt.org/releases/25.12.1/targets/*/config.buildinfo).
|
||||
# Appending our 4 packages to it left those in place, so `make defconfig`
|
||||
# re-selected EVERY kernel module of the target as =m; our kmod deps then pull in
|
||||
# `package/kernel/linux/compile`, which builds and `apk mkpkg`s the whole set —
|
||||
# 3593 kmod-* packages (mlx5, amdgpu, isdn, ...) in the v0.2.2 run. On a runner
|
||||
# under a 64 GB ZFS quota that is a guaranteed `Disk quota exceeded`.
|
||||
#
|
||||
# Fix (same shape as Slava-Shchipunov/awg-openwrt's "Setup SDK and feeds" step):
|
||||
# start the .config EMPTY so kconfig can only pull in what our 4 packages
|
||||
# actually select. Carried over from the SDK's .config, and nothing else:
|
||||
# CONFIG_TARGET_<board>[_<subtarget>]=y — the target choice. An SDK ships
|
||||
# exactly one target, so defconfig would almost certainly pick it anyway;
|
||||
# but guessing wrong means silently cross-compiling for the wrong arch, and
|
||||
# carrying two lines is far cheaper than that failure mode.
|
||||
# CONFIG_TARGET_{BOARD,SUBTARGET,ARCH_PACKAGES} — the string identities that
|
||||
# name the bin/ paths and the package arch; kept so a mismatch is impossible.
|
||||
# CONFIG_USE_APK — decides .apk vs .ipk output, i.e. the entire point of this
|
||||
# lane. 25.12 defaults it to y, but this lane must not depend on a default.
|
||||
# CONFIG_KERNEL_* (both `=y` and the `# ... is not set` form) — these feed
|
||||
# straight into the generated kernel .config. The SDK ships a PREBUILT kernel
|
||||
# tree, and our two nft kmods are packaged out of it; drop a knob the SDK's
|
||||
# kernel was configured with and the buildsystem sees a changed kernel
|
||||
# .config, reconfigures and REBUILDS the kernel — exactly the long, disk-hungry
|
||||
# detour we are trying to avoid. Carrying them verbatim keeps that tree valid.
|
||||
# Deliberately NOT carried: CONFIG_ALL*/CONFIG_PACKAGE_* (the bug itself);
|
||||
# CONFIG_TARGET_DEVICE_*/MULTI_PROFILE/ALL_PROFILES/PER_DEVICE_ROOTFS (image
|
||||
# knobs — we only ever run `package/<p>/compile`, never build an image); and
|
||||
# CONFIG_BUILDBOT/CONFIG_COLLECT_KERNEL_DEBUG/CONFIG_JSON_CYCLONEDX_SBOM, which
|
||||
# only add artifacts we neither ship nor have the disk for.
|
||||
grep -E '^CONFIG_TARGET_[a-z0-9_]+=y$|^CONFIG_TARGET_(BOARD|SUBTARGET|ARCH_PACKAGES)=|^CONFIG_USE_APK=|^CONFIG_KERNEL_|^# CONFIG_KERNEL_[A-Za-z0-9_]+ is not set$' \
|
||||
.config > .config.shater || true
|
||||
echo "[apk-sdk] .config rebuilt from scratch; $(wc -l < .config.shater) lines carried over"
|
||||
echo "[apk-sdk] (CONFIG_KERNEL_* kept verbatim and not echoed; identity lines:)"
|
||||
grep -v '^\(# \)\?CONFIG_KERNEL_' .config.shater | sed 's/^/[apk-sdk] /' || true
|
||||
mv -f .config.shater .config
|
||||
|
||||
for p in shaterd shater-core byedpi luci-app-shater; do
|
||||
echo "CONFIG_PACKAGE_$p=m" >> .config
|
||||
done
|
||||
@@ -151,11 +190,52 @@ fi
|
||||
echo "[apk-sdk] defconfig"
|
||||
make defconfig >/dev/null
|
||||
|
||||
# --- post-defconfig sanity + disk-cost readout -------------------------------
|
||||
# A failed run leaves a ~27 MB log; digging the cause out of it is miserable, so
|
||||
# print the handful of numbers that decide whether this run survives the
|
||||
# runner's disk quota BEFORE anything is compiled.
|
||||
kmods=$(grep -c '^CONFIG_PACKAGE_kmod.*=m' .config || true)
|
||||
echo "[apk-sdk] target: board=$(sed -n 's/^CONFIG_TARGET_BOARD=//p' .config)" \
|
||||
"subtarget=$(sed -n 's/^CONFIG_TARGET_SUBTARGET=//p' .config)" \
|
||||
"arch_packages=$(sed -n 's/^CONFIG_TARGET_ARCH_PACKAGES=//p' .config)"
|
||||
echo "[apk-sdk] kmod packages selected (=m): $kmods"
|
||||
echo "[apk-sdk] our packages after defconfig:"
|
||||
grep -E '^CONFIG_PACKAGE_(shaterd|shater-core|byedpi|luci-app-shater)=' .config \
|
||||
| sed 's/^/[apk-sdk] /' || true
|
||||
|
||||
# Each of our 4 must have SURVIVED defconfig. If kconfig dropped one, it is
|
||||
# because a symbol it `select`s (a DEPENDS entry) does not exist in the installed
|
||||
# feeds — with the old append-everything .config that was masked by the SDK
|
||||
# pre-selecting half the distro. `make package/<p>/compile` would then die with a
|
||||
# cryptic "No rule to make target", far from the real cause.
|
||||
for p in shaterd shater-core byedpi luci-app-shater; do
|
||||
grep -q "^CONFIG_PACKAGE_$p=m" .config || {
|
||||
echo "[apk-sdk] ERROR: $p is NOT selected after defconfig."
|
||||
echo " kconfig dropped it -> one of its DEPENDS is missing from the"
|
||||
echo " installed feeds (check the 'feeds install' step above)."; exit 10; }
|
||||
done
|
||||
|
||||
# Only our two nft kmods (+ whatever they themselves depend on) have any business
|
||||
# being selected here — a dozen at the very most. A count in the hundreds means an
|
||||
# ALL_KMODS-style mass-select crept back in, and the run would spend ~40 min
|
||||
# packing the kernel before dying on `Disk quota exceeded`. Fail now instead.
|
||||
[ "$kmods" -le 200 ] || {
|
||||
echo "[apk-sdk] ERROR: $kmods kmod packages selected — that is the whole kernel."
|
||||
echo " CONFIG_ALL_KMODS/CONFIG_ALL is back in .config; aborting before"
|
||||
echo " this fills the runner's disk. Offending lines:"
|
||||
grep '^CONFIG_ALL' .config || true; exit 11; }
|
||||
|
||||
for p in shaterd shater-core byedpi luci-app-shater; do
|
||||
echo "[apk-sdk] === build $p ==="
|
||||
make "package/$p/compile" V=s -j"$(nproc)"
|
||||
done
|
||||
|
||||
# What the build actually cost on disk. The runner's 64 GB ZFS quota is the
|
||||
# binding constraint on this lane, so record it while the tree still exists.
|
||||
echo "[apk-sdk] disk usage after compile:"
|
||||
du -sh build_dir staging_dir bin 2>/dev/null || true
|
||||
df -h /home/build || true
|
||||
|
||||
# A 25.12 apk-SDK must emit .apk — finding only .ipk means a wrong SDK was fed in.
|
||||
anyapk=$(find bin -type f -name '*.apk' | wc -l)
|
||||
[ "$anyapk" -gt 0 ] || {
|
||||
|
||||
@@ -2,6 +2,9 @@ package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
// lx:begin sec-oomgate
|
||||
"sync"
|
||||
// lx:end sec-oomgate
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
@@ -19,6 +22,10 @@ type ManagedService struct {
|
||||
handler ManagedHandler
|
||||
debug bool
|
||||
oomReporter oomkiller.OOMReporter
|
||||
// lx:begin sec-oomgate
|
||||
oomReportMu sync.Mutex
|
||||
oomReportLast time.Time
|
||||
// lx:end sec-oomgate
|
||||
}
|
||||
|
||||
type ManagedServiceOptions struct {
|
||||
@@ -90,6 +97,18 @@ func (s *ManagedService) TriggerOOMReport(ctx context.Context, _ *emptypb.Empty)
|
||||
if s.oomReporter == nil {
|
||||
return nil, status.Error(codes.Unavailable, "OOM reporter not available")
|
||||
}
|
||||
// lx:begin sec-oomgate
|
||||
// Rate-limit operator-triggered reports to at most one per minute: each write
|
||||
// dumps process state + the config snapshot (secrets) to disk, so an
|
||||
// authenticated client must not be able to spin it in a tight loop.
|
||||
s.oomReportMu.Lock()
|
||||
if !s.oomReportLast.IsZero() && time.Since(s.oomReportLast) < time.Minute {
|
||||
s.oomReportMu.Unlock()
|
||||
return nil, status.Error(codes.ResourceExhausted, "OOM report rate-limited (max 1/min)")
|
||||
}
|
||||
s.oomReportLast = time.Now()
|
||||
s.oomReportMu.Unlock()
|
||||
// lx:end sec-oomgate
|
||||
return &emptypb.Empty{}, s.oomReporter.WriteReport(memory.Total())
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -2,6 +2,9 @@ package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
// lx:begin sec-consttime
|
||||
"crypto/subtle"
|
||||
// lx:end sec-consttime
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
@@ -59,8 +62,11 @@ func authenticate(ctx context.Context, secret string) error {
|
||||
return status.Error(codes.Unauthenticated, "missing authorization")
|
||||
}
|
||||
token, isBearer := strings.CutPrefix(values[0], "Bearer ")
|
||||
if !isBearer || token != secret {
|
||||
// lx:begin sec-consttime
|
||||
// Constant-time compare: a plain != leaks the secret via response timing.
|
||||
if !isBearer || subtle.ConstantTimeCompare([]byte(token), []byte(secret)) != 1 {
|
||||
return status.Error(codes.Unauthenticated, "invalid authorization")
|
||||
}
|
||||
// lx:end sec-consttime
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -131,7 +131,9 @@ func (s *StartedService) StartTailscaleSSHSession(
|
||||
continue
|
||||
}
|
||||
go ssh.DiscardRequests(reqs)
|
||||
go s.forwardSSHAgentChannel(channel)
|
||||
// lx:begin sec-sshagent
|
||||
go s.forwardSSHAgentChannel(sessionCtx, channel)
|
||||
// lx:end sec-sshagent
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -313,7 +315,8 @@ func (s *StartedService) StartTailscaleSSHSession(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StartedService) forwardSSHAgentChannel(channel ssh.Channel) {
|
||||
// lx:begin sec-sshagent
|
||||
func (s *StartedService) forwardSSHAgentChannel(ctx context.Context, channel ssh.Channel) {
|
||||
defer channel.Close()
|
||||
fd, err := s.handler.ConnectSSHAgent()
|
||||
if err != nil {
|
||||
@@ -326,15 +329,33 @@ func (s *StartedService) forwardSSHAgentChannel(channel ssh.Channel) {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// The ssh-agent conn stays blocked in Read while idle, so io.Copy(channel,
|
||||
// conn) never returns on its own — without this it leaks a goroutine + the
|
||||
// agent fd for every closed session. Cancelling on either copy finishing (or
|
||||
// on the session ctx) closes both ends, unblocking the peer copy. Both Close
|
||||
// calls are idempotent with the deferred ones above.
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
conn.Close()
|
||||
channel.Close()
|
||||
}()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
io.Copy(conn, channel)
|
||||
cancel()
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
io.Copy(channel, conn)
|
||||
cancel()
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// lx:end sec-sshagent
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 419 KiB |
@@ -0,0 +1,18 @@
|
||||
# Документация shater
|
||||
|
||||
Документация продукта **shater** (управляемый интернет-шлюз для роутеров на
|
||||
OpenWrt). Лицо репозитория и быстрый старт — в корневом [`../README.md`](../README.md).
|
||||
|
||||
| Документ | О чём |
|
||||
|----------|-------|
|
||||
| [CONTEXT.md](CONTEXT.md) | **Начните здесь** — контекст проекта, история v0.1→v0.2, решения в кратце, testbed/инфра |
|
||||
| [INSTALL.md](INSTALL.md) | Сборка ship-артефакта (`shaterd`) и установка обоих фидов — opkg (24.10) и apk (25.12+) |
|
||||
| [ARCHITECTURE.md](ARCHITECTURE.md) | One-binary дизайн, auth-handoff LuCI→панель, data/DNS/apply-потоки (диаграммы) |
|
||||
| [FEATURES.md](FEATURES.md) | Полный список фич с тегами MVP/T1/T2 |
|
||||
| [ROADMAP.md](ROADMAP.md) | Фазовый план |
|
||||
| [DECISIONS.md](DECISIONS.md) | Почему sing-box, почему форк, split панели, лицензия и т.д. |
|
||||
| [DESIGN.md](DESIGN.md) | Визуальная система панели — направление «Faceplate», токены, компоненты |
|
||||
| [PORTING.md](PORTING.md) | Порт проверенных кусков из v0.1 |
|
||||
|
||||
Документация движка-форка (sing-box-lx) — в его слое: [`../docs-lx/`](../docs-lx/)
|
||||
и [`../SPECS/`](../SPECS/).
|
||||
@@ -2,6 +2,9 @@ package libbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
// lx:begin sec-consttime
|
||||
"crypto/subtle"
|
||||
// lx:end sec-consttime
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
@@ -97,9 +100,11 @@ func unaryAuthInterceptor(ctx context.Context, req any, info *grpc.UnaryServerIn
|
||||
if len(values) == 0 {
|
||||
return nil, status.Error(codes.Unauthenticated, "missing authentication secret")
|
||||
}
|
||||
if values[0] != sCommandServerSecret {
|
||||
// lx:begin sec-consttime
|
||||
if subtle.ConstantTimeCompare([]byte(values[0]), []byte(sCommandServerSecret)) != 1 {
|
||||
return nil, status.Error(codes.Unauthenticated, "invalid authentication secret")
|
||||
}
|
||||
// lx:end sec-consttime
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
@@ -115,9 +120,11 @@ func streamAuthInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServe
|
||||
if len(values) == 0 {
|
||||
return status.Error(codes.Unauthenticated, "missing authentication secret")
|
||||
}
|
||||
if values[0] != sCommandServerSecret {
|
||||
// lx:begin sec-consttime
|
||||
if subtle.ConstantTimeCompare([]byte(values[0]), []byte(sCommandServerSecret)) != 1 {
|
||||
return status.Error(codes.Unauthenticated, "invalid authentication secret")
|
||||
}
|
||||
// lx:end sec-consttime
|
||||
return handler(srv, ss)
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,11 @@ func (r *oomReporter) WriteReport(memoryUsage uint64) error {
|
||||
draftInfo = nil
|
||||
}
|
||||
reportsDir := filepath.Join(sWorkingPath, "oom_reports")
|
||||
err = os.MkdirAll(reportsDir, 0o777)
|
||||
// lx:begin sec-perms
|
||||
// OOM reports embed the config snapshot (server secrets, keys) and logs;
|
||||
// keep the tree owner-only (0700 dirs / 0600 files) instead of 0777/0666.
|
||||
err = os.MkdirAll(reportsDir, 0o700)
|
||||
// lx:end sec-perms
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -121,7 +125,9 @@ func discardDraftIfCurrent(draftPath string, draftInfo os.FileInfo) error {
|
||||
|
||||
func (r *oomReporter) writeSnapshot(destPath string, memoryUsage uint64) error {
|
||||
now := time.Now().UTC()
|
||||
err := os.MkdirAll(destPath, 0o777)
|
||||
// lx:begin sec-perms
|
||||
err := os.MkdirAll(destPath, 0o700)
|
||||
// lx:end sec-perms
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -44,7 +44,10 @@ func baseReportMetadata() reportMetadata {
|
||||
|
||||
func writeReportFile(destPath string, name string, content []byte) {
|
||||
filePath := filepath.Join(destPath, name)
|
||||
os.WriteFile(filePath, content, 0o666)
|
||||
// lx:begin sec-perms
|
||||
// Report files may carry the config snapshot (secrets) — owner-only.
|
||||
os.WriteFile(filePath, content, 0o600)
|
||||
// lx:end sec-perms
|
||||
chownReport(filePath)
|
||||
}
|
||||
|
||||
@@ -69,7 +72,9 @@ func copyConfigSnapshot(destPath string) {
|
||||
}
|
||||
|
||||
func initReportDir(path string) {
|
||||
os.MkdirAll(path, 0o777)
|
||||
// lx:begin sec-perms
|
||||
os.MkdirAll(path, 0o700)
|
||||
// lx:end sec-perms
|
||||
chownReport(path)
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 204 KiB |
@@ -25,7 +25,7 @@ LUCI_DEPENDS:=+shater-core +rpcd
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=0.2.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_MAINTAINER:=Shater <maqrota@icloud.com>
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
@@ -14,7 +14,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=shater-core
|
||||
PKG_VERSION:=0.2.0
|
||||
PKG_RELEASE:=2
|
||||
PKG_RELEASE:=3
|
||||
|
||||
PKG_MAINTAINER:=Shater <maqrota@icloud.com>
|
||||
PKG_LICENSE:=GPL-2.0-or-later
|
||||
|
||||
@@ -35,7 +35,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=shaterd
|
||||
PKG_VERSION:=0.2.0
|
||||
PKG_RELEASE:=2
|
||||
PKG_RELEASE:=3
|
||||
|
||||
PKG_MAINTAINER:=Shater <maqrota@icloud.com>
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 288 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 280 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 279 KiB |
@@ -159,7 +159,10 @@ func (r *Router) startIdleSuspend() error {
|
||||
if period < idleTickFloor {
|
||||
period = idleTickFloor
|
||||
}
|
||||
go r.idleSuspendLoop(period)
|
||||
// Pass the stop channel by value so the loop never reads the r.idleStop field
|
||||
// (stopIdleSuspend nils it right after close): a closed local channel stays
|
||||
// ready in select, so the loop exits even when Close races an in-flight tick.
|
||||
go r.idleSuspendLoop(period, r.idleStop)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -176,12 +179,12 @@ func (r *Router) stopIdleSuspend() {
|
||||
// asks each WG/AWG endpoint to suspend itself if it is unreachable AND idle past
|
||||
// the threshold. Per endpoint this is a single map lookup + an atomic idle
|
||||
// comparison; the endpoint owns the suspend/CAS/log decision.
|
||||
func (r *Router) idleSuspendLoop(period time.Duration) {
|
||||
func (r *Router) idleSuspendLoop(period time.Duration, stop <-chan struct{}) {
|
||||
ticker := time.NewTicker(period)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-r.idleStop:
|
||||
case <-stop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
r.suspendIdleEndpoints(r.reachableOutbounds())
|
||||
|
||||
@@ -163,10 +163,15 @@ func (t *adaptiveTimer) poll() {
|
||||
return
|
||||
}
|
||||
if t.timerConfig.policyMode == policyModeNetworkExtension {
|
||||
// lx:begin sec-oomcleanup
|
||||
// A trigger schedules a deferred FreeOSMemory for the next poll; run it
|
||||
// here and clear the flag (was inverted: it re-set true and so the free
|
||||
// after a trigger never happened).
|
||||
if t.cleanupTriggered {
|
||||
runtimeDebug.FreeOSMemory()
|
||||
t.cleanupTriggered = true
|
||||
t.cleanupTriggered = false
|
||||
}
|
||||
// lx:end sec-oomcleanup
|
||||
}
|
||||
if t.pendingPressureBaseline {
|
||||
t.pressureBaseline = sample
|
||||
@@ -202,7 +207,10 @@ func (t *adaptiveTimer) poll() {
|
||||
if !triggered {
|
||||
return
|
||||
}
|
||||
t.cleanupTriggered = false
|
||||
// lx:begin sec-oomcleanup
|
||||
// Schedule the deferred FreeOSMemory for the next network-extension poll.
|
||||
t.cleanupTriggered = true
|
||||
// lx:end sec-oomcleanup
|
||||
t.onTriggered(sample.usage)
|
||||
if rateTriggered {
|
||||
if t.killerDisabled {
|
||||
|
||||
Binary file not shown.
@@ -23,7 +23,7 @@ package v2rayxhttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"crypto/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -255,8 +255,12 @@ func (c *Client) newRequest(ctx context.Context, method, sessionID, seqStr strin
|
||||
// an opaque grouping key; the dashed format keeps it interchangeable with Xray.
|
||||
func newSessionID() string {
|
||||
var b [16]byte
|
||||
for i := range b {
|
||||
b[i] = byte(rand.Intn(256))
|
||||
// crypto/rand.Read fills b fully or returns an error; on every platform we
|
||||
// target it does not fail (it is the same entropy source behind Xray's
|
||||
// uuid.New()). A failure here means the OS CSPRNG is broken — there is no
|
||||
// safe fallback for a session id, so we intentionally do not swallow it.
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
const hexdigits = "0123456789abcdef"
|
||||
var h [32]byte
|
||||
|
||||
Reference in New Issue
Block a user