Skip to content

Latest commit

 

History

History
607 lines (490 loc) · 58.2 KB

guide.md

File metadata and controls

607 lines (490 loc) · 58.2 KB

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

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

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

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

Что он умеет

  • Правила, а не «весь трафик в VPN». Канал описывает, какой трафик и от каких клиентов уходит в какой выход. Каналы проверяются сверху вниз, побеждает первый совпавший.
  • Маршрутизация по доменам, а не только по адресам. Встроенный резолвер dnsd подменяет ответ на служебный адрес из диапазона 198.18.0.0/15 (адреса давно не спрошенных имён без соединений возвращаются в пул), а ядро возвращает настоящий через DNAT. Это точнее списков адресов: один адрес CDN обслуживает сотни сайтов, и по адресу их не различить. У правил спеки v2 может быть свой сервер DNS — DoT, DoH (в том числе по HTTP/3), 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

Обновление с 2.0.x на 2.1

steer-core 2.1.0 сам снимает пакеты steer-vless, steer-hysteria2, steer-proxy, steer-xsteer, steer-obfs, steer-warpbox, steer-tgws и steer-extended: протоколы теперь плагины, они ставятся из семени в самом пакете ядра, без сети, по видам выходов вашей спеки. Мост Telegram — тоже плагин (tgws), но из репозитория splify2-extended-plugins: в семени ядра его нет, ставьте его из вкладки «Плагины» splify2 или steer plugin install tgws (выход kind: tgws до этого — недоступен). Если прежние пакеты ставились файлами и apk отвечает breaks: world[steer-core><Q1…], назовите прежние модули в той же команде (… steer-vless steer-xsteer) или уберите закрепления способом ниже.

С коннектором (steer-box-connector, sing-box для podkop и forkop) ядро ставится вместе с пакетом steer-box-connector 2.1 одной командой (apk upgrade или оба файла в одном apk add): с 2.1 коннектор — плагин ядра box-connector, а пакет без файлов держит имя sing-box, которое требуют podkop и forkop, и ставит плагин. Одно ядро apk не поставит — пакет 2.0.x требует ядро своей версии. Подробно — в документации коннектора.

Пути, которые чужая программа ищет по известному адресу (у box-connector — /usr/bin/sing-box, /etc/init.d/sing-box), плагин объявляет в манифесте (steer.links, steer-api 1.3), а ставит их менеджер плагинов: ссылки в каталог плагина, настройки — копией. Путь, который занят файлом пакета, менеджер не перезаписывает — отказ называет пакет.

Своего экземпляра ядра у коннектора нет: sing-box run отдаёт переведённую спеку службе steer через её сокет. Чтобы правила steer не пересекались с метками podkop и forkop, плагин просит у ядра другое поле метки (0x000000ff), приоритет правил выходов, возврат решения после чужой перезаписи метки и каналы на сам роутер (steer.platform, steer-api 1.4). Менеджер плагинов пишет их в /etc/steer/plugins/platform.env и поднимает службу ядра заново; снят или выключен плагин — ядро возвращается к своему полю.

Если после неудачной установки apk отказывает во всём

apk add локального файла записывает в /etc/apk/world не просто имя пакета, а имя с контрольной суммой файла — steer-core><Q1…. Если установка оборвалась на середине (чаще всего на роутере с малым флешем: «No space left on device» при распаковке второго пакета), в этом файле остаются суммы новых файлов, а в базе — прежние пакеты. С тех пор любая команда apk, в том числе установка чужого пакета, отвечает:

ERROR: unable to select packages:
  steer-core-2.0.3-r1:
    breaks: world[steer-core><Q1…]

Поломка воспроизведена на apk 3.0.5 (его ставит OpenWrt 25.12.5). Чинится правкой самого файла (apk add имя в таком состоянии тоже отказывает): записи о пакетах, которых нет в базе, убираются, остальные теряют привязку к файлу и остаются просто именами.

awk 'FILENAME==ARGV[1]{if($0~/^C:/){c=substr($0,3);if(p!="")i[p]=c}else if($0~/^P:/){p=substr($0,3);if(c!="")i[p]=c}if($0==""){c="";p=""}next}{k=index($0,"><");if(!k){print;next}n=substr($0,1,k-1);if(n in i)print n}' /lib/apk/db/installed /etc/apk/world > /tmp/world.fix && cat /tmp/world.fix > /etc/apk/world && apk fix

После неё поставьте пакеты заново (при нехватке места — сначала освободите флеш: df -h /overlay). Установщики splify2, коннектора и Zapret Manager (части Steer) делают то же сами — до установки и после неё.

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

Пакет Что это
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-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-warpbox Cloudflare WARP выходом kind: warp: регистрация, подбор точек, замена (docs/warp.md). Нужен модуль ядра kmod-amneziawg или kmod-wireguard. В 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, warp) без своего плагина недоступен: status показывает его unavail с причиной «нужен плагин … — поставить», а apply спеку с ним не принимает с теми же словами.

Клиентам 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 — демон, компилятор, применение, резолвер и инструменты. Протоколы приходят плагинами (/usr/lib/steer/plugins/<id>/, ставит steer plugin): помощников их выходов демон поднимает программой плагина, а команды протоколов (vless-nodes, sub-fetch, xsteer-key, obfs-server и другие) объявляет манифест плагина — steerd запускает его программу с теми же словами, выводом и кодом возврата; нет плагина — нет и команды. Мост Telegram — тоже плагин (tgws, репозиторий splify2-extended-plugins). steer — маленький клиент: команды, на которые отвечает демон (status, diag, explain, conns, dns-log, apply, reload, subscribe), он посылает в сокет демона и печатает ответ с тем же выводом и кодом возврата, что у одноимённой подкоманды ядра; всё остальное — и любую команду, когда демон не запущен, — исполняет steerd. steer-tools — ссылка на steerd, которая отвечает только на инструменты (fit, srs-read, dev-id) и на команды плагинов (obfs-server, sub-fetch, 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 Для init-скрипта: кодом возврата 0 отвечает всегда (резолвер нужен всегда), отказ — на спеке, которую ядро не поняло.
dnsd Сам резолвер. Работает на переднем плане; в службе его держит демон.
failover Один проход сторожа: выбрать живое устройство для каждого выхода. В службе сторожит демон.
fit [файл] Подогнать список префиксов под память.
vless-nodes <выход|/файл> [--insecure] Плагин vless. Узлы подписки выхода или файла подписки, JSON. --insecure — только с файлом: в перечень входят и узлы с allowInsecure, с теми же номерами, что у выхода с insecure: true.
vless-probe <выход|/файл> [--node N] [--timeout С] [--insecure] Проверить узел и замерить задержку. --insecure — как у vless-nodes.
hysteria2-nodes <выход|/файл>, hysteria2-probe <выход|/файл> То же для выхода protocol: hysteria2, плагин hysteria2. --insecure принимается и ничего не меняет: узлы с insecure=1 входят в перечень всегда.
proxy-nodes <выход|/файл>, proxy-probe <выход|/файл> То же для выходов trojan, shadowsocks, socks, http и vmess, плагин proxy. --insecure — как у vless-nodes.
warp-scan <группа>, warp-reg <файл> Группа kind: warp: подбор точек без замены (JSON) и регистрация устройства WARP в файл. Плагин warpbox (docs/warp.md).
sub-fetch <ссылка> --out ФАЙЛ Плагин vless. Скачать подписку и разобрать, что ответила панель: пригодные узлы, название, остаток трафика, слово про устройство. JSON.
sub-quota <ссылка> --info ФАЙЛ Плагин vless. Спросить у панели один остаток трафика, не подменяя подписку. JSON.
sub-hwid Идентификатор этого роутера для панели подписки. JSON.
obfs-server --listen ПОРТ --forward АДРЕС:ПОРТ Плагин obfs. Серверная половина WireGuard поверх поддельного TCP, для VPS; клиентскую половину выходу interface с obfs поднимает демон.
conns Соединения, которые ведёт ядро: какой выход держит каждое (по метке conntrack).
dns-log Журнал имён: какие имена недавно спрашивали, в какой канал они попали и сколько раз.

Общие флаги: --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 и 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 назовёт это.

Публичный резолвер внутри списка канала. Списки по номеру автономной системы забирают всё, что живёт в той же 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). Для выходов interface, awg и членов kind: warp masquerade IPv4 ставит само ядро steer, в своей таблице (цепочка postrouting_nat4); выходам, которые сами завершают соединения (VLESS, hysteria2, прокси), и xsteer она не нужна. diag проверяет подмену для устройства и говорит об этом отдельной строкой.

Через туннель работает всё, кроме больших пакетов. 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/. Каталог в 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, прокси) несут IPv6 по TCP и UDP (ключ ipv6: off у выхода это отключает). У xsteer и моста Telegram IPv6 нет: IPv6 их правил отбрасывается, на AAAA их имён ответ пустой, и клиент переходит на IPv4.

  • IPv6 по правилам. Ключ правила family: 4 | 6 | both сужает правило до одного семейства: при 4 имена правила получают пустой AAAA, а IPv6 клиентов правила «весь трафик» отвергается, а не уходит напрямую; при 6 пустым отвечается A. Если IPv6 нужен, а выход его не несёт, on_family_unavailable: fallback оставляет правило по IPv4, block — закрывает имена правила совсем. Подробности и примеры — docs/spec-v2.md, раздел «Семейство IP правила».


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