El VpnService de Android no tiene ruta de exclusión, y ninguna cantidad de lectura de la API producirá una. Builder.addRoute() solo añade destinos al túnel, así que mantener un segmento de LAN local significa que ese segmento nunca entra en la lista de rutas. Lo que añades en su lugar es el complemento del segmento, expresado como prefijos ordinarios. El trabajo es aritmética, y falla en un puñado de lugares predecibles.

Recorre las comprobaciones de abajo en orden. Cada una supone que la anterior se cumplió, así que detente en el primer fallo en lugar de parchear síntomas más abajo.

Empieza por lo que el túnel reclama

Pasar 0.0.0.0/0 a addRoute() le dice a Android que cada destino IPv4 pertenece al túnel. Por eso tantos clientes parecen incapaces de hacer tunelización dividida: reclaman todo el espacio de direcciones y luego intentan restar de él. Nada en el constructor resta. Una ruta más específica para la red que querías mantener local hace lo contrario de lo que la gente espera, porque gana el prefijo coincidente más largo y esa red acaba dentro del túnel.

La lista de rutas es toda la política. No hay veto por destino detrás de ella, y la referencia de Builder no enumera ningún método que lo proporcionaría.

Genera el complemento con aritmética de prefijos

El complemento de un prefijo excluido es el conjunto de sus hermanos en el camino hacia la raíz del árbol de direcciones. Para cada longitud desde 1 hasta la longitud excluida, toma el bloque de esa longitud que contiene tu prefijo, invierte el bit inmediatamente por debajo del prefijo y emite el resultado. Suena abstracto hasta que lo ejecutas.

Excluir 10.0.0.0/8 produce ocho rutas: 128.0.0.0/1, 64.0.0.0/2, 32.0.0.0/3, 16.0.0.0/4, 0.0.0.0/5, 12.0.0.0/6, 8.0.0.0/7, 11.0.0.0/8. Ninguna contiene 10.0.0.0/8, y juntas cubren todo lo demás.

Ocho líneas generan esa lista para cualquier prefijo:

import ipaddress
def complement(addr, length):
    base = int(ipaddress.IPv4Address(addr))
    out = []
    for k in range(1, length + 1):
        block = base & (0xFFFFFFFF << (32 - k))
        sibling = block ^ (1 << (32 - k))
        out.append(f"{ipaddress.IPv4Address(sibling)}/{k}")
    return out
print(complement("10.0.0.0", 8))

El número de rutas es igual a la longitud del prefijo, y ese es el número que hay que vigilar. Una sola exclusión /21 cuesta 21 rutas y sigue siendo legible. Diez exclusiones pueden costar unos cientos, lo que suele significar que hay que cambiar el diseño de la red, no el generador.

Comprueba la familia de direcciones y el alcance del túnel

Las rutas IPv4 no hacen nada por IPv6. Si el túnel reclama una dirección IPv6, un prefijo de LAN IPv6 necesita su propio complemento, generado con la misma aritmética sobre 128 bits. Una causa habitual de que la exclusión funcione sobre el papel mientras el dispositivo local sigue inalcanzable es esta: se llegó al host por IPv6, y solo se había recortado IPv4. Comprueba ambas familias antes de concluir que las matemáticas están mal.

El alcance es la otra mitad de la comprobación. Android rastrea el enrutamiento por red y por aplicación, así que el mismo prefijo puede ir por el túnel para una aplicación y directo para otra según cómo se haya declarado la VPN.

Pon las reglas de DNS y por aplicación donde corresponden

Las exclusiones de ruta no cambian el resolutor. Un nombre de host local aún puede ser respondido por el servidor DNS del túnel mientras su dirección se enruta localmente, y el desajuste se ve como un nombre que resuelve a algo que la LAN no puede alcanzar. Define la política del resolutor a propósito, con un dominio de búsqueda o una regla de DNS dividido. No des por hecho que las rutas lo decidieron por ti.

addDisallowedApplication() mantiene una aplicación fuera del túnel; no mantiene un destino fuera para las aplicaciones que se quedan dentro. protect() permite que un socket que abres evite el túnel, y así un túnel evita capturar su propio transporte. Ninguno de los dos sustituye al complemento.

Verifica con una consulta, no con tu propia lista

Una vez levantado el túnel, pregunta al dispositivo a dónde va realmente una dirección en lugar de releer la lista que construiste. En una compilación que incluye iproute2, adb shell ip route get 10.1.2.3 nombra la interfaz que eligió el núcleo. Ejecuta el mismo comando para una dirección pública y confirma que nombra el túnel.

Dos comprobaciones más cierran el círculo. Resuelve primero un nombre local, luego ejecuta adb shell ip route get sobre la dirección resuelta y exige una interfaz que no sea el túnel, porque un adb shell ping -c 1 host.lan correcto demuestra alcanzabilidad y no el camino. Repite la prueba de familia de direcciones sobre IPv6 si el túnel reclama una. Prueba en el nivel de API que distribuyes, porque cómo se exponen las redes VPN a la tabla de enrutamiento ha cambiado entre versiones. Una lista de rutas que parece correcta sobre el papel y una consulta que elige la interfaz equivocada son fallos distintos, y solo el segundo llega a los usuarios.

Si la lista de exclusiones sigue creciendo, deja de generar rutas y renumera la red remota o estrecha el túnel en su lugar. El generador es un apaño para la API que falta, no una herramienta de diseño.

Artículos relacionados