fxTunnel

· Часть 9 серии «Пишу свой ngrok на Go: дневник разработки»

Пишу свой ngrok на Go: инспекция трафика — свой мини-Charles

Последний день активной разработки. fxTunnel работает, оптимизирован, протестирован, защищён. Но есть одна фича, которую хочу с самого начала: инспекция трафика.

Контекст

30 января. Последний день активной разработки. fxTunnel работает, оптимизирован, протестирован, защищён. Но есть одна фича, которую хочу с самого начала: инспекция трафика. Знаете Charles Proxy? Или вкладку Network в DevTools? Вот то же самое, но встроенное в туннель. Каждый HTTP-запрос, который проходит через fxTunnel, видим в реальном времени. Заголовки, тело, статус-код, время. Без установки дополнительного софта. Для разработчика, который показывает заказчику демо через туннель — очень полезно. Видишь, что заказчик делает на сайте в реальном времени.

От документа к коду

Это первая фича, которую я начал с документа, а не с кода. Написал дизайн: что хочу, как должно работать, какие паттерны. И только потом — реализация. Архитектура:

HTTP-запрос → Сервер перехватывает → Сохраняет в RingBuffer →
→ SSE-стрим → Веб-панель / GUI в реальном времени

Ключевые решения: - RingBuffer — не хочу бесконечно копить запросы в памяти - Fan-out подписки — несколько клиентов могут смотреть трафик одновременно - SSE (Server-Sent Events) — для реального времени в браузере

RingBuffer

Массив фиксированного размера. Заполнился — новые элементы перезаписывают старые. Как кассета, которая перематывается в начало. Почему не обычный массив: 1. Предсказуемое потребление памяти — буфер на 100 записей всегда занимает одинаково 2. O(1) добавление — никаких аллокаций 3. Безопасно для concurrent — с правильными блокировками Fan-out: новая запись → все подписчики получают уведомление через каналы. Подписался на трафик туннеля — получаешь каждый запрос в реальном времени.

HTTP-перехват

Сервер уже проксирует HTTP-трафик. Инспекция — дополнительный слой: перед отправкой запроса сохраняем его, после получения ответа — дополняем.

→ Входящий HTTP-запрос
→ Сохраняем: метод, URL, заголовки, тело
→ Проксируем через туннель (как обычно)
→ Получаем ответ
→ Дополняем: статус-код, заголовки, тело, время
→ Кладём в RingBuffer → Уведомляем подписчиков

Каждая запись содержит: метод, URL, заголовки запроса и ответа, тела (с лимитом), статус-код, время. 90 строк в HTTP-роутере.

SSE vs WebSocket

Для реального времени два варианта: WebSocket и SSE (Server-Sent Events). WebSocket — двунаправленный канал. Клиент и сервер обмениваются сообщениями. SSE — однонаправленный. Только сервер → клиент. Но: - Работает через обычный HTTP (легче проксировать) - Автоматический реконнект в браузере (встроено!) - Проще реализовать (раза в три меньше кода) - Двунаправленность нам и не нужна — мы только показываем трафик Выбрал SSE. На сервере:

go
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
ch := buffer.Subscribe()
defer buffer.Unsubscribe(ch)
for {
    select {
    case exchange := <-ch:
        data, _ := json.Marshal(exchange)
        fmt.Fprintf(w, "data: %s\n\n", data)
        flusher.Flush()
    case <-r.Context().Done():
        return
    }
}

На фронте:

javascript
const source = new EventSource('/api/inspect/stream?tunnel_id=xxx')
source.onmessage = (event) => {
    const exchange = JSON.parse(event.data)
    // Показываем в UI
}

Реальное время без библиотек, без сложности WebSocket. Просто работает.

UI

Добавил интерфейс инспекции в обоих клиентах: - Веб-панель: таблица запросов, клик — детали. Обновляется в реальном времени через SSE. - GUI: то же самое в десктопном приложении, через тот же SSE-стрим.

Грабли

Большие ответы. HTTP-ответы могут быть огромными (файлы, видео). Буферизовать всё в память нельзя. Ограничил размер тела — показываем что поместилось, для остального показываем размер. SSE и аутентификация. EventSource в браузере не поддерживает кастомные заголовки. Нельзя передать JWT через Authorization. Передаю токен через query-параметр. Не идеально (токен в логах), но для внутреннего инструмента — приемлемо. Конкурентный доступ. Несколько горутин пишут (каждый запрос), несколько читают (подписчики). sync.RWMutex решает: много читателей одновременно, один писатель.

Итог

30 января — инспекция трафика: - RingBuffer с fan-out подписками - HTTP-перехват с минимальным overhead - SSE-стриминг в реальном времени - UI в веб-панели и GUI

a6bb61b feat(inspect): add CapturedExchange data model
b577e7e feat(inspect): add Manager for per-tunnel buffers
5023c3b feat(inspect): capture HTTP traffic
efa4863 feat(api): add inspect handlers (list, detail, clear, SSE stream)
dffe5c2 feat(web): add traffic inspection UI
da2a494 feat(gui): add traffic inspection view

Фича, которая превращает fxTunnel из просто туннеля в инструмент разработки.


В следующей части: финальная. Итоги, статистика и что бы я сделал иначе.

Все части дневника разработки