Android VpnService падключаецца, але пакеты не даходзяць да сеткі
Тунэль узняты, а старонкі ўсё яшчэ не адкрываюцца: праверце рэжым чытання, абарону сокета, падтрымку TCP і спіс маршрутаў, перш чым вінаваціць VPN-сервер.
Тунэль узнімаецца, значок ключа з'яўляецца, а кожны запыт у браўзеры ўсё яшчэ сканчаецца тайм-аўтам. У праграме VpnService такое спалучэнне звычайна азначае, што цыкл чытання жывы, а выхадная палова зламаная, а не тое, што сервер адхіліў рукапоціск. Android ужо стварыў інтэрфейс, перамясціў у яго трафік праграмы і аддаў вашаму працэсу дэскрыптар файла. Усё пасля гэтага дэскрыптара — ваш код, і аднаго адсутнага выкліку дастаткова, каб праглынуць увесь паток.
Што сістэма дае вам, а што не
establish() вяртае ParcelFileDescriptor для інтэрфейса tun. Сістэма запісвае ў яго зыходныя IP-пакеты і чакае, што ваш працэс прачытае іх, вырашыць, куды яны ідуць, і адправіць іх праз звычайны сокет. Ніякай перадачы ў ядры пад гэтым няма, таму калі ніхто не чытае, пакеты скідаюцца, а лічыльнікі інтэрфейса ніколі не рухаюцца.
Пачніце не з здагадак, а з падліку. Павялічвайце лічыльнік пры кожным удалым чытанні і запісвайце прызначэнне і пратакол першых некалькіх пакетаў. Калі лічыльнік застаецца на нулі, пакуль трафік генеруецца, няспраўнасць вышэй за ваш код перасылкі, што звужвае пошук да канфігурацыі будаўніка і рэжыму чытання. Калі лічыльнік рухаецца, а браўзер усё яшчэ падае, няспраўнасць у шляху адпраўкі або на зваротным шляху.
Блакуючае чытанне вырашае, ці рухаецца цыкл
setBlocking(true) пераводзіць дэскрыптар у блакуючы рэжым і існуе з часоў API 21. У старых версіях абыход — невялікі памочнік JNI, які ачышчае O_NONBLOCK праз fcntl на сырым дэскрыптары. Рэжым важны, бо менавіта ён вызначае, што дазволена рабіць чытанню без чаканага пакета.
У неблакуючым рэжыме такое чытанне вяртаецца адразу з адмоўным значэннем, і цыкл, які ігнаруе вынік, круціць ядро, не перамяшчаючы ніводнага байта трафіку. У блакуючым рэжыме чытанне чакае, чаго і хоча паток перасылкі, але паток тады ўжо нельга перапыніць. Зачыненне дэскрыптара — падтрыманы спосаб прымусіць чытанне пацярпець няўдачу, каб цыкл мог выйсці. Забойства працэсу або перапыненне патоку замест гэтага можа пакінуць наступны запуск з дэскрыптарам, які больш не адпавядае інтэрфейсу.
Чытайце адзін пакет за выклік у буфер памеру MTU інтэрфейса, і лічыце нуль або адмоўны вынік памылкай, а не пустым пакетам.
protect() да першай адпраўкі, а не пасля яе
Кожны сокет, які нясе інкапсуляваны пакет, спачатку трэба абараніць. Незабяспечаны сокет падпарадкоўваецца тым жа правілам маршрутызацыі, якія вы толькі што ўсталявалі, таму яго пакеты вяртаюцца ў тунэль, і ваш уласны цыкл чытання падхоплівае іх ізноў. Тады тунэль перасылае ўласны выхад бясконца, і звонку гэта выглядае як злучэнне, якое адкрываецца і ніколі не адказвае.
Парадак такі: protect, затым connect або bind, затым send. Выклік protect пасля connect можа пакінуць ранейшае рашэнне аб маршруце, таму сокет працягвае выкарыстоўваць няправільны шлях, нават калі выклік вярнуў true. Праверце лагічнае значэнне, якое ён вяртае, і гучна памыляйцеся, калі яно false, замест таго каб ісці да адпраўкі, якая не можа працаваць.
Памылка правоў ад sendto на сокеце, які ніколі не кранаў protect(), — звычайная прыкмета гэтай памылкі.
Прыклад UDP не нясе HTTPS
Прыклад перасылкі, які капіруюць большасць падручнікаў, апрацоўвае UDP і больш нічога. TCP патрабуе рукапоціску, парадкавых нумароў, пацверджанняў і паўторнай перадачы, а цыкл, які капіруе карысную нагрузку паміж дэскрыптарам tun і сокетам, не дае нічога з гэтага. Таму запыт DNS праз UDP можа прайсці, пакуль кожны запыт HTTPS стаіць. Або запусціце стэк TCP у прасторы карыстальніка, або аддайце злучэнні лакальнаму проксі, які завяршае TCP на прыладзе і перасылае паток.
Праверце падзел, перш чым нешта перапісваць: адпраўце адзін запыт UDP і адкрыйце адно злучэнне TCP да таго ж хоста. Калі шлях UDP працуе, а шлях TCP вісіць на першым SYN, праблема ў кодзе перасылкі, а не ў серверы. Гэтае адно параўнанне эканоміць шмат часу, які марнуецца на чытанне серверных журналаў, што не ўтрымліваюць нічога карыснага.
Спіс маршрутаў і фільтры па праграмах абмяжоўваюць тое, што вы бачыце
addAddress, addRoute і addDns вызначаюць, што сістэма адпраўляе ў тунэль. Прызначэнне па-за спісам маршрутаў ніколі не дасягае вашага дэскрыптара, таму ніякая колькасць логікі перасылкі гэтага не выправіць. Запісвайце выклікі будаўніка, якія робіце, і параўноўвайце іх з адрасамі, да якіх праграма сапраўды спрабуе дасягнуць.
Выбар па праграмах мае сваю мяжу: addAllowedApplication і addDisallowedApplication патрабуюць API 21, а ў старых версіях гэтыя метады проста адсутнічаюць. Дызайн, які фільтруе па пакету, там увогуле не можа працаваць.
Паўторнае ўзняцце пакідае састарэлыя дэскрыптары
Паўторны выклік establish() вяртае новы дэскрыптар і зачыняе папярэдні інтэрфейс. Стары дэскрыптар цяпер несапраўдны, таму паток, які ўсё яшчэ блакуецца на ім, атрымлівае памылку, а сокеты, створаныя для старога шляху, магчыма, давядзецца замяніць. onRevoke — гэта зваротны выклік для тунэля, які забралі карыстальнік або сістэма. Утрыманне жыццёвага цыклу ў аднаго ўладальніка, які спыняе чытача перад паўторным узняццем, прадухіляе большасць станаў «падключана, але мёртва».
Чытайце кожную памылку па яе кроку. Скід злучэння ад recvfrom азначае, што далёкі бок зачыніўся, пакуль ваш код яшчэ чытаў. Памылка дрэннага адраса паказвае на разбор загалоўка або памылку зруху, а не на сетку.
Парадак працы: блакуючы рэжым уключаны, адзін пакет за чытанне, загаловак разабраны, сокет абаронены, нагрузка адпраўлена, чыстае зачыненне пры перадачы, і спіс маршрутаў, які пакрывае прызначэнне.