CONSTITUTION
September 5, 2026 · View on GitHub
Неизменяемые принципы проекта. При конфликте с любым SPEC/PLAN — приоритет у этого документа.
1. Миссия
sing-box-lx — тонкий downstream SagerNet/sing-box. Это upstream + небольшой набор клиентских фич, которых в upstream нет и не будет. Сейчас их несколько: XHTTP (клиентский v2ray-транспорт, совместимый с Xray XHTTP); AmneziaWG 2.0/3.x (AWG2, AWG3) — клиентский endpoint поверх WireGuard, с обфускацией (Jc/Jmin/Jmax, S1–S4, H1–H4, I1–I5; в 3.x — защита заголовка, паддинг, хвосты, тайминги); и расширения libbox command-протокола (проброс существующих возможностей ядра в наш CommandClient взамен вырезанных upstream-каналов, §3.6).
Набор фич со временем может расти — другие протоколы, новые возможности, — но философия тонкого форка неизменна: каждая новая фича обязана целиком укладываться в §2–3, иначе она не принимается. Текущие транспортные фичи отклонены upstream (XHTTP — not planned, AmneziaWG — closed not-planned), поэтому форк постоянный, а «согласованность с upstream» достигается не вливанием в него, а дешёвым ребейзом на каждый новый тег.
Этот баланс держится на трёх явных принципах, перечисленных в порядке приоритета: (1) тонкий слой — мы держим максимально тонкий слой поверх upstream: минимум кода, минимум тронутых upstream-файлов, потому что каждая правка upstream-файла и каждая строка диффа — это стоимость на каждом ручном ребейзе; (2) идём за upstream — синхронизация только дешёвым ребейзом на каждый новый тег, и при любом выборе побеждает вариант с самым дешёвым ребейзом, даже ценой удобства реализации; (3) делаем нужные фичи — если фича нужна нам или нашим пользователям и её нельзя получить в той поверхности дистрибуции, которую мы реально поставляем (§3.5), мы её делаем, но целиком в рамках §2–3. Принцип (3) подчинён (1)–(2): «нужно» — необходимое, но не достаточное условие; достаточность даёт только прохождение теста §3.1. Фича, которая делает ребейз дороже, переосмысливается, а не принимается.
2. Приоритеты (в порядке убывания)
- Согласованность с upstream / минимальный дифф. Любое решение выбирается так, чтобы ребейз на следующий тег был максимально дешёвым.
- Корректность и совместимость с реальными серверами Xray-XHTTP и AmneziaWG 2.0.
- Ребейзопригодность изменений (изоляция, атомарность, маркеры).
- Сами фичи (функциональность XHTTP/AWG2 и прочих принятых).
Появление принципа «делаем нужные фичи» (§1) НЕ повышает приоритет п.4 (сами фичи) и НЕ понижает п.1 (минимальный дифф / дешёвый ребейз). Желание иметь фичу — необходимое, но не достаточное условие: оно открывает тест §3.1, а не отменяет его. При конфликте «фича удобнее так / ребейз дешевле иначе» всегда выбирается дешёвый ребейз. Для генерируемого кода ребейзопригодность (п.3) означает воспроизводимую регенерацию из помеченного источника, а не ручной мёрж артефакта (§3.6).
Если фича требует жертвовать пунктом 1 — она переосмысливается, а не пункт 1.
3. Жёсткие правила (запреты и инварианты)
3.1 Объём
- Только принятые фичи. Любой код вне фич, оформленных через Spec Kit (
SPECS/TASKS/NNN-…), — вне скоупа. Багфиксы upstream не патчим у себя — ждём апстрим-тег. Исключение из «ждём тег»: если функциональность недостижима в нашем канале не из-за временной недоделки, а потому что upstream сознательно не выносит её в наш канал (тега, который это исправит, не будет — их клиенту это не нужно), ожидание тега не применяется: фича проходит как новая по §3.1(а). Исключение покрывает ТОЛЬКО восстановление в нашем канале того, что upstream уже реализовал в другом, и ТОЛЬКО для Spec-Kit-фич; новую функциональность, которой нет в upstream ни в одном канале, и точечные upstream-багфиксы оно не легализует (их по-прежнему ждём тегом). - Критерии новой фичи (все обязательны): (а) тест оправданности — все три под-условия обязательны: (а1) нужна нам или нашим пользователям — есть конкретный запрос/задача LxBox, а не «было бы хорошо»; (а2) её нет в нашем целевом канале дистрибуции — в той поверхности, которую форк реально поставляет (§3.5: desktop-бинарь
sing-boxи Androidlibboxчерез нативныйCommandClient). Если функциональность ЕСТЬ в upstream, но только в канале, который форк осознанно не собирает (например per-node delay и таблица правил доступны в upstream лишь через Clash API REST, вырезанный вместе сwith_clash_api), — (а2) выполнено. «Нет в нашем канале» — это НЕ «нет в удобной нам форме»: косметическая или дублирующая переупаковка уже доступной у нас возможности (а2) НЕ проходит. Критерий самозакрывающийся: как только канал начинает собираться, дверь захлопывается. (а3) держать у себя дешевле, чем альтернатива — свой дифф дешевле по ребейзу, чем (i) ждать/протолкнуть upstream-тег, (ii) вернуть вырезанную подсистему целиком, (iii) решить на стороне потребителя (LxBox). Дешевизна фиксируется в SPEC КОНКРЕТНЫМ перечнем тронутых upstream-файлов и зоной касания ребейза (аудируемое число, а не прозой), эталон — точечный бэкпорт SPEC 013 вместо полной 1.14-миграции из-за дорогого ребейза подмодуля; (б) она изолируется по правилам §3.2–3.3 (для расширений command-протокола — §3.6) — новые файлы, свой build-tag, минимальные помеченные швы; (в) проходит полный цикл Spec Kit, и цикл начинается с фичи:FEATURES/NNN-NAME/FEATURE.mdпишется до задач и до кода и фиксирует полный скоуп (для продуктовых — по шаблону чёрного ящика, правило «что, а не как»; см.SPECS/README.md→ «Методология»); только затем скоуп режется на задачиSPECS/TASKS/NNN-*. Фича, требующая размазанных правок upstream-файлов, переосмысливается или отклоняется (см. §2). - Scope — client-only. Реализуем outbound/endpoint и клиентскую сторону транспорта. Server/inbound — отложены (отдельные будущие задачи), в текущих спеках не реализуются.
3.2 Изоляция изменений
- Меняется зона ответственности задачи — дорабатывается ОНА, новая не пишется. Если результат текущей работы (merge, рефактор, фикс, новая фича) затрагивает область, за которую уже отвечает существующая задача
SPECS/TASKS/NNN-*, изменение вносится в ЭТУ задачу — правится еёSPEC.mdпод новое актуальное состояние (иHISTORY.md, если сменилась архитектура), а не заводится параллельная спека на «то же самое под другим углом». Одна область ответственности = одна задача-владелец; это правило действует всегда. Признак нарушения: две спеки описывают инвариант/механизм одной подсистемы. Новая спека оправдана только для действительно новой зоны ответственности (см. критерии фичи в §3.1 (а)). Пример: upstream-рефактор добавил ещё один reserved-clear на egress-приёме — это расширяет зону SPEC 026 (AWG magic vs reserved-clear), поэтому реестр в егоSPEC.mdдополняется (5→6 мест), а не создаётся SPEC про «egress reserved-clear». - SPEC.md = актуальное состояние сверху, НЕ хронология. SPEC.md всегда описывает ТЕКУЩУЮ архитектуру фичи; порядок разделов — от актуального к деталям, не по ходу разработки. Дневниковые пометки («исправлено в rc.N», «прежнее утверждение неверно», отвергнутые подходы) в SPEC.md запрещены. При смене архитектуры фичи SPEC.md переписывается под новое состояние, а старое + обоснование смены выносятся в
HISTORY.mdтой же папки (см.SPECS/README.md→ «Структура SPEC.md»). - Go module path остаётся
github.com/sagernet/sing-box. Не переименовывать — это ломает все внутренние импорты и каждый ребейз. - Новый код — в новых файлах/пакетах. XHTTP-транспорт — пакет
transport/v2rayxhttp. AWG — в выделенных файлах рядом сprotocol/wireguard/transport/wireguard. - Каждая фича — за build-tag:
with_xhttp,with_awg. Без тега сборка обязана быть байт-в-байт эквивалентна upstream по поведению (фича отсутствует). - Шаблон гейтинга —
include/*_stub.go(как у upstreaminclude/wireguard.go+wireguard_stub.go): реальная регистрация под тегом, заглушка с понятной ошибкой без тега. - Расширения libbox command-протокола изолируются не как новый-файл+тег целиком, а по специальному режиму §3.6 (handler'ы за build-tag по образцу
daemon/started_service_usbip{,_stub}.go; шов в.protoпод маркером;.pb.go— регенерируемый артефакт). Это единственное послабление формы изоляции, и оно компенсируется более строгим режимом, см. §3.6.
3.3 Правки upstream-файлов
- Допускаются только там, где иначе нельзя (диспетчеры, struct опций, списки констант,
go.mod, а также шов вdaemon/*.protoдля расширений по §3.6). Регенерируемые артефакты (*.pb.go,*_grpc.pb.go) под этот режим НЕ подпадают: они — машинный вывод protoc, маркеров не несут и руками не правятся; их форма целиком определяется помеченным.proto, на ребейзе они перегенерируются, а не мёржатся текстом. - Каждая такая правка обёрнута маркером:
// lx:begin xhttp ... // lx:end xhttp - Правки upstream-файлов выносятся в отдельные атомарные коммиты (см. IMPLEMENTATION_PROMPT). Один коммит = одна логическая правка одной зоны.
3.4 Синхронизация
- Ручной merge
upstream/testing, не rebase. Веткаlx— рабочая и релизная, никогда не форс-пушится; дрейф проверяется по merge-base (upstream/testingсам форс-пушится, счётчикиrev-listврут). Полный ритуал — docs-lx/lx-release-runbook.md. origin=Leadaxe/sing-box-lx,upstream=SagerNet/sing-box. Теги тянем изupstream.- Форк-сабмодули — часть дельты.
submodules/wireguard-go(Leadaxe/wireguard-go-awg2-lx) иsubmodules/sing-tun(Leadaxe/sing-tun-lx) подключеныreplace-директивами вgo.mod; встречный upstream-бамп этих зависимостей на мерже не принимается вслепую — он молча откатил бы наши патчи (обфускация AWG, self-heal acceptLoop).
3.5 Дистрибуция
- Desktop — бинарь
sing-box(drop-in для лаунчераsingbox-launcher, который ищетLookPath("sing-box")→bin/sing-box). - Android —
libbox.aar(+libbox-legacy.aar, SDK21): gomobile-сборкаexperimental/libboxчерез upstreammake lib_android, с зашитымиwith_xhttp/with_awg(иwith_lx_commandдля расширений §3.6) (cmd/internal/build_libbox,// lx:-блок; tailscale/clash_api выкинуты). Для встраивания в Android-приложение-потребитель.Libbox.version()→1.14.0-lx.N. - Идентичность сборки — в версии:
sing-box version/Libbox.version()→1.14.0-lx.N, где источник версии — lx-тегvX.Y.Z-lx.N(см. задачу BUILD_CI_RELEASE).
3.6 Расширения libbox command-протокола (мост LxBox ↔ ядро)
Отдельный, более узкий режим изоляции — только для класса «новый RPC в нативном протоколе управления ядром» (daemon/*.proto + experimental/libbox/), где упаковка «новый файл + свой build-tag» для самого RPC невозможна: RPC объявляется внутри единого upstream-service StartedService {}, а *.pb.go/*_grpc.pb.go регенерируются protoc и стирают любые // lx: маркеры. Допускается только под §3.1(а) и при выполнении ВСЕХ условий:
- (1) Потолок — только мост. Режим разрешён ИСКЛЮЧИТЕЛЬНО для проброса наружу уже существующей в ядре возможности через CommandClient (delay-тест outbound/endpoint, история URLTest, чтение таблицы правил). Новые подсистемы, серверные/inbound-фичи, бизнес-логика в
daemon/— не сюда. Если RPC тянет за собой новый пакет логики — это уже не «расширение протокола». - (2) Handler'ы и клиентские методы — в новых
*_lx.go. Реализация (func (s *StartedService) …и клиентские методы) выносится в новые файлы (daemon/started_service_*_lx.go,experimental/libbox/command_client_*_lx.go). В upstream-файлах (started_service.go,command_client.go) — ноль строк сверх диспетчерского шва, и тот за маркером, отдельным атомарным коммитом. - (3) Build-tag
with_lx_commandОБЯЗАТЕЛЕН — по доказанному в репозитории образцуdaemon/started_service_usbip{,_stub}.go: реальные handler'ы за//go:build with_lx_command, файл-близнец*_stub.goза//go:build !with_lx_commandвозвращаетcodes.Unimplemented. Без тега сборка по поведению эквивалентна upstream (RPC просто не обслуживается). Тег гейтит написанные руками handler-методы, а не генерируемые типы, поэтому регенерация.pb.goгейтингу не мешает; «тег невозможен» как исключение НЕ допускается. - (4) Шов в
.proto— за маркером. Новыеrpc-строки внутриservice StartedService {}и новыеmessage-типы обёрнуты// lx:begin lx_command … // lx:end lx_command. Шов виден глазом и переносится при ребейзе вручную (несколько строк), даже если upstream меняет соседние RPC. - (5)
.pb.go/*_grpc.pb.go— регенерируемый артефакт. Не правятся руками, маркеров не несут, на ребейзе пере-генерируются из смерженного.proto, а не мёржатся. Сегодня генерация НЕ зафиксирована (make protoшеллит системныйprotocи ставит плагины@latest, вMakefile.lxproto-таргета нет), поэтому детерминированная регенерация — ОБЯЗАТЕЛЬНЫЙ новый deliverable первого SPEC этого класса: добавить вMakefile.lxproto-таргет с зафиксированными версиямиprotoc/плагинов. Это требование, а не существующая гарантия. - (6) Точка прошивки тега в AAR. Новый тег добавляется в
sharedTagsвcmd/internal/build_libbox/main.go(файл уже несёт// lx:-блок) — иначе RPC не попадёт вlibbox.aar. Этот файл считается третьим тронутым upstream-ish файлом класса. - (7) CI-инвариант. CI обязан доказывать обе сборки: без
with_lx_command(поведенчески эквивалентна upstream,*_stub.goотдаёт Unimplemented) и с тегом (RPC обслуживается). Usbip-паттерн делает эту проверку дешёвой. - (8) Ребейз-цена — в SPEC. SPEC перечисляет точный список тронутых общих файлов (
.proto+ факт регенерации.pb.go+build_libbox/main.go) и оценивает стоимость ручного переноса. Выход за «несколько строк в.proto+ регенерация + строка тега» — фича переосмысливается (§2).
Инвариант §3.6: это ЕДИНСТВЕННОЕ послабление формы изоляции во всей конституции, ограниченное мостом daemon/*.proto+experimental/libbox/ и пробросом уже существующей возможности ядра. Любая попытка применить §3.6 за этими пределами или для новой подсистемы — нарушение §3.1, отклоняется. Объём кода под §3.6 держится минимальным наравне с приоритетом №1, и §3.6 не создаёт прецедента для будущих послаблений.
4. Архитектурные ориентиры (факты upstream-линии 1.14)
- v2ray-транспорты диспатчатся
switchпоoptions.Typeвtransport/v2ray/transport.go(NewClientTransport/NewServerTransport). Константы —constant/v2ray.go. Опции —option/v2ray_transport.go(_V2RayTransportOptions). VLESS/VMess/Trojan ходят через общий транспорт — пер-протокольных правок не требуется. - WireGuard — это endpoint:
protocol/wireguard/endpoint.go, регистрацияendpoint.Register[option.WireGuardEndpointOptions](registry, C.TypeWireGuard, NewEndpoint), проводка вinclude/wireguard.go(+wireguard_stub.go). Девайс — черезtransport/wireguard; зависимостьgithub.com/sagernet/wireguard-goвgo.modзаменена (replace) на форк-сабмодульsubmodules/wireguard-go. - libbox command-протокол — gRPC-сервис
StartedServiceвdaemon/started_service.proto(+ регенерируемые*.pb.go/*_grpc.pb.go), клиентexperimental/libbox/command_client.go. Опциональные RPC гейтятся build-tag'ом по парному паттернуdaemon/started_service_usbip{,_stub}.go(реальные handler'ы /codes.Unimplemented-заглушка). Это образец для §3.6.
5. Референсы (только как образец, код не тянуть «как есть»)
- AWG2 —
hoaxisr/amnezia-box(submodule +patches/amneziawg-go, тегwith_awg) — референс-образец; сегодня фактическая схема своя: форк-сабмодульsubmodules/wireguard-go= Leadaxe/wireguard-go-awg2-lx (sagernet-база + обфускация). - XHTTP —
hiddify/hiddify-sing-box, пакетtransport/v2rayxhttp. - Спецификация XHTTP — Xray-core (актуальная версия параметров
mode/path/host/extra). - Clash API как функциональный эталон для расширений §3.6 —
experimental/clashapi/(proxies.goper-node delay,rules.goтаблица правил): что именно пробрасываем в CommandClient. Код не тянуть — повторяем семантику через нативный канал.
6. Лицензия
Upstream — GPLv3. Портируемый код из сторонних проектов держать в отдельных файлах с сохранением исходных лицензионных заголовков и указанием происхождения.