Le VpnService d'Android n'a pas de route d'exclusion, et aucune lecture d'API, aussi poussée soit-elle, n'en produira une. Builder.addRoute() ne fait qu'ajouter des destinations au tunnel, donc garder un segment de LAN local signifie que ce segment n'entre jamais dans la liste des routes. Ce que vous ajoutez à la place, c'est le complément du segment, exprimé en préfixes ordinaires. Le travail est arithmétique, et il échoue en quelques endroits prévisibles.

Passez les vérifications ci-dessous dans l'ordre. Chacune suppose que la précédente a tenu, donc arrêtez-vous au premier échec plutôt que de rapiécer des symptômes plus bas.

Partez de ce que le tunnel revendique

Passer 0.0.0.0/0 à addRoute() dit à Android que toute destination IPv4 appartient au tunnel. C'est pourquoi tant de clients semblent incapables de faire du tunnel divisé : ils revendiquent tout l'espace d'adresses puis tentent d'en soustraire. Rien dans le constructeur ne soustrait. Une route plus spécifique pour le réseau que vous vouliez garder local fait l'inverse de ce qu'on attend, car le préfixe le plus long l'emporte et ce réseau atterrit dans le tunnel.

La liste des routes est toute la politique. Il n'y a aucun veto par destination derrière elle, et la référence Builder ne liste aucune méthode qui en fournirait un.

Générez le complément à partir de l'arithmétique des préfixes

Le complément d'un préfixe exclu est l'ensemble de ses frères sur le chemin vers la racine de l'arbre d'adresses. Pour chaque longueur de 1 jusqu'à la longueur exclue, prenez le bloc de cette longueur contenant votre préfixe, inversez le bit juste sous le préfixe et émettez le résultat. Cela paraît abstrait jusqu'à ce qu'on l'exécute.

Exclure 10.0.0.0/8 donne huit routes : 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. Aucune ne contient 10.0.0.0/8, et ensemble elles couvrent tout le reste.

Huit lignes génèrent cette liste pour n'importe quel préfixe :

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))

Le nombre de routes est égal à la longueur du préfixe, et c'est le nombre à surveiller. Une seule exclusion /21 coûte 21 routes et reste lisible. Dix exclusions peuvent coûter quelques centaines, ce qui signifie généralement que c'est la conception du réseau qu'il faut changer, pas le générateur.

Vérifiez la famille d'adresses et la portée du tunnel

Les routes IPv4 ne font rien pour IPv6. Si le tunnel revendique une adresse IPv6, un préfixe de LAN IPv6 a besoin de son propre complément, généré avec la même arithmétique sur 128 bits. Une cause fréquente d'une exclusion qui marche sur le papier alors que l'appareil local reste injoignable est celle-ci : l'hôte a été atteint en IPv6, et seul IPv4 avait été découpé. Vérifiez les deux familles avant de conclure que les mathématiques sont fausses.

La portée est l'autre moitié de la vérification. Android suit le routage par réseau et par application, donc le même préfixe peut passer par le tunnel pour une application et être direct pour une autre selon la façon dont le VPN a été déclaré.

Placez les règles DNS et par application là où elles appartiennent

Les exclusions de route ne changent pas le résolveur. Un nom d'hôte local peut encore être résolu par le serveur DNS du tunnel alors que son adresse est routée localement, et l'écart se voit comme un nom qui résout vers quelque chose que le LAN ne peut pas atteindre. Réglez la politique du résolveur délibérément, avec un domaine de recherche ou une règle de DNS scindé. Ne supposez pas que les routes en ont décidé pour vous.

addDisallowedApplication() garde une application hors du tunnel ; il ne garde pas une destination dehors pour les applications qui restent dedans. protect() permet à un socket que vous ouvrez de contourner le tunnel, et c'est ainsi qu'un tunnel évite de capturer son propre transport. Ni l'un ni l'autre ne remplace le complément.

Vérifiez par une recherche, pas par votre propre liste

Une fois le tunnel monté, demandez à l'appareil où va réellement une adresse au lieu de relire la liste que vous avez construite. Sur une compilation qui livre iproute2, adb shell ip route get 10.1.2.3 nomme l'interface choisie par le noyau. Lancez la même commande pour une adresse publique et confirmez qu'elle nomme le tunnel.

Deux vérifications de plus bouclent la boucle. Résolvez d'abord un nom local, puis lancez adb shell ip route get sur l'adresse résolue et exigez une interface qui n'est pas le tunnel, car un adb shell ping -c 1 host.lan réussi prouve l'accessibilité et non le chemin. Répétez le test de famille d'adresses en IPv6 si le tunnel en revendique une. Testez au niveau d'API que vous livrez, car la façon dont les réseaux VPN sont exposés à la table de routage a changé au fil des versions. Une liste de routes qui paraît correcte sur le papier et une recherche qui choisit la mauvaise interface sont des bogues différents, et seul le second atteint les utilisateurs.

Si la liste d'exclusions continue de grandir, arrêtez de générer des routes et renumérotez le réseau distant ou rétrécissez le tunnel à la place. Le générateur est un contournement de l'API manquante, pas un outil de conception.

Articles similaires