Så här undantar du ett subnät från en Android VPN-tunnel
VpnService kan inte undanta en rutt, så du lägger till komplementet: åtta genererade prefix för ett /8, plus IPv6-, DNS- och uppslagskontrollerna som fångar resten.
Androids VpnService har ingen undantagsrutt, och ingen mängd API-läsning producerar en. Builder.addRoute() lägger alltid bara till destinationer i tunneln, så att hålla ett LAN-segment lokalt innebär att segmentet aldrig kommer in i ruttlistan. Det du lägger till i stället är komplementet till segmentet, uttryckt som vanliga prefix. Arbetet är aritmetik, och det fallerar på en handfull förutsägbara ställen.
Gå igenom kontrollerna nedan i ordning. Var och en förutsätter att den föregående höll, så stanna vid det första felet i stället för att lappa symptom längre ner.
Börja med det tunneln hävdar
Att skicka 0.0.0.0/0 till addRoute() talar om för Android att varje IPv4-destination tillhör tunneln. Därför verkar så många klienter oförmögna till delad tunnling: de kräver hela adressutrymmet och försöker sedan dra ifrån det. Ingenting i byggaren drar ifrån. En mer specifik rutt för nätet du ville hålla lokalt gör motsatsen till vad folk förväntar sig, eftersom det längsta matchande prefixet vinner och det nätet hamnar inuti tunneln.
Ruttlistan är hela policyn. Det finns inget veto per destination bakom den, och Builder-referensen listar ingen metod som skulle ge ett.
Generera komplementet från prefixaritmetik
Komplementet till ett undantaget prefix är mängden av dess syskon på vägen mot roten av adressträdet. För varje längd från 1 upp till den undantagna längden tar du blocket med den längden som innehåller ditt prefix, vänder biten direkt under prefixet och skriver ut resultatet. Det låter abstrakt tills du kör det.
Undantag av 10.0.0.0/8 ger åtta rutter: 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 av dem innehåller 10.0.0.0/8, och tillsammans täcker de allt annat.
Åtta rader genererar den listan för vilket prefix som helst:
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))
Antalet rutter är lika med prefixets längd, och det är talet att hålla koll på. Ett enda /21-undantag kostar 21 rutter och förblir läsbart. Tio undantag kan kosta ett par hundra, vilket oftast betyder att nätverksdesignen måste ändras, inte generatorn.
Kontrollera adressfamiljen och tunnelns omfattning
IPv4-rutter gör ingenting för IPv6. Om tunneln hävdar en IPv6-adress behöver ett IPv6 LAN-prefix sitt eget komplement, genererat med samma aritmetik över 128 bitar. En vanlig orsak till att undantaget fungerar på papperet medan den lokala enheten förblir oåtkomlig är denna: värden nåddes över IPv6, och bara IPv4 hade skurits ut. Kontrollera båda familjerna innan du drar slutsatsen att matematiken är fel.
Omfattningen är den andra halvan av kontrollen. Android spårar routing per nätverk och per app, så samma prefix kan gå genom tunneln för en app och direkt för en annan beroende på hur VPN:en deklarerades.
Placera DNS- och per-app-regler där de hör hemma
Ruttundantag ändrar inte resolvern. Ett lokalt värdnamn kan fortfarande besvaras av tunnelns DNS-server medan dess adress routas lokalt, och avvikelsen visar sig som ett namn som löses upp till något som LANet inte når. Sätt resolverpolicyn medvetet, med en sökdomän eller en regel för delad DNS. Anta inte att rutterna avgjorde det åt dig.
addDisallowedApplication() håller en app utanför tunneln; det håller inte en destination utanför för de appar som stannar inne. protect() låter en socket du öppnar kringgå tunneln, och så undviker en tunnel att fånga sin egen transport. Ingen av dem ersätter komplementet.
Verifiera med ett uppslag, inte med din egen lista
När tunneln är uppe, fråga enheten vart en adress faktiskt tar vägen i stället för att läsa listan du byggde igen. På en build som levererar iproute2 namnger adb shell ip route get 10.1.2.3 gränssnittet som kärnan valde. Kör samma kommando för en offentlig adress och bekräfta att det namnger tunneln.
Två kontroller till sluter kretsen. Slå först upp ett lokalt namn, kör sedan adb shell ip route get på den uppslagna adressen och kräv ett gränssnitt som inte är tunneln, eftersom ett lyckat adb shell ping -c 1 host.lan bevisar nåbarhet och inte vägen. Upprepa adressfamiljetestet över IPv6 om tunneln kräver en. Testa på den API-nivå du levererar, eftersom hur VPN-nätverk exponeras mot routingtabellen har ändrats mellan utgåvor. En ruttlista som ser rätt ut på papperet och ett uppslag som väljer fel gränssnitt är olika buggar, och bara den andra når användarna.
Om undantagslistan fortsätter växa, sluta generera rutter och numrera om fjärrnätverket eller smalna av tunneln i stället. Generatorn är en kringgång för det saknade API:et, inte ett designverktyg.