Выяўленне VPN на Android: чаму tun0 і ping уводзяць у зман
Назва інтэрфейса — гэта ўмоўнасць; ping даказвае, што адзін хост быў дасягальны. Спытайце NetworkCapabilities, што дэкларуе сетка, і рэагуйце на зваротныя выклікі.
Праверка, якую большасць праграм для Android выкарыстоўваюць для выяўлення VPN, — гэта назва інтэрфейса, і яна ніколі не была праверкай VPN. Яна паведамляе, што існуе нейкі інтэрфейс з радком, які чакаў распрацоўшчык, што на адной прыладзе азначае OpenVPN, а на іншай — нешта зусім не звязанае. Ping — тое ж самае сведчанне: ён даказвае, што нейкае прызначэнне адказала, а не тое, што пакет прайшоў праз тунель. Тое, што платформа фактычна прадстаўляе, — гэта набор сетак і транспартаў, якія кожная з іх дэкларуе, і гэта іншае пытанне з іншым адказам.
Міф: калі tun0 існуе, значыць VPN працуе
Праграмы VPN для Android — гэта рэалізацыі VpnService, якія перадаюць сістэме дэскрыптар файла tun. Назва інтэрфейса залежыць ад таго, хто яго стварыў: OpenVPN звычайна стварае tun0, PPTP і L2TP ствараюць ppp0, WireGuard называе інтэрфейс па імені тунеля, а тунель IPsec можа наогул не мець звычайнай назвы. Таму чытанне /proc/net/dev або пералічэнне аб’ектаў NetworkInterface шукае ўмоўнасць, а не факт.
Абодва кірункі памылкі жывыя. Прылада можа мець tun0 для лакальнага тунеля, які не мае нічога агульнага з VPN карыстальніка, а цалкам здаровы тунель WireGuard можа быць нябачным для праверкі, якая ведае толькі радок tun0. Ілжывыя станоўчыя і ілжывыя адмоўныя ў адной функцыі робяць яе непрыдатнай для дыягностыкі, бо тыкет падтрымкі з запісам «VPN не выяўлены» нельга давяраць ні ў якім кірунку.
Доступ таксама звузіўся. Новыя выпускі Android абмяжоўваюць тое, што звычайная праграма можа чытаць з /proc, таму праверка на аснове назвы, якая працавала на Android 8, можа вярнуць пусты спіс на пазнейшай прыладзе без змены кода. Калі праверка ціха памыляецца ў адкрыты бок, праграма робіць выснову, што VPN няма на абсталяванні, дзе тунель спраўны.
Міф: паспяховы ping азначае, што тунель перадае трафік
Ping да ўнутранага хоста ўдаецца кожны раз, калі існуе маршрут да гэтага хоста. Пры раздзеленым тунеляванні хост можа быць дасягальны праз Wi-Fi, пакуль тунель уключаны і робіць зусім іншае. Пры VPN для кожнай праграмы праграма, якая выконвае ping, можа быць выключана з тунеля, таму ICMP-рэха выходзіць па-за яго і вяртаецца, што даказвае процілеглае таму, што мяркуе праверка.
ICMP і TCP таксама фільтруюцца асобна. Тунель, які дазваляе запыты рэха, але адкідвае TCP-порт, патрэбны сэрвісу, пройдзе тэст ping і праваліць праграму, і гэта правіла брандмаўэра на іншым баку, а не ўласцівасць прылады. Адзінае сумленнае прачытанне паспяховага ping — гэта тое, што гэта прызначэнне адказала гэтаму пратаколу ў гэты момант.
Што платформа фактычна паведамляе
Пачынаючы з API 21, ConnectivityManager.getAllNetworks() вяртае кожную сетку, вядомую сістэме, а getNetworkCapabilities(network) паведамляе, што гэта сетка дэкларуе. Сетка VPN нясе TRANSPORT_VPN. Перабор усіх сетак адказвае на адно пытанне: ці ёсць на гэтай прыладзе зараз якая-небудзь VPN?
Другое, больш вузкае пытанне патрабуе іншага выкліку. getActiveNetwork() вяртае сетку, якую трафік праграмы выкарыстаў бы зараз, і праверка той жа магчымасці на ёй адказвае: ці пойдзе мой трафік праз VPN? Абодва могуць разыходзіцца, і гэта разыходжанне інфарматыўнае. VPN, якая ўключана, але не актыўная, паказвае на маршрутызацыю для кожнай праграмы або на канфігурацыю раздзеленага тунеля, а не на адсутны тунель.
Network active = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(active);
boolean viaVpn = caps != null
&& caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
Ахоўвайце абодва выклікі. Магчымасці могуць быць null для сеткі, якая знікла паміж пералічэннем і запытам, а актыўная сетка можа быць null падчас перадачы. NET_CAPABILITY_NOT_VPN карысная ў адваротным кірунку — каб пацвердзіць, што сетка дакладна не тунель, а не проста пазбаўлена сцяга транспарту VPN.
NetworkInfo быў тым жа міфам з API
ConnectivityManager.TYPE_VPN з NetworkInfo — гэта другое пакаленне гэтай праверкі, і яно састарэла нездарма. У сучасных выпусках аб’ект, які вяртаецца для VPN, можа быць null, пакуль тунель падключаны, таму код, які трактуе null як «VPN няма», ціха паведамляе ілжывы адмоўны вынік на тых прыладах, дзе адказ найбольш важны.
Шырокавяшчальны сигнал, які звычайна яго суправаджае, не лепш. CONNECTIVITY_CHANGE мае дакументаваную гісторыю пропуску пераходаў VPN, уключаючы зборкі, дзе ён спрацоўваў толькі пры падключэнні або адключэнні VPN, а не пры кожнай змене сеткі. Слухач, пабудаваны на ім, стварае зменную стану, якая адхіляецца ад погляду платформы да наступнага поўнага перападключэння.
Рэагуйце на зваротныя выклікі замест апытання
Зарэгіструйце NetworkRequest, які патрабуе TRANSPORT_VPN і выдаляе NET_CAPABILITY_NOT_VPN, затым выклічце registerNetworkCallback з гэтым запытам. Тады onAvailable і onLost паведамляюць, калі дэкларацыя змяняецца, і гэта менавіта тая падзея, якая вас цікавіць. Адмяніце рэгістрацыю зваротнага выкліку, калі кампанент спыняецца, інакш праграма трымае спасылку на мёртвы экран, і зваротны выклік працягвае спрацоўваць.
NetworkRequest req = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build();
cm.registerNetworkCallback(req, callback);
Для гэтага патрэбны ACCESS_NETWORK_STATE — звычайны дазвол: без root, без дыялога згоды карыстальніка, без дазволу VPN. Апытанне па таймеры — гэта альтэрнатыва, да якой людзі звяртаюцца, і яно спальвае батарэю, але ўсё роўна прапускае пераход, карацейшы за інтэрвал, а гэта менавіта той пераход, пра які паведамляе карыстальнік.
Што адказ па-ранейшаму не можа вам сказаць
Ніводны з гэтых API не паведамляе, ці здаровы тунель, на якім серверы ён завяршаецца і ці знаходзіцца трафік пэўнай праграмы ўнутры яго. Сетка VPN можа існаваць без прыдатных маршрутаў, а тунель для кожнай праграмы можа выключыць праграму, якая задае пытанне.
Таму запісвайце тое, што бачыла праверка, а не толькі boolean. Запішыце ідэнтыфікатар сеткі, дэклараваныя транспарты і тое, ці была гэта сетка актыўнай. Справаздача з запісам «VPN не выяўлены» непрыдатная, а вось тая, што кажа: сетка дэкларавала TRANSPORT_VPN, але getActiveNetwork() вярнула Wi-Fi, — непасрэдна паказвае на маршрутызацыю або на спіс выключэнняў, і гэта выпраўленне, з якім можна працаваць.