У меня на рабочих ноутбуках и домашнем компьютере почти постоянно включён корпоративный VPN. Без него не открываются Jira и другие внутренние сервисы.

Запустить рядом второй VPN в моём случае нельзя, а переключаться между ними каждый раз, когда нужно открыть ChatGPT - неудобно.

При этом мне хотелось пользоваться всем в одном Chrome: в одной вкладке держать рабочую Jira, в другой — ChatGPT или любой другой внешний сервис.

В итоге у меня получилось не одно общее сетевое правило, а три разных маршрута:

Что я открываю

Какой маршрут используется

Jira и другие внутренние рабочие сервисы

Через корпоративный VPN

Отдельные внешние сайты, которые мне нужны в браузере

Через локальный прокси

Всё остальное

Напрямую, по обычному системному маршруту

Коротко: обычный proxy switcher хорошо маршрутизирует домены, которые пользователь уже знает. Я хотел автоматизировать предыдущий шаг — найти внешние домены, без которых сайт открывается, но работает только частично.

Да, можно завести второй браузер или отдельный профиль. Можно настроить split tunneling, если корпоративный VPN это позволяет.

В моём случае правила VPN находятся не под моим контролем, а постоянно переключаться между браузерами мне не хотелось. Поэтому я остановился на локальном прокси и PAC-маршрутизации по списку доменов.

PAC — это небольшой скрипт с функцией FindProxyForURL. Chrome передаёт ей адрес и получает в ответ, нужно открыть его через прокси или напрямую. Применяется такая конфигурация через chrome.proxy.

Сразу три уточнения.

Во-первых, расширение не предоставляет прокси и не поднимает его самостоятельно. Оно работает поверх уже настроенного локального клиента — например, V2Ray — и только говорит Chrome, какие сайты отправлять через него.

Во-вторых, proxy switcher’ов уже существует много. Списки доменов, PAC-скрипты, ручные Proxy- и Direct-правила — всё это давно решённые задачи. Я не пытался сделать ещё один «революционный переключатель прокси».

И третье — про авторство разработки.

Идея, пользовательские сценарии, требования, ручные проверки и продуктовые решения были моими. Код, тесты и большую часть рефакторинга делал Codex, а ChatGPT помогал мне разбирать варианты, формулировать задачи и проводить ревью результата.

Сам код вручную я почти не писал. По сути, я был продактом и тестировщиком собственного пет-проекта: описывал, как всё должно работать, проверял результат в браузере и возвращал разработку на следующий круг, если что-то работало не так.

Поэтому дальше для краткости я иногда пишу «я сделал», имея в виду «я придумал, сформулировал, проверил и довёл до работающего состояния».

А началась вся эта история с довольно простой проблемы.

Сайт открылся. Функция — нет

Обычно логика proxy switcher’а выглядит так:

  1. Добавляешь example.com.

  2. Включаешь применение правила ко всем поддоменам.

  3. www.example.com, api.example.com и остальные хосты внутри этого домена идут через прокси.

В большинстве случаев этого достаточно.

Но у меня быстро накопилось несколько исключений.

Основной сайт

Что не работало

Какой дополнительный домен понадобился

Letterboxd

Постеры и изображения

ltrbxd.com

YouTube

Превью роликов

i.ytimg.com

ChatGPT

Загрузка файлов

oaiusercontent.com и его поддомены

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

Правило для youtube.com покрывает www.youtube.com, m.youtube.com, studio.youtube.com, но никак не затрагивает i.ytimg.com, потому что ytimg.com — другой регистрируемый домен.

То есть похожие названия здесь не помогают: для proxy-правила это два независимых доменных дерева.

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

Обычно дальше нужно:

  • открыть DevTools;

  • перейти в Network или Console;

  • повторить сломанное действие;

  • найти запросы к внешним хостам;

  • попробовать понять, какой из них связан с проблемой;

  • добавить его в прокси;

  • проверить всё ещё раз.

Эти действия пришлось повторять не один раз. Я менял устройства, пробовал разные расширения, а нужные домены каждый раз собирал заново.

В какой-то момент автоматизировать процесс стало проще, чем снова лезть в DevTools.

Так появился Smart Proxy Route Helper, а одной из его основных функций стал поиск связанных доменов.

Первая попытка: посмотреть, что уже использует страница

Сначала решение казалось довольно очевидным.

Если YouTube уже попытался загрузить превью с i.ytimg.com, значит информация об этом ресурсе есть на странице.

Остаётся собрать внешние домены, убрать очевидный мусор и показать пользователю кандидатов.

Так появился режим Preview.

Я открываю сайт, нажимаю «Найти связанные домены», после чего расширение один раз анализирует текущую страницу.

Оно смотрит Resource Timing и ссылки на ресурсы, которые уже есть на странице: изображения, скрипты, стили, iframe, media-элементы, preload, preconnect и некоторые lazy-loading ресурсы. Отдельно оно заглядывает в доступные открытые shadow roots.

Обход ограничен по количеству элементов и результатов, чтобы диагностика не превращалась в тяжёлый краулер страницы.

Полный URL расширению при этом не нужен.

Например, если на странице найден ресурс https://i.ytimg.com/vi/abc123/hqdefault.jpg?some=params, в дальнейшую обработку отправляется только i.ytimg.com.

Путь, query-параметры, fragment и credentials отбрасываются сразу.

Дальше цепочка выглядит так:

ресурсы страницы
        ↓
нормализованные hostname
        ↓
классификация кандидатов
        ↓
выбор области будущего правила
        ↓
подтверждение пользователя
        ↓
добавление в PAC

Никакое правило на этапе Preview автоматически не создаётся. Расширение только показывает, что нашло, и отдельно предлагает сохранить выбранные варианты.

Для Letterboxd и YouTube этого подхода оказалось достаточно. Нужные ресурсы появляются уже при загрузке страницы, поэтому ltrbxd.com и i.ytimg.com можно увидеть обычным анализом.

На этом месте я решил, что основная задача почти закрыта.

А потом попробовал загрузить файл в ChatGPT — и эта красивая схема сломалась.

Что делать, если нужного домена на странице ещё нет

После добавления основных доменов ChatGPT у меня работали интерфейс и обычная переписка. Но когда я пытался загрузить файл, ничего не происходило.

В моём случае ChatGPT обращался при загрузке к oaiusercontent.com. Но до выбора файла простой Preview этого домена не показывал.

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

Запрос возникает только после того, как я выбираю файл и запускаю загрузку.

Получается замкнутый круг:

  • функция не работает из-за отсутствующего proxy-правила;

  • нужный hostname появляется только во время вызова этой функции;

  • разовый анализ уже загруженной страницы его не видит.

Для расширения понадобился отдельный режим — Record.

Режим

На какой вопрос отвечает

Когда использовать

Preview

Какие внешние ресурсы уже использует загруженная страница?

Изображения, скрипты, стили, превью и другие ресурсы, появляющиеся сразу

Record

Какие hostname появились во время конкретного действия?

Загрузка файла, запуск видео, открытие модального окна или отдельной функции SPA

Именно поэтому для YouTube достаточно Preview, а для загрузки файлов в ChatGPT понадобился Record.

Сценарий там выглядит так:

  1. Открываю popup расширения.

  2. Запускаю Record.

  3. Выбираю файл.

  4. Дожидаюсь неуспешной загрузки.

  5. Останавливаю запись.

  6. В результатах появляется oaiusercontent.com.

После добавления правила загрузка файлов начинает работать.

Как устроен Record

Что записывается

Я не хотел превращать расширение в постоянно работающий сетевой монитор. Поэтому запись запускается только вручную, привязана к конкретной вкладке и ограничена по времени.

Во время записи расширение временно оборачивает:

  • window.fetch

  • XMLHttpRequest.prototype.open

  • navigator.sendBeacon

Дополнительно оно следит за Resource Timing и ошибками загрузки обычных ресурсных элементов.

Код временно внедряется в MAIN world страницы. Это нужно потому, что fetch, XMLHttpRequest и sendBeacon, которыми пользуется сама страница, находятся именно там. Скрипт из стандартного isolated world расширения не смог бы просто обернуть их для страницы.

Hostname фиксируется в момент начала запроса, а не только после успешного ответа.

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

Какие данные остаются

Из страницы в расширение передаётся только hostname.

Из условной подписанной ссылки https://files-123.oaiusercontent.com/path?signature=...&expires=... останется только files-123.oaiusercontent.com.

Расширение не читает и не сохраняет:

  • содержимое загружаемого файла;

  • тела запросов;

  • заголовки;

  • cookies;

  • подписи временных URL;

  • ответы сервера;

  • текст страницы.

События привязаны к конкретной сессии случайным nonce и повторно проверяются на стороне расширения.

После остановки, отмены, тайм-аута или перехода на другую страницу временные hooks и observers удаляются.

Чего Record не видит

Но это всё ещё диагностика с неполным покрытием, а не полноценный перехват трафика.

Record может не увидеть запросы:

  • Service Worker;

  • Worker;

  • браузера;

  • другого расширения;

  • недоступного cross-origin frame;

  • произошедшие до начала записи.

Пустой результат означает не «внешних доменов нет», а только «доступные странице механизмы ничего не зафиксировали».

Плоский список hostname оказался почти таким же неудобным

Допустим, Record успешно собрал всё, что происходило во время загрузки файла.

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

Проблема в том, что одновременно с полезным запросом приложение может:

  • отправить событие аналитики;

  • обновить сессию;

  • обратиться к error monitoring;

  • подгрузить шрифт;

  • провести A/B-эксперимент;

  • запустить фоновый API-запрос.

Все эти домены действительно появились во время действия пользователя. Но только один из них может быть нужен для самой функции.

В результате первая версия плоского списка была технически честной, но практически не очень полезной. Я всего лишь перенёс часть работы из DevTools в popup.

Пришлось добавить классификацию.

Категория

Что означает

Поведение по умолчанию

Вероятно связан

Есть сильный сигнал, что домен относится к текущему сервису

Может быть выбран заранее, но не сохраняется без подтверждения

Требует проверки

Сторонний домен найден, но уверенности недостаточно

Показывается пользователю, но не выбирается автоматически

Игнорируется

Аналитика, реклама, helper или слишком широкая общая инфраструктура

Не предлагается для обычного добавления

Никакой ML-модели за этим нет.

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

Неизвестные домены я специально не скрываю агрессивно.

В этой задаче есть два типа ошибок.

  • False positive: расширение показало лишний кандидат - Это создаёт шум и может привести к ненужному правилу.

  • False negative: расширение спрятало действительно необходимый hostname - Тогда пользователь снова идёт в DevTools — то есть вся функция теряет смысл.

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

Пользователь также может локально изменить классификацию:

  • всегда предлагать этот домен для данного сайта;

  • всегда игнорировать его для данного сайта;

  • всегда оставлять его для ручной проверки;

  • игнорировать его глобально.

Это отдельная настройка, а не proxy-правило. Она влияет только на будущие результаты Preview и Record.

Встроенные связи и пользовательские настройки работают локально, без удалённой модели и без отправки истории диагностики на сервер.

Найти hostname недостаточно. Нужно ещё сохранить правильное правило

Следующая проблема обнаружилась на генерируемых хостах.

Во время загрузки файла расширение может увидеть что-то вроде files-12345.oaiusercontent.com.

Если добавить точное правило только для него, следующий файл может загружаться уже через files-67890.oaiusercontent.com.

Первое правило ничего не даст.

Для такого семейства логичнее сохранить oaiusercontent.com со всеми поддоменами.

Но применять эту операцию к каждому стороннему hostname опасно.

Например, если страница использует customer-a.cloudfront.net, нельзя автоматически превратить это в всё cloudfront.net с поддоменами.

За cloudfront.net находится инфраструктура множества не связанных между собой сервисов. То же самое относится к общим hosting-доменам вроде github.io, appspot.com или vercel.app.

Поэтому внутри расширения разделены два понятия:

  • observed host — конкретный hostname, который был замечен на странице;

  • route target — правило, которое в итоге будет сохранено в PAC.

Что обнаружено

Какое правило предложить

Почему

files-12345.oaiusercontent.com

oaiusercontent.com с поддоменами

Следующий запрос может уйти на другой сгенерированный hostname того же семейства

customer-a.cloudfront.net

Только точный hostname

Расширение до всего cloudfront.net затронет множество посторонних сервисов

assets.example.co.uk

example.co.uk

co.uk — публичный суффикс, а не домен конкретного сервиса

Найти hostname недостаточно. Нужно ещё выбрать минимальную область правила, которая не сломает маршрутизацию других сайтов.

Для определения регистрируемого домена используется Public Suffix List через локально собранную библиотеку tldts.

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

Почему не взять webRequest и не видеть всё сразу

На этом месте возникает закономерный вопрос: зачем временно оборачивать fetch и собирать Resource Timing, если у Chrome есть сетевые API?

chrome.webRequest. Даёт сетевые события, но для неизвестных заранее внешних доменов потребует широких host permissions.

chrome.debugger. Даёт более полную картину, включая frames и workers, но требует отдельного чувствительного permission и подключения к DevTools Protocol.

DevTools-панель. Позволила бы работать с данными Network, но пользователю всё равно пришлось бы открывать DevTools — ровно от этого сценария я хотел уйти.

Я пока выбрал более ограниченный вариант.

Текущий manifest содержит:

{
  "permissions": [
    "proxy",
    "storage",
    "activeTab",
    "scripting"
  ]
}

и не содержит постоянных host_permissions.

activeTab даёт временный доступ к текущей странице после действия пользователя, а chrome.scripting позволяет на основании этого временного разрешения выполнить код в выбранном контексте.

Я не считаю webRequest или chrome.debugger плохими API. Если главная цель — максимально полная диагностика, они могут быть более правильным выбором.

У меня был другой приоритет: диагностика запускается изредка, только на выбранном сайте и только по явной команде пользователя. Ради неё я не хотел запрашивать постоянный доступ ко всему трафику браузера.

Здесь нет варианта без компромисса: либо расширение получает больше сетевых данных и более широкие разрешения, либо сохраняет узкие permissions, но часть запросов может пропустить.

А что в расширении обычное

Всё остальное уже ближе к обычному PAC-менеджеру:

  • Proxy- и Direct-правила;

  • точный hostname и включение поддоменов;

  • локальная генерация PAC;

  • Chrome Sync;

  • импорт и экспорт настроек.

Список доменов синхронизируется между устройствами, а адрес, порт и тип локального прокси остаются локальными.

Для меня это удобно: список сайтов на рабочем и личном компьютере может быть одинаковым, а proxy-клиенты и их порты — разными.

Для Proxy-правил используется fail-closed поведение. Если я явно решил, что домен должен идти через прокси, PAC не содержит для него запасного DIRECT.

Если локальный прокси недоступен, совпавший запрос завершается ошибкой, а не незаметно уходит напрямую.

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

Интересная для меня часть находится перед ними:

сайт работает частично
        ↓
обнаружить внешнюю зависимость
        ↓
отделить её от фонового шума
        ↓
выбрать безопасную область правила
        ↓
получить подтверждение
        ↓
и только потом изменить маршрутизацию

Что я хочу попробовать дальше

Текущая версия уже позволяет мне реже открывать Network вручную. Но назвать определение зависимостей «умным» я пока не могу.

Главная нерешённая задача — ранжирование кандидатов.

Допустим, я запускаю Record и нажимаю кнопку загрузки файла. В этот момент появляются:

files.example.net
analytics.example.org
session.example.com
errors.example.io

Как понять, что первый hostname важнее остальных?

Пока я вижу три основных направления.

Отделять обычный фон от конкретного действия

Перед Record можно запомнить hostname, которые страница использует в спокойном состоянии, а затем повысить приоритет тех, которые впервые появились после действия пользователя.

Дополнительно можно учитывать:

  • тип запроса;

  • ошибку загрузки;

  • время между действием и запросом;

  • первое появление hostname;

  • fetch, XHR, resource element или sendBeacon.

Например, beacon на известный analytics-хост можно понизить, а новый XHR сразу после выбора файла — поднять.

Но для любого такого правила легко найти контрпример.

Аналитика тоже может ходить через fetch, а критическая часть приложения — загружаться как обычный JavaScript или изображение.

Проверять гипотезу и спрашивать пользователя

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

Так появится более сильный эксперимент:

без candidate.com — не работает
с candidate.com — работает

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

В большинстве случаев всё равно придётся спросить пользователя:

Проблема решилась?

Да
Частично
Нет

Связь вида chatgpt.com → oaiusercontent.com → помогло можно хранить только локально и учитывать при следующих проверках.

Но здесь возникает другая проблема: зависимости меняются. Сегодня сервис использует один CDN, завтра другой.

Поддерживать небольшую проверяемую базу известных связей

Для популярных сервисов можно хранить несколько подтверждённых пар прямо в коде расширения:

letterboxd.com → ltrbxd.com
chatgpt.com → oaiusercontent.com
youtube.com → i.ytimg.com

Предложения от сообщества можно принимать через GitHub issues или pull requests, проверять вручную и добавлять в следующую версию.

Я пока не хочу загружать такую базу удалённо во время работы расширения.

Помимо доверия к источнику, это создаёт вопросы версионирования, модерации и незаметного изменения логики уже установленного продукта.


Где мне особенно нужна обратная связь

Сейчас у меня нет ощущения, что задача решена окончательно.

Скорее есть рабочая первая версия и несколько направлений, каждое со своими минусами.

Особенно интересны ответы на четыре вопроса.

  1. Какие сигналы вы бы использовали для ранжирования найденных hostname, если не расширять текущие permissions?

  2. Что здесь опаснее: false positive, который создаст лишнее proxy-правило, или false negative, после которого пользователь всё равно пойдёт в DevTools? Где вы бы поставили порог для автоматического предложения?

  3. Не упускаю ли я более надёжный способ связать появившийся hostname с конкретным действием пользователя, не переходя на chrome.debugger или широкие host permissions?

  4. Имеет ли смысл локально запоминать подтверждённые связи site → domain, или такой кэш быстро устареет и начнёт мешать?

Если вы решали похожую задачу через другой API или совсем другую архитектуру, такой вариант тоже особенно интересен.

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

Именно на таких сценариях лучше всего проверять Preview, Record и качество фильтрации.

Проект открыт:

Я не ожидаю, что расширение полностью заменит DevTools.

Но хочется хотя бы сократить путь от «сайт вроде открылся, но что-то всё ещё сломано» до понятного и минимального набора proxy-правил.~~~~~~~~

Комментарии (2)


  1. nomark
    24.08.2026 14:21

    Мне кажется проще сделать вторую учетку в браузере, ну или запустить другой браузер в котором уже будет нужная прокся для всех сайтов. И не придется каждый домен отдельно прописывать


    1. GeoKhv Автор
      24.08.2026 14:21

      Да, это достаточно быстро и да, даже так и делал.

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