steer — ядро маршрутизации по правилам для OpenWrt#
steer решает, какой трафик уходит в туннель, а какой идёт напрямую. Правила описываются одним
файлом — спекой (YAML или JSON), ядро превращает его в правила nftables, таблицы маршрутизации и наборы адресов —
одной атомарной транзакцией.
Написан под нижнюю границу домашних роутеров: одно процессорное ядро, десятки мегабайт памяти, overlay-раздел на 6-7 МБ. Это ядро и только ядро: оно не скачивает списки и не имеет веб-интерфейса. Если нужен интерфейс с готовым каталогом сервисов и автообновлением списков — смотрите splify2, он управляет этим ядром.
Что он умеет#
- Правила, а не «весь трафик в 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: balance120 — места в карте ядра; диапазонов портов на канал 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/, файлы выходов AmneziaWGawg/, ключи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.