Détecter un VPN actif dans une app Mac sandboxée
Il n'existe pas d'API publique pour cette question, et l'astuce du nom d'interface n'est qu'une heuristique. Six vérifications pour décider de quoi votre app doit réellement dépendre.
Vous avez besoin qu'un écran se comporte différemment quand le tunnel est actif, et aucun appel ne vous le dit. C'est la contrainte de livraison : sur macOS, aucun framework public n'indique si un VPN quelconque est connecté, et sur iOS l'état appartient à l'app qui a installé la configuration. Les vérifications ci-dessous vont de cette limite à quelque chose que vous pouvez livrer, et chacune d'elles peut terminer la recherche plus tôt.
Vérification 1 : la question a-t-elle seulement une réponse ?
Le sandbox tranche la question avant toute API. Un processus tiers ne peut pas énumérer l'extension d'un autre fournisseur, et il ne peut pas non plus lire la configuration de cette extension, donc il n'existe aucun objet pris en charge à interroger.
Chaque contournement se heurte au même mur. Les listes d'interfaces, scutil --nc list et les lectures directes de la configuration système exigent soit des droits que le sandbox retient, soit un comportement qu'Apple n'a jamais promis. Si le plan a besoin de l'un de ces éléments, le problème est le plan. Une fonctionnalité bâtie sur un détail non documenté est un ticket de support avec une date de livraison.
Vérification 2 : votre app gère-t-elle le tunnel elle-même ?
Si oui, vous avez une réponse prise en charge pour votre propre configuration. Chargez les préférences, gérez l'erreur, puis lisez l'état et abonnez-vous aux changements au lieu d'interroger en boucle :
vpnManager.loadFromPreferences { error in
// handle a load failure before reading status
}
NotificationCenter.default.addObserver(
forName: .NEVPNStatusDidChange,
object: nil,
queue: nil
) { note in
let status = (note.object as? NEVPNConnection)?.status
}Soyez précis sur ce que cela renvoie : l'état du tunnel que vous avez installé. Un utilisateur qui fait tourner un autre client apparaîtra comme déconnecté, et ce n'est pas un bug dans votre code. Décidez laquelle de ces deux questions votre fonctionnalité pose vraiment avant de bâtir sur la réponse.
Vérification 3 : que vous dirait réellement une heuristique ?
L'approche largement copiée demande au dictionnaire de proxy du système ses entrées portant une portée et lit les clés, qui se trouvent être des noms d'interface :
let settings = CFNetworkCopySystemProxySettings()?.takeRetainedValue() as NSDictionary?
let scoped = settings?["__SCOPED__"] as? [String: Any]
let names = scoped?.keys ?? []Une clé commençant par tap, tun, ppp, ipsec ou utun implique qu'une interface de portée tunnel est présente. Faire correspondre le nom entier échoue aussitôt, car iOS 12 est arrivé avec utun1 et utun2 et certains profils se présentent comme ipsec4, ppp0 ou utun0. Comparez plutôt les préfixes.
Même ainsi, un nom n'identifie aucun propriétaire. Les fonctionnalités système et les extensions d'autres fournisseurs créent aussi des interfaces utun, et l'une peut exister sans porter le moindre de vos trafics. Lisez cela comme un indice qu'il y a quelque chose en forme de tunnel à proximité, rien de plus fort. Un faux positif ici est bon marché si vous ne conditionnez qu'une fonctionnalité, et coûteux si vous conditionnez l'écran de connexion.
Vérification 4 : pouvez-vous plutôt tester la destination ?
Si l'app a seulement besoin de savoir si un service interne est joignable, testez ce service. Une sonde courte avec un délai d'expiration répond à la question dont la fonctionnalité dépend réellement, et elle continue de fonctionner quand l'utilisateur change de client ou que le tunnel change de forme.
La joignabilité est une observation sur votre chemin. Un nom d'interface est une inférence sur quelque chose d'adjacent à votre chemin, et c'est pourquoi l'heuristique produit des disputes dans les rapports de bug tandis que la sonde produit des preuves. Si la sonde échoue alors que l'utilisateur insiste que le VPN est actif, vous avez désormais un chemin précis à déboguer : résolution de noms, route ou autorisation de service.
Vérification 5 : la plateforme peut-elle imposer ce dont vous avez besoin ?
Quand l'exigence est la garantie que rien ne s'échappe du tunnel, le profil est sa place, pas le détecteur. Activer includeAllNetworks dans une configuration de tunnel complet sur iOS 14 ou ultérieur change le comportement de la plateforme au lieu de demander à votre app de l'inférer. L'indicateur est documenté dans la référence NEVPNProtocol.
C'est un autre type de réponse : l'imposition au lieu de l'observation. Elle ne s'applique en outre qu'aux profils que vous contrôlez, donc elle n'aide pas une app grand public qui surveille le tunnel de quelqu'un d'autre.
Vérification 6 : rendez la panne sûre et observable
Ne coupez que la capacité qui a besoin du tunnel et laissez le reste de l'app utilisable. Un détecteur qui masque toute l'interface dès qu'il n'est pas sûr fabrique une panne pour quiconque dont le tunnel utilise un nom que vous n'avez jamais anticipé. Revenez plutôt au comportement conservateur : les fonctionnalités locales continuent de marcher et seul l'appel distant qui a vraiment besoin du tunnel est désactivé. Ce choix est une décision produit, et il mérite d'être écrit à côté de l'heuristique.
Journalisez ce que vous avez observé : noms d'interface, version de la plateforme et résultat d'une sonde. Cela transforme le prochain rapport d'une dispute en vérification. Préférez ensuite une déclaration prise en charge quand vous en avez une, car une configuration gérée peut porter l'intention explicitement et retirer l'essentiel de la raison d'inspecter des noms.
Si les vérifications 1 à 5 vous ont laissé sans réponse prise en charge, livrez la sonde et étiquetez le résultat honnêtement. Un signal documenté de meilleur effort est une meilleure décision produit qu'une API privée qui casse à la prochaine version.