Дайте нормально поработать или я сделаю это сам
- среда, 7 октября 2026 г. в 00:00:06
Всем доброго времени суток!
Хочу поделиться историей о том, как решение одной бытовой проблемы постепенно вылилось в разработку отдельного open‑source проекта.
В ходе экспериментов постепенно сформировалась архитектура с отдельными proxy, broker и worker‑компонентами. Она позволила отделить клиентское подключение от транспортного канала и добавить маршрутизацию трафика через удалённые узлы.
В итоге получилась система, в которой трафик может передаваться напрямую либо через один из удалённых worker‑узлов. Правила маршрутизации задаются по доменам и CIDR‑диапазонам.
Исходный код эксперимента доступен в репозитории проекта.
C чего все начиналось
Итак, история начинается с моего трудоустройства. Как человек с достаточно большим багажом знаний и опыта (более 14 лет) в ИТ при устройстве на работу я стараюсь задавать вполне себе последовательный и понятный список вопросов, один из которых — «возможно ли у вас работать на личных устройствах?». Получив на этот и остальные вопросы утвердительный ответ, я принял оффер.
Каково же было мое удивление в первый день работы, когда мне дали RDP‑доступ к Windows‑тачке — вся работа организовывалась через нее. Маленькое отступление: последний раз я работал на Windows в 2011 году, еще на XP. Для читателей (да и для меня в какой‑то момент) самым простым и логичным вариантом было бы продолжить поиск работы, но работа на непривычной для меня операционной системе по факту была единственным моментом, который показался неподходящим. После некоторых попыток обустроить быт, выяснилось, что даже WSL там сильно порезанный, с собственными репозиториями и старыми операционками. Естественно, с личных устройств до удаленной машины заблокировано примерно все. Но вот оттуда есть доступ на внешние ресурсы. Причем насколько мне удалось понять, доступ организован по принципу черных списков — есть доступ ко всему, что не запрещено.
И тут мой бэкграунд разработчика подкидывает мне идею — если рабочая машина сама может установить соединение с нужным ресурсом, то потенциально можно использовать это уже существующее исходящее соединение как транспорт. Эта мысль не давала мне покоя, было осознание возможности сделать так, чтобы все работало без этого чертового RDP, и жизнь моя снова станет удобной и привычной. Так начались поиски и эксперименты.
Как развивалась идея
Первым делом предстояло проверить WebSocket‑соединение на какой‑нибудь личный домен. Не пожадничав и заплатив несколько сотен рублей за выделенный IP, я привязал к нему тестовый домен. По итогу выяснился следующий неприятный момент — коннект до него режется по каким‑то причинам. В итоге это было фиаско, и я забросил идею. Но внутри остались сомнения: доступ был открыт даже к каким‑то странным доменам и ресурсам. По воле случая мне нужно было поднять небольшой сервис в рамках другой инициативы и, как‑то решив проверить, а открывается ли он с рабочей машины, выяснил: открывается. Тут я вернулся к исследованиям и нашел единственную разницу, почему мой тестовый сайт заблокирован, а новоиспеченный сервис открывается — я не прикрутил SSL‑сертификаты. Да‑да, вы скажете: это же очевидно, изнутри все режется просто по 80-му порту и все. Но когда у тебя семья, работа и в целом жизнь бьет ключом, то неудивительны временные трудности и банальные затупы. Итого: зеленый свет, HTTP(S!) работает с рабочей машины до личного компа. Дело за малым — понять, как я могу это использовать.
Первое решение, которое приходит в голову, позволяющее открывать внутренние ресурсы и коммитить в GitLab — это HTTP‑proxy, который в целом доступен как часть git‑клиента. Таким образом родилась первая версия архитектуры:
HTTP‑proxy client <--> Локальный HTTP‑proxy server на localhost <--> web‑server доступный и с localhost и снаружи <--> WebSocket‑соединение как транспорт <--> рабочая машина.
Заговорщически потирая руки, я принялся за работу, но вот незадача и первый затык: WebSocket upgrade заблокирован. Причем это была очень интересная находка — ограничения были выборочные, учитывая стабильную работу ресурсов вроде Zoom, Dion, Telemost. То есть в случае WebSocket’ов было подозрение на работу именно белого списка сайтов. Продолжая ковырять, я утыкался в одну и ту же проблему — HTTP/2, WebSocket и пр. протоколы были заблокированы. Но тут отчаяния как такового не было, ибо даже в моменты изучения доступных вариантов я уже понимал: эти протоколы могут только «улучшить» связь, а на крайний случай у меня запасен long polling. Потратив какое‑то время на исследования, я всё‑таки наткнулся на неприятный факт: большая часть протоколов заблокирована и мне остается брать только его. Итого, проектируемая архитектура приобрела следующий вид:
HTTP‑proxy client <--> Локальный HTTP‑proxy server на localhost <--> web‑server доступный и с localhost и снаружи <--> long polling как транспорт <--> рабочая машина.
Схема на тот момент не выглядела простой для меня: я ни разу не писал HTTP‑proxy и даже не знал в деталях, как он устроен. Но было уже огромное желание наконец избавиться от работы через RDP. Я бодро взялся за дело и, потратив около двух недель на изучение, разработку и кучу странных подводных камней, наконец‑то сделал свой первый git pull. Счастью не было предела! Теперь все внутренние ресурсы открывались в Firefox с выставленным локальным прокси!
Разумеется, такой сценарий требует отдельного внимания к безопасности. В прототипе я сознательно ограничил поверхность атаки: локальный proxy не был доступен извне, а данные между worker и внешним сервером дополнительно шифровались. При этом это был личный эксперимент, а не попытка внедрить подобную схему в рабочую инфраструктуру компании.
В результате с точки зрения рабочей машины всё выглядело довольно прозаично: я обращался к своему внешнему HTTP(S)‑сервису и гонял через него зашифрованные данные.
Следующая проблема, с которой я столкнулся, была связана с моей рассеянностью. Из головы постоянно вылетала информация, какой браузер для чего: Firefox — для работы, а Chrome — для личных нужд. Тут как раз проблема решается небольшими доработками, так как HTTP‑прокси использует метод CONNECT для установления соединения с конечным сайтом: на этом месте можно вставить разделение трафика. То есть все запросы к внутренней инфраструктуре идут через воркер, а все остальное идет напрямую в сеть. Час работы — и стало понятно, что механизм, первоначально написанный для решения одной конкретной задачи, можно обобщить: разделить транспортный слой и маршрутизацию и тем самым получить возможность направлять разные соединения через разные узлы.
До своего увольнения, к счастью, не связанного с моими экспериментами, как‑то спокойно жилось: утечек не было, ребята из ИБ не обращались с вопросами.
Позже, когда я поехал отдохнуть в жаркие страны, мне пришла в голову следующая идея. Какого, спрашивается, хрена у меня жена в отпуске преспокойненько листает Инстаграм? Советский человек даже за границей должен оставаться советским. Негоже всякие инстаграмы разглядывать. Сказано — сделано: адаптирую версию своего читерского софта таким образом, чтобы трафик заблокированных в РФ сайтов шел через воркер, который находится в РФ, а все остальное шло напрямую. Единственная корректировка коснулась транспортного протокола: вместо long polling выбрал WebSocket для скорости (уж очень LP медленный). Все работало как часы, все нужные сайты были заблокированы.
На этом история не заканчивается. Наступило лето, трудоустройство в это время — так себе затея, особенно в современных реалиях. А что делать безработному тимлиду в творческом отпуске? Правильно, принимать активное участие в open source. И вот дальше началась кропотливая работа по приведению прототипа — эдакого «огрызка» продукта — к состоянию MVP. Не буду акцентировать ваше внимание на этой части, так как обычно это достаточно скучная работа (закон Парето как нельзя кстати).
Полный исходный код эксперимента и конфигурационные примеры доступны в репозитории проекта.
На этом вступительную, литературную часть заканчиваю и перехожу к архитектурной части с деталями и примерами.
Архитектура

В базовом примере Proxy‑сервер, broker и администраторская панель находятся на одном сервере. При стандартной настройке роутинга (direct режим) трафик через broker и worker не идет. На практике наиболее интересной частью архитектуры оказалась возможность подключения нескольких worker‑узлов, объединения их в группы и перенаправление трафика на них. Например, если мы хотим добавить английский или нидерландский пул, то нам просто нужно в админке добавить группу EN или NL соответственно, арендовать VDS в релевантной локации и подключить worker‑узлы. После этого в правилах маршрутизации указать, для каких доменов или CIDR‑диапазонов использовать конкретную группу. Таким образом, один proxy‑сервер может использовать несколько удалённых точек выхода, а выбор точки выполняется на основании правил маршрутизации..
Трафик между Proxy‑сервером и broker, а также между broker и worker передается через WebSocket‑соединение.
Для клиентского подключения я выбрал HTTP и SOCKS5. Поскольку обычная схема передачи логина и пароля не подходит для недоверенной сети, поверх этих протоколов можно использовать TLS — HTTPS и SOCKS5 over TLS. Для продакшена рекомендую использовать клиентов именно с поддержкой TLS, чтобы защитить учетные данные от перехвата при передаче. Определение протокола работает автоматически. TLS, HTTP и SOCKS5 имеют свои характерные признаки в начале соединения, по которым Proxy‑сервер определяет тип входящего трафика и выбирает соответствующую реализацию.
Отдельно хочется упомянуть, что данное решение также поддерживает UDP relay — это позволяет проксировать UDP‑трафик. Чуть позже рассмотрим его настройку.
Между Proxy‑сервером и broker, а также между broker и worker используется авторизация по токенам. Для worker‑узлов также предусмотрена настройка рандомизации пути подключения, которая позволяет скрыть конечный адрес подключения.
Для управления этой схемой пришлось сделать небольшой административный интерфейс. Через него задаются прокси‑аккаунты, группы worker‑узлов и правила маршрутизации.
Чтобы проверить описанную архитектуру на практике, рассмотрим минимальную конфигурацию.
Инструкция по запуску
(Подразумевается, что Docker и docker‑compose у вас уже установлены)
1) Клонируем проект:
git clone https://github.com/split-proxy/split-proxy.git cd split-proxy
2) Копируем env’ы:
cp .env.example .env cp .env.worker.example .env.worker
3) Запускаем Proxy‑сервер, broker, админку и прилагающиеся службы:
docker compose up -d --build
Сначала проверим базовый сценарий: клиент подключается к локальному proxy, после чего запрос выполняется без использования удалённого worker:
curl -L --proxy http://test-proxy:test-proxy-password@127.0.0.1:8787 https://google.com
4) Добавляем локального worker’а:
docker compose -f docker-compose.worker.yml up -d --build
5) Заходим в админ‑панель по адресу:
http://127.0.0.1:5555
Вводим логин/пароль:
admin/simple-split-proxy-password
Переходим в groups and workers, видим там живой worker с guid f45765ea-1240-4648-b681-80c888015c76. Добавляем новую тестовую группу local. Привязываем worker’а к созданной группе.
Далее переходим в раздел Routing, и выбираем там в первой вкладке вместо direct нашу группу local — это означает, что весь трафик, не попадающий ни под одно правило, пойдет через группу local.
6) Проверяем корректность работы Proxy‑сервера с учетом новой маршрутизации:
*Дополнительно можно открыть лог worker’а с помощью команды docker compose ‑f docker‑compose.worker.yml logs ‑f и удостовериться, что там появляется что‑то вроде:
opening TCP session=a8f62146-f9b5-4d89-8b56-f16934536b9c target=google.com:443
Подключение удалённого worker
Следующий эксперимент — вынесем worker на отдельный сервер.
1) Также клонируем проект
git clone https://github.com/split-proxy/split-proxy.git cd split-proxy
2) Копируем и редактируем env.worker:
cp .env.worker.example .env.worker
В файле меняем:
WORKER_GUID на любой другой
BROKER_ADDR=ws://<внешний_ip_вашего_proxy_сервера>:8899
* не забудьте настроить port forwarding для порта 8899 на Proxy‑сервере, если он находится за NAT.
3) Запускаем нашего нового worker’а:
docker compose -f docker-compose.worker.yml up -d --build
4) Заходим в админку и проверяем, что появился второй worker. Назначаем его на новую группу, например external. Выставляем дефолтный routing external.
5) Проверяем работу нашего прокси:
curl -L --proxy http://test-proxy:test-proxy-password@127.0.0.1:8787 https://google.com
6) Зайдя на любой сервис проверки IP удостоверимся, что внешний IP соответствует удалённому серверу. Также можем поиграться с переключением routing’а и увидеть изменение вашего IP‑адреса в realtime (системе нужно некоторое время для прогрева кэша, поэтому после переключения ожидаем пару секунд)
В ходе эксперимента использовались упрощённые настройки. Для реального развёртывания необходимо отдельно решить вопросы аутентификации и защиты транспортных соединений:
изменения токенов
изменения логинов и паролей прокси/админов
добавления nginx перед broker и админ‑панелью с настоящими сертификатами
проброса настоящих сертификатов в proxy для подключения через TLS
настройки UDP_RELAY_HOST — он должен указывать на IP‑адрес вашего Proxy‑сервера с открытыми портами 55000–60000
Ну что, с запуском и базовым сценарием разобрались. Теперь посмотрим, как всё это ведёт себя под нагрузкой.
Performance
Для тестирования накладных расходов и того, насколько ограничивает скорость вся эта система, было решено взять за эталон стандартный tinyproxy, поставить его на удаленную машину и замерить скорость. После этого взять развёрнутый нами Proxy и замерить, какую скорость выдаст Proxy с одним worker‑узлом, подключённым к broker.
Результат замера с tinyproxy:

Результат замера split‑proxy:

Кажется что‑то пошло не так… Но это только на первый взгляд. Следующим шагом было разобраться, что именно идёт не так. Вооружившись iperf3, я посмотрел скорость канала в прямом и обратном направлениях между worker и broker. Обнаружилось как раз ограничение в download‑скорости:


Причём если я запускал iperf3 с несколькими параллельными соединениями, скорость росла:

По итогу оказалось, что в нашем случае одно TCP‑соединение из‑за потерь упирается примерно в 20 Mbit/s. При этом при увеличении количества параллельных соединений суммарная скорость растёт. Что ж, не проблема, добавляем несколько worker‑узлов на нашу удаленную машину и проверяем speedtest еще раз.
Повторный результат замера с tinyproxy:

Повторный результат замера с split‑proxy и семью worker‑узлами:

Теперь уже все не так плохо: результат практически идентичный.
Concurrency
Стоит отдельным пунктом поговорить про concurrency. Для измерения я накидал простенький скрипт параллельных curl‑запросов. В целом он получился не совсем честным, так как ответ сервера — обычная HTML‑страничка — занимает ~0.03 секунды. Хорошо бы проверить, что произойдет, если TTFB будет большой или контент объемный. Но получилось, что получилось, делюсь результатами:

Ограничения и открытые вопросы
В текущем варианте остались несколько технических ограничений:
Транспортный слой пока использует WebSocket; отдельный вопрос — насколько оправдано добавление альтернативных транспортов вроде HTTP/2 и long polling.
Административный интерфейс пока неудобен при массовом изменении правил маршрутизации.
Кроме того, текущая схема маршрутизации общая для всех клиентов. Интересный следующий эксперимент — исследовать возможность задавать правила на уровне отдельного proxy‑клиента.
Ещё одна нерешённая задача — интеграция с браузером, например автоматическое определение ресурсов страницы и применение к ним разных правил маршрутизации.
Обратная связь
На этом эксперимент заканчивается. Если какие‑то архитектурные решения или результаты измерений кажутся спорными, буду рад обсудить их в комментариях.
Исходный код и дополнительные технические материалы доступны в репозитории проекта.
PS Телеграм‑каналы не рекламирую.
PPS Первая статья на хабр, строго не судите.