AI Как мы написали свой forward-proxy на Go и отказались от VPN для доступа к админкам

  • Автор темы Автор темы AI
  • Дата начала Дата начала

AI

Команда форума
Редактор
Регистрация
23 Авг 2023
Сообщения
4,150
Реакции
0
Баллы
36
Ofline
Если коротко: После дефейса сайта нашего знакомого из-за утекшего пароля от админки мы поняли, что управлять доступами нетехнической команды (редакторы, SEO, подрядчики) через VPN или статические IP - это боль. Существующие proxy требовали рестартов и рулились конфигами. В итоге мы написали свой forward-proxy на Go, где доступ выдается токеном через расширение браузера, а правила (TTL, лимиты трафика, доступные домены) применяются на лету без разрыва соединений.


Так видит наше решение Google Gemini

Так видит наше решение Google Gemini

Почему мы отказались от классических решений​


Задача казалась тривиальной: дать безопасный доступ в админки ограниченному кругу лиц. Но на практике стандарты ломаются о реальные процессы:


  • VPN: Избыточен. Заворачивает весь трафик (привет, торренты и абузы на сервер), сложен в настройке для гуманитариев, а сегментация доступа (ACL) быстро превращается в хаос.


  • Статические IP: В эпоху 4G/5G, коворкингов и удаленки - почти не работают. Whitelist на сервере не дает сегментации по юзерам и гибкости отзыва.


  • Готовые forward-proxy (например, 3X-UI): Конфигурация через файлы. Выдать доступ подрядчику "на час" - это правка конфига, деплой и рестарт. UI поверх таких решений - просто обертка, каждое изменение требует reload, сбрасывая текущие сессии. Плюс зачастую настроить HTTPS прокси с сертификатом нормального CA - тот ещё квест.


  • Pomerium и другие Reverse Proxy / SSO-шлюзы: Отличные инструменты, но со своими фатальными для нашей задачи недостатками.

    • Во-первых, reverse-proxy меняет домен сайта (или требует сложных настроек) и терминирует TLS, устраивая MITM (Man-in-the-Middle).


    • Во-вторых, тяжелый онбординг. Чтобы пустить внешних аудиторов или временных подрядчиков, нужно сначала выпросить их email, завести учетки в IdP, настроить права, дать доступ к нужным ресурсам, а после завершения работ - не забыть удалить.

Разрыв между "инструмент работает" и "им удобно пользоваться" оказался большим. Нам нужен был инструмент управления доступами, а не инфраструктурой, без сложного онбординга.

Ключевая идея: доступ к конфигурации, а не к пользователю​


Мы изменили подход: админ создает конфигурацию (разрешенные домены, TTL, лимиты трафика). Для неё генерируется короткий base58-токен.

Этот токен передается пользователю. Никаких паролей, VPN-клиентов, сбора email-ов или IP-адресов. Скинул токен аудитору в Telegram - через заданный TTL доступ сам "превратился в тыкву".

Архитектура состоит из трех частей:


  1. Forward-proxy на Go.


  2. API управления (Control Plane).


  3. Расширение для браузера.

Чуть ниже я покажу 3 неочевидных юзкейса (например, как мы навсегда убили необходимость править /etc/hosts при переездах сайтов), но чтобы магия была понятна, давайте быстро заглянем под капот движка.

Миграция сайта теперь без правки /etc/hosts

Миграция сайта теперь без правки /etc/hosts

Как это работает под капотом​


Наш инструмент - это не универсальный прокси, а узкоспециализированный интернет-шлюз.


  • Деплой и автономность (Zero Config): Сам шлюз - это один бинарник на 8 МБ с нулем зависимостей, который ставится на "голую" Debian 13 (x86/ARM). У него вообще нет конфигурационного файла. Все настройки он получает с Control Plane. При этом шлюз умеет работать автономно: если после перезагрузки сервера связи с CP не будет, прокси поднимется из локального стейта и продолжит пускать пользователей по правилам (вплоть до протухания TLS-сертификата).


  • Безопасность транспорта (TLS): Соединение с прокси работает поверх TLS. Мы автоматизировали этот процесс: даже для self-hosted нод система сама выписывает и продлевает сертификаты (Google Trust Services, Let's Encrypt, ZeroSSL).


  • Скорость и TTFB: почему прокси работает быстрее прямого подключения: Казалось бы, любой шлюз - это лишний хоп, который должен замедлять загрузку. На практике мы получили обратный эффект. За счет того, что в шлюз встроен собственный кэширующий DNS-резолвер (работающий с 1.1.1.1/8.8.8.8), а сами ноды стоят на широких магистральных каналах дата-центров, Time To First Byte (TTFB) через наш прокси часто оказывается ниже, чем при прямом подключении через обычного домашнего провайдера с его медленными DNS.


  • Защита от сканеров и контроль шеринга (TLS SNI): Для маршрутизации к прокси используется wildcard DNS (например, *.de.l7ag.run), а каждый пользователь через расширение получает уникальный адрес вида DevID-user-v4.de.l7ag.run. Это дает две суперсилы. Во-первых, жесткая пред-авторизация на уровне TLS SNI: если бот или сканер стучится по IP или без корректного SNI - коннект мгновенно обрывается, а IP улетает в бан. Во-вторых, так мы идентифицируем конкретные устройства на одном токене (можно задать лимит: например, не больше 3 человек на токен).


  • Модель соединений: HTTP CONNECT. Шлюз работает как туннель: TLS целевого сайта не терминируется, payload не анализируется (zero-copy). Результат - нулевой MITM, низкая нагрузка на CPU и минимальная задержка.


  • Авторизация: Basic Auth, где логин/пароль привязаны к конфигурации, а не к конкретному человеку. Это позволяет одной ссылкой пустить команду подрядчиков, а затем в один клик убить токен для всех.


  • Хранилище: Ушли от Redis к embedded-базе BuntDB. При старте всё кэшируется в in-memory map. Никаких походов в БД на каждый CONNECT - задержка минимальна.
Продление доступа в реальном времени. В пару кликов и без разрыва соединений.

Продление доступа в реальном времени. В пару кликов и без разрыва соединений.

Киллер-фича: Динамическое обновление без разрыва соединений​


Главная проблема классических proxy: изменил конфиг -> сделал reload -> активные соединения упали. Если в этот момент редактор грузил большое видео в админку, он вас возненавидит.

Мы реализовали применение правил в рантайме. Шлюз отслеживает активные туннели:


  • Доступ выдан на час. Человек грузит дамп БД.


  • Время истекает - админ в UI жмет "Продлить на 30 минут".


  • Соединение не рвется. Движок шлюза обновляет параметры активного туннеля на лету.


  • И наоборот: если превышен лимит трафика или админ отозвал токен, шлюз мгновенно убивает активный туннель, не дожидаясь закрытия сессии клиентом.

Клиентская часть: Расширение и PAC-файлы​


Вместо системного proxy мы написали браузерное расширение, которое принимает токен и генерирует PAC-файл (Proxy Auto-Configuration) на лету.

Никаких "черных ящиков": вес 38 КБ и апрув в сторах

Наше расширение весит всего 38 КБ (вместе с иконками) - меньше, чем превью-картинка к этой статье. Мы принципиально не используем минификацию, обфускацию или аналитические трекеры. В коде даже оставлены оригинальные комментарии.

Именно благодаря такой прозрачности расширение уже успешно прошло модерацию и доступно в официальных сторах Chrome и Firefox. Нам нечего скрывать: любой разработчик может распаковать исходники и лично убедиться в безопасности плагина буквально за пару минут.


Почему это удобно:


  1. Легкость настройки: Нужно просто вставить 5-символьный токен, и маршрутизация работает. Никаких настроек с кучей опций или пересылок json-файлов с конфигами.


  2. "Настроил и забыл" + быстрый тест: Благодаря селективной маршрутизации вам не нужно постоянно включать или выключать расширение (как это бывает с VPN) - браузер сам знает, когда идти через шлюз, а когда напрямую. А тумблер вкл/выкл в расширении позволяет в один клик проверить, как сайт ведет себя через шлюз и как он выглядит "снаружи" для обычных посетителей.


  3. Изоляция трафика: Админка магазина идет через прокси, а YouTube в соседней вкладке - напрямую.


  4. Мультирегиональность: Расширение умеет раскидывать запросы по разным proxy-инстансам (админка для EU - через один шлюз, для US - через другой).
Пример селективной маршрутизации. Один сайт через Австрию, другой через Германию, админка доступна через шлюз (без него показывает 404), а YouTube идёт напрямую.   Демо сделано в Vivaldi

Пример селективной маршрутизации. Один сайт через Австрию, другой через Германию, админка доступна через шлюз (без него показывает 404), а YouTube идёт напрямую. Демо сделано в Vivaldi

3 неочевидных юзкейса, которые закрыл наш подход​


Помимо банальной защиты админок, такая маршрутизация на уровне браузера решила нам еще несколько головных болей:

1. Тестирование при переезде на новый сервер (Убийца [B]/etc/hosts[/B])

Когда вы переносите сайт на новый сервер, нужно, чтобы QA, SEOшники и заказчик всё проверили до обновления публичных DNS. Раньше приходилось писать инструкции для нетехнических людей: "откройте блокнот от имени администратора, найдите файл hosts...".

Теперь мы просто создаем токен "Новый сервер", где прописываем кастомный резолв домена на новый IP. Человек включает расширение - видит сайт на новом сервере. Выключает - видит старый (продакшен).

2. Безопасный доступ к localhost и private IP

Можно легко выдавать доступ к внутренним сервисам, которые физически не торчат наружу. Например, нужно дать девопсу доступ к Caddy API, который слушает только localhost:2019 на конкретном сервере, или пустить разработчика в админку базы данных во внутренней серой подсети. Используя домены, типа caddy.internal.

3. Внутренние DNS-имена без реальных DNS-записей

Доступ к инфраструктурным инструментам, для которых вы принципиально не хотите светить DNS-записи. Вы можете замапить в конфиге условный grafana.site.com на localhost:3000 (на машине, где крутится шлюз). Пользователь с токеном просто вводит grafana.site.com в адресную строку браузера (или нажимает на ссылку в расширении) и попадает в дашборды. Снаружи этого домена и порта не существует.

Интерфейс​


Отдельная боль многих энтерпрайз-решений (того же Pomerium) - это интерфейс. Настраивать каждого пользователя заново, прокликивать десятки чекбоксов, чтобы изменить права для группы доменов - это долго.

Поэтому UI личного кабинета мы спроектировали с оглядкой на десктопный UX. Нужно выделить десяток объектов? Не надо кликать десяток чекбоксов - просто выделяете их рамкой, как в Проводнике Windows, или через Shift/Ctrl. Нужные действия вызываются привычным контекстным меню по правой кнопке мыши. Любой объект можно продублировать в один клик, чтобы не повторять рутину. Никакого перегруженного веб-интерфейса - только быстрый SPA (Single-Page Application) на ванильном JS, где данные летают по WebSocket.

Выделение рамкой, контекстные меню - десктопный UX

Выделение рамкой, контекстные меню - десктопный UX

Модели изоляции: от Shared до Self-hosted​


Поскольку мы делали SaaS, нам пришлось сразу закладывать архитектуру под разные уровни паранойи и требований к инфраструктуре:

1. Shared-шлюзы (Managed)​


Полностью наша инфраструктура. Идеально для быстрого старта и небольших команд. Создал токен - и сразу работаешь. Единственный нюанс - мы применяем "ленивую" модерацию доменов, чтобы наши IP не улетели в блэклисты из-за любителей торрентов и спама.

2. Dedicated-шлюзы (Managed)​


Тоже наша инфраструктура, но выделенный инстанс под одного клиента. Это решает проблему "соседей" и дает чистый статический IP-адрес шлюза, который можно жестко прописать в whitelist на целевом сервере.

3. Self-hosted (Своя инфраструктура)​


Высший уровень контроля. В этом случае весь трафик идет через ваш сервер, у вас нет лимитов на трафик и домены без модерации, а оплата за сервер идет напрямую вашему хостинг-провайдеру

Делаем self-hosted доступнее​


Мы максимально упростили деплой self-hosted шлюзов. Наша цель была в том, чтобы поднять свою ноду мог менеджер проекта или руководитель агентства, который в глаза не видел консоль Linux и SSH.

Все делается из интерфейса: вводите API-ключ вашего облачного провайдера (Hetzner, Vultr, DigitalOcean, Upcloud, Akamai (Linode)), выбираете локацию и нажимаете кнопку. Наш бэкенд сам создаст сервер, скачает бинарник шлюза и параллельно выпустит сертификат Google Trust Services (Let's Encrypt или ZeroSSL). Меньше чем через 25 секунд вы получаете готовый к работе и безопасный прокси без единой строчки кода в терминале.

Либо можете поставить его руками на абсолютно любой Linux-сервер (хоть на Raspberry Pi в офисной кладовке).

Установка сводится к классическому one-liner:
curl -fsS https://l7.run | sh -s -- -t <token>

Для тех, кто обоснованно считает curl | sh плохой практикой

Вы можете просто скачать этот скрипт и изучить его. Там нет никакой магии или обфускации. Он определяет архитектуру, скачивает бинарник, прописывает systemd-сервис и закидывает конфиг в /etc/sysctl.d/ для тюнинга сетевого стека под высокие нагрузки: включаем алгоритм BBR, переиспользование TIME_WAIT сокетов, отключаем медленный старт после простоя и тюнит буферы.


Мы заморочились с концепцией Zero Dependency. Чтобы при установке не зависеть от наличия unzip или zstd на сервере, мы используем HTTP-сжатие zstd при скачивании через curl. Экономим трафик, распаковываем поток на лету, создаём юзера с минимальными правами и прописываем бинарник в автозапуск.

Дальше шлюз берет рутину на себя: сам запрашивает сертификаты и актуальные конфиги из Control Plane. А следуя лучшим современным практикам, бинарник умеет безопасно и "мягко" обновлять сам себя без разрыва активных соединений (graceful update). Никаких прерванных сессий или отвалившихся загрузок у пользователей! По умолчанию шлюз вешается на 443 порт, но изменить его можно в пару кликов прямо в веб-интерфейсе. Опять же, никаких текстовых конфигов и возни в терминале.

Эволюция костыля, или как мы докатились до SaaS​


Забавно, но этот проект вообще не планировался как SaaS. Всё началось с крошечного расширения для Chrome, которое мы написали для себя.

С чего всё начиналось

С чего всё начиналось

Изначально бэкенда не было в принципе. Под капотом крутился обычный 3X-UI, а в расширение нужно было просто скопировать JSON-конфиг. Но быстро выяснилось, что пересылать JSON-файлы нетехническим юзерам - это боль. Человеку нужно было куда-то его сохранить, потом зайти в настройки расширения, найти куда он сохранил файл... Мы решили упростить: пусть юзер вводит просто короткий токен, а расширение само скачивает конфиг по API. Идея зашла отлично.


Но тут начал напрягать сам 3X-UI. Процесс добавления доступа выглядел так: зайди в админку, вручную заведи юзера, потом отдельно пропиши правила (на какие сайты ему можно ходить). Потом не забудь сохранить конфиг и самое бесячее - сделать рестарт сервера. Во время одного из таких ручных ковыряний я случайно заблочил служебный канал самого 3X-UI. У меня и в мыслях не было, что инструмент может так легко заблокировать сам себя прямо из своего же UI, а потом просто крашиться при каждом рестарте 🙂

Чтобы не плодить ошибки, я набросал простенькую веб-страничку: вводишь домены и креды, а она генерирует готовый JSON. Но данные всё равно приходилось вводить в двух местах. Это жутко раздражало.

Генератор JSON-конфига

Генератор JSON-конфига

Тогда пришла крамольная мысль:

А что если выкинуть 3X-UI и написать свой максимально тупой и простой прокси, который будет рулиться по API и применять правила без рестартов?

Так началась череда экспериментов:


  1. Первая итерация: Взяли библиотеку elazarl/goproxy и прикрутили Redis для хранения стейта. Позже поняли, что это дикий оверинжиниринг: нам не нужен был MITM и глубокий анализ трафика, требовалось просто максимально быстро перекидывать байтики из сокета в сокет. В итоге выпилили оба инструмента, перейдя на самописный zero-copy движок и in-memory/BuntDB.


  2. Битва протоколов и публичный Wi-Fi: Попробовали SOCKS5, но уткнулись в ограничения браузеров - они просто не умеют передавать SOCKS5-авторизацию, плюс отваливался TLS. Пришлось возвращаться к HTTP CONNECT. При этом у нас было принципиальное требование безопасности: мы не хотели, чтобы человек, сидя в общественном Wi-Fi, светил кредами шлюза в открытом виде. Поэтому всё нужно было обязательно заворачивать в TLS.


  3. Браузерные грабли с кэшем: Вылезла максимально неочевидная проблема. Если человек менял токен в расширении (заходил под другим юзером), браузер продолжал упрямо слать старые заголовки авторизации (Proxy-Authorization). Браузеры жестко кэшируют креды прокси до полного перезапуска! Решение нашлось изящное: мы начали генерировать динамические SNI под каждого юзера, чтобы для браузера это выглядело как совершенно новый прокси-сервер. Кэш сбрасывался, магия работала.

Решая одну маленькую проблему за другой, мы шаг за шагом избавились от чужих костылей, пока в какой-то момент не посмотрели на результат и не поняли:

"Упс, кажется, мы написали полноценный SaaS"​

P.S. В процессе разработки мы собрали приличное количество граблей и нашли много неочевидных решений по их обходу (особенно в части работы с браузерами и расширениями). Если вам интересна техническая изнанка проекта - дайте знать в комментариях, и я напишу вторую статью с подробным разбором "подводных камней"!

P.P.S. Сервис пока на стадии закрытого бета тестирования. Желающие пощупать, могут зайти на временный лэндинг L7 Admin Guard и попробовать, как работают живые демки, расширение уже в сторах.
 
Назад
Сверху Снизу
Яндекс.Метрика Рейтинг@Mail.ru