Прозрачный VPN-клиент поверх открытого ядра Xray-core. Делает то же, что Happ / v2rayTun (подписки, VLESS+Reality и др.), но весь код обёртки открыт и лежит здесь — видно, что и куда уходит.
| Windows | desktop/Windows/ — Python + PySide6, ядро xray.exe рядом |
| macOS | desktop/macOS/ — нативный Swift, TUN через привилегированный демон (Apple Silicon) |
| Android | android/ — Kotlin, то же ядро внутри процесса (libv2ray.aar) |
| iOS | ios/ — SwiftUI, ядро внутри расширения туннеля (NEPacketTunnelProvider). Поддержка прекращена, см. ниже |
- Подписку — запрос на твой URL подписки, только когда ты нажал «Обновить».
Вместе с запросом уходит идентификатор устройства (
x-hwid, ОС, её версия и модель) — этого требуют панели с лимитом устройств, иначе они отдают заглушку вместо серверов. Сам идентификатор машины наружу не уходит: шлём его SHA-256 с солью, см.desktop/Windows/native/hwid.py,desktop/macOS/Sources/SCVPNCore/HWID.swiftиandroid/.../Hwid.kt. - VPN-трафик — через выбранный тобой сервер; этим занимается ядро Xray.
- Скачивание ядра (Windows и macOS) — один раз тянет
xray+ гео-базы с официального GitHub (XTLS/Xray-core), а для TUN —sing-box(SagerNet) и, только на Windows,wintun.dll(wintun.net). - Проверку соединения — при автоподборе отпечатка короткий запрос через
твой же сервер (
api.ipify.orgна десктопе,gstatic.com/generate_204на Android).
Никакой аналитики, телеметрии, «домашних» серверов и автообновлений нет.
Каждый сетевой вызов видно в коде: desktop/Windows/shared/subscription.py,
desktop/Windows/native/downloader.py, desktop/Windows/shared/connect.py;
android/.../SubscriptionParser.kt, XrayCore.kt.
Обе версии устроены одинаково: приложение не шифрует трафик само — это делает Xray. Наша задача — разобрать ссылки, собрать ядру конфиг, запустить его и завернуть в него системный трафик. Отсюда и деление на слои.
ссылка/подписка → модель Server → конфиг Xray (JSON) → ядро → сервер
↑
системный трафик заводится сюда
Общего кода между десктопами больше нет: Windows — это Python и Qt
(desktop/Windows/, где shared/ — разбор ссылок и конфигов, native/ —
системные вызовы), macOS — нативный Swift (desktop/macOS/) поверх
core-swift/, который он делит с iOS. Раньше Qt-версия работала и на macOS,
и ради этого Python-код лежал уровнем выше, в общей папке; нативная
версия её заменила, и код вернулся туда, где используется.
Одинаковыми остаются не файлы, а решения: одна палитра (её сверяет тест),
один набор форм состояний, одна шкала шрифтов и метрик. Что и почему —
в docs/ui-design.md.
| Слой | Windows | macOS и iOS | Android |
|---|---|---|---|
| Разбор ссылок и подписок | shared/subscription.py |
core-swift/…/Parsing/ |
SubscriptionParser.kt |
| Идентификатор устройства | native/hwid.py |
core-swift/…/HWID.swift |
Hwid.kt |
| Модель сервера | shared/models.py |
core-swift/…/Models/Server.swift |
Model.kt |
| Сборка конфига Xray | shared/xray_config.py |
core-swift/…/XrayConfig/ |
XrayConfig.kt |
| Хранение профилей | shared/storage.py (JSON в data/) |
core-swift/…/Storage/ (тот же JSON) |
Prefs.kt (SharedPreferences) |
| Туннель AmneziaWG | awg_runner.py |
core-swift/…/AWGRunner.swift |
AwgProcess.kt |
Строка про AmneziaWG в таблице — это запуск общего бинарника awg/,
а не три реализации протокола: сам туннель один на все платформы.
macOS и iOS делят один пакет core-swift/ — там же лежат общие проверки.
Профили переносятся между всеми четырьмя версиями файлом profiles.json: на
десктопе он лежит в папке данных, на телефонах есть «Сохранить в файл» и
«Загрузить из файла».
Разбор ссылок покрывает vless://, vmess://, trojan://, ss://,
wireguard:// и подписки (обычный список или base64). Модель Server —
плоский набор полей ссылки; из неё собирается секция outbounds конфига.
Ядро Xray умеет wireguard, но не умеет обфускацию Amnezia (Jc, S1…S4,
H1…H4). У sing-box то же самое. А H1…H4 подменяют тип сообщения WireGuard,
поэтому обычный WireGuard-клиент к такому серверу не подключится вовсе — не
«медленнее», а никак, без единой строки в логе.
Поэтому в репозитории есть awg/ — маленький бинарник scvpn-awg на
официальном amneziawg-go. Он
поднимает туннель юзерспейсным стеком (без TUN и без прав администратора) и
отдаёт его локальным SOCKS5:
трафик → инбаунды Xray → outbound socks → scvpn-awg → AmneziaWG → сервер
Xray остаётся впереди и делает всё то же самое: geosite, обход РФ, блокировку рекламы, DNS. Отличие ровно в одном outbound, поэтому маршрутизация, TUN, раздельное туннелирование и системный прокси общие с остальными протоколами. На Windows у этого есть побочный выигрыш: WireGuard работает и в режиме системного прокси, то есть без прав администратора.
Бинарник в репозиторий не кладётся, как и остальные ядра, — собирается из
исходников: cd awg && ./build.sh. Подробности — в awg/README.md.
Windows (desktop/Windows/) — ядро отдельным процессом, два способа завести трафик:
┌── режим «прокси» ──────────────────────────┐
приложения ─────────┤ системный прокси Windows (реестр) │
│ ↓ 127.0.0.1:HTTP │
│ xray.exe ── inbound socks+http │──→ сервер
└────────────────────────────────────────────┘
┌── режим «TUN» (нужен админ) ───────────────┐
весь трафик ОС ─────┤ wintun-адаптер ← sing-box.exe │
│ ↓ 127.0.0.1:SOCKS │
│ xray.exe │──→ сервер
└────────────────────────────────────────────┘
core_runner.py— запускает и глушитxray.exe, читает его лог;sysproxy.py— правит ключи системного прокси в реестре (без прав админа);tun.py— поднимаетsing-box, который создаёт TUN-адаптер и льёт всё в SOCKS-инбаунд Xray; требует администратора, поэтому есть перезапуск с UAC;downloader.py— разовое скачивание ядра и компонентов TUN;connect.py— автоподбор рабочего TLS-отпечатка (см. ниже);ping.py— TCP-замер, запасной путь: основной пинг идёт через ядро (connect.py);paths.py— все пути в одном месте (bin/,data/,%LOCALAPPDATA%\SCVPN);ui/— интерфейс:main_window.py(экран),widgets.py(кнопка и карточки списка),brandmark.py(фирменный знак),theme.py(палитра и стили).
Важное свойство: и системный прокси, и TUN откатываются при закрытии окна и при
падении ядра (closeEvent, _on_state), чтобы не остаться без интернета.
macOS (desktop/macOS/) — тот же Xray отдельным процессом, те же два способа:
┌── режим «прокси» ──────────────────────────┐
приложения ─────────┤ networksetup на активных сервисах │
│ ↓ 127.0.0.1:HTTP │
│ xray ── inbound socks+http │──→ сервер
└────────────────────────────────────────────┘
┌── режим «TUN» (нужен root) ────────────────┐
весь трафик ОС ─────┤ utun-адаптер ← sing-box (от root) │
│ ↑ поднимает демон по unix-сокету │
│ ↓ 127.0.0.1:SOCKS │
│ xray (от пользователя) │──→ сервер
└────────────────────────────────────────────┘
Отличие от Windows одно, и оно про надёжность. TUN требует root, а если приложение упадёт, снять root-овый sing-box будет некому: он останется держать маршруты, и весь трафик системы уйдёт в мёртвый туннель. Поэтому вместо запроса пароля на каждое подключение здесь стоит LaunchDaemon: приложение держит с ним открытый unix-сокет, и обрыв этого соединения демон читает как «приложение мертво» — и снимает туннель сам, через секунду, а не при следующем запуске.
Демон не принимает готовый конфиг: только параметры, каждый проверяет, конфиг
собирает сам, и запускает лишь бинарники из своей root-овой папки. Сокет открыт
группе admin, и всё, что оттуда приходит, считается недоверенным — см.
helper/config.py и helper/daemon.py.
Режим прокси root не требует: networksetup доступен администратору без пароля.
Прежние настройки прокси пишутся на диск перед включением, поэтому откат
переживает падение приложения. Откатываем при этом только своё: снимок на
диске — он же признак «прокси ставили мы», а порт в нём сверяется с тем, что
стоит в системе. Иначе SCVPN стирал бы настройки другого клиента (Happ,
Tailscale и подобные тоже живут на 127.0.0.1) просто по факту запуска.
Android (android/) — ядро внутри процесса приложения, трафик один способ:
весь трафик ОС → VpnService (TUN, fd) → libhev-socks5-tunnel (.so)
↓ 127.0.0.1:10808 SOCKS
Xray внутри процесса (libv2ray.aar) ──→ сервер
ScVpnService.kt— поднимает TUN черезVpnService.Builder, стартует ядро, запускает hev-мост; подъём вынесен в отдельный поток, потому что автоподбор отпечатка ходит в сеть, аonStartCommandвыполняется на главном;XrayCore.kt— обёртка надlibv2ray.aar(запуск, остановка, замер задержки);TProxyService.kt— JNI-обёртка надlibhev-socks5-tunnel.so. Единственный класс в чужом пакетеcom.v2ray.ang.service: под это имя .so был собран,RegisterNativesищет именно его;VpnState.kt— состояние туннеля и его рассылка в экран broadcast-ом, чтобы кнопка показывала реальное состояние, а не догадку по таймеру;MainActivity.kt— единственный экран.
Петля исключена через addDisallowedApplication(packageName): трафик самого
Xray к серверу идёт мимо TUN, иначе он заворачивал бы сам себя.
В свежих сборках Xray отпечаток chrome шлёт пост-квантовую кривую, которую
часть серверов не понимает, а randomized нестабилен. Поэтому перед
подключением клиент быстро пробует firefox/safari/edge/ios и берёт первый
рабочий. На десктопе (Windows и macOS) это общий shared/connect.py (можно
зафиксировать в меню «Отпечаток TLS»), на Android —
sanitizeFingerprint() плюс перебор в XrayCore.pingServer.
Подробности — в desktop/Windows/README.md, desktop/macOS/README.md и
android/README.md. Коротко:
# Windows: exe + установщик
cd desktop\Windows
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
build.bat # dist\SCVPN\SCVPN.exe
build_installer.bat # dist_installer\SCVPN-Setup-*.exe# macOS (Apple Silicon): SCVPN.app
cd desktop/macOS
./build.sh # dist/SCVPN.app# Android: APK
cd android
build_apk.bat # app\build\outputs\apk\debug\app-debug.apkСторонние бинарники (libv2ray.aar, libhev-socks5-tunnel.so, xray/xray.exe,
sing-box/sing-box.exe, wintun.dll) в репозиторий не кладутся — откуда их
взять написано в README соответствующей папки.
Версия написана целиком и работает — серверы, подписки, пинг, автоподбор отпечатка, QR, перенос профилей, — кроме самого туннеля.
Причина не в коде. Расширению нужен entitlement
com.apple.developer.networking.networkextension, а его Apple выдаёт только
участникам Developer Program ($99 в год). Бесплатный Apple ID отвечает прямо:
«Personal development teams do not support the Network Extensions capability»,
и профиль не подписывается. Держать платную подписку и продлевать её каждый
год ради одной платформы мы не готовы, поэтому версия дальше не развивается.
Код остаётся в репозитории: он собирается, проверки зелёные, приложение
ставится на устройство (ios/run-on-device.sh) и работает без подключения.
Если аккаунт появится, доводить нужно с того места, где остановились, —
Фаза 0 плана: подпись с entitlement, замер памяти расширения, проверка
туннеля на живом устройстве. Сама схема «ядро в процессе + мост + TUN»
проверена живьём на Android и работает.
Windows, macOS и Android поддерживаются как прежде.
Одна траектория из двух касающихся дуг, обведённая штрихом с круглыми концами.
Иконка приложения нигде не рисуется — ни на лету, ни при сборке. На каждой платформе она лежит в git уже в том формате, который просит система:
| Платформа | Файл | Что внутри |
|---|---|---|
| Windows | desktop/Windows/setup/scvpn.ico |
16, 32, 48, 64, 128, 256 px |
| macOS | desktop/macOS/Resources/scvpn.icns |
16…1024 px, включая @2x |
| Android | res/mipmap-anydpi-v26/ic_launcher.xml + res/mipmap-*/ic_launcher.png |
адаптивная векторная, PNG 48…192 px — fallback для API 24-25 |
| iOS | ios/SCVPN/Assets.xcassets/AppIcon.appiconset/icon-1024.png |
1024 px, остальное режет Xcode |
Скриптов, которые их рисуют, в проекте нет: знак меняется раз в несколько лет, и держать ради этого генератор, Pillow в зависимостях и шаг в сборке дороже, чем перерисовать иконку руками, когда понадобится. У macOS отличие от Windows-иконки одно: плашка занимает 80 % холста, вокруг прозрачное поле — иначе иконка в доке выглядит крупнее соседних.
В интерфейсе тот же знак рисуется векторно (desktop/Windows/ui/brandmark.py,
core-swift/.../BrandmarkShape, res/drawable/ic_launcher_foreground.xml) — это
не иконка, а элемент экрана: он перекрашивается по состоянию и тянется на любой
размер, поэтому картинкой его заменять нечем.
Геометрия во всех местах одна и та же, поэтому знак нигде не разъезжается.
- iOS-клиент
- Свой транспортный протокол
- Эксперименты со своим ядром вместо Xray
Обёртка — код этого репозитория. Используемые открытые проекты: Xray-core (MPL-2.0), sing-box (GPL-3.0), hev-socks5-tunnel (MIT), wintun (GPL-2.0).