Почему я решил создать свой ngrok
В последнее время многие разработчики сталкиваются с проблемой блокировок внешних сервисов туннелирования. Популярные решения вроде ngrok, хотя и удобны, имеют ограничения в бесплатных тарифах и иногда недоступны в определенных регионах. Мне нужно было простое решение для тестирования веб-приложений на локальной машине с доступом из интернета, но без зависимости от сторонних сервисов.
Архитектура решения
Мой self-hosted аналог ngrok состоит из двух компонентов:
- Клиент — запускается на локальной машине, устанавливает постоянное соединение с сервером и перенаправляет трафик
- Сервер — работает на VPS с публичным IP, принимает входящие соединения и перенаправляет их клиенту
Для мультиплексирования соединений (несколько запросов через одно TCP-соединение) я использовал библиотеку yamux, которая реализует протокол, похожий на HTTP/2, но для произвольных потоков данных.
1 Настройка проекта и зависимостей
Сначала я создал структуру проекта и определил зависимости в go.mod:
module myngrok
go 1.21
require (
github.com/hashicorp/yamux v0.1.1
github.com/spf13/cobra v1.8.0
)
2 Реализация серверной части
Сервер слушает два порта: один для управления (установка туннеля), другой для пользовательского трафика:
package main
import (
"fmt"
"io"
"net"
"sync"
"github.com/hashicorp/yamux"
)
type TunnelServer struct {
controlPort string
publicPort string
tunnels map[string]*TunnelSession
mu sync.RWMutex
}
type TunnelSession struct {
session *yamux.Session
domain string
}
func (s *TunnelServer) Start() error {
// Запускаем контрольный порт
go s.listenControl()
// Запускаем публичный порт
go s.listenPublic()
return nil
}
func (s *TunnelServer) listenControl() {
listener, err := net.Listen("tcp", ":"+s.controlPort)
if err != nil {
panic(err)
}
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go s.handleControl(conn)
}
}
3 Реализация клиента
Клиент подключается к серверу и создает туннель для локального сервиса:
package main
import (
"fmt"
"io"
"net"
"github.com/hashicorp/yamux"
)
type TunnelClient struct {
serverAddr string
localAddr string
subdomain string
}
func (c *TunnelClient) Start() error {
// Подключаемся к серверу
conn, err := net.Dial("tcp", c.serverAddr)
if err != nil {
return err
}
// Создаем yamux сессию
session, err := yamux.Client(conn, nil)
if err != nil {
return err
}
// Регистрируем туннель
if err := c.registerTunnel(session); err != nil {
return err
}
// Запускаем проксирование
return c.proxyTraffic(session)
}
func (c *TunnelClient) proxyTraffic(session *yamux.Session) error {
for {
// Принимаем входящий поток от сервера
stream, err := session.Accept()
if err != nil {
return err
}
go func() {
// Подключаемся к локальному сервису
localConn, err := net.Dial("tcp", c.localAddr)
if err != nil {
stream.Close()
return
}
// Проксируем данные в обе стороны
go io.Copy(stream, localConn)
io.Copy(localConn, stream)
}()
}
}
Как Claude Code помог в разработке
Claude Code оказался невероятно полезным на нескольких этапах:
- Проектирование архитектуры — помог выбрать правильные библиотеки и спроектировать взаимодействие компонентов
- Работа с yamux — объяснил тонкости работы с мультиплексированием и помог избежать распространенных ошибок
- Обработка ошибок — предложил robust-решения для обработки сетевых сбоев и переподключений
- Оптимизация — помог написать эффективный код для проксирования данных с минимальной задержкой
Важно: Хотя AI-ассистенты вроде Claude Code значительно ускоряют разработку, важно понимать код, который они генерируют. Всегда проверяйте безопасность сетевого кода, особенно когда дело касается туннелирования трафика.
Сравнение с альтернативами
| Решение | Self-hosted | Сложность настройки | Производительность | Особенности |
|---|---|---|---|---|
| Оригинальный ngrok | Нет | Очень низкая | Высокая | Облачный сервис, ограничения в бесплатной версии |
| Cloudflare Tunnel | Частично | Средняя | Очень высокая | Интеграция с Cloudflare, бесплатный тариф |
| bore (Rust) | Да | Низкая | Высокая | Простой, но меньше возможностей |
| Наше решение | Да | Средняя | Высокая | Полный контроль, можно модифицировать под свои нужды |
Примеры использования
Вот как использовать готовое решение:
Запуск сервера на VPS:
# Компилируем сервер
GOOS=linux GOARCH=amd64 go build -o myngrok-server ./cmd/server
# Копируем на VPS
scp myngrok-server user@vps.example.com:/opt/myngrok/
# Запускаем на VPS
ssh user@vps.example.com
cd /opt/myngrok
./myngrok-server --control-port 4444 --public-port 80
Запуск клиента на локальной машине:
# Компилируем клиент
go build -o myngrok-client ./cmd/client
# Запускаем туннель для локального веб-сервера
./myngrok-client \
--server vps.example.com:4444 \
--local localhost:3000 \
--subdomain myapp
После этого ваш локальный сервер на порту 3000 будет доступен по адресу http://myapp.vps.example.com (если настроить DNS или использовать IP напрямую).
Кому подойдет это решение
Мой self-hosted аналог ngrok идеально подойдет:
- Разработчикам, которые хотят тестировать веб-приложения с внешним доступом без облачных сервисов
- Компаниям с требованиями к безопасности, которым нельзя использовать сторонние туннелирующие сервисы
- Энтузиастам, которые хотят понять, как работают инструменты туннелирования изнутри
- Образовательным проектам — как практический пример сетевого программирования на Go
Дальнейшее развитие
За 10 часов я создал рабочее ядро, но есть много возможностей для улучшения:
- Поддержка HTTPS — добавление TLS для шифрования трафика между клиентом и сервером
- Веб-интерфейс — мониторинг активных туннелей и управление через браузер
- Аутентификация — добавление токенов для контроля доступа
- Метрики — сбор статистики по трафику и подключениям
- Поддержка UDP — для туннелирования игровых серверов или VoIP
Предупреждение: При использовании self-hosted решений для туннелирования убедитесь, что вы соблюдаете законодательство вашей страны и политики хостинг-провайдера. Некоторые провайдеры могут ограничивать входящий трафик на определенные порты.
Заключение
Создание собственного аналога ngrok оказалось удивительно доступной задачей благодаря современным инструментам. Go с его богатой стандартной библиотекой и простой моделью конкурентности идеально подходит для сетевых приложений. Claude Code значительно ускорил процесс, помогая с архитектурными решениями и написанием boilerplate-кода.
Полный код проекта доступен в открытом репозитории, и я приглашаю всех желающих его улучшать. Это отличный пример того, как современные разработчики могут создавать сложные сетевые инструменты за считанные часы, комбинируя мощь AI-ассистентов с эффективными языками программирования вроде Go.
Как показывает опыт создания таких инструментов, как LLaMA 3.1 для генерации 3D-мебели, грамотное использование AI-помощников открывает новые возможности для быстрого прототипирования и реализации сложных проектов.