splify2
Ядро steer

steer — ядро маршрутизации по правилам для OpenWrt#

steer решает, какой трафик уходит в туннель, а какой идёт напрямую. Правила описываются одним файлом — спекой (YAML или JSON), ядро превращает его в правила nftables, таблицы маршрутизации и наборы адресов — одной атомарной транзакцией.

Написан под нижнюю границу домашних роутеров: одно процессорное ядро, десятки мегабайт памяти, overlay-раздел на 6-7 МБ. Это ядро и только ядро: оно не скачивает списки и не имеет веб-интерфейса. Если нужен интерфейс с готовым каталогом сервисов и автообновлением списков — смотрите splify2, он управляет этим ядром.

Telegram Поддержать проект

Что он умеет#

  • Правила, а не «весь трафик в VPN». Канал описывает, какой трафик и от каких клиентов уходит в какой выход. Каналы проверяются сверху вниз, побеждает первый совпавший.
  • Маршрутизация по доменам, а не только по адресам. Встроенный резолвер dnsd подменяет ответ на служебный адрес из диапазона 198.18.0.0/15, а ядро возвращает настоящий через DNAT. Это точнее списков адресов: один адрес CDN обслуживает сотни сайтов, и по адресу их не различить. У правил спеки v2 может быть свой сервер DNS — DoT, DoH, DoQ, обычный UDP/TCP, напрямую или через выбранный выход, — группа из нескольких серверов (все сразу или по очереди с переключением), свой сервер для имён вне правил с запасным путём на DNS роутера и кэш ответов (dns в docs/spec-v2.md).
  • Экономия памяти. Национальный блок-лист в сотни тысяч префиксов не влезает в роутер как есть. steer fit ужимает его до заданного числа элементов набора, объединяя соседние префиксы по плотности, и честно сообщает, чего это стоило.
  • Отказоустойчивость. У выхода может быть список устройств в порядке предпочтения; сторож проверяет их и переключает трафик на первое живое. Когда не работает ни одно — поведение задаётся вами, а по умолчанию трафик останавливается, а не уходит в открытый интернет.
  • Свой клиент VLESS/Reality (пакет steer-vless) с транспортами tcp, grpc, xhttp, ws, httpupgrade, потоком Vision и собственным TUN. Несёт и TCP, и UDP, поэтому через такой выход работают QUIC (HTTP/3), WireGuard/WARP и игровой трафик. Ядро steer-core с библиотеками и криптографией вместе с модулями VLESS, xsteer, обфускатора и моста Telegram — около 3,1 МБ под mipsel_24kc без сжатия (пакеты .apk вместе — около 1,3 МБ; цифры сборки 2.0.0, без steer-hysteria2 и steer-proxy); Xray или sing-box занимают 20-40 МБ и на такой роутер не встают.
  • WireGuard поверх поддельного TCP для сетей, где UDP режут или пропускают по белому списку протоколов. Серверная половина ставится на VPS, см. server/.

Установка#

Возьмите пакет своей архитектуры из ветки dist или со страницы релизов. Содержимое одинаковое; ветка на зеркале нужна тем, у кого провайдер закрыл githubusercontent.com — прямая ссылка релиза ведёт именно туда и не откроется. Архитектура в имени файла — та же, что показывает DISTRIB_ARCH в /etc/openwrt_release.

Пакетов на архитектуру восемь: ядро steer-core, шесть модулей и мета-пакет steer-extended. Модуль без ядра не ставится, а файлы пакетов менеджер разрешает по зависимостям только тогда, когда видит их все сразу, поэтому ядро и нужные модули одной версии и одной архитектуры ставятся одной командой.

# OpenWrt 25.12 и новее (менеджер пакетов apk): ядро и нужные модули
apk add --allow-untrusted ./steer-core-<версия>-1_<арх>.apk ./steer-vless-<версия>-1_<арх>.apk

# OpenWrt 24.10, 23.05 и 22.03 (менеджер пакетов opkg)
opkg install ./steer-core-<версия>-1_<арх>.ipk ./steer-vless-<версия>-1_<арх>.ipk

Что за пакеты:

Пакет Что это
steer-core Ядро и общая криптография: маршрутизация, сторож, резолвер, steerd, steer, init-скрипт, библиотеки libsteer.so и libsteer-wolfssl.so (модель спеки, TLS, транспорты, криптография). Нужен всегда; туннель может быть уже готов — WireGuard, AmneziaWG, что угодно. Заменяет пакеты steer, libsteer и libsteer-wolfssl прежней раскладки: при установке они снимаются.
steer-vless Клиент VLESS/Reality: если туннель должен поднимать само ядро — ссылка подписки или отдельные ссылки vless://.
steer-xsteer Клиент звезды xsteer.
steer-obfs Обфускатор: WireGuard поверх поддельного TCP.
steer-tgws Мост Telegram.
steer-hysteria2 Клиент hysteria2 (QUIC, Brutal, Salamander, прыжки по портам): ссылки hysteria2:// или конфиг Xray-core в подписке (docs/hysteria2.md). В steer-extended не входит — ставится отдельно.
steer-proxy Клиенты trojan, shadowsocks, socks, http и vmess (docs/proxy.md). В steer-extended не входит — ставится отдельно.
steer-extended Устаревший мета-пакет: steer-core и первые четыре модуля разом (без steer-hysteria2 и steer-proxy). Взамен ставятся steer-core и нужные модули.

Модуль обязан быть той же версии, что ядро (зависимость steer-core (= версия)): менеджер пакетов не даст обновить одно без другого, а если файлы поставлены мимо него, демон отвергнет модуль другой версии и скажет об этом в журнале и в status. Выход, которому нужен модуль (VLESS — kind: tunnel, protocol: vless, в v1 kind: vless; hysteria2, прокси, xsteer), без своего пакета спека не принимает — с отказом «kind … требует пакет steer-…».

Клиентам VLESS, xsteer, hysteria2 и прокси нужен kmod-tun: без него туннель не поднимется, и ядро скажет об этом прямо.

Быстрый старт#

Спека лежит в /etc/steer/spec.yaml (спека v2, docs/spec-v2.md) или в /etc/steer/spec.json. Пример ниже и раздел «Настройка: spec.json» описывают прежний формат — спеку v1 (JSON со schema): ядро 2.0 читает её и переводит в v2. Минимальная конфигурация — «список адресов в туннель, всё остальное напрямую»:

{
  "schema": 1,
  "lan_devices": ["br-lan"],
  "outputs": {
    "vpn":    { "kind": "interface", "device": "wg0", "on_fail": "drop" },
    "direct": { "kind": "direct" }
  },
  "channels": [
    { "name": "блоклист",
      "match": { "prefixes_files": ["/etc/steer/lists/blocked.lst"] },
      "out": "vpn" }
  ]
}
steer apply --dry-run          # посмотреть, что получится, ничего не применяя
steer apply                    # применить
steer status                   # что стоит сейчас
steer diag                     # проверки состояния: что сломано и почему
steer explain example.org      # какой канал и выход достанутся этому имени

Списки — обычные текстовые файлы, по записи на строку. Адресные: 10.0.0.0/8, 1.2.3.4 (это то же, что /32), диапазон 10.0.0.1-10.0.0.9. Доменные: example.org, *.example.org, =exact.example.org (только это имя), re: с регулярным выражением. Пустые строки и строки, начинающиеся с # или ;, пропускаются.

Ядро списки не скачивает: оно читает то, что ему положили. Это сознательное разделение — скачивание, расписания и выбор источника принадлежат управляющему слою.

Настройка: spec.json#

Полное описание формата, ограничений и инвариантов — docs/contract-v1.md. Здесь — то, что нужно в первую очередь.

Верхний уровень#

Поле Значение
schema Версия формата, обязательно. 1 или 2. В схеме 2 у канала есть proto, ports и scope (см. ниже). Незнакомое число — отказ загрузки: скомпилировать в правила фаервола настройку, которой ядро не понимает, хуже, чем не применить ничего.
lan_devices Устройства, с которых берётся трафик клиентов. По умолчанию один br-lan. Список — потому что роутер бывает выходной точкой не только для своего моста: хостам из Tailscale или ZeroTier полагаются те же правила, что домашним. lan_device строкой — сокращение для списка из одного.
from_default Подсети клиентов по умолчанию — для каналов, где from не задан. Способ описать тех же клиентов адресом вместо устройства; вместе с несколькими lan_devices не задаётся.
outputs Выходы: куда можно направить трафик.
channels Правила: какой трафик куда идёт. Порядок значим.
traceroute_hops Необязательно. Делает узлы в traceroute осмысленными; требует правила в чужом firewall, поэтому по умолчанию выключено — ядро скажет, чего не хватает.

Не только устройства из LAN#

Роутер часто служит выходной точкой не одному мосту: к домашним устройствам добавляются хосты из Tailscale или ZeroTier, для которых он шлюз. Перечислите их интерфейсы, и те же каналы начнут работать и для них:

"lan_devices": ["br-lan", "tailscale0", "ztrq4abcde"]

Выбор идёт по устройству, а не по подсети, и это важно: у tailscale0 адрес на роутере обычно /32, то есть подсеть пиров из него не вывести вовсе. Устройства, которого сейчас нет, ядро не пугается — правило начнёт работать само, когда интерфейс поднимется, а steer diag тем временем скажет, что такого устройства на роутере нет.

Имена выходов и устройств могут состоять только из латиницы, цифр, _, - и точки: они подставляются в командные строки и в имена наборов nftables, и парсер отвергает всё остальное при загрузке. Имена каналов — подписи для человека, в них можно писать по-русски; нельзя только кавычку, обратную косую и управляющие символы.

Выходы#

"outputs": {
  "vpn":      { "kind": "interface", "devices": ["awg0", "wg0"], "on_fail": "drop" },
  "vless_out":{ "kind": "vless", "sub_file": "/etc/steer/sub.txt", "on_fail": "zapret" },
  "direct":   { "kind": "direct" }
}
  • kind — interface (уже существующее устройство), vless (ядро поднимает туннель само) или direct (без туннеля).
  • device / devices — имя устройства или список в порядке предпочтения. Список — это приоритет: сторож берёт первое живое. Задан один — второе выводится, так что дальше по коду путь один.
  • on_fail — что делать, когда не работает ни одно устройство выхода:

    Значение Поведение
    drop (по умолчанию) Трафик останавливается. Канал заводят именно для того, чтобы трафик НЕ шёл напрямую; молча вернуть его на открытый путь в момент поломки — значит нарушить единственное обещание выхода ровно тогда, когда это опаснее всего.
    direct Трафик идёт напрямую, как обычный трафик роутера: общий обход DPI (zapret), если он запущен, его разбирает.
    zapret Напрямую, но ядро проверяет, что обход DPI действительно запущен.
  • sub_file — только для kind: vless: файл с узлами подписки.

  • node — номер узла подписки. -1 (по умолчанию) означает «первый рабочий»: выбор делает проверка при подъёме, а не человек, угадывающий номер.
  • obfs — WireGuard поверх поддельного TCP, только для kind: interface. Поля: server и listen — адрес:порт литералами ("203.0.113.10:4567", "127.0.0.1:51820"; listen должен совпадать с Endpoint пира WireGuard), mode — wg-over-tcp (умолчание).

Каналы#

{ "name": "телевизор",
  "from": ["192.168.1.50"],
  "match": { "domains_files": ["/etc/steer/lists/tv.lst"], "mode": "fakeip" },
  "out": "vpn" }
  • name — подпись. Она же уходит в status, чтобы у счётчика было имя.
  • from — адреса или MAC-адреса клиентов. Либо адреса, либо MAC — смешивать нельзя: адрес меняется, MAC живёт, и для ядра это два разных условия. Чтобы охватить и хост, и MAC, заведите два канала.
  • enabled — false выключает правило целиком: ни набора, ни правила в цепочке, и проверки его пропускают. Удобно, чтобы отключить правило на вечер, не удаляя его вместе с выбранными списками.
  • out — имя выхода.
  • match — условия:
    • prefixes_files — файлы со списками адресов;
    • domains_files — файлы со списками доменов (работает через dnsd);
    • srs_files — наборы правил sing-box (.srs) как есть: имена из них берёт dnsd, подсети — набор nftables, сужение по протоколу и портам применяется само (см. ниже);
    • mode — режим резолвера для доменов, fakeip (по умолчанию) или realip, см. ниже;
    • any вместе с allow_all — забрать весь трафик указанных клиентов. Оба поля обязательны: один any отвергается, потому что это почти всегда описка, а её последствие — клиенты теряют и роутер, и DNS, то есть чинить придётся с провода;
    • proto — схема 2: tcp, udp или both. Поля нет — протокол не критерий, и both значит ровно то же;
    • ports — схема 2: порты назначения, массив строк вида "443" или "50000-65535" (тире, как у диапазона адресов в списках). Не больше 16 записей, пересечения отвергаются.

Рядом с match, на самом канале: "scope": "device" — схема 2, правило на одного хозяина из from, старше глобальных по построению (см. ниже).

Адреса и домены в одном канале — можно: набор один, просто заполняется с двух сторон.

Правило на устройство (схема 2)#

"scope": "device" рядом с from из одного хозяина делает правило старше любого глобального — и старше по построению, а не по месту в списке.

Так записываются две вещи, которые иначе не выразить надёжно: «весь трафик этого телефона в туннель» (out туннельного выхода плюс any) и «этот ноутбук не маршрутизируем вовсе» (out прямого выхода плюс any).

Приоритет — это порядок совпадения: правило заканчивается return, поэтому побеждает первое совпавшее. Пока порядок читался только со спеки, исключение для телефона работало ровно до тех пор, пока человек держал его выше глобальных: добавил новое глобальное правило первым — и исключение перестало действовать, молча. С признаком scope компилятор проходит спеку дважды и кладёт устройства первыми; внутри каждой из двух половин порядок спеки сохраняется.

Требования к такому правилу: непустой from, и в нём только одиночные хозяева — адрес, адрес с /32 или MAC. Подсеть отвергается: приоритет достался бы не одному устройству, а всем в ней, и достался бы тихо — снаружи правило выглядит точно так же. Пустой from отвергается тем же доводом в крайней форме.

any у такого правила законен БЕЗ allow_all: общий запрет заведён против описки «весь трафик в туннель», у которой цена — все клиенты без интернета и починка с провода. У правила на одного хозяина цена ошибки — один хозяин, и он же её и заметит.

MAC надёжнее адреса: адрес по DHCP сменится, MAC — нет. В правило он попадает как ether saddr; смешивать адреса и MAC в одном from нельзя (nft не умеет «или» внутри правила), и это отказ, а не половина правила.

Протокол и порты (схема 2)#

Ради одного случая, которого иначе не выразить: discord у itdoginfo/allow-domains — это не домены, а 104.16.0.0/12 плюс udp 50000-65535, 19000-20000. Подсеть эта — Cloudflare, и взять её без портов значит увести в туннель весь TCP к Cloudflare молча.

{ "name": "discord", "out": "vpn",
  "match": { "prefixes_files": ["/etc/steer/lists/discord.lst"],
             "proto": "udp", "ports": ["50000-65535", "19000-20000"] } }

Порты не источник совпадения: канал из одних портов, без списка адресов или any, отвергается — правило без ip daddr безусловно, то есть накрыло бы этими портами весь интернет.

Схема поднята до 2 именно потому, что порты совпадение сужают. Незнакомый ключ ядро пропускает, и для расширяющих полей это правильно; здесь пропуск означал бы «сузить забыли». Поэтому ядро постарше обязано отвергнуть такую спеку целиком — что оно и делает, а управляющий слой видит код 2 при проверке через apply --dry-run и просит обновить ядро.

Наборы sing-box (.srs)#

Списки itdoginfo/allow-domains и геоданные sing-box публикуются двоичными .srs. Канал читает их сам — раскладывать в .lst не нужно:

{ "name": "discord", "out": "vpn", "match": { "srs_files": ["/etc/steer/lists/discord.srs"] } }

Сужение у правил набора своё (у discord.srs имена без сужения, подсети — только udp и порты голоса), и ядро применяет его само: одно сужение на весь список — обычный набор с meta l4proto … th dport …, смешанное — один составной набор адрес . протокол . порты на канал (на старом ядре — по набору на вариант сужения). Логические правила, исключения, source_ip_cidr и package_name (на телефоне) тоже выражаются; то, что к платформе неприменимо (процессы, Wi-Fi), снимается с предупреждением. Подробно — docs/contract-v1.md, «Наборы sing-box». steer srs-read печатает набор нашим синтаксисом списка — посмотреть, что в нём.

fakeip или realip#

fakeip (по умолчанию) realip
Как работает Резолвер выдаёт клиенту служебный адрес из 198.18.0.0/15, ядро подменяет его на настоящий через DNAT Резолвер отдаёт настоящий ответ апстрима и кладёт этот адрес в набор канала
Точность Пер-домен, максимальная Теряется там, где два домена живут на одном адресе
traceroute Все узлы показывают поддельный адрес: ядро переписывает ICMP-ошибки так, будто они пришли с того адреса, к которому обращался клиент Узлы читаются нормально

Берите fakeip, пока не понадобится читаемый traceroute.

Команды#

В пакете ядра три имени. steerd — демон, компилятор, применение, резолвер и инструменты; помощников выходов поднимают модули — бинарники steer-vless, steer-xsteer, steer-obfs, steer-tgws, steer-hysteria2, steer-proxy рядом с ним, а команды модулей (vless, sub-fetch, xsteer-key, obfs и другие) steerd передаёт им сам, с тем же выводом и кодом возврата. steer — маленький клиент: команды, на которые отвечает демон (status, diag, explain, conns, dns-log, apply, reload, subscribe), он посылает в сокет демона и печатает ответ с тем же выводом и кодом возврата, что у одноимённой подкоманды ядра; всё остальное — и любую команду, когда демон не запущен, — исполняет steerd. steer-tools — ссылка на steerd, которая отвечает только на инструменты (fit, srs-read, obfs-server, sub-fetch, dev-id, tls-probe, xsteer-key, xsteer-link и соседние).

Службу держит /etc/init.d/steer: procd запускает один процесс steerd daemon --watch --supervise --apply. Он применяет спеку при старте, сторожит выходы, держит помощников выходов и резолвер. /etc/init.d/steer reload (и reload_dnsd, reload_zapret, reapply) просит демон перечитать спеку, и он трогает только изменившееся. stop гасит демон и снимает правила.

Ядро само себя документирует:

steer help                 # список команд по разделам
steer help apply           # что делает команда и какие у неё флаги
steer apply --help         # то же самое
steer --version            # версия, вариант сборки и ревизия (git describe той сборки)
Команда Что делает
apply [--dry-run] Скомпилировать спеку в правила. --dry-run печатает готовый ruleset и ничего не применяет.
status Применённое состояние: выходы, выбранные устройства, каналы, счётчики. JSON.
diag Проверки состояния: таблица, наборы, устройства, резолвер, маскарад, обфускация. JSON.
explain <адрес\|имя> Какой канал поймает адрес и в какой выход он уйдёт. Отвечает на вопрос «почему этот сайт идёт не туда».
reload Попросить демон перечитать спеку и применить только изменившееся. Без демона — отказ с кодом 3.
subscribe Печатать события демона по мере прихода: спека применена, выход переключён, помощник поднялся или упал.
daemon [--watch] [--supervise] [--apply] Сам демон: сокет управления, сторож выходов, помощники и резолвер детьми, применение при старте. Протокол — docs/ctl.md.
outputs [--kind K] [--obfs] Перечислить выходы.
needs-dnsd Кодом возврата: нужен ли резолвер этой спеке.
zapret-instances Выходы kind: zapret: имя, номер очереди nfqueue, файл ключей nfqws — через табуляцию.
steer-nfqws Q ФАЙЛ Отдельная команда, не подкоманда: обёртка, которую демон поднимает на каждый выход kind: zapret. Смена стратегии — это /etc/init.d/steer reload_zapret: демон перезапускает обработчик только того выхода, чей файл изменился.
dnsd Сам резолвер. Работает на переднем плане; в службе его держит демон.
failover Один проход сторожа: выбрать живое устройство для каждого выхода. В службе сторожит демон.
fit [файл] Подогнать список префиксов под память.
vless <выход> Поднять TUN и клиент VLESS/Reality. Только с пакетом steer-vless.
vless-nodes <выход\|/файл> [--insecure] Узлы подписки выхода или файла подписки, JSON. --insecure — только с файлом: в перечень входят и узлы с allowInsecure, с теми же номерами, что у выхода с insecure: true.
vless-probe <выход\|/файл> [--node N] [--timeout С] [--insecure] Проверить узел и замерить задержку. --insecure — как у vless-nodes.
hysteria2 <выход>, hysteria2-nodes <выход\|/файл>, hysteria2-probe <выход\|/файл> То же для выхода protocol: hysteria2. Только с пакетом steer-hysteria2. --insecure принимается и ничего не меняет: узлы с insecure=1 входят в перечень всегда.
proxy <выход>, proxy-nodes <выход\|/файл>, proxy-probe <выход\|/файл> То же для выходов trojan, shadowsocks, socks, http и vmess. Только с пакетом steer-proxy. --insecure — как у vless-nodes.
sub-fetch <ссылка> --out ФАЙЛ Скачать подписку и разобрать, что ответила панель: пригодные узлы, название, остаток трафика, слово про устройство. JSON.
sub-quota <ссылка> --info ФАЙЛ Спросить у панели один остаток трафика, не подменяя подписку. JSON.
sub-hwid Идентификатор этого роутера для панели подписки. JSON.
obfs <выход> WireGuard поверх поддельного TCP, клиентская половина.
obfs-server --listen ПОРТ --forward АДРЕС:ПОРТ Серверная половина, для VPS.

Общие флаги: --spec ФАЙЛ (по умолчанию /etc/steer/spec.json) и --state-dir КАТАЛОГ (по умолчанию /var/lib/steer) принимает каждая команда, читающая спеку.

status запоминает свой ответ в состоянии, а status --fast отдаёт запомненное немедленно, не разбирая спеку и не спрашивая ни /sys, ни nft: полный ответ стоит около 90 мс на роутере, и он же нужен первым при открытии окна управления. Запомненный ответ несёт "cached": true, а время сборки стоит в at у любого ответа — память нельзя рисовать живой. Снимок раз в пять минут освежает и демон; apply его снимает, потому что он описывает то, что было применено до транзакции.

Разбор аргументов строгий: неизвестный флаг, флаг, которого команда не понимает, потерянное значение и не-число там, где ждали число, — это отказ с кодом 2, а не молчание. Опечатка в --dry-run не применит правила по-настоящему.

Коды возврата и разделение потоков описаны в docs/contract-v1.md §6. Коротко: 0 — сделано, 2 — ядро отказалось, а 1 зависит от команды и означает «отрицательный ответ» у diag, needs-dnsd и fit, но настоящий провал у apply.

Подгонка списков под память#

steer fit --budget 20000 --report /tmp/fit.json < ru.lst > ru-fitted.lst

Фиттер объединяет соседние префиксы по плотности: если внутри /24 лежат хотя бы два адреса из списка, вся сеть заменяется одним элементом. Отчёт говорит, сколько адресов при этом добавилось лишних (waste_addresses) и влезло ли вообще (fits).

Если не влезает — по умолчанию список выходит целиком с пометкой fits:false, а не обрезается: тихая дыра над одним адресом хуже честного отказа. Обрезать хвост разрешает --truncate.

--exclude принимает список адресов, которые не должны попасть в результат. --punch-out вместо отказа от объединения выписывает пересечения в отдельный файл — их держат отдельным каналом с более высоким приоритетом. На национальном блок-листе против исключения в 44 тысячи префиксов это разница между 14 665 элементами (не влезает) и 10 225 плюс 443 исключения.

Диагностика#

steer diag отвечает JSON со списком проверок и вердиктами ok, note, warn, fail. Код возврата 1 означает, что хотя бы одна проверка — fail; JSON при этом полный и валидный.

steer explain <адрес|имя> называет канал и выход. Это первое, что стоит спросить, когда сайт идёт не туда.

Ядро steer пишет в журнал с префиксом уровня — steer[warn] или steer[info], — и классифицировать строки следует по префиксу, а не по тексту: формулировки меняются между версиями. За префиксом идёт метка подсистемы: apply, failover, dnsd, obfs, tunnel. Отказы тому, кто позвал ядро (негодная спека, плохой аргумент), уровня не несут: это ответ вызывающему, а не запись в журнал.

Что случается чаще всего#

Домены не маршрутизируются, а адресные списки работают. Резолвер не запущен или не перечитал спеку. steer diag назовёт это; needs-dnsd отвечает, нужен ли он вообще.

Публичный резолвер внутри списка канала. Списки по номеру автономной системы забирают всё, что живёт в той же AS, поэтому 8.8.8.0/24 приезжает в категорию «Google», а 1.1.1.0/24 — в Cloudflare. Такой адрес уводит в туннель DNS клиентов, которые ходят напрямую к публичному резолверу. diag находит это и называет и резолвер, и файл.

DNS-сервер внутри LAN перестал получать запросы. Загружен br_netfilter и включён net.bridge.bridge-nf-call-iptables — тогда кадры, ходящие внутри моста, проходят через ip-хуки netfilter, и перенаправление порта 53 забирает даже запросы клиента к Pi-hole на той же сети: правило смотрит на клиента, а не на получателя. diag называет это (bridge_nf). Само ядро steer настройку не трогает — она общесистемная, её ставят docker и libvirt.

Трафик уходит в туннель и не возвращается. У выхода нет NAT. diag проверяет наличие маскарада для устройства и говорит об этом отдельной строкой — само правило ядро steer не ставит, это чужая конфигурация.

Через туннель работает всё, кроме больших пакетов. MTU. Особенно при обфускации, см. server/README.md.

Канал заведён, а трафик им не идёт. Проверьте порядок: побеждает первый совпавший канал. И посмотрите from — если клиента там нет, правило его не касается.

Пинг через интерфейс идёт, а каналы через него молчат. Разъехалась политическая маршрутизация выхода, а не туннель: у выхода своя таблица и своя метка, и пинг с роутера в эту таблицу не заглядывает вовсе. Смотреть надо ip rule show | grep fwmark и ip route show table N (номер таблицы — в /var/lib/steer/registry): в таблице должен лежать default dev <выход>, а не blackhole default и не пусто. Сторож сверяет это каждый проход и возвращает сам, сообщая в журнал строкой «маршрутизация разъехалась»; перезапускать ядро для этого не нужно.

Обфускация: WireGuard поверх TCP#

Для сетей, где UDP режут. Клиентская половина — в ядре, серверная ставится на VPS из архива steer-obfs-<версия>-<арх>.tar.gz со страницы релизов или из ветки dist-vps — она нужна там, где закрыт githubusercontent.com, ровно как ветка dist для пакетов роутера:

tar xzf steer-obfs-<версия>-x86_64.tar.gz
cd obfs-x86_64 && sudo sh install.sh --port 4567 --forward 127.0.0.1:51820

Бинарник в архиве статический и собран из того же кода обфускации, что модуль steer-obfs в пакете для роутера (там этот код разложен по libsteer.so и модулю), — компилятор на сервере не нужен. Подробности, требования к firewall и расчёт MTU — server/README.md.

Формат на проводе совместим с phantun: с той стороны может стоять phantun_server.

Этот слой не шифрует (это делает WireGuard) и не защищает от целенаправленного зондирования: поток без ретрансмиссий отличим от настоящего TCP при анализе. Задача — пройти там, где UDP режут, а не спрятаться от исследователя.

Сборка из исходников#

make                      # нативная сборка для разработки
make test                 # весь набор стендов, офлайн: без роутера, без сети, без nft
make ext-test             # стенды расширенной части на настоящем wolfSSL (скачивается со сверкой суммы)
./build.sh                # пакеты всех архитектур (нужен Docker)
make libs-test            # разделяемая раскладка (библиотеки и модули) на хосте, на настоящем wolfSSL

Роутерные сборки — musl через кросс-тулчейн zig. Ядро и модули — динамические бинарники: загрузчик musl (/lib/ld-musl-<арх>.so.1) берётся из самой прошивки, а libsteer.so и libsteer-wolfssl.so лежат в пакете steer-core. Версию libc под релиз OpenWrt подбирать не нужно, но нужен musl 1.2 и новее (OpenWrt 22.03 и новее): на 32-битных архитектурах бинарники берут из libc символы времени 64 бит (__clock_gettime64 и соседи), которых в musl 1.1 нет.

./build.sh кладёт в out/ артефакты двух видов: .apk и .ipk для каждой архитектуры (восемь пакетов, перечислены в разделе «Установка») и архивы steer-obfs и steer-hub для x86_64 и aarch64 — статические.

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

Криптография — wolfSSL, собранная из исходников выпуска со своими опциями: версия и контрольная сумма архива — в build/wolfssl/fetch.sh, опции — в build/wolfssl/user_settings.h. В пакете роутера это libsteer-wolfssl.so внутри steer-core (steerd ходит по HTTPS сам); в статической базовой сборке (микропакет, архив для VPS) криптографии нет.

Лицензия#

steer распространяется под GNU GPL v3. Сторонний код: libyaml (src/third_party/libyaml, лицензия MIT), wolfSSL и ngtcp2 (лицензия MIT, с нашим патчем Brutal), которые в дереве не лежат, а скачиваются и собираются в расширенный пакет из исходников (wolfSSL — GPLv3; версии и суммы — build/wolfssl/fetch.sh, build/ngtcp2/fetch.sh).

Документация#

  • docs/contract-v1.md — формат спеки, формат состояния, ограничения ввода, контракт командной строки и журнала, инварианты. Это то, на что опирается управляющий слой.
  • docs/spec-v2.md — спека v2 в YAML: все ключи, умолчания и отказы.
  • docs/ctl.md — управляющий сокет демона: команды, события, сторож, помощники.
  • docs/architecture.md — устройство ядра для тех, кто его правит.
  • docs/vless.md — устройство встроенного клиента VLESS/Reality: транспорты, Reality, Vision, UDP и QUIC, задержка установления соединения, коды возврата.
  • docs/hysteria2.md — клиент hysteria2: формат узла, Brutal, Salamander и Gecko, прыжки по портам, слежка за узлом.
  • docs/proxy.md — клиенты trojan, shadowsocks, socks, http и vmess: форматы ссылок, что поддерживается и что нет.
  • docs/xsteer.md — свой протокол с обликом TLS: формат на проводе, рукопожатие Noise IK внутри ClientHello, пачки кадров, согласование MTU, конфигурация в стиле wg и список того, чего протокол не делает.
  • server/README.md — серверная половина обфускации.

Известные ограничения#

  • Числа в спеке. Выходов, каналов, правил, клиентов, списков, членов группы, узлов подписки и апстримов — сколько поместится в память: заранее заложенных «не больше N» у них нет. Пределы с настоящей причиной остались, и отказ называет число: выходов с меткой 211 на роутере (43 на телефоне) — размер поля метки; членов группы pick: balance 120 — места в карте ядра; диапазонов портов на канал 16; спека — 16 МиБ, тело apply по сокету — 1 МиБ, каталог списков — 4096 файлов и 128 МиБ.
  • Настройки переживают обновление прошивки, сам пакет — нет. Пакет объявляет свои настройки системе файлом /lib/upgrade/keep.d/steer, поэтому sysupgrade и «Создать архив» в LuCI их берут. В списке десять путей: спека (spec.json или spec.yaml), выбор групп select, sub.txt, sub.userinfo, каталог подписок subs/, свои списки lists/custom/, файлы выходов AmneziaWG awg/, ключи xsteer/ и стратегии выходов обхода zapret/. Каталог в keep.d берётся рекурсивно — sysupgrade кормит эти пути в find, — поэтому подкаталог объявляется одной строкой.

    Чего в списке нет намеренно: сам каталог /etc/steer/lists (десятки файлов, сотни килобайт) — отсутствующий список не мешает применению, и управляющий слой скачает его по расписанию. Исключение внутри него — lists/custom/: свой список набивает человек, и перекачать его неоткуда.

    Самого пакета в архиве нет и быть не может: пакетов проекта нет в feeds OpenWrt, а sysupgrade сохраняет файлы, не пакеты. После обновления прошивки ядро steer надо поставить заново — в splify2 это одна кнопка, — и оно найдёт свои настройки на месте. - ICMP через туннель VLESS не идёт. Успешный ping через прокси не означал бы работающий путь, поэтому всё, что не TCP и не UDP, получает ICMP «порт недостижим». - IPv6 — не у всех выходов. Подсети IPv6 из списков, «весь трафик» и доменные каналы идут в выходы interface и awg своей таблицей IPv6 (для WireGuard в зоне fw4 нужен masq6). На AAAA для имени под правилом резолвер отвечает поддельным IPv6 (режим fake-IP) или настоящим адресом (real-ip) только у спеки v2 (docs/spec-v2.md); у спеки v1 (прежний формат JSON со schema) ответ на AAAA такого имени пустой, и клиент идёт по IPv4. У туннелей ядра (VLESS, hysteria2, прокси), xsteer и моста Telegram IPv6 нет: IPv6 их правил отбрасывается, на AAAA их имён ответ пустой, и клиент переходит на IPv4. На старом ядре без nat для IPv6 (4.9 у части телефонов) fake-IP работает только по IPv4.


Нужен готовый интерфейс с каталогом сервисов и автообновлением списков — splify2.