Γιατί η εφαρμογή iOS VPN σου δείχνει αποσυνδεδεμένη ενώ η σήραγγα είναι ενεργή
Η εφαρμογή iOS ελέγχει τη σήραγγα αλλά δεν την κατέχει, και η κατάσταση του manager είναι προσωρινά αποθηκευμένη. Φόρτωσε πρώτα τις προτιμήσεις και μετά διάβασε κατάσταση και μετρητές διεπαφής.
Η εφαρμογή σου λέει αποσυνδεδεμένη, η γραμμή μενού λέει συνδεδεμένη και η κίνηση προφανώς ρέει. Αυτό το χάσμα δεν είναι συνθήκη αγώνα που διορθώνεται με επανάληψη: η εφαρμογή και η σήραγγα είναι ξεχωριστές διεργασίες, και το αντικείμενο που διαβάζει ο κώδικάς σου είναι μια προσωρινά αποθηκευμένη όψη μιας διαμόρφωσης που φυλάει το σύστημα. Η επιφάνεια Personal VPN της Apple είναι API ελέγχου, όχι διαδρομή δεδομένων, και η περισσότερη από την παράξενη συμπεριφορά της προκύπτει από αυτό το ένα γεγονός. Να τι συμβαίνει πραγματικά σε κάθε κλήση.
Το αντικείμενο του manager είναι αποθηκευμένη όψη, όχι η σήραγγα
NETunnelProviderManager συμπληρώνεται από ένα χώρο προτιμήσεων που ανήκει στο σύστημα, άρα οι ιδιότητές του δεν σημαίνουν τίποτα μέχρι να ολοκληρωθεί το loadFromPreferences. Αν διαβάσεις το connection.status πριν από αυτό, παίρνεις μια προεπιλεγμένη τιμή, γι’ αυτό η πρώτη ανανέωση κατάστασης μετά από επανεκκίνηση αναφέρει αποσυνδεδεμένη ενώ η σήραγγα είναι ενεργή. Φόρτωσε πρώτα και μετά διάβασε την κατάσταση και τη διαμόρφωση πρωτοκόλλου από τον ίδιο χειριστή ολοκλήρωσης. Παρατήρησε τις μεταγενέστερες αλλαγές μέσω NEVPNStatusDidChange αντί για περιοδική ερώτηση, και πρόσεξε ότι η ειδοποίηση ενεργοποιείται και για συνδέσεις που ξεκινά το On Demand, όπου δεν υπάρχει κλήση εφαρμογής για να επιστρέψει οτιδήποτε. Το ίδιο ισχύει και για τις ρυθμίσεις πρωτοκόλλου: διαβάζονται από τη φορτωμένη διαμόρφωση, όχι από ό,τι πέρασε η εφαρμογή.
Η ίδια η σήραγγα ζει σε διεργασία παρόχου Network Extension, άρα η εφαρμογή μπορεί να χάσει τη δική της κατάσταση χωρίς να χάσει τη σύνδεση. Ό,τι δείχνει το περιβάλλον χρήστη πρέπει να ξαναχτιστεί από τον manager, όχι να θυμάται από την προηγούμενη συνεδρία.
Σειρά εκκίνησης: αποθήκευσε, φόρτωσε και μετά ξεκίνα
Μια σύνδεση βασισμένη σε πάροχο χρειάζεται τρία βήματα με σειρά. Γράψε τη διαμόρφωση του παρόχου και κάλεσε το saveToPreferences, φόρτωσε τον manager ώστε η αποθηκευμένη διαμόρφωση να είναι αυτή που κρατάς, και μετά ξεκίνα μέσω NETunnelProviderSession.startVPNTunnel(). Αν παραλείψεις την αποθήκευση, η εκκίνηση δεν έχει τι να εκκινήσει· αν φορτώσεις πριν την αποθήκευση, ο manager εξακολουθεί να αντανακλά την παλιά διαμόρφωση.
Η λογική τιμή που επιστρέφει το startVPNTunnel λέει μόνο ότι το αίτημα έγινε δεκτό. Οι αποτυχίες σε επίπεδο παρόχου φτάνουν αργότερα ως αλλαγή κατάστασης σε αποσυνδεδεμένη, την οποία παρατηρείς μέσω της ειδοποίησης NEVPNStatusDidChange, και ο χειριστής της ειδοποίησης πρέπει να ξαναδιαβάσει το connection.status μετά το loadFromPreferences, γιατί η αποθηκευμένη τιμή μπορεί να είναι ακόμη η παλιά. Οι αποτυχίες σε επίπεδο αιτήματος επιστρέφουν αμέσως από τη μορφή κλήσης που πετά εξαίρεση ή μέσω της παραμέτρου εξόδου σφάλματος του startVPNTunnelAndReturnError:. Κώδικας που αντιμετωπίζει την επιστρεφόμενη τιμή ως αποτέλεσμα αναφέρει σύνδεση που δεν εγκαταστάθηκε ποτέ. Η άλλη σιωπηλή αποτυχία είναι ένα αναγνωριστικό πακέτου παρόχου που δεν ταιριάζει με την εγκατεστημένη επέκταση: το αίτημα απορρίπτεται και τίποτα στο μήνυμα δεν δείχνει το αναγνωριστικό. Αν το αναγνωριστικό είναι λάθος, το αίτημα συνήθως αποτυγχάνει αμέσως, πριν εμφανιστεί οποιαδήποτε γραμμή αρχείου καταγραφής του παρόχου.
Η επαναφόρτωση των προτιμήσεων μετά την εκκίνηση είναι αυτό που κάνει σωστές τις οθόνες κατάστασης. Ο manager δεν σου σπρώχνει νέα διαμόρφωση όταν την αλλάζει το σύστημα.
Το υλικό εμπιστοσύνης φτάνει μέσω προφίλ, ποτέ από κώδικα εφαρμογής
Μια εφαρμογή δεν έχει υποστηριζόμενο τρόπο να προσθέσει πιστοποιητικό ρίζας στο χώρο εμπιστοσύνης του iOS, άρα μια διαμόρφωση Personal VPN μπορεί να αναφέρεται μόνο σε υλικό που η συσκευή εμπιστεύεται ήδη: μια αλυσίδα που καθιέρωσε εγκατεστημένο προφίλ, διαχείριση συσκευής ή μια CA που το σύστημα εμπιστεύεται για άλλους λόγους. Μια εφαρμογή που δημιουργεί τη δική της CA στην πρώτη εκκίνηση και περιμένει το σύστημα να την τιμήσει θα αποτύχει στην επικύρωση μέσα στον πάροχο, όπου το περιβάλλον της εφαρμογής δεν μπορεί να δει τον λόγο.
Αντιμετώπισε αυτό το όριο ως τον σχεδιαστικό περιορισμό που είναι και παράδωσε την άγκυρα εμπιστοσύνης μαζί με το προφίλ εγγραφής. Κράτα τον έλεγχο ταυτότητας VPN χωριστά από την απόφαση εμπιστοσύνης, γιατί αποτυγχάνουν σε διαφορετικά σημεία και τα αποσφαλματώνουν διαφορετικοί άνθρωποι.
Οι μετρητές έρχονται από τη διεπαφή, όχι από το VPN API
Ο manager εκθέτει κατάσταση και διαμόρφωση και τίποτα που να μετρά byte ανά σήραγγα. Αν χρειάζεσαι αριθμούς χρήσης, κάλεσε το getifaddrs() και διάβασε το if_data για τη σχετική διεπαφή: en0 για Wi-Fi, τις διεπαφές pdp_ip για κινητή και μια συσκευή utun για την ίδια τη σήραγγα. Οι μετρητές του utun περιγράφουν την εσωτερική κίνηση και περιλαμβάνουν την επιβάρυνση του πρωτοκόλλου.
Αυτοί οι αριθμοί είναι εκτιμήσεις, όχι λογιστική ανά εφαρμογή, και μηδενίζονται όταν η διεπαφή δημιουργηθεί ξανά. Χαρακτήρισέ τους έτσι στο περιβάλλον χρήστη, αλλιώς κάποιος θα τους χρησιμοποιήσει για να χρεώσει πελάτη. Αν χρειάζεσαι αριθμούς ανά εφαρμογή, χρειάζεσαι εντελώς διαφορετικό μηχανισμό, όχι καλύτερη ανάγνωση αυτών των πεδίων.
Ο καθαρισμός ανήκει στο stopVPNTunnel, όχι στον τερματισμό
Το applicationWillTerminate σου δίνει λίγα δευτερόλεπτα πριν το σύστημα μπορεί να σκοτώσει τη διεργασία, κάτι που δεν είναι αξιόπιστο σημείο για να διαλύσεις μια σύνδεση. Στείλε τη διακοπή στο stopVPNTunnel() ή χειρίσου την στο callback διακοπής του παρόχου, και κράτα το αναγνωριστικό manager ή διαμόρφωσης με το οποίο ξεκίνησες, ώστε η διακοπή να στοχεύει εκείνη τη σύνδεση αντί για κάθε διαμόρφωση στη συσκευή.
Αποθήκευσε αυτό το αναγνωριστικό δίπλα στο προφίλ στο οποίο ανήκει, αφού ένα αντικείμενο manager δεν καταγράφει ποια διαμόρφωση επέλεξε ο χρήστης. Και θυμήσου ότι μια σύνδεση που την ανεβάζει το On Demand δεν έχει καμία κλήση εφαρμογής πίσω της, άρα μια οθόνη κατάστασης πρέπει ακόμη να φορτώνει προτιμήσεις πριν σχεδιάσει οτιδήποτε.