- Go 81.5%
- Python 11.6%
- Shell 5.3%
- Dockerfile 1.6%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| acme | ||
| cover-nginx | ||
| gateway | ||
| tt-image | ||
| .gitignore | ||
| docker-compose.yml | ||
| README.md | ||
CaviCodeVPN Plugin: TrustTunnel
Traffic-плагин для CaviCodeVPN, обеспечивающий маскировку VPN-трафика под легитимный HTTPS через протокол TrustTunnel.
Компоненты
| Директория | Назначение |
|---|---|
gateway/ |
Go L4-классификатор на :443. Парсит TLS ClientHello, маршрутизирует по стего-маркеру в Random[0..32]. Без совпадения — splice на сайт прикрытия |
tt-image/ |
Docker-образ TT endpoint + socat sidecar. No-auth режим на loopback, TCP и DNS/UDP идут через SOCKS5 Egress Core |
cover-nginx/ |
Реверс-прокси на настраиваемый внешний HTTPS-сервис (по умолчанию www.wikipedia.org). Pass-through для CDN. ACME HTTP-01 на :80 |
acme/ |
Certbot sidecar. Получает, обновляет и вручную применяет TLS-сертификаты, рестартит потребителей TLS |
Как это работает
Интернет :443
│
▼
Gateway ── маркер совпал ──► tt-shared (TT endpoint)
│ │
└── нет совпадения ──► cover-nginx (маскируемый внешний сервис)
│
SOCKS5 Egress (Core)
│
Интернет
- Клиент подключается к
vpn.example.com:443с deeplink'ом - Gateway читает начало TLS ClientHello, извлекает
Random[0..32] - Проверяет стего-маркер
(prefix & mask)— при совпадении splice в TT endpoint - TT endpoint работает без авторизации, прокидывает credentials клиента в SOCKS5 Egress через
extended_auth - SOCKS5 Egress валидирует login/password и выпускает TCP CONNECT и UDP ASSOCIATE в интернет
Если в логах Gateway есть marker_match, а клиент после VPN_SS_CONNECTED получает Authorization Required (5) или HTTP/2 407, маршрут через плагин уже найден. Следующий участок диагностики — контейнер SOCKS5 Egress Core: проверьте, что credential sync получил активные подписки, логин из профиля существует, пароль не устарел после ротации, подписка активна, не истекла и не упёрлась в лимит трафика.
Если клиент падает на Unexpected protocol, а Gateway пишет h2 ClientHello has no marker match, TLS дошёл до Gateway, но маркер ClientHello не совпал с registry/Core и соединение ушло в cover-nginx, где ALPN h2 не выбирается. Сверьте client_random в TOML/deeplink с активным TLS Prefix в настройках TT Gateway/Core; после смены TLS Prefix старые профили нужно перегенерировать.
Go Gateway принимает решение сразу после извлечения Random: если полный ClientHello ещё не дочитан, но marker уже известен, соединение всё равно уходит в tt-shared или cover-nginx, а недочитанный хвост handshake передаётся upstream через обычный splice. Это снижает риск ложного Decoy/read_timeout на клиентах, которые отправляют ClientHello фрагментированно или медленно.
Каждая запись Gateway connection log содержит preludeBytes — сколько байт Gateway успел прочитать до классификации/таймаута. preludeBytes=0 при read_timeout означает, что TCP был принят, но клиент не прислал ни одного байта ClientHello; preludeBytes>0 без randomPrefixHex означает неполный или оборванный ClientHello до поля Random. Если randomPrefixHex заполнен, Gateway уже видел TLS Random и можно сравнивать его с client_random профиля и активным TLS Prefix.
Если в Core UI во вкладке Логи → Соединения нет записей даже после попытки подключения, проверьте системные логи контейнера cavicodevpn-plugin-tt-gateway. При проблеме доставки batch-а Gateway пишет connection log flush failed: status=...; чаще всего это означает неверный plugin token, недоступный Core URL или ошибку вставки в gateway_connection_logs. При успешной инициализации логгера должна быть строка connection log flush loop started.
Установка
Плагин устанавливается через панель управления CaviCodeVPN Core:
- Откройте Плагины → Установка
- Git URL: URL этого репозитория
- Имя:
tt, Тип:Traffic - Нажмите «Установить» — Core клонирует, соберёт и запустит все 4 контейнера
Настройка
После установки настройте плагин через Плагины → карточка tt → шестерёнка:
Плагин предоставляет собственный интерфейс настроек (iframe) со следующими параметрами:
| Параметр | Описание | Обязателен |
|---|---|---|
cover_hostname |
Публичный домен VPN: DNS должен указывать на IP сервера; используется для TLS, SNI, deeplink и TOML профилей | Да |
cover_target_hostname |
Маскируемый внешний HTTPS-сервис для обычных запросов без TT-маркера, например www.wikipedia.org |
Нет, по умолчанию www.wikipedia.org |
acme_email |
Email для уведомлений Let's Encrypt | Для боевого ACME |
acme_staging |
Тестовый режим LE (без rate limit, сертификат не доверен) | Нет |
| TLS Prefix | Текущий стего-маркер Gateway в формате prefix или prefix/mask; применяется только отдельной кнопкой Применить TLS Prefix |
Нет |
marker_length |
Длина стего-маркера в байтах для первичной генерации, если manual marker ещё не задан (по умолчанию 6) | Нет |
marker_percent |
Плотность значимых бит маски для первичной генерации, если manual marker ещё не задан (по умолчанию 70) | Нет |
grace_period_days |
Grace для временно пропавших из sync подписок (по умолчанию 7) | Нет |
project_name |
Название проекта в имени профиля подключения (по умолчанию CaviCodeVPN) |
Нет |
node_name |
Название ноды в имени профиля подключения (по умолчанию Germany) |
Нет |
client_ipv6_enabled |
Включает has_ipv6 = true и публичный IPv6 route 2000::/3 в новых TOML-профилях. Включайте только если SOCKS5 Egress имеет рабочий IPv6 outbound |
Нет, по умолчанию выключено |
tt_icmp_enabled |
Прокидывание ICMP/ping через TrustTunnel endpoint (по умолчанию включено) | Нет |
tt_icmp_interface |
Сетевой интерфейс контейнера shared для raw ICMP socket (по умолчанию eth0) |
Нет |
После сохранения настроек нажмите Перезапустить — контейнеры пересоздаются с новыми переменными окружения. ACME контейнер автоматически запросит сертификат для указанного домена.
Если сервер переводится с acme_staging=true на боевой ACME, сохраните настройки,
перезапустите плагин и нажмите Выпустить заново в блоке TLS-сертификата:
обычный renewal может считать существующий staging-сертификат актуальным и не
заменить его production-цепочкой без принудительного выпуска.
В этом же блоке можно обновить список Certbot/manual сертификатов, применить
выбранный сертификат или загрузить свой PEM fullchain + private key вручную.
Протокол Config UI (postMessage)
Iframe плагина взаимодействует с Core через window.postMessage:
- Core → iframe:
{ type: 'cavicodevpn-plugin-load-settings', settings: {...} }— загрузка текущих настроек при открытии - iframe → Core:
{ type: 'cavicodevpn-plugin-save-settings', settings: {...} }— сохранение - iframe → Core:
{ type: 'cavicodevpn-plugin-request-settings' }— повторный запрос настроек - iframe → Core:
{ type: 'cavicodevpn-plugin-action', path: '/marker/apply', body: { markerHex: 'aabb/fff0' } }— явное callback-действие плагина через Core proxy; для сертификатов используются/certificates/list,/certificates/select,/certificates/renewи/certificates/upload - Core → iframe:
{ type: 'cavicodevpn-plugin-action-result', ok: true, result: {...} }— результат callback-действия
Передача настроек в контейнеры
Настройки из JSON передаются как env-переменные с префиксом CaviCodeVPN_:
cover_hostname→CaviCodeVPN_COVER_HOSTNAMEcover_target_hostname→CaviCodeVPN_COVER_TARGET_HOSTNAMEacme_email→CaviCodeVPN_ACME_EMAILacme_staging→CaviCodeVPN_ACME_STAGING(bool:true→1,false→0)project_name→CaviCodeVPN_PROJECT_NAMEnode_name→CaviCodeVPN_NODE_NAMEclient_ipv6_enabled→CaviCodeVPN_CLIENT_IPV6_ENABLEDtt_icmp_enabled→CaviCodeVPN_TT_ICMP_ENABLEDtt_icmp_interface→CaviCodeVPN_TT_ICMP_INTERFACE
Gateway загружает настройки из Core API при старте: GET /api/plugin-api/traffic/settings.
Генерация клиентского профиля дополнительно перечитывает актуальные настройки из Core, поэтому сохранённый cover_hostname сразу попадает в deeplink/TOML и не заменяется старым localhost. Для применения cover_target_hostname к nginx-прикрытию перезапустите плагин из Core UI.
Некоторые крупные сайты могут отдавать пустой ответ или антибот-страницу при проксировании из серверной сети. Например, vk.com в части окружений отвечает HTTP 418 с пустым телом. В таком случае смените cover_target_hostname на сервис, который стабильно отдаёт обычную HTML-страницу с IP вашего сервера.
ICMP не проходит через SOCKS5 Egress: SOCKS5 не поддерживает ICMP, поэтому TrustTunnel endpoint отправляет ping через raw socket из контейнера shared. Для этого контейнер получает CAP_NET_RAW, а бинарник trusttunnel_endpoint собран с cap_net_raw.
Контейнер shared на старте резолвит cavicodevpn-core-egress и предпочитает IPv4-адрес для внутреннего SOCKS5 upstream. Если в Docker-сети доступен только IPv6, адрес записывается в TOML как [IPv6]:port, чтобы trusttunnel_endpoint получил валидный socket address.
Генерация клиентских конфигураций
Gateway предоставляет callback для Core:
GET /client-config/options
POST /client-config
/client-config/options отдаёт поддержанные transport-режимы для Core UI.
Сейчас Gateway публикует только http2 с defaultFormat=toml; http3 не
показывается в панели, пока Gateway не начнёт реально отдавать HTTP/3-профиль.
Запрос:
{
"subscriptionId": "guid",
"login": "subscription-login",
"plainPassword": "одноразово известный пароль",
"transport": "http2",
"filterProfile": {
"vpnMode": "general",
"exclusions": ["example.com", "*.example.org", "203.0.113.10"],
"excludedRoutes": ["203.0.113.0/24"],
"includedRoutes": null
}
}
Ответ:
{
"deeplink": "tt://?...",
"toml": "vpn_mode = \"general\"...",
"markerHex": "aabb/ff00",
"warnings": []
}
TOML содержит client_random, dns_upstreams, vpn_mode, exclusions, [listener.tun].included_routes и [listener.tun].excluded_routes. По умолчанию DNS upstreams — https://1.1.1.1/dns-query и https://8.8.8.8/dns-query, чтобы DNS не зависел от UDP ASSOCIATE. Новые профили по умолчанию IPv4-only: has_ipv6 = false и full-tunnel route 0.0.0.0/0. Если у серверного SOCKS5 Egress настроен настоящий IPv6 outbound, включите client_ipv6_enabled, и новые TOML получат has_ipv6 = true плюс публичный IPv6 route 2000::/3. Локальные/private маршруты остаются вне TUN. Маркер берётся из активного TLS Prefix в Core custom field manual_marker_hex, затем из локального marker_state, и только при отсутствии обоих — из registry Gateway. Если запись подписки ещё не попала в registry или отстала после смены TLS Prefix, /client-config обновляет эту запись из active marker и данных callback-запроса Core перед выдачей TOML/deeplink. При h2 ClientHello без совпадения Gateway пишет runtime warning h2 ClientHello has no marker match с безопасным префиксом TLS Random, чтобы быстро отличить marker mismatch от недоступного tt-shared.
Core хранит активный TLS Prefix/Mask в custom field плагина manual_marker_hex; Gateway держит локальный cache/snapshot в SQLite /var/lib/cavicodevpn-plugin-tt/gateway.db, а compose монтирует для этого volume cavicodevpn-plugin-tt-state. Пока этот volume сохранён, пересоздание контейнера gateway не меняет локальные маркеры и не требует перевыпуска клиентских профилей даже при короткой недоступности Core. Sync не ротирует marker автоматически: подписки получают manual_marker_hex из Core или первый существующий marker из локального cache, а случайная генерация используется только при самом первом пустом состоянии. Если Core custom field ещё пуст после обновления со старой версии, Gateway импортирует текущий marker в Core без смены prefix. Изменение TLS Prefix происходит только через кнопку Применить TLS Prefix в настройках плагина и после этого требует повторной генерации профилей, которые должны использовать новый prefix. Генерация клиентского конфига всегда использует текущий manual_marker_hex и при необходимости обновляет запись подписки в локальном store/registry перед выдачей TOML/deeplink, поэтому старые подписки не должны получать исторический prefix и новая подписка не ждёт очередного sync. При временно пустом или неполном ответе Core sync не удаляет marker сразу: запись получает valid_until = now + grace_period_days, а если подписка снова появляется, активный manual marker сохраняется и обновляется login. Gateway marker store больше не хранит password_hash.
Deeplink исправлен под спецификацию TrustTunnel: upstream_protocol кодируется одним байтом (0x01 = http2, 0x02 = http3), dns_upstreams — TLV-массивом строк, а display name пишется в тег name как {project_name} @{login} ({node_name}). Текущая upstream-спецификация deeplink не содержит тегов для vpn_mode, exclusions и routes, поэтому при непустых фильтрах deeplink/QR возвращаются с предупреждением: фильтры сохраняются только в TOML.
Клиентский bypass остаётся основным механизмом защиты локальных ресурсов пользователя: Desktop применяет exclusions и routes из TOML. Core дополнительно привязывает выбранный filter profile к подписке, а SOCKS5 Egress блокирует совпадающие домены/IP/CIDR как серверную страховку для TCP/UDP. Этот блок только предотвращает выход с IP VPN-сервера; TT-плагин не заставляет клиента повторять запрос в обход VPN.
Требования
- CaviCodeVPN Core запущен и доступен
- Доменное имя направлено на IP сервера (для ACME сертификата)
- Порт 443 (TCP) открыт (входящие TLS-соединения)
- Порт 80 (TCP) открыт и не занят другим сервисом:
cover-nginxпубликует его для ACME HTTP-01 challenge - Для ping/ICMP у
sharedдолжен работатьCAP_NET_RAW; стандартный compose уже добавляет эту capability.
Docker Compose — сервисы
| Сервис | Контейнер | Описание |
|---|---|---|
| gateway | cavicodevpn-plugin-tt-gateway | Go L4-классификатор, слушает :443 |
| shared | cavicodevpn-plugin-tt-shared | TT endpoint + socat, принимает от gateway |
| cover | cavicodevpn-plugin-tt-cover | nginx реверс-прокси на выбранный внешний сервис |
| acme | cavicodevpn-plugin-tt-acme | Certbot, управляет TLS-сертификатами |
Compose использует готовые Docker images из registry, а не локальный build:. Для Forgejo Packages задайте в Core окружение:
CaviCodeVPN_PLUGIN_IMAGE_PREFIX=git.cavicode.tech/cavicode/
CaviCodeVPN_PLUGIN_IMAGE_TAG=latest
Core прокидывает эти значения в .env плагина при установке, поэтому тяжёлый tt-image собирается в CI и скачивается сервером как cavicodevpn-plugin-tt-shared.
Локальное состояние Gateway хранится в volume cavicodevpn-plugin-tt-state и содержит SQLite-базу markers/cache. Source of truth для ручного TLS Prefix — Core DB custom field manual_marker_hex. Общие ACME-тома: cavicodevpn-acme-certs (активный cert.pem/key.pem, ручные сертификаты и выбор активного сертификата), cavicodevpn-acme-webroot (ACME challenge), cavicodevpn-acme-work (Let's Encrypt lineage). ACME sidecar использует Docker socket, чтобы перезапустить TLS consumers после выдачи, выбора, загрузки или обновления сертификата.
Разработка Gateway
cd CaviCodeVPN-Plugin-TT/gateway
go test ./...
docker build --target test -t cavicodevpn-plugin-tt-gateway-test .
Переезд с версии без state volume
Если плагин уже работал до появления cavicodevpn-plugin-tt-state, перед первым пересозданием gateway сохраните текущую SQLite-базу из контейнера. Без этого новый volume будет пустым, а старые клиентские профили останутся с TLS Prefix, которого Gateway уже не знает.
docker cp cavicodevpn-plugin-tt-gateway:/var/lib/cavicodevpn-plugin-tt/gateway.db ./gateway.db.backup
docker compose up --no-start gateway
docker cp ./gateway.db.backup cavicodevpn-plugin-tt-gateway:/var/lib/cavicodevpn-plugin-tt/gateway.db
docker compose up -d
Связанные репозитории
- CaviCodeVPN-Core — ядро платформы
- CaviCodeVPN-Plugin-UserSync — синхронизация пользователей