Чатыры здагадкі пра Android VpnService аднойчы каштавалі мне большай часткі тыдня, і ні адна з іх не датычылася сеткавага пратаколу. Праграма казала, што злучэнне ёсць, значок ключа вісеў у радку стану, а трафік усё роўна ішоў звычайным маршрутам. Я ўвесь час меркаваў, што тунель мой, пакуль я яго не спыню, што дададзеныя мной маршруты і ёсць атрыманыя маршруты, і што выклік stopSelf забярэ інтэрфейс з сабой. Сэрвіс паводзіў сябе дакладна так, як апісана ў дакументацыі. Зламанай часткай была мая ўяўная мадэль.

Міф 1: тунель трымаецца, пакуль вы яго не спыніце

Android трымае адзін інтэрфейс VPN адначасова. Стварыце другі, і платформа не паставіць яго ў чаргу, а дэзактывуе ваш і выкліча onRevoke() у вашым сэрвісе. Калі ваш код зносіць усё толькі па націску кнопкі, перахоп пакідае дэскрыптар і паток чытання, і наступны запуск атрымлівае ў спадчыну абодва.

Даведнік VpnService прама кажа, што prepare() вяртае null, калі праграма ўжо мае згоду, і Intent, калі карыстальнік яшчэ павінен яе пацвердзіць. Гэтая згода прыйдзе толькі тады, калі вы перадасце намер праз startActivityForResult(). Прапусціце перадачу, і establish() верне null, што выглядае як ціхі збой, а не як праблема з правамі.

Always-on VPN дадае ўладальніка, якім вы не кіруеце. Сістэма можа вярнуць тунель пасля загрузкі або пасля збою, і яна перахапляе праграму VPN, якую карыстальнік запусціў уручную. Будуйце onRevoke() так, каб ён закрываў тыя ж рэсурсы, што і звычайнае спыненне, і стаўцеся да яго як да асноўнага шляху спынення, а не як да рэдкага выпадку. Карыстальнікі мяняюць праграмы VPN; ваш код павінен гэта перажыць.

Міф 2: addRoute перахоплівае менавіта тое, што вы пералічылі

Маршруты вызначаюць, якія пакеты дасягаюць вашага дэскрыптара, і памылка ў абсягу стварае знясільваючы выпадак, калі частка трафіку ідзе праз тунель, а частка не. addRoute("0.0.0.0", 0) бярэ ўвесь IPv4, а значыць, кожны пакет цяпер прыходзіць да вас і павінен быць перададзены або адкінуты. /32 для аднаго хоста не бярэ амаль нічога, і ўсё астатняе паводзіць сябе так, нібы VPN ніколі не ставілі.

Сямействы лічацца асобна. Маршрут толькі для IPv4 пакідае IPv6 у базавай сетцы, таму двухстэкавы хост можа ўсё яшчэ разрозніваць і злучацца па-за тунелем, выглядаючы цалкам злучаным унутры яго.

DNS ідзе па той жа логіцы. Серверы, дададзеныя праз addDnsServer(), трапляюць у праграмы, якія карыстаюцца рэзалверам тунеля, а праграма з уласнай шыфраванай канчатковай кропкай DNS або з Private DNS, зададзеным як імя хоста, цалкам іх ігнаруе. Калі імёны не разрозніваюцца для адной праграмы і працуюць для другой, праверце шлях рэзалвера, перш чым чапаць хоць адзін маршрут.

Міф 3: правілы па праграмах — гэта перамыкач, які можна перавярнуць у любы бок

addDisallowedApplication() і addAllowedApplication() — гэта два рэжымы, а не два фільтры. Будаўнік прымае адну мадэль на тунель, таму праграма, якая хоча ўсё, акрамя трох пакетаў, выкарыстоўвае забаронены спіс і пакідае дазволены спіс пустым. Гэтыя два ўзаемавыключальныя, і людзі даведваюцца пра гэта ў час выканання пасля таго, як экран налад ужо прапанаваў абодва перамыкачы.

Бачнасць пакетаў таксама кусаецца. На Android 11 і пазней вы можаце назваць толькі тыя пакеты, якія бачыце, таму спіс выключэнняў, пабудаваны з запыту, які вашай праграме не дазволена выконваць, ціха прапусціць запісы. Параўнайце спіс з тым, што насамрэч вяртае кіраўнік пакетаў, перш чым яму давяраць.

Працоўныя профілі зноў дзеляць аўдыторыю. Тунель, запушчаны ўнутры профілю, пакрывае гэты профіль і яго праграмы, а асабісты бок захоўвае ўласную сувязь. Гэта рашэнне пра абсяг, прынятае аднойчы, і яго месца ў інтэрфейсе, а не ў заяўцы ў падтрымку.

Міф 4: stopSelf завяршае тунель

Не завяршае. Інтэрфейс жыве ў дэскрыптары, які вяртае establish(), і аб'ект сэрвісу можа памерці, пакуль гэты дэскрыптар застаецца адкрытым. Спачатку закрыйце ParcelFileDescriptor, адпусціце спасылку, далучыце або перапыніце патокі чытання і запісу, затым выклічце stopForeground(true) і stopSelf() менавіта ў гэтым парадку.

Забыты дэскрыптар — звычайная прычына таго, што сэрвіс нібы перажывае ўласны запыт на спыненне, і гэта тлумачыць састарэлы маршрут, які жыве даўжэй за праграму, што яго дадала.

Адваротная памылка сустракаецца гэтак жа часта: дэскрыптар закрываюць, пакуль паток чытання ў сярэдзіне чытання, а затым пакідаюць гэты паток апрацоўваць выключэнне, для якога ніхто не пісаў апрацоўшчыка. Трымайце паслядоўнасць спынення ў адным месцы, і хай і дзеянне карыстальніка, і onRevoke() выклікаюць яе.

Што правяраць, калі тунель выглядае мёртвым

Абараніце доўгажывучыя сокеты праз protect() да таго, як тунель узнімецца. Без гэтага ўласны транспарт тунеля ізноў накіроўваецца ў тунель, і злучэнне застойваецца гэтак, што выглядае дакладна як збой сервера. Тэстуйце супраць адрасу, які існуе толькі за аддаленай сеткай, а не публічнага сайта, што адказвае з кэша або з базавага шляху.

Запісвайце вынік establish(), маршруты, выключаныя пакеты і прычыну спынення. Гэтыя чатыры радкі аддзяляюць праблему згоды ад праблемы абсягу і ад праблемы жыццёвага цыклу без перахопу пакетаў.

Падобныя артыкулы