Split tunneling στο Android VpnService: τι μπορείτε πραγματικά να δρομολογήσετε
Το VpnService προωθεί μόνο τα πακέτα που στέλνει ο builder στον περιγραφέα του. Ελέγξτε προθέματα, έλεγχο ταυτότητας proxy, DNS και λίστες εφαρμογών πριν κατηγορήσετε το split tunneling.
Το split tunneling σε μια εφαρμογή VpnService σπάει σε λίγα συγκεκριμένα σημεία και η διόρθωση διαφέρει για το καθένα. Ο πελάτης δείχνει μόνο έναν προορισμό που αποτυγχάνει, άρα η πρώτη δουλειά είναι να βρείτε ποιο όριο χτυπάτε: τις διαδρομές που πήρε ο builder, το proxy που δεν μπορείτε να πιστοποιήσετε, τον resolver που μόνο ανακοινώσατε ή τη λίστα εφαρμογών που εφαρμόζει η πλατφόρμα ανά χρήστη. Γι' αυτό κρατάω έναν έλεγχο τεσσάρων βημάτων. Τρέχει με τη σειρά παρακάτω και σταματά στην πρώτη αποτυχία.
Επιβεβαιώστε τι έδωσε ο builder στην πλατφόρμα
Ο VpnService.Builder είναι λίστα δηλώσεων. Η establish() επιστρέφει ένα ParcelFileDescriptor για εικονική διεπαφή και η πλατφόρμα στέλνει σε αυτόν τον περιγραφέα μόνο κίνηση που ταιριάζει με τα προθέματα των addAddress και addRoute· η addDnsServer λέει στους πελάτες ποιον resolver να ρωτήσουν. Προορισμός έξω από κάθε πρόθεμα που προσθέσατε δεν φτάνει ποτέ στην εφαρμογή σας, ό,τι κι αν υπόσχεται η διεπαφή. Αυτό το ένα γεγονός εξηγεί τις περισσότερες αναφορές για σπασμένο split tunneling πριν αλλάξει οποιοσδήποτε κώδικας.
Καταγράψτε τις λίστες διευθύνσεων, διαδρομών και DNS που συγκρότησε ο builder και συγκρίνετέ τις με τους προορισμούς που αποτυγχάνουν. Ένα /32 που λείπει για έναν host διαχείρισης είναι πολύ πιο συνηθισμένο από ένα σπασμένο τούνελ. Αν η λίστα είναι σωστή, προχωρήστε· αν είναι λάθος, το υπόλοιπο της λίστας ελέγχου είναι χαμένος κόπος.
Το excludeRoute υπάρχει μόνο από το API 33
Ο αποκλεισμός βάσει προορισμού είναι το αίτημα που καταλήγει πιο συχνά σε νήμα υποστήριξης. Το builder.excludeRoute(IpPrefix) ήρθε στο API 33· πριν από αυτό, η μόνη υποστηριζόμενη προσέγγιση είναι να προσθέσετε διαδρομές για όλα εκτός από το εξαιρούμενο πρόθεμα, δηλαδή να υπολογίσετε μόνοι σας τα συμπληρωματικά προθέματα. Οι δύο κλήσεις δεν είναι κατοπτρικές: η μία αφαιρεί από το σύνολο διαδρομών της διεπαφής, η άλλη χτίζει το σύνολο από το μηδέν, και μια παλαιότερη συσκευή θα πετάξει ή θα αγνοήσει σιωπηλά μια κλήση που δεν αναγνωρίζει. Διακλαδώστε με βάση το Build.VERSION.SDK_INT και δοκιμάστε και τις δύο διαδρομές σε πραγματικό υλικό.
Όταν υπολογίζετε συμπληρώματα, προσέξτε τη στοίχιση των προθεμάτων. Ένας αποκλεισμός /24 μέσα σε διαδρομή /16 χρειάζεται τα καλύπτοντα προθέματα που περιγράφουν πραγματικά το υπόλοιπο, όχι ένα παραμορφωμένο /8. Καταγράψτε την τελική λίστα και βεβαιωθείτε ότι ο εξαιρούμενος host εξακολουθεί να επιλύεται μέσω διαδρομής εκτός τούνελ.
Τα διαπιστευτήρια proxy δεν εκφράζονται
Η setHttpProxy(ProxyInfo) κατευθύνει τη διεπαφή σε proxy HTTP ή SOCKS και ένα PAC URL μπορεί να επιλέγει μεταξύ τους ανά προορισμό. Ούτε το ProxyInfo ούτε η μορφή PAC μεταφέρουν όνομα χρήστη και κωδικό, άρα ένα proxy με πιστοποίηση μένει έξω από το API της πλατφόρμας. Οι εφαρμογές που το χρειάζονται ενσωματώνουν συνήθως στοίβα χώρου χρήστη, tun2socks ή παρόμοια μηχανή και απαντούν μόνες στην πρόκληση 407.
Αυτή η επιλογή έχει συνέπειες. Μια στοίβα χώρου χρήστη βλέπει μόνο την κίνηση που δρομολογείτε προς αυτήν, άρα πρέπει να ζει μέσα στα ίδια όρια περιγραφέα και κληρονομεί την ίδια απαίτηση protect() για το δικό της socket ανόδου. Παραλείψτε την protect() και το τούνελ πιάνει τη δική του μεταφορά, που εμφανίζεται ως χειραψία που κολλάει μετά το πρώτο πακέτο. Δοκιμάστε απέναντι σε proxy που απαιτεί διαπιστευτήρια, γιατί ένα ανοιχτό proxy περνά στη διαδρομή της πλατφόρμας και δεν αποδεικνύει τίποτα.
Οι λίστες ανά εφαρμογή και το DNS είναι επίσης δηλώσεις
Ο Builder έχει και addAllowedApplication και addDisallowedApplication, και αλληλοαποκλείονται: η κλήση και των δύο πετά εξαίρεση. Σε κάθε περίπτωση η λίστα εφαρμόζεται ανά χρήστη κατά τη ρύθμιση της διεπαφής, άρα μια εφαρμογή προφίλ εργασίας διέπεται από το τούνελ του προφίλ εργασίας και όχι από το δικό σας. Αυτό που δεν δίνει κανένα API είναι έλεγχος στην απόφαση άλλης εφαρμογής: δεν μπορείτε να σπρώξετε ένα πακέτο στο τούνελ σας από έξω, ούτε να κλείσετε ή να αναδιαμορφώσετε ένα VPN που είναι ήδη ενεργό.
Το DNS ακολουθεί το ίδιο μοτίβο. Η addDnsServer ανακοινώνει μια διεύθυνση resolver· δεν επιτρέπει στην εφαρμογή σας να απαντά σε ερωτήματα. Αν θέλετε να επιλύετε ονόματα μόνοι σας, τρέξτε έναν resolver και δρομολογήστε τη διεύθυνσή του στο τούνελ. Εφαρμογές που χρησιμοποιούν DoT ή DoH προς σταθερό όνομα host μπορούν να παρακάμψουν τη διεύθυνση που ανακοινώσατε, οπότε θεωρήστε την επίλυση ονομάτων ως βέλτιστη προσπάθεια εκτός αν υποκλέπτετε και τη διαδρομή του ερωτήματος.
Manifest, always-on και το τεστ τεσσάρων βημάτων
Η καταλληλότητα δηλώνεται στο manifest: BIND_VPN_SERVICE, το φίλτρο intent android.net.VpnService και μια υπηρεσία foreground αν το τούνελ πρέπει να επιβιώσει στο παρασκήνιο. Το always-on VPN απαιτεί επιπλέον καθαρό onRevoke, γιατί το σύστημα το καλεί όταν ο χρήστης αλλάζει ρυθμίσεις ή όταν άλλη εφαρμογή αναλαμβάνει τον έλεγχο.
Εκτελέστε τους ελέγχους με σειρά και σταματήστε στην πρώτη αποτυχία. Πρώτον, εκτυπώστε τις λίστες διευθύνσεων, διαδρομών και DNS που δόθηκαν στον builder. Δεύτερον, επιβεβαιώστε ότι η establish() επέστρεψε περιγραφέα και ότι το socket μεταφοράς είναι προστατευμένο. Τρίτον, ζητήστε έναν προορισμό μέσα σε συμπεριλαμβανόμενο πρόθεμα και έναν μέσα σε εξαιρούμενο. Τέταρτον, ελέγξτε ποιος resolver απάντησε πραγματικά ρωτώντας ένα όνομα host που μόνο ο δικός σας resolver μπορεί να επιλύσει.
Μια ολοκληρωμένη χειραψία δεν αποδεικνύει κανένα από τα επόμενα βήματα. Κάθε έλεγχος απομονώνει ένα στρώμα, άρα μια επιτυχία στο δεύτερο βήμα δεν λέει τίποτα για το τέταρτο.
Η αναφορά του VpnService.Builder απαριθμεί όλη την επιφάνεια του builder. Ό,τι χρειάζεστε πέρα από αυτήν είναι άλλο API, άλλη εφαρμογή ή δική σας στοίβα χώρου χρήστη, και αυτό είναι απόφαση σχεδίασης, όχι σφάλμα split tunneling.