Туннель CONNECT: как работает прокси с шифрованием и что видит посредник
Метод CONNECT просит прокси открыть TCP-соединение до указанного узла и порта, после чего посредник перестаёт разбирать содержимое и просто перекладывает байты в обе стороны. Клиент шлёт одну строку CONNECT example.com:443 HTTP/1.1, получает в ответ 200 Connection established, и уже внутри этого канала браузер договаривается о шифровании напрямую с целевым сервером.
Ниже разобрано, откуда взялся такой порядок, как выглядит обмен на уровне байтов, где внутри туннеля живёт заголовок Proxy-Authorization, какие коды приходят на этапе установки и почему через тот же механизм проходит не только HTTPS. Команды даны такие, чтобы обмен можно было посмотреть своими глазами за минуту.
Откуда взялся метод CONNECT
Обычный HTTP через посредник устроен прямолинейно. Клиент отправляет прокси полную строку с абсолютным адресом, GET http://example.com/page HTTP/1.1, посредник читает её целиком, сам идёт на целевой сервер, забирает ответ и отдаёт его обратно. Пока содержимое открыто, схема работает без оговорок: прокси видит путь, заголовки, тело и коды ответа.
С шифрованием схема упирается в стену. TLS поднимается между браузером и целевым сервером, сессионные ключи считают только эти две стороны, и промежуточный узел получает поток, который ему нечем разобрать. Попытка вклиниться в такой обмен ломает проверку сертификата на стороне клиента, и браузер закрывает соединение с ошибкой.
Выход описали отдельным методом. CONNECT переводит посредника из режима разбора запросов в режим транспорта: прокси открывает TCP-соединение до узла, подтверждает готовность и дальше работает как труба. Метод закреплён в RFC 7231 и уточнён в RFC 9110, поддержка есть у всех распространённых прокси-серверов и у любого браузера. Мы держим этот механизм включённым на всех адресах пула, поэтому прокси HTTPS с поддержкой туннеля CONNECT работают с браузерами и парсерами без дополнительной настройки на стороне сервера.
Разница между обычным проксированием и туннелем определяет всё остальное: объём того, что посредник может прочитать, момент, когда авторизация ещё возможна, и набор протоколов, которые пройдут поверх.
Как выглядит обмен на установку туннеля и что идёт дальше
Обмен занимает две строки плюс пустая строка на каждой стороне. Клиент открывает TCP-соединение с прокси и отправляет:
CONNECT example.com:443 HTTP/1.1
Host: example.com:443
Proxy-Authorization: Basic dXNlcjU1MjE6cGYzOWtk
Proxy-Connection: Keep-Alive
User-Agent: Mozilla/5.0
Первая строка содержит метод, целевой узел с портом через двоеточие и версию протокола. Путь тут отсутствует: посреднику незачем знать, какая страница будет запрошена внутри, ему нужен адрес и порт для установки соединения. Заголовок Host дублирует ту же пару. Дальше идут заголовки уровня посредника, которые до целевого сервера не доедут.
Прокси разрешает имя example.com в адрес, открывает к нему TCP-соединение на порт 443 и, если всё сложилось, отвечает:
HTTP/1.1 200 Connection established
Proxy-Agent: proxy/1.1
Слово в статусной строке может отличаться: встречаются 200 Connection established, 200 OK и 200 Tunnel established. Значение одно, клиент смотрит на код. После пустой строки соединение перестаёт быть HTTP-обменом, и первым байтом от клиента идёт уже ClientHello протокола TLS.
| Часть обмена | Кто отправляет | Что несёт |
|---|---|---|
CONNECT host:port HTTP/1.1 | Клиент | Целевой узел и порт для установки соединения |
Host: host:port | Клиент | Та же пара, дублируется по требованию протокола |
Proxy-Authorization | Клиент | Учётные данные для доступа к посреднику |
200 Connection established | Прокси | Подтверждение, что соединение до узла открыто |
| Байты после пустой строки | Обе стороны | Содержимое туннеля, посредник его не трогает |
Момент отправки 200 меняет роль прокси-сервера полностью. До подтверждения он работал разборщиком HTTP: читал строку запроса, проверял заголовки, проверял право доступа. После подтверждения он держит два сокета и копирует байты между ними, пока одна из сторон не закроет соединение.
Внутри туннеля идёт TLS-рукопожатие. Браузер отправляет ClientHello, сервер отвечает ServerHello и цепочкой сертификатов, стороны согласуют шифры и считают общий ключ. Посредник видит эти пакеты как поток произвольных байтов. Разобрать их он не сможет: ключ считался по обмену, в котором прокси не участвовал.
Отсюда практическое следствие, ради которого туннель и придумали. Заголовки прикладного уровня, куки, тело POST-запроса, путь до страницы и коды ответа целевого сервера остаются внутри шифрования. В логах посредника от всей сессии осядут четыре величины: время, имя узла с портом, объём переданных байтов в каждую сторону и длительность соединения.
Одно соединение туннеля обслуживает всю сессию с узлом. Браузер открывает CONNECT к example.com:443 один раз, поднимает TLS и дальше гоняет внутри десятки HTTP-запросов по тому же каналу. Когда параллельно нужен второй узел, поднимается второй туннель со своей строкой CONNECT. При работе в много потоков это стоит держать в голове: число одновременных туннелей равно числу открытых соединений, и оно упирается в лимит потоков пакета.
Где живёт Proxy-Authorization и почему он уходит один раз
Заголовок Proxy-Authorization относится к отрезку между клиентом и посредником. Он несёт пару логина и пароля в виде Basic с кодированием base64 и обрабатывается прокси-сервером. До целевого сервера он не доходит никогда: посредник снимает его при обработке, а в туннельном режиме он физически не может попасть дальше, потому что после подтверждения посредник уже ничего не пишет в поток.
Отсюда правило, которое объясняет половину вопросов по настройке. Учётные данные уходят ровно один раз, в самом запросе CONNECT. Всё, что клиент отправит после 200 Connection established, зашифровано и адресовано целевому серверу. Прокси не разберёт эти байты и не найдёт там никаких заголовков, поэтому авторизоваться внутри поднятого туннеля невозможно по устройству механизма.
Практика из этого вытекает такая:
# curl подставляет Proxy-Authorization в CONNECT самостоятельно
curl -v -x http://user5521:[email protected]:8000 https://example.com
# та же пара отдельным ключом, результат идентичный
curl -v --proxy 185.24.87.14:8000 --proxy-user user5521:pf39kd https://example.com
Если пакет работает по привязке своего адреса, заголовок не нужен вообще: посредник узнаёт клиента по адресу источника и отвечает подтверждением сразу. Оба варианта доступа включены в пакет, переключение идёт в кабинете. Подробный разбор форматов строки подключения собран в материале про два способа доступа к прокси.
Вторая тонкость касается программ, которые умеют HTTP, но путаются в туннеле. Часть библиотек шлёт Authorization вместо Proxy-Authorization, и тогда посредник не находит учётных данных и отвечает отказом. Имя заголовка проверяется первым, когда доступ по логину отбивается при рабочей привязке.
Коды ответа на этапе установки туннеля
До подтверждения посредник отвечает обычными кодами HTTP, и каждый несёт свою причину. Разбор занимает секунды, если знать, что означает конкретная цифра.
| Код | Строка ответа | Что произошло | Что делать |
|---|---|---|---|
| 200 | Connection established | TCP-соединение до узла открыто, туннель поднят | Ничего, дальше идёт TLS |
| 403 | Forbidden | Посредник отказал в доступе к узлу или порту | Проверить порт назначения и права пакета |
| 407 | Proxy Authentication Required | Учётные данные отсутствуют или не подошли | Сверить логин, пароль и привязанный адрес |
| 502 | Bad Gateway | Посредник не смог открыть соединение до узла | Проверить имя узла, порт и доступность цели |
| 504 | Gateway Timeout | Целевой узел не ответил за отведённое время | Повторить запрос, проверить порт на цели |
Код 407 приходит с заголовком Proxy-Authenticate: Basic realm="proxy", который подсказывает клиенту нужную схему. Браузер на этот код показывает окно с полями логина и пароля, curl печатает ответ и завершает работу. Причина обычно сидит в одном из трёх мест: опечатка в паре, привязка оформлена на прежний адрес машины, программа отправила заголовок под неправильным именем. Мы разбираем такие обращения по логину и типу прокси, ответ приходит одним сообщением.
Код 403 на этапе CONNECT чаще всего означает запрет порта. Многие посредники пропускают через туннель только 443 и несколько известных портов, поэтому попытка открыть канал на нестандартный порт заканчивается отказом ещё до соединения. Наш пул пропускает произвольные порты, поэтому такой код тут указывает на другое: доступ к конкретному узлу закрыт или пакет неактивен.
Код 502 говорит о том, что посредник сделал свою часть работы и получил отказ от целевой стороны. Внутри него прячутся три разных случая: имя узла не разрешилось, порт закрыт, узел разорвал соединение сразу после установки. Отличить их помогает прямая проверка того же узла без посредника.
# смотрим, отвечает ли цель напрямую с этой машины
curl -v --connect-timeout 5 https://example.com -o /dev/null
# проверяем порт отдельно, без TLS и без HTTP
nc -vz example.com 443
Что видит посредник до и после туннеля
Разделение по времени тут строгое: до отправки 200 посредник читает открытый текст, после подтверждения он копирует байты. Таблица показывает, что остаётся доступным на каждом отрезке.
| Данные | До установки туннеля | После установки туннеля |
|---|---|---|
| Имя целевого узла | Видно в строке CONNECT | Видно из уже установленного соединения |
| Порт назначения | Видно в строке CONNECT | Видно |
| Путь до страницы | Отсутствует в обмене | Зашифрован |
| Заголовки запроса | Видны только заголовки посредника | Зашифрованы |
| Куки и токены сессии | Не передаются на этом этапе | Зашифрованы |
| Тело запроса и ответа | Отсутствует | Зашифровано |
| Код ответа целевого сервера | Отсутствует | Зашифрован |
| Объём переданных байтов | Ещё нулевой | Виден как число |
Имя узла попадает в лог посредника всегда: без него открыть соединение невозможно. Всё остальное содержимое сессии закрыто шифрованием. Именно поэтому туннельный режим считается основным для работы с личными кабинетами, панелями и любыми сервисами, где внутри ходят токены.
Отдельно смотрим на разрешение имён. При работе через CONNECT имя узла разрешает посредник, поэтому запрос к DNS уходит с его стороны, и локальный резолвер машины про целевой домен ничего не узнаёт. Такое поведение отличает туннель от схемы, где программа сама резолвит имя и передаёт готовый адрес. Проверять его удобно сразу после первой удачной установки туннеля.
Заголовки, которые видны посреднику до туннеля, тоже стоит держать под контролем. User-Agent в запросе CONNECT отправляют не все клиенты, Proxy-Connection встречается у браузеров, Via и X-Forwarded-For добавляет уже сам посредник, если настроен так. Мы служебных пометок в туннель не подмешиваем, и анонимные адреса без служебных заголовков отдают целевому серверу ровно то, что отправил клиент.
Почему через CONNECT проходит не только HTTPS
Метод не привязан к TLS ни одной строкой. В запросе указывается пара из имени узла и номера порта, посредник открывает TCP-соединение и передаёт байты. Что именно поедет внутри, механизму безразлично: там может идти TLS, открытый текст, бинарный протокол или обмен, придуманный самим приложением.
Отсюда список того, что реально ходит через туннель на практике:
| Протокол | Порт | Что идёт внутри туннеля |
|---|---|---|
| HTTPS | 443 | TLS-рукопожатие и HTTP-запросы поверх него |
| SMTP с STARTTLS | 587 | Открытый диалог SMTP с переходом на шифрование |
| SMTPS | 465 | TLS сразу после установки соединения |
| IMAP over TLS | 993 | Обмен почтового клиента с сервером |
| WebSocket over TLS | 443 | Апгрейд соединения внутри уже поднятого TLS |
| Собственный протокол приложения | Любой | Произвольный поток байтов |
Ограничение тут ставит сам посредник списком разрешённых портов. Классическая настройка Squid разрешает CONNECT только на 443 и 563, поэтому почтовый клиент через такой прокси не пройдёт. Наш пул работает с произвольными портами, и почтовые задачи закрываются тем же способом, что и веб: разбор отправки писем собран в материале про SMTP через прокси.
Один момент туннель не покрывает: транспорт остаётся строго TCP. Обмен по UDP через CONNECT провести нельзя, потому что механизм построен на установленном TCP-соединении. Программам, которым нужен UDP, подходит сокетный протокол с отдельной командой, и там работает прокси SOCKS5 для произвольных портов и протоколов.
Как посмотреть обмен своими глазами
Первый инструмент это curl с ключом -v. Он печатает обе стороны обмена с посредником, включая строку CONNECT и ответ на неё. Строки с > идут от клиента, строки с < приходят от прокси.
curl -v -x http://185.24.87.14:8000 https://example.com -o /dev/null
Полезная часть вывода выглядит так:
* Connected to 185.24.87.14 (185.24.87.14) port 8000
* allocate connect buffer
* Establish HTTP proxy tunnel to example.com:443
> CONNECT example.com:443 HTTP/1.1
> Host: example.com:443
> User-Agent: curl/8.5.0
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 200 Connection established
<
* CONNECT phase completed
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* Server certificate: example.com
Строка Establish HTTP proxy tunnel отмечает начало установки, CONNECT phase completed отмечает конец. Всё, что идёт после неё, относится уже к обмену с целевым сервером через поднятый канал. Если туннель не поднялся, вместо 200 в этом месте будет код отказа, и дальше вывод оборвётся.
Второй инструмент показывает сторону TLS подробнее. openssl s_client умеет ходить через посредника ключом -proxy и печатает цепочку сертификатов, версию протокола и выбранный шифр:
openssl s_client -proxy 185.24.87.14:8000 -connect example.com:443 -servername example.com
В выводе смотрим на четыре вещи: строку CONNECTED, цепочку Certificate chain, поле Verify return code: 0 (ok) и согласованную версию TLSv1.3. Нулевой код проверки подтверждает, что сертификат целевого сервера дошёл до клиента без подмены, и туннель отработал честной трубой. Когда пакет требует авторизации по логину, к команде добавляется ключ -proxy_user:
openssl s_client -proxy 185.24.87.14:8000 -proxy_user user5521 -connect example.com:443
Третий способ пригодится, когда нужно увидеть сами пакеты. tcpdump на интерфейсе машины покажет, что до посредника уходит открытая строка CONNECT, а весь остальной обмен идёт нечитаемым потоком:
tcpdump -i any -A -n 'host 185.24.87.14 and port 8000' -c 40
Разбор случаев, когда туннель не поднимается
Первый случай простой: curl печатает код отказа сразу после строки CONNECT. Тут причина лежит на стороне посредника, и код указывает на неё напрямую по таблице выше. Проверяем доступ, порт и активность пакета.
Второй случай интереснее. Туннель поднялся, 200 пришло, а дальше соединение обрывается на TLS-рукопожатии с сообщением вроде SSL routines::unexpected eof while reading. Тут посредник свою часть выполнил, и разбираться нужно с целевым узлом: он может отвечать на 443 чем-то отличным от TLS, требовать SNI или закрывать соединения от незнакомых сетей.
Третий случай встречается у программ со своим сетевым стеком. Клиент отправляет вместо CONNECT обычный GET https://example.com/, посредник получает запрос с абсолютным адресом на схеме https и отвечает отказом. Лечится настройкой: в программе указывается прокси именно как HTTP-посредник для схемы HTTPS, тогда библиотека сама переключится на туннельный режим. Мы проверяем эту настройку первой, когда программа отказывается работать по шифрованной схеме.
Четвёртый случай связан с числом одновременных туннелей. Каждый CONNECT удерживает соединение, пока клиент его не закроет, и при агрессивных настройках парсера открытых каналов набирается больше, чем даёт лимит пакета. Стандартные пакеты держат до 1000 потоков, корпоративный до 3000, при двух привязанных адресах общее число делится пополам. Когда прогон упирается в потолок, часть запросов повисает в ожидании, и в логах это выглядит как таймауты без кода ответа.
| Что видно в выводе | Где искать причину | Первый шаг |
|---|---|---|
| Код 407 сразу после CONNECT | Доступ к посреднику | Сверить пару логина или адрес привязки |
| Код 502 после CONNECT | Целевой узел | Проверить имя и порт прямой командой |
| Обрыв на TLS-рукопожатии | Целевой сервер или SNI | Повторить с ключом -servername |
| Программа шлёт GET вместо CONNECT | Настройка клиента | Указать посредника для схемы HTTPS |
| Таймауты без кода ответа | Число открытых туннелей | Снизить потоки в настройках прогона |
Отдельно проверяем сам факт, что запросы идут через пул. Быстрый способ занимает одну команду и показывает выходной адрес, который увидел целевой сервер. Мы держим около 12 000 активных адресов, ротация внутри пула автоматическая, поэтому выходной адрес от прогона к прогону меняется, и это нормальная картина. Работают такие адреса на собственном оборудовании, и серверные адреса с постоянным откликом держат туннель ровно столько, сколько нужно программе.
Туннель в браузере и в программах
Браузер выбирает режим сам по схеме адреса. Ввод http:// даёт обычное проксирование с абсолютным адресом в строке запроса, ввод https:// включает CONNECT. Никаких отдельных галочек для этого нет: один и тот же прокси в настройках обслуживает обе схемы, порт тоже один.
В программах картина зависит от библиотеки. Python с requests берёт адрес посредника из переменных окружения и переключается на туннель для схемы https автоматически:
export http_proxy=http://user5521:[email protected]:8000
export https_proxy=http://user5521:[email protected]:8000
python -c "import requests; print(requests.get('https://example.com').status_code)"
Обратите внимание на строку https_proxy: схема внутри неё остаётся http, потому что она описывает протокол общения с посредником. Само шифрование поднимется внутри туннеля. Эта деталь путает многих, и попытка написать там https:// заканчивается ошибкой подключения.
Парсеры и прикладные программы обычно предлагают выбор типа посредника списком. Вариант с пометкой HTTP или HTTPS означает туннельный режим для шифрованных адресов, и он подходит для сбора страниц, работы с кабинетами и проверки выдачи. Состав пакета с потоками, форматами выдачи и безлимитным трафиком описан там, где оформляется пакет с поддержкой HTTPS и туннельного режима, а разбор обычных запросов без шифрования собран на странице прокси HTTP для открытых запросов.
Частые вопросы
Подойдёт ли туннель CONNECT под мою программу?
Заранее предугадать поведение каждой библиотеки и каждого целевого сайта нельзя, поэтому перед покупкой доступен бесплатный тест длительностью до 2 часов под ваш запрос. Проверять стоит той же программой с теми же настройками, которая пойдёт в работу: вывод curl -v покажет строку CONNECT и код ответа за первые секунды.
Какой тип прокси выбрать, если нужен и HTTPS, и произвольный порт?
В пакете доступны IPv4, HTTP, HTTPS, SOCKS4 и SOCKS5, переключение идёт строкой подключения без доплат. Для веба и кабинетов берут туннельный режим по HTTPS, для программ с нестандартными портами и собственным транспортом рекомендуется SOCKS5. Адреса при этом одни и те же, меняется только протокол общения с посредником.
Почему выходной адрес внутри туннеля каждый раз другой?
Пул держится в районе 12 000 активных адресов, ротация внутри него автоматическая, список обновляется в реальном времени. География это микс со всего мира, выборка по отдельной стране не делается. Для сбора открытых данных, проверки выдачи и работы с кабинетами такая схема даёт разнообразие подсетей без ручной настройки.
Сколько туннелей можно держать одновременно?
Число открытых туннелей упирается в лимит потоков пакета: стандартные пакеты дают до 1000, корпоративный до 3000. Пакеты по потокам не складываются. При двух привязанных адресах общее число делится между ними пополам. Считать нагрузку удобнее по цифре из теста, увеличенной примерно на треть.
Соседние разборы по протоколам собраны отдельно: чем отличаются HTTP, HTTPS, SOCKS4 и SOCKS5 с таблицей по возможностям каждого, как проходят TCP и UDP через SOCKS5 с разбором команд сокетного протокола, какой почтовый порт выбрать под отправку писем через посредника. Когда туннель поднимается, а доступ отбивается кодом авторизации, порядок разбора описан в материале про ошибку 407 при подключении.