Proxy con Túnel Cloudflare: VLESS+WebSocket+TLS auto-alojado
Tutorial completo para un proxy autenticado auto-alojado que permite que tu teléfono salga a internet con la IP de tu casa, incluso cuando estás en una WiFi hostil o viajando al extranjero. La máquina de casa no tiene IP pública ni puertos abiertos. El Túnel de Cloudflare transporta el tráfico; Xray (VLESS sobre WebSocket sobre TLS 1.3) maneja autenticación y enrutamiento; v2RayTun en Android es el cliente. Incluye arquitectura, modelo de amenazas, pasos de instalación, cinco pruebas de verificación, blindaje always-on, guía de viaje, rotación de secretos, desmontaje, solución de problemas, costo y las alternativas que rechacé (Tailscale, WireGuard, VPN comercial, Cloudflare WARP).
1. Qué es esto y qué no lo es
InstalaciónEl problema
Cuando viajas, cada app de banca, contenido geo-bloqueado o detección de fraude ve la IP del WiFi o de la red celular en la que estás físicamente. Eso dispara MFA adicional, bloquea transacciones o simplemente no carga. La solución es hacer que esas apps salgan a internet con la misma IP residencial que ven siempre desde casa — sin exponer la máquina de casa al internet público.
Objetivos
- Todo el tráfico del teléfono sale por la IP del ISP de tu casa, sin importar dónde esté el teléfono.
- Cero puertos abiertos en la máquina de casa. Sin redirección de puertos en el router. Sin IP pública (funciona detrás de CGNAT).
- TLS 1.3 extremo a extremo entre teléfono y proxy. Autenticación por UUID y un path de WebSocket secreto de 128 bits, así los escaneos aleatorios solo ven un 404.
- Gratis: plan Free de Cloudflare Tunnel + tu ISP actual + un dominio que ya tengas.
- Sobrevive reinicios, cortes de luz y rarezas de WSL2. Un cron watchdog levanta el stack en menos de 5 minutos.
No-objetivos
- No es anti-censura. Si vives detrás del Gran Cortafuegos chino, necesitas Reality, no VLESS+WS+TLS clásico — aunque el mismo binario de Xray soporta ambos.
- No es VPN corporativa. No hay SSO, ni postura de dispositivo, ni DLP. Es un electrodoméstico de un solo usuario.
- No es un kill switch. Si el túnel muere, el teléfono cae a la red local. Corre las pruebas de la Sección 6 todos los días mientras viajas.
2. Arquitectura y modelo de amenazas
ArquitecturaCamino de datos extremo a extremo
Tres máquinas, dos fronteras de confianza, cero puertos entrantes en ningún lado. El teléfono habla VLESS-sobre-WebSocket-sobre-TLS contra un edge anycast de Cloudflare. El edge reenvía por un Cloudflare Tunnel que la máquina de casa marcó hacia afuera — no hay puerto a la escucha del lado de casa. Dentro de la máquina, cloudflared entrega HTTP plano por un bridge privado de Docker a Xray, que autentica el UUID, sniffea el destino y finalmente sale a internet con la IP de tu casa.
+------------------------+ wss://proxy.example.com/<random-path> +----------------------+
| Android phone | ------ VLESS over WebSocket over TLS --- | Cloudflare edge |
| v2RayTun (TUN mode, | --> | (anycast, ~20 ms |
| captures all traffic) | | from anywhere) |
+------------------------+ +----------+-----------+
|
Cloudflare Tunnel (outbound |
dialed by cloudflared, no |
inbound port at home) |
v
+-------------------------------------------------------------------------------------------------+
| Home machine (WSL2 / Linux) |
| |
| +---------------------------+ plain HTTP on docker bridge +-----------------------------+ |
| | proxy-cloudflared | ------------------------------> | proxy-xray | |
| | (cloudflare/cloudflared) | (proxy-net, host-only) | (ghcr.io/xtls/xray-core) | |
| | outbound dial to CF edge | | VLESS+WS server, UUID auth, | |
| | no inbound port | | blocks RFC1918 egress | |
| +---------------------------+ +--------------+--------------+ |
| | |
| v |
| outbound TCP/UDP to internet — |
| egress IP is your residential |
| IP. Banks, country-locked |
| apps, etc., see this address. |
+-------------------------------------------------------------------------------------------------+
Modelo del adversario
Adversario = quien observe la red WiFi o celular en la que está el teléfono, portales cautivos, resolutores DNS hostiles, fisgones oportunistas en el camino entre el teléfono y Cloudflare. El modelo asume que la máquina de casa es confiable; si te sacan root en el host, se acabó el juego.
Defensas
- Todo el tráfico teléfono↔Cloudflare va en TLS 1.3 (AES-256-GCM o CHACHA20-POLY1305).
- Todo el tráfico Cloudflare↔casa va en cifrado de túnel mutuamente autenticado (token de cloudflared).
- La autenticación es un UUID de 128 bits — la fuerza bruta es inviable.
- El path del WebSocket es un secreto de 128 bits — los escaneos aleatorios de
proxy.example.com/reciben 404 y nunca ven el endpoint WebSocket. - La regla de routing de Xray bloquea cualquier salida a rangos RFC1918 — previene SSRF hacia tu LAN de casa.
Lo que NO se defiende
- Malware en el teléfono (vive dentro del túnel).
- Compromiso de la máquina de casa (root en el host = juego perdido).
- Apps de banca que detectan la huella digital de la propia interfaz TUN (raro, pero posible).
3. Prerrequisitos
InstalaciónCuentas e infraestructura
- Un dominio real en tu propia cuenta de Cloudflare. El
pages.devgratuito no sirve — el hostname del túnel debe vivir en una zona de Cloudflare. Usa un dominio personal (.com,.co,.dev, etc.). No reutilices una cuenta corporativa de Cloudflare. - Máquina Linux que esté en línea casi siempre. Esta guía asume Fedora en WSL2 sobre Windows; Linux nativo funciona idéntico; macOS funciona con pequeños ajustes de rutas.
- Global API Key de Cloudflare desde
dash.cloudflare.com/profile/api-tokens→ "Global API Key" → View. Trátala como contraseña. Un API token con scopes también funciona; la guía usa la Global Key por simplicidad.
Software en el host
- Docker (rootless preferido — más seguro, instalación un poco más larga; rootful con
sudo dockerfunciona igual). - Plugin
docker compose. Viene en las instalaciones modernas de Docker. - Herramientas:
openssl,uuidgen,qrencode,curl,python3. En Fedora:sudo dnf install -y qrencodesi falta.
Teléfono
Probado en Samsung S24 Ultra con Android 16. Cualquier Android 7+ funciona. iOS funciona igual de bien con Streisand, Shadowrocket o Hiddify — el mismo share-link vless://.
4. Constrúyelo desde cero
Paso a paso4.1 Genera dos secretos
El stack lleva dos secretos criptográficos: un UUID que autentica al cliente y un path hex de 128 bits que esconde el endpoint WebSocket detrás del mecanismo "los escaneos aleatorios reciben 404". Ambos se generan localmente con herramientas estándar — sin servicios de terceros.
PROXY_UUID=$(uuidgen)
PROXY_WSPATH=$(openssl rand -hex 16)
echo "UUID=$PROXY_UUID"
echo "WSPATH=$PROXY_WSPATH"
Mantén ambos a la vista; van al config de Xray y al share-link vless://.
4.2 Crea el config de Xray
VLESS habla sin cifrado de capa de aplicación a propósito (decryption: none) porque TLS ya hace ese trabajo; el bloque de routing manda a blackhole los destinos RFC1918 para prevenir SSRF hacia la LAN de casa.
mkdir -p ~/proxy-stack/xray && cd ~/proxy-stack
cat > xray/config.json <<EOF
{
"log": { "loglevel": "warning" },
"inbounds": [
{
"tag": "vless-ws-in",
"listen": "0.0.0.0",
"port": 8080,
"protocol": "vless",
"settings": {
"clients": [
{ "id": "${PROXY_UUID}", "level": 0, "email": "[email protected]" }
],
"decryption": "none"
},
"streamSettings": {
"network": "ws",
"security": "none",
"wsSettings": { "path": "/${PROXY_WSPATH}", "headers": {} }
},
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"],
"routeOnly": false
}
}
],
"outbounds": [
{ "tag": "direct", "protocol": "freedom", "settings": { "domainStrategy": "UseIPv4v6" } },
{ "tag": "blocked", "protocol": "blackhole", "settings": {} },
{ "tag": "dns-out", "protocol": "dns" }
],
"dns": {
"servers": ["1.1.1.1", "1.0.0.1", "8.8.8.8"],
"queryStrategy": "UseIPv4v6"
},
"routing": {
"domainStrategy": "AsIs",
"rules": [
{ "type": "field", "ip": ["geoip:private"], "outboundTag": "blocked" },
{ "type": "field", "protocol": ["bittorrent"], "outboundTag": "blocked" }
]
}
}
EOF
4.3 Crea el stack docker-compose
Dos servicios en un bridge solo-host: cloudflared marca al edge de Cloudflare (sin puerto entrante) y proxiea hacia xray en el puerto 8080 dentro del bridge. Los logs están acotados para que esto no llene el disco durante un viaje largo.
cat > docker-compose.yml <<'EOF'
name: proxy-stack
services:
xray:
image: ghcr.io/xtls/xray-core:latest
container_name: proxy-xray
restart: unless-stopped
networks: [ proxy-net ]
volumes:
- ./xray/config.json:/etc/xray/config.json:ro
command: ["run", "-c", "/etc/xray/config.json"]
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
cloudflared:
image: cloudflare/cloudflared:latest
container_name: proxy-cloudflared
restart: unless-stopped
networks: [ proxy-net ]
depends_on: [ xray ]
command: ["tunnel", "--no-autoupdate", "--metrics", "0.0.0.0:2000", "run", "--token", "${CF_TUNNEL_TOKEN}"]
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
networks:
proxy-net:
name: proxy-net
driver: bridge
EOF
cat > .env <<EOF
PROXY_UUID=${PROXY_UUID}
PROXY_WSPATH=${PROXY_WSPATH}
PROXY_HOSTNAME=proxy.example.com
CF_TUNNEL_TOKEN=PASTE_TOKEN_AFTER_4.4
EOF
chmod 600 .env
4.4 Crea el Cloudflare Tunnel vía API
Esto se puede hacer también en el dashboard (Zero Trust → Networks → Tunnels → Create), pero la API es más rápida, scriptable y reproducible. El flujo: validar la key, descubrir zona y cuenta, crear el túnel, obtener el token del conector, configurar el hostname público y la regla de ingress, y crear un CNAME que apunte al túnel.
CF_KEY="<your Global API Key from dash.cloudflare.com/profile/api-tokens>"
CF_EMAIL="[email protected]"
DOMAIN="example.com" # your zone
SUB="proxy" # subdomain
HEAD=(-H "X-Auth-Email: $CF_EMAIL" -H "X-Auth-Key: $CF_KEY" -H "Content-Type: application/json")
# 1. Verify the key works
curl -sS "${HEAD[@]}" "https://api.cloudflare.com/client/v4/user" \
| python3 -c "import sys,json;d=json.load(sys.stdin);print('user:', d['result']['email']) if d.get('success') else print('auth FAILED:', d.get('errors'))"
# 2. Find the zone and the account it belongs to
ZONE_JSON=$(curl -sS "${HEAD[@]}" "https://api.cloudflare.com/client/v4/zones?name=$DOMAIN")
ZONE_ID=$(echo "$ZONE_JSON" | python3 -c "import sys,json;print(json.load(sys.stdin)['result'][0]['id'])")
ACCOUNT_ID=$(echo "$ZONE_JSON" | python3 -c "import sys,json;print(json.load(sys.stdin)['result'][0]['account']['id'])")
ACCOUNT_NAME=$(echo "$ZONE_JSON" | python3 -c "import sys,json;print(json.load(sys.stdin)['result'][0]['account']['name'])")
echo "Zone: $ZONE_ID"
echo "Account: $ACCOUNT_ID ($ACCOUNT_NAME) <-- confirm this is YOUR personal account"
# 3. Create the tunnel
TUNNEL_SECRET=$(openssl rand -base64 32)
TUNNEL_JSON=$(curl -sS "${HEAD[@]}" -X POST \
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/cfd_tunnel" \
--data "{\"name\":\"proxy-stack\",\"tunnel_secret\":\"$TUNNEL_SECRET\",\"config_src\":\"cloudflare\"}")
TUNNEL_ID=$(echo "$TUNNEL_JSON" | python3 -c "import sys,json;print(json.load(sys.stdin)['result']['id'])")
# 4. Fetch the connector token
CF_TUNNEL_TOKEN=$(curl -sS "${HEAD[@]}" \
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/cfd_tunnel/$TUNNEL_ID/token" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['result'])")
# 5. Configure ingress: $SUB.$DOMAIN -> http://xray:8080
curl -sS "${HEAD[@]}" -X PUT \
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/cfd_tunnel/$TUNNEL_ID/configurations" \
--data "{\"config\":{\"ingress\":[
{\"hostname\":\"$SUB.$DOMAIN\",\"service\":\"http://xray:8080\"},
{\"service\":\"http_status:404\"}
]}}" > /dev/null
# 6. Create the CNAME
curl -sS "${HEAD[@]}" -X POST \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
--data "{\"type\":\"CNAME\",\"name\":\"$SUB\",\"content\":\"$TUNNEL_ID.cfargotunnel.com\",\"proxied\":true,\"ttl\":1}" > /dev/null
# 7. Persist values for tear-down / watchdog
echo "$CF_KEY" > cf_api_key.secret && chmod 600 cf_api_key.secret
sed -i "s|^CF_TUNNEL_TOKEN=.*|CF_TUNNEL_TOKEN=$CF_TUNNEL_TOKEN|" .env
{ echo "CF_EMAIL=$CF_EMAIL"; echo "CF_ACCOUNT_ID=$ACCOUNT_ID"; echo "CF_ZONE_ID=$ZONE_ID"; echo "CF_TUNNEL_ID=$TUNNEL_ID"; } >> .env
4.5 Levanta el stack
Pull, start, y observa a cloudflared registrar cuatro conexiones HA contra cuatro POPs distintos de Cloudflare. Luego verifica que el hostname público devuelve 404 (prueba que la cadena es alcanzable) y que el path WebSocket secreto devuelve HTTP/1.1 101 Switching Protocols.
docker compose pull
docker compose up -d
sleep 5
docker compose ps
# Watch cloudflared register its 4 HA connections to the CF edge
docker logs proxy-cloudflared 2>&1 | grep -i "Registered tunnel connection"
# Expected: 4 lines, each with a different CF POP (mia05, bog01, etc.)
# Catch-all 404 from xray (proves CF -> tunnel -> xray is reachable)
curl -sI https://proxy.example.com/ | head -3
# WS upgrade at the secret path -- must return 101
curl -i --http1.1 \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
https://proxy.example.com/$PROXY_WSPATH 2>&1 | head -5
# Expected: HTTP/1.1 101 Switching Protocols
5. Configuración del teléfono (Android)
Móvil5.1 Elige cliente
v2rayNG es el cliente canónico pero no está en Google Play. Cualquiera de los siguientes funciona — todos aceptan el mismo share-link vless://.
| App | Origen | Por qué |
|---|---|---|
| v2RayTun | play.google.com/store/apps/details?id=com.v2raytun.android | El más liviano, lo más cercano a v2rayNG. Recomendado. |
| Hiddify Next | play.google.com/store/apps/details?id=app.hiddify.com | UI más limpia. |
| Sing-Box | play.google.com/store/apps/details?id=io.nekohasekai.sfa | Motor más nuevo, configuración JSON. |
iOS: instala Streisand, Shadowrocket o Hiddify desde la App Store — el mismo share-link funciona.
5.2 Genera el share-link
El enlace codifica UUID, host, puerto, transporte y el path WebSocket secreto. Renderízalo como QR para que el teléfono lo escanee.
LABEL=$(printf 'proxy-stack@%s' "proxy.example.com" | python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.stdin.read()))')
PATH_ENC=$(printf '/%s' "$PROXY_WSPATH" | python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.stdin.read(),safe=""))')
SHARELINK="vless://${PROXY_UUID}@proxy.example.com:443?encryption=none&security=tls&sni=proxy.example.com&type=ws&host=proxy.example.com&path=${PATH_ENC}#${LABEL}"
echo "$SHARELINK" > share-link.txt
cat share-link.txt
# Render as QR for easy phone scan
qrencode -t UTF8i -m 2 -s 1 "$(cat share-link.txt)"
5.3 Importa en el teléfono
- Abre v2RayTun → toca el + arriba a la derecha → Escanear código QR.
- Apunta la cámara al QR de la terminal. El perfil se importa como
[email protected]. - Abre Ajustes → DNS: pon Remote DNS =
1.1.1.1, Domestic DNS =1.1.1.1y activa Fake DNS. Es la protección más fuerte contra fugas. - De vuelta en la pantalla principal, toca el botón de play. Android pide permiso para la conexión VPN — acepta.
- Corre las cinco pruebas de la Sección 6.
6. Verificar la instalación — cinco pruebas reales
PruebasVisita cada URL con el proxy conectado
Abre Chrome en el teléfono con el proxy activo y visita cada URL siguiente. Las cinco deben mostrar tu país, IP y ASN de casa — nunca el país de la red WiFi/celular en la que estás físicamente.
| # | URL | Esperado |
|---|---|---|
| 1 | https://ifconfig.co/json | ip = tu IP pública de casa, country = tu país, asn_org = el ISP de tu casa. |
| 2 | https://1.1.1.1/cdn-cgi/trace | loc = código de tu país, colo = el POP de CF más cercano a casa (mia05, bog01, etc.). |
| 3 | https://browserleaks.com/dns | La lista de DNS Servers muestra Cloudflare o el ISP de tu casa — nunca el resolutor de la red WiFi. |
| 4 | https://www.dnsleaktest.com → Standard test | Mismo resultado que la prueba 3. |
| 5 | https://ipv6-test.com/ | Solo IPv4 (enrutamos IPv4) y sin fugas. |
Si alguna prueba muestra la ubicación equivocada, salta a la Sección 13 — Solución de problemas.
7. Operaciones
OperaciónHelper proxyctl.sh
Guarda este pequeño wrapper junto a docker-compose.yml. Expone una UX basada en verbos sobre las cuatro operaciones diarias: levantar, estado, logs y los generadores de share-link / QR.
cat > proxyctl.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
export DOCKER_HOST=${DOCKER_HOST:-unix:///run/user/1000/docker.sock}
[ -f .env ] && set -a && . .env && set +a
case "${1:-help}" in
up) docker compose pull && docker compose up -d && docker compose ps ;;
down) docker compose down ;;
restart) docker compose restart && docker compose ps ;;
logs) docker compose logs -f --tail=100 "${2:-}" ;;
status) docker compose ps ;;
share-link)
: "${PROXY_UUID:?}"; : "${PROXY_HOSTNAME:?}"; : "${PROXY_WSPATH:?}"
LABEL=$(printf 'proxy-stack@%s' "$PROXY_HOSTNAME" | python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.stdin.read()))')
PATH_ENC=$(printf '/%s' "$PROXY_WSPATH" | python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.stdin.read(),safe=""))')
echo "vless://${PROXY_UUID}@${PROXY_HOSTNAME}:443?encryption=none&security=tls&sni=${PROXY_HOSTNAME}&type=ws&host=${PROXY_HOSTNAME}&path=${PATH_ENC}#${LABEL}"
;;
qr) "$0" share-link | qrencode -t UTF8i -o - ;;
*) echo "Usage: $0 {up|down|restart|logs [svc]|status|share-link|qr}" ;;
esac
EOF
chmod +x proxyctl.sh
Operaciones comunes: ./proxyctl.sh up, status, logs, logs xray, share-link, qr, restart, down.
Watchdog (cron, cada 5 minutos)
El contenedor cloudflared puede quedarse Up mientras su túnel está muerto — raro, pero pasa. La verdad autoritativa vive en la API de Cloudflare. El watchdog le pregunta directo a Cloudflare, y si el túnel reporta cualquier cosa distinta a healthy con al menos una conexión, reinicia el stack.
cat > watchdog.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
[ -f .env ] && set -a && . .env && set +a
export DOCKER_HOST=${DOCKER_HOST:-unix:///run/user/1000/docker.sock}
log() { echo "$(date -u +%FT%TZ) $*"; }
restart_stack() { log "restarting: $1"; docker compose restart >/dev/null 2>&1 || docker compose up -d >/dev/null 2>&1; }
docker ps --filter name=proxy-cloudflared --filter status=running --format '{{.Names}}' | grep -q proxy-cloudflared \
|| { restart_stack "cloudflared container down"; exit 0; }
docker ps --filter name=proxy-xray --filter status=running --format '{{.Names}}' | grep -q proxy-xray \
|| { restart_stack "xray container down"; exit 0; }
[ -z "${CF_ACCOUNT_ID:-}" ] || [ -z "${CF_TUNNEL_ID:-}" ] && exit 0
[ -f cf_api_key.secret ] || exit 0
CF_KEY=$(cat cf_api_key.secret)
STATUS=$(curl -sS --max-time 8 \
-H "X-Auth-Email: ${CF_EMAIL:[email protected]}" -H "X-Auth-Key: $CF_KEY" \
"https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/$CF_TUNNEL_ID" \
| python3 -c "import sys,json;d=json.load(sys.stdin)['result'];print(f\"{d.get('status','?')}|{len(d.get('connections',[]))}\")")
health=${STATUS%|*}; conns=${STATUS#*|}
if [ "$health" != "healthy" ] || [ "$conns" -lt 1 ]; then
restart_stack "CF says status=$health conns=$conns"
fi
EOF
chmod +x watchdog.sh
# Install the cron entry (every 5 minutes, append output to watchdog.log)
( crontab -l 2>/dev/null; echo "*/5 * * * * $HOME/proxy-stack/watchdog.sh >> $HOME/proxy-stack/watchdog.log 2>&1" ) | crontab -
8. Blindaje always-on
Operación8.1 La cadena de arranque (específica de WSL2)
Windows boots
-- Windows Task Scheduler runs "wsl --boot" task at SYSTEM startup
-- WSL distro starts (because vmIdleTimeout=-1 in ~/.wslconfig)
-- /etc/wsl.conf systemd=true -- systemd starts inside WSL
-- [email protected] starts (because loginctl enable-linger)
-- ~/.config/systemd/user/docker.service starts (rootless dockerd)
-- proxy-xray + proxy-cloudflared resurrect (restart: unless-stopped)
8.2 Configura cada eslabón de la cadena
# (a) WSL2 doesn't sleep
mkdir -p /mnt/c/Users/$USER 2>/dev/null
cat > /mnt/c/Users/$USER/.wslconfig <<'EOF'
[wsl2]
memory=64GB # adjust to your RAM
processors=24 # adjust to your cores
vmIdleTimeout=-1 # never auto-suspend
EOF
# (b) systemd inside WSL (needed for rootless docker.service)
sudo tee /etc/wsl.conf >/dev/null <<EOF
[boot]
systemd=true
[user]
default = $USER
EOF
# (c) lingering -- user services run without an interactive session
sudo loginctl enable-linger $USER
# (d) install rootless docker (skip if you already have it)
curl -fsSL https://get.docker.com/rootless | sh
systemctl --user enable --now docker
# (e) restart WSL to pick everything up
# From PowerShell on Windows: wsl --shutdown
# Then re-open WSL and verify:
systemctl --user is-enabled docker # -> enabled
systemctl --user is-active docker # -> active
loginctl show-user $USER | grep Linger # -> Linger=yes
8.3 Haz que Windows arranque WSL al boot, no solo al login
Corre esto una vez en una PowerShell de Administrador. La tarea programada se dispara al arranque del SISTEMA, antes de que cualquier humano inicie sesión, y deja un proceso ocioso sleep infinity dentro de WSL para que la distro se mantenga caliente.
$action = New-ScheduledTaskAction -Execute "wsl.exe" -Argument "-d <YOUR-DISTRO> -u <YOUR-USER> -e bash -c 'sleep infinity'"
$trigger = New-ScheduledTaskTrigger -AtStartup
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
Register-ScheduledTask -TaskName "WSL2-KeepAlive" -Action $action -Trigger $trigger -Principal $principal
Reemplaza <YOUR-DISTRO> con la salida de wsl -l -q y <YOUR-USER> con tu usuario de WSL.
8.4 BIOS — encendido automático tras restablecer la red
Si la máquina de casa pierde corriente mientras estás fuera, Windows debe arrancar solo cuando vuelva la red. En la BIOS / UEFI, busca uno de estos ajustes (el nombre varía por board) y actívalo:
AC Power Recovery=Power OnRestore on AC/Power Loss=Last StateorPower OnState After Power Failure=Always On
Verifícalo antes de viajar. Desenchufa, vuelve a enchufar; la máquina debe arrancar en unos 30 segundos.
9. Modelo de seguridad — qué va cifrado y dónde
SeguridadCapas de cifrado, en orden
- Teléfono ↔ edge de Cloudflare: TLS 1.3, autenticado por el servidor con el certificado SSL universal que Cloudflare emite para tu zona. AES-256-GCM o CHACHA20-POLY1305, llaves ECDHE.
- Edge de Cloudflare ↔ cloudflared en casa: el propio Cloudflare Tunnel, autenticado mutuamente con el token del conector (32 bytes secretos).
- cloudflared ↔ Xray dentro de Docker: HTTP plano sobre un bridge de Docker solo-host. Este es el único salto sin cifrar, por diseño — ambos endpoints viven en la misma máquina, detrás de un namespace de red privado.
- Xray ↔ el internet abierto: lo que negocie el destino. Las apps HTTPS siguen HTTPS; el proxy nunca ve su texto plano.
"encryption: none" en VLESS — explicado
El share-link contiene encryption=none&security=tls. La gente entra en pánico con la primera parte. Está bien. VLESS es el sucesor de VMess; remueve a propósito el cifrado de capa de aplicación porque la capa de transporte (TLS) ya lo provee. Hacer cripto encima de TLS solía ser una huella que los sistemas de censura detectaban — VLESS lo eliminó. La mitad security=tls del share-link es lo que garantiza el cifrado.
echo | openssl s_client -connect proxy.example.com:443 \
-servername proxy.example.com -tls1_3 2>/dev/null \
| grep -E 'Protocol|Cipher|subject|issuer' | head -6
# Expect: Protocol: TLSv1.3, ECDHE+AES-256-GCM cipher
Cifrado y fugas de DNS
Las consultas DNS UDP de las apps del teléfono entran al TUN de v2RayTun, se encapsulan en VLESS-mux (que lleva UDP dentro del WebSocket), atraviesan hasta Xray, y Xray las resuelve por la conexión de casa. Los resolutores DNS ven consultas desde tu IP de casa, no desde la red en la que está el teléfono.
En v2RayTun Ajustes → DNS: Remote DNS = 1.1.1.1, Domestic DNS = 1.1.1.1 (forzar el mismo — no hacer bypass), Enable Fake DNS = ON (asigna IPs sintéticas; el sniffing del lado servidor recupera el dominio real — la protección más fuerte contra fugas).
10. Guía de viaje
OperaciónModos de falla y mitigaciones
| Riesgo | Mitigación |
|---|---|
| La casa pierde corriente | Auto-encendido por BIOS (Sección 8.4) + el módem del ISP en UPS si es posible |
| El router WiFi de casa se reinicia | El cron watchdog levanta los contenedores; Cloudflare se reconecta automáticamente |
| El hostname de Cloudflare queda zombie | El watchdog detecta "status≠healthy" y reinicia el stack |
| Al teléfono se le acaba la batería | Power bank en el equipaje de mano |
| Un portal cautivo bloquea WSS | Usa datos móviles; el túnel solo necesita HTTPS, así que casi cualquier red funciona |
| Te roban el teléfono | Rota UUID + WSPATH (Sección 11) y re-impórtalo en el teléfono nuevo |
| Olvidas el share-link estando fuera | Guárdalo en tu gestor de contraseñas ANTES de viajar. O corre ./proxyctl.sh share-link por SSH/Tailscale dentro de la máquina de casa. |
Antes de salir
- Prueba de arranque:
wsl --shutdownen PowerShell, espera 30 segundos, abre WSL de nuevo, corredocker ps— ambos contenedores deben estarUp. - Reinicio total de Windows. Espera 5 minutos. No inicies sesión. Desde el teléfono (todavía en casa, en otra WiFi/datos móviles), conecta v2RayTun y verifica
https://ifconfig.co. - Desconecta el cable de la pared. Espera 30 segundos. Vuelve a conectar. Repite la prueba.
- Guarda tu share-link en un gestor de contraseñas. Guarda también el ID del túnel y el ID de cuenta de CF — útiles si necesitas recuperar algo de forma remota.
Ya estando fuera
- Conecta v2RayTun antes de abrir apps sensibles.
- Corre la prueba de fugas #2 (
https://1.1.1.1/cdn-cgi/trace) una vez al día. Silocalguna vez muestra el país en el que estás, el proxy no está cargando ese tráfico. - Si un portal cautivo exige una página de login, desconecta el proxy un momento, completa el portal cautivo en el navegador y vuelve a conectar.
11. Rotación de secretos
SeguridadRota UUID + path de WebSocket
Hazlo si crees que un secreto se filtró — share-link enviado por email plano, teléfono perdido, etc. El túnel de Cloudflare y el registro DNS se mantienen iguales; solo se regenera la autenticación del lado de Xray, así que no hacen falta llamadas a la API.
cd ~/proxy-stack
NEW_UUID=$(uuidgen)
NEW_PATH=$(openssl rand -hex 16)
OLD_UUID=$(grep '^PROXY_UUID=' .env | cut -d= -f2)
OLD_PATH=$(grep '^PROXY_WSPATH=' .env | cut -d= -f2)
sed -i "s|$OLD_UUID|$NEW_UUID|g" .env xray/config.json
sed -i "s|$OLD_PATH|$NEW_PATH|g" .env xray/config.json
./proxyctl.sh restart
./proxyctl.sh share-link # re-import on the phone
Rota el túnel mismo
Si se filtró el token del conector, borra el túnel y recréalo vía API (corre de nuevo la sección 4.4 con el mismo dominio y subdominio). El CNAME de DNS se actualiza solo porque cambia el ID del túnel.
12. Desmontaje
OperaciónBorrar todo limpiamente
Detén los contenedores, quita el cron watchdog, borra el registro DNS, borra el túnel, y luego limpia el directorio de trabajo para que todos los secretos desaparezcan del disco.
# 1. Stop containers
cd ~/proxy-stack && docker compose down
# 2. Remove cron watchdog
crontab -l | grep -v "proxy-stack/watchdog.sh" | crontab -
# 3. Delete the CF tunnel + DNS record (use the .env values)
. ./.env
HEAD=(-H "X-Auth-Email: $CF_EMAIL" -H "X-Auth-Key: $(cat cf_api_key.secret)" -H "Content-Type: application/json")
# Find and delete the DNS record
DNS_ID=$(curl -sS "${HEAD[@]}" \
"https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records?name=$PROXY_HOSTNAME" \
| python3 -c "import sys,json;d=json.load(sys.stdin);print(d['result'][0]['id'] if d['result'] else '')")
[ -n "$DNS_ID" ] && curl -sS "${HEAD[@]}" -X DELETE \
"https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records/$DNS_ID" >/dev/null
# Delete the tunnel
curl -sS "${HEAD[@]}" -X DELETE \
"https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/$CF_TUNNEL_ID?cascade=true" >/dev/null
# 4. Wipe the working dir (kills all secrets)
cd ~ && rm -rf ~/proxy-stack
En el teléfono: borra el perfil de v2RayTun. Listo.
13. Recetario de solución de problemas
Operación"v2RayTun dice ‘connection refused’ / ‘EOF’"
Algo rechazó el handshake TLS. Revisa cómo se ve el túnel desde afuera:
curl -vI https://proxy.example.com/ 2>&1 | grep -E 'subject|issuer|HTTP'
# Cert subject must mention proxy.example.com or *.example.com
Si el certificado es para otro nombre, el "Universal SSL" de tu zona aún no cubre el subdominio — espera 10 minutos.
"El upgrade WS devuelve 400 en lugar de 101"
Estás mandando HTTP/2 con cabeceras Connection: Upgrade, lo cual es ilegal. Fuerza HTTP/1.1 explícitamente:
curl --http1.1 -H "Connection: Upgrade" ...
El cliente del teléfono ya usa HTTP/1.1; esto solo importa en pruebas manuales con curl.
"El teléfono conecta, pero ifconfig.co muestra el país de la WiFi"
v2RayTun se conectó, pero el enrutamiento está fugando. Revisa las reglas de ruteo en el teléfono — el modo VPN debe ser Global / TUN, no split-tunnel. Desactiva cualquier whitelist por app. Re-corre las cinco pruebas de la Sección 6.
"Funcionaba ayer, hoy en la mañana está muerto"
El cron watchdog debería haberlo capturado. Revisa:
tail -50 ~/proxy-stack/watchdog.log
docker ps --filter name=proxy-
Si WSL mismo se murió (raro), wsl --shutdown desde PowerShell, luego re-abre una terminal WSL — la tarea KeepAlive debería resucitarlo.
"El túnel de Cloudflare aparece como ‘down’ en el dashboard"
Inspecciona los logs de cloudflared y confirma que el token del conector siga coincidiendo con el del dashboard. Si alguna vez lo revocaste, regenera el token vía API y actualiza .env:
docker logs proxy-cloudflared --tail 100 | grep -iE 'error|unauth|registered'
docker compose restart cloudflared
14. Costo
OperaciónDesglose de costo mensual
| Ítem | Mensual |
|---|---|
| Cloudflare Tunnel (plan Free) | $0 |
| DNS / proxy de Cloudflare en la zona | $0 |
| Ancho de banda por el edge de Cloudflare | $0 (lo cubre el plan de tu ISP; CF no mide el ancho de banda del túnel) |
| Cómputo en la máquina de casa | Electricidad de la máquina siempre encendida |
| Renovación del dominio | ~$10/año según el TLD |
Total recurrente: básicamente gratis, más electricidad. El mayor costo es el tiempo de la instalación inicial, que esta guía está diseñada para comprimir a más o menos una noche.
15. Por qué rechacé las alternativas obvias
ArquitecturaConsideradas y descartadas
| Alternativa | Razón del descarte |
|---|---|
| Cloudflare WARP / Zero Trust + WARP Connector | Requiere un servidor Linux para el Connector; te encierra en la UI de Zero Trust; consume uno de los 50 asientos gratis; el WARP puro del teléfono enruta a cualquier lado pero no sale por la IP de tu casa. |
| WireGuard via cloudflared | cloudflared no transporta UDP; necesitarías Cloudflare Spectrum, que es de pago. |
| OpenVPN over TCP via cloudflared | Funciona, pero exige la app OpenVPN Connect (más pesada) y el modo TCP de OpenVPN es lento. |
| Plain SOCKS5 / HTTP proxy | Sin soporte UDP; no es system-wide en Android sin herramientas extra. |
| Tailscale | Excelente producto, pero es Tailscale, no Cloudflare — frontera de confianza distinta, red anycast distinta, modelo de amenazas distinto. |
| Native Android VPN (IKEv2 / L2TP) | La UI nativa es genial, pero estos protocolos requieren exposición directa por UDP (puerto 500/4500), lo cual rompe el requisito "sin IP pública". |
| Self-hosted Headscale + WireGuard | Lo más flexible a largo plazo; más piezas móviles que CF Tunnel + Xray. Vale la pena si superas esta arquitectura. |
| Commercial VPN (NordVPN, Mullvad, etc.) | Sale por la IP del proveedor de VPN — los bancos lo tratan como sospechoso y disparan MFA / bloqueos. El punto entero aquí es salir por tu IP residencial de casa. |
El stack elegido (cloudflared + Xray VLESS+WS+TLS + v2RayTun) es el conjunto más pequeño de piezas móviles que entrega TCP/UDP/DNS completo, sin IP pública, con clientes móviles maduros y costo recurrente cero.