Личный менеджер бесплатных VPN-подписок. Один статический бинарник на Go.
Он берёт только те GitHub-источники, которые ты сам добавил в whitelist,
парсит и дедуплицирует их, отбрасывает сломанные/неподдерживаемые/незашифрованные/
вредоносные узлы, пропинговывает каждый кандидат 3× через встроенный движок
mihomo (16–32 параллельных воркера), оставляет 3–5 лучших по стране по
задержке и генерирует подписки для sing-box, v2rayN/xray и Clash.Meta.
Долгоживущий встроенный планировщик сам обновляет их 24/7 (заменяет мёртвые/
деградировавшие узлы), а веб-интерфейс управления (пакет internal/web) управляет источниками и показывает статус.
Движок (mihomo) встроен в бинарник — отдельной загрузки ядра не требуется.
Целевая платформа: Linux (Ubuntu 22+). Windows/macOS — только для разработки. Речь с пользователем ведётся на русском.
- Требования
- Установка Go
- Сборка
- Быстрый старт
- Настройка источников
- Каталог конфигурации
- Выходные подписки
- Флаги командной строки
- Планировщик 24/7 и встроенный движок
- Безопасность
- Устранение неполадок
- Запуск как сервиса (systemd)
- Разработка
- Linux (Ubuntu 22.04 или новее). Код кроссплатформенный, но egress-изоляция заточена под Linux.
- Go 1.25+ (для сборки из исходников). Бинарник внешних зависимостей не требует.
- Доступ в интернет (HTTPS): менеджер скачивает базу GeoIP и сами подписки. Без egress к IP узлов пинги будут падать — проверь сеть заранее.
- Опционально
make(дляmake build/make run), ноgoдостаточно.
Если Go уже есть (go version ≥ 1.25), пропусти этот раздел.
# скачать последнюю стабильную 1.25.x (пример для x86_64 Linux)
curl -fsSL https://go.dev/dl/go1.25.0.linux-amd64.tar.gz -o /tmp/go.tgz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf /tmp/go.tgz
# добавить в PATH (для текущей сессии и навсегда)
export PATH=$PATH:/usr/local/go/bin
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
go version # должно показать go1.25.xБез прав root можно распаковать в $HOME/sdk/go и прописать этот путь в PATH.
Клонируй репозиторий и собери:
git clone <repo-url> sub-manager
cd sub-manager
make build # эквивалентно `go build ./...`
# или собрать исполняемый файл сразу:
go build -o sub-manager .Проверить код перед запуском:
make vet # go vet ./...
make test # go test ./... (юнит + интеграционные тесты)Бинарник sub-manager готов к запуску.
# запустить (откроется веб-интерфейс управления)
./sub-manager -web-token <токен> -web-secret <секрет-путь-24+ символа>
# или через make:
make run # go run .При первом запуске менеджер:
- Создаёт каталог конфигурации (
~/.config/vpn-sub-manager/). - Инициализирует встроенный движок mihomo (Go-модуль, часть бинарника — загрузки из сети не требуется).
- Скачивает базу GeoLite2-Country.mmdb в
assets/(один раз). - Запускает планировщик в фоне и поднимает веб-интерфейс управления по скрытому
пути
http://<web-addr>/<web-secret>/(по умолчаниюhttp://127.0.0.1:8090/<web-secret>/).
Открой в браузере http://127.0.0.1:8090/<web-secret>/, введи веб-токен
(-web-token) — и попадёшь в панель из 9 зон: Status, Sources, Nodes,
Cycle, Generate, Publish, Settings, Security, Pipeline.
Добавь источники (см. ниже) — планировщик сам их обойдёт, отфильтрует, пропингует
и сгенерирует подписки в out/. Остановить: Ctrl+C.
Скрипт scripts/install.sh сам скачает/соберёт бинарник, запишет все ключи
и настройки в config.json (со случайными секретами, chmod 600), поставит
systemd-юнит и выведет URL админки и подписок.
# одной строкой (бинарник собирается CI и кладётся в bin/ репозитория,
# релиз создавать не нужно). REPO/BRANCH можно переопределить:
bash <(curl -fsSL https://raw.githubusercontent.com/HnDK0/Sub-Manager-VPN/main/scripts/install.sh)
# или, если клонировал репозиторий локально — собрать из исходников:
bash scripts/install.shПереопределить любые значения можно через переменные окружения:
WEB_TOKEN, WEB_SECRET, SERVE_TOKEN, WEB_ADDR, SERVE_ADDR,
INTERVAL, TOPN, CORPSE, INSTALL_DIR. Повторный запуск с FORCE=1
перезаписывает бинарник и config.json (секреты сгенерируются заново).
После установки админка доступна по http://<web-addr>/<web-secret>/
(по умолчанию http://127.0.0.1:8090/<web-secret>/); токен — значение
web_token из config.json. Для headless-сервера, чтобы юнит жил без
активной сессии: loginctl enable-linger $USER.
Подписки публикуются встроенным HTTP-сервером по
http://<serve-addr>/s/<serve-token>/ (по умолчанию
http://127.0.0.1:18080/s/<serve-token>/). Чтобы выставить их в интернет
через HTTPS-домен — см. раздел «nginx / Caddy / Apache reverse proxy» ниже.
Источники хранятся только в твоём whitelist (обычный текстовый файл-реестр; сделано намеренно, чтобы избежать блокировок SQLITE_BUSY). Никакие источники не зашиты в код — менеджер не скачает ничего, чего ты не добавил и не включил.
Панель управления доступна по скрытому пути http://<web-addr>/<web-secret>/
(по умолчанию http://127.0.0.1:8090/<web-secret>/). При открытии запрашивается
веб-токен (-web-token); дальше всё управление — через браузер. Зоны панели:
- Status — живой прогресс планировщика и KPI узлов.
- Sources — управление whitelist-источниками (добавить/вкл-выкл/заменить/удалить).
- Nodes — таблица живых/мёртвых узлов, фильтр по стране, ручной тест выбранных.
- Cycle — принудительный прогон цикла прямо сейчас.
- Generate — превью сгенерированных подписок (sing-box / v2rayN / Clash).
- Publish — статус и ссылки встроенного HTTP-сервера подписок, snippet nginx.
- Settings — просмотр и редактирование параметров запуска (сохраняются в
config.json; часть параметров применяется после перезапуска). - Security — сводка применённых фильтров безопасности.
- Pipeline — воронка стадий обработки (fetched → parsed → dedup → … → kept).
Добавленные и включённые источники подхватываются планировщиком на следующем цикле автоматически — перезапуск не нужен.
Нагрузка на источники минимальна. Каждый цикл планировщик скачивает
источник условным запросом: отправляет сохранённые ETag/Last-Modified
(If-None-Match/If-Modified-Since). Если сервер отвечает 304 Not Modified —
тело не качается и не перепарсивается, берутся ранее распарсенные узлы из кэша.
Если сервер не отдаёт заголовки валидации — тело сравнивается по SHA с прошлым
скачиванием, и при совпадении тоже пропускается репарс. Дубли внутри цикла
убираются filter.Dedup; а «трупы» (узлы, мёртвые ≥ CorpseCycles, по умолчанию 5
циклов подряд) не пингуются повторно (filterCorpses). Живые узлы перепроверяются
каждый цикл — это и есть суть 24/7-мониторинга (детект смерти/деградации).
Положи список ссылок в файл (одна https-ссылка на строку, # — комментарий),
например sources.txt, и запусти:
./sub-manager -seed sources.txtКоманда заменяет весь текущий список источников содержимым файла (только
валидные https), выводит seeded N sources (M skipped) и завершается. Дальше
запускай обычно: ./sub-manager. Содержащийся в репозитории sources.txt — это
твой собственный список, а не встроенный набор.
По умолчанию: ~/.config/vpn-sub-manager/ (переопределяется флагами -state,
-assets, -out).
| Путь | Назначение |
|---|---|
config.json |
Сохранённые параметры запуска (входные: -web-secret, -web-token, -web-addr, -serve-*, -interval, -topn, …); флаги CLI переопределяют файл. Если забыл путь/токен админки — см. этот файл. Содержит секреты (web-token/web-secret) в открытом виде — защити правами (chmod 600) и не коммить. |
state.db |
SQLite: источники, узлы, результаты пингов, история (bounded retention) |
assets/ |
GeoLite2-Country.mmdb (GeoIP, скачивается один раз, далее офлайн) |
out/ |
Сгенерированные подписки (см. ниже) |
Генерируются в out/ и обновляются каждым циклом планировщика:
- sing-box JSON —
out/singbox.json(SFA-конфиг; импортируй файл в клиент напрямую или отдай по HTTP). - v2rayN / xray base64 —
out/v2rayn.txt(base64 от newline-списка нативных URI). - Clash.Meta YAML —
out/clash.yaml(proxies[]+proxy-groupsurl-test). - Сводка —
out/meta.json(небольшая текстовая сводка: время генерации / число узлов; не полные метаданные SFA).
Все три формата содержат только явные, известные поля узла; никакие
непроверенные/опасные ключи (поля Extra/Raw) в подписку не попадают. Если цикл
вернул пустой результат, предыдущий рабочий файл не перезаписывается (защита от
затирания хорошей подписки пустым циклом).
Имена узлов — единообразные. В подписках каждый узел назван по шаблону
<TYPE> <host:port> <ENC> <CC>: <TYPE> — латиницей (VLESS/TROJAN/HY2/TUIC/VMESS),
<ENC> — метод шифрования/безопасность, <CC> — реальный ISO-код страны (гео-база,
XX только если гео недоступно). Узлы с одинаковым host:port, но разными
креденшелами, получают стабильный суффикс-хеш от креденшела — имена воспроизводимы
между циклами. Оригинальное сырое имя хранится только в БД (Node.Name) — оно
нужно для точного маркера мёртвых узлов и чтобы не дублировать узлы при пинге; в
выходные подписки уходит только нормализованное имя.
Менеджер запускает встроенный HTTP-сервер, который отдаёт файлы из out/ по
в запрошенном формате. По умолчанию слушает на 127.0.0.1:18080 (переопределяется
флагом -serve-addr; -serve-addr "" — выключить).
Если передан -serve-token <токен>, доступ к файлам только по пути /s/<токен>/:
http://127.0.0.1:18080/s/mysecret/singbox.json
http://127.0.0.1:18080/s/mysecret/v2rayn.txt
http://127.0.0.1:18080/s/mysecret/clash.yaml
При попытке обратиться без токена → 404. Пустой путь /s/<токен>/ покажет
список файлов.
Если токен не задан, файлы отдаются из корня:
http://127.0.0.1:18080/singbox.json
Открой зону Publish в веб-панели, чтобы управлять публикацией подписок:
s— включить/выключить HTTP-сервер (или управление через флаги при запуске).d— обнаружить установленные nginx/caddy/apache и показать их пути.g <path>— сгенерировать snippet обратного проксирования nginx (напр.g /vpn-sub):location /vpn-sub/ { proxy_pass http://127.0.0.1:18080/s/<token>/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }w <n> <path>— (НЕ реализовано) запись snippet вsites-enabledвыбранного HTTPD; на данный момент реализована только генерация snippet (g), сама запись на диск отсутствует — snippet нужно скопировать вручную.
Чтобы опубликовать подписки и админку за пределами localhost (через свой HTTPS-домен), повесь обратный прокси. Готовые snippet'ы для обоих сервисов также показываются прямо в зоне Publish веб-панели (поля «nginx snippet · подписки» и «nginx snippet · админка (SSE)») — их можно скопировать как есть.
# /etc/nginx/sites-enabled/vpn-sub
# замени /vpn-sub/ и mysecret на свои
location /vpn-sub/ {
proxy_pass http://127.0.0.1:18080/s/mysecret/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}Подписки станут доступны по https://твой-домен/vpn-sub/singbox.json и т.д.
Админка монтируется под секретным префиксом /<web-secret>/ и стримит живой статус
по SSE — поэтому прокси обязательно отключает буферизацию, иначе интерфейс не
будет обновляться в реальном времени.
# /etc/nginx/sites-enabled/vpn-admin
# замени myverylongwebsecret12+12 на свой -web-secret
location /myverylongwebsecret12+12/ {
proxy_pass http://127.0.0.1:8090/myverylongwebsecret12+12/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE: держим стрим без буферизации
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 1d;
}Открывай админку по https://твой-домен/<web-secret>/.
| Флаг | По умолчанию | Описание |
|---|---|---|
-state |
~/.config/vpn-sub-manager/state.db |
путь к БД состояния |
-assets |
…/assets |
каталог гео-базы и ассетов |
-out |
…/out |
каталог сгенерированных подписок |
-interval |
30m |
интервал обновления планировщика |
-topn |
5 |
сколько лучших узлов на страну (зажимается 3..5) |
-degrade |
0 |
порог деградации задержки (мс); 0 — медианная логика |
-minkeep |
1 |
минимум сохраняемых версий подписки |
-seed |
"" |
засеять источники из файла и выйти (заменяет список) |
-serve-addr |
127.0.0.1:18080 |
адрес встроенного HTTP-сервера подписок ("" — выкл.) |
-serve-token |
"" |
секретный токен пути (/s/<token>/); пусто — без авторизации |
-web-addr |
127.0.0.1:8090 |
адрес веб-интерфейса управления ("" — выкл.) |
-web-token |
"" |
обязательный токен (Bearer) для веб-интерфейса; пусто — веб не стартует |
-web-secret |
"" |
секретный префикс пути (≥24 символа / «12+12»); весь UI+API монтируется под http://<addr>/<secret>/; пусто — веб не стартует |
Пример:
./sub-manager -interval 15m -topn 4 -out /srv/vpn/out- Планировщик работает в памяти процесса: тикер
time.Tickerзапускает циклы обхода источников → фильтрации → пинга → выбора → генерации. - Движок: встроенный mihomo (Go-модуль, часть бинарника) — никакой загрузки ядра из сети и SHA-проверки не требуется.
- GeoIP:
GeoLite2-Country.mmdbберётся изsapics/ip-location-db(релизlatest; один раз, далее офлайн). Если скачать не удалось — используется имя из#fragmentузла. - Самовосстановление: мёртвые/деградирующие узлы вытесняются живыми кандидатами
той же страны; узлы, мёртвые
Kциклов подряд (флаг-corpse/CorpseCycles, по умолчанию 5,0— выключено), пропускаются при пинге, чтобы не тратить пробы. На первом цикле часть узлов может прийти «мёртвыми», пока mihomo-хаб не поднял порт, — последующие циклы исправляют это сами (см. ниже).
- Белый список протоколов: Trojan, VLESS (
tls/reality), VMess (tls), Hysteria2, TUIC, и Shadowsocks — только AEAD-шифры (aes-256-gcm,aes-128-gcm,chacha20-ietf-poly1305,xchacha20-ietf-poly1305,2022-blake3-aes-256-gcm,2022-blake3-aes-128-gcm,2022-blake3-chacha20-poly1305). Не-AEAD Shadowsocks (cfb/rc4/none/…) и WireGuard отбрасываются фильтромDropUnsupported; любой SS-плагин (obfs/v2ray-plugin) отбрасываетсяDropMalware. - Только HTTPS-источники: не-https ссылки отклоняются и никогда не сохраняются.
- Генераторы строят выходные структуры/URI строго из известных полей узла — опасный ключ не может «протечь» в твою подписку.
- Egress-изоляция: тестовый конфиг mihomo использует
freedom-outbound (прямой egress), а кандидат-узел выступает целью SOCKS5 CONNECT; хост-трафик никогда не маршрутизируется через узел как транзит (свойство безопасности сохраняется), а проба измеряет задержку TCP-соединения с узлом — не задержку «туннеля через узел». - Малварь: эвристики (в рамках
Node.Extra) отбрасываютexec/command/script/hijack/routeиssconf://-ссылки на исполняемые пейлоады. - Незащищённые узлы (VLESS без
tls/reality,security=none,insecure=1/allow_insecure/skip-cert-verify) отбрасываются фильтромDropInsecure. - Скрытый путь админки: весь веб-UI и API монтируются только под секретным
префиксом
http://<web-addr>/<web-secret>/(длина секрета ≥ 24 символа / «12+12»). Запросы вне этого пути (в т.ч./и/api/...) получают404— панель невозможно найти перебором. Поверх скрытого пути обязателен Bearer-токен (-web-token); секрет пути и токен действуют совместно (оба нужны).
- Все узлы мертвы на самом первом цикле — скорее всего, mihomo-хаб ещё не успел
поднять порт, когда движок начал пинговать. Это исправлено: движок
теперь ждёт, пока mihomo-хаб реально привяжет порт, прежде чем слать пробы. Если видишь
это на очень медленной машине, подними таймаут готовности движка в
internal/mihomo(по умолчанию 5 с). На последующих циклах всё само восстановится. - Узлы мертвы стабильно — проверь исходящий egress из хоста/контейнера к IP
узлов (например,
curl -I https://github.com). Менеджер пингует реальные удалённые узлы, а не локальные заглушки. - Источник не скачивается — убедись, что ссылка
https://и (для github.com) это либо raw-файл, либо/raw/branch/path, либо корень репозитория. Форматы кандидатов:.txt,.yaml,.yml,.jsonлибо имена сsub/subscribe/subscription. - Планировщик не видит новый источник — источник должен быть включён
(вкл/выкл в зоне Sources веб-панели, или добавлен через
-seed). Только enabled-источники обходятся.
Пример юнита /etc/systemd/system/vpn-sub-manager.service:
[Unit]
Description=Sub Manager VPN (free VPN subscription manager)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/sub-manager -serve-addr 127.0.0.1:18080 -serve-token mysecret
Restart=on-failure
RestartSec=10
# раскомментируй, чтобы засеять источники один раз:
# ExecStartPre=/usr/local/bin/sub-manager -seed /etc/vpn-sub-manager/sources.txt
[Install]
WantedBy=multi-user.targetsudo cp sub-manager /usr/local/bin/
sudo systemctl daemon-reload
sudo systemctl enable --now vpn-sub-manager
journalctl -u vpn-sub-manager -fПодписки появятся в ~/.config/vpn-sub-manager/out/. Встроенный HTTP-сервер
отдаёт их по http://127.0.0.1:18080/s/<token>/ (или без токена — в корне).
Подключи в свой клиент (sing-box / v2rayN / Clash.Meta) напрямую файлом или URL.
make build # go build ./...
make vet # go vet ./...
make test # go test ./... (полный набор: юнит + интеграция)
make test-race # go test -race ./... (перед релизами)
make run # go run .
make clean # go clean ./...Интеграционный тест internal/integration (TestIntegrationRealMihomo) поднимает
встроенный mihomo-хаб, синхронизирует узел и пропинговывает его; при отсутствии
реального egress к узлу проба возвращает dead и тест чисто пропускается
(никакой загрузки ядра/сети не требуется).
Пайплайн (6 стадий + подсистемы):
whitelist (config) → fetch → parse → filter → test (ping через mihomo) → select → generate (3 формата)
│ │
└────────── state (SQLite) ◄── scheduler (24/7) ◄───────┘
│
mihomo (встроен) + geo (mmdb)