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 с сообщением:
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 напечатан отдельной строкой:
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 против тестового прокси:
* 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, чтобы разделить два кода состояния:
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.0 | curl 8.18.0 и 8.22.0 |
|---|---|---|
https://example.org/ (заблокирован) | exit 56, curl: (56) CONNECT tunnel failed, response 403 | exit 7, curl: (7) CONNECT tunnel failed, response 403 |
http://example.org/ (заблокирован) | exit 0, страница 403 от прокси выведена как тело ответа | exit 0, страница 403 от прокси выведена как тело ответа |
http://example.org/ с --fail | exit 22, curl: (22) The requested URL returned error: 403 | exit 22, curl: (22) The requested URL returned error: 403 |
https://example.com/ (разрешён) | exit 0, http=200 connect=200 | exit 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) — его собственное; настоящий прокси песочницы или провайдера пришлёт другой текст.
Связанные материалы
- Как исправить ошибку прокси 407 — когда прокси вместо этого просит учётные данные.
- Как исправить ERR_TUNNEL_CONNECTION_FAILED — тот же отказ внутри Chromium, Playwright и Puppeteer.
- Переменные окружения прокси — какую переменную клиент читает на самом деле.
- NO_PROXY matching, tested — почему запись для обхода совпадает в одном клиенте и не совпадает в другом.
- Прокси и вычисления для AI-агента — каким частям агента вообще нужен прокси.
ipvolt строит прокси-инфраструктуру для разработчиков и агентов. Вступите в лист ожидания — одно письмо, когда откроется доступ.
Источники
- RFC 9110: CONNECT
- anthropics/claude-code#56959: sandbox allowlist answering 403 to CONNECT
- SocketDev/socket-sdk-js#659: CI sandbox firewall blocking one host
- curl discussion #15718: proxy responses for http and https URLs
- Squid configuration: http_access suggested rules
- Bright Data: proxy error catalog
- Decodo: residential proxy restricted targets
- Node.js CLI: NODE_USE_ENV_PROXY