Android VpnService се свързва, но пакетите не стигат до мрежата
Тунелът е вдигнат, а страниците все още не се отварят: проверете режима на четене, защитата на сокета, поддръжката на TCP и списъка с маршрути, преди да обвините VPN сървъра.
Тунелът се вдига, иконата с ключ се появява, а всяка заявка в браузъра все още изтича. В приложение с VpnService тази комбинация обикновено означава, че цикълът за четене е жив, а изходната половина е счупена, а не че сървърът е отхвърлил ръкостискането. Android вече е създал интерфейса, преместил е трафика на приложението в него и е дал на процеса ви файлов дескриптор. Всичко след този дескриптор е ваш код и едно липсващо извикване е достатъчно да погълне целия поток.
Какво ви дава системата и какво не
establish() връща ParcelFileDescriptor за интерфейс tun. Системата записва изходящи IP пакети в него и очаква процесът ви да ги прочете, да реши накъде да отиват и да ги изпрати през обикновен сокет. Под това няма препращане в ядрото, затова когато никой не чете, пакетите се изхвърлят и броячите на интерфейса никога не се движат.
Започнете с броене вместо с догадки. Увеличавайте брояч при всяко успешно четене и записвайте дестинацията и протокола на първите няколко пакета. Ако броячът остане нула, докато се генерира трафик, повредата е преди кода ви за препращане, което стеснява търсенето до конфигурацията на builder и режима на четене. Ако броячът се движи, а браузърът пак се проваля, повредата е в пътя на изпращане или на връщане.
Блокиращото четене решава дали цикълът се движи
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 определят какво системата изпраща в тунела. Дестинация извън списъка с маршрути никога не достига до вашия дескриптор, затова никакво количество логика за препращане няма да я оправи. Записвайте извикванията на builder, които правите, и ги сравнявайте с адресите, до които приложението наистина се опитва да достигне.
Изборът по приложения има свой под: addAllowedApplication и addDisallowedApplication изискват API 21, а в по-стари версии методите просто липсват. Дизайн, който филтрира по пакет, там изобщо не може да работи.
Повторното установяване оставя стари дескриптори
Повторното извикване на establish() връща нов дескриптор и затваря предишния интерфейс. Старият дескриптор вече е невалиден, затова нишка, която още блокира на него, получава грешка, а сокетите, създадени за стария път, може да се нуждаят от замяна. onRevoke е обратното извикване за тунел, който потребителят или системата са отнели. Задържането на жизнения цикъл у един собственик, който спира четеца преди ново установяване, предотвратява повечето от състоянията свързан-но-мъртъв.
Четете всяка грешка по стъпката, към която принадлежи. Нулиране на връзка от recvfrom означава, че отдалеченият край се е затворил, докато кодът ви още е чел. Грешка за лош адрес сочи към разбор на заглавка или бъг в отместване, а не към мрежата.
Ред на работа: блокиращ режим включен, един пакет на четене, заглавката разбрана, сокетът защитен, полезният товар изпратен, чисто затваряне при предаване и списък с маршрути, който покрива дестинацията.