Како искључити подмрежу из Android VPN тунела
VpnService не може да искључи руту, па додајете комплемент: осам генерисаних префикса за /8, плус провере IPv6, DNS и претраге које хватају остатак.
VpnService на Android-у нема руту за искључивање, и ниједно читање API-ја је неће створити. Builder.addRoute() увек само додаје одредишта у тунел, тако да задржавање LAN сегмента локалним значи да тај сегмент никада не улази у листу рута. Оно што уместо тога додајете је комплемент сегмента, изражен обичним префиксима. Посао је аритметика, и он пада на неколико предвидивих места.
Прођите провере испод по реду. Свака претпоставља да је претходна издржала, зато станите на првом неуспеху уместо да крпите симптоме ниже.
Почните од онога што тунел тврди
Прослеђивање 0.0.0.0/0 у addRoute() говори Android-у да свако одредиште IPv4 припада тунелу. Зато толико клијената изгледа неспособно за подељено тунелирање: траже цео адресни простор и затим покушавају да одузму од њега. Ништа у градитељу не одузима. Конкретнија рута за мрежу коју сте хтели да задржите локалном чини супротно од очекиваног, јер најдужи префикс који се поклапа побеђује и та мрежа завршава унутар тунела.
Листа рута је цела политика. Иза ње нема вета по одредишту, и референца Builder не наводи ниједан метод који би га обезбедио.
Генеришите комплемент из аритметике префикса
Комплемент искљученог префикса је скуп његове браће и сестара на путу ка корену стабла адреса. За сваку дужину од 1 до искључене дужине узмите блок те дужине који садржи ваш префикс, обрните бит непосредно испод префикса и испишите резултат. Звучи апстрактно док га не покренете.
Искључивање 10.0.0.0/8 даје осам рута: 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. Ниједна од њих не садржи 10.0.0.0/8, а заједно покривају све остало.
Осам редова генерише ту листу за било који префикс:
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))
Број рута једнак је дужини префикса, и то је број који треба пратити. Једно искључивање /21 кошта 21 руту и остаје читљиво. Десет искључивања може да кошта неколико стотина, што обично значи да треба мењати дизајн мреже, а не генератор.
Проверите породицу адреса и обим тунела
IPv4 руте не чине ништа за IPv6. Ако тунел тражи IPv6 адресу, локалном IPv6 LAN префиксу потребан је сопствени комплемент, генерисан истом аритметиком над 128 бита. Чест разлог зашто искључивање ради на папиру док локални уређај остаје недоступан је овај: до хоста се стигло преко IPv6, а исечен је само IPv4. Проверите обе породице пре него што закључите да је математика погрешна.
Обим је друга половина провере. Android прати рутирање по мрежи и по апликацији, тако да исти префикс може ићи кроз тунел за једну апликацију и директно за другу, зависно од тога како је VPN декларисан.
Поставите правила за DNS и по апликацији тамо где припадају
Искључивања рута не мењају резолвер. Локално име хоста и даље може да одговори DNS сервер тунела док се његова адреса рутира локално, а неслагање се показује као име које се разрешава у нешто до чега LAN не може да стигне. Подесите политику резолвера намерно, са доменом претраге или правилом подељеног DNS-а. Не претпостављајте да су руте то одлучиле уместо вас.
addDisallowedApplication() држи једну апликацију изван тунела; не држи одредиште изван за апликације које остају унутра. protect() дозвољава сокету који отворите да заобиђе тунел, и тако тунел избегава хватање сопственог транспорта. Ниједно од тога не замењује комплемент.
Проверите претрагом, не сопственом листом
Када је тунел подигнут, питајте уређај куда адреса заиста иде уместо да поново читате листу коју сте саставили. На билду који доставља iproute2, adb shell ip route get 10.1.2.3 именује интерфејс који је изабрало језгро. Покрените исту команду за јавну адресу и потврдите да именује тунел.
Још две провере затварају петљу. Прво разрешите локално име, затим покрените adb shell ip route get на разрешеној адреси и захтевајте интерфејс који није тунел, јер успешан adb shell ping -c 1 host.lan доказује достижност, а не пут. Поновите тест породице адреса преко IPv6 ако тунел тражи једну. Тестирајте на нивоу API-ја који достављате, јер се начин на који се VPN мреже излажу табели рутирања мењао између издања. Листа рута која на папиру изгледа исправно и претрага која бира погрешан интерфејс су различите грешке, и само друга стиже до корисника.
Ако листа искључивања настави да расте, престаните да генеришете руте и пренумеришите удаљену мрежу или уместо тога сузите тунел. Генератор је заобилажење за API који недостаје, а не алат за дизајн.