05 — sing-box: устройство, конфиг, настройка
sing-box используется как кросс-платформенный клиент: WireGuard как транспорт к нашему MikroTik-серверу, доставка/обновление конфига через подписку (URL от control plane). Это снимает зависимость iOS от MDM для реконфига.
Ментальная модель
sing-box — это не «VPN-клиент», а маршрутизатор пакетов с конвейером:
inbound ──▶ dns + route (правила решают куда) ──▶ outbound / endpoint
(как вошёл) (куда вышел)
Под наш кейс: tun inbound перехватывает весь трафик устройства → правила маршрутизации
решают, что гнать в туннель → wireguard endpoint выносит это на MikroTik. WG-endpoint в
маршрутизации ведёт себя как обычный именованный outbound — это используется для failover ниже.
Совместимость с MikroTik: на проводе это стандартный WireGuard (Noise), ROSv7-WG видит sing-box как обычного пира. Ключи — base64 (как у MikroTik; Xray использует hex — не наш случай).
Минимальный профиль (его рендерит control plane per-device)
Сразу с группой failover (объяснение — в разделах ниже):
{
"log": { "level": "warn" },
"inbounds": [
{ "type": "tun", "tag": "tun-in", "address": ["172.19.0.1/30"],
"auto_route": true, "strict_route": true }
],
"endpoints": [
{ "type": "wireguard", "tag": "wg-r5", "system": false, "mtu": 1420,
"address": ["10.50.5.23/32"], "private_key": "<CLIENT_PRIV>",
"peers": [{ "address": "r5a.vpn.corp", "port": 51820,
"public_key": "<R5A_PUB>", "allowed_ips": ["0.0.0.0/0"],
"persistent_keepalive_interval": 25 }] },
{ "type": "wireguard", "tag": "wg-r5b", "system": false, "mtu": 1420,
"address": ["10.50.5.23/32"], "private_key": "<CLIENT_PRIV>",
"peers": [{ "address": "r5b.vpn.corp", "port": 51820,
"public_key": "<R5B_PUB>", "allowed_ips": ["0.0.0.0/0"],
"persistent_keepalive_interval": 25 }] }
],
"outbounds": [
{ "type": "direct", "tag": "direct" },
{ "type": "selector", "tag": "vpn", "outbounds": ["auto","wg-r5","wg-r5b"],
"default": "auto", "interrupt_exist_connections": true },
{ "type": "urltest", "tag": "auto", "outbounds": ["wg-r5","wg-r5b"],
"url": "https://r5a.vpn.corp/gen_204", "interval": "30s", "tolerance": 50,
"interrupt_exist_connections": true }
],
"route": {
"rules": [
{ "action": "sniff" },
{ "protocol": "dns", "action": "hijack-dns" },
{ "ip_is_private": true, "outbound": "direct" }
],
"final": "vpn",
"auto_detect_interface": true
},
"experimental": {
"clash_api": { "external_controller": "127.0.0.1:9090" },
"cache_file": { "enabled": true }
}
}
Это и есть «подписка»: control plane отдаёт этот JSON по аутентифицированному URL, приложение тянет и применяет. Смена региона = тот же URL отдаёт endpoint(ы) другого региона.
Рычаги настройки — по убыванию пользы
1. Client-side failover через urltest
Вместо одной точки входа кладёшь несколько (wg-r5, wg-r5b) и группируешь в urltest.
sing-box щупает задержку до каждой по url каждые interval и переключается на лучшую;
interrupt_exist_connections: true рвёт активные соединения при смене, форсируя переезд.
Это решает intra-region failover на клиенте, без DNS-ре-резолва и без обязательного общего
серверного ключа. Цена: профиль несёт пиры/pubkey-и всех кандидатов (все предзаготовлены).
⚠️ Проверь на своей версии, что группа
urltestреально щупает endpoint-теги — WG переезжал изoutboundвendpoint, и это место стоит протестировать.
2. Сплит-туннель через route-правила
В примере ip_is_private → direct шлёт локальную LAN мимо туннеля. Но если ресурсы магазинов
сидят в приватных диапазонах за туннелем — это правило их сломает. Тогда наоборот:
{ "ip_cidr": ["10.50.0.0/16","10.0.0.0/8"], "outbound": "vpn" }
и "final": "direct". На Android доступен package_name (per-app), на десктопе — process_name.
3. Remote rule-sets для меняющейся топологии
Список «какие подсети внутренние» можно вынести в remote rule-set (бинарный .srs по URL),
который обновляется отдельно от профиля:
"route": {
"rule_set": [
{ "type": "remote", "tag": "corp-nets", "format": "binary",
"url": "https://cp.vpn.corp/rs/corp-nets.srs", "download_detour": "vpn" }
],
"rules": [ { "rule_set": "corp-nets", "outbound": "vpn" } ]
}
Меняешь карту магазинов — не пере-выпуская конфиги клиентам.
4. DNS-движок
У sing-box свой DNS с правилами. hijack-dns перехватывает DNS-запросы; правилом можно гнать
корп-зоны (*.vpn.corp) на свой PowerDNS через туннель, остальное — на публичный резолвер.
Это стыкует low-TTL DNS-failover (файл 04): имена точек входа резолвятся через корп-DNS.
⚠️ Блок
dns— самая перепиленная часть схемы (в 1.12 серверы стали типизированными). Пиши DNS строго под пинованную версию, сверяясь с актуальной докой.
5. Серверная сторона (твой control plane = subscription-сервер)
См. файл 03: валидирует токен, рендерит JSON с подставленными ключом/IP/endpoint/pubkey, ставит
заголовки profile-update-interval и subscription-userinfo, отдаёт по TLS, HTTP Basic.
6. clash_api — наблюдаемость и локальное управление
Блок experimental.clash_api даёт локальный контроллер: статус, трафик, переключение selector
(selector управляется только через Clash API). Это локально на устройстве — удалённо им не
рулишь, но как телеметрия и ручной override при отладке полезно.
Грабли (под стык с MikroTik)
- Версия: WG-endpoint появился в 1.11, старый WG-
outboundудалён в 1.13. route-actions (1.11) и DNS (1.12) менялись с breaking-ами. → пинуй конкретную версию, пиши конфиг под неё. - Ключи: base64, не hex.
- MTU: дефолт sing-box — 1408, а не 1420. Выставь явно и согласованно с MikroTik, иначе фрагментация.
reserved: нужно только для Cloudflare WARP — для голого WG не ставь.- iOS фоновое автообновление подписки ненадёжно (ограничения фонового исполнения): апдейт часто прилетает, когда юзер открыл приложение / туннель переподключился. → связку «миграция → Telegram-пинок открыть приложение» (файл 03) закладывай сразу.
- Дистрибуция приложения на iOS шаткая: у официального sing-box были проблемы с Apple developer account (переезд на новые Bundle ID, перебои с обновлением в App Store, TestFlight для спонсоров), нужен не-китайский Apple ID. → пинуй проверенную сборку и раздавай через MDM/VPP, либо обёртка на том же ядре (Hiddify, Karing) — но провенансь, оно держит твои ключи.