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

Τι σας δίνει το σύστημα και τι όχι

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

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

Η αποκλειστική ανάγνωση κρίνει αν κινείται ο βρόχος

Το setBlocking(true) βάζει τον περιγραφέα σε αποκλειστική λειτουργία και υπάρχει από το API 21. Σε παλαιότερες εκδόσεις η λύση είναι ένας μικρός βοηθός JNI που καθαρίζει το O_NONBLOCK με fcntl στον ακατέργαστο περιγραφέα. Η λειτουργία μετράει γιατί καθορίζει τι επιτρέπεται να κάνει μια ανάγνωση χωρίς εκκρεμές πακέτο.

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

Διαβάστε ένα πακέτο ανά κλήση σε buffer μεγέθους MTU της διεπαφής, και αντιμετωπίστε το μηδέν ή μια αρνητική επιστροφή ως σφάλμα, όχι ως κενό πακέτο.

protect() πριν την πρώτη αποστολή, όχι μετά

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

Η σειρά είναι protect, μετά connect ή bind, μετά send. Κλήση protect μετά το connect μπορεί να αφήσει την προηγούμενη απόφαση διαδρομής, οπότε το socket συνεχίζει να χρησιμοποιεί λάθος μονοπάτι παρόλο που η κλήση επέστρεψε true. Ελέγξτε το boolean που επιστρέφει και αποτύχετε ηχηρά όταν είναι false, αντί να μπείτε σε αποστολή που δεν μπορεί να δουλέψει.

Σφάλμα άδειας από sendto σε socket που δεν άγγιξε ποτέ το protect() είναι η συνήθης υπογραφή αυτού του λάθους.

Το δείγμα UDP δεν μεταφέρει HTTPS

Το δείγμα προώθησης που αντιγράφουν τα περισσότερα μαθήματα χειρίζεται UDP και τίποτε άλλο. Το TCP χρειάζεται χειραψία, αριθμούς ακολουθίας, επιβεβαιώσεις και επαναμετάδοση, και ένας βρόχος που αντιγράφει ωφέλιμο φορτίο μεταξύ περιγραφέα tun και socket δεν παρέχει κανένα από αυτά. Γι' αυτό ένα ερώτημα DNS μέσω UDP περνά ενώ κάθε αίτημα HTTPS κολλάει. Είτε τρέξτε στοίβα TCP στον χώρο χρήστη είτε δώστε τις συνδέσεις σε τοπικό proxy που τερματίζει το TCP στη συσκευή και προωθεί τη ροή.

Δοκιμάστε τον διαχωρισμό πριν ξαναγράψετε οτιδήποτε: στείλτε ένα ερώτημα UDP και ανοίξτε μία σύνδεση TCP στον ίδιο κεντρικό υπολογιστή. Αν η διαδρομή UDP δουλεύει και η διαδρομή TCP κολλά στο πρώτο SYN, το πρόβλημα είναι ο κώδικας προώθησης και όχι ο διακομιστής. Αυτή η μία σύγκριση γλιτώνει πολύ χρόνο που σπαταλιέται διαβάζοντας αρχεία καταγραφής διακομιστή χωρίς τίποτα χρήσιμο.

Η λίστα διαδρομών και τα φίλτρα ανά εφαρμογή περιορίζουν το τι βλέπετε

Τα addAddress, addRoute και addDns ορίζουν τι στέλνει το σύστημα στο τούνελ. Προορισμός έξω από τη λίστα διαδρομών δεν φτάνει ποτέ στον περιγραφέα σας, οπότε καμία ποσότητα λογικής προώθησης δεν θα το διορθώσει. Καταγράψτε τις κλήσεις builder που κάνετε και συγκρίνετέ τις με τις διευθύνσεις που η εφαρμογή πραγματικά προσπαθεί να φτάσει.

Η επιλογή ανά εφαρμογή έχει το δικό της κατώφλι: τα addAllowedApplication και addDisallowedApplication χρειάζονται API 21, και σε παλαιότερες εκδόσεις οι μέθοδοι απλώς λείπουν. Σχεδιασμός που φιλτράρει κατά πακέτο δεν μπορεί να τρέξει καθόλου εκεί.

Η επανίδρυση αφήνει παλιούς περιγραφείς

Νέα κλήση establish() επιστρέφει νέο περιγραφέα και κλείνει την προηγούμενη διεπαφή. Ο παλιός περιγραφέας είναι τώρα άκυρος, οπότε νήμα που μπλοκάρει ακόμη πάνω του παίρνει σφάλμα, και sockets που φτιάχτηκαν για την παλιά διαδρομή μπορεί να χρειάζονται αντικατάσταση. Το onRevoke είναι το callback για τούνελ που πήραν ο χρήστης ή το σύστημα. Το να κρατάτε τον κύκλο ζωής σε έναν ιδιοκτήτη, που σταματά τον αναγνώστη πριν την επανίδρυση, αποτρέπει τις περισσότερες καταστάσεις συνδεδεμένο-αλλά-νεκρό.

Διαβάστε κάθε σφάλμα κατά το βήμα στο οποίο ανήκει. Επαναφορά σύνδεσης από recvfrom σημαίνει ότι το απομακρυσμένο άκρο έκλεισε ενώ ο κώδικάς σας διάβαζε ακόμη. Σφάλμα κακής διεύθυνσης δείχνει σε ανάλυση κεφαλίδας ή σφάλμα offset, όχι στο δίκτυο.

Σειρά εργασίας: αποκλειστική λειτουργία ενεργή, ένα πακέτο ανά ανάγνωση, κεφαλίδα αναλυμένη, socket προστατευμένο, ωφέλιμο φορτίο σταλμένο, καθαρό κλείσιμο στην παράδοση και λίστα διαδρομών που καλύπτει τον προορισμό.

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