Як выключыць падсетку з VPN-тунэля на Android
VpnService не можа выключыць маршрут, таму вы дадаяце дапаўненне: восем згенераваных прэфіксаў для /8, плюс праверкі IPv6, DNS і пошуку, якія ловяць астатняе.
У Android VpnService не мае маршруту выключэння, і ніякае чытанне 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 патрабуе сваё дапаўненне, згенераванае той жа арыфметыкай на 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, а не інструмент дызайну.