El problema

Cada temporada de fútbol, LaLiga bloquea rangos de IPs (principalmente de Cloudflare) para combatir el streaming pirata. Como esas IPs son compartidas por miles de webs sin ninguna relación con el fútbol, sitios legítimos como homelabeiro.com —que está detrás de un Cloudflare Tunnel— se caían como daño colateral en días de partido.

Mis servicios funcionaban perfectamente, mi homelab estaba encendido, mi túnel estaba activo... y aun así la web no cargaba. El problema no estaba en mi infraestructura, sino en el camino que el tráfico tomaba para llegar hasta ella.

La solución elegida

Montar un VPS propio con IP dedicada (no compartida) como nuevo punto de entrada público, conectado al homelab mediante un túnel saliente (rathole), sin abrir ningún puerto en el router.

Cloudflare se mantiene activo el resto del tiempo por sus protecciones (WAF, anti-DDoS, caché). El VPS queda como ruta alternativa para los días de bloqueo.

¿Por qué un VPS con IP propia sí esquiva el bloqueo?

El bloqueo de LaLiga actúa a nivel de ISP español, cortando el acceso a IPs concretas: las de Cloudflare, precisamente por ser compartidas por miles de dominios. No importa en qué país esté físicamente el servidor de origen; lo que importa es a qué IP resuelve el dominio.

Una IP dedicada de un VPS normal (Hetzner, Oracle, Contabo...) no comparte destino con servicios de streaming pirata, así que no entra en los listados de bloqueo masivo.

Proveedor elegido: Oracle Cloud Free Tier

Valoré Hetzner (~4-5 €/mes) y Contabo (~5 $/mes) como opciones de pago, pero me decanté por Oracle Cloud Free Tier, que ofrece recursos gratis de forma indefinida (no es un trial de 12 meses):

  • 2 OCPUs ARM Ampere + 12 GB de RAM (a repartir entre instancias)
  • IP pública dedicada
  • 200 GB de almacenamiento en bloque
Nota: Oracle redujo esta oferta a la mitad el 15 de junio de 2026 (antes eran 4 OCPUs / 24 GB), sin avisar públicamente. Si estás leyendo guías o comparativas antiguas, es posible que veas la cifra anterior — comprueba siempre la documentación oficial antes de dimensionar tu instancia.

Contras conocidos: el registro pide tarjeta para verificación, es común el error "Out of capacity" en ARM, y Oracle puede reclamar instancias infrautilizadas (no debería aplicar aquí, porque el túnel corre 24/7).

Paso a paso

0. Crearse una cuenta en Oracle Cloud Free Tier

Esto es fácil accedes a la web de Oracle https://www.oracle.com/cloud/free/ y sigues los pasos para crear una cuenta. Al final te pedirá una tarjeta y te hará un cargo simbólico, esto es simplemente una prueba de identidad para evitar que la gente se cree multicuentas, el dinero que te carguen, en mi caso centimos, se devolverá.

1. Creación de la instancia — Basic information

Región elegida: Germany Central (Frankfurt), puedes elegir otra ubicación Europea dentro de Oracle Cloud. Nombre de la instancia y compartment por defecto. El Availability Domain no importa para el shape ARM: se puede crear en cualquiera.

2. Opciones avanzadas de Placement

Aquí dejé los valores por defecto: On-demand capacity, sin cluster placement group y fault domain automático.

3. Imagen y Shape

  • Imagen: Ubuntu 24.04 LTS. Mejor soporte de paquetes y comandos para Docker y rathole que Oracle Linux, y además coincide con lo que ya uso en el homelab.
  • Shape: Ampere VM.Standard.A1.Flex (marcado como Always Free-eligible), con 1 OCPU / 6 GB de RAM. De sobra para correr el túnel, y deja margen dentro del límite gratuito total (4 OCPU / 24 GB) por si necesito otra instancia en el futuro.

4. Verificación del coste estimado

La calculadora mostró 1,85 €/mes correspondientes al boot volume, pero ese importe no tiene en cuenta los descuentos del Always Free (hay un aviso explícito en la propia pantalla). La etiqueta "Always Free-eligible" del shape es la confirmación real de que no hay cargo.

5. Networking — creación de VCN y subnet pública

Intenté primero seleccionar una VCN y una subnet existentes, pero el toggle de "Assign a public IPv4 address" no se dejaba activar, porque la subnet era privada por defecto y la VCN estaba sin completar.

La solución fue cambiar a "Create new virtual cloud network" + "Create new public subnet", que genera automáticamente el Internet Gateway necesario.

Aun así, la IP pública no se pudo activar en este paso, así que decidí continuar sin ella y asignarla después desde la instancia ya creada (Oracle permite hacerlo en cualquier momento).

6. Storage

Boot volume por defecto (46,6 GB, dentro del límite gratuito de 200 GB), sin tamaño personalizado y con in-transit encryption activado por defecto.

El aviso de "2 Errors" que apareció estaba relacionado con la configuración de red pendiente, no con el storage en sí.

7. SSH Keys

Elegí "Generate a key pair for me": Oracle genera el par de claves y te descarga la clave privada en ese momento. Es la única oportunidad, porque Oracle no la vuelve a mostrar.

La guardé en el ttyd del homelab con nano ~/.ssh/oracle-vps.key (pegando el contenido manualmente) y después chmod 600.

8. Creación de la instancia

Instancia creada con estado Running y con IP privada asignada, pero todavía sin IP pública. Eso llega en el paso siguiente.

9. Asignación de IP pública reservada

Desde Instance → Networking → Attached VNICs → VNIC primaria → Ip Administration → Edit :

  • Public IP type: Reserved public IP, no Ephemeral, para que la IP no cambie si se reinicia la instancia
  • Create new Reserved IP Address, nombrada vps-ip-fija
  • IP Address Source: Oracle (asignación automática del pool público)
  • Route table: por defecto

10. Conexión SSH y endurecimiento de seguridad

Conexión verificada con éxito:

ssh -i ~/.ssh/oracle-vps.key ubuntu@<IP_PUBLICA>

Estos son los pasos de endurecimiento que apliqué en la instancia:

# Actualizar sistema
sudo apt update && sudo apt upgrade -y

# Instalar utilidades base
sudo apt install -y ufw fail2ban unattended-upgrades curl

# Firewall
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

# fail2ban (bloqueo de fuerza bruta SSH)
sudo systemctl enable fail2ban --now

# Actualizaciones automáticas de seguridad
sudo dpkg-reconfigure --priority=low unattended-upgrades

Ojo a esto: además del ufw interno, Oracle tiene su propio firewall de red (la Security List de la VCN). Hay que abrir ahí también los puertos. Lo veremos en el paso 14.

11. Decisión de arquitectura: rathole sin Caddy

Descarté montar Caddy como reverse proxy en el VPS. Ya tengo Nginx Proxy Manager (NPM) en el homelab gestionando TLS y el routing por dominio para todos los servicios, así que duplicar esa lógica no aportaba nada.

rathole en el VPS actúa como tubería TCP pura: reenvía los puertos 80 y 443 tal cual, sin terminar TLS ni decidir dominios. Esa lógica la sigue llevando NPM en destino, igual que hace ahora con Cloudflare Tunnel.

Convive sin conflicto con cloudflared, que sigue corriendo en la misma máquina del homelab: ambos son solo clientes salientes, ninguno escucha puertos, así que no hay colisión. Mientras el DNS apunte a Cloudflare, el tráfico real pasa por ahí y el túnel de rathole queda listo pero inactivo hasta que se cambie el DNS.

12. Generación del token y elección del puerto de control

openssl rand -hex 32

El mismo token se usa en el servidor (VPS) y en el cliente (homelab): es lo que autentica la conexión.

Decidí no usar el 2333 por defecto —el de la documentación oficial de rathole, muy predecible y escaneado— y usar en su lugar el puerto 58422, como medida extra de seguridad por oscuridad.

13. Instalación de rathole en el VPS y configuración del servidor

Instalación del binario para arquitectura ARM:

cd ~
wget https://github.com/rapiz1/rathole/releases/latest/download/rathole-aarch64-unknown-linux-musl.zip
sudo apt install -y unzip
unzip rathole-aarch64-unknown-linux-musl.zip
sudo mv rathole /usr/local/bin/
rathole --version

/etc/rathole/server.toml:

[server]
bind_addr = "0.0.0.0:58422"

[server.services.http]
token = "TOKEN_GENERADO"
bind_addr = "0.0.0.0:80"

[server.services.https]
token = "TOKEN_GENERADO"
bind_addr = "0.0.0.0:443"

/etc/systemd/system/rathole-server.service:

[Unit]
Description=Rathole Server
After=network.target

[Service]
ExecStart=/usr/local/bin/rathole --server /etc/rathole/server.toml
Restart=always
RestartSec=5
User=root

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable rathole-server --now
sudo systemctl status rathole-server

Y abrimos el puerto de control en el firewall interno:

sudo ufw allow 58422/tcp

Los puertos 80 y 443 ya estaban abiertos desde el endurecimiento inicial del paso 10.

14. Security List de Oracle: apertura de los tres puertos

Oracle tiene dos capas de firewall independientes: ufw, dentro de la instancia, y el Security List de la VCN, a nivel de red. Sin abrir esta segunda capa no llega nada, por muy bien configurado que esté ufw.

En la consola: Networking → Virtual Cloud Networks → [tu VCN] → [tu subnet] → Security Lists → Default Security List → Add Ingress Rules

Tres reglas, con el Source Port Range vacío (All) en las tres. Ese campo es el puerto de origen del visitante, no el de destino:

Source CIDR IP Protocol Destination Port Range
0.0.0.0/0 TCP 80
0.0.0.0/0 TCP 443
0.0.0.0/0 TCP 58422

15. Cliente rathole en Docker (homelab)

Lo instalé en la misma máquina donde ya corre cloudflared, en la misma red que NPM.

docker-compose.yml:

services:
  rathole-client:
    image: rapiz1/rathole:latest
    container_name: rathole-client
    restart: always
    network_mode: host
    volumes:
      - ./client.toml:/app/config.toml
    command: --client /app/config.toml

client.toml:

[client]
remote_addr = "IP_DEL_VPS:58422"

[client.services.http]
token = "TOKEN_GENERADO"
local_addr = "127.0.0.1:80"

[client.services.https]
token = "TOKEN_GENERADO"
local_addr = "127.0.0.1:443"

NPM publica sus puertos como 80:80 y 443:443 en el mismo host, así que con network_mode: host basta con apuntar a 127.0.0.1.

docker compose up -d
docker logs -f rathole-client

Incidencia: "No route to host"

Al levantar el cliente, la conexión fallaba con:

Failed to connect to IP_DEL_VPS:58422: No route to host (os error 113)

Esto fue lo que comprobé, y todo estaba correcto:

  • rathole-server activo y escuchando en 0.0.0.0:58422
  • ufw del VPS con las reglas correctas ✅
  • IP pública confirmada con curl ifconfig.me
  • telnet desde fuera confirmó que el puerto no respondía → problema de red, no de configuración de rathole

Causa probable: como tuve fricción durante la creación de la VCN y la subnet pública (paso 5), es posible que el Internet Gateway o el routing no hubieran terminado de provisionarse correctamente.

Solución: un reboot de la instancia del VPS lo resolvió. Tras el reinicio, el control channel se estableció correctamente (Control channel established en los logs del cliente).

Si te pasa lo mismo y la configuración se ve bien, no descartes que simplemente sea Oracle terminando de aplicar la red.

Verificación end-to-end

Pruebas con curl, sin tocar el DNS, forzando la conexión a la IP del VPS:

# HTTP
curl -H "Host: <dominio>" http://<IP_DEL_VPS>

# HTTPS (mantiene el SNI correcto para que NPM sirva el certificado adecuado)
curl -v --resolve <dominio>:443:<IP_DEL_VPS> https://<dominio>

Resultado: todo el camino funciona —VPS → rathole → homelab → NPM → certificado → contenido— tanto en HTTP como en HTTPS, para los dominios dados de alta en NPM.

Prueba real de conmutación de DNS

Con el proceso manual ya claro, hice una prueba real sobre homelabeiro.com, el dominio de menor criticidad, usado como conejillo de indias antes de tocar los otros.

Cambio en Cloudflare (DNS → Records → Edit sobre el registro):

  • Type: de CNAME (apuntando al túnel .cfargotunnel.com) a A
  • Target/Content: la IP del VPS
  • Proxy status: desactivar el toggle "Proxied" (nube naranja 🟠 → gris ⚪ "DNS only")

Verificación de propagación:

nslookup homelabeiro.com 1.1.1.1

Resolvió correctamente a la IP del VPS, y propagó en pocos minutos con el TTL en "Auto".

Un error transitorio por el camino

Justo después de desactivar el proxy, el navegador mostró un error 525 SSL handshake failed.

Mi primera teoría fue caché de DNS, pero no era eso. La causa real: homelabeiro.com no pasaba por NPM hasta ese momento. Tenía su propio cloudflared instalado directamente en esa VM, apuntando al servicio sin pasar por el reverse proxy central.

Al cambiar el DNS al VPS + rathole, el tráfico llegaba hasta NPM, pero NPM no tenía ningún Proxy Host configurado para ese dominio: ni backend ni certificado asociados. De ahí el fallo de handshake.

Se resolvió creando el Proxy Host correspondiente en NPM, y aproveché el cambio para centralizar homelabeiro.com en NPM como el resto de dominios, en vez de mantener un cloudflared independiente en esa VM.

Estado actual

  • ✅ VPS creado en Oracle Cloud Free Tier (Frankfurt), Ubuntu 24.04, shape Ampere A1.Flex (1 OCPU / 6 GB) con IP pública fija
  • ✅ Firewall, fail2ban y actualizaciones automáticas configurados
  • ✅ Túnel rathole activo: servidor en el VPS (puerto 58422) y cliente en Docker en el homelab, como tubería TCP hacia NPM
  • ✅ Firewall de Oracle (Security List) y ufw alineados
  • ✅ Tráfico HTTP/HTTPS verificado end-to-end contra NPM
  • ✅ Convive sin conflicto con cloudflared, ambos activos a la vez

homelabeiro.com se queda intencionadamente en modo prueba, sirviendo en producción real a través del VPS + rathole + NPM, sin pasar por Cloudflare. El resto de dominios siguen sin tocar, detrás de Cloudflare Tunnel como siempre.

Para revertirlo cuando quiera, basta con volver el registro a CNAME apuntando al túnel y reactivar el "Proxied".

Lo que queda pendiente

Decidir e implementar el modelo de conmutación para el resto de dominios entre Cloudflare (por defecto) y el VPS (fallback en días de bloqueo):

  • Manual: en el panel de Cloudflare (DNS → Records), cambiar el registro A/CNAME del dominio a la IP del VPS y desactivar el proxy. Revertir del mismo modo cuando pase el bloqueo. Se hace dominio por dominio, no hay un interruptor global.
  • Automático: un script en cron que monitorice el dominio y cambie el DNS vía API de Cloudflare si detecta caída, revirtiendo cuando vuelva a responder. Pendiente de diseñar si decido ir por esta vía.

Conclusión

Lo interesante de este montaje no es el VPS en sí, sino que el homelab no ha cambiado ni una línea. NPM sigue gestionando TLS y routing exactamente igual, y los servicios ni se enteran de por dónde les llega el tráfico. Lo único que cambia es la puerta de entrada.

Y eso es justo lo que quería: no depender de que un tercero decida, un domingo por la tarde, que mi IP se parece demasiado a la de otro.

¿Te ha servido este post?

Si te ha resultado útil, tienes dudas sobre alguna parte del montaje o quieres adaptarlo a tu propio caso, escríbeme por LinkedIn. También puedes ver más proyectos como este en el blog.