Спека v2 — справочник формата#
Спека v2 — формат, который ядро читает наряду с JSON со schema: 1|2 (спека v1,
docs/contract-v1.md). Во что спека разбирается — разделы 3 и 4в
docs/architecture.md; здесь — все ключи, умолчания и отказы. Разбор — src/model/v2.c, печать (steer spec convert) — src/model/v2print.c,
стенд — tests/v2match.sh.
Формат и файл#
- YAML (читается libyaml из дерева,
src/third_party/libyaml) или JSON: JSON — это тоже YAML, и спеку v2 можно записать JSON-объектом с"version": 2. Алиасы и якоря (*имя,&имя), повтор ключа и несколько документов в одном файле — отказ (src/lib/ynode.h). version: 2наверху обязателен. Формат узнаётся по содержимому, а не по имени файла:- текст начинается с
{— это JSON: ключversionнаверху — спека v2, иначе — спека v1 с отказами v1 слово в слово; иversion, иschemaсразу — отказ; - всё остальное — YAML, то есть спека v2; YAML без
version— отказ «нет version: 2», YAML соschema— отказ «спека v1 пишется JSON-объектом».
- текст начинается с
- Файл по умолчанию —
{etc}/spec.json(роутер:/etc/steer, телефон:/data/misc/steer) или{etc}/spec.yamlрядом. Лежит одна — читается она; нет ни одной —spec.json(туда пишет управляющий слой). Лежат обе — отказ «две спеки», а не правило старшинства: какая из двух настоящая, знает только тот, кто их положил, и молча читать одну значило бы, что правки во второй не действуют. Путь, названный--spec, читается как есть — кроме самихspec.jsonиspec.yamlпо умолчанию: названный так путь значит «какая из двух лежит» (init-скрипт роутера всегда передаёт--spec /etc/steer/spec.json). Пакет роутера знает оба имени: init-скрипт (reapply), hotplug и список sysupgrade (/lib/upgrade/keep.d/steer). steer apply --spec, клиентsteer(телоapplyдемону) иctl apply(тело спеки в сокет) — всё идёт через один разбор (load_spec), поэтому тело может быть и v1 JSON, и v2 YAML/JSON.ctl applyкладёт тело в тот файл, который сейчас спека (spec.jsonилиspec.yaml), какого бы формата тело ни было.
Общие правила разбора#
- Неизвестный ключ — отказ, на любом уровне, с подсказкой, какие ключи там есть. v1 незнакомые
ключи пропускал (совместимость вперёд внутри мажора), и опечатка в ключе молча не действовала;
у v2 совместимость вперёд держит
version. - Ключ чужого вида — отказ («ключ strategy есть только у kind: zapret»).
- Каждый отказ — с местом:
файл:строка:столбец: что не так. - Перечень — список
[a, b]или одно значение:to: youtubeиto: [youtube]значат одно. - Строка — скаляр в любом стиле записи (
wg0,"wg0"). Число и true/false — только без кавычек ("5"— строка, как в JSON), по базовой схеме YAML 1.2:yes/no/on/off— строки, а не логические значения. - Имена клиентов, списков, выходов и апстримов — буквы, цифры,
_ - ., до 31 байта. У каждого раздела своё пространство имён. Занятые слова: клиентlan, списокall. - Ссылки по именам (
for,to,out,members,default,over,dns) проверяются: несуществующее имя — отказ; круг в группах и вover— отказ. - Ещё не поддерживается. То, что формат уже описывает, а ядро ещё не умеет, разбирается и
проверяется как всё остальное (и хранится в модели там, где у модели есть место), но спека
отвергается отказом
ещё не поддерживается в этой версии ядра steer: …с местом в тексте. Этот отказ звучит ПОСЛЕ всех настоящих проверок: ошибка в спеке называется раньше, чем то, чего ядро ещё не умеет. Молча не игнорируется ничего. Перечень — в конце файла.
Пример#
То, что ядро принимает сегодня (стенд tests/v2match.sh проверяет, что оно разбирается):
version: 2
lan: { devices: [br-lan] }
clients:
kids: { mac: [aa:bb:cc:dd:ee:01, aa:bb:cc:dd:ee:02] }
tv: { addr: [192.168.1.50] }
lists:
work: { prefixes_file: lists/work.lst, domains_file: lists/work.dom }
voice: { prefixes_file: lists/dc.lst, proto: udp, ports: [50000-65535] }
outputs:
wg0: { kind: interface, device: wg0 }
wg1: { kind: interface, device: wg1 }
nl: { kind: tunnel, protocol: vless, subscription: sub/nl, nodes: [3, 5], over: wg0 }
res: { kind: group, pick: order, members: [wg0, wg1], on_fail: drop }
fast: { kind: group, pick: latency, members: [nl, wg1], tolerance: 50 }
eu: { kind: group, pick: manual, members: [nl, res], default: nl }
bal: { kind: group, pick: balance, members: [nl, wg1], weights: [2, 1] }
dpi: { kind: zapret }
dns:
mode: fakeip
rules:
- { name: kids, for: [kids], to: all, out: res, scope: device }
- { name: work, to: [work], out: fast, resolve: realip }
- { name: voice, for: tv, to: voice, out: res }
- { name: bulk, for: tv, to: all, out: bal }
Разделы#
Все разделы необязательны, кроме version. Спека из одного version: 2 законна: ядро настроено и
ничего не направляет (так же, как пустая спека v1).
version#
2, числом. Другое число — отказ «version N не поддерживается».
lan — клиенты по умолчанию#
for правила без клиентов значит «клиенты по умолчанию».
| Ключ | Значение | Умолчание |
|---|---|---|
devices |
перечень устройств, с которых ядро забирает трафик клиентов | [br-lan] |
addr |
перечень адресов, подсетей или диапазонов клиентов, IPv4 и IPv6 (в v1 — from_default); из одних IPv4 — IPv6 клиентов берётся по devices |
— |
Отказы: пустой devices; имя устройства негодного состава; устройство дважды; addr рядом с
несколькими devices (клиенты описаны дважды — «уберите addr»); выход, ведущий в устройство из
devices (петля).
На роутере с Linux 5.10 и новее правила раздачи ставятся на хук ingress устройств из devices
(docs/contract-v1.md, §7). Устройство, которого нет при apply, не отказ: его трафик размечается
в prerouting, как на старом ядре, а в цепочку ingress оно попадает при следующем apply или
reload. Клиент, заданный адресом, чей трафик приходит не с устройства из devices, тоже
размечается в prerouting.
clients — именованные группы клиентов#
имя: { ключ: перечень }.
| Ключ | Значение |
|---|---|
addr |
адреса, подсети или диапазоны IPv4 и IPv6 (смесь законна; клиент из одних IPv4 по IPv6 не узнаётся — нужен адрес IPv6 или mac) |
mac |
MAC-адреса aa:bb:cc:dd:ee:ff |
uid |
только на телефоне: UID приложения 10123 или диапазон 10000-10099 |
self |
только на телефоне: true — трафик всех приложений устройства |
app |
только на телефоне: имена пакетов приложений — ещё не поддерживается (пишите uid) |
Отказы: пустой клиент; addr и mac в одном клиенте (nft не умеет «или» внутри правила — два
клиента); телефон (uid, self) вместе с клиентами раздачи; self рядом с uid; uid 0 (root);
uid/self/app на роутере.
lists — назначения#
имя: { ключ: значение }.
| Ключ | Значение |
|---|---|
srs |
перечень наборов sing-box (.srs): имена, подсети и сужение в одном файле |
prefixes_file |
перечень файлов адресов и подсетей (строка списка — как у v1; строки IPv6 идут в парный набор <группа>6) |
domains_file |
перечень файлов доменов |
proto |
tcp, udp или both (записанное умолчание — сужения нет) |
ports |
перечень портов 443 и диапазонов 50000-65535 |
all |
true — весь трафик (с сужением proto/ports, если оно нужно) |
override_port |
подмена порта назначения, 1..65535: соединение TCP и UDP к поддельному адресу имени из списка уходит на настоящий адрес с этим портом (как override_port у route-options sing-box). Только у списка имён (domains_file без prefixes_file, srs и all) и только в режиме fakeip: подмена идёт по карте fake-IP. У списка с подменой свой набор (<выход>_dom_c<N>_p<M>), соседние имена того же выхода порт не теряют |
domains, prefixes |
встроенные записи — ещё не поддерживается (вместо них — файлы) |
Путь файла — до 255 байт; относительный путь — от каталога файла спеки (lists/dc.lst рядом
с /etc/steer/spec.yaml — это /etc/steer/lists/dc.lst), спека со стандартного ввода — от
рабочего каталога. Так же читаются subscription, conf и strategy у выходов. У v1 путь
относительный от рабочего каталога ядра, поэтому steer spec convert печатает пути абсолютными.
Отказы: у списка нет источника (порты источником не являются — правило без набора адресов
забрало бы весь трафик с этими портами); all рядом с файлами; негодный порт или диапазон
(1..65535, начало не больше конца); пересекающиеся диапазоны.
outputs — куда#
имя: { kind: вид, … }. Общие ключи:
| Ключ | У кого | Значение | Умолчание |
|---|---|---|---|
kind |
у всех | вид выхода (ниже) | обязателен |
on_fail |
у выхода с устройством, группы, zapret, tgws | drop, direct, zapret (не на телефоне) |
drop |
over |
у выхода со своим соединением с сервером | выход-подложка, через который идёт трафик самого туннеля (в v1 — via) |
напрямую |
device |
interface, tunnel, xsteer, awg | устройство | у interface обязательно; у остальных выводится из имени |
ipv6 |
routed, nat — interface, awg; off — ещё zapret и группа |
IPv6 от хоста на том конце туннеля (ниже, «IPv6 от хоста») | ключа нет — выход несёт IPv6, если его несёт вид |
prefix |
выход с ipv6: routed |
префикс хоста, 2001:db8:1::/56 (длина 1..64, без битов хоста) |
выводится из адресов LAN |
Виды и их ключи (проверяет ключи сам вид — kind_ops.parse, один разбор на оба формата):
kind |
Ключи | Что это |
|---|---|---|
direct |
— | не маршрутизировать: трафик идёт обычным путём |
interface |
device, obfs: { mode, server, listen } |
устройство системы (wireguard, pppoe…); obfs — WireGuard поверх поддельного TCP: server и listen — адрес:порт литералами, mode — wg-over-tcp (умолчание) |
tunnel |
protocol: vless, hysteria2, trojan, shadowsocks, socks, http или vmess; subscription, nodes, device, transport, insecure, exclude, exclude_name, active, by, interval, silence |
туннель по подписке; subscription — файл подписки (v1: sub_file), nodes — номера пригодных узлов по предпочтению (пусто — первый рабочий); transport — какими транспортами узлов ходить (ниже; только у vless); exclude, exclude_name — какие узлы не брать (ниже; у всех протоколов); active, by, interval, silence — сколько узлов работают сразу, как делятся соединения и как клиент следит за узлами (ниже, «Пул узлов туннеля») |
xsteer |
conf, stream, stream_port, device |
xsteer; conf — файл в стиле wg (умолчание {etc}/xsteer/<имя>.conf) |
awg |
conf, device |
AmneziaWG, устройство заводит ядро; conf — умолчание {etc}/awg/<имя>.conf |
zapret |
strategy |
обход DPI через nfqws; strategy — файл ключей nfqws (v1: opts_file, умолчание {etc}/zapret/<имя>.opts); не на телефоне |
tgws |
domain |
мост Telegram; domain обязателен; on_fail — только drop |
group |
members, pick, default, tolerance, interval, url, idle_timeout, weights |
группа выходов (ниже) |
Почему VLESS пишется kind: tunnel, protocol: vless, а не kind: vless: туннель поверх потоков —
общий стек, а протокол — его дайлер (docs/architecture.md, «Туннели: стек, дайлер, транспорт»).
kind: vless в v2 — отказ с подсказкой. xsteer туннелем в этом смысле не является (везёт пакеты,
а не потоки) и остаётся своим видом.
protocol: hysteria2 — клиент hysteria2 (QUIC), только с пакетом steer-hysteria2 (в steer-extended
он не входит; без него спека отвергается словами «kind hysteria2 требует пакет steer-hysteria2»).
Ключи те же, что у vless, кроме transport: у hysteria2 транспорта нет, и ключ — ошибка разбора.
Узлы подписки — ссылки hysteria2:// / hy2:// либо конфиг Xray-core
(docs/hysteria2.md). В v1 тот же выход пишется kind: hysteria2.
protocol: trojan, shadowsocks, socks, http, vmess — клиенты протоколов прокси
(podkop/forkop поверх steer через коннектор sing-box), только с пакетом steer-proxy (в
steer-extended он не входит; без него — «kind … требует пакет steer-proxy»). Узлы из подписки:
стандартные share-ссылки trojan://, ss://, socks*://, http(s)://, vmess://
(docs/proxy.md). Ключа transport нет (транспорт берётся из ссылки узла); insecure
есть у trojan, vmess и http (есть TLS), у ss и socks его нет.
transport туннеля — tcp, grpc, xhttp, ws, httpupgrade, одно имя или список
(transport: ws, transport: [ws, httpupgrade]); ключа нет — любые. Это фильтр узлов
подписки, а не замена их транспорта: транспорт — свойство входа на сервере, и узлы с ним приходят
из подписки (type= ссылки, network конфига Xray). Нужен тому, у кого панель отдаёт один сервер
в нескольких видах: transport: ws держит туннель на узлах через CDN и после обновления
подписки, когда номера уехали. Вместе с nodes — пересечение, номера при этом те же (среди всех
пригодных, как печатает steer vless-nodes); не осталось ни одного узла — туннель не
поднимается и говорит «среди выбранных узлов нет ни одного с транспортом из transport», а не
уходит на узел другого транспорта. steer spec convert печатает одно имя строкой, несколько —
списком в порядке имён выше. Отказы: незнакомое имя, имя дважды, пустой список, ключ у другого
вида. Параметры самих транспортов (path, host, заголовки) — у узла в подписке
(docs/vless.md, «ws и httpupgrade»). Постквантовые поля узла — encryption (шифрование
VLESS) и pqv (ключ проверки подписи ML-DSA-65 у Reality) — тоже приходят из подписки
(docs/vless.md, «Постквантовая часть»); в спеке для них ключей нет, а помощник
перезапускается при их изменении, потому что подпись помощника включает содержимое файла подписки.
insecure туннеля vless — true: не проверять сертификат узлов security=tls этого выхода
(цепочку, имя, срок, закрепления; подпись CertificateVerify остаётся). По умолчанию выключен;
узлы подписки с allowInsecure без этого ключа пропускаются с причиной. Пока ключ включён,
steer diag предупреждает, а steer status печатает "insecure":true. У hysteria2 ключа нет
(там insecure — параметр ссылки узла), у других видов — ошибка разбора. Закрепление сертификата
(pcs) и имена проверки (vcn) — поля узла в подписке (docs/vless.md, «Проверка
сертификата узла»).
exclude и exclude_name туннеля — какие узлы подписки не брать в кандидаты, у всех протоколов
туннеля. exclude — страны: код из двух заглавных латинских букв (ISO 3166-1), одно значение или
список (exclude: RU, exclude: [RU, US]). Страна узла — флаг-эмодзи в его имени: первая пара подряд
идущих символов regional indicator где угодно в имени («🇳🇱 Амстердам» → NL), так же, как страну по
имени понимает интерфейс splify2. Узел без флага страны не имеет, и exclude его не исключает;
названия стран словами ядро не читает. exclude_name — куски имени узла без учёта регистра латиницы
и кириллицы (exclude_name: [Мобильный, LTE]): узел, в имени которого есть любой из них, не берётся.
Исключение называет свойство узла, а не номер, поэтому переживает обновление подписки: новые узлы
исключённой страны тоже не берутся, в том числе при пустом nodes («первый рабочий»). Это только
отбор: номера узлов от него не сдвигаются (они по-прежнему среди всех пригодных, как печатает
steer vless-nodes), вместе с nodes и transport — пересечение. Не осталось ни одного кандидата —
туннель не поднимается и говорит «все … узлов-кандидатов исключены exclude или exclude_name», а не
перебирает исключённые. Узел, названный явно (*-probe --node N), проверяется и исключённым. В
перечне узлов (*-nodes) у узла — cc (страна по флагу) и, у выхода, "excluded":true у исключённых
(contract-v1.md, раздел 6). steer spec convert печатает оба ключа списками в
порядке записи. Отказы: код не из двух заглавных латинских букв («ru», «RUS»), код или кусок
дважды, пустая строка, ключ у другого вида. Умение названо в features status (exclude).
Пул узлов туннеля — active, by, interval, silence у kind: tunnel:
outputs:
nl: { kind: tunnel, protocol: vless, subscription: sub/nl.txt, active: 3, by: site }
| Ключ | Значение | Умолчание |
|---|---|---|
active |
сколько узлов из кандидатов (nodes, transport — порядок предпочтения) работают сразу, 1..65536; больше, чем узлов в nodes, — отказ; кандидатов в подписке меньше — активны все |
1 |
by |
как новые соединения делятся между активными узлами: connection — каждое на случайный живой узел (поровну), site — по адресу назначения (сайт на одном узле), site_client — по паре «клиент, адрес»; только при active больше 1 |
connection |
interval |
период проверки каждого активного узла, секунды, 5..86400 | 60 |
silence |
порог молчания узла на живом соединении, секунды: 0 — порога нет, иначе 5..32767 | 20 (у hysteria2 — 30) |
Клиент держит active живых узлов сразу; соединение получает узел при открытии и остаётся на нём до
конца. Имена значений by — те же, что у by группы balance, а адрес назначения — тот, что в
пакете (при fake-IP — уже настоящий адрес сайта). Каждый активный узел проверяется раз в interval
той же проверкой, по которой выбран при подъёме; две неудачи подряд (после обрыва соединения по
silence — одна: узел и так молчал дольше порога) — узел мёртв: соединения через
него сбрасываются (RST клиенту — приложение переподключается сразу, на живой узел), а на его место
берётся следующий свободный кандидат по порядку предпочтения — в том же процессе, без перезапуска и
не трогая соединения живых узлов. Замены нет — круг через 15 с (сперва прежний узел, потом свободные
кандидаты), с каждым пустым кругом вдвое дольше, до 5 минут. Демону (--supervise) клиент говорит
down, только когда живых узлов не осталось, и up, когда живой нашёлся
(docs/ctl.md, «Сторож выходов»).
silence — молчание узла под живым соединением: отправленное узлу не подтверждено им дольше порога
(узел умер под загрузкой, звонком, ТСПУ режет поток после N КБ) или от узла ничего нет дольше порога
и он не отвечает на проверку TCP keepalive (умер под закачкой). Такое соединение ядро обрывает,
клиенту уходит RST, а слежке — повод проверить узел сразу, не дожидаясь interval. Долгий опрос и
простаивающее соединение не страдают: живой узел подтверждает принятое и отвечает на проверку сам,
даже когда приложению отвечать ещё нечего. Плата — одна пустая проверка TCP за порог простоя на
соединение. У hysteria2 silence — срок простоя QUIC (PING — втрое чаще срока, но не реже 10 с), а
silence: 0 оставляет прежние 30 с: соединению QUIC срок простоя нужен всегда.
Где что есть: у vless и протоколов прокси (trojan, shadowsocks, socks, http, vmess) — все четыре
ключа; у hysteria2 — только interval и silence (соединение с узлом у него одно QUIC на все потоки,
и active больше 1 или by — отказ). Какие узлы активны сейчас — steer status (объект vless или
proxy у выхода: active — номера и имена) и helper демона (поле active,
docs/ctl.md); steer diag предупреждает, пока активных меньше active. steer spec
convert печатает только отличное от умолчания. Отказы: active вне 1..65536 или больше узлов в
nodes; by не из трёх имён или при active: 1; interval вне 5..86400; silence 1..4 («меньше
5 с: столько длится повторная передача на медленной линии, и живые соединения обрывались бы») или
больше 32767 (предел ядра для keepalive); ключ у вида не tunnel. Умение названо в features
status (active_nodes).
Группа (kind: group) — выход, у которого вместо устройства члены:
| Ключ | Значение | Умолчание |
|---|---|---|
members |
перечень выходов с устройством (в том числе других групп), по предпочтению | обязателен |
pick |
order — первый живой; latency — самый быстрый с допуском (urltest); manual — выбор человека командой select; balance — ядро раскидывает новые соединения по живым членам |
order |
tolerance |
pick: latency: допуск в мс, 0..60000 |
умолчание сторожа (50) |
interval |
pick: latency: интервал замера в секундах, 5..86400 (у демона — свой таймер группы, и интервал может быть короче периода сторожа) |
умолчание сторожа (180) |
url |
pick: latency: адрес проверки — http://… или https://… (https — в пакете steer-core; в статической сборке без TLS, микропакете tgws, — отказ разбора) |
http://cp.cloudflare.com/generate_204 |
idle_timeout |
pick: latency: сколько секунд без трафика через группу замер не делается, 0..86400; 0 — мерить всегда |
телефон — 1800, роутер — 0 |
default |
pick: manual: член до первой команды select |
первый |
weights |
pick: balance: вес каждого члена по порядку members, 1..100 — доля новых соединений |
все по 1 |
by |
pick: balance: как раздаются новые соединения — connection (каждое само по себе, случайно по весам), site (один сайт — один член), site_client (сайт у одного устройства — на одном члене) |
connection |
Как выбирает каждый pick:
order— первый живой член; возврат на более предпочтительный — после нескольких проходов подряд, когда он отвечает (гистерезис сторожа).latency— задержка члена меряется как urltest sing-box: запросGETкurlчерез члена (сокет с меткой члена — его правило и таблица, тот же путь, что у трафика группы, включая туннель VLESS и цепочкиover), время до первого байта ответа 204 или 200. Мерятся живые члены раз вinterval; выбирается самый быстрый, а из тех, кто хуже него не больше чем наtolerance, — самый предпочтительный по порядку; с живого текущего группа уходит только за выигрыш большеtolerance. Замер идёт своим таймером группы раз вinterval, а не с проходом сторожа, и сторож переключает группу сразу, как только замер меняет выбор (docs/ctl.md, «Сторож выходов»). Пока через группу нет трафика дольшеidle_timeout(по счётчикам её правил), проверочных запросов нет вовсе. Имя изurlразрешает системный резолвер. Выбор члена один на оба семейства: если все живые члены несут IPv6, замер идёт и по IPv4 (запись A), и по IPv6 (AAAA, сокет IPv6 с меткой члена), и у члена берётся худший из двух — член, быстрый по IPv4, но медленный или глухой по IPv6, не выигрывает; если по IPv6 не ответил никто (у адреса нет AAAA, туннели не выпускают IPv6), — по IPv4. У группы, где хоть один член IPv6 не несёт (VLESS), и у пулаdevicesспеки v1 — только по IPv4. Литерал IPv6 вurlне принимается.manual— член, выбранный командойselect <группа> <член>(docs/ctl.md), без apply; выбор переживает перезапуск и перезагрузку (файлselectрядом со спекой). Не работает выбранный член — группа применяет свойon_fail, а не переходит на другой: manual — выбор человека. При запуске это действует сразу, до первого прохода сторожа: таблица группы — на выбранном члене или, если его устройства нет,on_fail(docs/ctl.md, «Сторож выходов», «До первого прохода»).-
balance— ядро раздаёт НОВЫЕ соединения по живым членам по весам (карта из 120 слотов), член соединения запоминается в метке соединения, и соединение остаётся на своём члене. Упавший член сторож убирает из карты (только карту, без перезагрузки набора правил); живых нет —on_failгруппы. IPv6 раздаётся так же, если его несут все члены; если хоть один не несёт (VLESS), IPv6 правил группы отвергается, как у выхода без IPv6. Для правил на сам телефон (uid,self) — ещё не поддерживается. Слот нового соединения выбираетby(режимыload-balanceу Clash):connection— случайно (numgen random): каждое новое соединение само по себе, и соединения одного сайта расходятся по членам;site— хеш адреса назначения (jhash ip daddr, у IPv6 —ip6 daddr; consistent-hashing): все соединения к сайту — через один член, у любого клиента. Сайт — адрес назначения до подмены fake-IP: у канала с fake-IP это имя (у каждого имени свой поддельный адрес), без fake-IP — адрес, и имя на нескольких адресах может попасть на разные члены;site_client— хеш пары «адрес клиента, адрес назначения» (sticky-sessions): сайт у одного устройства — через один член, у другого устройства — возможно, через другой.
Хеш постоянный: тот же сайт попадает на тот же член и после
apply, и после перезагрузки (семя хеша — номер таблицы группы). Упал член — его сайты расходятся по живым по весам, а сайты живых членов остаются на своих местах; член вернулся — его сайты снова на нём (установленные соединения при этом остаются там, где начались).
Вложенность. order, latency и manual разворачивают выбор до листа: трафик группы идёт в
устройство, которое сейчас несёт трафик выбранного члена (у вложенной группы — её текущий лист). У
balance вложенная группа — один член со своим весом: её собственный выбор (order, manual)
сохраняется внутри её доли, а вложенная balance делит свою долю дальше по своим весам и своему
by: сайт остаётся на одном выходе до самого листа, только если by: site (или site_client) у
всех balance на пути; хеш у каждой группы свой, поэтому вложенная группа делит сайты своей доли
по всем своим членам.
Отказы: члена нет; группа в себе самой; член дважды; член без устройства (direct, zapret, tgws);
круг групп; default не из членов или не у manual; tolerance/interval/url/idle_timeout не
у latency; url не http(s)://хост[:порт][/путь], https:// в сборке без TLS; weights не у
balance или не по числу членов; by не у balance или не connection/site/site_client; группа balance членом группы order/latency/manual (у такой
группы трафик идёт в одно устройство, а у balance устройство на каждое соединение своё); device у
группы.
Отказы вида (текст — вида, место — выхода): у interface нет device; у tunnel нет subscription;
путь conf/strategy с кавычкой; stream_port без stream; имя устройства awg
длиннее 15 символов или выдаёт туннель; у tgws нет domain. Сквозные (как у v1): выход в
локальное устройство; устройство дважды в группе; over у вида без своего соединения с сервером,
на выход без устройства, на себя, круг (в том числе через устройства членов группы), цепочка
длиннее трёх.
IPv6 от хоста (ipv6 у выхода). Хост — сервер на том конце туннеля WireGuard, у
которого IPv6 есть; роутер без своего IPv6 у провайдера получает IPv6 через него, внутри самого
туннеля. Сторону хоста человек настраивает сам (обычный сервер WireGuard с IPv6), раздачу IPv6 в
LAN — тоже сам, в netifd и odhcpd; steer маршрутизирует и в diag говорит, чего не хватает (id
ipv6_host).
outputs:
host: { kind: interface, device: wg1, ipv6: routed, prefix: "2001:db8:1::/56" }
other: { kind: interface, device: wg2 }
routed— хост маршрутизует на пира префикс (или отвечает за адреса из него NDP proxy), и у клиентов LAN адреса из него. Роутер:option ip6prefix '2001:db8:1::/56'у интерфейса туннеля иoption ip6assign '64'у LAN в/etc/config/network— netifd выдаст LAN кусок префикса, odhcpd раздаст его клиентам. IPv6 клиентов с адресом из префикса идёт по правилам: правила в этот выход («донор») ведут IPv6 в него; у правил в любые другие выходы (и вdirect) IPv6 отвергается сразу (ICMPv6 «administratively prohibited» — клиент переходит на IPv4), а имена под ними получают пустой AAAA; всё несовпавшее IPv6 из префикса — в донора. Адрес из префикса не уходит ни в другой выход, ни в WAN никогда (другой сервер отбросил бы его по AllowedIPs, провайдер — по BCP38): такой пакет отвергается, в каком бы направлении его ни увёл маршрут. Донор лёг — IPv6 отвергается сразу (запретprohibitв его таблице), а не уходит напрямую. Донор в спеке один.prefix— необязателен: без него префикс выводится из адресов LAN — нуль-маршрутunreachable P, который netifd ставит на каждый раздаваемый префикс, накрывающий глобальный адрес устройства раздачи; префикс провайдера (с маршрутомdefault from Pчерез WAN) не берётся; двух кандидатов ядро не угадывает — тогда нуженprefix. Выведенный префикс сторож переписывает сам, когда netifd его выдаёт или меняет. ULA-префикс хоста (хост сам делает NAT66 для ULA) — толькоprefixявно. Клиенты с ULA рядом с префиксом ходят к поддельным адресам fake-IP (fdfe:…) со своего ULA, и такой IPv6 в донора отвергается (хост его не пропустит) — доменные правила fake-IP в донора для них идут по IPv4;resolve: realipили LAN только с префиксом хоста (ip6class) это снимают.nat— у пира один адрес IPv6, клиенты LAN на ULA: masquerade IPv6 на устройство выхода ставит само ядро steer, в своей таблице (зоны fw4 не трогаются; masq6 зоны для этого не нужен), и IPv6 идёт по правилам, как IPv4. Роутер: у интерфейса туннеля — адрес IPv6, выданный хостом; если у LAN только ULA —option ra_default '1'в секции dhcp LAN, иначе odhcpd не объявит маршрут по умолчанию.off— выход IPv6 не несёт, хотя вид умеет: IPv6 его правил отвергается, AAAA имён под ними пуст.- Ключа нет — выход несёт IPv6, если его несёт вид. Рядом с донором выход без ключа IPv6 не несёт
(как
off);natрядом с донором несёт IPv6 клиентов с ULA, а адрес из префикса и туда не уходит. - Телефон. Раздачей IPv6 там владеет Android (Tethering):
routedиnatразбираются, но действуют как отсутствие ключа —applyиdiagговорят об этом;offдействует.
Отказы: неизвестный режим; routed/nat у группы («задайте у члена») или у вида без туннеля;
ipv6 у вида без IPv6 (tunnel, xsteer, tgws, direct); prefix без routed; префикс с битами хоста,
длиннее /64, служебный (link-local, multicast, пул fake-IP); второй routed в спеке.
dns#
| Ключ | Значение | Умолчание |
|---|---|---|
mode |
fakeip или realip — режим доменных правил без своего resolve; оба семейства: на AAAA имени под правилом — поддельный IPv6 из пула fdfe:dcba:9876::/96 (fakeip) или настоящий адрес в наборе IPv6 правила (realip), если правило ведёт в выход с IPv6 (interface, awg, direct, группа из таких) и его клиенты узнаются по IPv6 (см. ниже) |
fakeip |
traceroute_hops |
true — трассировка через fake-IP видит настоящие узлы (в v1 — traceroute_hops верхнего уровня) |
false |
cache |
записей в кэше ответов на имена под правилами (и на имена вне правил, когда задан other); 0 — кэша нет |
0 |
cache_ttl |
{ min, max, negative } — секунд: ответ живёт не меньше min и не дольше max, отрицательный (NXDOMAIN, пустой) — negative. TTL в ответе клиенту зажат теми же пределами и уменьшается на возраст записи |
{ min: 10, max: 3600, negative: 30 } |
upstreams |
имя: { url, out, ips, bootstrap } — серверы DNS, или имя: { servers, mode } — группа серверов, см. ниже |
— |
upstream |
имя из upstreams (сервер или группа) — общий сервер для имён под правилами, у которых своего нет |
нет: прежний путь наверх |
other |
имя из upstreams (сервер или группа) — сервер для имён, не попавших ни под одно правило, см. ниже |
нет: прежний путь наверх |
bootstrap |
до четырёх адресов серверов обычного DNS, которыми разрешается имя сервера DoT/DoH/DoQ | — |
Апстримы. url выбирает транспорт: https://имя[:порт][/путь] — DoH (RFC 8484, POST
application/dns-message, HTTP/1.1 с keep-alive, путь по умолчанию /dns-query); tls://имя[:порт]
— DoT (853); quic://имя[:порт] — DoQ (RFC 9250, UDP, порт 853, ALPN doq); udp://адрес[:порт] и
tcp://адрес[:порт] — обычный DNS (у udp усечённый ответ повторяется по TCP); адрес IPv6 пишется в
скобках. Для udp:// и tcp:// нужен адрес, не имя. Другой записи DoQ (doq://) нет.
DoQ — одно долгоживущее соединение QUIC на апстрим, каждый вопрос идёт своим потоком, поэтому
потеря пакета задерживает только тот вопрос, чей ответ в нём. Простаивающее соединение закрывается
через 5 минут кодом DOQ_NO_ERROR; соединение, молчащее 1,5 с после отправленного вопроса,
пересоздаётся, а вопрос уходит на новом. Билеты сессии хранятся в памяти процесса резолвера (по одному
на сервер — имя и порт, не больше восьми серверов), на диск не пишутся: со второго соединения к тому же
серверу рукопожатие идёт по билету, а первый вопрос уходит в 0-RTT (early data, RFC 9250 разрешает —
вопрос DNS идемпотентен), не дожидаясь конца рукопожатия. Если сервер 0-RTT не принял, вопросы уходят
заново на новом потоке того же соединения. STEER_DOQ_EARLY_DATA=0 в окружении резолвера выключает
билеты и 0-RTT.
out— выход, через который уходит запрос к серверу (выход со своим устройством:interface,awg,tunnelлюбого протокола,xsteer; группа, zapret и tgws не годятся); нет ключа, или выходdirect, или словоdirect— напрямую. Запрос уходит с меткой выхода, так что может идти по отдельному стабильному выходу, не тому, что у трафика правила. Выход лёг — запрос не уходит мимо него (набор правил не выпускает помеченный пакет в другое устройство), а клиент получает SERVFAIL, не прежний путь наверх.- Имя сервера DoT/DoH/DoQ разрешается не системным резолвером:
ips(адреса сервера, имя тогда не разрешается вовсе) либоbootstrapапстрима, либо общийdns.bootstrap. Bootstrap — обычный DNS по UDP, с той же меткой пути, что и апстрим; результат живёт срок TTL ответа (60-3600 с), при молчании bootstrap берутся прежние адреса. Ниips, ни bootstrap — отказ. - Сертификат сервера проверяется до корней (на роутере —
/etc/ssl/certs/ca-certificates.crt, на телефоне — системное хранилище), имя — по SNI (у DoQ — имя изurl, то же и при адресе, найденном черезipsили bootstrap). Без TLS в сборке (мини-пакет tgws, статическийsteerdбез пакета TLS) DoT и DoH не работают, без обёртки QUIC (статическая сборка безsrc/proto/quic) не работает DoQ: апстрим показывается вdns-logсо состояниемno-tls, вопросы к нему получают SERVFAIL.
Апстрим задаётся у правила (dns: имя или сразу dns: { url: …, out: … } — те же ключи) или
общий (dns.upstream). Правила с одним выходом, клиентами и режимом, но разными апстримами
остаются отдельными каналами резолвера. Имена вне правил спрашиваются, как раньше (dnsmasq на
роутере, исходный сервер запроса на телефоне), если не задан dns.other.
Группа серверов — запись upstreams с ключами servers (имена серверов из upstreams, по
порядку; число не ограничено) и mode вместо url. Группа годится везде, где имя апстрима:
dns.upstream, dns.other, dns правила (в том числе сразу в правиле:
dns: { servers: [a, b], mode: race }). Годный ответ — любой, кроме SERVFAIL и REFUSED (NXDOMAIN и
пустой ответ годны: это ответ об имени, а не отказ сервера).
mode: race— вопрос уходит всем серверам группы сразу, клиенту — первый годный ответ; отказ (SERVFAIL клиенту), только если годного не дал никто.mode: failover(умолчание) — по очереди: вопрос уходит первому; он ответил SERVFAIL/REFUSED или не принял вопрос (нет соединения, пауза после неудачи соединения) — сразу следующему; не ответил за 1,5 с — вопрос уходит и следующему, а годный ответ любого из спрошенных — ответ группы. Сервер, который отказал (SERVFAIL, REFUSED, нет ответа за 4 с, не принял вопрос), уходит на паузу 5 с, 10 с, 20 с … до 300 с: пока она идёт, он спрашивается последним, после всех серверов без паузы. Пауза кончилась — сервер снова первый в своём порядке, и следующий вопрос проверяет его; годный ответ снимает паузу, отказ удлиняет вдвое. Медленный, но отвечающий сервер паузы не получает.
У каждого сервера группы — свой путь (out), свои адреса и соединения. Отказы: url и servers
вместе; пустой servers; имя, которого нет в upstreams; имя дважды; группа в группе («перечислите
её серверы»); mode не race и не failover.
Сервер для остальных имён (dns.other). Имена, не попавшие ни под одно правило, резолвер
спрашивает этим сервером или группой, а не прежним путём. Имена своей сети остаются у DNS роутера:
имя без точки (имя устройства из DHCP), домены lan, local, home.arpa, internal, localhost,
localdomain, home и обратные зоны in-addr.arpa, ip6.arpa. Сервер не ответил (нет ответа за
4 с, нет соединения) — вопрос уходит прежним путём, клиент получает ответ DNS роутера, а следующие
имена вне правил идут прежним путём сразу, пока длится пауза (5 с, 10 с … до 300 с, как у группы);
после паузы сервер снова спрашивается первым. Ответ SERVFAIL или REFUSED — этот вопрос тоже уходит
прежним путём, но без паузы. Так интернет не пропадает вместе с сервером. Кэш (dns.cache) держит и
ответы other. Без other — как раньше. На телефоне прежний путь — сервер, к которому шёл запрос
приложения.
dns:
cache: 2048
bootstrap: [1.1.1.1, 8.8.8.8]
upstream: doh3
other: g
upstreams:
g: { url: https://dns.google/dns-query, out: dnsvpn }
dot: { url: tls://one.one.one.one, ips: [1.1.1.1, 1.0.0.1] }
q9: { url: https://dns.quad9.net/dns-query, ips: [9.9.9.9] }
cf: { url: https://cloudflare-dns.com/dns-query, ips: [1.1.1.1] }
doh3: { servers: [q9, cf, g], mode: failover }
rules:
- { name: work, to: [work], out: eu, dns: dot }
- { name: yt, to: [youtube], out: dpi } # спрашивает doh3
- { name: ai, to: [ai], out: eu, dns: { servers: [q9, cf], mode: race } }
# имена вне правил — g; router, nas.lan — DNS роутера
steer explain <имя> называет под ответом строкой DNS:, каким сервером или группой ответил
резолвер; dns-log — то же у каждого имени (поле dns), состояние групп и other (docs/ctl.md).
IPv6 доменных правил. Клиенты правила узнаются по IPv6, когда они — lan (клиенты по
умолчанию: IPv6 берётся по devices), устройства, MAC, адреса IPv6 или приложения телефона;
клиент из одних адресов IPv4 по IPv6 не узнаётся. У правила без IPv6 (выход tunnel любого
протокола, xsteer, tgws, группа с таким членом, или клиент из одних IPv4) на AAAA его имён — пустой ответ, и
клиент с двумя стеками сразу идёт по IPv4. Имя, совпавшее с несколькими правилами, получает AAAA,
только если IPv6 есть у всех них: иначе поддельный или настоящий IPv6 у клиентов правила без
IPv6 ушёл бы мимо его выхода. fakeip на старом ядре без nat для IPv6 (4.9 у части телефонов) —
только IPv4, realip — оба семейства. На HTTPS и SVCB для имён под правилом ответ пустой. Поддельный IPv6 — пара поддельного IPv4 того же имени (fdfe:dcba:9876::c612:5 —
198.18.0.5); steer explain по такому адресу называет правило и имя.
rules — сверху вниз, выше — сильнее#
Список отображений.
| Ключ | Значение | Умолчание |
|---|---|---|
name |
подпись: в status и журнале резолвера; по-русски можно, кавычку нельзя; до 31 байта | rule-<номер> |
for |
клиенты из clients; lan — клиенты по умолчанию |
lan |
to |
списки из lists; all — весь трафик |
обязателен |
out |
выход или группа | обязателен |
resolve |
fakeip или realip |
dns.mode |
dns |
имя апстрима (сервера или группы) из dns.upstreams или отображение { url, out, ips, bootstrap } либо { servers, mode } |
dns.upstream |
enabled |
false — правило лежит в спеке, но не действует |
true |
scope |
device — правило на одно устройство, старше глобальных по построению (в v1 — scope канала); global |
global |
to: all — явное «весь трафик»; согласия allow_all, как у v1, не нужно: слово all пишут
нарочно, опечаткой оно не получается.
Сменили правилу out (или сняли, выключили правило) — после применения его установленные
соединения прежнего выхода снимаются, и приложения соединяются заново уже через новый выход:
иначе они жили бы на прежнем до своего конца (у группы balance, где прежний выход — член, —
держались бы на нём). Правило узнаётся по name, поэтому переименование вместе со сменой выхода
читается как новое правило и снятое прежнее. Подробно — docs/ctl.md, apply, «Смена выхода
правила».
Несколько клиентов или списков у правила компилятор пока ведёт одним клиентом и одним списком,
поэтому разбор сводит их, когда это не меняет смысла: клиенты одного вида (все адреса, все MAC,
все UID) — в одного клиента, списки с одним сужением и без all — в один список, ровно как v1
пишет несколько файлов в одном канале. Не сводится (адреса с MAC, списки с разными proto/ports)
— ещё не поддерживается: разнесите по правилам.
Отказы: нет out или to; ссылка на несуществующее имя; имя дважды в for/to; lan или all
рядом с другими; у включённого правила на устройство — нет клиентов или у клиента подсеть,
диапазон или диапазон UID (приоритет достался бы не одному устройству); правило на сам телефон в
выход только для клиентов раздачи (tgws). Выключенное правило по смыслу не проверяется (иначе
сломанное правило нельзя выключить, только удалить), но имена в нём обязаны существовать.
steer spec convert#
steer spec convert [--spec ФАЙЛ] читает спеку (v1 или v2) и печатает её спекой v2 в стиле
примера раздела 3. Файл спеки не меняется. Напечатанное даёт тот же набор правил, что исходная
спека v1, до байта — это проверяет tests/v2match.sh на всех спеках v1 из стендов генератора.
Ответы резолвера после перевода меняются в одном: у спеки v1 на AAAA имени под доменным правилом ответ всегда пустой, а у напечатанной спеки v2 — поддельный или настоящий IPv6 там, где это разрешают выход и клиенты правила (см. «IPv6 доменных правил» выше). Если туннель на той стороне IPv6 не везёт, после перевода клиент с двумя стеками сперва пробует IPv6.
Что придумывается при переводе — у v1 этого нет:
- клиент и список канала получают имя канала, если оно годится в идентификатор (иначе
rule<номер>); список «весь трафик» без сужения —to: all, с сужением — списокall: true; - выход v1 с
devices(пул) — группаpick: order(илиlatencyприprefer: latency, сtolerance/interval) из выходовkind: interfaceс именами<пул>.<устройство>. Они печатаются после всех выходов спеки: реестр раздаёт метки по порядку выходов, и прежние выходы сохраняют прежние метки. В v2 это обычные выходы — со своей меткой и таблицей; via—over;sub_file—subscription;opts_file—strategy;node: N—nodes: [N];from_default—lan.addr;lan_device(s)—lan.devices;mode: realip—resolve: realip;traceroute_hops—dns.traceroute_hops;- умолчания, которые вид выводит из имени выхода (путь
conf/strategy, имя устройства tunnel, xsteer и awg), не печатаются.
Ещё не поддерживается#
Спека с этим отвергается ещё не поддерживается в этой версии ядра steer: …:
| Что | Чем выразить сейчас |
|---|---|
pick: balance для правил на сам телефон (uid, self) |
группой order, latency или manual |
встроенные domains/prefixes списка |
файлами: domains_file, prefixes_file, srs |
app клиента |
uid |
| клиенты разных видов или списки с разными сужениями в одном правиле | разнести по правилам |