ZOYKUHot Board

podkop-net/podkop-engineДокументация

Several patches for compatibility between sing-box and xray-core

Содержание (32 разделов)▾
Чем отличается от sing-box из репозитория OpenWrtВетки и форматыУстройствоЛокальная сборка одной архитектурыПроверкиВесь r13 под нагрузкой: утечки и гонкиpodkop-engine и podkop-engine-full (1.13, 1.14)Старт, когда списки не скачиваютсяПроверка загрузки перед переключением urltest (1.13, 1.14)Резервные выходы urltest (1.13, 1.14, с r10)Транспорт xhttp (1.13, 1.14, с r11)Шифрование VLESS (1.13, 1.14, с r14)Ссылка в outbound: sing-box tools decode-link (1.13, 1.14; все протоколы — с r13)Виды ссылокЧто отклоняется и о чём предупреждаетсяОпции sing-box без общепринятого параметра: sb.<путь>Ссылки панелейСравнение с разбором ссылок в podkopСтрока Features: (с r11)Поддельный ClientHello: tls.spoof (1.13, 1.14, с r13)Автосмена отпечатка uTLS (1.13, 1.14, с r13; с r17 — свой порядок и запоминание)Что пробуется (с r17)Как замечается безответный ClientHello (с r13)Как включаетсяЗамеры (r17)DNS-запрос новой UDP-сессии (1.13, 1.14, с r13)Резервный DNS (1.13, 1.14)Группа DNS-серверов и DNS провайдера (1.14, с r21)IPv6 для имён без IPv4: dns.ipv6_if_no_ipv4 (1.13, 1.14, с r17)FakeIP (1.13, 1.14)Память (1.13, 1.14)Failsafe
Языковое уведомление:Перевод на данный язык отсутствует. Отображается официальный английский оригинал.
Быстрая установка
git clone https://github.com/podkop-net/podkop-engine.git

podkop-engine

Неофициальная сборка sing-box для podkop на OpenWrt. Проект не связан с sing-box и его авторами; пакет называется иначе, потому что лицензия sing-box запрещает производным работам использовать его имя. Бинарь по-прежнему устанавливается как /usr/bin/sing-box, пакет объявляет PROVIDES: sing-box, поэтому зависимость podkop от sing-box выполняется.

Распространяется на условиях GPL-3.0-or-later (см. LICENSE; исходная лицензия sing-box — LICENSE.sing-box). Упаковка основана на net/sing-box из openwrt/packages.

Чем отличается от sing-box из репозитория OpenWrt

REALITY работает с Xray-core 26.7+ и 26.9+ (как Xray-клиент 26.9.9): ClientHello несёт X25519MLKEM768, клиент заявляет версию 26.3.27
отпечатки uTLS firefox = Firefox 148, safari = Safari 26.3 (как в Xray), random выбирает только из chrome/firefox/safari; randomized согласован: X25519MLKEM768 всегда есть и в группах, и в ключах, ALPN всегда (иначе при части запусков — CurvePreferences includes unsupported curve, отказ REALITY у Xray 26.9.8+, gRPC без h2)
состав (1.13, 1.14) два пакета, ставится один. podkop-engine — только то, что использует podkop: входы tproxy, direct, mixed; выходы direct, socks, shadowsocks, trojan, vless, hysteria2, tuic, selector, urltest (1.14 на x86_64 в r9 — 24,3 МБ вместо 29,2); с r15 ещё http — для локального HTTP-прокси в «Outbound Config», сам podkop его не пишет. podkop-engine-full — плюс все остальные выходы для «Outbound Config», в том числе NaiveProxy; см. ниже. Теги: with_quic (Hysteria2, TUIC), with_utls (REALITY, fp=), with_clash_api (дашборд LuCI, YACD)
1.14: без sing-box api нет CLI sing-box api и API-сервиса (~6 МБ)
DoH (1.13, 1.14) таймаут одного медленного запроса больше не рвёт соединение вместе с запросами, на которые уже пришли ответы: на стенде 800 из 800 обычных запросов, одно соединение на весь прогон (раньше на пути podkop 1218 из 1600 ответов уходили на резервный DNS)
память (1.13, 1.14) лимит памяти Go следует за памятью, которую система ещё может дать: под нагрузкой сборщик мусора работает раньше OOM-killer (контейнер 128 МиБ, 72 МиБ заняты другим процессом: без лимита OOM-kill в каждом прогоне, с лимитом — ни одного). Простаивающее соединение больше не держит буфер 32 КиБ: VLESS без flow, Trojan, Hysteria2, TUIC, gRPC, Vision с TLS 1.3 внутри — 5–30 КиБ на соединение вместо 37–63; см. ниже
журнал DNS (1.13, 1.14) ошибка обмена с DNS-сервером пишется один раз на сервер, дальше — одна сводка раз в 10 с (раньше 200–400 строк в минуту)
urltest: сервер молчит (1.13, 1.14) соединение, на которое нет ответа дольше обычного для этого сервера (RTO по RFC 6298 + 1 с), запускает внеочередную проверку: молчащий Hysteria2 или VLESS с мёртвым выходом — переключение через ~17 с вместо ~41 с и до 3 минут; таймауты QUIC тоже считаются отказом
Vision (1.13, 1.14) после перехода Vision в прямой режим (TLS 1.3 внутри) данные копируются через splice, без буфера в простое; для этого sing-vmess берётся из форка FiyeroT/sing-vmess
uTLS (1.13, 1.14) состояние рукопожатия uTLS/REALITY освобождается после рукопожатия (−13 КиБ на соединение)
старт без списков (1.13, 1.14) если remote rule-set не скачался при старте (WAN ещё не поднялся, неверное время, первый узел urltest мёртв), sing-box стартует с пустым списком и докачивает его (5, 10, 20 с, дальше раз в 30 с), а не падает; после докачки один раз запускает podkop list_update
urltest (1.13) перепроверяет узлы при смене сетевого интерфейса (как 1.14), а не через 3 минуты
urltest: резервные выходы (1.13, 1.14, с r10) список fallbacks в группе: выходы, на которые группа уходит, только когда ни один из outbounds не отвечает, — первый живой по порядку списка; как только отвечает основной, группа возвращается на него. Без поля всё как раньше; см. ниже
urltest: ссылка проверки загрузки (1.13, 1.14, с r10) download_url в группе: своя ссылка для проверки 64 КиБ, "" — проверка в группе выключена, поля нет — ссылка из /etc/config/sing-box, как раньше; см. ниже
транспорт xhttp (1.13, 1.14, с r11) клиент транспорта XHTTP из Xray-core для VLESS и Trojan: режимы packet-up, stream-up, stream-one и auto, HTTP/1.1, HTTP/2 и HTTP/3, TLS и REALITY, отдельный сервер для скачивания, XMUX. Имена опций — как в sing-box-extended. На стенде против Xray 26.9.9 ведёт себя не хуже его собственного клиента; см. ниже
ссылка в outbound (1.13, 1.14, с r11) sing-box tools decode-link 'vless://…' печатает outbound для ссылки, включая параметр extra транспорта xhttp; с r13 — для всех протоколов сборки, см. ниже
список возможностей (с r11) sing-box version заканчивается строкой Features: — по ней podkop решает, какие опции писать в конфиг и показывать в LuCI; см. ниже
сниффер (1.13, 1.14, с r10) один специально собранный QUIC-пакет из LAN больше не роняет sing-box (разбор ClientHello читал за концом расширения); паника любого сниффера стоит одного соединения, а не процесса, и пишется в лог со стеком. UDP-сниф читает не больше 16 пакетов: поток QUIC Initial, который никогда не складывается в ClientHello, держал до 66 пакетов по 16 КиБ на сессию
исправления upstream (1.13, 1.14, с r10) из ветки stable sing-box после 1.14.2: клиенты WebSocket и HTTPUpgrade теряли байты, пришедшие вместе с ответом 101, клиент WebSocket после первого соединения терял заголовок Sec-WebSocket-Protocol; повторный UDP-сниф отдавал уже прочитанные пакеты второй раз; переключение urltest закрывало соединения под общей блокировкой группы. Проверки входных данных sing-vmess (Vision, XUDP, mux) — в форке. На 1.14 с r15 всё это уже есть в самой 1.14.3 и в версиях библиотек, которые она берёт
Clash API (1.13, 1.14, с r10) строки лога для /logs формируются, только пока его кто-то читает: с включённым дашбордом строка ниже уровня лога стоит ~45 нс и 1 выделение памяти вместо ~1200 нс и 16 (1.13; 1.14 — ~500 нс и 6). Занятый адрес external_controller больше не роняет старт: ошибка в лог, повтор в фоне. Проверка задержек группы с вложенной urltest (дашборд podkop, таймаут 10 с) не ждёт проверок загрузки внутри неё (1.14)
списки (1.13, 1.14, с r10) недоступный remote rule-set пишет ошибку один раз и потом раз в 10 минут со счётчиком (раньше — каждые 30 с, 2880 строк в сутки), и сборка мусора запускается только после реальной загрузки списка
init-скрипт (с r10) лимит дескрипторов 65536 (4096 по умолчанию кончались примерно на 2000 соединений, дальше падали и DNS-запросы), procd ждёт остановки 15 с вместо 5 (sing-box закрывает соединения до 10 с), в системном логе нет escape-кодов цветов
xhttp: сброс сети (1.13, 1.14, с r12) в r11 выход с транспортом xhttp переставал работать до перезапуска sing-box после смены интерфейса по умолчанию (переподключение WAN), первого появления WAN после загрузки или кнопки «закрыть все соединения» в панели; теперь сброс закрывает соединения, и следующее открывается заново
резервный DNS (1.13, 1.14, с r12) отказ сервера считается один раз на событие и только для имён, которые DNS провайдера разрешил: одно «битое» имя или один сброс соединения DoH больше не уводят всю сеть на DNS провайдера; после сброса сети состояние серверов забывается; ubus после промаха перечитывается не чаще раза в 30 с
FakeIP (1.13, 1.14, с r12) пока файл кэша не записывается (кончилось место, раздел только для чтения), новые имена получают адреса из памяти, а не остаются без ответа; соединение на устаревший адрес FakeIP пишется предупреждением с причиной
REALITY и отпечатки (1.13, 1.14, с r12) android и 360 с REALITY — ошибка конфигурации (эти отпечатки не умеют TLS 1.3); для edge, ios, qq — предупреждение при старте, а ошибка проверки называет отпечаток как вероятную причину (серверы на Xray-core 26.9.8+ их не принимают)
проверки при старте (1.13, 1.14, с r12) неверный uplink_http_method и download без server_port у xhttp отклоняются sing-box check; негодная ссылка проверки загрузки urltest из окружения — одно предупреждение при старте; в логе виден код завершения хука восстановления списков
HTTP/2 (1.13, с r12) golang.org/x/net поднят до версии линии 1.14: в прежней один кадр HTTP/2 неизвестного клиенту типа (ALTSVC, ORIGIN) от сервера DoH или прокси ронял весь процесс
failsafe и init-скрипт (с r12) служба failsafe работает при любом порядке установки и после обновления с r1–r3, выключается опцией failsafe; подряд считаются падения sing-box с промежутком меньше 10 минут (было — меньше часа)
ссылки всех протоколов (1.13, 1.14, с r13) sing-box tools decode-link читает ссылки vless, trojan, ss, socks, hysteria2/hy2, tuic, http/https (в podkop-engine — с r15), а в podkop-engine-full ещё vmess, hysteria, anytls, naive+https/naive+quic. Перед печатью outbound строится тем же кодом, что sing-box check: напечатанное этот sing-box примет. Опции sing-box, у которых нет общепринятого параметра ссылки, задаются как sb.<путь опции>; см. ниже и справку по всем ключам ссылок — docs/LINKS.md
tls.spoof (1.13, 1.14, с r13) опция TLS spoof из sing-box 1.14 перенесена в 1.13 и на обеих линиях работает с REALITY: перед настоящим ClientHello уходит поддельный с именем по выбору, который сервер отбрасывает. Поддельный пакет уходит тем же маршрутом, что соединение; см. ниже
автосмена отпечатка uTLS (1.13, 1.14, с r13; с r17 — свой порядок) до отпечатка из конфига пробуются тот же отпечаток без ключа X25519MLKEM768 (как его шлёт sing-box без патчей под REALITY), затем firefox с ключом, затем отпечаток как есть; первый, на который сервер ответил, используется и запоминается для сервера в cache.db на сутки. Безответный ClientHello замечается за секунду-другую; сервер REALITY 26.9.8+, отвергший ClientHello без ключа, сразу переводит на следующий. Когда дело не в отпечатке (не отвечает ни один; тот же на следующем соединении ответ получил), автосмена на время отходит в сторону. По умолчанию выключена; включается разделом experimental.utls_fallback в config.json (его пишет podkop, см. ниже)
самопроверка (с r13) failsafe раз в 20 с спрашивает у sing-box имя через 127.0.0.42; если процесс работает, но не отвечает 49–72 с, он завершается и procd запускает его заново. Молчание считается за процессом, который уже ответил адресом FakeIP, и за тем, что запущен после такого завершения; не считается, пока молчит сам роутер (переполнена таблица соединений, нет ответа на собственный ping, опрос опоздал). На стенде DNS возвращался через 56–75 с после kill -STOP (10 из 10 с итоговым скриптом), лишних завершений за 30 минут простоя, перезапуски, обновления списков и отключение WAN — ни одного; см. «Failsafe»
списки (1.13, 1.14, с r13) локальный rule-set, который podkop переписывает при обновлении списков, больше не читается одновременно с заменой: на стенде с детектором гонок Go (файл заменяется каждые 150 мс под нагрузкой) r12 даёт десятки сообщений о гонке за минуту, r13 — ни одного. Remote rule-set отклоняется до чтения, если сервер объявил длину больше предела (50 МБ) или список формата .srs начинается не как .srs, а ошибка называет размер, Content-Type и первые байты ответа: файл 60 МБ с объявленной длиной под видом списка стоил r12 136–139 МБ памяти, r13 — столько же, сколько без загрузки (21–25 МБ). Ответ без объявленной длины (chunked или сжатый сервером) читается, как и в r12, до предела и отклоняется после. Загрузка списка через группу urltest не обрывается, когда группа переключается (в r12 — ошибка use of closed network connection и повтор): она остаётся на участнике, через которого началась, и заканчивается сама или через 30 с без данных. Текст сервера в строке ошибки загрузки — одной строкой, без управляющих символов, не длиннее 512 байт
urltest (1.13, 1.14, с r13) при пропадании маршрута по умолчанию результаты проверок участников не сбрасываются: отказ соединения с ошибкой самого роутера (network is unreachable, когда интерфейс выключен; no such device, когда он исчез, как при обрыве PPPoE или выдернутом модеме) оценивается через 3 с и только если сеть за это время не объявлена пропавшей; отказ того, что началось до последнего пропадания или возврата сети, не оценивается вовсе. На стенде (podkop с группой из двух серверов, ifdown wan на 30 с) r12 терял результаты обоих участников и переключал группу, r13 — нет; после возврата сети проверка проходит через секунду. Сброс сети (соединения закрыты, выходам сообщён новый интерфейс) считается таким же событием, даже когда монитор ничего не говорил — на WAN появился адрес, соединения закрыты из панели: Hysteria2 и TUIC отвечают на него ожидающим соединениям ошибкой network changed, и группа больше не уходит из-за неё на другого участника. Где монитор сети не видит маршрута по умолчанию вообще (маршруты в другой таблице), его слову перестают верить, пока хоть у одного участника есть результат проверки новее его слова, и периодические проверки группы идут (без этого участник, потерявший результат, не проверялся бы больше никогда). Принудительная проверка, пришедшая во время идущей, выполняется сразу после неё; отказ соединения даёт одну строку ошибки с тегом выхода вместо двух
память на соединение (1.13, 1.14, с r13) отпечатки chrome, edge, ios, qq, 360, android после рукопожатия освобождают расширения ClientHello. На стенде с 5000 соединений и отпечатком chrome живая куча на соединение — 23,8 КБ вместо 26,7 через REALITY и 14,0 вместо 16,8 через TLS (−2,9 КБ, обе линии); для остальных отпечатков в тесте −0,4…1,0 КБ. Запись Clash API о соединении не держит состояние сниффера
VLESS с flow (1.13, 1.14, с r13) flow поверх ws, grpc, http, quic и httpupgrade — предупреждение при старте (каждое соединение такого выхода падает, раньше причина не называлась); decode-link такие ссылки отклоняет. С r14 это относится к выходу без шифрования VLESS: с опцией encryption режим Vision работает и поверх транспорта
VMess с TLS (1.13, 1.14, с r13; podkop-engine-full) шифрование auto остаётся auto (AES-128-GCM или ChaCha20-Poly1305) и при TLS. sing-box подставлял там zero, а Xray-core убрал zero и none и закрывает такое соединение: на стенде с Xray 26.9.9 из 17 ссылок на входы VMess с TLS не работала ни одна, теперь работают все. decode-link читает шифр из ссылки, как клиент Xray: всё, кроме двух AEAD-шифров, — auto
фрагментация ClientHello (1.13, 1.14, с r13) tls.fragment: кусок ClientHello, который сеть не подтвердила, больше не подвешивает соединение, пока его не оборвёт ядро (около четверти часа): ожидание подтверждения заканчивается, как только соединение закрыто, в том числе по таймауту рукопожатия. На линии 1.13 tls.fragment без tls.record_fragment не делал ничего — теперь фрагментирует, как на 1.14. Имя сервера с пустой меткой (начинается с точки: .example.org) больше не завершает sing-box на первом же соединении: место разреза выбиралось как случайное число от нуля до длины метки, а для нуля это паника; то же для опции правила маршрута tls_fragment, которой имя присылает клиент из LAN
REALITY: длинный short_id (1.13, 1.14, с r13) short_id длиннее 16 шестнадцатеричных цифр — ошибка конфигурации, у выхода и у входа; раньше sing-box на нём падал (index out of range) и при старте, и в sing-box check
Clash API: список соединений (1.13, 1.14, с r13) закрытие соединения больше не пишет время закрытия в запись, которую в этот момент читает запрос /connections (его шлёт панель, показывающая соединения). Эту гонку нашёл детектор гонок Go в десятиминутном прогоне всего релиза под нагрузкой: 3 и 4 сообщения на двух линиях; в sing-box 1.13.21 и 1.14.2 код тот же. После правки такой же прогон сборок с детектором на обеих линиях сообщений не дал
DNS: запрос новой UDP-сессии (1.13, 1.14, с r13) при hijack-dns без действия sniff перед ним запрос, с которого начинается UDP-сессия входа, и запрос, пришедший с того же порта сразу за ним, иногда оставались без ответа и без строки в журнале: так сессию передаёт обработчику библиотека udpnat. Теперь маршрут сам читает первый пакет сессии до установки обработчика и ещё 20 мс дочитывает её очередь. Под нагрузкой sing-box 1.13.21 без патчей оставил без ответа 41 запрос из 42 306, r13 — ни одного из 56 тысяч на каждой линии. Конфигурации podkop это не касалось: в ней перед hijack-dns стоит sniff; см. ниже
шифрование VLESS (1.13, 1.14, с r14) опция encryption выхода vless — шифрование VLESS (VLESS Encryption) из Xray-core, сторона клиента: сервер, у которого оно задано, клиента без него не принимает. Значение — строка клиента Xray (mlkem768x25519plus.native.0rtt.<ключ>); режим Vision работает поверх него с любым транспортом; decode-link переносит параметр encryption ссылки (в r13 такая ссылка отклонялась); см. ниже
DoH: сервер закрыл соединение (1.13, 1.14, с r14) запрос, который шёл по соединению HTTP/2, когда сервер закрыл его без кадра GOAWAY (так поступает Quad9), задаётся ещё раз на новом соединении. Раньше он падал с unexpected EOF, и ответ приходил только на повтор самого клиента. На стенде с Quad9, по 112 запросов в прогоне: у r13 в журнале 3 и 1 строка ошибок, у r14 отказов нет, повторно задано 12 и 8 запросов
резервный DNS: журнал (1.13, 1.14, с r14) все сообщения резерва пишутся уровнем WARN: при уровне журнала podkop по умолчанию видно, включён ли резерв для сервера, какие серверы провайдера взяты и когда сервер вернулся. В r13 уровнем WARN писались три сообщения из восьми (об отказах), остальные пять — INFO: на стенде с отказавшим и вернувшимся сервером при уровне warn в журнале r13 была одна строка, у r14 — четыре
сниффер DNS (1.13, 1.14, с r14) датаграмма UDP, которая разбирается как сообщение DNS, но не является запросом (первая запись рукопожатия DTLS, запрос клиента NTP), больше не останавливает перебор снифферов: правила protocol: dtls и protocol: ntp срабатывают, строки sniffed packet protocol: с пустым значением в журнале нет
проверки входных данных в библиотеках (1.13, 1.14, с r14) исправления upstream, которых нет в версиях библиотек, закреплённых sing-box. Shadowsocks 2022: первый пакет UDP от сервера с идентификатором сессии 0 ронял процесс (sing-shadowsocks2 a69d108; на 1.14 с r15 эту версию берёт сама 1.14.3). TUIC: фрагменты пакета UDP общим размером больше 65535 байт заканчивались паникой. TUIC, Hysteria2 и Hysteria: размер датаграммы от сервера не больше заголовка фрагмента заканчивался паникой или бесконечным циклом (два коммита upstream в форке FiyeroT/sing-quic; на 1.14 с r15 — в версии upstream, которую берёт 1.14.3). Все случаи вызывает только свой прокси-сервер. 1.13: Vision на этапе дополнения режет запись от 8171 байта на части не длиннее кадра, как на 1.14 (форк sing-vmess v0.2.8-pdk13.3)
init-скрипт под apk (с r14) на OpenWrt 25.12 apk не перезаписывает /etc/init.d/sing-box, если файл правили или он не принадлежит ни одному пакету (движок, поставленный мимо пакетного менеджера), а скрипт пакета кладёт рядом как .apk-new. С чужим скриптом sing-box работает без того, что задаёт только скрипт пакета: без резервного DNS, проверки загрузкой urltest и лимита дескрипторов 65536, а перезапуск при падении и остановка — как их настроил чужой скрипт. Теперь пакет при установке ставит свой скрипт на место, прежний сохраняет в /etc/sing-box/sing-box.init.before-podkop-engine; см. docs/INSTALL.md
memory_limit и urltest_download_url (с r14) init-скрипт передаёт sing-box только значения, которые безопасно передавать: лимит памяти — off или размер в MiB либо GiB (число без единицы Go читает как байты), адрес проверки загрузки — http:// или https:// без пробелов (адрес с пробелом обрезался по пробелу); остальное пропускается со строкой в журнале
основа 1.14 (с r15) sing-box 1.14.3 вместо 1.14.2. Среди его изменений — закрытие простаивающих соединений выходов и DNS-серверов, на которые ничего не ссылается (например, невыбранных узлов urltest); транспорт xhttp в это не входит, его соединения закрываются по-прежнему (с r18 — входит, см. ниже). Форк sing-quic на 1.14 больше не нужен: оба его исправления вошли в версию, которую берёт 1.14.3
списки доменов и подсетей (1.13, 1.14, с r15) наборы правил из JSON и списки подсетей строятся за один проход в компактные структуры; результат тот же, что у sing-box, а то, что так не читается, идёт прежним путём. Эмулятор aarch64 с 256 МБ, sing-box без podkop (замер на лабораторных сборках с 1.14.2): JSON на 100 000 разнообразных доменов — r14 падал по нехватке памяти на 135 МБ, теперь 2 с и пик 24 МБ; на 1 000 000 — 8 с, пик 75 МБ. JSON на 1 000 000 подсетей — r14 падал на 129 МБ, теперь 3 с и пик 48 МБ; .srs на 1 000 000 подсетей — 4 с и пик 34 МБ вместо 11 с и 90 МБ
формат списка text (1.14, с r15) rule-set с "format": "text": список доменов и адресов простым текстом, как его публикуют многие сайты, запись на строку, локальный или удалённый. Домен совпадает сам и с поддоменами (domain_suffix), адрес или подсеть — по назначению (ip_cidr); # и // начинают комментарий. Строки, которые не домен и не адрес, пропускаются; список без единой записи (страница блокировки вместо списка) — ошибка
выход http в podkop-engine (1.13, 1.14, с r15) тип http есть и в обычном пакете, а не только в podkop-engine-full: локальный HTTP-прокси, заданный в «Outbound Config» (например, opera-proxy на 127.0.0.1:18080), больше не отвергается. Сжатый пакет больше на 4–11 КБ, бинарь — на 0–64 КБ (замер на пяти архитектурах)
xhttp: конец соединения (1.13, 1.14, с r15) в режиме stream-up и stream-one запись после того, как сервер закончил загрузку (Xray делает так в конце соединения, через uplinkOnly после закрытия назначения), больше не пишется в журнал как ERROR connection upload closed: io: read/write on closed pipe: это обычный конец соединения. В режиме stream-up обрыв ответа на загрузку при живом скачивании завершает сессию с причиной, а не оставляет скачивание ждать
мелкие исправления (1.13, 1.14, с r15) сниффер: при ошибке сопоставления правил удержанные пакеты возвращаются в пул; набор правил, закрытый перезагрузкой конфигурации в том же процессе, не разбирает и не сохраняет список, скачанный перед самым закрытием (разбор большого списка рядом с загрузками нового экземпляра — пик памяти); DoH: после перевода системных часов назад зависшее соединение снова сбрасывается по таймауту; REALITY: в ClientHello уходит ALPN отпечатка, а alpn из конфигурации действует только у отпечатка randomized; так было и раньше, теперь это сказано в документации sing-box (docs/configuration/shared/tls.md в дереве с патчами), а пустой цикл, который будто бы подставлял alpn, убран
обновление дашборда (1.13, 1.14, с r16) дашборд Clash API (external_ui: при старте в пустой каталог и по кнопке обновления, POST /upgrade/ui) распаковывается в отдельный каталог внутри каталога дашборда и заменяет прежние файлы, только когда распакован целиком. Раньше архив распаковывался поверх, а при ошибке каталог очищался: обновление на роутере, где для 4,6 МБ Yacd-meta не хватило места на flash, оставляло дашборд пустым до следующего скачивания. Теперь при нехватке места, битом или пустом архиве прежний дашборд остаётся как был. Каталог дашборда может быть точкой монтирования или ссылкой: файлы переносятся, а не копируются. Обновлению нужно место для новой копии рядом с прежней: замена 4,6 МБ на 4,6 МБ требует около 4,6 МБ свободных (раньше распаковка шла поверх). Файлы, которых нет в архиве, после обновления удаляются (как у mihomo). Архив с путём за пределы каталога отклоняется
перезагрузка во время чтения списков (1.13, 1.14, с r16) набор правил, закрытый перезагрузкой конфигурации в том же процессе (SIGHUP или Go-бэкенд podkop), прекращает читать скачанный список, а не дочитывает его рядом с новым экземпляром, который скачивает и читает свой. После перезагрузки роутера все списки скачиваются заново, и пять перезагрузок за пять секунд складывали в памяти полные разборы больших списков. Замер: два списка (500 000 имён JSON, миллион подсетей текстом), процессор ограничен половиной ядра, пять SIGHUP во время чтения: пик памяти 119–121 МБ в r15, 78 МБ в r16 (столько же, сколько один запуск)
ответ «не изменилось» (1.13, 1.14, с r16) когда сервер списка отвечает 304, в cache.db записывается только время проверки (отдельный раздел rule_set_checked), а не весь список заново. Раньше каждый интервал обновления переписывал каждый список целиком: при кэше на flash это многие мегабайты записи в день. Замер на тех же двух списках: одно обновление с ответами 304 записывало 64 МБ, теперь 141 КБ. Версия без этого раздела при запуске удаляет его и берёт время из самого списка, как раньше
резервный DNS: все аплинки (1.13, 1.14, с r16) когда sing-box не привязывает сокеты к устройству (Go-бэкенд podkop: без auto_detect_interface и «Output Network Interface»), DNS провайдера берутся со всех аплинков — интерфейсов OpenWrt с маршрутом по умолчанию, кроме туннелей, — и каждый сервер спрашивается через свой аплинк. Раньше брались только DNS интерфейса с наименьшей метрикой маршрута по умолчанию: когда mwan3 уводил трафик на резервный модем, резерв спрашивал DNS проводного провайдера, а тот через модем недоступен. Список DNS провайдера теперь свой у каждого экземпляра sing-box: после перезагрузки конфигурации в том же процессе (SIGHUP, Go-бэкенд podkop) новый экземпляр не берёт список и выходной интерфейс старого. Замер с mwan3 (политика failover; DNS каждого провайдера отвечает только через свой аплинк; основной DNS-сервер не отвечает): проводной WAN сломан за шлюзом, интерфейс поднят — до r16 0 ответов из 8, в r16 8 из 8. См. «Резервный DNS» ниже, там и об mwan3
IPv6 для имён без IPv4 (1.13, 1.14, с r17) со стратегией ipv4_only и опцией dns.ipv6_if_no_ipv4 запрос AAAA уходит, когда у имени нет адреса IPv4 (ответ на A без записей A), — сайт только с IPv6 открывается; имя с адресом IPv4 и имена FakeIP по-прежнему без AAAA. Go-бэкенд podkop включает опцию. См. ниже
основа 1.14 (с r18) sing-box 1.14.4 вместо 1.14.3. Среди его изменений: группа отдаёт цепочку выходов, через которую идёт соединение (проверка петель переведена на неё); повтор тега среди узлов одной группы (selector, urltest) — ошибка конфигурации; действие правила direct убрано; Clash API на 0.0.0.0 слушает только IPv4. Форк sing-vmess — v0.2.9-pdk.2: исправление upstream закрытия соединения после прямой записи Vision и наше чтение в прямом режиме
urltest на 1.14.4 (с r18) sing-box 1.14.4 набирает соединение сразу на узле, который выбрала группа, мимо самой группы. В podkop-engine соединение по-прежнему идёт через urltest: иначе не работает то, что выше описано для urltest, — узел, который отказал или молчит, проверяется вне очереди и сразу оставляется, берётся резервный, а узел, чья проверка ещё идёт, не используется. Без этого мёртвый узел получал новые соединения до следующего раунда проверок (по умолчанию до 3 минут)
списки в файлах (1.13, 1.14, с r18) содержимое удалённого rule-set лежит в своём файле рядом с cache.db (<путь cache.db>.rule-sets/<тег>-<хэш>), а не внутри: bbolt не отдаёт освобождённые страницы, и cache.db был в 3–4 раза больше самих списков (замер: 34 МБ списков — cache.db 106 МБ после первого обновления и 141 МБ после пяти; теперь 32 КБ и файлы на 34 МБ). Списки прошлой версии при старте переносятся в файлы, а разросшийся cache.db переписывается заново. Списки от 1 МиБ обновляются по одному, а не все разом. Откат на прошлую версию скачивает списки заново; каталог файлов остаётся (в RAM, если там cache.db) до следующего запуска r18, который убирает лишнее
xhttp: простаивающие соединения (1.14, с r18) соединения пула xhttp закрываются, когда sing-box закрывает простаивающие соединения выхода, на который ничего не ссылается (невыбранный узел urltest), а не через 5 минут простоя пула. Соединения сессий не трогаются
сниффер: WireGuard (1.13, 1.14, с r18) первый пакет туннеля WireGuard (148 байт) больше не принимается за пакет uTP: раньше каждый такой туннель через sing-box назывался BitTorrent (3 из 3 на обеих линиях)
uTLS у QUIC (1.13, 1.14, с r18) у hysteria, hysteria2 и tuic отпечаток uTLS отбрасывается с предупреждением: ClientHello там делает quic-go, и раньше с заданным отпечатком падало каждое соединение (unsupported usage for uTLS). Так же поступают разборщик ссылок и Xray
замена выходов на ходу (1.14, с r19) для программ, которые встраивают sing-box (Go-бэкенд podkop): Box.UpdateOutbounds строит и запускает новые выходы и ставит их в работающий sing-box под те же или новые теги, а ненужные теги убирает. Соединение, начатое после замены, идёт через новые объекты, начатое до неё — через прежние, а прежние закрываются, когда через них не идёт ни одно соединение из списка Clash API: не раньше чем через 2 минуты и не позже чем через сутки. Соединение через заменённый сервер с общим транспортом (gRPC, HTTP/2, XHTTP, QUIC, multiplex), открытое дольше суток, при этом обрывается. Без Clash API прежние держатся сутки, а если прежних серверов больше, чем разрешил вызывающий, замена отвергается. Заменённая группа urltest перестаёт проверять узлы; новая берёт её выбор, если у неё тот же узел, и проверяет только узлы без результата, по 3 одновременно. Выход, который остаётся, не может идти через заменяемый — такая замена отвергается, как и любая неудачная: тогда всё остаётся как было. Из конфигурации и командной строки не вызывается. Стенд podkop-exp на собранном бинаре, долгие потоки по 1 МБ/с: ни одно соединение не оборвалось при переименовании сервера с переходом в другую группу (vision, 6 из 6), удалении последнего сервера группы при каждом правиле пустой группы (XHTTP, 4 из 4) и смене отпечатка сервера gRPC (4 из 4); та же смена отпечатка на прежней версии оборвала 6 из 6. Смена списка каждые 30 секунд в течение часа — без перезапуска и обрывов, память выходит на плато. Остальные изменения по-прежнему перезапускают sing-box и обрывают соединения vision, gRPC и XHTTP
detour по тегу (1.14, с r19) выход, через который набирает другой (detour: клиент HTTP списков, DNS-сервер, загрузка XHTTP), находится по тегу при каждом наборе, а не запоминается при первом: замена на ходу его не оставляет на прежнем объекте. Если тег убран, остаётся выход, найденный при старте, а он закрывается: такой тег вызывающий убирать не должен
выход block в podkop-engine (1.14, с r19) тип block есть и в обычном пакете: в Go-бэкенде podkop секция типа group, чья группа не выбрала ни одного сервера и по своей настройке отказывает, отказывает через селектор этого выхода. Селектор заменяется на ходу, правило маршрута — нет. Ошибка соединения, которое отклонил выход block, пишется на уровне debug, как у действия reject; строка самого выхода blocked connection to … остаётся на уровне info (при уровне warn её не видно)
urltest: только резервные, block в резерве (1.14, с r20) группа может состоять из одних fallbacks, без outbounds: так остаётся правило, у которого провайдер убрал все серверы. Резервный выход типа block не проверяется и «отвечает» отказом: группа берёт его, когда до него в списке не отвечает ничего, и его отказы не считаются сбоем участника. Пока группа не закончила первый раунд проверок (старт, замена на ходу), резервные не берутся вовсе: выбран первый выход группы, как без fallbacks (у группы из одних резервных — первый резервный, а не block дальше по списку), — иначе резервный block отказывал бы во всех соединениях до конца раунда, а резервный direct пускал бы их мимо прокси. Пока выбран резервный block, раунды идут чаще интервала (через 30 с, затем каждый раз вдвое дольше, не дольше интервала): block не проверяется и через него не набирается ни один сервер, поэтому после сбоя сети серверы иначе вернулись бы только к следующему плановому раунду; см. ниже
urltest: не больше 4 проверок сразу (1.14, с r20) URL-проверки всех групп процесса идут не больше чем по 4 одновременно (раньше — до 10 на группу, у каждой группы в своё время): пачки TLS-приветствий — то, на чём DPI операторов замораживает соединения, а слабый процессор роутера под ними отстаёт. Время ожидания очереди в таймаут проверки не входит. Проверка, которая подтверждает отказ сервера по ошибкам его соединений, очереди не ждёт: она одна на сервер и не чаще, чем позволяет её пауза. Стенд podkop-exp, подписка из 120 серверов на двух правилах: не больше 6 одновременных соединений проверок вместо 20, все 120 проверены за 40 с
детектор блокировок (1.14, с r21) для программ, которые встраивают sing-box (Go-бэкенд podkop): по каждому серверу REALITY движок записывает в файл кеша, остался ли без ответа каждый ClientHello, ответил ли сервер, не доведя рукопожатие, и не отверг ли он первого кандидата автосмены отпечатка (без X25519MLKEM768) как чужого, пропустив следующего, — так отвечает сервер Xray-core с v26.9.8. Соединения считают байты в обе стороны и узнают, закончил ли поток удалённый конец, — так обрыв по пути отличается от закрытия. История URL-проверок хранит время последней неудачной проверки и итог последней проверки загрузки. Записи на соединения не влияют. Одна перемена в поведении: неудача, которую оборвал конец раунда (срок проверки задержки Clash API) или вызывающий, сбоем сервера больше не считается
websocket, httpupgrade: зависший upgrade (1.14, с r21) набор через транспорт ws или httpupgrade кончается вместе со своим контекстом: раньше сервер, который принял соединение и не ответил на HTTP upgrade, держал набор (и URL-проверку), пока не умрёт само соединение. Соединение, уже прошедшее upgrade, с контекстом не обрывается
Clash API: резервные urltest (1.14, с r21) у группы urltest в /proxies есть поле fallbacks — теги резервных выходов, которыми кончается all: проверка задержки группы их не проверяет, и без этого поля клиент не мог отличить неответивший сервер от непроверенного резервного
DNS: группа серверов и DNS провайдера (1.14, с r21) новые типы DNS-сервера: group спрашивает свои серверы по порядку списка или сначала самый быстрый по замеру и отставляет отказавший, provider спрашивает DNS провайдера из netifd; см. ниже
decode-link: JSON в extra (1.14, с r21) проверка «вторая ссылка в одной строке» отвергала любое значение с пробелом и ://, а JSON в extra ссылок XHTTP держит их в своих строках (User-Agent браузера рядом с Referer или адресом): подписка из таких ссылок оставалась без серверов. Теперь верный JSON проходит, ссылка после JSON по-прежнему отвергается. На 1.13 (r18) проверка прежняя
urltest: несколько URL, медиана, повтор (1.14, с r24) опция urls — список адресов проверки, по одному на раунд по очереди; номер раунда считается по часам, так что все группы процесса в одном раунде проверяют одним адресом. Задержка участника — медиана последних раундов (их столько, сколько адресов): адрес, который не ответил, не оставляет замера, случайная проверка не сдвигает выбор. Участник, не ответивший адресу раунда, проверяется ещё раз адресом, который в прошлый раз через него отвечал: недоступен он, когда не ответили два адреса, а не один (сервер, у хостера которого закрыты два оператора из списка, больше не выпадает на раунд). Проверки всех групп идут одним проверяющим процесса: проверка, которую уже ждёт другая группа, не повторяется, а проверки через один и тот же первый сервер цепочки начинаются с разницей в 2 с — двадцать участников за одним detour не шлют ему пачку TLS-приветствий
urltest: режимы loss и balance (1.14, с r24) опция mode: fastest (по умолчанию, как раньше); loss — задержки сравниваются с поправкой на потери участников (цена страницы, измеренная на стенде: 59 + 4,85·RTT + p·(155 + 1,2·RTT) мс), участник в работе и три лучших других раз в loss_interval (по умолчанию 30 мин) скачивают 256 КБ с loss_url, а участник, на которого группа переключилась бы без свежего счёта, — сначала 1 МБ в фоне; потери, которые видны у всех посчитанных участников, — потери линии и ничего не двигают; balance — соединения распределяются по адресу источника между не более чем balance_size (по умолчанию 3, до 16) лучшими участниками (в пределах tolerance, потери не выше loss_threshold, по умолчанию 1 %); устройство остаётся на своём участнике, уходит сразу, когда тот упал, и заново распределяется после balance_idle (по умолчанию 5 мин) без соединений. Потери считает зонд на своём TCP-соединении с приёмным буфером 16 КБ (common/probesock): заголовки его пакетов читаются из пакетного сокета с фильтром по его портам, дыра в последовательности, заполненная позже четверти наименьшего RTT, — потеря, раньше — перестановка; после зонда окно соединения возвращается к системному максимуму. Соединение не своё (мультиплексированный транспорт, QUIC) не считается, участник идёт по задержке. Clash API показывает окна потерь (1–24 ч), режим и участников баланса
REALITY: fragment и record_fragment (1.14, с r24) у выхода с REALITY опции fragment и record_fragment работают, как у uTLS: ClientHello делится на несколько TCP-сегментов или TLS-записей по имени сервера. Раньше клиент REALITY их принимал и молча не применял. Сервер собирает тот же ClientHello из записей (RFC 8446 §5.1 разрешает делить сообщения рукопожатия на записи), и проверка REALITY в нём проходит: Xray-core REALITY и sing-box читают ClientHello через слой записей. Go-бэкенд podkop предлагает record_fragment как обход «заморозки» по отпечатку на сервере с таким вердиктом: на пути с заморозкой 0 из 15 подключений без него и 15 из 15 с ним
DNS: группа после таймаута (1.14, с r25) сервер типа group спрашивает свои серверы по порядку; когда один не ответил за свой срок, а впереди больше одного, остальные спрашиваются сразу все, и берётся первый ответ. Раньше каждый следующий ждал своего срока, и четыре мёртвых DoH съедали 12 с при сроке запроса 10 с. Стенд (четыре DoH и bootstrap отброшены на роутере, свежий старт): первый запрос — ответ за 6,0 с, в r24 — без ответа
списки: резервные клиенты (1.14, с r25) у HTTP-клиента опция fallback — теги других общих клиентов: remote rule-set, который не скачался через свой клиент (ошибка, чужой ответ, 30 с без данных, не список), повторяет загрузку через каждый резервный по очереди, у каждого свой срок без данных. Go-бэкенд podkop качает списки напрямую, а при неудаче — через серверы первых трёх правил. Стенд (прямой путь к серверу списка отброшен): r25 — список через сервер правила через 5 с, r24 — не пришёл за 121 с
без MPTCP на слушающих сокетах (1.14, с r25) значения GODEBUG, с которыми upstream собирает свои выпуски (multipathtcp=0, tlssha1=1), вшиты в бинарь (-X runtime.godebugDefault): Go 1.24+ открывает слушающие сокеты как MPTCP, которых sing-box в выпусках upstream не открывает. Стенд: 4 сокета MPTCP в пространстве имён роутера при работе r24, 0 у r25
серия по слоям (1.14, с r25) патчи собраны заново: один на функцию, по слоям (00xx–04xx), дерево форка то же байт в байт. Группа urltest форка — в своём пакете protocol/group/urltestgroup, хранилище FakeIP — TableStore в своих файлах, командная строка снова там, где у upstream: файлы upstream protocol/group/urltest.go, dns/transport/fakeip/store.go, memory.go и весь cmd/sing-box (кроме двух строк) — как у upstream. Перенос на 1.15.0-alpha.9: 36 патчей из 57 касаются файлов, которые upstream менял (было 110 из 195), конфликтов при наложении по одному — 12
DNS по UDP (1.13, 1.14) каждый запрос уходит со своего сокета со случайным портом, как у dnsmasq: публичные резолверы, режущие поток с одного сокета (Яндекс — около 20 запросов в секунду), больше не теряют ответы под нагрузкой. Так же для detour в direct (резолвер VPN-секции podkop); через прокси — одна общая сессия, как раньше: новая UDP-сессия VLESS/Trojan/SOCKS — это новое TCP+TLS-соединение к серверу
DNS по TLS 1.13: соединения пула, простоявшие больше 8 с, закрываются раньше, чем их молча закроет сервер (Cloudflare и Quad9 — через 10 с): после паузы запросы не падают с EOF. 1.14 (там все запросы идут по одному соединению): не больше 16 запросов в полёте на соединение, остальные — отдельными соединениями, иначе Яндекс сбрасывает соединение
резервный DNS (1.13, 1.14) если DNS-сервер, к которому sing-box ходит напрямую через WAN, не отвечает или отвечает SERVFAIL, ответ берётся у DNS провайдера; см. ниже
память DNS (1.14) запросы, которые dnsmasq присылает на 127.0.0.42, снова обслуживаются без отдельных горутин на каждый запрос (как в 1.13): при 200 запросах в секунду 22 горутины вместо 3000 и 50 МБ памяти вместо 90; сам обмен идёт вне цикла чтения входа, так что медленный запрос не задерживает остальные
FakeIP (1.13, 1.14) таблица «адрес — имя» целиком в памяти, cache.db — её копия, которая дописывается в фоне: ни DNS-запрос, ни новое соединение не ждут файла. Адрес, которым не пользовались сутки, освобождается; новое имя не отнимает адрес у имени, которым пользуются. После падения, OOM или SIGKILL выданные адреса не достаются другим доменам. Если записи для адреса нет, соединение идёт по имени, которое называет сам клиент (TLS, HTTP, QUIC); см. ниже
пачки DNS-ответов (1.13, 1.14) очередь исходящих пакетов UDP-входа — 1024 пакета вместо 64. Раньше, когда готово сразу 70 и больше ответов (dnsmasq держит в работе до 150 запросов), часть ответов выбрасывалась, и клиент ждал их по таймауту — секунды: в тесте пачки по 100 запросов теряли 0,2 % ответов, по 200 — 0,7–0,9 %
failsafe если sing-box падает раз за разом и procd перестаёт его перезапускать, podkop останавливается: dnsmasq возвращается к обычным серверам, интернет и DNS работают без прокси, причина — в логе; при каждом падении в лог пишется и память роутера; см. ниже
зависимости без kmod-tun и kmod-inet-diag
версия sing-box version → 1.14.4-pdk-r25 (версия upstream и ревизия пакета; в r1 было просто -pdk)

Чего нет по сравнению с полным sing-box (ни в одном пакете): серверных входов (кроме tproxy, direct и mixed, которые слушает podkop), TUN, WireGuard/Tailscale внутри sing-box (WireGuard/AmneziaWG в podkop подключаются интерфейсом OpenWrt — это работает), DNS-серверов DoQ/DoH3 и DHCP, служб, ACME и т.п. В podkop-engine, кроме того, нет выходов vmess, snell, tor, ssh, shadowtls, anytls, hysteria и naive — они в podkop-engine-full (http с r15 есть в обоих). Конфиг с отсутствующим типом не проходит sing-box check с понятной ошибкой: vmess outbound is not included in this build (podkop_slim), install podkop-engine-full or rebuild with -tags podkop_full.

Совместимость с серверами. Патченный клиент ведёт себя как Xray-клиент 26.9.9. Единственный случай, где обычный sing-box работает, а этот — нет: Xray-сервер ≤ 25.4.30 и сайт-прикрытие, выбирающий X25519MLKEM768 (большинство крупных зарубежных сайтов), — без автосмены отпечатка uTLS. Лечится обновлением Xray на сервере; с автосменой (с r17) первым идёт ClientHello без X25519MLKEM768, который такой сервер принимает.

Ветки и форматы

Собираются последние теги веток 1.12, 1.13, 1.14 (versions.env) под все архитектуры, для которых sing-box есть в официальном репозитории OpenWrt:

  • .ipk (opkg, OpenWrt 24.10 и старше) — SDK 24.10;
  • .apk (apk, OpenWrt 25.12 и snapshot) — SDK 25.12.

Релизы — по одному на ветку (тег v<версия sing-box>-r<ревизия>, например v1.14.2-r1), файлы названы как у sing-box: podkop-engine_1.14.2-r1_openwrt_<pkgarch>.ipk / .apk, плюс SHA256SUMS. С r9 (1.13, 1.14) рядом лежит podkop-engine-full_<версия>-r<ревизия>_openwrt_<pkgarch>.ipk / .apk.

Go во всех сборках один — закреплённый lang/golang из openwrt/packages (Go 1.26.x).

Устройство

На 1.14 с r25 серия разложена по слоям, один патч на функцию; номер файла говорит слой: 00xx сборка (include, go.mod), 01xx перенесённые исправления upstream, 02xx точки подключения в файлах upstream (несколько строк на функцию), 03xx свои модули (только новые файлы), 04xx изменения поведения в файлах upstream. В каждом патче — строки Pdk-Feature:, Pdk-Zone:, Pdk-Layer: и список коммитов прежней серии, из которых он собран (ветка archive/r25-1.14-commits в podkop-net/sing-box). Список ниже — прежняя нумерация 1.12 и 1.13 (и 1.14 до r24).

patches/v1.12, v1.13, v1.14   патчи к тегу upstream (git format-patch)
  0001  reality: X25519MLKEM768 и версия клиента 26.3.27
  0002  utls: firefox -> Firefox 148, safari -> Safari 26.3
  0003  (1.14) тег podkop_slim: без snell и API-сервиса
  0003 (1.13) / 0004 (1.14)  rule-set: старт с пустым списком, докачка, хук
                             SING_BOX_RULESET_RECOVERED_HOOK
  0004  (1.13) urltest: перепроверка при смене интерфейса
  0005  (1.13) rule-set: закрыть соединение неудачной загрузки (иначе при
        повторах 404/5xx утекают сокеты и память; в 1.14 так уже в upstream)
  0006  (1.13) / 0005 (1.14)  dns/udp: свой сокет на каждый запрос (серверы не за прокси)
  0007  (1.13) dns/tls: закрывать простаивающие соединения пула до сервера
  0006  (1.14) dns/transport: не больше 16 запросов в полёте на соединение TCP/TLS
  0008  (1.13) / 0007 (1.14)  dns: резервный DNS провайдера (dns/client_fallback.go)
  0008  (1.14) route: DNS из сессий NAT (вход direct) — обработчиком, без горутин
  0009  (1.13, 1.14) dns: DNS провайдера из netifd для выходного устройства, выбор самого быстрого
  0010  (1.13, 1.14) utls: randomized — X25519MLKEM768 в группах и ключах вместе, ALPN всегда
  0011  (1.13, 1.14) dns/fakeip: таблица в памяти, запись в cache.db в фоне, неиспользуемые
        адреса освобождаются
  0012  (1.14) route: DNS из сессий NAT — обмен вне цикла чтения входа
  0012 (1.13) / 0013 (1.14)  route: адрес FakeIP без записи — по имени из TLS, HTTP, QUIC
  0013 (1.13) / 0014 (1.14)  listener: очередь исходящих пакетов UDP-входа — 1024 вместо 64
  0014 (1.13) / 0015 (1.14)  sniff: границы CRYPTO-кадров QUIC (паника, зацикливание, 64 КиБ)
  0015 (1.13) / 0016 (1.14)  route: отказ в дозвоне direct обратно во вход, откуда пришло
                             (DNS-петля 127.0.0.42:53)
  0016 (1.13) / 0017 (1.14)  transport: неудачный дозвон HTTP/2 и gRPC-lite не вешает запись
  0017 (1.13) / 0018 (1.14)  sniff: TLS — без обмена ключами после разбора ClientHello
  0018 (1.13) / 0019 (1.14)  dns: повтор UDP-запроса через 1 и 3 с, пока нет ответа
  0019 (1.13) / 0020 (1.14)  rule-set: обрыв загрузки без данных 30 с, старт без списка за 30 с
  0020 (1.13) / 0021 (1.14)  urltest: проверка каждый интервал, а не через раз
  0021 (1.13) / 0022 (1.14)  urltest: уход с члена по ошибке дозвона/первой записи (warn в логе)
  0022  (1.13) tls: таймаут рукопожатия TLS-дозвона 15 с (как в 1.14)
  0023  (1.13, 1.14) urltest: проба загрузки 64 КиБ перед переключением
        (SING_BOX_URLTEST_DOWNLOAD_URL)
  0024  (1.13, 1.14) include: без службы resolved (−15 % размера)
  0025  dns/https: сброс DoH по таймауту, только если позже начатые запросы не получили ответа
  0026  tls: состояние рукопожатия uTLS/REALITY освобождается после него
  0027  dns: ошибки обмена — строка на сервер и сводка раз в 10 с
  0028  urltest: таймаут, который заодно сообщает «закрыто» (quic-go), — отказ члена
  0029  badtls: дочитать alert после данных, как crypto/tls
  0030  tls: соединения TLS-дозвона ждут данных без буфера (badtls)
  0031  readwait: ожидание данных без буфера у потоков QUIC (Hysteria2, TUIC) и gRPC-lite
  0032  box: лимит памяти Go следует за памятью, которую система ещё может дать
  0033  urltest: внеочередная проверка члена, если соединение не получает ответа
  0034  go.mod: sing-vmess из форка (Vision в прямом режиме — splice)
  0035  include: тег podkop_slim — протоколы podkop, podkop_full — все выходы
        (0025–0035 — у обеих веток под одними номерами)
openwrt/podkop-engine/        Makefile пакета (+ init-скрипт и UCI-конфиг из net/sing-box);
                              init передаёт sing-box хук files/lists-recovered и включает
                              резервный DNS, если стоит podkop; files/failsafe и
                              files/podkop-engine.init — служба failsafe;
                              files/podkop-engine.uci-defaults — dns_fallback,
                              urltest_download_* и liveness после обновления
openwrt/podkop-engine/naive.pkgarchs
                              pkgarch, для которых sing-box публикует cronet (NaiveProxy
                              в podkop-engine-full); files/config.json — конфиг по умолчанию
versions.env                  версии upstream, sha256 архивов, ревизии пакета, Go, lld, релизы SDK
scripts/gen-matrix.py         pkgarch -> образ openwrt/sdk
scripts/sdk-build.sh          сборка в образе SDK (одна или несколько веток)
lab/                          стенд: Xray-сервер/клиент + sing-box, проверка пакета в OpenWrt
.github/workflows/build.yml   ручной запуск: сборка -> проверка -> черновик релиза

Локальная сборка одной архитектуры

docker run --rm -v "$PWD:/src:ro" -v "$PWD/out:/out" \
  openwrt/sdk:x86-64-v24.10.8 sh /src/scripts/sdk-build.sh 1.14

NaiveProxy в podkop-engine-full линкуется только lld (объекты cronet с релокациями CREL GNU ld не берёт): для архитектур из naive.pkgarchs скрипт скачивает пакет clang Chromium (≈70 МБ, LLD_URL в versions.env, кэшируется в dl/) и берёт из него lld. CI запускает контейнер с --user root, чтобы отдать buildbot восстановленный из кэша dl/.

Проверки

# пакет в OpenWrt + podkop + `sing-box check` на конфиге, собранном кодом podkop
# (-e PKG=podkop-engine-full — второй пакет: все выходы и NaiveProxy)
docker run --rm -v "$PWD/out:/pkgs:ro" -v "$PWD/lab/podkop-check:/t:ro" -v "$PWD/lab:/lab:ro" \
  openwrt/rootfs:x86-64-24.10.8 sh /lab/pkg-test.sh
# стенд REALITY: Xray 24.11 … 26.9 × прикрытие с/без MLKEM, сверка с ожиданием
lab/fetch-bins.sh out lab/bin && lab/gate.sh lab/bin

Весь r13 под нагрузкой: утечки и гонки

Один sing-box с тем, что настраивает podkop: mixed-вход и DNS-вход, FakeIP с файлом кэша, локальный rule-set, который каждые 3 с заменяется переименованием, удалённый rule-set, группа urltest из двух VLESS-выходов (TLS и REALITY) с проверкой загрузки и автосменой отпечатка, Clash API. Нагрузка: 12 клиентов HTTP через прокси, клиенты DNS, опрос Clash API. Сервер в это время по кругу на 8 с отключает первый выход, 6 с отбрасывает ClientHello chrome и 5 с — любой ClientHello. До и после нагрузки (после двух принудительных сборок мусора) считаются горутины, открытые дескрипторы и живая куча.

1.13.21-r13 1.14.2-r13
бинарники из пакетов, 20 минут
запросов HTTP выполнено / не выполнено 500 803 / 252 503 394 / 264
запросов DNS / без ответа 218 332 / 0 219 736 / 0
горутины до → после 23 → 23 19 → 19
открытые дескрипторы до → после 15 → 15 13 → 13
живая куча до → после, КБ 4996 → 5534 5578 → 6106
живая куча под нагрузкой, каждые 2 минуты, МБ 7,2 · 9,3 · 8,3 · 9,1 · 10,3 · 7,7 · 11,9 · 6,8 · 8,1 11,3 · 9,1 · 12,0 · 12,3 · 10,5 · 12,5 · 8,5 · 10,8 · 10,4
RSS под нагрузкой, МБ 46–50 48–50
сборки с детектором гонок Go, 10 минут: сообщений о гонках 0 0

Не выполненные запросы HTTP — те, что пришлись на секунды, когда сервер стенда отключал выход или отбрасывал любой ClientHello. Живая куча под нагрузкой колеблется без роста от замера к замеру; после нагрузки она на 0,5 МБ больше, чем до неё, на обеих линиях.

Оба прогона сделаны до последней правки автосмены, с которой в список выхода с REALITY вошли edge, ios и qq: у REALITY-выхода стенда список тогда был chrome, safari, randomized, firefox. С пакетами после этой правки стенд под нагрузкой не повторялся; с r17 у автосмены другой порядок пунктов, его замеры — в разделе «Автосмена отпечатка uTLS».

Этот стенд нашёл две ошибки, которых не показали модульные тесты: гонку в списке соединений Clash API и DNS-запрос новой сессии без ответа (строки таблицы в начале и раздел «DNS-запрос новой UDP-сессии»). Гонка есть в коде sing-box обеих линий; запросы так же теряет sing-box 1.13.21 без патчей, а на линию 1.14 потерю принёс r4.

Выпуск новой версии — docs/RELEASE.md, установка на роутер вручную — docs/INSTALL.md.

podkop-engine и podkop-engine-full (1.13, 1.14)

podkop-engine содержит то, что пишет в конфиг podkop, TUIC и, с r15, http, а на 1.14 с r19 и block. Если в «Outbound Config» нужен другой протокол, ставится podkop-engine-full: в нём есть все выходы sing-box, кроме bridge и, на 1.13, block (на 1.14 с r19 block есть в обоих пакетах), а входы, DNS-серверы и службы — те же, что в podkop-engine.

podkop-engine podkop-engine-full
выходы direct, socks, shadowsocks, trojan, vless, hysteria2, tuic, selector, urltest, http (с r15), block (1.14, с r19) то же + vmess, snell (1.14), tor (нужен пакет tor), ssh, shadowtls, anytls, hysteria, naive
1.14, x86_64 / mipsel 24,3 / 25,8 МБ 26,0 / 27,7 МБ, с NaiveProxy на x86_64 — 37,0 МБ
1.13, x86_64 / mipsel 21,2 / 22,7 МБ 22,4 / 23,9 МБ, с NaiveProxy на x86_64 — 33,3 МБ

(До r9 оба набора были в одном пакете: 1.14 — 29,2 / 31,0 МБ, 1.13 — 25,3 / 26,8 МБ.)

Размеры в таблице измерены на r9. В r13 бинарь на x86_64 больше примерно на 0,6 МБ: 1.14 — 24,9 МБ и 37,5 МБ с NaiveProxy, 1.13 — 21,9 и 33,9 МБ.

NaiveProxy собирается из cronet (сетевой стек Chromium), который sing-box публикует только для части архитектур, и добавляет к бинарю около 11 МБ. Он есть в podkop-engine-full для x86_64, i386_pentium4, aarch64 (все четыре pkgarch), armv7 с VFP (arm_cortex-a5_vfpv4, a7_neon-vfpv4, a7_vfpv4, a8_vfpv3, a9_neon, a9_vfpv3-d16, a15_neon-vfpv4), mipsel_24kc, mipsel_74kc, riscv64_generic и loongarch64_generic. На остальных (mips big-endian — ath79 и подобные, mips64, armv5/v6, arm без FPU, mipsel_24kf, mipsel_mips32) выход naive отвечает naive outbound is not included in this build. mipsel_mips32 исключён сознательно: cronet собран под MIPS32r2.

Переход с одного пакета на другой (настройки /etc/config/sing-box сохраняются):

# opkg (OpenWrt 24.10 и старше): podkop зависит от sing-box, отсюда --force-depends
opkg remove --force-depends podkop-engine && opkg install /tmp/podkop-engine-full_<версия>_openwrt_<pkgarch>.ipk
# apk (OpenWrt 25.12 и snapshot): одной транзакцией
apk add --allow-untrusted /tmp/podkop-engine-full_<версия>_openwrt_<pkgarch>.apk '!podkop-engine'
service podkop restart

Обратно — то же с именами наоборот.

Старт, когда списки не скачиваются

Обычный sing-box при недоступном remote rule-set завершается с FATAL ... initial rule-set; procd после нескольких перезапусков сдаётся, и роутер остаётся без прокси и без DNS (dnsmasq podkop уже переключил на sing-box). podkop-engine 1.13/1.14 в этом случае стартует с пустым списком и докачивает его сам. Когда докачка удалась (путь до списков заработал), sing-box один раз выполняет команду из SING_BOX_RULESET_RECOVERED_HOOK; init-скрипт пакета задаёт её, только если установлен podkop: /usr/libexec/podkop-engine/lists-recovered дожидается стартового list_update podkop (если тот ещё идёт) и запускает podkop list_update — так догружаются списки, которые podkop качает сам (подсети в nft, текстовые списки). podkop при этом не меняется; sing-box не перезапускается. После перезапуска sing-box списки берутся из кэша (с r18 — из файлов рядом с cache.db), хук не срабатывает.

Проверка загрузки перед переключением urltest (1.13, 1.14)

Обычная проба urltest — HEAD на generate_204, несколько сотен байт. Сервер за DPI, который «замораживает» соединение после ~16 КБ (не отвечает, не рвёт), такую пробу проходит с хорошей задержкой, и группа выбирает его: работают только мелкие запросы. Поэтому перед тем, как переключиться на член группы, sing-box скачивает через него 64 КиБ. Член, через который загрузка встала, пропускается (его результат пробы сохраняется, в LuCI он «живой») и пробуется снова, когда придёт его следующий результат периодической пробы — раз в интервал, пока он остаётся лучшим по задержке. Если других членов нет, замороженный всё же лучше мёртвого. Выбранный член не проверяется: заморозка уже используемого сервера здесь не видна.

Включено по умолчанию, настраивается в /etc/config/sing-box:

uci set sing-box.main.urltest_download_check=0     # выключить
uci set sing-box.main.urltest_download_url='https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js'
uci commit sing-box; service podkop restart

Адрес должен начинаться с http:// или https:// и не содержать пробелов: с r14 другой адрес init-скрипт не передаёт, пишет об этом строку в лог, и проверка загрузкой не работает (до r14 адрес с пробелом обрезался по пробелу).

URL по умолчанию — https://speed.cloudflare.com/__down?bytes=65536: отдаёт ровно столько байт, сколько попросили, без кэша. Подойдёт любой URL с телом ≥ 64 КиБ или с известной длиной (Content-Length): читается не больше 64 КиБ, Range сервер может игнорировать. Цена — 64 КиБ на каждое переключение; в покое ничего не скачивается. При обновлении со старых версий оба параметра дописываются в /etc/config/sing-box сами (uci-defaults), заданные значения не меняются.

С r10 ссылку можно задать в самой группе urltest (config.json), так её сможет задавать podkop:

{ "type": "urltest", "tag": "auto", "outbounds": ["a", "b"], "download_url": "https://speed.cloudflare.com/__down?bytes=65536" }

Поле группы главнее /etc/config/sing-box: "download_url": "" выключает проверку для этой группы, ссылка — включает с ней, даже если urltest_download_check=0. Группа без поля берёт ссылку из /etc/config/sing-box, как раньше. Ссылка без схемы http/https или без хоста — ошибка sing-box check.

Резервные выходы urltest (1.13, 1.14, с r10)

{
  "type": "urltest", "tag": "auto",
  "outbounds": ["vps-1", "vps-2"],
  "fallbacks": ["backup-vps", "direct-out"]
}

Пока хоть один из outbounds проходит проверку, группа выбирает среди них, как обычно (по задержке и tolerance); резервные не выбираются, даже если они быстрее. Когда не отвечает ни один основной (нет результата проверки или загрузка встала), группа берёт первый резервный по порядку списка, у которого есть результат, — задержки резервных не сравниваются. Как только любой основной снова проходит проверку, группа возвращается на него; с резервного на более ранний резервный — так же.

Резервные проверяются в каждом раунде вместе с основными (один HEAD на резервный выход раз в интервал), поэтому их состояние известно заранее и видно в дашборде. Всё, что группа знает о трафике (отказ соединения, сервер молчит), и проверка загрузки работают для резервных так же, как для основных. В списке членов группы (Clash API, LuCI) резервные идут после основных. Тег не может повторяться в outbounds и fallbacks. Без этого поля группа работает как раньше.

На 1.14 с r20 outbounds может не быть вовсе, если есть fallbacks: тогда группа выбирает среди резервных. Резервный выход типа block в раундах не проверяется: группа берёт его, когда ни один выход перед ним в списке не отвечает, и соединение получает отказ сразу, без попытки набора. До конца первого раунда проверок после старта или замены на ходу резервные не берутся: у выходов ещё нет результатов, и группа идёт через первый выход списка, как без fallbacks. Пока выбран резервный block, следующий раунд идёт через 30 с, затем каждый раз вдвое позже, но не позже чем через интервал.

fallbacks и download_url есть только в podkop-engine с r10: podkop-engine r9 и старше и обычный sing-box отвергают такой конфиг (json: unknown field "fallbacks"). С r11 о них сообщает строка Features: (см. ниже): podkop пишет эти поля и показывает их в LuCI, только когда видит в ней urltest.fallbacks и urltest.download_url.

Транспорт xhttp (1.13, 1.14, с r11)

{
  "type": "vless", "tag": "xh", "server": "example.org", "server_port": 443, "uuid": "…",
  "tls": { "enabled": true, "server_name": "example.org", "utls": { "enabled": true, "fingerprint": "chrome" } },
  "transport": {
    "type": "xhttp",
    "mode": "auto",
    "host": "example.org",
    "path": "/api/",
    "x_padding_bytes": "100-1000",
    "xmux": { "max_concurrency": "16-32", "h_max_request_times": "600-900" }
  }
}

Клиент транспорта XHTTP (SplitHTTP) из Xray-core 26.9.9, только исходящий, для VLESS и Trojan. Имена опций совпадают с sing-box-extended (x_padding_bytes, sc_max_each_post_bytes, session_placement, xmux { … }, download { server, server_port, tls, detour, … }); диапазоны пишутся как "100-1000", "30", числом или {"from": 100, "to": 1000}. Неизвестное поле — ошибка sing-box check.

  • Режимы: packet-up, stream-up, stream-one, auto. auto — как в Xray: с REALITY stream-one (stream-up, если задан download), иначе packet-up.
  • Версия HTTP: как в Xray. REALITY — HTTP/2; без TLS — HTTP/1.1; единственный ALPN http/1.1 или h3 выбирает эту версию; иначе HTTP/2.
  • download: отдельный сервер для скачивания со своим адресом, TLS или REALITY и опциями.
  • VLESS flow поверх xhttp без шифрования VLESS не поддерживается: такой outbound не проходит sing-box check. С опцией encryption (с r14, следующий раздел) — работает.

Проверено на стенде против сервера Xray 26.9.9, рядом с клиентом Xray: 104 варианта настроек (из них 84–85 проходят все проверки, остальные упираются в настройки сервера или прокси одинаково у обоих клиентов), 900 одновременных сессий на HTTP/2 и HTTP/3, 10 000 соединений подряд без роста памяти, горутин и дескрипторов, UDP.

Где клиент намеренно ведёт себя иначе, чем клиент Xray:

  • Отдача в заголовках и cookie (uplink_data_placement: header, cookie): если sc_max_each_post_bytes не задан, запрос несёт не больше 4096 байт (у Xray — 1 МБ, что не проходит ни в один сервер: сервер Xray по умолчанию принимает 8192 байта заголовков). Заданное значение используется как есть; если сервер его не принимает (ответ 431 или 413), ошибка называет опцию, а остальные соединения выхода продолжают работать.
  • Много соединений на HTTP/3: когда на QUIC-соединении заканчиваются потоки (сервер разрешает 100), открывается ещё одно; клиент Xray в этом случае ждёт (стенд: 900 сессий из 900 против 300 из 900).
  • Данные перед закрытием: записанное перед закрытием соединения досылается (до 5 с).
  • Получатель не читает: неотвеченных запросов отдачи у сессии не больше 32 или 4 МиБ, дальше запись ждёт, как в TCP. На HTTP/1.1 запрос отдачи ждёт ответа не дольше 60 с плюс время на его размер.
  • Простой: соединение пула без сессий закрывается через 5 минут; на 1.14 с r18 — и когда sing-box закрывает простаивающие соединения выхода, на который ничего не ссылается.
  • Память: кадр HTTP/2 при записи не больше 16 КиБ (иначе клиент Go держит 512 КиБ на каждую потоковую отдачу), паддинг не больше 64 КиБ, длина идентификатора сессии не больше 1024.

Чего нет: серверной стороны, finalmask, Browser Dialer (шифрование VLESS есть с r14, см. следующий раздел); на 1.13 HTTP/3 идёт без отпечатка Chrome для QUIC (на 1.14 — с ним, кроме disable_sni).

На роутере без аппаратного AES шифр важнее транспорта. MT7621, скачивание 50 МБ: xhttp с отпечатком chrome (он первым предлагает AES-GCM) — 1,9 МБ/с; без utls (Go выбирает ChaCha20) — 3,26 МБ/с при 3,75 МБ/с без прокси.

Шифрование VLESS (1.13, 1.14, с r14)

Шифрование VLESS (VLESS Encryption) — слой под VLESS из Xray-core: обмен ключами ML-KEM-768 и X25519 и шифрование всего потока до сервера. Сервер, у которого оно задано (decryption), клиента без него не принимает: до r14 такой сервер был недоступен, а decode-link его ссылки отклонял. Здесь сделана сторона клиента — опция encryption выхода vless:

{
  "type": "vless",
  "server": "203.0.113.20",
  "server_port": 443,
  "uuid": "6f1c9a0e-3b52-4c1e-9a55-2d4a1b0f7c11",
  "flow": "xtls-rprx-vision",
  "encryption": "mlkem768x25519plus.native.0rtt.<ключ>",
  "tls": { "enabled": true, "server_name": "e.example.com" }
}

Значение — та же строка, что у клиента Xray: её печатает xray vlessenc, и панели ставят её в параметр encryption ссылки. Вид: mlkem768x25519plus.<режим>.<1rtt|0rtt>[.<дополнение>…].<ключ>[.<ключ>…].

  • Режим — native, xorpub или random: как выглядит начало обмена на проводе.
  • 0rtt — следующие соединения отправляют данные сразу, по билету от первого обмена, пока сервер его помнит; 1rtt — полный обмен на каждом соединении. Сервер, перезапущенный с билетом в памяти клиента, отвергает одно соединение, следующее идёт полным обменом.
  • Дополнение — необязательные сегменты вида 100-111-1111, как в Xray.
  • Ключ — base64url от 32 байт (X25519) или от 1184 байт (ML-KEM-768); несколько ключей — для цепочки серверов.

Строку, которую не принял бы и Xray, sing-box отклоняет при старте, а decode-link — с отказом vless-encryption. Пустое значение и none — шифрования нет, как раньше.

Vision. С шифрованием flow: xtls-rprx-vision работает на его слое — с любым транспортом и без TLS; сервер Xray принимает flow поверх XHTTP только так. TLS под слоем шифрования Vision не разворачивает, как и Xray. Без encryption правила прежние: flow нужен TLS или REALITY и обычный TCP.

Шифр — AES-GCM при аппаратном AES, иначе ChaCha20-Poly1305, как в Xray. Обмен ключами ограничен по времени: молчащий сервер даёт ошибку VLESS Encryption handshake: context deadline exceeded через 15 с, а не подвешивает соединение.

С podkop. podkop 0.7.23 передаёт в decode-link только ссылки с транспортом xhttp; у остальных ссылок VLESS его shell-разбор параметр encryption отбрасывает. Пока это не изменится в podkop, шифрование работает для ссылок xhttp и для выхода, заданного в «Outbound Config». Имени в строке Features: у этой возможности нет.

Чего нет: стороны сервера. Пока соединение через слой шифрования простаивает (кроме Vision в прямом режиме), оно держит буфер копирования — как соединение через websocket (там на стенде 32 КиБ, после объёмной передачи 64 КиБ); для слоя шифрования замера нет. Суммарная пауза в настройке дополнения — не больше 10 с: с большей настройка отклоняется. Код слоя перенесён из Xray-core v26.9.30 (лицензия MPL-2.0, пометка в файлах protocol/vless/encryption); бинарник вырос на 44 КиБ.

Проверка Результат
сервер Xray 26.9.9, 51 сочетание: по обычному TCP — режим × 1rtt/0rtt × вид ключа × с Vision и без, и дополнение; с 0rtt — поверх TLS, REALITY, ws, httpupgrade, grpc и xhttp (auto, packet-up, stream-up), без TLS и с ним, с Vision и без. По каждому три запроса HTTP, три HTTPS (TLS 1.3 внутри, Vision уходит в прямой режим) и три датаграммы UDP всё проходит на обеих линиях
один выход без шифрования с обеих сторон и три контроля: клиент без шифрования, другой ключ, другой режим первый работает, через контрольные не проходит ничего, как и должно
то же на 1.13 против Xray 25.12.8, 26.3.27 и 26.7.28 55 из 55 на каждом
настоящий сервер за границей, Xray 26.3.27: XHTTP + REALITY (auto, stream-up, packet-up) и TCP + REALITY, с Vision и без; три запроса и файл 10 МБ; рядом клиент Xray 26.9.9 с той же ссылкой по свободному пути проходит всё, 10 МБ за 0,9–1,6 с
тот же сервер по пути, где соединение без имени из белого списка обрывается после 25 пакетов с целью REALITY из белого списка проходит всё (10 МБ за 1,2–1,9 с); с собственным именем сервера файл не идёт ни у sing-box, ни у клиента Xray; с tls.spoof проходит на обеих линиях (10 МБ за 1,4–5,6 с)
сервер, который принимает соединение и молчит запрос через такой выход завершается ошибкой через 14,6–15,0 с

Первые три строки и последняя получены на бинарниках из пакетов podkop-engine для x86_64 (в них к запросам добавлена отдача 300 КБ по HTTP, три раза на сочетание), строки с настоящим сервером — на сборках тех же исходников вне SDK; XHTTP с целью из белого списка повторён на бинарниках из пакетов по обоим путям.

echo 'vless://…@example.org:443?type=xhttp&security=tls&…&extra=%7B…%7D#name' | sing-box tools decode-link --compact

Печатает outbound для одной ссылки (из stdin или аргументом; через stdin ссылку не видно в ps).

Код Значение stdout stderr
0 outbound напечатан JSON строки warning: … — что ссылка просит, а outbound не умеет
2 ссылку использовать нельзя пусто одна строка error: …
1 всё остальное (например, неизвестный флаг) пусто —

Сообщения не повторяют ссылку: в ней секреты. Это относится и к тому, что говорит сам sing-box, когда не принимает outbound: из его сообщения берётся первая строка, из неё убираются пользователь, пароль, ключи и любой текст ссылки, который не является простым именем. Каждая ошибка и каждое предупреждение — одна строка, что бы ни стояло в ссылке. Вызывать из скриптов нужно с --compact: sing-box без этой команды на вызов без флагов печатает справку с кодом 0, а с флагом выходит с кодом 1.

Полный перечень параметров ссылок, с примерами и тем, что из каждого получается в config.json: docs/LINKS.md — 369 строк, каждая проверена запуском команды на обеих линиях.

Напечатано — значит, работает в этой сборке. С r13 команда перед печатью строит outbound тем же кодом, что sing-box check, как единственный выход конфигурации. Поэтому:

  • напечатанный outbound проходит sing-box check этого же бинарника (проверено на 1533 ссылках четырьмя сборками: 1.13 и 1.14, podkop-engine и podkop-engine-full — расхождений нет);
  • ссылка на протокол, которого в пакете нет, отклоняется словами сборки: vmess outbound is not included in this build (podkop_slim), install podkop-engine-full …;
  • опция, которой нет в этой линии sing-box, — тоже ошибка, а не молчаливый пропуск;
  • outbound, при построении которого код sing-box падает (значение длиннее места, куда его разбирают), и построение, которое не закончилось за 20 с, — тоже ошибка с кодом 2, а не аварийный выход команды.

«Проходит check» ещё не значит «соединяется»: это проверено отдельно, на стенде с настоящими серверами (см. «Сравнение с разбором ссылок в podkop» ниже).

Виды ссылок

Схема Outbound Пакет Что читается
vless://uuid@host:port?… vless оба параметры предложения Xray (#716): security, sni, fp, alpn, pbk, sid, flow, type, host, path, serviceName, mode, extra, headerType, packetEncoding, ech, allowInsecure; с r14 — encryption (шифрование VLESS)
trojan://password@host:port?… trojan оба то же; TLS включён, если ссылка не говорит security=none
ss://… shadowsocks оба SIP002 (метод и пароль в base64 или открыто для методов 2022), старый вид «всё в base64», plugin (simple-obfs = obfs-local), uot
socks5://, socks5h://, socks4://, socks4a://, socks:// socks оба user:password; у socks:// (v2rayN) пользователь в base64; порт по умолчанию 1080
hysteria2://, hy2:// hysteria2 оба пароль, список портов на месте порта или в mport, obfs, obfs-password, upmbps/downmbps, sni, insecure, alpn; из fm (Remnawave) — обфускация, смена портов и скорости
tuic://uuid:password@host:port?… tuic оба congestion_control, udp_relay_mode, udp_over_stream, zero_rtt_handshake/reduce_rtt, heartbeat_interval, sni, alpn, allow_insecure, disable_sni
vmess:// vmess full JSON в base64 (v2rayN) и вид URL с параметрами как у vless
http://, https:// http оба (с r15; раньше full) только с портом и без пути: адрес страницы или подписки, вставленный вместо прокси, отклоняется
hysteria://host:port?… hysteria full auth, peer, upmbps, downmbps, obfs=xplus+obfsParam, mport, alpn, insecure
anytls://password@host[:port]?… anytls full sni, insecure, alpn, fp
naive+https://, naive+quic:// naive full с NaiveProxy user:password, sni, extra-headers, insecure-concurrency, cert

У tor, ssh, shadowtls и snell вида ссылки нет: их никто не выдаёт ссылками, а нужное им (второй выход, локальные файлы, ключи узла) ссылке задавать нельзя. Они задаются как outbound в JSON.

Что отклоняется и о чём предупреждается

Ссылка отклоняется, только когда без запрошенного до сервера не дойти или собранный outbound заведомо не заработает. То, что решает один клиент или что лишь добавляет проверку, — предупреждение: outbound печатается и соединяется.

Отклоняется Почему
настройка шифрования VLESS (encryption=mlkem768x25519plus…), которую не принимает и Xray: неизвестный режим, не 1rtt и не 0rtt, ключ не в 32 и не в 1184 байта с такой настройкой sing-box не стартует; верная с r14 записывается в опцию encryption (в r13 отклонялась любая)
flow поверх ws, grpc, http, httpupgrade, xhttp и без TLS — когда в ссылке нет шифрования VLESS на стенде с Xray 26.9.9 не работает ни в одном сочетании: поверх ws и grpc sing-box сам обрывает каждое соединение, поверх httpupgrade отказывает сервер
транспорт mKCP; шифрование и заголовки пакетов транспорта QUIC в sing-box их нет
headerType=http вместе с TLS без TLS это транспорт http sing-box (на стенде работает), с TLS он говорил бы на HTTP/2
имя сервиса gRPC в виде пути, кроме /имя/Tun (в том числе в двойном кодировании, как его пишет Marzban: svc%252Fpath) sing-box строит путь /имя/Tun с именем одним сегментом; другой путь до сервера не доходит (стенд)
fm (finalmask) с маской, кроме fragment и noise остальные маски меняют то, что идёт по сети, и сервер их ждёт
отпечаток, которого не знает и Xray, вместе с REALITY REALITY построен на uTLS
Shadowsocks поверх транспорта или TLS Xray; TUIC v4 (токен вместо пароля); protocol=faketcp у hysteria в sing-box этого нет
http(s):// с путём или без порта это адрес страницы, а не прокси
sid у REALITY не из шестнадцатеричных цифр, нечётной длины или длиннее 16 короткий идентификатор — до восьми байт; до r13 длинный sid завершал sing-box аварийно (и при старте, и в check)
tls.spoof вместе с tcp_fast_open поддельный ClientHello шлётся по установленному соединению, а с TFO настоящий уходит вместе с попыткой соединения: каждое соединение такого выхода падает
список портов hysteria/hysteria2, в котором портов больше, чем их существует (mport=1-65535,1-65535,…) sing-box при построении выхода выписывает порты по одному; повторённые диапазоны съели бы память роутера
параметр cert плагина Shadowsocks (plugin=v2ray-plugin;tls;cert=…) это локальный файл: sing-box читает его при построении выхода
пользователь TUIC не в виде UUID; метод Shadowsocks или обфускация Hysteria 2 не в виде имени sing-box назвал бы такое значение в своей ошибке, а на этом месте в битой ссылке может стоять пароль
Предупреждение Что происходит
gRPC mode=multi используется gun: сервер отвечает на оба вида потоков (стенд)
gRPC authority, не равный имени сервера TLS sing-box шлёт имя сервера TLS; обратный прокси, выбирающий сервер по authority, может его не найти
отпечаток, который знает только Xray (hellochrome_131, randomizednoalpn) берётся отпечаток того же браузера; неизвестный никому при TLS — без имитации, как в Xray
pcs/pinSHA256, vcn закреплённый сертификат не поддерживается: сервер проверяется по системным корням, сервер с самоподписанным сертификатом будет отвергнут (Xray закрепляет хеш сертификата, sing-box — хеш ключа, одно из другого не получить). В новых версиях Xray allowInsecure убран в пользу pcs, так что такие ссылки будут встречаться
pqv проверка ML-DSA-65 сервера REALITY не выполняется
fragment, noise, fm с масками fragment/noise это настройки клиента Xray; у sing-box своя фрагментация: sb.tls.fragment=1
ech в виде «имя+резолвер» конфигурация ECH спрашивается у DNS sing-box, названный резолвер не используется
?ed= в пути httpupgrade ранних данных у этого транспорта в sing-box нет, из пути они убираются
type=http/h2 без TLS sing-box говорит там HTTP/1.1, а сервер Xray ждёт HTTP/2 (h2c) и не ответит; сервер на sing-box принимает оба
ссылка TUIC без alpn sing-box для TUIC своего ALPN не предлагает; сервер, у которого ALPN задан (обычно h3), такому клиенту откажет — и наоборот: alpn=h3 не примет сервер без ALPN (обе пары проверены на стенде)
шифр VMess (scy, в виде URL — encryption), кроме auto, aes-128-gcm и chacha20-poly1305 читается как auto, как у клиента Xray: его сервер с 2026 года отвергает none, zero и aes-128-cfb, а то, из чего выбирает auto, принимает любой сервер VMess. Опцию sing-box можно задать путём: sb.security=zero
параметр, имя которого — не имя (в нём есть что-то кроме латинских букв, цифр, -, _ и ., или оно длиннее 128 знаков) отбрасывается, и имя в сообщении не повторяется: на месте имени может стоять что угодно, в том числе часть второй ссылки с её пользователем
неизвестный параметр игнорируется

И три правила, которые ни отказ, ни предупреждение, — так же поступает клиент Xray:

  • alpn из ссылки не передаётся транспортам ws, httpupgrade, grpc и http: они сами просят http/1.1 или h2. Панели пишут один alpn=h2,http/1.1 всем хостам; с ним сервер выбирал HTTP/2, и рукопожатие WebSocket не проходило (на стенде — все пять таких ссылок). Если нужен именно свой ALPN — sb.tls.alpn=…;
  • транспорт QUIC идёт без отпечатка uTLS (с ним каждое соединение падало с unsupported usage for uTLS), а без alpn в ссылке предлагаются h3, h2 и http/1.1: сервер Xray знает два последних, сервер sing-box — первый;
  • VMess с TLS: значение scy=auto остаётся auto (см. строку «VMess» в таблице выше).

Опции sing-box без общепринятого параметра: sb.<путь>

Опцию выхода, для которой у ссылок нет имени, ссылка называет её путём в конфигурации после sb.:

vless://…?security=tls&sb.tls.fragment=1&sb.tls.spoof=packagist.org
…&sb.multiplex.enabled=1&sb.multiplex.protocol=h2mux
…&sb.transport.headers.User-Agent=curl%2F8

Значение читается по типу, который опция имеет в этой сборке: флаг — 1/0/true/false, список — через запятую, остальное — как в конфигурации; значение, начинающееся со скобки или кавычки, читается как JSON. Путь побеждает то, что ссылка говорит иначе (с предупреждением); группа опций с выключателем, которую задали и не включили (sb.multiplex.protocol без sb.multiplex.enabled=1), — тоже предупреждение. Выход после этого строится как любой другой, и то, что он отвергнет, становится ошибкой команды.

Имена, которые уже используют клиенты, читаются как соответствующие опции: tfo, tcp-fast-open, tcp_fast_open; uot, udp-over-tcp; из Throne — tls_fragment, tls_record_fragment, tls_spoof, tls_spoof_method, mux_protocol, mux_max_connections, mux_min_streams, mux_max_streams, mux_padding, brutal_up_mbps, brutal_down_mbps. Голые mux и fragment — нет: у разных клиентов они значат разное.

Ссылка — чужой текст: она может настраивать, как достичь её собственного сервера, и ничего больше. Через sb. нельзя задать: ссылки на другие объекты конфигурации (detour, domain_resolver, netns), локальный маршрут (bind_interface, адреса привязки, routing_mark, network_strategy), локальные файлы (любая опция *_path, кроме флага tcp_multi_path; параметр cert плагина Shadowsocks), возможности системы (tls.engine, kTLS), пределы самого роутера (tls.handshake_timeout: ссылка сняла бы ограничение времени своего же рукопожатия; tls.fragment_fallback_delay: пауза между частями фрагментированного ClientHello пережидается, что бы ни случилось с соединением, и ссылка держала бы каждое соединение сколько скажет), realm у hysteria2, устаревшие опции, а также то, что ссылка говорит сама (type, tag, server, server_port). Значением не может быть объект JSON: каждая опция задаётся своим путём, иначе имена внутри объекта прошли бы мимо этого списка. Полный список по каждому типу выхода, с причиной отказа, лежит в исходниках (common/sharelink/testdata/options-1.13.txt, options-1.14.txt): тест не даёт появиться новой опции, пока её не отнесут к одному из двух классов.

Ссылки панелей

Декодер прогнан по ссылкам, которые выдают собственные генераторы панелей на матрице «протокол × транспорт × защита × заголовок × настройки узла × flow».

Панель Ссылок Принято Отклонено и почему
Marzban 0.8.4 723 503 имя сервиса gRPC в виде пути, кроме /имя/Tun (панель пишет его в двойном кодировании), — 90; mKCP — 60; транспорт QUIC с шифрованием пакетов — 30, с REALITY — 10 и без TLS — 5; headerType=http с TLS/REALITY — 25. В podkop-engine отклоняются и остальные ссылки vmess: принято 403 (нужен podkop-engine-full)
Remnawave 3.4.5 493 348 flow не поверх TCP с TLS/REALITY и без шифрования VLESS — 66; mKCP — 35; headerType=http с TLS — 30; finalmask header-custom — 14

Строка Remnawave — замер r14: из 91 ссылки с шифрованием VLESS принимаются 78, остальные 13 отклоняются из-за mKCP и headerType=http с TLS; в r13 отклонялись все 91, и принято было 270.

Матрица перебирает и сочетания, которые панель позволяет настроить, а сервер не обслуживает, поэтому доля отказов здесь выше, чем среди ссылок работающих серверов.

Сравнение с разбором ссылок в podkop

Работает ли то, что получилось. Стенд: сервер Xray 26.9.9 с 85 входами (VLESS, Trojan, VMess, Shadowsocks, SOCKS, HTTP × транспорты × без защиты, TLS, REALITY), Xray 1.8.24 для транспортов HTTP/2 и QUIC, которых в новом Xray уже нет, сервер Hysteria 2.13.0, sing-box 1.13.21 без патчей для TUIC, AnyTLS и Hysteria. Ссылки на эти входы пишут собственные генераторы Marzban и Remnawave и команда hysteria share, остальное — вручную по правилам схем. Каждую ссылку разбирают shell-код podkop 0.7.22 и decode-link (сборка podkop-engine-full), и через каждый полученный outbound трижды скачивается файл 200 КБ. Обе линии дают одно и то же:

Из 252 ссылок shell-код podkop decode-link
файл скачивается, 3 из 3 97 238
outbound выдан, но ничего не проходит 87 3
outbound выдан, sing-box его не принимает 3 0
ссылка отклонена 65 11

Ссылок, которые у shell-кода работают, а у decode-link нет, — ни одной. 11 отказов decode-link — то, чего sing-box не умеет: заголовок HTTP у TCP вместе с TLS (5), путь gRPC из нескольких сегментов (3), mKCP (3). Три неработающих выхода: HTTP/2 без TLS против сервера Xray (ссылку пишет генератор Marzban; sing-box говорит на этом транспорте HTTP/1.1, и команда об этом предупреждает) и две намеренно несогласованные пары TUIC с ALPN только на одной стороне (о ссылке без alpn команда предупреждает, о ссылке с alpn=h3 к серверу без ALPN — нет).

Почему не работают 87 выходов shell-кода: транспорт, которого он не знает (xhttp, httpupgrade, http, заголовок HTTP у TCP, QUIC, mKCP), молча превращается в обычный TCP — 68; у ws с ранними данными нет заголовка Sec-WebSocket-Protocol — 8; ALPN из ссылки, с которым транспорт не работает (ws с h2 в списке — 4, grpc с одним http/1.1 — 1), — 5; trojan без security остаётся без TLS — 3; путь gRPC, который sing-box запросить не может, — 3. Ещё три выхода sing-box не принимает (метод xchacha20-poly1305, flow xtls-rprx-vision-udp443). Отклоняет shell-код 65 ссылок: VMess — 33, адреса IPv6 — 17, пароли с экранированными @, : и / — 3, Shadowsocks старого вида (всё в base64) — 1, схемы, которых он не знает (http, https, socks5h, socks:// в base64, tuic, anytls, hysteria), — 11.

UDP (датаграмма до эхо-сервера и обратно) проходит через все рабочие выходы обоих разборов, кроме тех, где его нет в самом протоколе (SOCKS4, HTTP), и одного выхода SOCKS5 по адресу IPv6 (socks5://[::1]:…), где на стенде UDP не прошёл.

Пока стенд собирался, он нашёл в decode-link четыре вида ссылок, дававших нерабочий outbound, и одну ошибку в sing-box; всё исправлено в r13, а первый прогон дал 162 рабочих из 223:

  • alpn из ссылки для ws, grpc, http (см. выше);
  • транспорт QUIC с uTLS и без подходящего ALPN;
  • путь gRPC в двойном кодировании Marzban — теперь отказ;
  • HTTP/2 без TLS — теперь предупреждение;
  • VMess с TLS не работал с Xray 26 вообще (17 ссылок из 17): sing-box заменял auto на zero, а Xray убрал zero и none и закрывает такое соединение.

Что получается из одной и той же ссылки. На наборе из 1533 ссылок (генераторы панелей по полной матрице настроек и ссылки из открытых подписок) оба разбора дали outbound для 530, одинаковый — для 176 (замер r14; в r13 — 490 и 176); отличия — те же, что выше, плюс:

  • нет fp — отпечаток chrome, как у Xray (кроме протоколов на QUIC);
  • allowInsecure=1 у trojan без security начинает действовать (shell-разбор не включает там TLS; в остальных ссылках он этот параметр читает и сам);
  • ws без host: в Host идёт имя сервера TLS;
  • + в пароле остаётся плюсом;
  • шифрование VLESS: shell-разбор параметр encryption отбрасывает, и выход получается без шифрования — сервер такого клиента не примет; decode-link переносит параметр в опцию (40 ссылок набора);
  • 223 ссылки, из которых shell-разбор делал заведомо неработающий выход (mKCP, путь gRPC в двойном кодировании, flow поверх ws и т.п.), отклоняются с причиной. Из 316 ссылок, на которых shell-разбор падает, а decode-link нет, 100 — VMess.

Строка Features: (с r11)

$ sing-box version
sing-box version 1.14.4-pdk-r24
…
Features: urltest.fallbacks,urltest.download_url,transport.xhttp,tools.decode-link,tls.utls-fallback

Перечень того, что эта сборка добавляет к sing-box и от чего может зависеть конфиг или скрипт. Имя из списка не удаляется и не меняет смысла. podkop (начиная с itdoginfo/podkop#445) читает строку и без нужного имени не пишет опцию в конфиг и не показывает её в LuCI; ссылка с транспортом xhttp без tools.decode-link останавливает podkop с сообщением о требуемой версии. В r10 поля fallbacks и download_url уже есть, а строки Features: ещё нет: для podkop это то же, что отсутствие возможности.

С r13 добавилось одно имя: tls.utls-fallback — сборка понимает раздел experimental.utls_fallback в config.json (автосмена отпечатка uTLS; без раздела она выключена). С r17 автосмена пробует свои пункты (см. её раздел), и список fingerprints, который пишет podkop на shell, не читается: раздел с ним принимается, но порядок у автосмены свой. То, что decode-link с r13 читает ссылки всех протоколов сборки, и опция spoof отдельных имён не получили: ссылку, которую сборка не читает, команда отклоняет сама, с причиной и статусом 2, а r13 от более ранних ревизий отличает версия (…-pdk-r13; с новой версией sing-box счёт ревизий начинается с 1, поэтому сравнивать нужно версию целиком, а не число после -pdk-r).

Поддельный ClientHello: tls.spoof (1.13, 1.14, с r13)

Опция TLS spoof появилась в sing-box 1.14: перед настоящим ClientHello по тому же соединению уходит поддельный, с именем из опции, собранный так, чтобы сервер его отбросил (spoof_method: wrong-sequence по умолчанию, wrong-checksum, wrong-ack, wrong-md5, wrong-timestamp). Фильтр на пути, решающий по первому ClientHello соединения, видит поддельное имя. То же делает опция правила маршрута tls_spoof. В r13:

  • опция перенесена в линию 1.13 (только Linux; пакеты совпадают с пакетами 1.14 побайтно);

  • на обеих линиях она работает с REALITY: upstream её там запрещает, письменной причины запрета нет, проверка подлинности REALITY считается по настоящему ClientHello и поддельный её не задевает;

  • поддельный пакет уходит тем же маршрутом, что соединение: сырой сокет получает метку, интерфейс и локальный адрес соединения (в upstream — нет, и при routing_mark, bind_interface или auto_detect_interface пакет мог уйти через другой интерфейс);

  • чтение номеров последовательности не шлёт лишний пробный пакет перед ClientHello;

  • wrong-timestamp отклоняется на соединении без меток времени TCP: сервер принял бы там поддельный ClientHello за данные;

  • сырой сокет открывается в момент отправки, а не при создании соединения: в upstream он оставался открытым навсегда, если рукопожатие обрывалось до первой записи.

  • поддельный пакет несёт то же время жизни (TTL, hop limit), что пакеты самого соединения: в upstream в его заголовке всегда стоит 64, и там, где системное значение другое (net.ipv4.ip_default_ttl, как его ставят на роутерах за модемом сотового оператора), это был бы единственный пакет соединения, который отличается;

  • в соединение, по которому уже шли данные, ничего не подделывается, а через detour к прокси-выходу опция отклоняется с ошибкой: там сокет под соединением принадлежит туннелю, и подделка ушла бы на его сервер внутри его соединения. Через detour к выходу direct (другой аплинк по интерфейсу или метке) сокет ведёт к самому серверу, и опция работает;

  • на линии 1.13 tls_spoof правила с действием bypass применяется, как на 1.14 (остальные опции маршрута у bypass линия 1.13 по-прежнему не применяет).

В ссылке опция задаётся как sb.tls.spoof=<имя> и sb.tls.spoof_method=<способ>.

Что нужно знать:

  • Нужны права. Сырой сокет и чтение состояния TCP требуют root (CAP_NET_RAW и CAP_NET_ADMIN); podkop запускает sing-box от root. Без прав каждое соединение выхода падает с ошибкой, называющей нужные права.
  • Маскировка слабее. По соединению идут два ClientHello с разными именами и разной формы; наблюдатель, который смотрит на оба, это видит. Формулировка upstream «неотличим» для поддельного пакета неверна: он собран стандартной библиотекой Go, а не копирует настоящий.
  • Способ зависит от сервера и пути. Сервер должен отбросить поддельный пакет: wrong-md5 отбрасывает Linux, собранный с TCP-MD5 (обычные дистрибутивы), wrong-checksum могут исправить или отбросить по дороге. wrong-ack отбрасывает только Linux с правкой «tcp: do not accept ACK of bytes we never sent» (ядро 6.7 и стабильные выпуски с декабря 2023 года); сервер на более старом ядре примет поддельный ClientHello за данные соединения, и каждое рукопожатие сорвётся — это выведено из исходников ядра, на стенде сервер был с ядром 6.17. wrong-sequence (по умолчанию) от ядра сервера не зависит. По сообщениям с мест, некоторые фильтры рвут соединение, заметив подделку; у нас это не воспроизводилось.
  • Только соединение прямо к серверу TLS. Не работает вместе с tcp_fast_open (ссылка, которая задаёт и то и другое, отклоняется) и поверх транспорта, несущего несколько потоков в одном соединении. Опция правила маршрута tls_spoof рассчитана на правило с выходом direct. На правиле с прокси-выходом она бесполезна, а с выходом, который до первых данных ничего не шлёт (Shadowsocks, VMess и VLESS без TLS), поддельный ClientHello уйдёт на прокси-сервер первым пакетом его соединения — так же, как в upstream; на такие правила её ставить не нужно.
  • Заголовок IP не копирует соединение целиком. Время жизни совпадает, а флаг DF у поддельного пакета снят и метка потока IPv6 нулевая.
  • Имя своё у каждого оператора. Какое имя пропускает фильтр, sing-box не знает; его задаёт пользователь.

Проверено: модульные и интеграционные тесты (root, tcpdump, все пять способов, IPv4 и IPv6) на обеих линиях; с настоящим сервером REALITY (Xray 26.3.27) через сеть с NAT — 12 из 12 запусков, все пять способов, обе линии; в контейнерах OpenWrt с настоящим podkop (его правила nft, auto_detect_interface) и бинарниками из пакетов обеих линий поддельный ClientHello выходит на сторону WAN с того же адреса и порта, что настоящий, при всех пяти способах. Не проверено на железе роутеров: TCP_REPAIR и сырой сокет на их ядрах.

На пути с блокировкой (2026-10-08, один провайдер; фильтр обрывает соединение после 25 пакетов с данными, если в первом ClientHello нет имени из его белого списка): сервер REALITY с именем не из списка, бинарники из пакетов обеих линий, собранных в тот день. Без spoof загрузка 200 КБ останавливалась на 5–11 КБ во всех 8 запусках (отпечатки safari, chrome, firefox); со spoof и именем из белого списка проходила за 1,0–1,4 с в 19 запусках из 21 и за 3,4 и 5,0 с в двух остальных (способы wrong-sequence, wrong-md5, wrong-timestamp, wrong-ack). wrong-checksum не помогал (2 из 2), имя не из списка — тоже (2 из 2).

Автосмена отпечатка uTLS (1.13, 1.14, с r13; с r17 — свой порядок и запоминание)

Фильтр, решающий по ClientHello, может отбрасывать ClientHello одного браузера и пропускать другого. Соединение TCP при этом устанавливается, ClientHello уходит и остаётся без подтверждения, а рукопожатие ждёт весь свой таймаут — на каждом соединении.

Что пробуется (с r17)

В октябре 2026 года на путях российских провайдеров ClientHello отпечатка chrome (Chrome 133 с ключом X25519MLKEM768, около 1,8 КБ) замораживается, когда к одному серверу открывается сразу много соединений, — а так podkop их и открывает: после старта, при проверках и переключениях urltest. Замер с машины лаборатории до сервера пользователя, на которого жаловались: пачка из 20 соединений с chrome — не прошло ни одно, и путь не восстановился и через минуту. Тот же Chrome 133 без ключа X25519MLKEM768 (ClientHello около 550 байт — так его шлёт sing-box без патчей под REALITY) проходил: 46–49 из 50. firefox (Firefox 148 с ключом) проходил тоже (19 из 20, 49 из 50), а safari и randomized замерзали, как chrome. Узел, который это делает, стоит между домашним роутером и первым узлом провайдера; ответ сервера для этого не нужен.

С другой стороны, сервер REALITY на Xray-core 26.9.8 и новее отвечает на ClientHello без ключа X25519MLKEM768 как постороннему — сертификатом сайта-прикрытия. Поэтому автосмена пробует по порядку:

  1. отпечаток из конфига без ключа X25519MLKEM768; отпечаток, в котором ключа нет и так (edge, ios, qq, 360, android), — как есть;
  2. firefox с ключом;
  3. отпечаток из конфига как есть.

Пункт, совпавший с предыдущим, не повторяется (с firefox в конфиге их два, с edge — тоже). Первый пункт, на ClientHello которого сервер ответил, используется дальше. Он запоминается для сервера (отпечаток из конфига, имя сервера и адрес) в cache.db, если кэш включён, и после перезапуска соединения сразу идут с ним. Выбор действует сутки, потом одно соединение снова пробует первый пункт (сервер могли обновить, путь мог измениться), остальные идут с выбранным. Пока для сервера ничего не известно, пункты пробует одно соединение, а остальные ждут его исхода, но не дольше 5 с: пачка соединений не перебирает пункты разом (сервер REALITY отказывает каждому, а путь замораживает пачку). Проба, которая идёт дольше, задержана не отпечатком (сервер не работает, соединение TCP не устанавливается) — дальше соединения идут сами. Пока автосмена отдыхает (см. ниже), никто не ждёт. Ответ сервера как постороннему переводит на следующий пункт сразу.

Список fingerprints раздела с r17 не читается; раздел с ним по-прежнему принимается.

Как замечается безответный ClientHello (с r13)

Во время рукопожатия с отпечатком uTLS sing-box следит за соединением (TCP_INFO, раз в 50 мс). Если записанное не подтверждено после двух повторов подряд (на пути с задержкой 50 мс это около секунды; жёсткий предел 5 с) и сервер ничего не прислал, соединение закрывается и устанавливается заново. Дальше так:

  • Первый пункт и пункт, который на этом пути уже работал, покидаются после двух безответных соединений подряд, второе — установленное уже после отказа первого. Один безответный ClientHello может быть секундой потерь на линии, а новое соединение ставится по пути, который только что пропустил его SYN. Пункт, ещё ни разу не сработавший, покидается после первого же безответного соединения.
  • Сработавший пункт используется дальше — отдельно для каждого локального адреса (другой адрес — другая сеть: новый начинает с выбора, запомненного для сервера, и дальше идёт своим путём) и у каждого выхода свой.
  • Без ответа остались все пункты — автосмена отдыхает. Значит, дело не в отпечатке: блокируют адрес или имя, сервер не работает, действует ограничение числа соединений, и каждая новая попытка — лишнее соединение. На время отдыха (30 с; если такие отдыхи идут один за другим — вдвое дольше каждый раз, до 10 минут) соединения устанавливаются в точности как без автосмены: одно соединение, обычный таймаут рукопожатия — с пунктом, который на этом пути работал последним (с отпечатком из конфига, если не работал никакой; тогда после отдыха список пробуется с начала).
  • Пункт, оставшийся без ответа, на следующем соединении ответ получил — отдых 30 с. Так выглядит потеря пакетов, и другой отпечаток тут не нужен. Но так же выглядит фильтр, который отбрасывает только часть ClientHello одного вида. Поэтому после трёх таких отдыхов подряд следующий безответный ClientHello сразу переводит на следующий пункт. «Подряд» — когда без ответа каждый раз остаётся один из первых десяти ClientHello, отправленных после отдыха. Пять минут без таких случаев — и счёт начинается заново.
  • Пункт, которому сервер отвечает, но отказом (ответ REALITY как постороннему, нет нужного расширения, не та версия TLS), покидается, пока ни разу не сработал; ответ REALITY как постороннему — и тогда, когда пункт на этом пути уже работал: так выглядит сервер, который обновили. Рукопожатие, прождавшее весь свой таймаут, переводит на следующий пункт, но новых попыток на этом соединении уже нет.
  • На одно соединение — не больше четырёх рукопожатий.

Как включается

По умолчанию автосмена выключена. Включает её конфигурация — раздел experimental в config.json:

"experimental": {
  "utls_fallback": { "enabled": true }
}

Go-бэкенд podkop (podkop-experimental) пишет раздел всегда: по умолчанию с "enabled": true, выключается в его настройках. podkop на shell пишет раздел, когда в его настройках включён флаг «Enable uTLS-fallback»; список fingerprints, который он передаёт, с r17 не читается. Понимает ли раздел сборка, podkop узнаёт по имени tls.utls-fallback в строке Features:.

Других выключателей у пакета нет: в /etc/config/sing-box параметров автосмены нет. Нет раздела или в нём "enabled": false — автосмены нет, поведение как у upstream. Для проверок без podkop сборка читает ещё переменную окружения SING_BOX_UTLS_FALLBACK (с r17 — любое непустое значение): она включает автосмену для конфигурации, в которой раздела нет. Если раздел есть, переменная не читается.

Журнал — уровнем WARN, кроме первой строки ниже (INFO). Одинаковая строка пишется не чаще раза в 10 с, со счётчиком пропущенных:

tls: chrome without X25519MLKEM768 is answered; it is used
tls: the handshake with 203.0.113.5:443 refused chrome without X25519MLKEM768, as a REALITY server since Xray-core v26.9.8 does, retrying with firefox
tls: firefox is answered where chrome without X25519MLKEM768 was not; it is used from now on
tls: ClientHello to 203.0.113.5:443 unanswered with chrome without X25519MLKEM768, trying it once more
tls: ClientHello to 203.0.113.5:443 unanswered with chrome without X25519MLKEM768, retrying with firefox
tls: chrome without X25519MLKEM768 is answered after its ClientHello was not: a loss on the way, or something that does not choose by the ClientHello; the fallback rests for 30s
tls: ClientHello to 203.0.113.5:443 unanswered with all of chrome without X25519MLKEM768, firefox, chrome; the fallback rests for 30s, chrome without X25519MLKEM768 is used: it worked here last
tls: chrome without X25519MLKEM768 is answered again; it is used from now on

Ограничения:

  • только Linux и только соединение, которое sing-box сам открыл как сокет TCP к серверу: не через detour к прокси-выходу (через detour к выходу direct — действует), не для hysteria2/tuic (у QUIC отпечатка uTLS нет), не для shadowtls, не вместе с tcp_fast_open;
  • замечается только ClientHello, который отброшен молча. Если фильтр отвечает сбросом соединения или пропускает рукопожатие и режет соединение позже, автосмена не срабатывает;
  • не действует вместе с kTLS и с ECH, конфигурация которого спрашивается у DNS (обе линии); с ECH, заданным списком, действует;
  • первое соединение через заблокированный пункт дольше обычного на два ожидания (на стенде 1,3 с, один раз на адрес);
  • пока первое соединение к серверу пробует пункты, остальные соединения к нему ждут его исхода, до 5 с, — при первом подключении и после перезапуска без cache.db;
  • путь, на котором соединение не проходит по другой причине (чёрная дыра PMTU, межсетевой экран, который отвечает на SYN и отбрасывает данные), сначала стоит до четырёх соединений (при двух пунктах — до трёх) вместо одного, потом автосмена отдыхает, и поведение прежнее; после отдыха проба повторяется.

Замеры (r17)

Сборки r17 обеих линий; клиент — sing-box с выходом vless REALITY или TLS и mixed-входом, каждый запрос своим соединением; пачка — столько запросов одновременно, таймаут клиента 20 с. Путь — машина лаборатории за провайдером, путь которого замораживает пачки ClientHello chrome (см. выше), до своего сервера в другой сети или до сервера пользователя.

Сервер r16 (chrome) r17 (chrome, автосмена включена)
REALITY, Xray 26.6.27 (принимает ClientHello без ключа), пачки 20 и 50 chrome без автосмены: 0 из 20, 0 из 50, путь замерзает на 40 с 18–20 из 20, 47–50 из 50; выбран chrome without X25519MLKEM768, сервер не отказал ни разу
REALITY, Xray 26.10.21 (отвергает ClientHello без ключа) chrome: 0 из 20, 0 из 50 один отказ сервера на запуск клиента, дальше firefox: 19–20 из 20, 47–49 из 50
то же, перезапуск клиента с тем же cache.db — 20 из 20, ни одного нового отказа: firefox берётся сразу
то же, 8 соединений в секунду 90 с — в каждом 10-секундном окне прошло 75–80 из 79–80
TLS (Xray 26.6.27, самоподписанный сертификат), пачки 20 и 50 6 из 20, 48 из 50 20 из 20, 48 из 50
сервер пользователя (REALITY, Xray 26.7.11), пачка 50 после трёх минут тишины 5 из 50 (автосмена r16), путь не восстановился за 120 с 48 из 50 (1.14), 47 из 50 (1.13), путь работает

Замеры прежнего списка отпечатков (r13–r16) — в истории этого файла.

DNS-запрос новой UDP-сессии (1.13, 1.14, с r13)

Запросы, которые hijack-dns перехватывает на UDP-входе, sing-box раскладывает по сессиям (адрес и порт отправителя). Пакеты сессии доставляет библиотека udpnat: обработчику, если он у сессии есть, иначе в очередь; установка обработчика забирает из очереди то, что лежит в ней в этот момент. Пакет, который udpnat ставит в очередь мгновением позже, не забирает никто: запрос остаётся без ответа и без строки в журнале. Так сессию передаёт sing-box 1.13; на линии 1.14 так же делает podkop-engine с r4 (строка «память DNS» в таблице). На линии 1.13 такими сессиями идут и запросы DNS, перехваченные tproxy; в 1.14 у tproxy другая таблица сессий.

Так терялись два вида запросов, когда перед hijack-dns нет действия sniff (например, {"inbound": "dns-in", "action": "hijack-dns"}):

  • Запрос, открывший сессию. Маршрут для новой сессии udpnat запускает раньше, чем ставит её первый пакет в очередь. С r13 маршрут сам читает этот пакет до установки обработчика.
  • Запрос, пришедший с того же порта сразу за первым (так шлют A и AAAA клиенты, которые обращаются ко входу сами). С r13 после установки обработчика очередь сессии дочитывается ещё 20 мс.

Замеры: один sing-box, четыре клиента DNS и нагрузка HTTP через прокси, 150 с; нашёл потерю прогон всего релиза под нагрузкой.

Что спрашивают Сборка Осталось без ответа
один запрос с порта, hijack-dns без sniff sing-box 1.13.21 без патчей 41 запрос из 42 306
r13 до правки 94 из 29 227 (1.13), 16 из 51 600 (1.14)
r13 0 из 56 346 (1.13), 0 из 56 591 (1.14)
один запрос с порта, sniff перед hijack-dns, как у podkop r13 0 из 54 921 (1.14)
два запроса с порта подряд, hijack-dns без sniff r13 до правки без одного ответа 112 пар из 24 805 (1.13), 87 из 30 949 (1.14)
r13 0 из 55 347 (1.13), 0 из 55 226 (1.14)
два запроса с порта подряд, sniff перед hijack-dns r13, как и до правки без одного ответа 42 пары из 43 599 (1.13), 13 из 51 803 (1.14)

Последняя строка — то, что не закрыто. После sniff сессия передаётся как раньше: первый пакет там читает сам sniff, а дочитывание очереди стоило бы каждому запросу таймера и пробуждения: около 20 мкс процессорного времени на запрос в замере на самой таблице сессий (2000 сессий в секунду), а на стенде с одним DNS — на 3, 8 и 17 % больше процессорного времени sing-box в трёх парах прогонов. Конфигурации podkop хватает и передачи как раньше: dnsmasq каждый запрос пересылает со своего порта (dnsmasq 2.93: в 45 поисках из 45 на OpenWrt 24.10 и в 45 из 45 на 25.12 запросы A и AAAA ушли к sing-box с разных портов). Пара запросов с одного порта на вход со sniff — это клиент, который обращается к такому входу сам, а на линии 1.13 ещё и клиент из LAN, чей DNS идёт через tproxy: второй запрос пары у него изредка остаётся без ответа, как и до r13.

Резервный DNS (1.13, 1.14)

Работает только вместе с podkop и включён по умолчанию. Выключается так: uci set sing-box.main.dns_fallback=0; uci commit sing-box; service podkop restart. При обновлении со старых версий параметр dns_fallback дописывается в /etc/config/sing-box сам (скрипт uci-defaults пакета), уже заданное значение не меняется.

Какие серверы. Решение принимается один раз, по конфигу sing-box: сервер резервируется, если sing-box ходит к нему обычным сокетом (без detour или с detour на direct), без привязки к интерфейсу, метки маршрутизации, адреса источника или netns, и адрес сервера — публичный или доменное имя. Не резервируются: серверы за прокси и группами outbound, серверы VPN-секций podkop («Резолвер доменов» ходит через bind_interface туннеля), локальные и частные резолверы (AdGuardHome на 127.0.0.x или в LAN, 10.x, 192.168.x, 100.64.x). Маршруты самой системы не учитываются: сервер, заведённый в VPN маршрутом, а не через секцию VPN podkop, выглядит как обычный. В логе при первом запросе к серверу: server dns-server falls back to provider DNS when it fails или no provider DNS fallback for server <тег>: <причина>.

Когда. Сервер не ответил за 3,5 с, отказал в соединении, сбросил его, ошибка TLS/HTTP — запрос повторяется у DNS провайдера. После двух таких отказов подряд сервер отставляется: запросы сразу идут провайдеру, а сервер раз в 30 с (дальше реже, до 5 минут) проверяется копией запроса; первый нормальный ответ возвращает его. SERVFAIL тоже переспрашивается у провайдера, но сервер не отставляет (обычно это ошибка одного домена). TTL ответов провайдера — не больше 60 с. Если провайдер не ответил два раза подряд, резерв для этого сервера выключается на минуту (sing-box ведёт себя как без него), потом провайдер пробуется снова. NXDOMAIN — это ответ, провайдеру он не переспрашивается.

DNS провайдера — DNS-серверы интерфейсов OpenWrt из netifd (через ubus; resolv.conf.auto, где OpenWrt сваливает DNS всех интерфейсов, не используется). Какие именно, зависит от того, привязывает ли sing-box сокеты к устройству:

  • Привязывает (auto_detect_interface или default_interface; в podkop на shell это всегда так, в Go-бэкенде — при «Output Network Interface»): DNS интерфейсов на этом устройстве. У DHCP и PPPoE это DNS интерфейсов wan и wan6 (wan_6). Туннели VPN, LAN с DNS, заданным вручную, и резервный модем — другие устройства, их DNS не берутся.
  • Не привязывает (с r16; Go-бэкенд podkop без «Output Network Interface»): DNS всех аплинков — интерфейсов, которые подняты и имеют маршрут по умолчанию (IPv4 или IPv6), кроме туннелей (WireGuard, AmneziaWG, OpenVPN и подобных), в порядке метрики этого маршрута; до 4 серверов с устройства, до 8 всего. Каждый сервер спрашивается через свой аплинк: сокет запроса привязан к устройству этого аплинка, ведь DNS провайдера обычно отвечает только из его сети. Так при переключении на резервный модем (mwan3, метрики) отвечает DNS модема. В логе: provider DNS for the fallback, each through its uplink: wan on eth0: …; modem on wwan0: ….

Список читается при первом запросе к такому серверу и заново, когда sing-box меняет выходной интерфейс или сбрасывает сеть (переключение uplink, WAN упал и поднялся, новая сессия PPPoE), когда ни один сервер не ответил, и через 30 с, если ubus не ответил. С r16 список свой у каждого экземпляра sing-box: перезагрузка конфигурации в том же процессе начинает его заново.

mwan3. mwan3 метит всё, что отправляет сам роутер, по своей политике и ищет маршрут в таблице выбранного аплинка, а за ней стоит правило unreachable. Сокет, привязанный к другому аплинку, маршрута в этой таблице не находит: ядро считает адрес назначения соседним по каналу, и запрос не доходит. Значит, запрос к DNS резервного аплинка проходит, только пока политика выбрала его же, а при политике balanced — примерно через раз. Чтобы привязанные сокеты шли по основной таблице (в ней маршруты по умолчанию всех аплинков), нужно правило раньше правил mwan3. Go-бэкенд podkop ставит его сам для своих пакетов: ip rule add priority 106 fwmark 0x200000/0x200000 oif <устройство аплинка> lookup main. Замер на стенде (DNS проводного провайдера мёртв, интернет через него жив, failover): без правила 0 ответов из 8, с правилом 8 из 8. Без этого правила проходит только случай, когда mwan3 уже перевёл трафик на резервный аплинк.

Из нескольких серверов берётся самый быстрый, как у dnsmasq: первый запрос уходит всем сразу, следующие — тому, кто ответил первым; всех снова спрашивают раз в 20 с или 50 запросов и сразу, если он не ответил. Запрос к провайдеру идёт тем же путём, что и к серверу, со своего сокета; у этих сокетов свой лимит (256), так что всплеск запросов, ждущих молчащий сервер, не мешает ответам провайдера.

Группа DNS-серверов и DNS провайдера (1.14, с r21)

Два новых типа DNS-сервера. Go-бэкенд podkop (podkop-exp с 0.8.9) пишет их, когда в списке DNS больше одной строки или есть строка «DNS провайдера»; правила, final и default_domain_resolver указывают на группу.

"dns": {
  "servers": [
    {"type": "https", "tag": "dns-1", "server": "1.1.1.1"},
    {"type": "provider", "tag": "dns-2"},
    {"type": "udp", "tag": "dns-3", "server": "77.88.8.8"},
    {"type": "group", "tag": "dns-server", "servers": ["dns-1", "dns-2", "dns-3"], "strategy": "order"}
  ]
}

provider спрашивает DNS провайдера — те же серверы и тем же путём, что резервный DNS (см. выше): из netifd, каждый через свой аплинк, самый быстрый из нескольких. Если роутер их сейчас не знает, запрос сразу получает ошибку.

group спрашивает своих членов — другие DNS-серверы конфига, кроме group, fakeip и hosts:

  • strategy: "order" (по умолчанию) — по порядку списка: следующий спрашивается, только когда предыдущие не ответили;
  • strategy: "fastest" — сначала тот, кто отвечал быстрее всех (сглаженное время ответов, новый ответ весит 1/4), неизмеренные — после измеренных, по порядку списка. Члену, чей замер старше минуты, в фоне уходит копия запроса, на который ответил другой: не больше одной копии на запрос и одной в полёте на члена.

Ответ — это NOERROR и NXDOMAIN. Другой код (SERVFAIL, REFUSED) обычно про имя, а не про сервер: спрашивается следующий, а если ответа не дал никто, возвращается первый такой код. Ошибка или таймаут — сбой, спрашивается следующий. Таймаут попытки — 4 сглаженных времени члена, не меньше 1,5 с и не больше 3 с; 3 с, пока член не измерен. У последней попытки своего таймаута нет — только срок запроса.

Член, отказавший два раза подряд, отставляется: пока есть здоровые, его не спрашивают, а копия одного запроса уходит ему в фоне через 30 с, потом каждый раз вдвое реже, до 5 минут; первый ответ возвращает его (в order — сразу первым). Если отставлены все, спрашиваются все по порядку, как здоровые. Сброс сети (смена аплинка, WAN упал и поднялся) забывает сбои и замеры. Смены состояния пишутся на уровне warn, в том числе когда их вызвал собственный запрос sing-box: server dns-1 failed 2 times in a row (…), set aside until it answers again, server dns-1 answers again.

Страховка. При включённом резервном DNS (SING_BOX_DNS_FALLBACK), если в группе нет члена provider и хотя бы один член получил бы резервный DNS сам по себе (обычный сокет к публичному адресу или имени, см. выше), запрос, на котором отказали все члены, спрашивается у DNS провайдера; пока все отставлены, провайдер спрашивается первым, а члены — пробами. Группа из одних локальных, частных или VPN-резолверов (два AdGuard Home в LAN, DNS за туннелем) страховки не получает, как не получил бы её и один такой сервер: в логе no provider DNS fallback for group dns-server: none of its servers would have it alone (…). У самих членов группы своего резервного DNS нет: кого спрашивать, решает группа.

Замер на закрытом стенде (три сервера: 80, 20 и 40 мс): order — медиана 82 мс, все ответы от первого; fastest — 22 мс, как у одного быстрого сервера, фоновые копии — 0,6 % запросов. Сервер, через который шли запросы, замолчал: ровно 2 запроса ждали около 1,5 с, через 3,2 с он отставлен, дальше ожиданий нет. Пачки по 100 запросов при одном мёртвом члене — 300 ответов из 300.

Для программ, которые встраивают sing-box: adapter.DNSGroupTransport (GroupState() — состояние членов) и adapter.DNSProviderTransport (ProviderServers()), см. adapter/dns_podkop.go.

IPv6 для имён без IPv4: dns.ipv6_if_no_ipv4 (1.13, 1.14, с r17)

Со стратегией ipv4_only sing-box отвечает на любой запрос AAAA пустым ответом. Имя, у которого есть только адреса IPv6, при этом не разрешается вовсе. podkop всегда пишет ipv4_only: FakeIP и правила файрвола у него только для IPv4. Поэтому через роутер с podkop сайт, доступный только по IPv6, не открывается, даже если IPv6 у провайдера есть и сайт не в списках (itdoginfo/podkop#251).

С опцией раздела dns

"dns": {
  "strategy": "ipv4_only",
  "ipv6_if_no_ipv4": true
}

запрос AAAA уходит к серверу, если запрос A того же имени через тот же сервер получил ответ без записей A (успешный: ошибка и NXDOMAIN не в счёт). Имя, у которого адрес IPv4 есть, получает пустой ответ на AAAA, как и раньше: устройства с двумя стеками по-прежнему ходят к нему только по IPv4. Сервер FakeIP запрос A не получает, а на AAAA не отвечает: имена из списков podkop не затронуты. Поиск адреса самим sing-box (адрес сервера выхода, действие resolve) так же берёт адреса IPv6 имени без IPv4: сервер прокси, у которого есть только IPv6, становится доступен, если IPv6 есть у роутера. Стратегия ipv4_only, заданная в правиле DNS, тоже учитывает опцию.

Запрос A идёт через кэш DNS: устройство спрашивает A и AAAA одновременно, и оба вопроса получают один ответ сервера. С отключённым кэшем на каждый вопрос AAAA уходит лишний запрос A. Без опции поведение как у upstream.

FakeIP (1.13, 1.14)

В обычном sing-box таблица FakeIP живёт в cache.db. Каждый поиск читает файл; каждое новое имя — отдельная запись в файл, которой ждёт DNS-ответ (не больше ~100 новых имён в секунду). Позиция выдачи попадает в файл только при штатной остановке: после падения адреса раздаются заново с начала диапазона, и устройство со старым адресом попадает на чужой сайт. Диапазон обходится по кругу вслепую, записи не удаляются никогда.

Здесь таблица целиком в памяти; cache.db читается один раз при старте и дальше только получает изменения.

Скорость. Имя и адрес ищутся в памяти (60 нс вместо 650). Новое имя получает ответ сразу; изменения пишет фоновый цикл — всё, что накопилось, одной транзакцией, без пауз. Пачка из 100 новых имён получает последний ответ через 2–5 мс, и с cache.db на флешке тоже (обычный sing-box — около секунды).

Запись в файл. Одна транзакция cache.db — это 20–35 КБ записи, сколько бы имён в ней ни было. Обычный sing-box делает транзакцию на каждое новое имя. Здесь всё, что пришло, пока пишется предыдущая транзакция, уходит следующей: когда имена приходят по одному, выходит 21 КБ на имя вместо 29–35; когда пачкой — 5 КБ на имя с cache.db в /tmp и 0,3 КБ на флешке (эмуляция SPI-NAND; там транзакция пишется дольше, и в неё попадает больше имён).

Срок жизни адреса. Адрес занят, пока им пользуются: считаются DNS-запрос имени и новое соединение на адрес. Сутки без использования — запись удаляется, cache.db не растёт без конца. Если в диапазоне занято больше четверти адресов (для 198.18.0.0/15 — 32 767), уходят те, которыми не пользовались дольше всех, но не использованные за последний час; жёсткий предел — вдвое больше. Новое имя берёт следующий свободный адрес, занятые пропускаются.

Падения. Позиция выдачи сохраняется с запасом в 1024 адреса. После остановки без сохранения (падение, OOM, SIGKILL) выдача продолжается за запасом, а свободные адреса внутри него час не выдаются: адрес, выданный перед самым падением, другому домену не достанется. Запись уходит из памяти только после того, как удалена из файла.

Адрес без записи. Имя, выданное за мгновение до падения, может не успеть в файл; запись могла быть удалена как неиспользуемая, а устройство адрес ещё помнит. Соединение на такой адрес идёт по имени, которое называет сам клиент: TLS SNI, HTTP Host, QUIC (нужно правило sniff, в конфиге podkop оно есть). Если клиент имени не называет, соединение отклоняется (missing fakeip record), как раньше; на чужой сайт оно не попадает. QUIC на стенде не проверялся.

Цена. Около 170 байт памяти на имя: 5 МБ на 30 000 имён. Файл cache.db при этом вдвое меньше.

Совместимость cache.db. Формат файла прежний, в нём остаётся одна таблица «адрес → имя» и позиция выдачи. Файл обычного sing-box и прежних ревизий читается как есть, имена сохраняют адреса. Если вернуться на прежнюю ревизию или обычный sing-box, файл откроется, но известным именам один раз будут выданы новые адреса (таблицы «имя → адрес» в файле больше нет).

Имена сравниваются без учёта регистра. «Flush FakeIP» в Clash API (кнопка в дашборде) очищает и память, и файл.

Память (1.13, 1.14)

Сборщик мусора Go по умолчанию даёт куче вырасти вдвое против живых данных, прежде чем запуститься. На роутере со 128 МБ под нагрузкой это доводит до OOM-killer, и вместе с sing-box падают прокси и DNS. Здесь sing-box держит лимит памяти Go (GOMEMLIMIT) равным памяти Go плюс тому, что система ещё может дать (MemAvailable, а в cgroup с лимитом — остаток до него), минус страницы собственного кода; пересчитывает его после каждой сборки мусора. Собственные выделения лимит не двигают, его сдвигают только другие процессы, когда берут или отдают память. Когда памяти мало, сборщик работает чаще и раньше отдаёт свободные страницы системе. Когда памяти много, лимит не мешает: процессор тратится так же, как без него. При старте в лог пишется memory limit <N>, following the available memory.

Задать лимит вручную или выключить:

uci set sing-box.main.memory_limit=64MiB      # фиксированный лимит
uci set sing-box.main.memory_limit=off        # без лимита
uci commit sing-box; service podkop restart

Значение передаётся как GOMEMLIMIT. Принимается off либо размер в MiB или GiB (64MiB, 1GiB); всё остальное init-скрипт пропускает с записью в лог, и лимит остаётся автоматическим. Так сделано потому, что Go с неверным GOMEMLIMIT не стартует, а число без единицы читает как байты: с r14 не принимаются и такие числа, ноль, единицы мельче MiB (B, KiB) и TiB, а число — не длиннее семи цифр. С лимитом в десятки байт сборщик мусора работал бы без остановки (до r14 такие значения проходили).

Failsafe

Служба /etc/init.d/podkop-engine слушает события procd. Одиночное падение sing-box procd перезапускает сам, в логе отмечается причина (в том числе OOM-killer). Если sing-box падает раз за разом и procd перестаёт его перезапускать, dnsmasq продолжает слать все запросы на 127.0.0.42, где уже никто не слушает, — сеть остаётся без DNS. В этот момент служба выполняет /etc/init.d/podkop stop: podkop возвращает dnsmasq к обычным серверам и убирает свои правила nft, интернет работает без прокси. В лог пишется причина и последняя ошибка sing-box. При каждом падении туда же пишется память: MemTotal и MemAvailable, сколько занимал sing-box, если его убил OOM-killer, заполнение /tmp и размер cache.db, три самых больших процесса — видно, кончилась ли память роутера (Wi-Fi, другие службы) или вырос sing-box. Запустить podkop снова — service podkop start или перезагрузка, когда причина устранена. Расход: три процесса sh и ubus subscribe; раз в 5 с проверяется, не перезапускался ли ubusd, раз в 20 с — один вызов ubus и один запрос dig (когда ответа нет — ещё один ping).

Самопроверка (с r13). procd не перезапускает sing-box, который работает, но не отвечает (взаимная блокировка, зависание), а dnsmasq продолжает слать ему все запросы — сеть без DNS, как после серии падений. Поэтому служба раз в 20 с спрашивает у sing-box через 127.0.0.42 проверочное имя podkop (fakeip.podkop.fyi; sing-box отвечает на него сам, адресом FakeIP, без обращения к серверам). После трёх запросов подряд без ответа (49–72 с тишины) процесс завершается, procd запускает его заново, и это считается падением: шесть зависаний подряд останавливают podkop так же, как шесть падений.

Чьё молчание считается:

  • процесса, который уже дал ответ самого podkop — адрес FakeIP (198.18.0.0/15). sing-box с другой конфигурацией и тот, что ещё запускается, не трогаются;
  • процесса, который procd запустил сразу после завершённого самопроверкой: у него та же конфигурация, и зависни он снова до первого ответа, он остался бы навсегда. Ему даётся время на запуск — 180 с, а когда в конфигурации много remote rule-set, больше (они скачиваются до того, как sing-box начинает слушать: 30 с на каждые пять списков, не дольше 480 с). Если sing-box остановили (procd два опроса подряд не показывает экземпляра), это правило снимается: запущенный позже — уже не тот преемник.

Что не считается:

  • молчание во время podkop list_update;
  • запрос, который не доставил сам роутер: таблица соединений заполнена на 95 % или на собственный ping 127.0.0.42 нет ответа за 3 с. Ping служит контролем, только если роутер на него уже отвечал: с net.ipv4.icmp_echo_ignore_all=1 или правилом, режущим ICMP, самопроверка работает без него и один раз пишет об этом в журнал;
  • опрос, в котором сам скрипт или его запрос опоздали: процессам роутера не хватает времени, и sing-box в том числе;
  • опрос, в котором procd не ответил, какой процесс он запустил.

После трёх таких опросов подряд в журнале появляется строка the liveness probe waits: …. PID берётся у procd, а не по имени: sing-box check и decode-link — тоже процессы sing-box. Есть ли podkop, проверяется при каждом опросе: самопроверка начинает работать без перезагрузки, в каком бы порядке ни ставились пакеты.

На стенде (OpenWrt 24.10 с 1.13 и 25.12 с 1.14, настоящий podkop):

Случай Результат
kill -STOP работающего sing-box, 5 раз на каждом роутере DNS возвращался через 56–75 с, 10 из 10
преемник остановлен до первого ответа, дважды на каждом завершён через 248–251 с после запуска, 4 из 4 (до r13-правки преемник оставался навсегда)
после завершения podkop остановлен, вручную запущен sing-box с другой конфигурацией не тронут
две трети новых потоков к 127.0.0.42 (запросы и ping) случайно отбрасываются 12 минут ни одного завершения на одном роутере, одно на другом (скрипт до этой правки — три за те же 12 минут)
остановка на 45 с с продолжением, 4 раза; 10 перезапусков podkop; 3 list_update; 3 минуты без WAN; запуск без WAN; 30 минут простоя ни одного завершения
шесть зависаний подряд procd сдаётся, podkop остановлен, DNS работает без него, причина записана в failsafe.last
проба через nslookup busybox, без dig отвечает и молчит так же, ответ podkop отличает от чужого

Отдельно проверены пакеты на свежем OpenWrt 24.10 с настоящим procd, оба пакета обеих линий: podkop-engine до podkop, после него и обновление с r12 на месте — самопроверка срабатывает и до перезагрузки, и после неё. Выключить только самопроверку: uci set sing-box.main.liveness=0; uci commit sing-box; service podkop-engine restart (вместо 0 годятся off, false, no, disabled). На железе под нагрузкой (MT7621) не проверялось; если самопроверка завершает здоровый sing-box, в журнале будет строка has not answered DNS on 127.0.0.42 for N s.

Когда failsafe останавливает podkop, причина и состояние памяти записываются ещё и в /etc/sing-box/failsafe.last (с r13): журнал живёт в памяти и теряется при перезагрузке. Дата в файле — время события; файл перезаписывается при следующем срабатывании.

procd считает падения подряд, если между ними меньше 10 минут (respawn 600 5 5), и сдаётся на шестом; более редкие падения он перезапускает каждый раз. До r12 порог был час (умолчание procd), и шесть падений с промежутками в десятки минут останавливали podkop насовсем.

С r12 служба запускается и включается независимо от порядка установки: раньше она не работала до перезагрузки, если podkop ставили после podkop-engine, и пропадала после перезагрузки у тех, кто обновлялся на месте с r1–r3. Выключить (вместе с самопроверкой: она часть этой службы): uci set sing-box.main.failsafe=0; uci commit sing-box; service podkop-engine restart.

Зеркало документации архивировано из официального репозитория GitHub · Перейти на GitHub
Дата снимка: 2026-11-13