Мне нужен был 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 — потому что сервер стоит на вашем железе.
| Возможность | ngrok | Cloudflare | frp | fxTunnel |
|---|---|---|---|---|
| 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 лицензия.