Анализ7 мин чтения

Почему ваш агент для программирования получает «CONNECT tunnel failed, response 403»

curl вашего агента падает с CONNECT tunnel failed, response 403, хотя другие хосты работают. Прокси отказал в туннеле. Как понять, кто его заблокировал, и что исправить.

На этой странице
code
curl: (56) CONNECT tunnel failed, response 403

Так curl 8.15–8.17 сообщает, что попросил прокси открыть туннель к хосту https:// и получил отказ. curl 8.18.0 и 8.22.0 печатают то же сообщение, но с кодом выхода 7 вместо 56. Тот же отказ, записанный через один локальный тестовый прокси, в Python Requests 2.34.2 выглядит так — библиотека бросает requests.exceptions.ProxyError с сообщением:

code
HTTPSConnectionPool(host='example.org', port=443): Max retries exceeded with url: / (Caused by ProxyError('Unable to connect to proxy', OSError('Tunnel connection failed: 403 Forbidden')))

А во встроенном fetch Node.js 24.20.0 с NODE_USE_ENV_PROXY=1, где каждый уровень err.cause напечатан отдельной строкой:

code
depth 0: TypeError: fetch failed
depth 1: DOMException: Request was cancelled.
depth 2: RequestAbortedError [UND_ERR_ABORTED]: Proxy response (403) !== 200 when HTTP Tunneling

Во всех трёх случаях прокси сказал «нет» раньше, чем ваш запрос ушёл дальше. Через тот же прокси https://example.com/ во всех клиентах вернул 200. Изменился только хост.

Что означает этот 403

Для URL https:// клиент отправляет прокси CONNECT example.org:443 и ждёт разрешения открыть туннель. RFC 9110 говорит, что любой ответ 2xx переводит соединение в режим туннеля, а «любой ответ, кроме успешного, означает, что туннель ещё не образован». 403 — это отказ прокси открыть туннель. RFC 9110

Отсюда два следствия:

  • Дело не в учётных данных. Прокси, которому нужны учётные данные, отвечает 407 с вызовом Proxy-Authenticate; этот случай разобран в руководстве по 407. 403 означает, что прокси решил не пересылать этот запрос, поэтому смена пароля не поможет.
  • Целевой сайт так и не был достигнут. Нет туннеля — нет TLS-рукопожатия и нет запроса к example.org. Это видно по write-out curl: connect=403 — ответ прокси, а http=000 означает, что сайт так ничего и не ответил.

Почему на это натыкаются агенты для программирования

Песочницы и CI-раннеры часто пропускают весь исходящий трафик через egress-прокси со списком разрешённых хостов. Адрес прокси приходит в HTTPS_PROXY, поэтому curl, pip, npm и fetch идут через него. Хосты из списка получают туннель; все остальные — 403. Поэтому из одной и той же оболочки один хост работает, а следующий падает.

Этот паттерн видно в двух публичных отчётах:

  • В anthropics/claude-code#56959 у Claude Code, работающего с Bedrock, HTTPS_PROXY указывал на собственный прокси песочницы на localhost. Этот прокси разрешал четыре хоста (bedrock-runtime.us-east-1.amazonaws.com, api.anthropic.com и два шаблона Sentry), а для любого другого хоста, по словам автора, «возвращает HTTP 403 от локального прокси». curl https://github.com падал с curl: (56) CONNECT tunnel failed, response 403. Автор сообщил, что записи sandbox.network.allowedDomains в пользовательских и управляемых настройках в этом режиме игнорировались. Issue закрыли как дубликат #37970.
  • В SocketDev/socket-sdk-js#659 агент, выполнявший еженедельное обновление зависимостей в CI, не смог завершить работу. Инструмент обновления taze искал версии на npm.antfu.dev, который «блокирует файрвол песочницы CI (403 CONNECT tunnel failed)». curl к этому хосту вернул HTTP 000 с ошибкой curl 56, а curl https://registry.npmjs.org/semver вернул 200. Мейнтейнеры исправили это, оставив запросы на registry.npmjs.org, который уже был в списке разрешённых.

Как понять, кто сказал «нет»

Запустите падающий URL один раз с curl -v через тот же прокси. Строки, начинающиеся с >, — это то, что отправил curl; строки, начинающиеся с <, до CONNECT tunnel failed — ответ прокси. Вот curl 8.16.0 против тестового прокси:

code
*   Trying 127.0.0.1:41745...
* CONNECT: no ALPN negotiated
* allocate connect buffer
* Establish HTTP proxy tunnel to example.org:443
> CONNECT example.org:443 HTTP/1.1
> Host: example.org:443
> User-Agent: curl/8.16.0
> Proxy-Connection: Keep-Alive
> 
< HTTP/1.1 403 Forbidden
< Content-Type: text/plain
< Content-Length: 32
< Connection: close
< 
* CONNECT tunnel failed, response 403
* closing connection #0
curl: (56) CONNECT tunnel failed, response 403

Строка Trying называет прокси, а не сайт. 403 приходит в ответ на CONNECT, и после него нет строк TLS, значит, сайт в обмене не участвовал. Если же 403 появляется после успешного CONNECT и TLS-рукопожатия, его прислал сайт, и прокси здесь ни при чём.

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

sh
curl --disable -sS -o /dev/null -x http://127.0.0.1:41745 -w 'http=%{http_code} connect=%{http_connect} exit=%{exitcode}\n' https://example.com/
curl --disable -sS -o /dev/null -x http://127.0.0.1:41745 -w 'http=%{http_code} connect=%{http_connect} exit=%{exitcode}\n' https://example.org/

С curl 8.18.0 первая команда напечатала http=200 connect=200 exit=0. Вторая напечатала http=000 connect=403 exit=7 и curl: (7) CONNECT tunnel failed, response 403. Тот же клиент, тот же прокси, другой хост: у прокси правило для каждого хоста. Повторите сравнение со своими разрешённым и заблокированным хостами. В песочнице агента возьмите хост, который точно работает, например реестр пакетов, до которого агент уже достучался.

Наконец, проверьте, какой прокси процесс использует на самом деле. Выведите HTTPS_PROXY, HTTP_PROXY, NO_PROXY и их варианты в нижнем регистре в той же оболочке или шаге задания, где возникает сбой. Хост из NO_PROXY идёт мимо прокси напрямую, а это песочница может блокировать иначе. Руководство по переменным окружения прокси объясняет, как клиенты выбирают между этими переменными, а статья NO_PROXY matching, tested показывает, что клиенты по-разному понимают, что совпадает с записью в NO_PROXY. Встроенный fetch Node игнорирует эти переменные, пока не задан NODE_USE_ENV_PROXY=1 или --use-env-proxy, поэтому скрипт на Node и команда curl в одной оболочке могут идти разными маршрутами. Документация Node.js CLI

Исправьте, не обходя прокси

403 — это решение политики, поэтому исправлять нужно на стороне политики или на стороне источника:

  • Разрешите хост. Добавьте точный домен, который нужен инструменту, в список разрешённых egress-хостов песочницы или CI. Берите домен из строки CONNECT в curl -v или из ошибки самого инструмента, а не из его конфигурации: в отчёте Socket taze искал версии на npm.antfu.dev, а не в реестре npm.
  • Используйте источник, который уже разрешён. Зеркало, внутренний реестр или официальный реестр, который политика уже пропускает, часто избавляет от необходимости менять политику. Именно так исправили проблему в Socket.
  • Проверьте порт. Некоторые прокси туннелируют только порт 443. Предлагаемая конфигурация Squid содержит http_access deny CONNECT !SSL_ports, где SSL_ports — порт 443. Squid http_access В тестовом прокси, который разрешал example.com только на порту 443, https://example.com:8443/ получил тот же CONNECT tunnel failed, response 403, что и заблокированный хост.
  • Проверьте правила провайдера для назначений. Коммерческие прокси-провайдеры тоже могут отказывать в доступе к назначениям. Каталог ошибок Bright Data перечисляет ошибки политики в разделе HTTP 403, например «You tried to target %HOST% which is blocked by Bright Data policy». Каталог ошибок Bright Data Decodo перечисляет категории целей, например сайты банков и государственных органов, которые он по умолчанию ограничивает. Ограниченные цели Decodo

Не обходите egress-правила песочницы. Если снять HTTPS_PROXY, добавить хост в NO_PROXY или пустить трафик через другой прокси, чтобы обойти список разрешённых хостов, вы сведёте на нет контроль, который кто-то поставил намеренно, — и это может просто не сработать: автор #56959 обнаружил, что песочница блокировала и прямой сетевой доступ в обход прокси. Попросите того, кто управляет песочницей или CI-раннером, разрешить хост, и передайте ему точную строку CONNECT из curl -v. Агент в отчёте Socket сделал именно так: остановился, сообщил о заблокированном хосте и не пытался обойти файрвол.

Почему curl по-другому сообщает о http://

Для URL http:// никакого CONNECT нет. curl отправляет прокси весь запрос, и 403 от прокси — это ответ на этот запрос. Тестовый прокси ответил 403 в обоих случаях, но curl сообщил о них по-разному:

Через тот же проксиcurl 8.15.0–8.17.0curl 8.18.0 и 8.22.0
https://example.org/ (заблокирован)exit 56, curl: (56) CONNECT tunnel failed, response 403exit 7, curl: (7) CONNECT tunnel failed, response 403
http://example.org/ (заблокирован)exit 0, страница 403 от прокси выведена как тело ответаexit 0, страница 403 от прокси выведена как тело ответа
http://example.org/ с --failexit 22, curl: (22) The requested URL returned error: 403exit 22, curl: (22) The requested URL returned error: 403
https://example.com/ (разрешён)exit 0, http=200 connect=200exit 0, http=200 connect=200

Для http:// curl -v показывает > GET http://example.org/ HTTP/1.1, затем < HTTP/1.1 403 Forbidden, и curl печатает тело. Мейнтейнер curl Даниэль Стенберг описал эту разницу в обсуждении curl #15718, которое посвящено 407, но относится к любому отказу: если туннель не удалось установить, передача не выполнена, и это ошибка, а GET к прокси, который вернул ответ, — это «успешная передача, содержащая код 4xx». Поэтому URL http:// в скрипте без --fail может выглядеть как успех.

Другие клиенты разделились так же, с одним исключением. Python Requests вернул обычный Response со статусом 403 для http://example.org/. fetch в Node.js 24.20.0 с NODE_USE_ENV_PROXY=1 открыл туннель CONNECT example.org:80 даже для URL http://, поэтому упал с той же причиной Proxy response (403) !== 200 when HTTP Tunneling.

Ещё одна деталь версий: использованная нами статическая сборка curl 8.22.0 включает HTTP/3. Когда хост объявлял HTTP/3, она сначала пыталась открыть туннель CONNECT-UDP, поэтому -v показал два ответа 403 перед итоговым curl: (7) CONNECT tunnel failed, response 403.

Воспроизведите у себя

Всё описанное выше записано 2 октября 2026 года с помощью лаборатории из папки загрузок: прокси на Node.js на 127.0.0.1, который туннелирует только к example.com на порт 443 и отвечает 403 на любой другой CONNECT и обычный HTTP-запрос; curl 8.15.0, 8.16.0, 8.17.0, 8.18.0 и 8.22.0, Python 3.14.4 с Requests 2.34.2 и urllib3 2.8.0 и Node.js 24.20.0. Сырой вывод — в results.json. Тело ответа 403 тестового прокси (403 Forbidden: lab proxy policy) — его собственное; настоящий прокси песочницы или провайдера пришлёт другой текст.

Связанные материалы

ipvolt строит прокси-инфраструктуру для разработчиков и агентов. Вступите в лист ожидания — одно письмо, когда откроется доступ.

Источники

  1. RFC 9110: CONNECT
  2. anthropics/claude-code#56959: sandbox allowlist answering 403 to CONNECT
  3. SocketDev/socket-sdk-js#659: CI sandbox firewall blocking one host
  4. curl discussion #15718: proxy responses for http and https URLs
  5. Squid configuration: http_access suggested rules
  6. Bright Data: proxy error catalog
  7. Decodo: residential proxy restricted targets
  8. Node.js CLI: NODE_USE_ENV_PROXY

Теги:ProxiesTroubleshooting