Назад к блогу

fxTunnel: self-hosted reverse tunnel на Go с HTTP/TCP/UDP и GUI-клиентом

Техническая история создания self-hosted reverse tunnel сервера с HTTP/TCP/UDP туннелями, Web UI, GUI-клиентом и yamux-мультиплексированием.

fxTunnel: self-hosted reverse tunnel на Go с HTTP/TCP/UDP и GUI-клиентом

Мне нужен был self-hosted reverse tunnel с HTTP, TCP и UDP, кастомными поддоменами, веб-панелью и десктопным клиентом. Существующие решения закрывали часть требований, но не все. Поэтому я написал fxTunnel.

Что не так с ngrok, Cloudflare и frp

ngrok — отличный продукт, но коммерческий. Бесплатный план даёт случайный поддомен, который меняется при каждом запуске. Для вебхуков это значит — каждый раз обновлять URL в настройках платёжки. Кастомные поддомены — от $10/мес. Данные проходят через серверы ngrok, и для некоторых проектов это не вариант по compliance.

Cloudflare Tunnel — бесплатный, но только HTTP/HTTPS. Нужно пробросить SSH к серверу? Базу данных? Игровой сервер по UDP? Не получится. Плюс привязка к экосистеме Cloudflare — DNS должен быть у них.

frp — ближайший конкурент, open source, self-hosted. Но UI нет, управления пользователями нет, 2FA нет, GUI-клиент нет. Проект развивается, но медленно.

Мне нужно было: self-hosted, HTTP + TCP + UDP, кастомные поддомены без лимитов, веб-панель для управления пользователями, десктопный клиент для тех, кто не хочет трогать терминал. Ничего из существующего не закрывало всё разом.

Архитектура: yamux, custom protocol и три менеджера

Архитектура fxTunnel построена вокруг одной идеи: клиент устанавливает одно TCP-соединение с сервером, и через это соединение мультиплексируется весь трафик всех туннелей. Для этого я использую yamux от HashiCorp — библиотеку stream multiplexing, которая позволяет открывать виртуальные потоки внутри одного TCP-соединения.

Internet -> nginx (wildcard SSL, *.tunnel.example.com)
  -> fxtunnel-server
      +-- HTTP Router     — маршрутизация по Host header -> клиентский туннель
      +-- TCP Manager     — выделение порта из диапазона -> проксирование к клиенту
      +-- UDP Manager     — аналогично, но для UDP (DNS, VoIP, gaming)
      +-- Web Admin UI    — Vue 3 SPA для управления
      +-- REST API        — CRUD пользователей, токенов, доменов
  -> yamux session (multiplexed)
  -> fxtunnel-client

Почему yamux, а не свой мультиплексор? Потому что yamux используется в Consul и Nomad от HashiCorp — библиотека проверена на серьёзных production-нагрузках, поддерживает flow control, heartbeat, graceful shutdown. Писать своё — значит повторять чужие баги с опозданием на годы.

Поверх yamux работает custom protocol — length-prefixed JSON over TCP. Каждое сообщение начинается с 4-байтного заголовка с длиной тела, затем JSON-payload. Это не protobuf и не msgpack — осознанный выбор. JSON читается человеком при дебаге (tcpdump | jq), а length prefix убирает проблему partial reads. Для control plane — а протокол используется именно для управления, не для данных — оверхед JSON несущественен. Данные пользователей идут через отдельные yamux-стримы без какой-либо обёртки, чистый TCP relay.

Не уверен, что JSON — лучший выбор на длинной дистанции. Если когда-нибудь потребуется совместимость между версиями клиента и сервера с разными схемами сообщений, protobuf был бы лучше. Но пока JSON работает и дебажить удобно.

HTTP-туннели работают через роутинг по Host header. Когда клиент регистрирует туннель myapp.tunnel.example.com, сервер добавляет маршрут в in-memory map. Входящий HTTP-запрос на этот поддомен — nginx терминирует TLS, проксирует на http_port fxtunnel-server, сервер смотрит Host, находит соответствующий yamux session, открывает новый stream и проксирует request/response.

TCP-туннели выделяют порт из настроенного диапазона (tcp_port_range в конфиге сервера). Клиент просит TCP-туннель — сервер назначает порт, слушает на нём, и каждое входящее соединение проксирует через yamux stream на клиент. SSH, базы данных, RDP — что угодно.

UDP-туннели — аналогичная схема, но с нюансами. UDP stateless, поэтому сервер отслеживает виртуальные соединения по паре src_addr:src_port с таймаутом неактивности. Работает для DNS, VoIP, игровых протоколов. UDP в reverse tunnels — редкость, и это одно из реальных преимуществ fxTunnel перед конкурентами.

Почему Go

Я писал об этом подробнее в отдельном посте, но здесь — суть для контекста fxTunnel.

Goroutines. Каждое входящее соединение — отдельная goroutine. Каждый yamux stream — goroutine. Проксирование io.Copy между двумя соединениями — две goroutines. При тысяче одновременных туннелей это тысячи goroutines, и Go справляется с этим на десятках мегабайт памяти.

Один бинарник. go build — и готово. Никаких рантаймов, интерпретаторов, зависимостей. Пользователь скачивает файл, делает chmod +x, запускает. Для серверного софта, который ставят на VPS, это критично.

Кросс-компиляция. Четыре строки в Makefile — бинарники для Linux amd64/arm64, macOS arm64, Windows amd64.

Стандартная библиотека. net, net/http, crypto/tls, encoding/json — всё, что нужно для сетевого сервера, есть из коробки. Внешних зависимостей у fxtunnel-server минимум: yamux, JWT-библиотека, SQLite-драйвер, TOTP.

Web Admin Panel: Vue 3, пользователи, 2FA

fxTunnel — не просто туннель, это платформа с пользователями. Серверу нужна панель управления, и я написал её на Vue 3.

Функциональность панели:

  • Управление пользователями — регистрация по инвайт-кодам (открытая регистрация отключена по умолчанию), роли, блокировка.
  • TOTP 2FA — Google Authenticator, Authy и любой TOTP-клиент.
  • Scoped API tokens — пользователь создаёт токены с конкретными правами: только HTTP-туннели, только определённые домены, read-only. Если токен утёк — ущерб ограничен скоупом.
  • Управление доменами — какие wildcard-домены доступны, кому назначены, лимиты по количеству поддоменов.
  • Мониторинг активных туннелей — кто подключён, какие туннели открыты, трафик в реальном времени.

Авторизация — JWT. Данные хранятся в SQLite — для single-node deployment это оптимальный выбор: нет внешних зависимостей, бэкап — копирование одного файла.

GUI-клиент на Wails: не Electron

Я подробно писал про Wails здесь, но в контексте fxTunnel важно вот что.

Десктопный клиент fxTunnel — это Wails-приложение: Go-бэкенд + Vue 3 фронтенд + нативный WebView системы. Бинарник весит ~15 MB. Аналогичное Electron-приложение весило бы порядка 150 MB, потому что тащит за собой Chromium и Node.js.

Для утилиты, которая показывает список туннелей, кнопку Connect и лог — Electron абсурден. Wails использует WebView2 на Windows, WebKit на macOS и Linux. Фронтенд — тот же Vue 3, тот же код компонентов, что и в Web Admin Panel (часть переиспользуется).

Клиент живёт в system tray. Минимизация — приложение уходит в трей, туннели продолжают работать. Конфигурация — YAML-файл, тот же формат, что и для CLI-клиента. Auto-reconnect работает из коробки с экспоненциальным backoff.

Self-hosted: nginx, wildcard SSL, Docker

Развёртывание fxTunnel — это nginx + Let’s Encrypt + Docker-контейнер.

nginx стоит перед fxtunnel-server и делает две вещи: терминирует TLS для wildcard-домена (*.tunnel.example.com) и проксирует трафик на http_port сервера. Wildcard-сертификат от Let’s Encrypt получается через DNS-01 challenge — certbot с DNS-плагином для вашего провайдера.

server {
    listen 443 ssl;
    server_name *.tunnel.example.com;

    ssl_certificate /etc/letsencrypt/live/tunnel.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/tunnel.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Docker-образ на GHCR. docker pull ghcr.io/mephistofox/fxtunnel-server, пробросить порты — control_port, http_port, tcp_port_range, udp_port_range — и готово. SQLite-файл монтируется как volume для персистентности.

Безопасность: HTTP-туннели показывают interstitial warning page при первом заходе — предупреждение, что контент предоставляется через туннель и может быть от кого угодно. Это защита от фишинга. TLS — на уровне nginx, токены скоупированы, 2FA включается на уровне пользователя.

Ретроспективно, я бы сделал конфигурацию сервера проще. Сейчас слишком много параметров, которые нужно указать при первом запуске. Для self-hosted решения это барьер — пользователь хочет поднять и попробовать, а не разбираться с десятком портов и доменов. В планах — wizard при первом запуске или хотя бы sensible defaults для всего, кроме домена.

Результаты и что дальше

fxTunnel закрывает конкретную проблему: полностью self-hosted reverse tunneling без ограничений. Кастомные поддомены, TCP и UDP туннели, управление пользователями с 2FA, GUI-клиент, и никаких лимитов на bandwidth — потому что сервер стоит на вашем железе.

ВозможностьngrokCloudflarefrpfxTunnel
Self-hostedНетНетДаДа
HTTP-туннелиДаДаДаДа
TCP-туннелиПлатноНетДаДа
UDP-туннелиНетНетЧастичноДа
Кастомные поддоменыПлатноДаКонфигБезлимитно
GUI-клиентНетНетНетДа
Web Admin PanelПлатноDashboard CFНетДа
2FA + User MgmtПлатноЧерез CFНетДа

Что дальше: метрики и observability (Prometheus endpoint), WebSocket pass-through оптимизация, и документация — потому что self-hosted решение без хорошей документации бесполезно.

Проект доступен на fxtun.dev и GitHub. Open source, MIT лицензия.