Hoe je een subnet uitsluit van een Android VPN-tunnel
VpnService kan geen route uitsluiten, dus voeg je het complement toe: acht gegenereerde prefixen voor een /8, plus de IPv6-, DNS- en opzoekcontroles die de rest vangen.
De VpnService van Android heeft geen uitsluitingsroute, en geen enkele hoeveelheid API-lezen levert er een op. Builder.addRoute() voegt altijd alleen bestemmingen aan de tunnel toe, dus een LAN-segment lokaal houden betekent dat dat segment nooit in de routelijst komt. Wat je in plaats daarvan toevoegt is het complement van het segment, uitgedrukt in gewone prefixen. Het werk is rekenwerk, en het faalt op een handvol voorspelbare plekken.
Werk de controles hieronder op volgorde af. Elk gaat ervan uit dat de vorige standhield, dus stop bij de eerste fout in plaats van symptomen verderop te pleisteren.
Begin bij wat de tunnel claimt
Het doorgeven van 0.0.0.0/0 aan addRoute() vertelt Android dat elke IPv4-bestemming bij de tunnel hoort. Daarom lijken zoveel clients niet in staat tot gesplitst tunnelen: ze claimen de volledige adresruimte en proberen daar vervolgens van af te trekken. Niets in de builder trekt af. Een specifiekere route voor het netwerk dat je lokaal wilde houden doet het tegenovergestelde van wat mensen verwachten, omdat het langste overeenkomende prefix wint en dat netwerk in de tunnel belandt.
De routelijst is het hele beleid. Er zit geen veto per bestemming achter, en de Builder-referentie noemt geen methode die er een zou leveren.
Genereer het complement uit prefix-rekenkunde
Het complement van een uitgesloten prefix is de verzameling van zijn broers en zussen op het pad naar de wortel van de adresboom. Neem voor elke lengte van 1 tot de uitgesloten lengte het blok van die lengte dat je prefix bevat, draai de bit direct onder het prefix om en geef het resultaat weer. Het klinkt abstract tot je het uitvoert.
Het uitsluiten van 10.0.0.0/8 levert acht routes op: 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. Geen ervan bevat 10.0.0.0/8, en samen dekken ze al het andere.
Acht regels genereren die lijst voor elk prefix:
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))
Het aantal routes is gelijk aan de prefixlengte, en dat is het getal om in de gaten te houden. Eén enkele /21-uitsluiting kost 21 routes en blijft leesbaar. Tien uitsluitingen kunnen een paar honderd kosten, wat meestal betekent dat het netwerkontwerp moet veranderen, niet de generator.
Controleer de adresfamilie en de omvang van de tunnel
IPv4-routes doen niets voor IPv6. Als de tunnel een IPv6-adres claimt, heeft een IPv6 LAN-prefix zijn eigen complement nodig, gegenereerd met dezelfde rekenkunde over 128 bits. Een veelvoorkomende oorzaak dat de uitsluiting op papier werkt terwijl het lokale apparaat onbereikbaar blijft, is deze: de host werd via IPv6 bereikt, en alleen IPv4 was uitgesneden. Controleer beide families voordat je concludeert dat de wiskunde fout is.
Omvang is de andere helft van de controle. Android volgt routing per netwerk en per app, dus hetzelfde prefix kan voor de ene app door de tunnel gaan en voor de andere direct, afhankelijk van hoe de VPN is gedeclareerd.
Zet DNS- en per-app-regels waar ze horen
Route-uitsluitingen veranderen de resolver niet. Een lokale hostnaam kan nog steeds door de DNS-server van de tunnel worden beantwoord terwijl het adres lokaal wordt gerouteerd, en de mismatch uit zich als een naam die oplost naar iets wat het LAN niet kan bereiken. Stel het resolverbeleid bewust in, met een zoekdomein of een split-DNS-regel. Ga er niet van uit dat de routes dat voor je hebben besloten.
addDisallowedApplication() houdt één app buiten de tunnel; het houdt geen bestemming buiten voor de apps die binnen blijven. protect() laat een socket die je opent de tunnel omzeilen, en zo vermijdt een tunnel zijn eigen transport te vangen. Geen van beide vervangt het complement.
Verifieer met een opzoeking, niet met je eigen lijst
Zodra de tunnel staat, vraag het apparaat waar een adres werkelijk heen gaat in plaats van de lijst die je hebt gebouwd opnieuw te lezen. Op een build die iproute2 meelevert, noemt adb shell ip route get 10.1.2.3 de interface die de kernel heeft gekozen. Voer dezelfde opdracht uit voor een openbaar adres en bevestig dat die de tunnel noemt.
Twee controles meer sluiten de lus. Los eerst een lokale naam op, voer dan adb shell ip route get uit op het opgeloste adres en eis een interface die niet de tunnel is, want een geslaagde adb shell ping -c 1 host.lan bewijst bereikbaarheid en niet het pad. Herhaal de adresfamilie-test over IPv6 als de tunnel er een claimt. Test op het API-niveau dat je uitlevert, want hoe VPN-netwerken aan de routingtabel worden blootgesteld is tussen releases veranderd. Een routelijst die op papier klopt en een opzoeking die de verkeerde interface kiest zijn verschillende bugs, en alleen de tweede bereikt gebruikers.
Als de uitsluitingslijst blijft groeien, stop dan met routes genereren en hernummer het externe netwerk of versmal de tunnel in plaats daarvan. De generator is een omweg voor de ontbrekende API, geen ontwerpgereedschap.