No description
  • Go 81.5%
  • Python 11.6%
  • Shell 5.3%
  • Dockerfile 1.6%
Find a file
DSvinka e2ee0eb92a
Some checks failed
ci / ci (push) Failing after 1s
CI: переход на cavicode/runner:docker
2026-06-23 18:04:42 +03:00
.forgejo/workflows CI: переход на cavicode/runner:docker 2026-06-23 18:04:42 +03:00
acme
cover-nginx Улучшения безопасности и стабильности 2026-06-20 14:01:56 +03:00
gateway Улучшения безопасности и стабильности 2026-06-20 14:01:56 +03:00
tt-image Улучшения безопасности и стабильности 2026-06-20 14:01:56 +03:00
.gitignore
docker-compose.yml Поддержка перенаправления на локальные ресурсы 2026-06-08 17:24:00 +03:00
README.md CI: переход на cavicode/runner:docker 2026-06-23 18:04:42 +03:00

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)
                                 │
                             Интернет
  1. Клиент подключается к vpn.example.com:443 с deeplink'ом
  2. Gateway читает начало TLS ClientHello, извлекает Random[0..32]
  3. Проверяет стего-маркер (prefix & mask) — при совпадении splice в TT endpoint
  4. TT endpoint работает без авторизации, прокидывает credentials клиента в SOCKS5 Egress через extended_auth
  5. 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:

  1. Откройте Плагины → Установка
  2. Git URL: URL этого репозитория
  3. Имя: tt, Тип: Traffic
  4. Нажмите «Установить» — 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_hostnameCaviCodeVPN_COVER_HOSTNAME
  • cover_target_hostnameCaviCodeVPN_COVER_TARGET_HOSTNAME
  • acme_emailCaviCodeVPN_ACME_EMAIL
  • acme_stagingCaviCodeVPN_ACME_STAGING (bool: true1, false0)
  • project_nameCaviCodeVPN_PROJECT_NAME
  • node_nameCaviCodeVPN_NODE_NAME
  • client_ipv6_enabledCaviCodeVPN_CLIENT_IPV6_ENABLED
  • tt_icmp_enabledCaviCodeVPN_TT_ICMP_ENABLED
  • tt_icmp_interfaceCaviCodeVPN_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

Связанные репозитории