Pones un comando route en /etc/ppp/ip-up, vuelves a conectar el túnel de la oficina y el archivo nunca se ejecuta. Con el script no hay nada mal: esa conexión no pasa por PPP en absoluto. Los clientes integrados Cisco IPSec e IKEv2 nunca inician pppd, así que el directorio de hooks nunca se lee, mientras que PPTP y L2TP sobre IPSec siguen pasando por él. Averigua qué caso tienes y luego dispara el caso no-PPP desde un trabajo de vigilancia launchd.

Comprueba la interfaz antes de editar cualquier hook

Dos comandos deciden qué mecanismo puede funcionar. Lista los servicios configurados y luego lista las interfaces que existen mientras el túnel está activo. El nombre de la interfaz es la prueba; la etiqueta en Configuración del Sistema no lo es.

scutil --nc list
ifconfig -l | tr ' ' '\n' | grep -E '^(utun|ppp)[0-9]+$'

Un servicio que produce ppp0 ejecuta los hooks de PPP y ejecutará tu archivo. Un servicio Cisco IPSec o IKEv2 produce en cambio un dispositivo utun, una interfaz de túnel sin capa PPP, así que nada dentro de /etc/ppp puede alcanzarlo. Cuando aparecen ambos tipos, comprueba a qué servicio te conectaste. La gente pierde una tarde en el hook equivocado por leer el nombre del cliente.

En la ruta PPP el archivo debe pertenecer a root y ser ejecutable, porque pppd comprueba ambas cosas antes de ejecutar nada. install fija propietario y modo en un solo paso.

sudo install -o root -g wheel -m 0755 /dev/null /etc/ppp/ip-up
sudo ls -l /etc/ppp/ip-up

Un archivo creado con Finder o un editor gráfico a menudo acaba siendo propiedad de tu cuenta, o sin el bit de ejecución, y entonces falla tan silenciosamente como el hook ausente. Pon ip-down a su lado ahora. root y wheel en la columna de propietario más rwxr-xr-x en la columna de modo es lo que quieres.

Deja que launchd dispare el script cuando aparezca un túnel

Para los clientes que nunca tocan PPP, el disparador debe venir de otro sitio, y el almacén de configuración del sistema es la fuente honesta: macOS lo reescribe en cada transición de túnel. Un trabajo launchd que vigila ese directorio se llama cuando un túnel aparece o desaparece.

<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
  <key>WatchPaths</key>
  <array><string>/Library/Preferences/SystemConfiguration</string></array>
  <key>Label</key><string>net.example.tunnel-watch</string>
  <key>ProgramArguments</key>
  <array><string>/bin/sh</string><string>/usr/local/sbin/tunnel-hook</string></array>
  <key>RunAtLoad</key><true/>
  <key>ThrottleInterval</key><integer>20</integer>
</dict>
</plist>

Instálalo como daemon launch, no como agente por usuario, porque la reparación de rutas pertenece a la máquina. bootstrap sustituye a la forma antigua load -w en las versiones actuales. RunAtLoad hace observable la primera ejecución, y ThrottleInterval evita que una ráfaga de cambios de conexión Wi-Fi lance el script repetidamente.

sudo install -o root -g wheel -m 0644 tunnel-watch.plist \
  /Library/LaunchDaemons/net.example.tunnel-watch.plist
sudo launchctl bootstrap system \
  /Library/LaunchDaemons/net.example.tunnel-watch.plist

La trampa es que el directorio vigilado cambia por eventos que no tienen nada que ver con tu VPN: una conexión Wi-Fi, una renovación DHCP, un cable enchufado. El guardián de abajo convierte un disparador ruidoso en un hook utilizable, y una línea de registro explica una ruta a una hora extraña.

La propiedad cubre el plist igual que el script. Un daemon que ejecuta un archivo en un directorio donde tu propia cuenta puede escribir le entrega root a esa cuenta.

Protege el script para que una segunda ejecución no cambie nada

El script primero decide si el túnel existe y sale en silencio cuando no. Todo lo posterior debe ser seguro de repetir, porque el disparador se activa mucho más a menudo que una sola conexión. Resuelve la interfaz en tiempo de ejecución en lugar de codificar un índice que cambia en la siguiente conexión.

#!/bin/sh
set -u
TUN=$(ifconfig -l | tr ' ' '\n' | grep -m1 '^utun[0-9]' || true)
[ -n "$TUN" ] || exit 0
netstat -rn -f inet | grep -q '^10\.42' && exit 0
route -n add -net 10.42.0.0/16 -interface "$TUN"

route add sale con estado distinto de cero cuando la ruta ya existe, así que un script que trata cada estado distinto de cero como fallo informa de un hook roto cada vez que hace lo correcto. La prueba con netstat de arriba convierte ese caso en un no-op explícito.

La presencia de la interfaz es una prueba más débil de lo que parece, porque otros clientes VPN y algunos entornos de contenedores también crean dispositivos utun. Pregunta al propio servicio con scutil --nc status "Office VPN" y exige el estado conectado antes de tocar el enrutamiento. Guarda la respuesta en una línea de registro, porque una semana después ese registro será la única explicación que tengas.

Recupera la puerta de enlace sin romper el DNS

Cuando el túnel asume la ruta predeterminada, la navegación normal sale por la oficina y la ruta local a internet deja de funcionar. Captura primero el router de la interfaz física y luego apunta la ruta predeterminada de vuelta a él. DHCP supera a analizar una tabla que ya contiene la ruta predeterminada propia del túnel.

PHYS=en0
GW=$(ipconfig getoption "$PHYS" router)
[ -n "$GW" ] && sudo route -n change default "$GW"

Los resolutores son una capa aparte con una herramienta aparte, y reparar la ruta no los repara. Mira qué está en vigor con scutil --dns y luego corrige lo que lo produce: el orden de servicios y las entradas DNS del servicio físico. Vaciar la caché diagnostica una consulta atascada tras un cambio de red; no es algo que se programe.

Un túnel que sobrevive a un cambio de Wi-Fi mientras el DNS se atasca es un tercer fallo: sudo killall -HUP mDNSResponder hace que el resolutor relea su configuración. Úsalo una vez, a mano.

Demuestra que el trabajo se dispara solo

Fuerza una ejecución con kickstart -k antes de confiar en la ruta de vigilancia y luego lee el estado de salida que registró el trabajo. Una salida limpia demuestra que el script funciona; no demuestra que funcione el disparador. Después desconecta y vuelve a conectar con el trabajo cargado y observa una ejecución que no pediste.

sudo launchctl kickstart -k system/net.example.tunnel-watch
sudo launchctl print system/net.example.tunnel-watch | grep -E 'last exit code|state ='

Comprueba scutil --nc status antes y después de cada intento. Un script que se ejecutó mientras el túnel estaba caído no demuestra nada sobre la ruta de conexión.

La rama de desmontaje merece el mismo trato que la rama de conexión: quita la ruta a mano, deja que el trabajo se ejecute y confirma que vuelve. Un hook que añade una ruta y nunca la quita deja una máquina que se comporta mal una semana después, sin nada que conecte los dos sucesos. Cuando ambos comportamientos se sostienen, sudo launchctl bootout system/net.example.tunnel-watch retira el trabajo limpiamente. Un daemon de vigilancia dejado cargado es como una reparación puntual se convierte en una ruta inexplicada meses después.

Artículos relacionados