Зачем я вообще это делаю
24 декабря 2025. Канун Нового года. Все нормальные люди готовят оливье, а я сижу и злюсь на ngrok. У меня куча проектов на локалке: бот для Telegram, API для мобильного приложения, веб-сервис для клиента. И каждый раз, когда нужно быстро показать результат — «вот, смотри, всё работает» — начинается цирк. ngrok просит деньги за кастомный домен. serveo.net то работает, то нет. localhost.run — ограничения. И тут я подумал: а что если написать своё? Полностью. С нуля. На своём сервере, без ограничений. Звучит как безумие? Наверное. Но руки чешутся — значит надо делать.
Идея за 5 минут на салфетке
Вся штука называется «обратный туннель» (reverse tunnel). Объясняю максимально просто:
Представьте: у вас на ноутбуке крутится веб-приложение — пока только на localhost:3000. Его никто в мире не видит. А вам нужно показать заказчику. Прямо сейчас.
Обратный туннель — это невидимый мост:
Заказчик в браузере
↓
Ваш сервер (VPS за $5/мес)
↓
~ магия ~
↓
Ваш ноутбук → localhost:3000
Заказчик открывает myapp.yourdomain.com — и видит ваш локальный сервис, как будто он задеплоен. Никакого деплоя, никакого Docker, никакого AWS. Просто мост между ноутбуком и интернетом.
Фишка в том, что именно ваш ноутбук подключается к серверу, а не наоборот. Поэтому не нужен белый IP, не нужно ковырять роутер, вообще ничего не нужно настраивать на стороне сети.
Стек: почему Go
Я выбрал Go. Три причины:
1. Один файл = весь продукт. Go компилируется в один бинарник. Никаких npm install, никаких виртуальных окружений. Скопировал файл на сервер — работает.
2. Заточен под сеть. В Go есть горутины — штука, которая позволяет обрабатывать тысячи одновременных соединений без боли. Для туннелей — то что нужно.
3. Кросс-компиляция. Одной командой собираю бинарники под Linux, Windows, macOS. Клиент должен работать везде.
Для мультиплексирования взял yamux от HashiCorp (это ребята, которые делают Terraform и Vault). Одно TCP-соединение — множество логических каналов внутри. Как множество вкладок в одном окне браузера.
Протокол: JSON, но хитрый
Как сервер и клиент общаются? Я придумал простой протокол: каждое сообщение — это JSON, но перед ним идут 4 байта с длиной. Зачем? Потому что TCP — потоковый, он не знает, где заканчивается одно сообщение и начинается другое. 4 байта длины решают эту проблему.
[4 байта: "сколько читать"] [JSON: "что делать"]
Типы команд простые:
- auth — «привет, вот мой токен»
- tunnel_request — «хочу туннель на порт 3000»
- tunnel_created — «готово, твой адрес myapp.example.com»
- new_connection — «к тебе пришёл пользователь, открывай канал»
- ping/pong — «ты жив?» / «жив»
Вот и весь протокол. Просто, читаемо, дебажится в два клика.
Самый важный кусок кода
Вся магия туннелирования — в двух строках. Серьёзно.
Когда кто-то заходит на myapp.example.com, сервер уведомляет клиент. Клиент подключается к локальному сервису и запускает двунаправленное копирование:
go func() { io.Copy(local, stream) }() // интернет → ваш сервис
go func() { io.Copy(stream, local) }() // ваш сервис → интернет
io.Copy — встроенная функция Go, которая просто перекачивает байты из одного потока в другой. Одна горутина качает в одну сторону, другая — в обратную. Всё.
Конечно, вокруг этих двух строк — ещё 3500 строк обвязки: аутентификация, управление туннелями, обработка ошибок, реконнект. Но ядро — вот оно.
Что получилось за один вечер
К ночи 24 декабря — один коммит, 3578 строк кода, и оно работает:
# На сервере:
./fxtunnel-server --config server.yaml
# На ноутбуке:
./fxtunnel http 3000 --server myserver:4443 --token sk_xxx
# ✓ Tunnel created: myapp.example.com → localhost:3000
Что умеет: - HTTP-туннели — показывай веб-сервис через красивый поддомен - TCP-туннели — пробрасывай SSH, базу данных, что угодно - UDP-туннели — даже это (игровые серверы, например) - Автореконнект — отвалился WiFi? Клиент сам переподключится - Токенная аутентификация — не любой может создать туннель Ноль UI, ноль тестов, только консоль. Но работало — а это главное на данном этапе.
Грабли
Без них никуда: yamux роли. Клиент и сервер должны использовать разные роли при создании сессии. Перепутал — всё зависло без единой ошибки. Дебажил 40 минут, пока не прочитал документацию. Мораль: RTFM. Timeout на авторизацию. Без таймаута кто угодно мог подключиться к серверу и «повиснуть» навечно, занимая ресурсы. Добавил 30-секундный дедлайн — не авторизовался, до свидания.
Итог
За один вечер — работающий self-hosted аналог ngrok. До продакшена, конечно, далеко. Но основа есть, и она работает. Пользоваться можно уже сейчас — пусть и из консоли.
Коммит: 9e851ab — feat: initial implementation of fxTunnel reverse tunneling system
В следующей части: как за один день превратить консольную утилиту в нормальный продукт с веб-панелью, десктопным приложением и CI/CD. Спойлер: 24 000 строк кода и пять фиксов линтера подряд.