Новый VLESS клиент на Windows или почему HAPP стоит выкинуть на мусорку
На первый взгляд писать ещё один клиент для VLESS в 2026 году — странная затея. Есть v2rayN, HAPP, Nekoray и другие приложения, а сетевые ядра Xray и sing-box уже решают большую часть низкоуровневых задач.
Проблема начинается не с подключения к серверу, а со сценариев вокруг него.
Например, мне хотелось:
вставить ссылку подписки и не выяснять заранее, в каком она формате;
использовать системный прокси для браузера, но при необходимости переключаться на полноценный TUN;
отправлять одни приложения через VPN, другие напрямую;
не редактировать JSON ради добавления одного домена;
обновлять подписку без сброса выбранного сервера;
использовать Zapret отдельно или одновременно с VPN;
видеть понятную ошибку, если ядро не приняло конфигурацию.
Большинство существующих клиентов умеют хотя бы часть этого. Но интерфейс, формат подписки и логика маршрутизации у каждого свои. В какой-то момент я решил собрать нужный мне сценарий в одном приложении.
Так появился Lumen KVN — открытый клиент для Windows на Python, PyQt6 и QML, работающий одновременно с Xray Core и sing-box extended.

Клиент — это не ядро
Сам Lumen не реализует VLESS, Reality, WireGuard или TUN. Этим занимаются готовые сетевые ядра. Задача приложения — принять конфигурацию, преобразовать её в подходящий формат, запустить нужный процесс и объяснить пользователю, если что-то пошло не так.
Упрощённо архитектура выглядит так:
┌───────────────────┐
│ QML-интерфейс │
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ AppController │
│ состояние и логика│
└──────┬───────┬────┘
│ │
┌────────────▼─┐ ┌─▼──────────────┐
│ Xray Core │ │ sing-box ext. │
│ HTTP / SOCKS │ │ TUN / Wintun │
└──────────────┘ └────────────────┘
│
┌──────▼──────┐
│ Windows API │
│ proxy/routes│
└─────────────┘
Интерфейс написан на QML. Python связывает его с моделями серверов, настройками, подписками и внешними процессами.
Выбор Python здесь не связан с производительностью сетевого трафика: пакеты обрабатывают Xray, sing-box, Wintun и WinDivert в отдельных процессах и драйверах. Python в основном управляет их жизненным циклом и формирует конфигурации.
Обратная сторона такого стека — размер сборки и реакция антивирусов. PyInstaller-приложение, внутри которого лежат сетевые ядра, Wintun, WinDivert и несколько исполняемых файлов, выглядит для эвристики Windows Defender довольно подозрительно. Подписи кода у проекта пока нет, поэтому ложные срабатывания возможны.
Зачем понадобились два сетевых ядра
Изначально хотелось обойтись одним ядром, но на практике Xray и sing-box удобны в разных сценариях.
Xray Core используется преимущественно для системного прокси:
VLESS;
VMess;
Trojan;
Shadowsocks;
HTTP и SOCKS;
Reality и XHTTP.
Клиент поднимает локальные HTTP- и SOCKS-порты, после чего может прописать их как системный прокси Windows. Этот режим относительно лёгкий и подходит приложениям, которые учитывают системные настройки прокси.
sing-box extended используется для TUN и конфигураций, которым требуется работа на сетевом уровне:
WireGuard;
AmneziaWG;
WARP;
Hysteria и Hysteria2;
TUIC;
Mieru;
MASQUE;
пользовательские конфигурации sing-box.
В TUN-режиме создаётся виртуальный адаптер Wintun. Через него можно направить трафик программ, которые ничего не знают о системном прокси: игр, лаунчеров, UDP-приложений и системных служб.
Для отдельных конфигураций возможна гибридная схема:
Приложение
│
▼
sing-box TUN
│
▼
локальный relay
│
▼
Xray
│
▼
VPN-сервер
Это позволяет оставить перехват трафика на стороне sing-box, но использовать Xray там, где он лучше совместим с конкретным VLESS-транспортом.
Минус очевиден: два ядра означают два формата конфигурации, разные сообщения об ошибках и больше пограничных случаев. Значительная часть кода Lumen как раз занимается согласованием этих различий.
Подписка — это не формат
Со словом «подписка» связана отдельная проблема. Формально это может быть почти что угодно:
обычный URL, возвращающий список ссылок;
base64 со строками
vless://;Clash YAML;
JSON Xray;
outbound или полный конфиг sing-box;
WireGuard INI;
ссылка Happ Crypt;
простой текст, скопированный из Telegram.
Поэтому импорт построен как последовательность проверок:
Входные данные
│
├─ JSON? ───────────────► разбор Xray/sing-box
│
├─ Clash YAML? ─────────► разбор proxies
│
├─ [Interface]/[Peer]? ─► WireGuard/AWG
│
└─ обычный текст ───────► построчный разбор URI
Для URI схема определяет дальнейший парсер:
vless://
vmess://
trojan://
ss://
hysteria2://
tuic://
wireguard://
awg://
warp://
mieru://
masque://
Ошибочная строка не должна отменять весь импорт. Корректные серверы добавляются, а проблемы собираются отдельно и выводятся пользователю.
Есть и ограничения на размер входных данных: импортировать бесконечный JSON или несколько миллионов строк из недоверенного источника — плохая идея. Сейчас парсер ограничивает объём файла, количество строк и итоговое число узлов.
После разбора сервер приводится к внутренней модели Lumen. Уже из неё строится конфигурация для выбранного ядра.
Что делать с Happ Crypt
Некоторые провайдеры не отдают обычный URL подписки, а используют ссылки следующего вида:
happ://crypt/...
happ://crypt2/...
happ://crypt3/...
happ://crypt4/...
happ://crypt5/...
Передать такую ссылку напрямую в Xray или sing-box нельзя: внутри находится зашифрованная полезная нагрузка.
В Lumen расшифровка выполняется локально. Старые варианты Happ Crypt используют RSA-блоки, более новые — схему с ChaCha20-Poly1305 и дополнительным преобразованием полезной нагрузки.
После расшифровки результат снова отправляется в обычный конвейер импорта. Внутри может оказаться URL, список URI или готовая конфигурация.
happ://crypt5/...
│
▼
локальная расшифровка
│
▼
URL / JSON / список серверов
│
▼
обычный парсер подписок
Внешний конвертер для этого не используется.
При этом гарантировать вечную совместимость невозможно. Формат не является открытым стандартом, поэтому очередное изменение на стороне HAPP может потребовать обновления клиента.
Маршрутизация без ручного JSON
Следующей задачей было разделение трафика.
В простейшем варианте пользователь выбирает один из готовых режимов:
всё через VPN;
через VPN только выбранные сервисы;
всё через VPN, кроме российских ресурсов.
За этим стоят обычные правила с действиями proxy, direct и block.
Например:
youtube.com → proxy
github.com → proxy
router.local → direct
ads.example.com → block
Можно добавлять IP-адреса и подсети:
192.168.0.0/16 → direct
10.0.0.0/8 → direct
203.0.113.10/32 → proxy
А в TUN-режиме — правила для процессов:
discord.exe → proxy
telegram.exe → proxy
steam.exe → direct
Для популярных сервисов есть готовые наборы доменов. Они не заменяют полноценную маршрутизацию, но избавляют от необходимости вручную искать адреса CDN, API и авторизации.

Отдельно обрабатываются локальные сети. Если отправить адрес роутера или NAS в туннель, пользователь потеряет к ним доступ сразу после подключения. Поэтому частные подсети можно автоматически направлять напрямую.
DNS оказался сложнее маршрутизации
В TUN-режиме недостаточно просто перехватить IP-трафик. Если DNS продолжит работать отдельно от правил маршрутизации, появляются странные эффекты:
сайт резолвится в недоступный IPv6-адрес;
DNS-запрос уходит напрямую, хотя сам сайт должен идти через прокси;
адрес VPN-сервера пытается разрешиться через ещё не запущенный туннель;
домен получает один IP снаружи туннеля и другой внутри;
некоторые приложения полностью обходят выбранный DNS.
Поэтому в Lumen есть прямые DNS-серверы и DNS через прокси. Для каждого варианта можно отдельно выбрать транспорт и стратегию IPv4/IPv6.
Пример базовой схемы:
Direct DNS:
1.1.1.1
8.8.8.8
transport: UDP
strategy: IPv4 only
Proxy DNS:
cloudflare-dns.com
dns.google
transport: HTTPS
strategy: IPv4 only
В TUN можно включить перехват DNS-запросов и Fake DNS/FakeIP. Последний вариант особенно полезен для приложений, которые сначала самостоятельно разрешают домен, а затем подключаются уже к готовому IP-адресу: без FakeIP движок может потерять информацию о первоначальном домене.
Адрес самого VPN-сервера при этом должен разрешаться и маршрутизироваться отдельно, иначе легко получить цикл:
Для запуска VPN нужен DNS
▲
│
DNS отправляется через VPN
▲
│
VPN ещё не запущен
Обновление подписки без полного сброса
Наивная реализация обновления выглядит просто:
nodes.clear()
nodes.extend(download_subscription())
Но вместе со старым списком исчезает состояние интерфейса:
выбранный сервер;
результаты пинга;
результаты проверки скорости;
порядок узлов;
информация о текущем подключении.
Поэтому Lumen пытается сопоставить серверы до и после обновления. Если узел сохранился в подписке, сохраняется и связанное с ним локальное состояние.
Активный сервер переключается только тогда, когда он действительно исчез из новой версии подписки или его конфигурация стала несовместимой.
Сетевая загрузка выполняется в отдельном Qt-потоке. Это тоже оказалось важной деталью: зависший HTTP-запрос не должен замораживать интерфейс, а отмена импорта не должна уничтожать ещё работающий QThread.
Проверка сервера — это не проверка порта
Открытый TCP-порт ещё не означает, что VPN-сервер работает.
Сервер может принять соединение, но затем отклонить:
UUID;
пароль;
Reality-параметры;
TLS handshake;
transport path;
short ID;
flow.
Поэтому для проверки задержки Lumen запускает реальное прокси-подключение и выполняет небольшой HTTP-запрос через тестируемый узел.
Проверка скорости работает похожим образом, но загружает ограниченный объём данных в течение заданного времени.
Тесты выполняются параллельно, однако число одновременных процессов ограничивается. Иначе подписка на несколько сотен серверов способна одновременно запустить столько проверок, что измеряться будет уже производительность компьютера пользователя.
Где здесь Zapret и droute
Lumen можно использовать не только как VPN-клиент.
В приложение встроено управление Zapret 2: можно выбирать, запускать и редактировать пресеты обхода DPI. Zapret работает через WinDivert и может использоваться отдельно от VPN.

Есть и отдельный сценарий для Discord Voice. droute направляет голосовой и стриминговый трафик Discord через локальный SOCKS5-прокси без включения полного TUN.
Получается три независимых режима:
Системный прокси → браузеры и приложения с поддержкой proxy
TUN → весь системный и UDP-трафик
Zapret / droute → отдельные сценарии без полного VPN
Их можно использовать раздельно, а некоторые — одновременно.
Что пока не идеально
Проект молодой, поэтому перечислю ограничения без маркетинга.
Во-первых, Lumen сейчас ориентирован только на Windows 10 и 11. TUN, системный прокси, WinDivert и управление маршрутами тесно связаны с Windows API.
Во-вторых, для TUN и Zapret требуются права администратора.
В-третьих, поддержка большого количества форматов увеличивает вероятность несовместимости. Два провайдера могут называть подпиской совершенно разные данные, а параметры одного VLESS URI иногда трактуются клиентами по-разному.
В-четвёртых, использование сразу двух ядер усложняет диагностику. Ошибка может возникнуть в парсере, генераторе конфигурации, Xray, sing-box, Wintun или системной маршрутизации Windows.
Наконец, неподписанные PyInstaller-сборки с сетевыми компонентами могут вызывать предупреждения антивирусов. Исходный код открыт, но для обычного пользователя это всё равно выглядит неприятно.
Что получилось в итоге
Lumen KVN не пытается заменить Xray или sing-box. Это оболочка, которая объединяет несколько повседневных сценариев:
Получить подписку в одном из распространённых форматов.
Преобразовать её в понятную внутреннюю модель.
Выбрать Xray или sing-box в зависимости от режима.
Построить конфигурацию прокси, TUN, DNS и маршрутизации.
Запустить ядро и проверить, что оно действительно готово.
Показать пользователю нормальную ошибку, если запуск не состоялся.
Проект распространяется под GPL-3.0. Есть обычный установщик и portable-версия.
Исходный код и релизы:
github.com/krambovic/Lumen-KVN
Больше всего сейчас интересуют отчёты о несовместимых подписках, TUN на разных версиях Windows и конфигурациях, которые работают в других клиентах, но не импортируются в Lumen.
Для полезного баг-репорта желательно приложить:
версию Lumen;
используемое ядро;
режим Proxy или TUN;
обезличенный конфиг;
последние строки журнала;
описание ожидаемого и фактического поведения.
К критике в духе «зачем ещё один клиент» тоже готов. Если коротко — потому что клиентская часть оказалась не менее интересной задачей, чем сами сетевые протоколы.