Come escludere una sottorete da un tunnel VPN Android
VpnService non può escludere una rotta, quindi aggiungi il complemento: otto prefissi generati per un /8, più i controlli IPv6, DNS e di ricerca che intercettano il resto.
Il VpnService di Android non ha una rotta di esclusione, e nessuna quantità di lettura delle API ne produrrà una. Builder.addRoute() aggiunge sempre e solo destinazioni al tunnel, quindi tenere locale un segmento LAN significa che quel segmento non entra mai nell'elenco delle rotte. Ciò che aggiungi invece è il complemento del segmento, espresso come prefissi ordinari. Il lavoro è aritmetica, e fallisce in una manciata di punti prevedibili.
Esegui i controlli qui sotto in ordine. Ognuno presuppone che il precedente abbia retto, quindi fermati al primo fallimento invece di rattoppare i sintomi più avanti.
Parti da ciò che il tunnel rivendica
Passare 0.0.0.0/0 a addRoute() dice ad Android che ogni destinazione IPv4 appartiene al tunnel. È per questo che tanti client sembrano incapaci di fare tunneling diviso: rivendicano l'intero spazio di indirizzi e poi provano a sottrarne. Nulla nel builder sottrae. Una rotta più specifica per la rete che volevi mantenere locale fa l'opposto di ciò che ci si aspetta, perché vince il prefisso più lungo che corrisponde e quella rete finisce dentro il tunnel.
L'elenco delle rotte è tutta la politica. Non c'è alcun veto per destinazione dietro di esso, e il riferimento Builder non elenca alcun metodo che lo fornirebbe.
Genera il complemento dall'aritmetica dei prefissi
Il complemento di un prefisso escluso è l'insieme dei suoi fratelli sul percorso verso la radice dell'albero degli indirizzi. Per ogni lunghezza da 1 fino alla lunghezza esclusa, prendi il blocco di quella lunghezza che contiene il tuo prefisso, inverti il bit immediatamente sotto il prefisso ed emetti il risultato. Sembra astratto finché non lo esegui.
Escludere 10.0.0.0/8 produce otto rotte: 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. Nessuna contiene 10.0.0.0/8, e insieme coprono tutto il resto.
Otto righe generano quell'elenco per qualsiasi prefisso:
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))
Il numero di rotte è uguale alla lunghezza del prefisso, ed è il numero da tenere d'occhio. Una singola esclusione /21 costa 21 rotte e resta leggibile. Dieci esclusioni possono costare qualche centinaio, il che di solito significa che va cambiato il progetto della rete, non il generatore.
Controlla la famiglia di indirizzi e l'ambito del tunnel
Le rotte IPv4 non fanno nulla per IPv6. Se il tunnel rivendica un indirizzo IPv6, un prefisso LAN IPv6 ha bisogno del proprio complemento, generato con la stessa aritmetica su 128 bit. Una causa comune per cui l'esclusione funziona sulla carta mentre il dispositivo locale resta irraggiungibile è questa: si è raggiunto l'host tramite IPv6, ed era stato ritagliato solo IPv4. Controlla entrambe le famiglie prima di concludere che la matematica è sbagliata.
L'ambito è l'altra metà del controllo. Android traccia l'instradamento per rete e per app, quindi lo stesso prefisso può passare dal tunnel per un'app e diretto per un'altra a seconda di come è stato dichiarato il VPN.
Metti le regole DNS e per app dove appartengono
Le esclusioni di rotta non cambiano il resolver. Un nome host locale può ancora essere risolto dal server DNS del tunnel mentre il suo indirizzo è instradato localmente, e la discrepanza si vede come un nome che risolve in qualcosa che la LAN non riesce a raggiungere. Imposta la politica del resolver deliberatamente, con un dominio di ricerca o una regola di DNS diviso. Non dare per scontato che le rotte lo abbiano deciso per te.
addDisallowedApplication() tiene un'app fuori dal tunnel; non tiene fuori una destinazione per le app che restano dentro. protect() consente a un socket che apri di aggirare il tunnel, ed è così che un tunnel evita di catturare il proprio trasporto. Nessuno dei due sostituisce il complemento.
Verifica con una ricerca, non con la tua lista
Una volta attivo il tunnel, chiedi al dispositivo dove va davvero un indirizzo invece di rileggere l'elenco che hai costruito. Su una build che include iproute2, adb shell ip route get 10.1.2.3 nomina l'interfaccia scelta dal kernel. Esegui lo stesso comando per un indirizzo pubblico e conferma che nomina il tunnel.
Altri due controlli chiudono il cerchio. Risolvi prima un nome locale, poi esegui adb shell ip route get sull'indirizzo risolto e richiedi un'interfaccia che non sia il tunnel, perché un adb shell ping -c 1 host.lan riuscito dimostra la raggiungibilità e non il percorso. Ripeti il test della famiglia di indirizzi su IPv6 se il tunnel ne rivendica una. Prova al livello di API che distribuisci, perché il modo in cui le reti VPN sono esposte alla tabella di routing è cambiato tra le versioni. Un elenco di rotte che sembra corretto sulla carta e una ricerca che sceglie l'interfaccia sbagliata sono bug diversi, e solo il secondo arriva agli utenti.
Se l'elenco delle esclusioni continua a crescere, smetti di generare rotte e rinumera la rete remota oppure restringi il tunnel. Il generatore è un aggiramento dell'API mancante, non uno strumento di progettazione.