Skip to content

Прокси и пул прокси

Будь то обход корпоративной сети, ротация исходных IP для задач сбора данных или обход IP-блокировок на целевом сайте — прокси остаются частой потребностью HTTP-клиента. В HTTPC встроены четыре режима прокси — одиночный прокси, обнаружение системного прокси, ротация пула и ротация по статус-коду — покрывающие весь спектр от «фиксированного выхода» до «смены IP на каждый запрос» и работающие совместно с защитой от SSRF, проверкой TLS и движком повторов. Вся конфигурация прокси сосредоточена в ConnectionConfig.

Обзор режимов прокси

Четыре режима применяются автоматически по приоритету; при одновременной настройке нескольких действует только режим с высшим приоритетом:

ПриоритетНастройкаПоведениеТипичный сценарий
1 (высший)ProxyURLВсегда использовать указанный прокси (режим одиночного прокси)Выход корпоративной сети, локальный VPN-порт
2ProxyPoolРотация в пуле прокси с размыканием и восстановлениемСбор данных, распределение нагрузки, ротация IP
3EnableSystemProxyАвтообнаружение системных настроек проксиДесктоп-приложения, следующие настройкам пользователя
4 (низший)НетПрямое подключениеПоведение по умолчанию

Подсказка

Если заданы одновременно ProxyURL и ProxyPool, применяется ProxyURL. Чтобы использовать пул прокси, очистите ProxyURL.

Одиночный прокси

ProxyURL задаёт фиксированный прокси, поддерживаются четыре протокола:

ПротоколЗаписьОписание
HTTPhttp://proxy:8080Самый распространённый; HTTPS-запросы идут через CONNECT-туннель
HTTPShttps://proxy:8443TLS используется и для соединения с самим прокси-сервером
SOCKS5socks5://proxy:1080Целевой домен разрешается локально, соединение устанавливается через прокси
SOCKS5hsocks5h://proxy:1080Разрешение домена выполняется на стороне прокси, обходит локальное загрязнение DNS
go
package main

import (
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.DefaultConfig()
    cfg.Connection.ProxyURL = "socks5://proxy.example.com:1080"

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    result, err := client.Get("https://api.example.com/data")
    if err != nil {
        log.Fatal(err)
    }
    // Вывод: статус: 200, прокси: socks5://proxy.example.com:1080
    log.Printf("статус: %d, прокси: %s", result.StatusCode(), result.Meta.ProxyURL)
}

Аутентификация и маскирование

Учётные данные прокси указываются прямо в userinfo-части URL:

go
cfg := httpc.DefaultConfig()
cfg.Connection.ProxyURL = "http://user:[email protected]:8080"

Автоматическое маскирование учётных данных

Config.String() заменяет имя пользователя и пароль в URL прокси на ***:***; URL в сообщениях ошибок и логах маскируется так же (учётные данные и чувствительные параметры запроса скрываются). Учётные данные не утекут в логи, но саму конфигурацию всё равно нужно хранить надёжно.

Обнаружение системного прокси и NO_PROXY

После включения автоматически определяются настройки прокси операционной системы — задавать ProxyURL вручную не нужно:

go
cfg := httpc.DefaultConfig()
cfg.Connection.EnableSystemProxy = true

Особенности платформ

ПлатформаИсточник обнаружения
WindowsРеестр Internet Settings (ProxyEnable / ProxyServer)
macOSЧтение Web/Secure Web Proxy предпочтительной сетевой службы командой networksetup
LinuxПеременные окружения HTTP_PROXY / HTTPS_PROXY

Meta.ProxyURL не охватывает системный прокси

Выбор системного прокси не записывается в Result.Meta.ProxyURL (поле имеет значение только при явном Connection.ProxyURL или ProxyPool; при прямом соединении и системном прокси оно остаётся пустым). Если нужно отслеживать исходящий прокси для каждого запроса, используйте явную конфигурацию.

Порядок и детали обнаружения

  1. Переменные окружения приоритетны (на всех платформах): сначала читаются HTTP_PROXY / HTTPS_PROXY / NO_PROXY (регистр не важен); при наличии значения используются сразу, системные настройки не запрашиваются.
  2. Платформенное обнаружение как запасной вариант: без переменных окружения читаются настройки платформы. Прокси рабочего стола Linux (GNOME/KDE) обычно уже экспортированы сессией в переменные окружения — движок не читает gsettings/dconf напрямую.
  3. Разделение запросов: HTTPS-запросы предпочитают HTTPS_PROXY, при отсутствии откатываются к HTTP_PROXY; HTTP-запросы используют только HTTP_PROXY, а при его отсутствии тоже откатываются к HTTPS_PROXY — так же, как в net/http.
  4. Прямое подключение в CGI-окружении: прокси из переменных окружения не действует в CGI-среде (когда задан REQUEST_METHOD) — поведение как в net/http.
  5. Автодополнение «голого» адреса: значения без префикса протокола вроде HTTP_PROXY=proxy:8080 автоматически трактуются как http://.
  6. Кэширование: результат обнаружения кэшируется на время жизни клиента и не обновляется при изменении переменных окружения; чтобы подхватить изменения, создайте новый клиент.

Правила обхода NO_PROXY

NO_PROXY перечисляет хосты, которые не должны идти через прокси; семантика совпадает с пакетом httpproxy из net/http:

ПравилоПримерОбласть сопоставления
Обход всего*Все хосты — прямое подключение
Суффикс доменаexample.com или .example.comДомен и все его поддомены
Поддомены по маске*.example.comЭквивалент .example.com
Литерал IP10.0.0.5Точное совпадение IP
CIDR-диапазон10.0.0.0/8Все IP диапазона
Хост + портexample.com:443Хост совпадает и порт точно равен

Несколько правил разделяются запятыми; localhost всегда подключается напрямую — прописывать его в NO_PROXY не нужно.

bash
# Пример настройки Linux/macOS
export HTTPS_PROXY=http://proxy.corp.example.com:8080
export NO_PROXY=localhost,127.0.0.1,.internal.corp.com,10.0.0.0/8
powershell
# Пример настройки Windows (PowerShell)
$env:HTTPS_PROXY = "http://proxy.corp.example.com:8080"
$env:NO_PROXY = "localhost,127.0.0.1,.internal.corp.com"

Ограничение динамического localhost-прокси

В режиме системного прокси список освобождений SSRF сканируется один раз при построении клиента. Если за время работы системный прокси сменился на новый внутренний/loopback-адрес (например 127.0.0.1), этот адрес может быть заблокирован защитой от SSRF. В динамических сценариях задавайте явно Connection.ProxyURL (адрес прокси всегда освобождён от SSRF-проверок) или настройте SSRFExemptCIDRs.

Пул прокси

Когда запросы нужно распределять между несколькими прокси-IP (сбор данных, распределение нагрузки, ротация IP), пул прокси даёт автоматическую ротацию, пассивное размыкание и смену прокси по статус-коду — без каких-либо внешних компонентов.

Базовое использование

go
package main

import (
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.DefaultConfig()
    cfg.Connection.ProxyPool = []string{
        "http://proxy1:8080",
        "http://proxy2:8080",
        "http://proxy3:8080",
    }
    cfg.Connection.ProxyPoolStrategy = httpc.ProxyStrategyRoundRobin // значение по умолчанию, можно опустить

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    result, err := client.Get("https://api.example.com/data")
    if err != nil {
        log.Fatal(err)
    }
    // Вывод: статус: 200, прокси: http://proxy1:8080 (следующий запрос автоматически получит proxy2)
    log.Printf("статус: %d, прокси: %s", result.StatusCode(), result.Meta.ProxyURL)
}

Записи пула поддерживают протоколы http, https, socks5, socks5h и могут смешиваться.

Поля конфигурации

ПолеТипПо умолчаниюОписание
ProxyPool[]stringnilСписок URL прокси
ProxyPoolStrategyProxyStrategyProxyStrategyRoundRobinСтратегия выбора
ProxyFailureThresholdint3 (0 → значение по умолчанию)Порог последовательных сбоев соединения для размыкания
ProxyCooldowntime.Duration30s (0 → значение по умолчанию)Время охлаждения разомкнутого прокси
ProxyRotatePerRequestboolfalseПринудительная смена прокси на каждый запрос (отключает переиспользование простаивающих соединений)
ProxyRotateOnStatus[]intnilСтатус-коды, запускающие повтор со сменой прокси (каждый в пределах 100–599)

Стратегия выбора

СтратегияКонстантаОписание
Round-robin (по умолчанию)ProxyStrategyRoundRobinЦиклический выбор по порядку, курсор сдвигается при каждом выборе
СлучайнаяProxyStrategyRandomРавномерный случайный выбор из здоровых прокси

Round-robin + повторы = автоматическая смена IP

Стратегия round-robin сдвигает курсор при каждом выборе, поэтому повтор, снова запускающий выбор, естественным образом попадает на следующий прокси — без какой-либо дополнительной настройки.

Пассивный размыкатель цепи

В пул прокси встроена пассивная проверка работоспособности. Размыкание запускают только сбои на уровне соединения (dial/TLS); HTTP-статус-коды — нет:

text
Сбой соединения с прокси

Счётчик сбоев +1

Последовательных сбоев ≥ ProxyFailureThreshold → размыкание (исключение из ротации)

Ожидание ProxyCooldown → полузакрытая проба (возврат в ротацию)

Успех → сброс счётчика, замыкание
Первый сбой → повторное размыкание
go
cfg.Connection.ProxyFailureThreshold = 5        // терпимее к единичным сбоям
cfg.Connection.ProxyCooldown = 60 * time.Second // более долгое охлаждение

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

Ротация по статус-коду

Для сценариев IP-блокировок Cloudflare/WAF — при возврате определённых статус-кодов автоматически меняется прокси и выполняется повтор:

go
package main

import (
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.DefaultConfig()
    cfg.Connection.ProxyPool = []string{
        "http://proxy1:8080",
        "http://proxy2:8080",
        "http://proxy3:8080",
    }
    cfg.Connection.ProxyRotateOnStatus = []int{403} // при 403 — смена прокси и повтор
    cfg.Retry.MaxRetries = 3                        // повторы должны быть включены

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    result, err := client.Get("https://protected-site.example.com/data")
    if err != nil {
        log.Fatal(err)
    }
    // Вывод: статус: 200, прокси: http://proxy2:8080, попыток: 2
    log.Printf("статус: %d, прокси: %s, попыток: %d",
        result.StatusCode(), result.Meta.ProxyURL, result.Meta.Attempts)
}

Ротация по статус-коду ≠ размыкание

Ротация, запущенная ProxyRotateOnStatus, не размыкает прокси — IP-блокировки часто специфичны для цели (прокси, заблокированный на сайте A, может нормально работать на сайте B). Размыкание запускается только сбоями на уровне соединения. Для срабатывания требуется Retry.MaxRetries > 0.

Когда задан ProxyRotateOnStatus и в пуле несколько прокси, бюджет повторов автоматически повышается до len(ProxyPool) - 1 (с пределом MaxRetries = 10), чтобы каждый прокси получил шанс быть опробованным.

Ротация для каждого запроса

ProxyRotatePerRequest решает проблему закрепления прокси-туннеля из-за переиспользования соединений: пул HTTP-соединений переиспользует установленные TCP-соединения, включая их прокси-туннели. Это значит, что последовательные запросы к одному хосту используют прокси предыдущего запроса, даже если курсор ProxyPoolStrategy уже сдвинут.

После включения в начале каждого запроса закрываются все простаивающие соединения, вынуждая Transport заново оценить пул прокси — цена в отсутствии переиспользования соединений (новое соединение + прокси-туннель на каждый запрос), зато ротация по запросам гарантирована:

go
package main

import (
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.DefaultConfig()
    cfg.Connection.ProxyPool = []string{
        "http://proxy1:8080",
        "http://proxy2:8080",
        "http://proxy3:8080",
    }
    cfg.Connection.ProxyRotatePerRequest = true // смена прокси на каждый запрос

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    for i := 0; i < 3; i++ {
        result, err := client.Get("https://api.example.com/data")
        if err != nil {
            log.Fatal(err)
        }
        // Вывод (по порядку): http://proxy1:8080 / http://proxy2:8080 / http://proxy3:8080
        log.Printf("Запрос %d через: %s", i+1, result.Meta.ProxyURL)
    }
}

Сценарии применения

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

Как и ProxyRotateOnStatus, ProxyRotatePerRequest при нескольких прокси в пуле автоматически повышает бюджет повторов до len(ProxyPool) - 1 (предел 10), гарантируя, что каждый прокси будет опробован хотя бы раз.

Детерминированная ротация

Повторы при ротации по статус-коду отличаются от обычных повторов: движок резервирует для каждого запроса базовый индекс прокси, и N-й повтор всегда использует прокси «база + N». Отсюда три гарантии:

  • Повтор всегда меняет прокси: в рамках одного запроса N-й повтор обязательно попадает на другой прокси, чем (N-1)-й;
  • Цепочка перенаправлений не сбивается: несколько переходов внутри одной попытки делят один индекс — следование перенаправлениям не расходует лишние выборы прокси и не смещает порядок;
  • Ротация продолжается между запросами: базовые индексы разных запросов возрастают, сохраняя в целом round-robin/случайное распределение.

Кроме того, сбои соединения самого прокси (сбой dial/TLS) при активной ротации всегда повторяемы — даже если обычная классификация признаёт ошибку неповторяемой (например, постоянная ошибка адреса), следующая попытка сменит прокси, а пассивное размыкание естественно отсеет плохие прокси. Подробнее о стороне повторов — в Повторных попытках и отказоустойчивости.

Как узнать, какой прокси использовался

В сценариях с пулом прокси часто нужен аудит: «через какой выход реально прошёл этот запрос». Работают в паре два инструмента:

  • Result.Meta.ProxyURL: сообщает прокси, использованный попыткой, породившей итоговый ответ; при прямом подключении — пустая строка, системный прокси (EnableSystemProxy) также не записывается (остаётся пустым). При ротации каждая попытка может использовать разный прокси — поле соответствует последней попытке.
  • Колбэк WithOnResponse: срабатывает на каждую попытку (включая повторы со сменой прокси), позволяя наблюдать статус-коды и число попыток по мере выполнения.
go
package main

import (
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.DefaultConfig()
    cfg.Connection.ProxyPool = []string{
        "http://proxy1:8080",
        "http://proxy2:8080",
        "http://proxy3:8080",
    }
    cfg.Connection.ProxyRotateOnStatus = []int{403}

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    result, err := client.Get("https://protected-site.example.com/data",
        httpc.WithOnResponse(func(resp httpc.ResponseMutator) error {
            log.Printf("Попытка %d получила %d", resp.Attempts(), resp.StatusCode())
            return nil
        }),
    )
    if err != nil {
        log.Fatal(err)
    }

    // Вывод: итоговый прокси: http://proxy2:8080, всего попыток: 2
    log.Printf("итоговый прокси: %s, всего попыток: %d", result.Meta.ProxyURL, result.Meta.Attempts)
}

Колбэки выполняются по попыткам, middleware — по запросам

WithOnResponse срабатывает внутри движка на каждую попытку (включая повторы); цепочка middleware оборачивает весь цикл повторов и на один логический запрос выполняется единожды. Для наблюдения за отдельными попытками используйте колбэк, для аудита запроса целиком — цепочку промежуточного ПО.

Взаимодействие прокси и повторов

Ротация прокси глубоко связана с движком повторов — стоит запомнить два правила.

1. Бюджет повторов повышается автоматически. При заданном ProxyRotateOnStatus или ProxyRotatePerRequest и более одном прокси в пуле:

text
Эффективный MaxRetries = max(настроенный MaxRetries, len(ProxyPool) - 1) (предел 10)

Например, 5 прокси и MaxRetries = 3: бюджет повышается до 4 (= 5 - 1) — первый запрос идёт через proxy1, после 403 прокси меняются по очереди proxy2…proxy5, каждый опробован по разу.

2. Сбой соединения прокси принуждает к повтору. При активной ротации сбои соединения самого прокси (сбой dial, сбой TLS) обходят обычную классификацию повторяемости и сразу переходят к следующему повтору со следующим прокси — безнадёжные прокси (неверный порт, недостижимый хост) не нужно отсеивать вручную: принудительный повтор плюс размыкание по последовательным сбоям вытеснят их сами.

Условия повторов, математика отката и общий бюджет таймаута на все повторы — в Повторных попытках и отказоустойчивости.

Вопросы безопасности при работе с прокси

Связанные с прокси функции автоматически обрабатывают следующие аспекты безопасности — ручная настройка не требуется:

  • Освобождение от SSRF: адреса хостов прокси (ProxyURL и все записи ProxyPool) автоматически добавляются в список освобождений SSRF и не блокируются проверками приватных IP — локальные прокси (например 127.0.0.1:7890) тоже работают.
  • Дедупликация: записи с одинаковыми host:port автоматически сливаются, предотвращая перекос ротации и двойной подсчёт.
  • Проверка URL: все URL прокси проходят проверку безопасности (белый список протоколов http/https/socks5/socks5h, непустой host, защита от инъекций); некорректное значение возвращает ошибку уже в New(), а не молча игнорируется.

Отношение к TLS: HTTPS-запрос через CONNECT-туннель HTTP-прокси остаётся сквозным TLS — проверка сертификатов, минимальная версия TLS и закрепление сертификатов действуют для целевого сайта как обычно, прокси лишь пересылает шифротекст. При опасении локального загрязнения DNS предпочитайте socks5h (разрешение домена на стороне прокси); при включённом DoH бизнес-домены разрешаются по шифрованному каналу, но сам адрес прокси через DoH не проходит (прокси задаётся разработчиком явно, соединение устанавливается напрямую с хостом прокси). Полная картина защиты — в Защите от SSRF.

Частые проблемы

ПроблемаПричинаРешение
Прокси не действуетЗаданы одновременно ProxyURL и ProxyPool, приоритет у ProxyURLОчистите ProxyURL, используйте только ProxyPool
Исходный IP не меняется при последовательных запросах к одному хостуПереиспользование соединения закрепило прошлый прокси-туннельВключите ProxyRotatePerRequest
Прокси часто размыкаетсяProxyFailureThreshold слишком малУвеличьте порог или ProxyCooldown
Ротация по статус-коду не срабатываетRetry.MaxRetries = 0 или в пуле один проксиЗадайте MaxRetries > 0; держите в пуле минимум 2 прокси
Что будет, когда разомкнуты все проксиПоследовательные сбои всего пулаДвижок резервом вернёт прокси, ближайший к восстановлению, и не отказывает сразу; проверьте доступность прокси и пороги
Системный прокси не обнаруженНет переменных окружения, платформенные настройки выключеныПроверьте HTTP_PROXY/HTTPS_PROXY или включение системного прокси
localhost-прокси блокирован SSRFСписок освобождений системного прокси сканируется один раз при построенииЗадайте явно Connection.ProxyURL (всегда освобождён) или настройте SSRFExemptCIDRs
Размыкается ли прокси после смены по 403Ошибочное ожидание, что статус-код вызывает размыканиеНет — ротация по статус-коду не размыкает прокси, размыкание вызывают только сбои уровня соединения
Meta.ProxyURL всегда пустоДействует только системный прокси — EnableSystemProxy в это поле не записываетсяЕсли нужно наблюдать исходящий адрес, используйте явный ProxyURL или ProxyPool

Полное описание полей — в Конфигурация API — Пул прокси.

Лучшие практики

СценарийРекомендуемая настройка
Фиксированный корпоративный выходProxyURL (http/https/socks5 по необходимости)
Следование настройкам системы пользователяEnableSystemProxy + NO_PROXY для внутренних диапазонов
Распределение нагрузки между проксиProxyPool + стратегия round-robin по умолчанию
Сбор данных с одного хостаProxyPool + ProxyRotatePerRequest
IP-блокировка CF/WAFProxyPool + ProxyRotateOnStatus: []int{403}
Неоднородное качество проксиУвеличьте ProxyFailureThreshold (терпимость к сбоям) и ProxyCooldown
Аудит исходного IPЧитайте Result.Meta.ProxyURL, наблюдайте попытки через WithOnResponse
Хранение учётных данных проксиВ userinfo URL, логи маскируются автоматически; файл конфигурации храните отдельно

Цена ротации для производительности

ProxyRotatePerRequest и путь повторов ротации по статус-коду закрывают простаивающие соединения, жертвуя переиспользованием ради смены выхода. Для чувствительных к производительности сценариев без ротации оставьте значения по умолчанию (переиспользование соединений); управление простаивающими соединениями — в Пуле соединений и DNS.

Что дальше