splify2
xsteer

Обвязка: xs-quick и xs-install#

Клиент и хаб — это один файл xsteer, и он умеет всё сам: поставить адрес, MTU, серверы имён и маршруты, согласовать предел пути пробами, поднять устройство. Обвязка нужна не для этого. Она нужна для двух вещей, которых сам xsteer не делает намеренно, и для одной, которой он не делает по существу.

Что берёт на себя обвязка и почему не сам xsteer#

Ключи-крючки. PreUp, PostUp, PreDown, PostDown и Table разбор конфигурации отвергает, называя причину: клиент не исполняет команды из файла. Это не придирчивость — конфигурация приходит от кого угодно, и «выполни вот эту строку с правами root» в ней быть не должно. Но человеку крючки нужны: правило firewall, свой resolver, отметка в журнале. Поэтому их исполняет xs-quick, а клиенту достаётся файл, из которого они вырезаны. Ровно такое же разделение труда у wg-quick: wg setconf про PostUp тоже ничего не знает.

Слежение за процессом. Клиент работает на переднем плане до отмены. xs-quick заводит его в фоне, помнит номер и умеет снять туннель по имени.

Установка хаба и выдача пиров. Это xs-install.sh: вопросы, юнит, masquerade, меню для добавления и удаления пиров с готовыми конфигурациями.

xs-quick — туннель на Linux#

sudo install -m 755 contrib/xs-quick /usr/local/sbin/xs-quick
sudo install -m 644 contrib/xs-quick@.service /etc/systemd/system/
sudo xs-quick add home < xsteer-home.link    # или < xsteer-home.conf

sudo xs-quick up home        # поднять
sudo xs-quick status         # что происходит
sudo xs-quick list           # что настроено на этой машине
sudo xs-quick down home      # снять
sudo xs-quick strip home     # показать, что увидит клиент (без крючков)

Имя без косой черты берётся как /etc/xsteer/<имя>.conf, а если такого файла нет — как /etc/xsteer/<имя>.link: ссылка xs:// это тот же доступ одной строкой, и хаб выдаёт её рядом с файлом. Путь с косой чертой ведёт прямо к файлу, и .link по пути тоже понимается. Имя туннеля становится именем устройства, поэтому оно не длиннее 15 знаков.

add кладёт присланный доступ на место — то же движение, что «сканировать QR» в приложении на телефоне. Читает он стандартный ввод, а не аргумент: в ссылке приватный ключ, а аргументы команды видны в списке процессов всякому на машине. Проверка идёт до записи и тем же клиентом, который будет это поднимать; не разобралось — на диске не остаётся ничего. Каталог заводится с правами 0700, файл — с 0600, иначе клиент такую конфигурацию читать откажется.

В крючках подставляется %i — имя туннеля. Table = off означает «маршруты не трогать» (клиенту уходит --no-routes); числовых таблиц клиент не знает, и делать вид, что знает, нельзя, поэтому любое другое значение — отказ.

Отказ крючка не рушит подъём: правило firewall могло уже стоять, и падать из-за этого значило бы остаться без туннеля из-за пустяка. Но в вывод он попадает всегда.

Журнал клиента идёт в /run/xsteer/<имя>.log. Вырезанная конфигурация лежит рядом с правами 0600 и на tmpfs: после перезагрузки её нет вовсе — второго экземпляра секрета на диске не остаётся.

При загрузке#

sudo systemctl enable --now xs-quick@home

Юнит oneshot с RemainAfterExit, как у wg-quick@.service, и цена этого названа прямо: упавший клиент systemd не поднимет, потому что ExecStart отработал успешно и юнит считается активным. Кому нужен присмотр, тому нужен другой юнит — с Type=simple и клиентом напрямую:

[Service]
Type=simple
ExecStart=/usr/local/sbin/xsteer up /etc/xsteer/home.conf --dev home --state /run/xsteer-home.json
Restart=always
RestartSec=3
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW

Он присматривает за процессом, но не исполняет крючки и не понимает Table. Выбор между присмотром и крючками принадлежит человеку, а не скрипту; если нужно и то и другое — крючки переносятся в ExecStartPost/ExecStopPost этого же юнита.

xs-install.sh — хаб на сервере#

Одной командой, без клонирования хранилища:

curl -fsSLO https://raw.githubusercontent.com/splify2/xsteer/main/server/xs-install.sh
sudo bash xs-install.sh

Двумя строками, а не curl | bash, и это не педантизм: скрипт спрашивает, а в конвейере его стандартный ввод занят самим скриптом — ответить было бы нечем, и первый же вопрос завершил бы установку отказом «ввод закончился».

Из клона хранилища — то же самое:

sudo bash server/xs-install.sh

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

QR — для телефона. При добавлении пира установщик предлагает показать код прямо в терминале, и тот же код достаётся из меню в любой момент. В коде лежит ссылка xs:// — приложение под Android читает её камерой («Сканировать QR»). qrencode ставится по согласию и только когда код действительно понадобился; отказ ничего не ломает — конфигурация и ссылка лежат файлами рядом.

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

Про транспорт — главный вопрос, и он не косметический. Поддельный TCP экономит повторные передачи: потеря наружу остаётся потерей одного внутреннего пакета, а не превращается в задержку всему потоку. Но он невозможен на Windows без драйвера-перехватчика. Режим потока работает всюду, выглядит настоящим TLS полнее и стоит TCP внутри TCP. Пиры на Windows умеют только поток — и чаще всего именно это решает выбор. Установщик поэтому предлагает и третий вариант: оба транспорта на разных портах.

Порт поддельного TCP принадлежит хабу целиком: слушающий сокет ядра на нём отвечал бы SYN-ACK нашим же пирам, и рукопожатие ломалось бы через раз. Установщик проверяет занятость порта сразу.

Конфигурации пиров кладутся в /root/xsteer-<имя>.conf. Ключи пира делает сервер — как в wireguard-install, и это компромисс: приватный ключ пира проходит через хаб. Аккуратнее сделать пару на самом пире (xsteer genkey) и принести сюда только публичную половину; сказано прямо, чтобы выбор был осознанным.

Правило masquerade живёт в своей таблице nft (xsteer_nat): чужих правил установщик не трогает, а снятие хаба уносит ровно своё. Ключи и список пиров при снятии не удаляются молча — восстановить их нельзя, а пиров пришлось бы перевыпускать все до одного.

xsteer check#

xsteer check /etc/xsteer/hub.conf

Разбирает конфигурацию и ничего не поднимает: печатает роль (пир или хаб), адрес, MTU, пиров с их AllowedIPs. Роль выводится из файла, а не спрашивается ключом.

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

Проверки#

sudo sh tests/quick.sh    # xs-quick поднимает и снимает настоящий туннель в netns

Стенд проверяет именно побочные эффекты, потому что рассуждением они не проверяются: исполнились ли крючки, указывает ли номер процесса на клиента (а не на промежуточную оболочку), вернул ли up управление (фоновый процесс не должен держать открытым stdout вызывающего), удержал ли Table = off маршрут по умолчанию, убрано ли устройство после down. Первые три пункта — это ошибки, которые в xs-quick уже были и которые нашёл этот стенд, а не чтение кода.