user@elrise.io:~/projects/$ ← elrise.ru
· [active] теги: shclisystemdlinux

sing-box-vpn — менеджер VPN-профилей для sing-box

CLI-обёртка вокруг sing-box: набор JSON-профилей, share-link, генератор конфигурации, systemd-сервис и автопереключение при отказе upstream.

→ репозиторий

Обзор

sing-box-vpn — локальный менеджер клиентских профилей для sing-box. Репозиторий содержит набор скриптов и шаблон конфигурации: скрипты читают *.json-профили, подставляют активный профиль в общий шаблон, валидируют результат и запускают sing-box через systemd.

В каждый момент времени активен ровно один профиль. Он подставляется в один outbound с тегом proxy-out; всё остальное — inbound'ы, DNS, маршрутизация и rule set'ы — живёт в общем шаблоне и не меняется при смене профиля.

Основные практические сценарии:

  • локальный SOCKS/mixed-прокси на 127.0.0.1:12334 для браузеров и CLI-утилит, поддерживающих SOCKS5 с удалённым DNS-резолвом;
  • переключение между несколькими outbound-серверами (разные провайдеры, протоколы, IPv4/IPv6);
  • автопереключение на резервный профиль при недоступности активного через systemd-таймер.

Что это и чем не является

Проект делает:

  • хранит набор outbound-профилей как обычные JSON-файлы;
  • выбирает активный профиль и рендерит финальный config.json из шаблона;
  • запускает и контролирует sing-box через systemd;
  • валидирует конфигурацию через sing-box check;
  • предоставляет локальный mixed/SOCKS endpoint и Clash API на loopback;
  • опционально переключает профиль при отказе upstream.

Проект не делает:

  • не разворачивает VPN-сервер и не генерирует серверные конфиги Hysteria2, VLESS, Xray и т. п.;
  • не создаёт TUN/TAP-интерфейс и не проксирует системный трафик автоматически;
  • не поставляет правил policy-routing/nftables для прозрачного проксирования (TPROXY inbound присутствует в шаблоне, но маршрутизация для него не настроена);
  • не реализует kill switch;
  • не предоставляет GUI, индикатора трафика или per-app split tunneling;
  • не управляет мобильными, macOS и Windows-клиентами — для них используются внешние приложения (Nekoray, v2rayN и т. п.);
  • не гарантирует обход DPI или конкретных блокировок — это зависит от выбранного протокола и сервера.

Требования

Компонент Требование
ОС Linux с systemd ≥ 245
Ядро 4.18+
Bash фактически обязателен (используются bash-массивы и [[ ]])
Python 3.8+ (для URL-парсера)
sing-box 1.13.0+ (шаблон использует route.default_domain_resolver и action: sniff)
Прочие утилиты jq, curl, systemctl, journalctl, nft/iptables, GNU coreutils
Права root для install, apply, test-all, on/off/restart, use/add/add-json/del
Сеть рабочий IPv4 или IPv6 egress; проверки зависят от https://cloudflare.com/cdn-cgi/trace и https://1.1.1.1/dns-query

Проверялся на Arch-based и Debian-семействах. Поведение на других дистрибутивах не гарантируется.

Архитектура и поток данных

/home/alex/sing-box-vpn/profiles/<name>.json
        │
        │  apply-profiles.sh (копирует source → runtime)
        ▼
/etc/sing-box/profiles/<name>.json
        │
        │  generate-config.sh (читает шаблон + active profile)
        ▼
/etc/sing-box/config.json
        │
        │  sing-box.service
        ▼
  mixed/SOCKS на 127.0.0.1:12334
        │
        │  proxy-out → upstream (Hysteria2 / VLESS / Shadowsocks / VMess / Trojan)
        ▼
        сервер

Слои:

  1. Source-профили — канонические *.json в репозитории (на практике в ~/sing-box-vpn/profiles/). Хранятся в Git, реальные секреты — в gitignore.
  2. Runtime-профили — копии в /etc/sing-box/profiles/ с правами 0640. Именно их читает генератор.
  3. Active marker — файл /etc/sing-box/active_profile с именем активного профиля.
  4. Сгенерированный конфиг/etc/sing-box/config.json, собирается из sing-box-config.json путём подстановки активного outbound'а.
  5. Systemd unitsing-box.service запускает /usr/local/bin/sing-box run -c /etc/sing-box/config.json с hardening-параметрами.

Шаблон sing-box-config.json содержит:

  • два inbound: mixed-in на :: / 12334 и tproxy-in на :: / 12335 (маршрутизация для TPROXY в проекте не настроена);
  • селектор select, direct, и плейсхолдер proxy-out, который заменяется активным outbound'ом;
  • DNS-стек: dns-remote (DoH 1.1.1.1 через proxy), dns-direct (тот же DoH напрямую), dns-lan (UDP на 192.168.0.1:53), dns-system-hosts, dns-local; final = dns-remote, strategy = prefer_ipv4;
  • маршруты: sniff, hijack порта 53, reject loopback:9090, bypass для private-domains rule set, bypass для ряда частных CIDR и .ru;
  • Clash API на 127.0.0.1:9090 без аутентификации;
  • route.default_domain_resolver = dns-remote (требует sing-box ≥ 1.13).

Установка

Стандартная установка запускается из каталога репозитория от root:

sudo ./install.sh

Скрипт:

  1. требует root и пишет лог в /tmp/sing-box-install.log;
  2. ищет sing-box в PATH; при отсутствии пробует AUR, иначе скачивает бинарь 1.11.0 (имейте в виду — см. раздел «Обновление sing-box»);
  3. создаёт /var/lib/sing-box, runtime-каталоги и копирует шаблон с профилями;
  4. при отсутствии active marker выбирает warp-client;
  5. генерирует конфигурацию и прогоняет её через sing-box check;
  6. ставит и запускает sing-box.service;
  7. создаёт /etc/sing-box/private-domains.txt (после старта сервиса);
  8. по умолчанию ставит и включает failover; отключается переменной SKIP_FAILOVER=1;
  9. включает nftables и сохраняет текущий ruleset в /etc/nftables.conf;
  10. завершается прямой curl-проверкой внешнего IP без SOCKS.
Важно

До запуска install.sh в репозитории уже должен лежать runtime-пригодный профиль (по умолчанию ожидается warp-client.json). Без него шаг генерации конфигурации завершится ошибкой. Финальное сообщение VPN OK означает лишь успех прямой проверки egress и не подтверждает работу SOCKS.

Альтернативная установка без failover:

sudo SKIP_FAILOVER=1 ./install.sh

Файлы и каталоги

Путь Назначение
~/sing-box-vpn/ Каталог репозитория
~/sing-box-vpn/profiles/*.json Source-профили (в Git)
~/sing-box-vpn/vpn CLI-скрипт
~/sing-box-vpn/failover.sh Скрипт автопереключения
~/sing-box-vpn/generate-config.sh Генератор конфигурации
~/sing-box-vpn/sing-box-config.json Шаблон конфигурации
/usr/local/bin/sing-box Бинарь sing-box
/usr/local/libexec/sing-box-vpn/ Каталог, куда installer копирует vpn и failover.sh
/etc/sing-box/ Runtime-каталог (настраивается через RUNTIME_DIR)
/etc/sing-box/config.json Сгенерированная активная конфигурация
/etc/sing-box/profiles/*.json Копии профилей, используемые runtime
/etc/sing-box/active_profile Имя активного профиля
/etc/sing-box/private-domains.txt Rule set для bypass-доменов
/etc/systemd/system/sing-box.service Systemd unit
/etc/systemd/system/vpn-failover.{service,timer} Failover unit + таймер
/var/log/sing-box.log Файловый лог sing-box
/var/lib/sing-box Каталог, разрешённый systemd sandboxing'ом
/var/tmp/sing-box-vpn-broken/ Чёрный список временно нерабочих профилей
/tmp/sing-box-install.log Лог установщика

Переменные окружения

Переменная Default Где действует
PROJECT_DIR каталог, где лежит vpn CLI, генератор, failover, installer
RUNTIME_DIR /etc/sing-box CLI, генератор, apply, installer
PROFILES_DIR <PROJECT_DIR>/profiles (CLI/apply) / <RUNTIME_DIR>/profiles (generator) семантика различается по скриптам
RUNTIME_PROFILES_DIR $RUNTIME_DIR/profiles CLI, apply
ACTIVE_FILE $RUNTIME_DIR/active_profile CLI, генератор
TEMPLATE $PROJECT_DIR/sing-box-config.json генератор
OUT $RUNTIME_DIR/config.json генератор
GEN_SCRIPT $PROJECT_DIR/generate-config.sh CLI
CLI $PROJECT_DIR/vpn apply, test-all
SERVICE_NAME sing-box CLI/apply/installer
MIXED_PORT 12334 CLI, failover; в шаблоне порт жёстко задан и не подменяется
SING_BOX_BIN /usr/local/bin/sing-box installer
SING_BOX /usr/local/bin/sing-box apply
LOG /tmp/sing-box-install.log или rollback-лог installer, rollback
SKIP_FAILOVER 0 installer
TRACE_HOST https://cloudflare.com/cdn-cgi/trace failover
PROBE_TIMEOUT 8 секунд failover
DRY_RUN 0 failover
BROKEN_DIR /var/tmp/sing-box-vpn-broken failover
BROKEN_TTL_SEC 1800 failover

Часть переменных заявлена в vpn как override, но фактически не читается unit'ом и шаблоном: RUNTIME_DIR, SING_BOX_BIN, MIXED_PORT (в шаблоне всегда 12334), CONFIG_TEMPLATE/CONFIG_OUT (генератор использует TEMPLATE/OUT). Нестандартные значения этих переменных считайте неподдерживаемыми без ручной правки unit и шаблона.

Профили

Схема JSON

Один файл profiles/<name>.json соответствует одному outbound. Имя файла должно совпадать с полем внутри (хотя vpn add-json это не проверяет — см. ниже).

Минимальный набор полей:

{
  "type": "hysteria2",
  "server": "example.com",
  "server_port": 443,
  "password": "..."
}

Поддерживаются поля, специфичные для протокола: tls, transport, multiplex, flow и т. д. Они почти без изменений переносятся в сгенерированный outbound; исключаются только служебные (name, description, type, tag, server, server_port).

Добавление из URL

sudo ./vpn add my-profile 'vless://uuid@server:443?type=tcp&security=reality#name'

Поддерживаемые схемы: vless://, hy2:// / hysteria2://, ss://, vmess://, trojan://. Парсер не покрывает все варианты share-link (транспорт WebSocket/gRPC, плагины Shadowsocks, нестандартные схемы) — для таких случаев используйте add-json.

Передача URL в аргументах сохраняет секрет в shell history и временно делает его видимым в списке процессов.

Добавление из JSON

sudo ./vpn add-json my-profile ./profile.json

Проверяется только валидность JSON и наличие type, server, server_port. Полная проверка через sing-box check не выполняется; запустите её вручную.

IPv6-серверы

В JSON-файле адрес IPv6 указывается без квадратных скобок:

{ "type": "hysteria2", "server": "2001:db8::1", "server_port": 443, ... }

Для работы IPv6-only профиля на хосте должен быть IPv6 default route. Проверка: ip -6 route show default.

Хранение секретов

Реальные профили исключены из Git через .gitignore. Это не шифрование и не гарантия, что секреты не попали в историю коммитов до того, как были добавлены в gitignore. Для шеринга между машинами используйте отдельный зашифрованный канал (gpg-агент, age, vault и т. п.).

CLI-команды

Команда Root Что делает
./vpn on да Запускает sing-box.service через systemd; если уже активен — генерирует конфиг и выполняет SOCKS-trace
./vpn off да Останавливает sing-box.service. nftables/iptables не сбрасываются
./vpn restart да systemctl restart sing-box. Конфиг предварительно не генерируется
./vpn status нет Показывает состояние сервиса, active marker, source/runtime-пути и Clash API; проверяет внешний IP напрямую, не через SOCKS
./vpn list нет Печатает source-профили с description и маркером активного
./vpn current нет Печатает имя активного профиля или (none)
./vpn use <name> да Записывает active marker, генерирует конфиг, перезапускает сервис только если он уже был активен; sing-box check не выполняет
./vpn add <name> <URL> да Создаёт source-профиль из share-link; runtime и сервис не трогает
./vpn add-json <name> <file> да Копирует JSON как source-профиль с правами 0644; базовая валидация (JSON + три обязательных поля)
./vpn del <name> да Удаляет source- и runtime-файл; активный профиль удалить нельзя; конфиг/сервис не перегенерирует
./vpn test нет До трёх SOCKS-запросов к Cloudflare; ожидает строки ip=, colo=, loc=. Из-за set -euo pipefail неуспешный pipeline может прервать команду до повторов
./vpn help нет Печатает справку

Имена профилей в mutating-командах не санитизируются. Безопасный формат — ^[A-Za-z0-9._-]+$.

Применение и проверка профилей

Помимо vpn, есть три вспомогательных скрипта:

  • sudo ./generate-config.sh — только собирает /etc/sing-box/config.json из шаблона и active profile. Не валидирует и не перезапускает сервис.
  • sudo ./apply-profiles.sh — копирует source-профили в runtime, перебирает все профили, для каждого меняет active marker, генерирует конфиг и прогоняет SOCKS-тест; при полном успехе восстанавливает исходный active (или warp-client). Stale runtime-файлы не удаляет.
  • sudo ./test-all.sh — запускает сервис, перебирает и тестирует все source-профили; исходный активный профиль не восстанавливает.

Из-за set -euo pipefail в этих скриптах первая неудачная проверка может прервать выполнение до сбора полного summary. Если нужна полная картина — запускайте vpn test по каждому профилю вручную.

SOCKS, DNS и маршрутизация

SOCKS endpoint

  • Протокол: mixed (SOCKS4, SOCKS5, HTTP CONNECT).
  • Адрес по умолчанию: 127.0.0.1:12334 (или [::1]:12334).
  • Inbound в шаблоне слушает на :: — это все IPv6-интерфейсы, а в зависимости от системы и IPv4. Без firewall это потенциально открывает прокси шире, чем loopback.
  • Аутентификация на inbound не задана.

Проверка:

curl --max-time 8 --socks5-hostname 127.0.0.1:12334 \
     https://cloudflare.com/cdn-cgi/trace

--socks5-hostname обязателен: иначе DNS-резолв идёт мимо прокси.

DNS

  • dns-remote — DoH https://1.1.1.1/dns-query через proxy-out (трафик DNS идёт через туннель).
  • dns-direct — тот же DoH напрямую.
  • dns-lan — UDP на 192.168.0.1:53 для локальных имён.
  • dns-system-hosts и dns-local — для /etc/hosts и коротких имён.
  • final = dns-remote, strategy = prefer_ipv4.

Bypass

/etc/sing-box/private-domains.txt — rule set формата sing-box source JSONL. По умолчанию направляет эти домены в direct. Также в шаблоне:

  • hijack DNS (порт 53 → hijack-dns);
  • reject loopback:9090;
  • direct для .local, .localhost, ряда частных IPv4/IPv6 CIDR, домена .ru;
  • всё остальное → proxy-out.

Полный список bypass-доменов и CIDR см. в sing-box-config.json и etc-sing-box-private-domains.txt.

Clash API

  • Адрес: 127.0.0.1:9090.
  • Аутентификация не настроена.
  • Это control plane (просмотр правил, live-трафик), а не ещё один proxy-endpoint.

Автопереключение (failover)

Алгоритм

failover.sh:

  1. проверяет активный профиль через SOCKS-запрос к cloudflare.com/cdn-cgi/trace с таймаутом PROBE_TIMEOUT (8 секунд по умолчанию);
  2. успех — наличие хотя бы одной из строк ip=, colo=, loc=;
  3. при отказе записывает timestamp в /var/tmp/sing-box-vpn-broken/<profile> (TTL = BROKEN_TTL_SEC, 30 минут);
  4. получает кандидатов через vpn list, исключает активный и недавно отказавшие;
  5. по очереди вызывает sudo vpn use <candidate> и повторяет probe;
  6. первый успешный профиль остаётся активным;
  7. если все отказали — возвращает 1; активным может остаться последний проверенный.

Переменные

TRACE_HOST, PROBE_TIMEOUT, MIXED_PORT, LOG_TAG, DRY_RUN, BROKEN_DIR, BROKEN_TTL_SEC, VPN (путь к vpn).

DRY_RUN=1 только печатает, какой vpn use был бы выполнен, и не проверяет кандидатов.

Exit codes

Код Значение
0 Текущий профиль исправен или переключение удалось
1 Ни один кандидат не сработал
2 Нет sudo или vpn не найден

Systemd

System-wide таймер: первый запуск через 2 минуты после boot, затем каждые 5 минут, persistent.

TimeoutStartSec=30s на сервисе. Полный обход нескольких профилей может не уложиться (один probe — до 8 секунд, плюс sleep 2 между кандидатами). При большом числе профилей увеличьте таймаут через drop-in.

Особенности установки

Стандартный install.sh копирует vpn и failover.sh в /usr/local/libexec/sing-box-vpn. После этого vpn начинает считать этот каталог PROJECT_DIR, но шаблона и profiles/ там нет. Для корректной работы rotation задайте в /etc/default/vpn-failover:

VPN=/home/alex/sing-box-vpn/vpn

Per-user инструкция в docs/FAILOVER.md копирует только unit-файлы, тогда как unit ожидает скрипты в %h/.local/bin/ и %h/.local/libexec/sing-box-vpn/. Положите их туда вручную или используйте system-wide установку.

Для журналов надёжнее:

journalctl -u vpn-failover.service

а не journalctl -t vpn-failover (скрипт не вызывает logger, в unit не задан SyslogIdentifier).

Повседневная эксплуатация

sudo ./vpn status                       # состояние сервиса и active profile
./vpn list                              # все source-профили
./vpn current                           # имя активного профиля
sudo ./vpn use my-profile               # переключиться
sudo ./vpn add new-profile 'vless://...' # добавить из share-link
sudo ./vpn del old-profile              # удалить профиль
sudo systemctl restart sing-box         # применить изменения вне CLI
journalctl -u sing-box -f               # лог sing-box в реальном времени
journalctl -u vpn-failover.service      # лог failover
systemctl list-timers vpn-failover.timer # состояние таймера

В репозитории нет файлов с shell-aliases (vpnon, vpnuse и т. п.), которые упоминаются в docs/OPERATIONS.md. Если нужны — добавьте сами в ~/.bashrc или в /etc/profile.d/.

Обновление sing-box

Шаблон рассчитан на sing-box ≥ 1.13 и использует route.default_domain_resolver и action: sniff. При обновлении до 1.14+ потребуется миграция DNS-схемы (см. CHANGELOG.md).

Рекомендуемый порядок:

# 1. проверить версии
/usr/local/bin/sing-box version
sing-box version   # новый бинарь

# 2. бэкап
cp -a /etc/sing-box /etc/sing-box.bak
cp -a ~/sing-box-vpn ~/sing-box-vpn.bak

# 3. валидация новым бинарём
sudo /usr/local/bin/sing-box check -c /etc/sing-box/config.json

# 4. перегенерация и перезапуск
sudo ~/sing-box-vpn/generate-config.sh
sudo systemctl restart sing-box

# 5. проверка
~/sing-box-vpn/vpn test
systemctl status vpn-failover.timer

В комплекте есть scripts/run-after-upgrade.sh, но он жёстко привязан к конкретному списку профилей и перезапускает сервис до замены симлинка. Для продакшна используйте его только как отправную точку.

Диагностика

Состояние

~/sing-box-vpn/vpn status
~/sing-box-vpn/vpn current
~/sing-box-vpn/vpn list
systemctl status sing-box
systemctl is-active sing-box

Согласованность runtime

active_profile, runtime-профиль и сгенерированный конфиг должны соответствовать друг другу:

cat /etc/sing-box/active_profile
ls /etc/sing-box/profiles/
sudo sing-box check -c /etc/sing-box/config.json

Генератор завершится ошибкой, если active marker или runtime-профиль отсутствуют.

Прямая vs SOCKS-проверка

curl https://cloudflare.com/cdn-cgi/trace
curl --socks5-hostname 127.0.0.1:12334 https://cloudflare.com/cdn-cgi/trace

Сравните поля ip= и colo=. vpn status выполняет прямую проверку, не SOCKS — учитывайте это.

Логи

journalctl -u sing-box -n 200 --no-pager
journalctl -u sing-box -f
tail -f /var/log/sing-box.log
journalctl -u vpn-failover.service -n 100 --no-pager

IPv6

ip -6 route show default

Нет default route — IPv6-only профиль работать не будет, даже если сам сервер отвечает.

Безопасность

  • Секреты в профилях. Пароли, UUID и приватные ключи лежат в JSON-файлах в Git (если не в gitignore). Передача share-link в аргументах CLI оставляет секрет в shell history и делает его временно видимым в ps.
  • Права на файлы. Source-профили от vpn add создаются с umask пользователя, от vpn add-json0644. Runtime-профили получают 0640 через install.sh и apply-profiles.sh. Это не шифрование и не защита от root.
  • Mixed inbound. Слушает на :: (все IPv6-интерфейсы и потенциально IPv4), аутентификация не задана. Без firewall прокси может быть доступен шире, чем loopback.
  • Clash API. Ограничен 127.0.0.1:9090, но аутентификация отсутствует.
  • Systemd hardening. sing-box.service запускается от root с CAP_NET_ADMIN, CAP_NET_RAW, CAP_NET_BIND_SERVICE; включены NoNewPrivileges, ProtectSystem, ProtectHome, PrivateTmp, ProtectKernelTunables и т. п.
  • tls.insecure=true. В нескольких Hysteria2-профилях отключена проверка серверного сертификата. Это упрощает MITM и должно использоваться осознанно.
  • DNS-leaks. Гарантия отсутствия утечек возможна только для приложений, которые используют прокси и не выполняют собственный DoH/DoT. Уровень системы (systemd-resolved, NetworkManager) может ходить в обход.
  • Kill switch. Не реализован. При падении sing-box приложения, не настроенные на SOCKS, продолжат использовать direct network.
  • .ru, private CIDR, private-domains обходят VPN по дизайну.

Ограничения

  • URL-парсер не покрывает все варианты share-link (transport WebSocket/gRPC/H2, plugin Shadowsocks, нестандартные схемы). Для них используйте add-json с последующей sing-box check.
  • bracketed IPv6 ([2001:db8::1]:443) в URL обрабатывается некорректно; в JSON пишите IPv6 без скобок.
  • vpn test, apply-profiles.sh и test-all.sh используют set -euo pipefail — первая неуспешная проверка может прервать скрипт до сбора полного summary.
  • vpn status показывает внешний IP через прямой запрос, не через SOCKS. Для проверки именно туннеля используйте ручной curl --socks5-hostname.
  • apply-profiles.sh не удаляет stale runtime-файлы при удалении соответствующего source-профиля.
  • rollback.sh — частичный emergency rollback, не uninstall.
  • Поведение на дистрибутивах вне Arch-based и Debian-семейства не проверялось.
  • sing-box 1.11 (которую может скачать installer) несовместим с текущим шаблоном.
  • sing-box 1.14+ потребует миграции DNS-схемы.

Rollback и удаление

rollback.sh отключает и останавливает sing-box.service, удаляет nftables table inet sing-box и отдельные policy-routing правила, пытается удалить iptables chain SINGBOX, пытается включить ранее отключённый Hiddify user-сервис. Рекомендует reboot.

rollback.sh не удаляет:

  • /etc/sing-box (конфиг и профили остаются);
  • source-профили;
  • systemd unit;
  • vpn-failover.timer;
  • /usr/local/bin/vpn и /usr/local/bin/vpn-failover;
  • не гарантирует восстановление исходной сетевой конфигурации.

Для полного удаления вручную:

sudo systemctl disable --now sing-box vpn-failover.timer
sudo rm -f /etc/systemd/system/sing-box.service \
           /etc/systemd/system/vpn-failover.service \
           /etc/systemd/system/vpn-failover.timer
sudo systemctl daemon-reload
sudo rm -rf /etc/sing-box /usr/local/libexec/sing-box-vpn
sudo rm -f /usr/local/bin/vpn /usr/local/bin/vpn-failover

После — откатите изменения в /etc/nftables.conf, если они были сделаны installer'ом.

Справочник

CLI

См. раздел «CLI-команды».

Переменные окружения

См. раздел «Переменные окружения».

Systemd units

  • sing-box.service — основной сервис.
  • vpn-failover.service + vpn-failover.timer — автопереключение.

Логи

  • journalctl -u sing-box
  • journalctl -u vpn-failover.service
  • /var/log/sing-box.log
  • /tmp/sing-box-install.log

Репозиторий и исходники