Пошаговое руководство по созданию прокси-сервера на Python
Резюме
Python-прокси-серверу нужны два разных кодовых пути, а не один. Обычные HTTP-запросы поступают в виде полной строки запроса, которую сервер может переслать напрямую; HTTPS-запросы приходят как запрос CONNECT, который сервер должен туннелировать непрозрачно, а не разбирать.
asyncio обрабатывает множество одновременных подключений без ограничения на поток за подключение. Пять одновременных запросов через прокси, созданный в этом руководстве, завершились менее чем за 40 мс в общей сложности, при этом ни один запрос не блокировал другой.
Метод CONNECT является тем, что делает работу HTTPS через прокси возможной, и это шаг, который большинство обучающих материалов с нуля пропускают или не проверяют — это руководство реализует его и проверяет на реальном сайте HTTPS.
Добавление поддержки Proxy-Authorization: Basic превращает открытый реле в аутентифицированный, и разница поддается проверке: неверные или отсутствующие учетные данные возвращают реальный код 407, правильные — 200.
Самостоятельно размещенный прокси имеет ровно столько IP-адресов для выхода, сколько у вас машин для его запуска — обычно один. Это устраивает для локальной разработки или реле в одном регионе, но это то, с чем вы сталкиваетесь, если цель — распределить запросы по множеству IP-адресов.
Ничто из этого не расшифровывает и не инспектирует HTTPS-трафик. Туннель CONNECT пересылает зашифрованные байты как есть, что является правильным, менее удивительным поведением для личного форвард-прокси и избегает необходимости управлять сертификатами TLS для перехвата.
Введение: написание собственного форвард-прокси вместо простого использования
Попробуйте Nstproxy - Начните бесплатный тест сегодня
Форвард-прокси находится между клиентом и интернетом, принимая запросы клиента и выполняя их от своего имени. Создание одного из них на Python — это действительно другое задание, чем использование прокси из скрипта на Python — указание requests на шлюз другого человека — это всего несколько строк; корректная переработка как обычного HTTP, так и HTTPS, обработка нескольких клиентов одновременно и отказ от несанкционированного использования — это то, на чем останавливаются большинство учебных пособий с нуля. Это руководство создает тот сервер с нуля на стандартной библиотеке Python, проверяет каждый путь на реальной цели и откровенно говорит о том, где ограничения самоуправляемого реле проявляются на практике.
Создание здесь использует только asyncio, который идет в комплекте с Python 3.7+ — никаких дополнительных пакетов не требуется для основного сервера. Все проверяется, запуская его: каждый блок кода ниже был выполнен либо против локального тестового сервера, либо против реального HTTPS-сайта, при этом точные команды и результаты показаны рядом с кодом, а не просто описаны. Самостоятельно размещенный прокси, как этот, также является общим строительным блоком для тестирования условий сети и рабочих процессов контроля качества, где команде нужно полное представление и контроль над тем, что действительно делает запрос перед тем, как он покинет сеть.
Установка: что вам нужно (и что не нужно)
Сервер прокси сам по себе не имеет внешних зависимостей — asyncio, base64 и sys являются частью стандартной библиотеки на любой установке Python 3.7+. Две вещи используются только для тестирования, а не для самого прокси:
curl, чтобы управлять прокси из командной строки с флагом -x.
Библиотека requests (pip install requests), чтобы подтвердить, что прокси также работает так, как ожидает обычный HTTP-клиент на Python — точная комбинация, подразумеваемая ключевыми словами "python proxy server", которая охватывает как написание прокси на Python, так и управление им из кода Python.
Ничто из этого не требует прав суперпользователя или конкретной ОС; сервер привязывает простой TCP-сокет к 127.0.0.1 в примерах, и замена на 0.0.0.0 (с правилом брандмауэра, ограничивающим, кто может к нему получить доступ) — единственное изменение, необходимое для приема подключений от других машин.
Быстрый обзор
Если цель состоит в маршрутизации трафика через множество IP-адресов выхода, а не в изучении того, как внутренне работает прокси, шлюз Nstproxy предлагает вам готовый `host:port`, на который можно направить HTTP/SOCKS5-клиенты, вместо того, чтобы поддерживать этот код реле самостоятельно.
Настройка сокета для прослушивания и разбора запросов
Форвард-прокси должен выполнять три действия для каждого подключения: принимать его, прочитать достаточно запроса, чтобы понять, куда он направляется, и соответственно туннелировать или передавать. asyncio.start_server обрабатывает шаг приема и передает каждому подключению пару (reader, writer):
header_lines =[first_line.decode(errors="replace").rstrip("\r\n")]whileTrue: line =await reader.readline()if line in(b"\r\n",b"\n",b""):break header_lines.append(line.decode(errors="replace").rstrip("\r\n")) method, target, _ = header_lines[0].split(" ",2)# метод - "CONNECT" для HTTPS, или "GET"/"POST"/и т.д. для обычного HTTP
Строка запроса является разветвляющей точкой: CONNECT host:port HTTP/1.1 означает, что клиент хочет установить HTTPS туннель и не намерен, чтобы прокси читала его фактический трафик; все остальное является обычным HTTP запросом, который прокси может обработать и передать самостоятельно.
Базовая реализация: ретрансляция обычных HTTP запросов
Для запроса, который не является CONNECT, цель находится либо в самой строке запроса (абсолютная форма, GET http://host:port/path HTTP/1.1), либо в заголовке Host:. Прокси соединяется с этой целью, восстанавливает запрос без заголовков Proxy-*, которые реальный сервер не ожидал бы, и передает байты в обоих направлениях:
Внутри handle_client не CONNECT ветка анализирует цель и восстанавливает запрос:
if target.startswith("http://"): rest = target[len("http://"):] host_port, _, path = rest.partition("/") path ="/"+ path
else: path = target
host_port =next((h.split(":",1)[1].strip()for h in header_lines[1:]if h.lower().startswith("host:")),None,)host, _, port = host_port.partition(":")port =int(port or80)remote_reader, remote_writer =await asyncio.open_connection(host, port)rebuilt =f"{method}{path} HTTP/1.1\r\n"for h in header_lines[1:]:ifnot h.lower().startswith("proxy-"): rebuilt += h +"\r\n"rebuilt +="\r\n"remote_writer.write(rebuilt.encode())await remote_writer.drain()await asyncio.gather(pipe(remote_reader, writer), pipe(reader, remote_writer))
Тестировалось с одноразовым локальным HTTP сервером (http.server, привязанным к 127.0.0.1:9000, возвращающим фиксированное тело) с прокси, слушающим на 127.0.0.1:8080:
Это вернуло тело hello-from-local-target и HTTP_STATUS:200. Подтверждено с curl -v, что запрос действительно проходил через прокси (> GET http://127.0.0.1:9000/ HTTP/1.1 отправлен на порт 8080, ответ перенаправлен обратно), а не соединение curl с целевым хостом напрямую — реальный риск в любой тестовой среде, где настройка no_proxy может незаметно обойти прокси для определенных хостов, поэтому проверка реального пути имеет больше значения, чем доверять только коду статуса.
Расширенные паттерны: HTTPS туннелирование, аутентификация и конкурентность
Туннелирование HTTPS с помощью CONNECT
Прокси, который обрабатывает только вышеописанный случай, не может передавать HTTPS трафик — клиент собирается начать TLS рукопожатие с целью, и у прокси нет оснований (или возможностей, без закрытого ключа для целевого сайта) его проверять. Решение — это метод CONNECT: прокси открывает сырой TCP соединение с указанным host:port, отвечает 200 Connection Established, и с этого момента просто перегоняет байты в обоих направлениях, не смотря на них:
if method =="CONNECT": host, _, port = target.partition(":") port =int(port or443) remote_reader, remote_writer =await asyncio.open_connection(host, port) writer.write(b"HTTP/1.1 200 Connection Established\r\n\r\n")await writer.drain()await asyncio.gather( pipe(reader, remote_writer), pipe(remote_reader, writer),)
Это именно тот механизм, который описан в справке метода CONNECT MDN: "метод HTTP CONNECT запрашивает, чтобы прокси установил HTTP туннель к целевому серверу, и если он успешен, слепо пересылает данные в обе стороны, пока туннель не закроется." Тестировалось на реальном HTTPS сайте (не макете) через прокси на порту 8080:
curl -v на той же команде подтвердил полный путь: CONNECT pypi.org:443 отправлен на прокси, 200 Connection Established возвращен, затем SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384, и наконец HTTP/2 200 ответ от самого pypi.org — TLS-рукопожатие произошло от конца до конца между curl и pypi.org через туннель, при этом прокси никогда не видел открытую информацию. Тот же URL также работал из библиотеки requests Python, направленной на прокси через proxies={"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}, возвращая 200 и корректный JSON — подтверждая, что сервер ведет себя как прокси для реальной HTTP-клиентской библиотеки, а не только для curl.
Требуется аутентификация
Открытый релей на публичном интернет быстро злоупотребляют. Добавление основной аутентификации означает проверку заголовка Proxy-Authorization перед выполнением любого пересылки, и возвращение 407 (эквивалент прокси к 401), когда он отсутствует или неправильный:
import base64
AUTH = base64.b64encode(b"devuser:s3cret").decode()# обычно загружается из конфигурации, а не закодировано в кодеdefcheck_auth(headers:list[str])->bool:for h in headers:if h.lower().startswith("proxy-authorization:"): value = h.split(":",1)[1].strip()if value.startswith("Basic "):return value[len("Basic "):].strip()== AUTH
returnFalseasyncdefsend_407(writer: asyncio.StreamWriter): body =b"Требуется аутентификация прокси" writer.write(b"HTTP/1.1 407 Требуется аутентификация прокси\r\n"b'Proxy-Authenticate: Basic realm="proxy"\r\n'b"Content-Length: "+str(len(body)).encode()+b"\r\n"b"Connection: close\r\n\r\n"+ body
)await writer.drain() writer.close()
Живые результаты с сервером, поддерживающим аутентификацию, на порту 8081:
Запрос
Результат
Нет заголовка Proxy-Authorization
407 Требуется аутентификация прокси
Неправильные учетные данные (devuser:wrongpass)
407 Требуется аутентификация прокси
Правильные учетные данные, целевой HTTP
200, тело переслано корректно
Правильные учетные данные, целевой HTTPS через CONNECT
200
Обе ошибки были воспроизведены путем фактической отправки неправильных или отсутствующих учетных данных, а не на основании прочтения кода — та же дисциплина, которую этот проект применяет к каждому заявлению о конфигурации прокси, которое он публикует.
Параллелизм без потока на соединение
Поскольку handle_client является корутиной, событийный цикл asyncio обрабатывает множество соединений параллельно на одном потоке, а не создает поток ОС на каждое клиентское соединение, что является шаблоном, используемым в большинстве учебников по прокси с нуля. Пять одновременных запросов через тот же работающий экземпляр прокси:
Это вывело req1:200 req2:200 req3:200 req4:200 req5:200 и real 0m0.033s. Все пять завершились за 33 миллисекунды, при этом ни один запрос не ждал другой — это поведение, зафиксированное для API потоков asyncio, где обратный вызов start_server выполняется один раз для каждого соединения как независимая корутина, а не блокирующий вызов.
Все вышеперечисленное — это реальный, работающий обратный прокси — и у него все еще есть границы, о которых стоит знать, прежде чем полагаться на него для чего-либо, кроме локальной разработки или одно-регионального реле.
Самый конкретный случай проявляется в тот момент, когда целевой сайт начинает блокировать IP-адрес прокси: этот сервер имеет только один выходной IP, адрес машины, на которой он работает, так что блокировка здесь блокирует каждого клиента за ним, пока этот IP не изменится. В этот момент ротационный шлюз становится более прямым инструментом для работы, чем сервер, описанный в этом руководстве — линия Residential Lite от Nstproxy работает с множеством независимых выходных IP-адресов за одним host:port, используя тот же формат подключения HTTP/SOCKS5, описанный для шлюза, так что запрос, который в противном случае застрял бы на одном заблокированном IP, отправляется с другого, а клиентская сторона кода не должна знать, что произошла ротация. Это выставляется по модели предоплаты, платите по мере использования на странице цен Residential Lite, а не по фиксированной подписке, и предназначено для скриптов и сервисов, которым необходимо распределять трафик по большому, географически разнообразному пулу IP (более 200 стран и регионов по данным объяснения Nstproxy о том, как работают HTTP-прокси), а не для команд, которые конкретно хотят владеть и изменять код реле сами — если проверка или ведение журнала трафика на уровне прокси является фактической целью, то самостоятелно размещенный сервер, подобный описанному выше, все еще остается правильным инструментом, а ротационный шлюз является дополнительным шагом вверх, а не заменой.
Большой распределенный пул выходных IP — множество независимых жилых IP-адресов за шлюзом означает, что один заблокированный или ограниченный по количеству IP не останавливает всю работу так, как это было бы на одном адресе этого самостоятелно размещенного сервера.
Такая же форма клиентского протокола — шлюз работает с HTTP, HTTPS и SOCKS5 на одной конечной точке, так что код, написанный для прокси в этом руководстве (или для словаря proxies библиотеки requests), указывает на шлюз с тем же кодом подключения, просто с другим host:port.
Глобальные регионы выхода — полезно, когда контент целевого сайта, доступность или лимиты скорости варьируются в зависимости от местоположения запрашивающего, что один самостоятелно размещенный сервер не может воспроизвести, не развернув экземпляр в каждом регионе сам.
Две вещи, которые этот сборка целенаправленно не делает и не следует предполагать, что она делает без дополнительных усилий: она не расшифровывает и не проверяет полезные нагрузки HTTPS (тunnel CONNECT является непрозрачным по дизайну, что правильно для личного реле и избегает управления сертификатами TLS для перехвата), и она не сохраняет журналы, не ограничивает скорость для клиентов и не применяет списки доступа за пределами одной учетной записи Basic-auth, показанной — прокси, открытый за пределами 127.0.0.1, нуждается как минимум в учетных данных для каждого клиента и ограничениях соединения перед тем, как его безопасно оставить работающим без присмотра.
Устранение распространенных ошибок
curl: (7) Не удалось подключиться почти всегда означает, что процесс прокси не слушает на адресе/порту, который был указан curl, или брандмауэр блокирует этот порт — подтвердите с помощью ss -tlnp | grep <port>, что что-то действительно привязано там, перед тем как проверять код прокси.
Запрос, кажется, проходит успешно, даже с неправильным портом прокси, или неправильные учетные данные, похоже, работают — проверьте, установлены ли переменные окружения no_proxy/NO_PROXY в среде оболочки и включают ли целевой хост; несколько изолированных и CI-сред по умолчанию устанавливают это, и curl или requests тихо обойдут настроенный прокси полностью для любого хоста из этого списка. Запустите env -u no_proxy -u NO_PROXY перед командой тестирования, чтобы исключить этот вариант, и подтвердите с помощью curl -v, что строка запроса показывает порт прокси, а не прямое подключение.
502 Bad Gateway от этого прокси означает, что прокси не смог добиться успеха при подключении к целевому хосту — проверьте, что имя хоста целевого объекта разрешается и что порт доступен с машины, запускающей прокси, а не от клиента.
HTTPS работает, но обычный HTTP нет (или наоборот) — это почти всегда связано с разделом строки запроса: подтвердите, что клиент действительно отправляет CONNECT для HTTPS (некоторые библиотеки HTTP-клиентов нуждаются в явной записи прокси https, отдельной от http, прежде чем они это сделают) и в строке запроса в абсолютной форме или в заголовке Host: для обычного HTTP.
Заключение
Работающий прокси-сервер на Python сводится к двум формациям запросов, которые обрабатываются по-разному — обычный HTTP пересылается и перестраивается, HTTPS туннелируется непрозрачно через CONNECT — плюс любая аутентификация и управление параллелизмом, которые действительно нужны развертыванию. Каждый кусочек из этого, включая части, которые большинство быстрых руководств пропускает (реальный HTTPS-туннель, реальный 407, реальные параллельные запросы), был протестирован против живой цели в этом руководстве, а не описан из памяти. Точка, в которой предел одноразового выходного IP становится настоящим узким местом, это тот момент, когда стоит обратиться к управляемому ротационному шлюзу вместо дальнейшей масштабирования этого кода.
В: Может ли созданный вами Python-прокси-сервер обрабатывать HTTPS-трафик?
Да, но только реализовав метод CONNECT и туннелируя зашифрованные данные, не проверяя их — прокси, который только анализирует строки запроса HTTP (обычная версия первого урока), не может обрабатывать HTTPS, так как клиент никогда не отправляет фактический запрос назначения в открытом виде.
В: Законно ли запускать свой собственный прокси-сервер?
Запуск прокси-сервера сам по себе законен; важно, для чего он используется и авторизован ли трафик, проходящий через него — использование собственного или стороннего прокси для обхода контрольных механизмов доступа, scraping данных в нарушение условий сайта или скрытие незаконной деятельности несет в себе юридические риски вне зависимости от того, чей код осуществляет пересылку.
В: Почему прокси должен поддерживать именно метод CONNECT, вместо того чтобы просто перенаправлять HTTPS-запросы, как HTTP?
Потому что HTTPS-запрос шифруется до того, как он покинет клиент, поэтому прокси, который обрабатывает его так же, как и обычный HTTP, должен видеть открытый текст, который он никогда не получает; CONNECT избегает этого, позволяя прокси слепо передавать байты после открытия туннеля, так что TLS-рукопожатие происходит непосредственно между клиентом и реальной целью.
В: Чем отличается созданный вами прокси-сервер от коммерческого ротационного прокси-сервиса?
Самостоятельно размещенный сервер, как тот, что описан в этом руководстве, имеет столько же выходных IP-адресов, сколько машин, на которых он работает — обычно один — в то время как коммерческий ротационный шлюз находится перед большим управляемым пулом IP-адресов и меняет, какой из них обрабатывает каждый запрос, что имеет значение, когда блокировка по IP или ограничения по скорости (а не сложность кода) становятся фактическим ограничением.
В: Работает ли этот прокси с библиотекой Python requests, или только с curl?
Работает с обоими — сервер поддерживает обычное проксирование HTTP и туннелирование CONNECT на уровне протоколов, так что любой клиент, который реализует это правильно, включая requests через его аргумент proxies, curl -x и браузеры, может использовать его без специальной обработки.
Ivy Lin
Aug. 6th 2026
110M+ реальных IP с 99.9% успешных доступов
Средний отклик ~0.5с для задач высокой конкуренции
Всего от $0.1/GB
Мгновенный доступ к премиальным residential, datacenter, IPv6 и ISP пулам.