Guía paso a paso para construir un servidor proxy en Python
Resumen
Un servidor proxy de Python necesita dos rutas de código diferentes, no una sola. Las solicitudes HTTP simples llegan como una línea de solicitud completa que el servidor puede reenviar directamente; las solicitudes HTTPS llegan como una solicitud CONNECT que el servidor debe tunelar de manera opaca en lugar de analizar.
asyncio maneja muchas conexiones simultáneas sin un límite de hilo por conexión. Cinco solicitudes concurrentes a través del proxy construido en esta guía se completaron en menos de 40 ms en total, sin que una solicitud bloqueara a otra.
El método CONNECT es lo que hace que HTTPS funcione a través de un proxy en absoluto, y es el paso que la mayoría de los tutoriales desde cero omiten o no prueban; este lo implementa y lo prueba contra un sitio HTTPS real.
Agregar soporte para Proxy-Authorization: Basic convierte un relé abierto en uno autenticado, y la diferencia es verificable: credenciales incorrectas o faltantes devuelven un 407 real, las correctas devuelven 200.
Un proxy autohospedado tiene exactamente tantos IPs de salida como máquinas en las que lo ejecutas — generalmente uno. Esto está bien para el desarrollo local o un relé de una sola región, pero es el límite que enfrentas si el objetivo es distribuir las solicitudes a través de muchas IPs.
Nada de esto desencripta o inspecciona el tráfico HTTPS. El túnel CONNECT retransmite bytes cifrados tal como están, que es el comportamiento correcto y menos sorprendente para un proxy personal, y evita tener que gestionar certificados TLS para la interceptación.
Introducción: escribiendo tu propio proxy de reenvío en lugar de solo usar uno
Un proxy de reenvío se sitúa entre un cliente y el internet, aceptando las solicitudes del cliente y realizándolas en su nombre. Construir uno en Python es un ejercicio genuinamente diferente de usar un proxy desde un script de Python: apuntar requests a la puerta de enlace de otra persona son unas pocas líneas; hacer que tu propio proceso retransmita correctamente tanto HTTP simple como HTTPS, maneje múltiples clientes a la vez y rechace el uso no autorizado es donde la mayoría de los tutoriales desde cero se quedan cortos. Esta guía construye ese servidor de principio a fin en la biblioteca estándar de Python, prueba cada ruta contra un objetivo real y es honesta sobre dónde se muestran en la práctica los límites de un relé autohospedado.
La construcción aquí utiliza solo asyncio, que se incluye con Python 3.7+ — no se requieren paquetes adicionales para el servidor central. Todo se verifica ejecutándolo: cada bloque de código a continuación fue ejecutado contra un servidor de prueba local o un sitio HTTPS real, y los comandos y resultados exactos se muestran junto al código, no solo se describen. Un proxy autohospedado como este también es un bloque de construcción común para pruebas de condiciones de red y flujos de trabajo de QA donde un equipo quiere tener visibilidad y control total sobre lo que realmente hace una solicitud antes de que salga de la red.
Instalación: lo que necesitas (y lo que no)
El servidor proxy en sí no tiene dependencias externas: asyncio, base64 y sys son parte de la biblioteca estándar en cualquier instalación de Python 3.7+. Dos cosas se utilizan solo para pruebas, no para el proxy en sí:
curl, para controlar el proxy desde la línea de comandos con el flag -x.
La biblioteca requests (pip install requests), para confirmar que el proxy también funciona como lo esperaría un cliente HTTP normal de Python; la combinación exacta implicada por la palabra clave "servidor proxy de Python," que cubre tanto la escritura de un proxy en Python como el control de uno desde código Python.
Nada de esto necesita privilegios de root o un sistema operativo específico; el servidor vincula un socket TCP simple en 127.0.0.1 en los ejemplos, y cambiar a 0.0.0.0 (con una regla de firewall que limite quién puede acceder) es el único cambio necesario para aceptar conexiones desde otras máquinas.
Échale un Vistazo Rápido
Si el objetivo es enrutar tráfico a través de muchas IPs de salida en lugar de aprender cómo funciona un proxy internamente, la puerta de enlace de Nstproxy te brinda un `host:puerto` listo para señalar a clientes HTTP/SOCKS5, en lugar de mantener tú mismo este código de relé.
Configurar el socket de escucha y el análisis de solicitudes
Un proxy de reenvío necesita hacer tres cosas por cada conexión: aceptarla, leer suficiente de la solicitud para saber a dónde va, y ya sea tunelar o retransmitir en consecuencia. asyncio.start_server maneja el paso de aceptación y asigna a cada conexión un par (lector, escritor):
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)# el método es "CONNECT" para HTTPS, o "GET"/"POST"/etc. para HTTP simpleLa línea de solicitud es el punto de bifurcación: `CONNECT host:port HTTP/1.1` significa que el cliente desea un túnel HTTPS y nunca tiene intención de que el proxy lea su tráfico real; cualquier otra cosa es una solicitud de HTTP simple que el proxy puede analizar y reenviar por su cuenta.## Implementación básica: reenvío de solicitudes HTTP simplesPara una solicitud que no sea `CONNECT`, el destino está ya sea en la línea de solicitud misma (forma absoluta, `GET http://host:port/path HTTP/1.1`) o en el encabezado `Host:`. El proxy marca ese destino, reconstruye la solicitud sin los encabezados `Proxy-*` que un servidor real no esperaría, y canaliza bytes en ambas direcciones:
```python
asyncdefpipe(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):try:whileTrue: data =await reader.read(65536)ifnot data:break writer.write(data)await writer.drain()except(ConnectionResetError, BrokenPipeError):passfinally:try: writer.close()except Exception:pass
Dentro de handle_client, la rama no CONNECT analiza el destino y reconstruye la solicitud:
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))
Probaron contra un servidor HTTP local desechable (http.server, vinculado a 127.0.0.1:9000, devolviendo un cuerpo fijo) con el proxy escuchando en 127.0.0.1:8080:
Eso devolvió el cuerpo hello-from-local-target y HTTP_STATUS:200. Se confirmó con curl -v que la solicitud realmente pasó a través del proxy (> GET http://127.0.0.1:9000/ HTTP/1.1 enviado al puerto 8080, respuesta reenviada) en lugar de que curl se conectara directamente al destino: un riesgo real en cualquier entorno de prueba en caja donde una configuración de no_proxy puede evitar silenciosamente un proxy para ciertos hosts, así que verificar la ruta real tomada importa más que confiar solo en el código de estado final.
Patrones avanzados: túneles HTTPS, autenticación y concurrencia
Túneles HTTPS con CONNECT
Un proxy que solo maneja el caso anterior no puede transportar tráfico HTTPS: el cliente está a punto de iniciar un apretón de manos TLS con el destino, y el proxy no tiene nada que hacer (ni la capacidad, sin una clave privada para el sitio de destino) inspeccionando eso. La solución es el método CONNECT: el proxy abre una conexión TCP pura al host:port solicitado, responde con 200 Connection Established, y a partir de ese momento solo transfiere bytes en ambas direcciones sin mirarlos:
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),)
Este es el mecanismo exacto que describe la referencia al método CONNECT de MDN: "el método HTTP CONNECT solicita que un proxy establezca un túnel HTTP a un servidor de destino, y si tiene éxito, reenvía ciegamente datos en ambas direcciones hasta que se cierra el túnel." Se probó contra un sitio HTTPS real (no un simulacro) a través del proxy en el puerto 8080:
curl -v en el mismo comando confirmó la ruta completa: CONNECT pypi.org:443 enviado al proxy, 200 Connection Established devuelto, luego SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384, y finalmente una respuesta HTTP/2 200 de pypi.org en sí — el apretón de manos TLS ocurrió de extremo a extremo entre curl y pypi.org a través del túnel, sin que el proxy viera texto en claro. La misma URL también funcionó desde la biblioteca requests de Python señalando al proxy a través de proxies={"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}, devolviendo 200 y JSON válido — confirmando que el servidor se comporta como un proxy ante una biblioteca cliente HTTP real, no solo ante curl.
Requiriendo autenticación
Un relay abierto en Internet público se abusa rápidamente. Agregar autenticación básica significa verificar un encabezado Proxy-Authorization antes de hacer cualquier reenvío, y devolver 407 (el equivalente específico del proxy de 401) cuando falta o es incorrecto:
import base64
AUTH = base64.b64encode(b"devuser:s3cret").decode()# normalmente cargado desde la configuración, no codificadodefcheck_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"Proxy Authentication Required" writer.write(b"HTTP/1.1 407 Proxy Authentication Required\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()
Resultados en vivo con el servidor habilitado para autenticación en el puerto 8081:
Solicitud
Resultado
Sin encabezado Proxy-Authorization
407 Proxy Authentication Required
Credenciales incorrectas (devuser:wrongpass)
407 Proxy Authentication Required
Credenciales correctas, objetivo HTTP plano
200, cuerpo reenviado correctamente
Credenciales correctas, objetivo HTTPS a través de CONNECT
200
Ambos casos de fallo se reprodujeron enviando realmente credenciales incorrectas o ausentes, no se afirmaron leyendo el código — la misma disciplina que este proyecto aplica a cada afirmación de configuración del proxy que publica.
Concurrencia sin un hilo por conexión
Debido a que handle_client es una corutina, el bucle de eventos de asyncio ejecuta muchas conexiones concurrentemente en un solo hilo en lugar de crear un hilo del sistema operativo por cliente, que es el patrón que la mayoría de los tutoriales de proxies desde cero utilizan. Cinco solicitudes simultáneas a través de la misma instancia de proxy en ejecución:
Eso imprimió req1:200 req2:200 req3:200 req4:200 req5:200 y real 0m0.033s. Todas las cinco completaron en 33 milisegundos sin que una solicitud esperara a otra — el comportamiento documentado para las APIs de stream de asyncio, donde el callback de start_server se ejecuta una vez por conexión como una corutina independiente en lugar de una llamada bloqueante.
Límites honestos de un servidor proxy hecho en casa
Todo lo anterior es un proxy avanzado y funcional — y aún tiene límites que vale la pena conocer antes de depender de él para algo más allá del desarrollo local o un relay de una sola región.
El más concreto aparece en el momento en que un sitio objetivo comienza a bloquear la IP del proxy: este servidor tiene exactamente una IP de salida, la dirección de la máquina en la que se ejecuta, por lo que un bloqueo allí bloquea a cada cliente detrás de él hasta que esa IP cambie. Ese es el punto donde un puerto de salida rotativo se convierte en la herramienta más directa para el trabajo en lugar del servidor construido en esta guía: la línea Residential Lite de Nstproxy ejecuta muchas IPs de salida residenciales independientes detrás de un único host:port, utilizando el mismo formato de conexión del cliente HTTP/SOCKS5 documentado para el gateway, por lo que una solicitud que de otro modo se detendría en una IP bloqueada se envía en una diferente, y el código del lado del cliente no necesita saber que ocurrió una rotación. Se factura bajo un modelo prepago, de pago por uso en la página de precios de Residential Lite en lugar de una suscripción fija, y está construido para scripts y servicios que necesitan distribuir el tráfico a través de una gran piscina de IP geográficamente variada (más de 200 países y regiones, según la propia explicación de Nstproxy sobre cómo funcionan los proxies HTTP) en lugar de para equipos que desean poseer y modificar el código del relay en sí; si inspeccionar o registrar el tráfico en la capa del proxy es el objetivo real, un servidor autoalojado como el mencionado anteriormente sigue siendo la herramienta adecuada, y un gateway rotativo es un salto complementario hacia arriba, no un reemplazo para ello.
Gran piscina de IPs de salida distribuida — muchas IPs residenciales independientes detrás del gateway significan que una sola IP bloqueada o limitada no detiene un trabajo entero de la manera en que lo haría en la única dirección de este servidor autoalojado.
Misma forma de protocolo del lado del cliente — el gateway habla HTTP, HTTPS y SOCKS5 en un solo punto final, por lo que el código escrito contra el proxy de esta guía (o contra el diccionario proxies de requests) se dirige al gateway con el mismo código de conexión, solo que con un host:port diferente.
Regiones de salida global — útil cuando el contenido del sitio objetivo, la disponibilidad o los límites de velocidad varían según la ubicación del solicitante, algo que un solo servidor autoalojado no puede replicar sin desplegar una instancia en cada región.
Dos cosas que esta construcción deliberadamente no hace y no deberían asumirse sin más trabajo: no descifra ni inspecciona las cargas útiles HTTPS (el túnel CONNECT es opaco por diseño, lo cual es correcto para un relay personal y evita gestionar certificados TLS para la interceptación), y no persiste logs, no limita la tasa de clientes, ni impone listas de acceso más allá de la única credencial de autenticación básica mostrada: un proxy expuesto más allá de 127.0.0.1 necesita al menos credenciales por cliente y límites de conexión antes de que sea seguro dejarlo corriendo desatendido.
Solucionar errores comunes
curl: (7) Falló al conectar casi siempre significa que el proceso del proxy no está escuchando en la dirección/puerto que curl recibió, o un firewall está bloqueando ese puerto — confirme con ss -tlnp | grep <puerto> que algo está realmente vinculado allí antes de revisar el código del proxy.
Una solicitud parece tener éxito incluso con el puerto del proxy incorrecto, o las credenciales incorrectas parecen funcionar — verifique si no_proxy/NO_PROXY está configurado en el entorno del shell e incluye el host objetivo; varios entornos de sandbox y CI configuran esto por defecto, y curl o requests omitirán silenciosamente el proxy configurado completamente para cualquier host en esa lista. Ejecute env -u no_proxy -u NO_PROXY antes del comando de prueba para descartar esto, y confirme con curl -v que la línea de solicitud muestra el puerto del proxy, no una conexión directa.
502 Bad Gateway de este proxy significa que el proxy no pudo alcanzar el host de destino — verifique que el nombre de host objetivo se resuelva y que el puerto sea accesible desde la máquina que ejecuta el proxy, no desde el cliente.
HTTPS funciona pero HTTP simple no (o viceversa) — esto casi siempre se debe a la rama de la línea de solicitud: confirme que el cliente está realmente enviando CONNECT para HTTPS (algunas bibliotecas de clientes HTTP necesitan una entrada de proxy https explícita, separada de http, antes de que lo hagan) y una línea de solicitud en forma absoluta o un encabezado Host: para HTTP simple.
Conclusión
Un servidor proxy Python funcional se reduce a dos formas de solicitud manejadas de manera diferente: HTTP simple reenviado y reconstruido, HTTPS tunelizado opacamente a través de CONNECT — además de cualquier autenticación y manejo de concurrencia que el despliegue realmente necesite. Cada parte de eso, incluidos los aspectos que la mayoría de los tutoriales rápidos omiten (un verdadero túnel HTTPS, un verdadero 407, solicitudes concurrentes reales), se realizó contra un objetivo activo en esta guía en lugar de describirse de memoria. Donde el límite de una única IP de salida de esta construcción se convierte en el verdadero cuello de botella es el punto en el que se debe optar por un gateway rotativo gestionado en lugar de escalar este código más.
P: ¿Puede un servidor proxy en Python que construyes tú mismo manejar tráfico HTTPS?
Sí, pero solo implementando el método CONNECT y haciendo túneles de los bytes encriptados sin inspeccionarlos; un proxy que solo analiza líneas de solicitud HTTP en texto plano (la versión común del primer tutorial) no puede llevar HTTPS, ya que el cliente nunca envía la solicitud real del destino a través de él en texto claro.
P: ¿Es legal ejecutar tu propio servidor proxy?
Ejecutar un servidor proxy por sí mismo es legal; lo que importa es para qué se utiliza y si el tráfico que pasa a través de él está autorizado. Usar un proxy construido por ti mismo o de terceros para eludir controles de acceso, raspar datos en contra de los términos de un sitio o esconder actividades ilegales conlleva riesgos legales independientemente de quién esté relaying el código.
P: ¿Por qué un proxy necesita soportar específicamente el método CONNECT, en lugar de simplemente reenviar solicitudes HTTPS como las de HTTP?
Porque una solicitud HTTPS está encriptada antes de salir del cliente, por lo que un proxy que la maneje como lo haría con HTTP en texto plano necesitaría ver texto claro que nunca recibe; CONNECT evita eso al hacer que el proxy reenvíe ciegamente los bytes después de que el túnel se abre, de modo que el apretón de manos TLS ocurre directamente entre el cliente y el destino real.
P: ¿En qué se diferencia un servidor proxy que construyes tú mismo de un servicio comercial de proxy rotativo?
Un servidor autoalojado como el de esta guía tiene exactamente tantos IP de salida como máquinas en las que se ejecuta — generalmente uno — mientras que una puerta de enlace rotativa comercial se sitúa frente a un gran grupo de IPs gestionado y cambia cuál maneja cada solicitud, lo que importa una vez que el bloqueo basado en IP o los límites de tasa (no la complejidad del código) se convierten en la verdadera limitación.
P: ¿Funciona este proxy con la biblioteca requests de Python, o solo con curl?
Ambos — el servidor habla proxy HTTP en texto plano y túneles CONNECT a nivel de protocolo, por lo que cualquier cliente que implemente eso correctamente, incluyendo requests a través de su argumento proxies, curl -x, y navegadores, puede usarlo sin manejo especial.
Ivy Lin
Aug. 6th 2026
110M+ IP reales con 99.9% de acceso exitoso
Respuesta media ultrarrapida ~0.5s para tareas de alta concurrencia
Desde solo $0.1/GB
Acceso inmediato a pools premium de proxies residenciales, datacenter, IPv6 e ISP.