# OpenVPN: cómo excluir destinos del túnel con `net_gateway` (el caso de apple.com / apple.es)

> Documento de referencia operativa. Origen: el Mac del operador navega con túnel completo de las VPN propias de OpenVPN (dos servidores Ubuntu 24 con `openvpn-server@server`, servicio `openvpn-server@server.service`), y apple.es / apple.com devolvían `503` mientras iCloud y App Store entraban en bucle silencioso. Solución aplicada y verificada en ambos servidores el 2026-08-27.
>
> **Palabras clave para búsqueda:** OpenVPN, split tunneling, `net_gateway`, `redirect-gateway`, exclusión de destinos, `push route`, Apple, `17.0.0.0/8`, macOS, Safari, 503, iCloud, App Store, OpenVPN Connect, system extension, SplitTunnel, CDN Akamai, Fastly, Private Relay, Acceso privado a iCloud, `mask.apple-dns.net`, ODOH, tcpdump, curl HEAD 403.

## El diagnóstico correcto: no es per-app, es por destino

macOS no soporta enrutado por aplicación (per-app routing) con clientes OpenVPN estándar — ni Tunnelblick, ni Viscosity, ni OpenVPN Connect. Las apps de terceros que lo prometen (p. ej. SplitTunnel) usan extensiones de red y DNS que interceptan todo el tráfico; en esta estación resultaron inestables y se retiraron.

El problema real con Apple no era la app que navega, sino **el destino**: gran parte de la infraestructura de Apple vive en su propio bloque `17.0.0.0/8` (iCloud, App Store, activación, servidores corporativos), y ese bloque rechaza o degrada tráfico que llega desde IPs de datacenter/VPN. La solución quirúrgica es **excluir destinos del túnel**, no aplicaciones: el tráfico hacia `17.0.0.0/8` sale por el gateway físico pre-VPN y todo lo demás sigue cifrado por el túnel.

## El mecanismo: `net_gateway`

En la config del servidor OpenVPN, el keyword `net_gateway` significa «la gateway que tenía el cliente ANTES de levantar el túnel» (su router doméstico u office). Se combina con `push` para que el servidor la inyecte como ruta en cada cliente al conectar:

```bash
push "route 17.0.0.0 255.0.0.0 net_gateway"

```

- Con `redirect-gateway def1` activo, OpenVPN secuestra el tráfico con dos rutas `/1` (`0.0.0.0/1` y `128.0.0.0/1`) que cubren todo el espacio IPv4; la ruta `17.0.0.0/8` es **más específica** y gana sin tocar nada más.
- Es un cambio **solo de servidor**: los clientes (Mac, iPhone, iPad, Linux) no necesitan configurar nada.
- El `push` se aplica al **conectar**: los clientes ya conectados no lo ven hasta reconectar.
- Sobrevive a updates de macOS y reinicios del cliente: la ruta se re-aplica en cada reconexión por ser server-side (verificado tras un update de macOS con reinicio posterior: ruta activa y extensiones fantasma de SplitTunnel intactas).

## Aplicación en el servidor (patrón con backup e idempotencia)

Config en `/etc/openvpn/server/server.conf` (Ubuntu 24, unidad `openvpn-server@server`). Procedimiento por servidor:

```bash
cd /etc/openvpn/server

# Idempotente: solo añade si no está
grep -q "route 17.0.0.0 255.0.0.0 net_gateway" server.conf || {
  cp -a server.conf server.conf.$(date +%F).bak
  printf '\n# Exclude Apple (17.0.0.0/8) from tunnel - route via pre-VPN gateway\npush "route 17.0.0.0 255.0.0.0 net_gateway"\n' >> server.conf
}

# Reinicio controlado (corta las sesiones activas; los clientes reconectan solos)
systemctl restart openvpn-server@server
sleep 2
systemctl is-active openvpn-server@server
ss -ulnp | grep 1194
journalctl -u openvpn-server@server --since "1 minute ago" --no-pager | grep -E "Completed|error"

```

Verificación server-side esperada: servicio `active`, socket UDP en escucha y `Initialization Sequence Completed` en el journal.

## Verificación en dos puntos (REGLA 0)

**1. Desde el servidor:** journal limpio tras el reinicio (arriba).

**2. Desde el cliente conectado** (Mac aquí), verificar el efecto y **un control negativo** para demostrar que el resto del tráfico SÍ sigue por la VPN:

```bash
# La ruta push llegó (debe existir la línea "17" apuntando a tu router LAN)
netstat -rn -f inet | grep -E "^0/1|^128\.0/1|^17"
#    0/1  y 128.0/1  → 10.8.0.1  utunX   (túnel completo activo)
#    17              → 192.168.x.1  en0    (exclusión Apple)

# El detalle de ruta hacia una IP de Apple sale por la interfaz física
route -n get 17.253.144.10 | grep -E "gateway|interface"
#    gateway: 192.168.x.1 / interface: en0

# CONTROL: el resto del tráfico sale por la IP de la VPN (no por casa)
curl -s https://api.ipify.org; echo

# Apple accesible con el túnel activo
curl -sI https://www.apple.com/library/test/success.html | head -1   # HTTP/2 200

```

Cuidado con las páginas que redirigen: `apple.es` responde `301` a `https://www.apple.com/es/` (servida por Akamai, FUERA del bloque 17/8). Un `curl -I` de una sola pata puede dar «correcto» en el primer salto y que el fallo esté en el siguiente. Verificar SIEMPRE la cadena completa:

```bash
curl -sIL -o /dev/null -w "%{http_code} -> %{url_effective}\n" https://www.apple.es/
#    200 -> https://www.apple.com/es/

```

Y una trampa de cliente HTTP que imita a un bloqueo: `curl -sIL` envía peticiones **HEAD**, y algunos endpoints de auth de Apple (`idmsa.apple.com/IDMSWebAuth/signin.html`) responden `403` a HEAD pero `200` a GET. Un `403` en una cadena HEAD no significa que la página falle en el navegador; antes de culpar a la red, repetir con GET:

```bash
curl -sL -o /dev/null -w "%{http_code}\n" "https://idmsa.apple.com/IDMSWebAuth/signin.html?..."
#    200 (el 403 de la cadena HEAD era un artefacto del método)

```

Si la IP de `api.ipify.org` es la del servidor VPN y el `route get` de Apple responde con tu router LAN, las dos cosas a la vez están probadas.

## CDN de Apple fuera del bloque: añadir `/32` puntuales

`www.apple.com` y buena parte de los assets estáticos se sirven por Akamai (`www.apple.com.edgekey.net`), IPs que **no** están en `17.0.0.0/8`. Eso normalmente funciona igual por el túnel; si alguna página o descarga concreta sigue fallando, localiza su IP y exclúyela puntualmente:

```bash
# Localizar la IP real del dominio que falla
dscacheutil -q host -a name <dominio>.apple.com | grep ip_address

# En AMBOS servidores: ruta /32 específica
push "route A.B.C.D 255.255.255.255 net_gateway"

```

Reiniciar y reconectar para aplicarla. Es el mismo mecanismo; la receta genérica vale para cualquier destino problemático (un banco que bloquee IPs de datacenter, un servicio que exija IP doméstica, etc.).

## Diagnóstico: captura de DNS y SYNs con tcpdump

Cuando las pruebas por consola están verdes pero un navegador falla, la captura de cabeceras (sin contenido, privacidad intacta) dice exactamente con quién habla el navegador y por dónde sale. Lanzarla en background y dejarla corriendo mientras se reproduce el fallo:

```bash
nohup sudo tcpdump -i any -n -l -c 4000 \
  '(udp port 53) or (tcp[tcpflags] & tcp-syn != 0 and dst port 443)' \

  > /tmp/captura.txt 2>&1 & disown
```

Particularidades medidas en macOS:

- `-i any` funciona (interfaz PKTAP); no existe `timeout` en BSD, acotar con `-c` (líneas).
- `tcp[tcpflags] & tcp-syn != 0` casa también los SYN-ACK; al analizar, filtrar solo los salientes (`grep "Flags \[S"` y contar el destino en el campo 5).
- Lanzado desde un shell no interactivo (agente, script), el proceso hijo **muere cuando muere el shell padre**: imprescindible `nohup … & disown`; comprobar el PID y el fichero de salida antes de dar por iniciada la captura.
- Análisis: nombres consultados con `grep "A?"` y destinos SYN con conteo por IP; correlacionar las IPs desconocidas con el mapa host→IP del flujo depurado (`dscacheutil -q host -a name <dominio>`).

## El caso Safari residual: iCloud Private Relay

Con la exclusión `17/8` aplicada y el flujo de login completo verificado por `curl`, Safari podía seguir mostrando `503` en la redirección post-login de Apple ID — y una ventana privada no lo evitaba. Causa: **iCloud Private Relay (Acceso privado a iCloud)** activo.

- El Relay solo afecta al tráfico de **Safari** (y alguna app de sistema): `curl`, Chrome, Firefox o cualquier CLI van directos y no lo usan. Por eso todo lo verificado por consola estaba verde y solo el navegador fallaba — el síntoma «solo Safari» es su firma.
- Con el Relay activo, el tráfico de Safari no sale ni por casa ni por el túnel: va por los proxies de egreso de Apple, cuyas IPs **rotan entre peticiones**, y su DNS se resuelve vía ODOH (`mask.apple-dns.net`, `mask.icloud.com`). Una sesión de Apple ID creada desde una IP de egreso y continuada desde otra dispara el filtrado anti-abuso de Apple → `503` justo en la redirección post-login.
- La ventana privada NO desactiva el Relay: es un ajuste de la cuenta iCloud, no del modo de navegación.

Detección (prueba directa, no fiarse de procesos): la captura tcpdump durante el fallo muestra consultas DNS recurrentes por `mask.apple-dns.net` / `mask.icloud.com` y conexiones a los proxies de egreso de Apple (`17.248.x.x`) mezcladas con el tráfico del sitio. La ausencia de procesos tipo `netsesamd`/`relayd` en `ps` NO prueba que el Relay esté apagado; el ajuste en Ajustes del Sistema es la referencia.

Solución: Ajustes del Sistema → [Apple ID] → iCloud → **Acceso privado a iCloud** → desactivar, cerrar Safari (`Cmd+Q`) y reabrir. Si se quiere conservar el Relay, la alternativa es usar un navegador no-WebKit (Chrome/Firefox) para los sitios hostiles a VPN: el Relay no les afecta.

## Nota sobre IPv6

Estos servidores hacen `push "block-ipv6"`, así que con el túnel activo todo va por IPv4 y la exclusión `17.0.0.0/8` cubre a Apple completa. Si algún día se retira el `block-ipv6`, la exclusión solo cubriría el IPv4 de Apple; habría que valorar la contrapartida IPv6 (`route` IPv6 con `net_gateway` o bloqueo selectivo).

## Nota sobre SplitTunnel y su herencia en macOS

Si se probó SplitTunnel (boundarylabs) y se desinstaló arrastrándolo a la papelera, quedan restos que CleanMyMac no ve:

- **Extensiones de sistema registradas** (`systemextensionsctl list`): `ca.boundarylabs.SplitTunnel.Extension` y `ca.boundarylabs.SplitTunnel.DNSExtension` quedan como `[activated disabled]`. En ese estado no cargan código ni interceptan nada, pero siguen listadas.
- **No se pueden des-registrar por CLI con SIP activado**: `sudo systemextensionsctl uninstall <teamID> <bundleID>` responde «cannot be used if System Integrity Protection is enabled». La vía soportada es reinstalar la app y usar SU desinstalación (deactivation cooperativa), o vivir con el registro fantasma, que es inocuo.
- Restos de usuario sí se limpian a mano: `~/Library/Group Containers/group.ca.boundarylabs.*`, `~/Library/HTTPStorages/ca.boundarylabs.*` y `~/Library/Application Scripts/group.ca.boundarylabs.*`.