WireGuard: La Base
Todo arranca con WireGuard. WireGuard es un protocolo de VPN que hace una cosa excepcionalmente bien: crear túneles encriptados punto a punto con configuración mínima.
Una config completa de WireGuard se ve así:
# /etc/wireguard/wg0.conf en Nodo A
[Interface]
PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=
Address = 10.0.0.1/24
ListenPort = 51820
[Peer]
PublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=
AllowedIPs = 10.0.0.2/32
Endpoint = 203.0.113.50:51820
Eso es todo. Sin certificados, sin autoridades certificantes, sin negociación HMAC. Solo pares de claves Curve25519 y encriptación ChaCha20-Poly1305. El protocolo cabe en unas 4.000 líneas de código del kernel.
Pero WireGuard crudo tiene un problema fundamental para uso real: se necesita saber la dirección IP y el puerto de cada peer de antemano. Si el Nodo B está detrás de un NAT, su endpoint público cambia. Si el Nodo C está en una red móvil, su IP cambia cada vez que cambia de antena. Con 50 nodos, tendrías que actualizar 2.450 configuraciones de peer cada vez que una IP cambia. Ahí es donde entra Tailscale.
La Arquitectura de Tailscale
Tailscale es, en esencia, un sistema de coordinación que resuelve el problema de configuración de WireGuard. A continuación, va la arquitectura:
┌─────────────────────────────────────────────┐
│ Servidor de Coordinación │
│ (Tailscale SaaS o Headscale self-hosted) │
│ │
│ • Registro de nodos (claves púb, endpoints) │
│ • Motor de políticas ACL │
│ • Configuración DNS │
│ • Autenticación OIDC │
└──────────────┬───────────────────────────────┘
│ HTTPS (solo plano de control)
┌──────────┼──────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Nodo A │ │ Nodo B │ │ Nodo C │
│agente │ │agente │ │agente │
│tailsca │ │tailsca │ │tailsca │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
└──────────┼──────────┘
Túneles WireGuard
(directos, peer-to-peer)
El servidor de coordinación nunca ve tu tráfico de datos. Solo maneja:
- Registro de nodos y sus claves públicas
- Distribución de información de endpoints (direcciones IP, puertos)
- Evaluación de políticas ACL
- Coordinación de NAT traversal
Todo el flujo de datos real va directamente entre nodos sobre WireGuard. El servidor de coordinación es un plano de control, no un plano de datos.
NAT Traversal: La Parte Difícil
Hacer que dos dispositivos detrás de NAT se hablen directamente es el problema de networking más difícil que Tailscale resuelve. Hay varios tipos de NAT, y cada uno requiere estrategias diferentes:
STUN: Descubriendo Tu Dirección Pública
El primer paso es averiguar tu propia IP pública y puerto. Tailscale usa STUN (Session Traversal Utilities for NAT):
Nodo A (detrás de NAT) Servidor STUN Nodo B (detrás de NAT)
│ │ │
│── STUN Binding Request ────→ │ │
│ │ │
│←── Tu IP:puerto público ─────│ │
│ (203.0.113.50:34567) │ │
│ │ │
│ │ ←── STUN Request ────│
│ │ │
│ │── Tu IP pública ───→ │
│ │ (198.51.100.22: │
│ │ 45678) │
Hole Punching
Una vez que ambos nodos conocen sus direcciones públicas (por STUN), intentan UDP hole punching:
Nodo A (NAT: 203.0.113.50:34567) Nodo B (NAT: 198.51.100.22:45678)
│ │
│── paquete UDP a 198.51.100.22:45678 ─→│ (puede ser descartado por el NAT de B)
│ │
│←── paquete UDP a 203.0.113.50:34567 ──│ (puede ser descartado por el NAT de A)
│ │
│── paquete UDP (retransmit) ──────────→│ (AHORA el NAT de B tiene un mapping)
│ │
│←── paquete UDP (retransmit) ──────────│ (AHORA el NAT de A tiene un mapping)
│ │
│◄═══════ túnel WireGuard ═══════════►│ (¡conexión directa!)
Ambos lados simultáneamente envían paquetes UDP a la dirección pública del otro. Los primeros paquetes pueden ser perdidos porque el NAT no vio tráfico de esa fuente todavía. Pero el paquete saliente crea un mapping NAT, así que cuando el paquete del otro lado llega, el NAT lo deja pasar.
Esto funciona para full cone, restricted cone y port-restricted NAT. Para symmetric NAT (donde el puerto cambia por destino), el hole punching falla porque el puerto descubierto por STUN no coincide con el puerto usado para la conexión al peer.
Predicción de Puertos
Para tipos de NAT más difíciles, Tailscale intenta predicción de puertos. Si un NAT asigna puertos secuenciales (muchos lo hacen), se puede predecir el siguiente puerto:
// Lógica simplificada de predicción de puertos
func predictPorts(observedPorts []uint16) []uint16 {
if len(observedPorts) < 2 {
return nil
}
// Chequear patrón secuencial
delta := observedPorts[1] - observedPorts[0]
predicted := observedPorts[len(observedPorts)-1] + delta
// Retornar un rango alrededor del puerto predicho
candidates := make([]uint16, 0, 32)
for i := predicted - 16; i <= predicted + 16; i++ {
candidates = append(candidates, i)
}
return candidates
}
Cuando la predicción de puertos no es posible, Tailscale usa un approach de "ataque de cumpleaños", ambos lados simultáneamente prueban muchos puertos, esperando una colisión. Con 256 pruebas de cada lado, hay aproximadamente un 50% de probabilidad de pegar con la combinación de puerto correcta (matemática de la paradoja del cumpleaños).
DERP: Cuando las Conexiones Directas Fallan
Cuando el hole punching falla (NAT simétrico en ambos lados, o UDP bloqueado completamente), Tailscale cae a DERP, Designated Encrypted Relay for Packets.
Los servidores DERP son esencialmente relays WebSocket que reenvían paquetes WireGuard encriptados:
Nodo A Servidor DERP Nodo B
│ │ │
│── paquete WG ───────→│ │
│ encriptado/HTTPS │── paquete WG ────────→ │
│ │ encriptado/HTTPS │
│ │ │
│←── paquete WG ───────│←── paquete WG ────────│
│ encriptado/HTTPS │ encriptado/HTTPS │
Puntos clave sobre DERP:
- Los servidores DERP no pueden desencriptar tráfico. Solo reenvían paquetes encriptados de WireGuard. El servidor DERP ve blobs encriptados.
- DERP corre sobre HTTPS (TCP puerto 443). Funciona a través de firewalls corporativos y portales cautivos que bloquean UDP.
- Tailscale corre servidores DERP globalmente. El tráfico rutea por el servidor DERP más cercano basado en latencia.
- DERP es un fallback, no el default. Tailscale continuamente intenta establecer conexiones directas y solo usa DERP cuando es necesario.
Se puede correr tu propio servidor DERP:
# Compilar el servidor DERP
go install tailscale.com/cmd/derper@latest
# Correrlo
derper \
--hostname=derp.example.com \
--certmode=letsencrypt \
--certdir=/var/lib/derper/certs \
--stun \
--verify-clients
MagicDNS: Nombres en Vez de IPs
MagicDNS es el sistema DNS automático de Tailscale. Cada nodo recibe un nombre legible en tu tailnet:
laptop.tail12345.ts.net → 100.64.0.1
server.tail12345.ts.net → 100.64.0.2
nas.tail12345.ts.net → 100.64.0.3
Por debajo, Tailscale corre un resolver DNS local (generalmente en 100.100.100.100) que intercepta queries DNS para tu dominio tailnet, los resuelve de la lista de nodos provista por el servidor de coordinación, y reenvía todas las demás queries a tu DNS upstream configurado.
Armando Tu Propia Red: Headscale
Headscale es una implementación open-source del servidor de coordinación de Tailscale. Es compatible con el cliente oficial de Tailscale, así que se tiene toda la magia del NAT traversal y WireGuard sin depender de la infraestructura de Tailscale.
Configurando Headscale
# Instalar Headscale
wget https://github.com/juanfont/headscale/releases/download/v0.23.0/headscale_0.23.0_linux_amd64.deb
sudo dpkg -i headscale_0.23.0_linux_amd64.deb
Configurarlo:
# /etc/headscale/config.yaml
server_url: https://headscale.example.com:443
listen_addr: 0.0.0.0:443
tls_cert_path: /etc/headscale/tls/cert.pem
tls_key_path: /etc/headscale/tls/key.pem
db_type: sqlite3
db_path: /var/lib/headscale/db.sqlite
ip_prefixes:
- 100.64.0.0/10
- fd7a:115c:a1e0::/48
dns_config:
nameservers:
- 1.1.1.1
- 8.8.8.8
magic_dns: true
base_domain: mesh.example.com
derp:
server:
enabled: true
region_id: 999
region_code: "custom"
region_name: "Mi DERP"
stun_listen_addr: 0.0.0.0:3478
urls: []
auto_update_enabled: false
oidc:
issuer: https://auth.example.com
client_id: headscale
client_secret: tu-secreto
scope: ["openid", "profile", "email"]
Registrar Nodos
# Crear un usuario (namespace)
headscale users create engineering
# Generar una pre-auth key
headscale preauthkeys create --user engineering --reusable --expiration 24h
# Output: key-abc123def456...
# En el nodo cliente, unirse usando el cliente oficial de Tailscale
tailscale up --login-server https://headscale.example.com --authkey key-abc123def456
Políticas ACL
Headscale soporta el formato de políticas ACL de Tailscale:
{
"acls": [
{
"action": "accept",
"src": ["group:engineering"],
"dst": ["tag:production:443", "tag:production:22"]
},
{
"action": "accept",
"src": ["group:ops"],
"dst": ["*:*"]
}
],
"groups": {
"group:engineering": ["user1@example.com", "user2@example.com"],
"group:ops": ["admin@example.com"]
},
"tagOwners": {
"tag:production": ["group:ops"],
"tag:staging": ["group:engineering"]
}
}
Avanzado: Subnet Routing y Exit Nodes
Subnet Router
Un subnet router expone una red entera a tu tailnet sin instalar Tailscale en cada dispositivo:
# En el nodo subnet router
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
# Anunciar la subnet local
tailscale up --advertise-routes=192.168.1.0/24,10.0.0.0/8
# En el servidor headscale, aprobar las rutas
headscale routes enable --route 192.168.1.0/24
headscale routes enable --route 10.0.0.0/8
Ahora cada nodo en tu tailnet puede alcanzar 192.168.1.0/24 a través del subnet router, sin ningún cambio en los dispositivos de esa subnet.
Exit Node
Un exit node rutea todo el tráfico de internet a través de un nodo específico, útil para acceder a servicios con restricción regional o rutear por una red confiable:
# En el exit node
tailscale up --advertise-exit-node
# En headscale, aprobarlo
headscale routes enable --route 0.0.0.0/0
headscale routes enable --route ::/0
# En el cliente, usar el exit node
tailscale up --exit-node=server.mesh.example.com
Monitoreo de Tu Mesh
A continuación, un script para monitorear la salud de la mesh:
#!/bin/bash
# mesh-health.sh - Monitorear conectividad de la mesh Tailscale/Headscale
echo "=== Reporte de Salud de la Mesh ==="
echo "Fecha: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo ""
STATUS=$(tailscale status --json)
DIRECT=$(echo "$STATUS" | jq '[.Peer[] | select(.CurAddr != "")] | length')
RELAY=$(echo "$STATUS" | jq '[.Peer[] | select(.Relay != "" and .CurAddr == "")] | length')
OFFLINE=$(echo "$STATUS" | jq '[.Peer[] | select(.Online == false)] | length')
TOTAL=$(echo "$STATUS" | jq '.Peer | length')
echo "Peers: $TOTAL total, $DIRECT directos, $RELAY relayeados, $OFFLINE offline"
echo ""
echo "=== Peers Relayeados (subóptimo) ==="
echo "$STATUS" | jq -r '.Peer[] | select(.Relay != "" and .CurAddr == "") |
"\(.HostName)\t\(.TailscaleIPs[0])\trelay: \(.Relay)"'
Rendimiento: WireGuard vs OpenVPN vs IPSec
Estos benchmarks se corrieron en dos máquinas conectadas por un link de 1Gbps a través de un NAT:
| Protocolo | Throughput | Overhead de Latencia | Uso de CPU |
|---|---|---|---|
| Sin VPN (baseline) | 940 Mbps | 0ms | - |
| WireGuard | 890 Mbps | 0.3ms | 2% |
| OpenVPN (UDP) | 620 Mbps | 1.2ms | 15% |
| OpenVPN (TCP) | 410 Mbps | 2.8ms | 22% |
| IPSec (IKEv2) | 820 Mbps | 0.5ms | 5% |
La ventaja de rendimiento de WireGuard viene de ejecución en kernel-space, criptografía moderna, overhead de protocolo mínimo y headers de tamaño fijo.
El Estado del Arte en 2026
Tailscale y el ecosistema mesh de WireGuard maduraron considerablemente:
- Tailscale Funnel: Exponer servicios específicos de tu tailnet al internet público con TLS automático.
- Taildrop: File sharing encriptado entre nodos. Como AirDrop, pero a través de cualquier dispositivo en tu tailnet.
- Headscale 0.23+: Compatibilidad completa con features del cliente Tailscale, incluyendo MagicDNS, subnet routing y exit nodes.
- Netbird y Nebula: Proyectos alternativos de mesh networking con filosofías de diseño diferentes.
El espacio de VPN mesh pasó de "pesadilla enterprise-only" a "instalar un binario y funciona." La simplicidad de WireGuard hizo esto posible, y Tailscale/Headscale lo hizo accesible. Para quienes todavía administran configs de OpenVPN o túneles SSH para conectividad interna, este approach vale la pena evaluar. El tiempo de setup es medible en minutos, no en días.