Τέσσερις υποθέσεις για το Android VpnService μου κόστισαν κάποτε το μεγαλύτερο μέρος μιας εβδομάδας, και καμία δεν αφορούσε το πρωτόκολλο δικτύου. Η εφαρμογή έλεγε συνδεδεμένο, το εικονίδιο κλειδιού καθόταν στη γραμμή κατάστασης, και η κίνηση έβγαινε ακόμη από την κανονική διαδρομή. Συνέχιζα να υποθέτω ότι η σήραγγα ήταν δική μου μέχρι να τη σταματήσω, ότι οι διαδρομές που πρόσθετα ήταν οι διαδρομές που έπαιρνα, και ότι η κλήση stopSelf θα κατέβαζε και τη διεπαφή μαζί της. Η υπηρεσία συμπεριφερόταν ακριβώς όπως τεκμηριώνεται. Το σπασμένο κομμάτι ήταν το νοητικό μου μοντέλο.

Μύθος 1: η σήραγγα μένει όρθια μέχρι να τη σταματήσετε

Το Android κρατά μία διεπαφή VPN κάθε φορά. Στήστε δεύτερη και η πλατφόρμα δεν τη βάζει σε ουρά: απενεργοποιεί τη δική σας και καλεί onRevoke() στην υπηρεσία σας. Αν ο κώδικάς σας κατεδαφίζει μόνο με πάτημα κουμπιού, μια κατάληψη αφήνει πίσω έναν περιγραφέα και ένα νήμα ανάγνωσης, και η επόμενη εκκίνηση κληρονομεί και τα δύο.

Η αναφορά του VpnService λέει ρητά ότι το prepare() επιστρέφει null όταν η εφαρμογή έχει ήδη συγκατάθεση και ένα Intent όταν ο χρήστης πρέπει ακόμη να εγκρίνει. Αυτή η συγκατάθεση φτάνει μόνο αν παραδώσετε το intent μέσω startActivityForResult(). Αν παραλείψετε την παράδοση, το establish() επιστρέφει null, που διαβάζεται ως σιωπηλή αποτυχία και όχι ως πρόβλημα αδειών.

Το Always-on VPN προσθέτει έναν ιδιοκτήτη που δεν ελέγχετε. Το σύστημα μπορεί να επαναφέρει τη σήραγγα μετά από εκκίνηση ή μετά από κατάρρευση, και προλαμβάνει μια εφαρμογή VPN που ξεκίνησε ο χρήστης με το χέρι. Χτίστε το onRevoke() ώστε να κλείνει τους ίδιους πόρους με το κανονικό σας τερματισμό, και αντιμετωπίστε το ως κύρια διαδρομή τερματισμού και όχι ως ακραία περίπτωση. Οι χρήστες αλλάζουν εφαρμογές VPN· ο κώδικάς σας πρέπει να το αντέξει.

Μύθος 2: το addRoute πιάνει ακριβώς ό,τι καταγράψατε

Οι διαδρομές αποφασίζουν ποια πακέτα φτάνουν στον περιγραφέα σας, και ένα λάθος εύρους παράγει την εξοργιστική περίπτωση όπου μέρος της κίνησης περνά από τη σήραγγα και μέρος όχι. Το addRoute("0.0.0.0", 0) παίρνει όλο το IPv4, που σημαίνει ότι κάθε πακέτο φτάνει πλέον σε εσάς και πρέπει να προωθηθεί ή να απορριφθεί. Ένα /32 για έναν μόνο υπολογιστή δεν παίρνει σχεδόν τίποτα, και όλα τα άλλα συμπεριφέρονται σαν να μην εγκαταστάθηκε ποτέ το VPN σας.

Οι οικογένειες μετριούνται χωριστά. Μια διαδρομή μόνο για IPv4 αφήνει το IPv6 στο υποκείμενο δίκτυο, οπότε ένας κόμβος διπλής στοίβας μπορεί ακόμη να επιλύει και να συνδέεται έξω από τη σήραγγα ενώ μέσα της φαίνεται απόλυτα συνδεδεμένος.

Το DNS ακολουθεί την ίδια λογική. Οι διακομιστές που προστίθενται με addDnsServer() πηγαίνουν σε εφαρμογές που χρησιμοποιούν τον επιλυτή της σήραγγας, ενώ μια εφαρμογή με δικό της κρυπτογραφημένο τελικό σημείο DNS ή με Private DNS σε όνομα κεντρικού υπολογιστή τα αγνοεί εντελώς. Όταν τα ονόματα αποτυγχάνουν για μια εφαρμογή και δουλεύουν για άλλη, ελέγξτε τη διαδρομή του επιλυτή πριν αγγίξετε έστω μία διαδρομή.

Μύθος 3: οι κανόνες ανά εφαρμογή είναι διακόπτης που τον γυρίζετε προς όποια πλευρά

Το addDisallowedApplication() και το addAllowedApplication() είναι δύο λειτουργίες, όχι δύο φίλτρα. Ο κατασκευαστής δέχεται ένα μοντέλο ανά σήραγγα, οπότε μια εφαρμογή που θέλει τα πάντα εκτός από τρία πακέτα χρησιμοποιεί τη λίστα αποκλεισμών και αφήνει τη λίστα επιτρεπόμενων κενή. Τα δύο αλληλοαποκλείονται, και οι άνθρωποι το ανακαλύπτουν κατά την εκτέλεση αφού η οθόνη ρυθμίσεων έχει ήδη προσφέρει και τους δύο διακόπτες.

Η ορατότητα πακέτων δαγκώνει επίσης. Στο Android 11 και μετά μπορείτε να ονομάσετε μόνο πακέτα που βλέπετε, οπότε μια λίστα εξαιρέσεων χτισμένη από ερώτημα που η εφαρμογή σας δεν επιτρέπεται να εκτελέσει θα χάσει καταχωρήσεις σιωπηλά. Συγκρίνετε τη λίστα με ό,τι επιστρέφει πραγματικά ο διαχειριστής πακέτων πριν την εμπιστευτείτε.

Τα προφίλ εργασίας χωρίζουν ξανά το κοινό. Μια σήραγγα που ξεκινά μέσα στο προφίλ καλύπτει αυτό το προφίλ και τις εφαρμογές του, ενώ η προσωπική πλευρά κρατά τη δική της συνδεσιμότητα. Είναι απόφαση εύρους που λαμβάνεται μία φορά, και ανήκει στη διεπαφή και όχι σε ένα δελτίο υποστήριξης.

Μύθος 4: το stopSelf τερματίζει τη σήραγγα

Δεν την τερματίζει. Η διεπαφή ζει στον περιγραφέα που επιστρέφει το establish(), και το αντικείμενο της υπηρεσίας μπορεί να πεθάνει ενώ ο περιγραφέας μένει ανοιχτός. Κλείστε πρώτα το ParcelFileDescriptor, αφήστε την αναφορά, ενώστε ή διακόψτε τα νήματα ανάγνωσης και εγγραφής, και μετά καλέστε το stopForeground(true) και το stopSelf() με αυτή τη σειρά.

Ένας ξεχασμένος περιγραφέας είναι ο συνηθισμένος λόγος που μια υπηρεσία φαίνεται να επιβιώνει από το δικό της αίτημα τερματισμού, και εξηγεί μια παρωχημένη διαδρομή που ζει περισσότερο από την εφαρμογή που την πρόσθεσε.

Το αντίστροφο λάθος είναι εξίσου συνηθισμένο: κλείσιμο του περιγραφέα ενώ ένα νήμα ανάγνωσης είναι στα μισά της ανάγνωσης, και μετά αφήνουμε αυτό το νήμα να χειριστεί μια εξαίρεση για την οποία κανείς δεν έγραψε κώδικα. Κρατήστε τη σειρά τερματισμού σε ένα σημείο και αφήστε και την ενέργεια του χρήστη και το onRevoke() να καλούν μέσα της.

Τι να ελέγξετε όταν η σήραγγα μοιάζει νεκρή

Προστατέψτε τις μακρόβιες υποδοχές με protect() πριν σηκωθεί η σήραγγα. Χωρίς αυτό, η ίδια η μεταφορά της σήραγγας δρομολογείται πίσω μέσα στη σήραγγα και η σύνδεση κολλάει με τρόπο που μοιάζει ακριβώς με βλάβη διακομιστή. Δοκιμάστε απέναντι σε μια διεύθυνση που υπάρχει μόνο πίσω από το απομακρυσμένο δίκτυο, όχι σε δημόσιο ιστότοπο που απαντά από την προσωρινή μνήμη ή από την υποκείμενη διαδρομή.

Καταγράψτε το αποτέλεσμα του establish(), τις διαδρομές, τα εξαιρούμενα πακέτα και τον λόγο τερματισμού. Αυτές οι τέσσερις γραμμές χωρίζουν ένα πρόβλημα συγκατάθεσης από ένα πρόβλημα εύρους και από ένα πρόβλημα κύκλου ζωής χωρίς καταγραφή πακέτων.

Σχετικά άρθρα