Sådan udelukker du et undernet fra en Android VPN-tunnel
VpnService kan ikke udelukke en rute, så du tilføjer komplementet: otte genererede præfikser til et /8, plus IPv6-, DNS- og opslagscheck, der fanger resten.
Androids VpnService har ingen udelukkelsesrute, og ingen mængde API-læsning vil frembringe en. Builder.addRoute() tilføjer altid kun destinationer til tunnelen, så at holde et LAN-segment lokalt betyder, at segmentet aldrig kommer i rutelisten. Det, du i stedet tilføjer, er komplementet til segmentet, udtrykt som almindelige præfikser. Arbejdet er aritmetik, og det fejler på en håndfuld forudsigelige steder.
Gå gennem kontrollerne nedenfor i rækkefølge. Hver enkelt forudsætter, at den forrige holdt, så stop ved den første fejl i stedet for at lappe symptomer længere nede.
Start med det, tunnelen påstår
At sende 0.0.0.0/0 til addRoute() fortæller Android, at enhver IPv4-destination hører til tunnelen. Derfor ser så mange klienter ude af stand til delt tunellering: de kræver hele adresseområdet og prøver så at trække fra det. Intet i byggeren trækker fra. En mere specifik rute til det netværk, du ville holde lokalt, gør det modsatte af, hvad folk forventer, fordi det længste matchende præfiks vinder, og netværket ender inde i tunnelen.
Rutelisten er hele politikken. Der er ingen veto pr. destination bag den, og Builder-referencen angiver ingen metode, der ville give en.
Generér komplementet ud fra præfiksaritmetik
Komplementet til et udelukket præfiks er mængden af dets søskende på vejen mod roden af adressetræet. For hver længde fra 1 op til den udelukkede længde tager du blokken af den længde, der indeholder dit præfiks, vender bitten umiddelbart under præfikset og udsender resultatet. Det lyder abstrakt, indtil du kører det.
Udelukkelse af 10.0.0.0/8 giver otte ruter: 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. Ingen af dem indeholder 10.0.0.0/8, og sammen dækker de alt andet.
Otte linjer genererer den liste for ethvert præfiks:
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))
Antallet af ruter er lig præfikslængden, og det er tallet at holde øje med. En enkelt /21-udelukkelse koster 21 ruter og forbliver læsbar. Ti udelukkelser kan koste et par hundrede, hvilket normalt betyder, at netværksdesignet skal ændres snarere end generatoren.
Kontrollér adressefamilien og tunnelens omfang
IPv4-ruter gør intet for IPv6. Hvis tunnelen kræver en IPv6-adresse, har et IPv6-LAN-præfiks brug for sit eget komplement, genereret med samme aritmetik over 128 bit. En almindelig årsag til, at udelukkelsen virker på papiret, mens den lokale enhed forbliver utilgængelig, er denne: værten blev nået over IPv6, og kun IPv4 var skåret ud. Kontrollér begge familier, før du konkluderer, at matematikken er forkert.
Omfang er den anden halvdel af kontrollen. Android sporer routing pr. netværk og pr. app, så det samme præfiks kan gå gennem tunnelen for én app og direkte for en anden afhængigt af, hvordan VPN'en er erklæret.
Placér DNS- og pr.-app-regler, hvor de hører til
Ruteudelukkelser ændrer ikke resolveren. Et lokalt værtsnavn kan stadig besvares af tunnelens DNS-server, mens dets adresse routes lokalt, og uoverensstemmelsen viser sig som et navn, der opløses til noget, LAN'et ikke kan nå. Sæt resolverpolitikken bevidst, med et søgedomæne eller en split-DNS-regel. Antag ikke, at ruterne har afgjort det for dig.
addDisallowedApplication() holder én app ude af tunnelen; det holder ikke en destination ude for de apps, der bliver inde. protect() lader en socket, du åbner, omgå tunnelen, og det er sådan en tunnel undgår at fange sin egen transport. Ingen af dem erstatter komplementet.
Bekræft med et opslag, ikke med din egen liste
Når tunnelen er oppe, så spørg enheden, hvor en adresse faktisk går hen, i stedet for at læse listen, du byggede, igen. På et build, der leverer iproute2, navngiver adb shell ip route get 10.1.2.3 det interface, kernen valgte. Kør den samme kommando for en offentlig adresse, og bekræft, at den navngiver tunnelen.
To kontroller mere lukker cirklen. Slå først et lokalt navn op, kør derefter adb shell ip route get på den opløste adresse og kræv et interface, der ikke er tunnelen, fordi et vellykket adb shell ping -c 1 host.lan beviser nåelighed og ikke vejen. Gentag adressefamilie-testen over IPv6, hvis tunnelen kræver en. Test på det API-niveau, du leverer, fordi måden VPN-netværk eksponeres for routingtabellen på, har ændret sig gennem udgivelser. En ruteliste, der ser rigtig ud på papiret, og et opslag, der vælger det forkerte interface, er forskellige fejl, og kun den anden når brugerne.
Hvis udelukkelseslisten bliver ved med at vokse, så stop med at generere ruter og omnummerér fjernnetværket eller indskrænk tunnelen i stedet. Generatoren er en omvej for den manglende API, ikke et designværktøj.