Android VpnService холбогддог ч пакет сүлжээнд хүрдэггүй
Туннель ажиллаж байгаа ч хуудсууд нээгдэхгүй: VPN серверийг буруутгахаас өмнө унших горим, socket хамгаалалт, TCP дэмжлэг, маршрутын жагсаалтыг шалгана уу.
Туннель босч, түлхүүрийн дүрс гарч, гэхдээ хөтчийн бүх хүсэлт хугацаа хэтэрч байна. VpnService апп-д энэ хослол ихэвчлэн унших давталт амьд, гаралтын тал эвдэрсэн гэсэн үг, сервер гар барилтыг татгалзаагүй. Android интерфейсийг аль хэдийн үүсгэж, аппын траффикийг түүн рүү шилжүүлж, процессд тань файлын дескриптор өгсөн. Тэр дескрипторийн дараах бүхэн таны код, нэг дутуу дуудалт бүх урсгалыг залгихад хангалттай.
Систем юу өгдөг, юу өгдөггүй
establish() нь tun интерфейсийн ParcelFileDescriptor буцаана. Систем гарч буй IP пакетуудыг түүн рүү бичиж, таны процесс тэдгээрийг уншиж, хаашаа явахыг шийдэж, энгийн socket-ээр илгээнэ гэж хүлээдэг. Доор нь цөмийн дамжуулалт байхгүй тул хэн ч уншихгүй бол пакетууд хаягдаж, интерфейсийн тоолуур хэзээ ч хөдлөхгүй.
Таамаглахын оронд тоолж эхэл. Амжилттай унших бүрт тоолуурыг нэмж, эхний пакетуудын чиглэл, протоколыг бүртгэ. Траффик үүсгэж байхад тоолуур тэг хэвээр байвал алдаа нь таны дамжуулах кодоос дээгүүр, энэ нь хайлтыг builder тохиргоо, унших горимд хумина. Тоолуур хөдөлж, хөтч нөгөөтэйгээр бүтэлгүйтвэл алдаа нь илгээх эсвэл буцах замд.
Блоклох уншилт давталт хөдөлнө эсэхийг шийднэ
setBlocking(true) дескрипторыг блоклох горимд оруулж, API 21-ээс хойш байдаг. Хуучин хувилбарт шийдэл нь fcntl-ээр түүхий дескриптороос O_NONBLOCK-ыг арилгадаг жижиг JNI туслах юм. Горим чухал, учир нь хүлээж буй пакетгүй уншилт юу хийж болохыг тэр шийднэ.
Блоклохгүй горимд тэр уншилт шууд сөрөг утгаар буцаж, үр дүнг үл тоомсорлосон давталт траффик хөдөлгөхгүйгээр цөмийг эргэлдүүлнэ. Блоклох горимд уншилт хүлээнэ, энэ нь дамжуулах thread-д хэрэгтэй, гэхдээ дараа нь thread-ийг таслах боломжгүй. Дескрипторыг хаах нь уншилтыг бүтэлгүй болгох дэмжигдсэн арга бөгөөд давталт гарч чадна. Процессыг алах эсвэл thread-ийг таслах нь дараагийн эхлэлд интерфейст тохирохгүй дескриптор үлдээж болно.
Дуудалт бүрт интерфейсийн MTU хэмжээтэй буферт нэг пакет уншиж, тэг эсвэл сөрөг буцаалтыг хоосон пакет биш, алдаа гэж үз.
protect() эхний илгээлтийн өмнө, дараа нь биш
Инкапсуллагдсан пакет зөөдөг socket бүр эхлээд хамгаалагдсан байх ёстой. Хамгаалагдаагүй socket таны саяхан суулгасан маршрутын дүрмийг дагадаг тул пакетууд нь туннель рүү буцаж, таны унших давталт тэдгээрийг дахин авна. Тэгээд туннель өөрийн гаралтыг хязгааргүй дамжуулж, гаднаас нээгдээд хэзээ ч хариулахгүй холболт мэт харагдана.
Дараалал нь protect, дараа нь connect эсвэл bind, дараа нь send. connect-ийн дараа protect дуудвал өмнөх маршрутын шийдвэр үлдэж, дуудалт true буцаасан ч socket буруу замыг үргэлжлүүлнэ. Буцаасан boolean-ыг шалгаж, false үед ажиллах боломжгүй илгээлт рүү орохын оронд чанга бүтэлгүйт.
protect()-д хэзээ ч хүрээгүй socket дээр sendto-гоос гарах зөвшөөрлийн алдаа нь энэ алдааны ердийн шинж.
UDP жишээ HTTPS зөөдөггүй
Ихэнх заавар хуулдаг дамжуулах жишээ зөвхөн UDP-г боловсруулдаг. TCP-д гар барилт, дарааллын дугаар, баталгаажуулалт, дахин илгээлт хэрэгтэй бөгөөд tun дескриптор болон socket хооронд ачааг хуулдаг давталт эдгээрийн аль нэгийг ч өгдөггүй. Тиймээс UDP DNS асуулга өнгөрч, HTTPS хүсэлт бүр зогсоно. Хэрэглэгчийн орон зайд TCP стек ажиллуулах эсвэл төхөөрөмж дээр TCP-г дуусгаж урсгалыг дамжуулдаг локал проксид холболтуудыг өг.
Ямар нэг зүйлийг дахин бичихээс өмнө хуваалтыг шалга: нэг UDP асуулга илгээж, мөн хост руу нэг TCP холболт нээ. UDP зам ажиллаж, TCP зам эхний SYN дээр гацвал асуудал дамжуулах кодонд, серверт биш. Тэр нэг харьцуулалт нь ашиггүй серверийн лог уншихад зарцуулагдах их цагийг хэмнэнэ.
Маршрутын жагсаалт, апп тус бүрийн шүүлтүүр харж буй зүйлийг хязгаарлана
addAddress, addRoute, addDns нь систем туннель рүү юу илгээхийг тодорхойлно. Маршрутын жагсаалтаас гадуурх чиглэл таны дескрипторт хэзээ ч хүрэхгүй, тиймээс ямар ч хэмжээний дамжуулах логик үүнийг засахгүй. Хийж буй builder дуудлуудаа бүртгэж, апп үнэхээр хүрэхийг оролдож буй хаягуудтай харьцуул.
Апп тус бүрийн сонголт өөрийн доод хязгаартай: addAllowedApplication, addDisallowedApplication-д API 21 хэрэгтэй, хуучин хувилбарт эдгээр арга огт байхгүй. Пакетаар шүүдэг загвар тэнд ажиллахгүй.
Дахин босгох нь хуучин дескрипторуудыг үлдээдэг
establish()-ыг дахин дуудвал шинэ дескриптор буцаж, өмнөх интерфейс хаагдана. Хуучин дескриптор хүчингүй болсон тул түүн дээр блоклогдсон thread алдаа авна, хуучин замд үүсгэсэн socket-уудыг солих шаардлагатай байж болно. onRevoke бол хэрэглэгч эсвэл систем аваад явсан туннелийн callback. Амьдралын мөчлөгийг дахин босгохын өмнө уншигчийг зогсоодог нэг эзэмшигчид байлгавал холбогдсон-боловч үхсэн төлөвүүдийн ихэнхийг сэргийлнэ.
Алдаа бүрийг хамаарах алхамаар нь унш. recvfrom-оос холболт дахин тохируулагдсан нь таны код уншиж байх үед алсын тал хаагдсаныг илэрхийлнэ. Муу хаягийн алдаа нь толгой задлан шинжлэх эсвэл offset алдаа руу заадаг, сүлжээ рүү биш.
Ажлын дараалал: блоклох горим асаалттай, уншилт бүрт нэг пакет, толгой задлан шинжилсэн, socket хамгаалагдсан, ачаа илгээгдсэн, шилжүүлэх үед цэвэр хаагдах, чиглэлийг хамарсан маршрутын жагсаалт.