# 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. [Referencia de línea de comandos](#3-referencia-de-línea-de-comandos)
4. [Arquitectura rápida](#4-arquitectura-rápida)
5. [Configuración del servidor](#5-configuración-del-servidor)
6. [Configuración del agente](#6-configuración-del-agente)
7. [Gestión de usuarios](#7-gestión-de-usuarios)
8. [Puesta en marcha](#8-puesta-en-marcha)
9. [Uso con TLS / Let's Encrypt](#9-uso-con-tls--lets-encrypt)
10. [Integración con nginx](#10-integración-con-nginx)
11. [Referencia de logs](#11-referencia-de-logs)
12. [Referencia de códigos HTTP](#12-referencia-de-códigos-http)
13. [Preguntas frecuentes](#13-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. Referencia de línea de comandos

El binario `httptn` tiene tres subcomandos. No acepta opciones globales fuera de los subcomandos.

```
httptn <subcomando> [opciones]
```

### Subcomandos

| Subcomando | Descripción |
|---|---|
| `server` | Arranca el servidor público (acepta conexiones de agentes y de browsers) |
| `agent` | Arranca el agente en la red local (conecta al servidor y reenvía al upstream) |
| `version` | Imprime la versión, commit y fecha de compilación, y termina |

### Sintaxis completa

```
httptn server [--config <ruta>]
httptn agent  [--config <ruta>]
httptn version
```

### Flag `--config`

| Caso | Comportamiento |
|---|---|
| `--config /ruta/archivo.json` | Usa exactamente esa ruta |
| *(sin flag)* | Busca `<nombre-del-ejecutable>.json` en el mismo directorio del binario |

**Ejemplos de resolución automática:**

```
/usr/local/bin/httptn server          →  /usr/local/bin/httptn.json
/opt/tunnel/httptn agent              →  /opt/tunnel/httptn.json
./httptn server                       →  ./httptn.json
```

Si el binario se renombra, el archivo de configuración esperado cambia con él:

```
mv httptn mi-tunel
./mi-tunel server                     →  ./mi-tunel.json
```

### Ejemplos de uso

```bash
# Servidor con config por defecto (busca httptn.json junto al binario)
httptn server

# Servidor con config explícita
httptn server --config /etc/httptn/produccion.json

# Agente con config por defecto
httptn agent

# Agente con config explícita
httptn agent --config /home/usuario/.config/httptn/agente.json

# Ver versión
httptn version
# httptn 1.0.0 (commit: a1b2c3d, built: 2026-04-12T10:00:00Z)
```

### Señales del sistema operativo

Tanto el servidor como el agente responden a las siguientes señales:

| Señal | Efecto |
|---|---|
| `SIGTERM` | Cierre ordenado: el servidor drena conexiones activas (hasta 15 s); el agente envía close frame al servidor |
| `SIGINT` (Ctrl+C) | Idéntico a `SIGTERM` |

> En Windows no existe `SIGTERM`; se puede usar `Ctrl+C` o detener el proceso desde el Administrador de tareas.

---

## 4. 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.

---

## 5. Configuración del servidor

El servidor busca su configuración en `<nombre-del-ejecutable>.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 `<id>.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).

---

## 6. Configuración del agente

El agente también busca `<nombre-del-ejecutable>.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/<id>.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 |

---

## 7. 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: `<subdomain>.<domain>` |
| `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.

---

## 8. 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/
```

---

## 9. 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"
}
```

---

## 10. 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 ✓
}
```

---

## 11. 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}
```

---

## 12. 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 |

---

## 13. 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/<id>.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.