Тунел се подиже, икона кључа се појављује, а сваки захтев у прегледачу и даље истиче. У VpnService апликацији та комбинација обично значи да је петља читања жива, а излазна половина покварена, а не да је сервер одбио руковање. Android је већ створио интерфејс, преместио саобраћај апликације у њега и дао вашем процесу датотечни дескриптор. Све после тог дескриптора је ваш код, а један пропуштен позив довољан је да прогута цео ток.

Шта вам систем даје, а шта не

establish() враћа ParcelFileDescriptor за tun интерфејс. Систем уписује одлазне IP пакете у њега и очекује да их ваш процес прочита, одлучи куда иду и пошаље их кроз обичан сокет. Испод нема прослеђивања у језгру, па када нико не чита, пакети се одбацују и бројачи интерфејса се никада не померају.

Почните бројањем уместо нагађањем. Повећајте бројач при сваком успешном читању и забележите одредиште и протокол првих пакета. Ако бројач остане на нули док се саобраћај генерише, грешка је изнад вашег кода за прослеђивање, што сужава претрагу на конфигурацију builder-а и режим читања. Ако се бројач помера, а прегледач и даље не ради, грешка је у путу слања или на повратном путу.

Блокирајуће читање одлучује да ли се петља помера

setBlocking(true) ставља дескриптор у блокирајући режим и постоји од API 21. На старијим издањима решење је мали JNI помоћник који брише O_NONBLOCK помоћу fcntl на сировом дескриптору. Режим је важан јер одлучује шта читању без пакета на чекању сме да ради.

У неблокирајућем режиму то читање се одмах враћа са негативном вредношћу, а петља која игнорише резултат врти језгро без иједног пренесеног бајта саобраћаја. У блокирајућем режиму читање чека, што нит за прослеђивање и жели, али нит тада више не може бити прекинута. Затварање дескриптора је подржани начин да читање не успе како би петља могла да изађе. Убијање процеса или прекидање нити уместо тога може оставити следеће покретање са дескриптором који више не одговара интерфејсу.

Читајте један пакет по позиву у бафер величине MTU интерфејса, а нулу или негативан повратак третирајте као грешку, а не као празан пакет.

protect() пре првог слања, не после

Сваки сокет који носи инкапсулирани пакет мора прво бити заштићен. Незаштићени сокет слуша иста правила рутирања која сте управо поставили, па се његови пакети враћају у тунел и ваша сопствена петља читања их поново преузима. Тунел тада бесконачно прослеђује сопствени излаз, а споља то изгледа као веза која се отвара и никада не одговара.

Редослед је protect, па connect или bind, па send. Позив protect после connect може оставити ранију одлуку о рути, па сокет наставља да користи погрешан пут иако је позив вратио true. Проверите boolean који враћа и гласно не успејте када је false, уместо да уђете у слање које не може да ради.

Грешка дозволе из sendto на сокету који никада није дотакао protect() уобичајен је потпис ове грешке.

UDP пример не носи HTTPS

Пример прослеђивања који већина упутстава копира обрађује UDP и ништа друго. TCP захтева руковање, редне бројеве, потврде и поновно слање, а петља која копира терет између tun дескриптора и сокета не пружа ништа од тога. Зато UDP DNS упит може да прође док сваки HTTPS захтев застаје. Или покрените TCP стек у корисничком простору или предајте везе локалном proxy-ју који завршава TCP на уређају и прослеђује ток.

Тестирајте поделу пре него што било шта препишете: пошаљите један UDP упит и отворите једну TCP везу ка истом хосту. Ако UDP пут ради, а TCP пут виси на првом SYN, проблем је у коду за прослеђивање, а не у серверу. То једно поређење штеди много времена проведеног у читању серверских дневника који не садрже ништа корисно.

Листа рута и филтери по апликацији ограничавају шта видите

addAddress, addRoute и addDns дефинишу шта систем шаље у тунел. Одредиште изван листе рута никада не стиже до вашег дескриптора, па никаква количина логике прослеђивања то неће поправити. Бележите builder позиве и упоредите их са адресама којима апликација заиста покушава да приступи.

Избор по апликацији има свој праг: addAllowedApplication и addDisallowedApplication захтевају API 21, а на старијим издањима тих метода једноставно нема. Дизајн који филтрира по пакету тамо уопште не може да ради.

Поновно успостављање оставља застареле дескрипторе

Поновни позив establish() враћа нови дескриптор и затвара претходни интерфејс. Стари дескриптор је сада неважећи, па нит која још блокира на њему добија грешку, а сокети створени за стари пут можда морају да се замене. onRevoke је повратни позив за тунел који су корисник или систем одузели. Држање животног циклуса код једног власника, који зауставља читача пре поновног успостављања, спречава већину стања повезано-али-мртво.

Читајте сваку грешку према кораку којем припада. Ресет везе из recvfrom значи да се удаљена страна затворила док је ваш код још читао. Грешка лоше адресе упућује на парсирање заглавља или грешку помераја, не на мрежу.

Редослед рада: блокирајући режим укључен, један пакет по читању, заглавље парсирано, сокет заштићен, терет послат, чисто затварање при предаји и листа рута која покрива одредиште.

Повезани чланци