Обвязка: 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 уже были и которые нашёл этот стенд, а не чтение кода.