Android VPN: VpnService-ის აღმწერი root tun მოწყობილობის წინააღმდეგ
Android აპლიკაციები თავად ვერ ქმნიან tun0-ს: რას აბრუნებს establish(), სად ჩერდება Binder და სად იწყება JNI, და როდის იმარჯვებს root-ით შექმნილი მოწყობილობა.
ორ გზას შეუძლია IP ინტერფეისი მოათავსოს Android აპლიკაციის წინ, და ორივე ერთიდაიგივე ტიპის ფაილის აღმწერზე მთავრდება ძალიან განსხვავებული მფლობელებით. ერთი VpnService-ისა და მისი builder-ის გავლით მიდის, მეორე root shell-ისა და /dev/net/tun-ის გავლით. ამ აღმწერზე ყოველი წაკითხვა გამავალი პაკეტია, ხოლო ყოველი ჩაწერა შემავალი. გზებს შორის განმასხვავებელია ის, ვის უჭირავს პოლიტიკა, ვინ გადაურჩება OS-ის განახლებას და მარშრუტიზაციის ცხრილის რამდენ ნაწილს შეეხებით.
აირჩიე პლატფორმის გზა, თუ არ შეგიძლია დაასახელო კონკრეტული რამ, რაზეც ის უარს ამბობს. ეს უარი ჩვეულებრივ TAP-ია, აპლიკაციათა საკუთარი ნაკრების გარეთ აღება, ან მარშრუტიზაციის წესი, რომელსაც ჩარჩო არასოდეს ამჟღავნებს.
პლატფორმის გზაზე განზრახვას აცხადებ ბრძანებების ნაცვლად. აწვდი მინიმუმ ერთ მისამართს, სასურველ მარშრუტებს, DNS სერვერებსა და სურვილისამებრ აპლიკაციების სიას, შემდეგ Android-ის სისტემური სერვისი ასრულებს პრივილეგირებულ კონფიგურაციას საკუთარი იდენტობით. ინტერფეისის სახელს წინასწარ არასოდეს იგებ, და მის შემქმნელ ioctl-ს არასოდეს გასცემ, და ეს არის არსი.
IP ინტერფეისის მიღების ორი გზა
root გზა უფრო ძველი რეცეპტია. ხსნი /dev/net/tun-ს, იძახებ TUNSETIFF-ს IFF_TUN-ით, ასწევ ინტერფეისს ip link-ით და ხელით უმატებ მისამართებსა და მარშრუტებს. ყოველი ნაბიჯი შენ უნდა გააკეთო სწორად, მათ შორის ისინი, რომლებსაც ჩარჩო ჩუმად შეასრულებდა. ერთი დაკარგული მარშრუტი მომხმარებელს იმავე ანგარიშს აძლევს, რასაც მკვდარი სერვერი.
გამართვა ისევე განსხვავდება, როგორც კონფიგურაცია. root გზაზე ინტერფეისი ან არსებობს, ან შენმა ioctl-მა ისეთი შეცდომა დააბრუნა, რომელსაც პირდაპირ წაიკითხავ. პლატფორმის გზაზე ინტერფეისი მხოლოდ establish()-ის წარმატების შემდეგ არსებობს, ამიტომ წარუმატებლობა გამონაკლისის ან null აღმწერის სახით მოდის და არა მოწყობილობის კვანძის სახით, რომელშიც ჩხირავ.
რას აბრუნებს establish() სინამდვილეში
Builder.establish()-ს სამი შედეგი აქვს და სამივე განსხვავებულ პასუხს მოითხოვს. აღმწერი ნიშნავს, რომ ინტერფეისი კონფიგურირებულია და შენია გამოსაყენებლად, null ნიშნავს, რომ მოთხოვნა არ გავიდა, ხოლო გამოგდებული გამონაკლისი ნიშნავს, რომ სისტემურმა სერვისმა ის უარყო. null-ისა და წარმატების ერთ boolean-ში გაერთიანება ყველაზე გავრცელებული გზაა, რომლითაც აპლიკაცია აცხადებს მუშა გვირაბს, რომელიც არასოდეს ატარებს პაკეტს. Builder-ის საცნობარი აღნიშნავს მისამართის მოთხოვნას და ღირს მისი წაკითხვა, სანამ სერვერს დაადანაშაულებ.
მიმართულებებს აღმწერი განსაზღვრავს და არა შენი პროტოკოლი. წაკითხვები გაძლევს გამავალ პაკეტებს, რომლებიც უკვე შერჩეულია VPN-ისთვის, ხოლო ჩაწერები შემავალ პაკეტებს უკან ინტერფეისში უშვებს. შენი საკუთარი ტრანსპორტი შიფრავს, ამოწმებს და აჩარჩოებს ამ პაკეტებს, რადგან პლატფორმა პაკეტის საზღვარზე ჩერდება.
Binder ერთი ნაბიჯია, JNI სხვა ღერძი
builder-ის მოთხოვნა Binder-ის გავლით შედის სისტემაში, და სადღაც ამის ქვემოთ პლატფორმა იძახებს ნატიურ კოდს მოწყობილობის კონფიგურაციისთვის. ეს ორი განსხვავებული მექანიზმია ერთ გამოძახებაში დაწყობილი. Binder არის პროცესთაშორისი კომუნიკაცია შენს აპსა და სისტემურ სერვისს შორის, ხოლო JNI არის ხიდი Java-დან ნატიურ კოდამდე პროცესის შიგნით. Android 9-ის AOSP ხის წაკითხვა სტეკს ლიტერალურად აჩვენებს და ღირს გახსენება, რომ შიდა კლასების სახელები და ფაილების ბილიკები გამოშვებებს შორის იცვლება.
რაც არ იცვლება, არის ხელშეკრულება, რომელსაც შეიძლება დაეყრდნო: builder, მის მიერ დაბრუნებული აღმწერი და მის გარშემო სერვისის სასიცოცხლო ციკლი. არცერთმა აპლიკაციამ არ უნდა გამოიძახოს შიდა სერვისის ტრანზაქცია ან თავიდან შექმნას მოწყობილობის კვანძი ინტერფეისის მისაღებად. თუ შენი კოდი დამოკიდებულია AOSP კლასის სახელზე, ის გატყდება იმ გამოშვებაზე, რომელიც არ გამოგიცდია.
TUN ატარებს IP-ს, TAP ატარებს Ethernet-ს
TUN IP ინტერფეისია. TAP Ethernet ინტერფეისია და ეს სხვაობა წყვეტს, შესაძლებელია თუ არა დაკავშირება საერთოდ. TUN მოწყობილობას არ აქვს MAC მისამართი და არ აქვს გადასაცემი Ethernet სათაური, ამიტომ TUN კლიენტი ვერ შეუერთდება layer-2 ხიდს, როგორც არ უნდა იყოს მარშრუტები დაწერილი.
VpnService გაძლევს IP ვარიანტს, რომელიც სჭირდება აპლიკაციის დონის VPN-ს და რომლის მხარდაჭერაც შეუძლია ნედლი Ethernet სტეკის გარეშე. თუ შენი დიზაინი ხიდს მოითხოვს, პლატფორმის გზა იქ არ მიგიყვანს, ხოლო root გზა მხოლოდ მაშინ, თუ ბირთვი და მოწყობილობის ნებართვები ითანამშრომლებენ.
სად კვლავ იმარჯვებს root გზა
სამი საქმე აშორებს ადამიანებს პლატფორმის გზას. layer-2 ხიდს TAP სჭირდება. საკუთარი VPN არეალის გარეთ აპლიკაციების ტრაფიკის დაჭერას სჭირდება ხილვადობა, რომელსაც ჩარჩო არ იძლევა. პოლიტიკური მარშრუტიზაცია, რომელსაც builder არასოდეს ამჟღავნებს, მაგალითად ცალკე ცხრილი ერთი დანიშნულებისთვის, მოითხოვს წესებს, რომლებსაც თავად აყენებ.
ხარჯები მოგვიანებით მოდის: გახსნილი bootloader ან root-ჩადგმული იმიჯი, SELinux პოლიტიკა, რომელიც შენ ქვეშ იძვრება, და მოწყობილობა, რომელიც OTA-ს შემდეგ წყვეტს სწორად ქცევას. root-რეცეპტი ასევე ვერ გავრცელდება აპლიკაციების ჩვეულებრივი დისტრიბუციით, ამიტომ გუნდების უმეტესობა მას შიდა დიაგნოსტიკურ გზად ინახავს და არა როგორც პროდუქტს.
არჩევანი მათ შორის
დაიწყე ინტერფეისიდან, რომელიც გჭირდება და არა კოდიდან, რომელიც გაქვს. თუ პასუხია TUN მოწყობილობა აპლიკაციით შემოსაზღვრული მარშრუტებით, პლატფორმის გზა უფრო მოკლეა და განახლებებს გადაურჩება. თუ პასუხი მოიცავს Ethernet კადრებს, სხვა აპლიკაციების აღებას ან მარშრუტიზაციის წესებს, რომლებსაც builder უარყოფს, root ტერიტორიაზე ხარ და ეს ოპერაციულ ხარჯად უნდა დაგეგმო.
შემოწმება, რომელიც უმეტეს კამათს აწყვეტს: ჯერ პლატფორმის გზა ააშენე, ჩაწერე establish()-ის შედეგი დისტანციური სესიის მდგომარეობისგან ცალკე და დაადასტურე ერთი პაკეტი თითოეული მიმართულებით. მხოლოდ ამის შემდეგ გადაწყვიტე, ამართლებს თუ არა დაკარგული შესაძლებლობა მხარდაჭერილი გზის დატოვებას.