# httptn — Manual de Usuario **httptn** es un túnel HTTP inverso que permite exponer un servicio web de una red local o corporativa en internet sin abrir puertos en el router ni modificar reglas de firewall. Funciona de manera similar a ngrok: un servidor público enruta el tráfico hacia un agente que corre en la red local, el cual lo reenvía transparentemente al servidor web destino (nginx, Apache, Node.js, etc.). --- ## Índice 1. [Requisitos](#1-requisitos) 2. [Instalación](#2-instalación) 3. [Arquitectura rápida](#3-arquitectura-rápida) 4. [Configuración del servidor](#4-configuración-del-servidor) 5. [Configuración del agente](#5-configuración-del-agente) 6. [Gestión de usuarios](#6-gestión-de-usuarios) 7. [Puesta en marcha](#7-puesta-en-marcha) 8. [Uso con TLS / Let's Encrypt](#8-uso-con-tls--lets-encrypt) 9. [Integración con nginx](#9-integración-con-nginx) 10. [Referencia de logs](#10-referencia-de-logs) 11. [Referencia de códigos HTTP](#11-referencia-de-códigos-http) 12. [Preguntas frecuentes](#12-preguntas-frecuentes) --- ## 1. Requisitos | Componente | Requisitos | |---|---| | **Servidor** | VPS o máquina con IP pública, Linux/macOS/Windows, puerto 443 libre | | **Agente** | Cualquier máquina en la red local, sin puertos abiertos requeridos | | **Go** | 1.22 o superior (solo para compilar desde fuentes) | | **DNS** | Registro wildcard `*.tudominio.com → IP del servidor` | --- ## 2. Instalación ### Compilar desde fuentes ```bash tar -xzf httptn-source.tar.gz cd httptn go build -mod=mod \ -ldflags="-X main.version=1.0.0" \ -o httptn ./cmd/httptn ``` El resultado es un único binario `httptn` sin dependencias externas. ### Verificar ```bash ./httptn version # httptn 1.0.0 (commit: abc1234, built: 2026-04-11T22:18:28Z) ``` --- ## 3. Arquitectura rápida ``` Internet │ ▼ [*.tudominio.com → IP pública] │ ▼ puerto 443 ┌──────────────────────────────────┐ │ httptn server │ │ │ │ tunnel.tudominio.com ──► control │ ◄── agente conecta aquí (WSS) │ alice.tudominio.com ──► ingress │ ──► reenvía al agente de "alice" └──────────────────────────────────┘ │ │ WebSocket TLS (puerto 443) ▼ ┌──────────────────────────────────┐ │ httptn agent │ (red local / doméstica / corporativa) │ │ │ recibe request → HTTP → nginx │ └──────────────────────────────────┘ ``` - **Un solo puerto 443** para todo el tráfico (agentes y usuarios finales). - El subdominio `tunnel.tudominio.com` es exclusivo para conexiones de agentes. - Cada usuario tiene su propio subdominio (p.ej. `alice.tudominio.com`). - El agente reenvía el tráfico a cualquier servidor local como si viniera de la red interna. --- ## 4. Configuración del servidor El servidor busca su configuración en `.json` junto al binario, o en la ruta indicada con `--config`. ```json { "addr": ":443", "domain": "tudominio.com", "control_subdomain": "tunnel", "users_dir": "./users", "max_body_size": 10485760, "log_level": "info", "tls": { "cert": "/etc/letsencrypt/live/tudominio.com/fullchain.pem", "key": "/etc/letsencrypt/live/tudominio.com/privkey.pem" }, "timeouts": { "request": 30000, "read": 60000, "write": 60000, "idle": 120000 }, "websocket": { "ping_interval": 30000, "pong_timeout": 10000 } } ``` ### Referencia de campos | Campo | Tipo | Unidad | Descripción | |---|---|---|---| | `addr` | string | — | Dirección y puerto de escucha (`:443`, `0.0.0.0:8080`) | | `domain` | string | — | Dominio base. Los subdominios se derivan de él | | `control_subdomain` | string | — | Subdominio reservado para conexiones de agentes | | `users_dir` | string | — | Directorio que contiene los archivos `.json` de usuarios | | `max_body_size` | entero | bytes | Tamaño máximo del body de una petición. `0` = sin límite | | `log_level` | string | — | Nivel de log: `debug`, `info`, `warn`, `error` | | `tls.cert` | string | — | Ruta al certificado PEM. Si se omite, el servidor arranca sin TLS | | `tls.key` | string | — | Ruta a la clave privada PEM | | `timeouts.request` | entero | ms | Tiempo máximo esperando respuesta del agente antes de 504 | | `timeouts.read` | entero | ms | Timeout de lectura de la conexión HTTP | | `timeouts.write` | entero | ms | Timeout de escritura de la conexión HTTP | | `timeouts.idle` | entero | ms | Tiempo máximo de una conexión keepalive sin actividad | | `websocket.ping_interval` | entero | ms | Intervalo entre pings al agente para mantener la conexión viva | | `websocket.pong_timeout` | entero | ms | Tiempo máximo esperando pong antes de considerar el agente caído | > **Nota sobre TLS:** si `tls.cert` y `tls.key` están vacíos, el servidor escucha en HTTP plano. Útil para desarrollo o cuando hay un proxy TLS frontal (nginx, Caddy, Cloudflare). --- ## 5. Configuración del agente El agente también busca `.json` junto al binario o usa `--config`. ```json { "server_url": "wss://tunnel.tudominio.com/connect", "auth": { "id": "alice", "password": "supersecret" }, "upstream": { "url": "http://localhost:80", "host_header": "mi-sitio.local", "timeout": 30000, "keepalive": { "enabled": true, "idle_timeout": 60000, "max_idle_conns": 32 } }, "reconnect_max_backoff": 60000, "log_level": "info", "websocket": { "ping_interval": 30000, "pong_timeout": 10000 } } ``` ### Referencia de campos | Campo | Tipo | Unidad | Descripción | |---|---|---|---| | `server_url` | string | — | URL WebSocket del servidor. `wss://` para TLS, `ws://` para texto plano | | `auth.id` | string | — | Identificador del usuario (nombre del archivo JSON sin extensión) | | `auth.password` | string | — | Contraseña en texto plano (debe coincidir con `users/.json`) | | `upstream.url` | string | — | URL base del servidor web local al que reenviar el tráfico | | `upstream.host_header` | string | — | Valor del header `Host` que recibirá el upstream. Si está vacío se usa el `X-Forwarded-Host` | | `upstream.timeout` | entero | ms | Timeout para cada petición al upstream | | `upstream.keepalive.enabled` | bool | — | Activar reutilización de conexiones TCP hacia el upstream | | `upstream.keepalive.idle_timeout` | entero | ms | Tiempo máximo de una conexión idle en el pool. **Debe ser menor que `keepalive_timeout` de nginx** | | `upstream.keepalive.max_idle_conns` | entero | — | Número máximo de conexiones idle en el pool | | `reconnect_max_backoff` | entero | ms | Espera máxima entre intentos de reconexión (backoff exponencial) | | `log_level` | string | — | Nivel de log: `debug`, `info`, `warn`, `error` | | `websocket.ping_interval` | entero | ms | Intervalo entre pings al servidor | | `websocket.pong_timeout` | entero | ms | Tiempo máximo esperando pong antes de reconectar | --- ## 6. Gestión de usuarios Cada usuario es un archivo JSON en el directorio `users_dir` del servidor. El nombre del archivo (sin `.json`) es el identificador de autenticación. ``` users/ ├── alice.json ├── bob.json └── empresa-xyz.json ``` ### Estructura del archivo de usuario ```json { "password": "contraseña-en-texto-plano", "subdomain": "alice", "enabled": true, "description": "Servidor de Alice - entorno dev" } ``` | Campo | Descripción | |---|---| | `password` | Contraseña para autenticación Basic. Sin hash (texto plano) | | `subdomain` | Subdominio asignado. Determina la URL pública: `.` | | `enabled` | `false` bloquea la conexión sin eliminar el archivo | | `description` | Nota libre, solo informativa (aparece en los logs de conexión) | ### Añadir un usuario ```bash cat > users/bob.json << 'EOF' { "password": "clave-segura", "subdomain": "bob", "enabled": true, "description": "Bob - servidor de producción" } EOF ``` El servidor detecta el archivo en el siguiente intento de conexión del agente. **No requiere reinicio.** ### Deshabilitar un usuario ```bash # Editar el campo enabled jq '.enabled = false' users/bob.json > /tmp/bob.json && mv /tmp/bob.json users/bob.json ``` La conexión activa del agente de Bob no se interrumpe inmediatamente; se rechazará en la próxima reconexión. --- ## 7. Puesta en marcha ### En el servidor (VPS) ```bash # Copiar el binario y la configuración scp httptn usuario@mi-servidor:/usr/local/bin/ scp httptn-server.json usuario@mi-servidor:/etc/httptn/httptn.json scp -r users/ usuario@mi-servidor:/etc/httptn/ # Arrancar ssh usuario@mi-servidor /usr/local/bin/httptn server --config /etc/httptn/httptn.json ``` #### Como servicio systemd ```ini # /etc/systemd/system/httptn.service [Unit] Description=httptn tunnel server After=network.target [Service] ExecStart=/usr/local/bin/httptn server --config /etc/httptn/httptn.json Restart=on-failure RestartSec=5 User=nobody AmbientCapabilities=CAP_NET_BIND_SERVICE [Install] WantedBy=multi-user.target ``` ```bash systemctl daemon-reload systemctl enable --now httptn journalctl -u httptn -f ``` ### En la red local (agente) ```bash # Copiar binario y configuración cp httptn ~/bin/ cp httptn-agent.json ~/bin/httptn.json # Arrancar ~/bin/httptn agent --config ~/bin/httptn.json ``` #### Verificar que el túnel funciona ```bash curl -H "Host: alice.tudominio.com" http://tu-servidor-ip/ # o desde un navegador: https://alice.tudominio.com/ ``` --- ## 8. Uso con TLS / Let's Encrypt ### Obtener certificado wildcard con Certbot ```bash certbot certonly \ --manual \ --preferred-challenges dns \ -d "tudominio.com" \ -d "*.tudominio.com" ``` Añadir el registro DNS TXT que Certbot indique, luego configurar el servidor: ```json { "tls": { "cert": "/etc/letsencrypt/live/tudominio.com/fullchain.pem", "key": "/etc/letsencrypt/live/tudominio.com/privkey.pem" } } ``` ### Sin TLS propio (detrás de Cloudflare o nginx) Si hay un proxy frontal que termina TLS, dejar `tls.cert` y `tls.key` vacíos y escuchar en HTTP: ```json { "addr": "127.0.0.1:8080", "tls": { "cert": "", "key": "" } } ``` En el agente usar `ws://` en lugar de `wss://`: ```json { "server_url": "ws://tunnel.tudominio.com/connect" } ``` --- ## 9. Integración con nginx El agente reenvía el tráfico como si fuera un proxy reverso convencional. nginx recibe headers estándar y no distingue si la petición viene del túnel o de la red local. ### Headers que el agente añade automáticamente | Header | Contenido | |---|---| | `X-Forwarded-For` | IP real del cliente en internet (se acumula si ya existía) | | `X-Real-IP` | IP real del cliente | | `X-Forwarded-Proto` | `https` o `http` según la conexión del cliente al servidor | | `X-Forwarded-Host` | Host original solicitado por el cliente | > El header `Authorization` del cliente **no llega al upstream** — el agente lo elimina antes de reenviar. > Los headers `Cookie` (request) y `Set-Cookie` (response) se transmiten con **todos sus valores preservados**. Múltiples cookies en una misma respuesta llegan al browser como headers `Set-Cookie` separados, sin concatenación. ### Configuración nginx recomendada ```nginx server { listen 80; server_name mi-sitio.local; location / { proxy_pass http://localhost:3000; # Leer los headers del túnel proxy_set_header Host $host; proxy_set_header X-Real-IP $http_x_real_ip; proxy_set_header X-Forwarded-For $http_x_forwarded_for; proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; # Keepalive hacia el upstream local proxy_http_version 1.1; proxy_set_header Connection ""; } } ``` ### Configuración keepalive nginx ↔ agente El `idle_timeout` del agente debe ser **menor** que el `keepalive_timeout` de nginx para evitar broken pipes: ```nginx # nginx.conf keepalive_timeout 75s; # valor por defecto de nginx ``` ```json // httptn-agent.json "keepalive": { "idle_timeout": 60000 // 60s < 75s ✓ } ``` --- ## 10. Referencia de logs Todos los logs son JSON estructurado en stdout. Campos siempre presentes: `time`, `level`, `component`. ### Eventos del servidor | `event` | `level` | Descripción | |---|---|---| | `starting` | info | Servidor iniciando | | `listening` | info | Servidor escuchando, campo `tls: true/false` | | `agent_connected` | info | Agente conectado. Incluye `subdomain`, `remote_addr`, `description` | | `agent_replaced` | warn | Nuevo agente reemplazó conexión existente para el mismo subdominio | | `agent_disconnected` | info | Agente desconectado | | `auth_failed` | warn | Intento de conexión con credenciales inválidas | | `no_agent` | info | Request a subdominio sin agente conectado | | `proxied` | debug | Request proxiada con éxito. Incluye `method`, `path`, `status`, `req_id` | | `timeout` | warn | Agente no respondió en `timeouts.request` ms | | `upstream_error` | warn | El agente reportó error al contactar su upstream | | `shutting_down` | info | Servidor recibió señal de parada | ### Eventos del agente | `event` | `level` | Descripción | |---|---|---| | `starting` | info | Agente iniciando | | `connected` | info | Conexión WebSocket establecida con el servidor | | `disconnected` | warn | Conexión perdida. Incluye `attempt` y `error` | | `reconnecting` | info | Intentando reconexión. Incluye `wait_ms` | | `request_received` | debug | Request recibida del servidor. Incluye `req_id`, `method`, `path` | | `upstream_ok` | debug | Respuesta del upstream obtenida. Incluye `status`, `body_sz` | | `upstream_error` | error | No se pudo contactar el upstream | | `retrying` | debug | Reintentando request al upstream por error de conexión | | `shutting_down` | info | Agente recibió señal de parada | ### Ejemplo de sesión normal ```json {"time":"2026-04-11T10:00:00Z","level":"info","component":"server","event":"listening","tls":true} {"time":"2026-04-11T10:00:05Z","level":"info","component":"control","event":"agent_connected","subdomain":"alice","remote_addr":"203.0.113.5:51234","description":"Servidor de Alice"} {"time":"2026-04-11T10:00:10Z","level":"debug","component":"ingress","event":"proxied","subdomain":"alice","req_id":"req_1712829610000000000","method":"GET","path":"/api/users","status":200} ``` --- ## 11. Referencia de códigos HTTP | Código | Origen | Causa | |---|---|---| | `401 Unauthorized` | Servidor | Credenciales del agente incorrectas o usuario deshabilitado | | `404 Not Found` | Servidor | El host de la request no coincide con ningún subdominio del dominio configurado | | `413 Request Entity Too Large` | Servidor | El body supera `max_body_size` | | `503 Service Unavailable` | Servidor | No hay agente conectado para ese subdominio | | `502 Bad Gateway` | Servidor | El agente no pudo contactar el upstream o la conexión WS falló | | `504 Gateway Timeout` | Servidor | El agente no respondió en `timeouts.request` ms | --- ## 12. Preguntas frecuentes **¿Puedo tener varios agentes con el mismo subdominio?** No. Si un segundo agente se conecta con el mismo subdominio, reemplaza al anterior. El agente viejo recibe un close frame y se desconecta. Los requests en vuelo en ese momento reciben 502. **¿Qué pasa si el agente se desconecta mientras hay requests en vuelo?** El servidor devuelve 502 a todos los requests que estaban esperando respuesta. **¿El agente necesita IP pública?** No. El agente inicia la conexión hacia el servidor. Solo el servidor necesita IP pública y DNS configurado. **¿Funciona detrás de un proxy HTTP corporativo?** Sí, siempre que el proxy permita conexiones WebSocket (Upgrade: websocket). Al ir por el puerto 443, la mayoría de proxies corporativos lo permiten. **¿Los datos van cifrados?** Si el servidor tiene TLS configurado (recomendado), toda la comunicación va cifrada. El túnel WebSocket también va sobre TLS. **¿Puedo cambiar la contraseña de un usuario sin reiniciar el servidor?** Sí. Edita el archivo `users/.json`. El cambio aplica en la próxima conexión del agente, no en la conexión activa. **¿Cómo expongo varios servicios locales a la vez?** Ejecuta un agente por cada servicio, cada uno con su propio archivo de configuración apuntando a un upstream diferente y autenticado con un usuario diferente (subdominio diferente). **¿Qué tamaño máximo de body tiene sentido configurar?** Para APIs REST: 1–10 MB. Para uploads de archivos: según necesidad. El body se almacena completamente en memoria antes de enviarlo por WebSocket, así que valores muy grandes (>100 MB) pueden presionar la memoria del servidor.