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. Приоритеты (в порядке убывания)

  1. Согласованность с upstream / минимальный дифф. Любое решение выбирается так, чтобы ребейз на следующий тег был максимально дешёвым.
  2. Корректность и совместимость с реальными серверами Xray-XHTTP и AmneziaWG 2.0.
  3. Ребейзопригодность изменений (изоляция, атомарность, маркеры).
  4. Сами фичи (функциональность 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 и Android libbox через нативный 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 (как у upstream include/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 через upstream make lib_android, с зашитыми with_xhttp/with_awgwith_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.lx proto-таргета нет), поэтому детерминированная регенерация — ОБЯЗАТЕЛЬНЫЙ новый deliverable первого SPEC этого класса: добавить в Makefile.lx proto-таргет с зафиксированными версиями 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. Референсы (только как образец, код не тянуть «как есть»)

  • AWG2hoaxisr/amnezia-box (submodule + patches/amneziawg-go, тег with_awg) — референс-образец; сегодня фактическая схема своя: форк-сабмодуль submodules/wireguard-go = Leadaxe/wireguard-go-awg2-lx (sagernet-база + обфускация).
  • XHTTPhiddify/hiddify-sing-box, пакет transport/v2rayxhttp.
  • Спецификация XHTTP — Xray-core (актуальная версия параметров mode/path/host/extra).
  • Clash API как функциональный эталон для расширений §3.6 — experimental/clashapi/ (proxies.go per-node delay, rules.go таблица правил): что именно пробрасываем в CommandClient. Код не тянуть — повторяем семантику через нативный канал.

6. Лицензия

Upstream — GPLv3. Портируемый код из сторонних проектов держать в отдельных файлах с сохранением исходных лицензионных заголовков и указанием происхождения.